String深入解析
String具有不变性的原因:
- String被final修饰,它不可能被继承,也就是任何对String的操作方法,都不会被继承覆写
- String中保存数据的是一个char数组的value,它被final修饰,它的内存地址一旦赋值无法修改
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; }
String相等判断源码
public boolean equals(Object anObject) { // 判断内存地址是否相同 if (this == anObject) { return true; } // 待比较的对象是否是 String,如果不是 String,直接返回不相等 if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; // 两个字符串的长度是否相等,不等则直接返回不相等 if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; // 依次比较每个字符是否相等,若有一个不等,直接返回不相等 while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
- 判断类型是否相同
- 判断长度是否相同
- 比较每一个字符是否相等
当上述3个条件均成立时,两个String相等
String的replace和replaceAll区别在于前者入参可以是char也可以是String,此外replaceAll是支持正则表达式的,效率上replace高效
Long
Long自己实现了一种缓存机制,缓存了-128到127内的所有Long,如果值在Long,则从缓存中取,不会被初始化
Long中的静态内部类实现了LongCache
private static class LongCache { private LongCache(){} // 缓存,范围从 -128 到 127,+1 是因为有个 0 static final Long cache[] = new Long[-(-128) + 127 + 1]; // 容器初始化时,进行加载 static { // 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128 for(int i = 0; i < cache.length; i++) cache[i] = new Long(i - 128); } }
使用Long时,推荐valueOf,少用parseLong方法,因为前者如果缓存命中从缓存取值,而后者不会
static
static修饰的方法时,要注意线程安全,static修饰的工具类方法要避免使用到方法外部的公共变量。工具类方法建议使用public final static
Arrays
Arrays二分查询方法能快速定位元素
其二分查找底层如下:
// a:我们要搜索的数组,fromIndex:从那里开始搜索,默认是0; toIndex:搜索到何时停止,默认是数组大小 // key:我们需要搜索的值 c:外部比较器 private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex, T key, Comparator<? super T> c) { // 如果比较器 c 是空的,直接使用 key 的 Comparable.compareTo 方法进行排序 // 假设 key 类型是 String 类型,String 默认实现了 Comparable 接口,就可以直接使用 compareTo 方法进行排序 if (c == null) { // 这是另外一个方法,使用内部排序器进行比较的方法 return binarySearch0(a, fromIndex, toIndex, key); } int low = fromIndex; int high = toIndex - 1; // 开始位置小于结束位置,就会一直循环搜索 while (low <= high) { // 假设 low =0,high =10,那么 mid 就是 5,所以说二分的意思主要在这里,每次都是计算索引的中间值 int mid = (low + high) >>> 1; T midVal = a[mid]; // 比较数组中间值和给定的值的大小关系 int cmp = c.compare(midVal, key); // 如果数组中间值小于给定的值,说明我们要找的值在中间值的右边 if (cmp < 0) low = mid + 1; // 我们要找的值在中间值的左边 else if (cmp > 0) high = mid - 1; else // 找到了 return mid; // key found } // 返回的值是负数,表示没有找到 return -(low + 1); // key not found. }
Arrays提供了很多parallel开头的方法,比如parallelSort方法,方法底层有判断,只有数据量大于8192时,才会真正走并行的实现。
Collections
Colletcions.max
ArrayList
ArrayList无参构造器初始化时,默认大小是空数组,当第1次add的时候数组扩容到大小为10。如果第一次初始化如果大小<10,那么初始化大小按10计算
扩容后的大小是原来的1.5倍
扩容源码如下
private void ensureCapacityInternal(int minCapacity) { //如果初始化数组大小时,有给定初始值,以给定的大小为准,不走 if 逻辑 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //确保容积足够 ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { //记录数组被修改 modCount++; // 如果我们期望的最小容量大于目前数组的长度,那么就扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); } //扩容,并把现有数据拷贝到新的数组里面去 private void grow(int minCapacity) { int oldCapacity = elementData.length; // oldCapacity >> 1 是把 oldCapacity 除以 2 的意思 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果扩容后的值 > jvm 所能分配的数组的最大值,那么就用 Integer 的最大值 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 通过复制进行扩容 elementData = Arrays.copyOf(elementData, newCapacity); }
ArrayList无参构造器构造,现在add一个值进去,此处的数组大小是1,下一次扩容前最大可用大小是10。
数组初始化,初加入一个值后,如果使用addAll方法,一下子加入15个值,那么最终数组的大小是16。因为
// newCapacity 本次扩容的大小,minCapacity 我们期望的数组最小大小 // 如果扩容后的值 < 我们的期望值,我们的期望值就等于本次扩容的大小 if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
toArray方法
public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); Integer[] a1 = new Integer[3]; Integer[] a2 = list.toArray(a1); System.out.println(a1.hashCode()); System.out.println(a2.hashCode()); System.out.println("================================"); Integer[] a3 = new Integer[2]; Integer[] a4 = list.toArray(a3); System.out.println(a3.hashCode()); System.out.println(a4.hashCode()); }
可以发现toArray方法,如果入参数组大小小于集合大小,那么它就会返回一个新的数组,否则返回则就是入参数组
LinkedList
从头节点增加
// 从头部追加 private void linkFirst(E e) { // 头节点赋值给临时变量 final Node<E> f = first; // 新建节点,前一个节点指向null,e 是新建节点,f 是新建节点的下一个节点,目前值是头节点的值 final Node<E> newNode = new Node<>(null, e, f); // 新建节点成为头节点 first = newNode; // 头节点为空,就是链表为空,头尾节点是一个节点 if (f == null) last = newNode; //上一个头节点的前一个节点指向当前节点 else f.prev = newNode; size++; modCount++; }
从头节点删除
//从头删除节点 f 是链表头节点 private E unlinkFirst(Node<E> f) { // 拿出头节点的值,作为方法的返回值 final E element = f.item; // 拿出头节点的下一个节点 final Node<E> next = f.next; //帮助 GC 回收头节点 f.item = null; f.next = null; // 头节点的下一个节点成为头节点 first = next; //如果 next 为空,表明链表为空 if (next == null) last = null; //链表不为空,头节点的前一个节点指向 null else next.prev = null; //修改链表大小和版本 size--; modCount++; return element; }
节点查询,如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。否则后半部分查询
// 根据链表索引位置查询节点 Node<E> node(int index) { // 如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。 if (index < (size >> 1)) { Node<E> x = first; // 直到 for 循环到 index 的前一个 node 停止 for (int i = 0; i < index; i++) x = x.next; return x; } else {// 如果 index 处于队列的后半部分,从尾开始找 Node<E> x = last; // 直到 for 循环到 index 的后一个 node 停止 for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
LinkedList实现了Queue接口,在链表为空时返回值也不太一样
因为LinkedList实现双向的迭代方向,新增了一个迭代接口:ListIterator
ArrayList有最大容量,为Integer的最大值,LinkedList底层是双向链表,理论可无限大,但源码中LinkedList实际大小用int类型,这也说明了LinkedList不能超过Integer的最大值。
HashMap
底层数据结构是:数组+链表+红黑树。其中当链表的长度大于等于8时,链表会转化成红黑树,当红黑树大小小于等于6时,红黑树转化成链表。
HashMap允许null值。
//初始容量为 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //负载因子默认值 static final float DEFAULT_LOAD_FACTOR = 0.75f; //桶上的链表长度大于等于8时,链表转化成红黑树 static final int TREEIFY_THRESHOLD = 8; //桶上的红黑树大小小于等于6时,红黑树转化成链表 static final int UNTREEIFY_THRESHOLD = 6; //当数组容量大于 64 时,链表才会转化成红黑树 static final int MIN_TREEIFY_CAPACITY = 64; //记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast transient int modCount; //HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化) transient int size; //存放数据的数组 transient Node<K,V>[] table; // 扩容的门槛,有两种情况 // 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。 // 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75 int threshold; //链表的节点 static class Node<K,V> implements Map.Entry<K,V> { //红黑树的节点 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } key 在数组中的位置公式:tab[(n - 1) & hash]
计算hash值时,为什么需要右移16位
hash算法是h^(h>>>16),这是为了h的高低16位都能参与计算,计算出的hash更分散
为什么把数组长度取模操作换成了&操作
取模操作处理器计算慢,处理器对&操作比较擅长
为什么提倡数组大小是2的幂次方
因为只有大小是2的幂次方,才能使hash值%n(数组大小)==(n-1)&hash
hashmap的添加元素
// 入参 hash:通过 hash 算法计算出来的值。 // 入参 onlyIfAbsent:false 表示即使 key 已经存在了,仍然会用新值覆盖原来的值,默认为 false final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // n 表示数组的长度,i 为数组索引下标,p 为 i 下标位置的 Node 值 Node<K,V>[] tab; Node<K,V> p; int n, i; //如果数组为空,使用 resize 方法初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 如果当前索引位置是空的,直接生成新的节点在当前索引位置上 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 如果当前索引位置有值的处理方法,即我们常说的如何解决 hash 冲突 else { // e 当前节点的临时变量 Node<K,V> e; K k; // 如果 key 的 hash 和值都相等,直接把当前下标位置的 Node 值赋值给临时变量 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果是红黑树,使用红黑树的方式新增 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 是个链表,把新节点放到链表的尾端 else { // 自旋 for (int binCount = 0; ; ++binCount) { // e = p.next 表示从头开始,遍历链表 // p.next == null 表明 p 是链表的尾节点 if ((e = p.next) == null) { // 把新节点放到链表的尾部 p.next = newNode(hash, key, value, null); // 当链表的长度大于等于 8 时,链表转红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } // 链表遍历过程中,发现有元素和新增的元素相等,结束循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //更改循环的当前元素,使 p 在遍历过程中,一直往后移动。 p = e; } } // 说明新节点的新增位置已经找到了 if (e != null) { V oldValue = e.value; // 当 onlyIfAbsent 为 false 时,才会覆盖值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 返回老值 return oldValue; } } // 记录 HashMap 的数据结构发生了变化 ++modCount; //如果 HashMap 的实际大小大于扩容的门槛,开始扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashMap、TreeMap、LinkedHashMap三者异同
相同点:
- 在特定情况下,都使用红黑树
- 底层的hash算法相同
- 在迭代过程中,如果Map的数据结果被改动,都会报ConcurrentModificationException
不同点: - HashMap以数组为主,查询快,TreeMap以红黑树为主,利用红黑树特点进行排序,LinkedHashMap在HashMap上增加链表结构,实现顺序插入和最少访问删除
- 三者使用场景不同
- api上层略有不同
HashSet
它底层基于HashMap实现,key为HashMap的key,value为private static final Object PRESENT = new Object();
// 对 HashMap 的容量进行了计算 public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); }
HashSet的初始大小为取最大值(期望的值 / 0.75 + 1,默认值 16),expectSize * 0.75 + 1刚好不用扩容,如果对HashMap的初始化大小值选取,可借鉴此公式。
CopyOnWriteArrayList
CopyOnWriteArrayList通过锁+数组拷贝+volatile保证线程安全,它线程安全的原因是每次操作都是在新拷贝数组上操作,当操作完成后,再把新数组引用赋值给原数组。当用迭代器操作时,如果有另一线程对CopyOnWriteArrayList进行增删操作,迭代器未结束时持有的仍是旧数组,所以它是线程安全的。
注意:volatile直接修饰数组时,当数组某一元素改变仍无法保证其可见性,只有当数组引用发生改变时才保证其可见性,这就是为什么源码setArray方法是修改数组的引用
CopyOnWriteArrayList的add方法
// 添加元素到数组尾部 public boolean add(E e) { final ReentrantLock lock = this.lock; // 加锁 lock.lock(); try { // 得到所有的原数组 Object[] elements = getArray(); int len = elements.length; // 拷贝到新数组里面,新数组的长度是 + 1 的,因为新增会多一个元素 Object[] newElements = Arrays.copyOf(elements, len + 1); // 在新数组中进行赋值,新元素直接放在数组的尾部 newElements[len] = e; // 替换掉原来的数组 setArray(newElements); return true; // finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁 } finally { lock.unlock(); } }
批量删除
// 批量删除包含在 c 中的元素 public boolean removeAll(Collection<?> c) { if (c == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; // 说明数组有值,数组无值直接返回 false if (len != 0) { // newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素 int newlen = 0; Object[] temp = new Object[len]; // 循环,把不包含在 c 里面的元素,放到新数组中 for (int i = 0; i < len; ++i) { Object element = elements[i]; // 不包含在 c 中的元素,从 0 开始放到新数组中 if (!c.contains(element)) temp[newlen++] = element; } // 拷贝新数组,变相的删除了不包含在 c 中的元素 if (newlen != len) { setArray(Arrays.copyOf(temp, newlen)); return true; } } return false; } finally { lock.unlock(); } }
源码思想借鉴:批量删除是把先找出已存在的元素组成数组,然后再把原数组引用指向新数组。避免直接循环单个元素删除造成频繁数组拷贝
ConcurrentHashMap
添加元素
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); //计算hash int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //table是空的,进行初始化 if (tab == null || (n = tab.length) == 0) tab = initTable(); //如果当前索引位置没有值,直接创建 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //cas 在 i 位置创建新的元素,当 i 位置是空时,即能创建成功,结束for自循,否则继续自旋 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //如果当前槽点是转移节点,表示该槽点正在扩容,就会一直等待扩容完成 //转移节点的 hash 值是固定的,都是 MOVED else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); //槽点上有值的 else { V oldVal = null; //锁定当前槽点,其余线程不能操作,保证了安全 synchronized (f) { //这里再次判断 i 索引位置的数据没有被修改 //binCount 被赋值的话,说明走到了修改表的过程里面 if (tabAt(tab, i) == f) { //链表 if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; //值有的话,直接返回 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; //把新增的元素赋值到链表的最后,退出自旋 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } //红黑树,这里没有使用 TreeNode,使用的是 TreeBin,TreeNode 只是红黑树的一个节点 //TreeBin 持有红黑树的引用,并且会对其加锁,保证其操作的线程安全 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; //满足if的话,把老的值给oldVal //在putTreeVal方法里面,在给红黑树重新着色旋转的时候 //会锁住红黑树的根节点 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } //binCount不为空,并且 oldVal 有值的情况,说明已经新增成功了 if (binCount != 0) { // 链表是否需要转化成红黑树 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; //这一步几乎走不到。槽点已经上锁,只有在红黑树或者链表新增失败的时候 //才会走到这里,这两者新增都是自旋的,几乎不会失败 break; } } } //check 容器是否需要扩容,如果需要去扩容,调用 transfer 方法去扩容 //如果已经在扩容中了,check有无完成 addCount(1L, binCount); return null; }
LinkedBlockingQueue
put方法
// 把e新增到队列的尾部。 // 如果有可以新增的空间的话,直接新增成功,否则当前线程陷入等待 public void put(E e) throws InterruptedException { // e 为空,抛出异常 if (e == null) throw new NullPointerException(); // 预先设置 c 为 -1,约定负数为新增失败 int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; // 设置可中断锁 putLock.lockInterruptibly(); try { // 队列满了 // 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后就会唤醒此处被阻塞的线程) while (count.get() == capacity) { // await 无限等待 notFull.await(); } // 队列没有满,直接新增到队列的尾部 enqueue(node); // 新增计数赋值,注意这里 getAndIncrement 返回的是旧值 // 这里的 c 是比真实的 count 小 1 的 c = count.getAndIncrement(); // 如果链表现在的大小 小于链表的容量,说明队列未满 // 可以尝试唤醒一个 put 的等待线程 if (c + 1 < capacity) notFull.signal(); } finally { // 释放锁 putLock.unlock(); } // c==0,代表队列里面有一个元素 // 会尝试唤醒一个take的等待线程 if (c == 0) signalNotEmpty(); } // 入队,把新元素放到队尾 private void enqueue(Node<E> node) { last = last.next = node; }