1. ArrayList
情况一:不指定容量,默认大小是十个数组的大小。在创建的时候不会分配十个数组,在第一次add 元素的时候才会进行扩容至默认的十个大小。
接下来容量超出的时候扩容机制如下:
比如放第11个的时候:
add 源码如下: java.util.ArrayList#add(E)
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
1》 先扩容
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
(1)ensureCapacityInternal 确保容量满足当前size + 1, 也就是确保能放下下一个元素
第一步先调用calculateCapacity获取大小,如果数组中没元素是个空数组就拿默认的大小10返回去; 否则返回上面的size + 1
第二步调用 ensureExplicitCapacity 如果minCapacity大于当前数组的大小,则需要扩容。
第三步 调用grow 方法进行扩容:(扩容为原来大小的1.5倍)
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
第四步扩容的时候调用到 java.util.Arrays#copyOf(U[], int, java.lang.Class<? extends T[]>)
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
java.lang.System#arraycopy 是一个native 方法
2》 后放置元素
情况二: 创建的时候指定大小, 默认会开辟指定大小的数组, 扩容机制同上
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
Vector 和 ArrayList 一样是基于数组,只不过Vectror 的add 方法和 remove 方法加了synchronized 修饰, 变成了同步方法; 而且扩容默认是成倍的扩容。
2. LinkedList 源码
我们知道LinkedList 是基于链表的结构(双向链表),链表的结构查找元素的时间复杂度为O(n), ArrayList 是基于数组,查找元素的时间复杂度为O(1), 这里说的查找都是根据下标进行查找。
关于增删元素其空间复杂度,ArrayList 从尾部插入的时候空间复杂度是O(1)--这也是很多框架查出来对象之后直接映射成ArrayList 的原因直接, 中间插入和删除元素时空间复杂度是O(n),需要进行元素的移动;LinkedList 的空间复杂度是O(1)。
其继承关系如下,可以作为一个队列来使用
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
其内部类Node 用来存放元素,其结构如下:
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
查看其add 方法:java.util.LinkedList#add(E)
/** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last; public boolean add(E e) { linkLast(e); return true; } /** * Links e as last element. */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
可以看到插入元素就是添加了一个节点,并且作为last 元素存到成员属性中。
java.util.LinkedList#get 根据下标获取元素的源码如下:
public E get(int index) { checkElementIndex(index); return node(index).item; } private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } /** * Returns the (non-null) Node at the specified element index. */ Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
可以看到先检查需要获取的元素是否存在越界;然后这里大概是用了个二分查找的思想,分成两半从前从后找。这里也是需要遍历一半的元素进行链表的遍历获取元素。
补充:ArrayList 和 LinkedList 的区别
1. 底层数据结构不同,ArrayList 是基于数组,LinkedList 是基于链表(双向)实现的。
2. 适用场景不同,Arraylist适合随机查找,LinkedList 适合删除和添加。查询、添加、删除的时间复杂度不同。
链表的结构查找元素的时间复杂度为O(n), ArrayList 是基于数组,查找元素的时间复杂度为O(1), 这里说的查找都是根据下标进行查找。
关于增删元素其空间复杂度,ArrayList 从尾部插入的时候空间复杂度是O(1)--这也是很多框架查出来对象之后直接映射成ArrayList 的原因直接, 中间插入和删除元素时空间复杂度是O(n),需要进行元素的移动;LinkedList 的空间复杂度是O(1)。
3. Arraylist 和linkedList 都实现了List 接口,但是LinkedList 还实现了Deque 接口,所以LinkedList 还可以当作队列来使用。
Queue的方法也非常简单,就是三组(一个会抛出异常,一个返回特殊值):
补充:栈和队列的基本使用
Java 也有栈结构,比如:java.util.Stack
package java.util; /** * The <code>Stack</code> class represents a last-in-first-out * (LIFO) stack of objects. It extends class <tt>Vector</tt> with five * operations that allow a vector to be treated as a stack. The usual * <tt>push</tt> and <tt>pop</tt> operations are provided, as well as a * method to <tt>peek</tt> at the top item on the stack, a method to test * for whether the stack is <tt>empty</tt>, and a method to <tt>search</tt> * the stack for an item and discover how far it is from the top. * <p> * When a stack is first created, it contains no items. * * <p>A more complete and consistent set of LIFO stack operations is * provided by the {@link Deque} interface and its implementations, which * should be used in preference to this class. For example: * <pre> {@code * Deque<Integer> stack = new ArrayDeque<Integer>();}</pre> * * @author Jonathan Payne * @since JDK1.0 */ public class Stack<E> extends Vector<E> { /** * Creates an empty Stack. */ public Stack() { } /** * Pushes an item onto the top of this stack. This has exactly * the same effect as: * <blockquote><pre> * addElement(item)</pre></blockquote> * * @param item the item to be pushed onto this stack. * @return the <code>item</code> argument. * @see java.util.Vector#addElement */ public E push(E item) { addElement(item); return item; } /** * Removes the object at the top of this stack and returns that * object as the value of this function. * * @return The object at the top of this stack (the last item * of the <tt>Vector</tt> object). * @throws EmptyStackException if this stack is empty. */ public synchronized E pop() { E obj; int len = size(); obj = peek(); removeElementAt(len - 1); return obj; } /** * Looks at the object at the top of this stack without removing it * from the stack. * * @return the object at the top of this stack (the last item * of the <tt>Vector</tt> object). * @throws EmptyStackException if this stack is empty. */ public synchronized E peek() { int len = size(); if (len == 0) throw new EmptyStackException(); return elementAt(len - 1); } /** * Tests if this stack is empty. * * @return <code>true</code> if and only if this stack contains * no items; <code>false</code> otherwise. */ public boolean empty() { return size() == 0; } /** * Returns the 1-based position where an object is on this stack. * If the object <tt>o</tt> occurs as an item in this stack, this * method returns the distance from the top of the stack of the * occurrence nearest the top of the stack; the topmost item on the * stack is considered to be at distance <tt>1</tt>. The <tt>equals</tt> * method is used to compare <tt>o</tt> to the * items in this stack. * * @param o the desired object. * @return the 1-based position from the top of the stack where * the object is located; the return value <code>-1</code> * indicates that the object is not on the stack. */ public synchronized int search(Object o) { int i = lastIndexOf(o); if (i >= 0) { return size() - i; } return -1; } /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = 1224463164541339165L; }
测试:
package com.xm.ggn.test; import java.util.LinkedList; import java.util.Stack; public class PlainTest { public static void main(String[] args) { /** * 栈和队列的简单使用(stack 内部继承自Vector, push 是调用add 向尾部追加元素; pop是调用peek 拿出最后一个元素,然后调用remove 删除最后一个元素) */ // 1. 栈的使用 Stack<String> strings = new Stack<>(); strings.push("1"); strings.push("2"); strings.push("3"); System.out.println(strings); // peek 方法不弹出,只拿栈顶的元素 System.out.println(strings.peek()); System.out.println(strings.peek()); // pop 方法栈顶的元素, 相当于peek + remove 方法 System.out.println(strings.pop()); System.out.println(strings.pop()); System.out.println(strings.pop()); System.out.println(strings.isEmpty()); System.out.println("=========="); // 队列的使用: /** * 对应的三组API:左边会抛出异常,右边不会抛出异常 * add offer 插入(入队列) * remove poll 移除且返回头元素 * element peek 检查但是不返回头元素 */ LinkedList<String> queue = new LinkedList<>(); // offer内部调用add 方法, 实际是向数组尾部添加元素,类似于向queue 对尾部添加元素 queue.offer("1"); queue.offer("2"); queue.offer("3"); queue.add("4"); queue.addLast("5"); queue.addFirst("0"); System.out.println(queue); // peek 是拿队列头部的元素,不会移除元素 System.out.println(queue.peek()); // 0 // poll是弹出队列头部的元素并且删除去掉和后面元素的引用关系 System.out.println(queue.poll()); // 0 System.out.println(queue.pollFirst()); // 1 System.out.println(queue.pollLast()); // 5 System.out.println(queue.poll()); // 2 System.out.println(queue.poll()); // 3 System.out.println(queue.poll()); // 4 } }
3. Map
关于map 的主要有HashMap, HashTable, LinkedHashMap, TreeMap。
关于HashMap 和 Hashtable 的区别:
1. hashTable的初始容量为11, 负载因子:0.75; hashMap的初始容量为16,负载因子为:0.75
2. hashTable扩容的方式: old容量*2 +1; hashMap扩容的方式:2的次幂的最靠近的那个数,或者原容量的两倍
3. hashTable线程安全(put、get、size 等方法都加了synchronized),hashMap线程不安全
4. hashTable中key value的值都不允许为空;hashMap的key或者value都允许为空
补充下java.util.Collections#synchronizedMap 是返回了一个自己的内部同步Map, 这个Map 也是基于synchronized 进行同步, 在读方法或者size()方法加锁是为了保证内存的可见性,保证多线程读取到的数据是一致的。
这里说下HashMap的简单原理。
JDK1.7 是基于数组加链表, 且hash 碰撞之后会采用头插法解决冲突。
JDK1.8 之后引入了红黑树的概念。 在链表的长度达到8之后会变为红黑树,链表长度变为6之后又变为链表。 原因是转为红黑树是为了解决遍历查询慢的问题。8和6数字的出现是因为: 6之前,遍历链表的平均时间复杂度要小于红黑树,所以采用链表; 8之后红黑树的时间复杂度为O(logn), 要小于链表的时间复杂度O(n)。 插入的话,链表快于红黑树。所以引入红黑树重点是为了解决查询慢的问题。
关于扩容: JDK7 是先扩容,后头插法插入元素; JDK8 是先放元素,后尾插法插入元素。
红黑树是一种平衡二叉查找树的变体,它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树(AVL),但 对之进行平衡的代价较低, 其平均统计性能要强于 AVL
红黑树:每个结点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 所有叶子都是黑色。(叶子是NIL结点)
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点,黑节点的父亲必红)
性质5. 从任一节结点其每个叶子的所有路径都包含相同数目的黑色结点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
1. HashMap 中几个重要的属性:
/** * The default initial capacity - MUST be a power of two. (默认容量大小,默认16, 扩容时成倍扩容) */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 最大容量 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,即使链表长度达到8,也不会进行树形化,首先进行扩容操作) // 为了避免进行扩容和树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD static final int MIN_TREEIFY_CAPACITY = 64; // 存放具体的元素的数组 transient Node<K,V>[] table; // 存储元素的数组,总是2的幂次倍 transient Node<k,v>[] table; // 存放具体元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的个数(不是数组的长度) transient int size; // 扩容和修改的计数变量 transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold; // 加载因子 final float loadFactor;
threshold 临界值: 其计算方式是加载因子乘以容量,加载因子默认是0.75, 可以在构造方法中指定。 当放完元素之后,判断当前map的size,如果大于该临界值会进行扩容。
loadFactor加载因子:默认是0.75;加载因子就是表示哈希表中元素填满的程度,当表中元素过多,超过加载因子的值时,哈希表会自动扩容,一般是一倍,这种行为可以称作rehashing(再哈希)。
加载因子的值设置的越大,添加的元素就会越多,确实空间利用率的到了很大的提升,但是毫无疑问,就面临着哈希冲突的可能性增大,反之,空间利用率造成了浪费,但哈希冲突也减少了,所以我们希望在空间利用率与哈希冲突之间找到一种我们所能接受的平衡,经过一些试验,定在了0.75f。
2. 两个重要的节点:
Node 单链表结构: hash 是保存上面计算出的hash 值, 用来查找位置以及比对元素是否相同
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
treeNode 节点: 红黑树节点, 新创建的树节点默认是红色。 继承LinkedHashMap.Entry,Entry<K,V> extends HashMap.Node<K,V> 间接继承自上面的Node 链表节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } /** * Returns root of tree containing this node. */ final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } }
3. put 方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hash 方法中的代码可以被称为扰动函数,如果key 是null 直接返回0; 否则计算得到其hashCode值并赋值给一个临时变量h, 然后向右移动16位(int 是32 位,相当于右移16位然后高位补0)之后进行按位进行异或运算(异或运算是相同为0,不同为1)。相当于是将hashcode值的高16位与低16位进行按位异或运算(相同为0,相异为1)。
java.util.HashMap#putVal 如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table未初始化(为null)或者长度为0,调用 resize 进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 若桶为空,即无发生碰撞 // (n - 1) & hash 用来确定元素存放在哪个位置,即哪个桶中 if ((p = tab[i = (n - 1) & hash]) == null) // 新生成结点放入桶中(数组中) tab[i] = newNode(hash, key, value, null); // 若桶中已经存在元素 else { Node<K,V> e; K k; // 若节点 key 存在,就和要插入的key比较 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 如果key相同就直接覆盖 value e = p; // hash值不相等,即key不相等,转为红黑树结点 else if (p instanceof TreeNode) // 插入到树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 若是为链表结点 else { // 遍历找到尾节点插入 for (int binCount = 0; ; ++binCount) { // 到达链表的尾部 if ((e = p.next) == null) { // 在尾部插入新结点 p.next = newNode(hash, key, value, null); // 结点数量达到阈值,转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 跳出循环 break; } // 遍历的过程中,遇到相同 key 则覆盖 value if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循环 break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } // 在桶中找到key值、hash值与插入元素相等的结点 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent 为 false 或者旧值为 null if (!onlyIfAbsent || oldValue == null) // 用新值替换旧值 e.value = value; // 访问后回调 afterNodeAccess(e); // 返回旧值 return oldValue; } } // 结构性修改 ++modCount; // 超过最大容量,扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null; }
为了使得元素的下标落在数组的大小范围内,需要根据数组的大小进行运算,JDK7 是 hash % (length - 1); 在这里的计算应该存放的数组下标的方法是: hash值 & (tab.length - 1), 进行与运算之后相当于确保元素坐落在数组的范围内。
这也是HashMap 的大小要求是2的N次幂的好处之一, 比如16是 10000, 减一之后是 1111。 这也可以确保数组的散列性,碰撞概率就低了。
这里也有个注意点: key的hashCode 用于计算数组的下标,key 的equals 方法用于hash 冲突之后判断是否需要覆盖元素。
Put大致流程如下:
先定位到具体的数组位置,例如叫做 A
若A 处没有元素就直接插入
若A处有元素就和待插入的 key 比较
若key相同就直接覆盖
若key不相同,就判断 p 是否是一个树节点
如果是就调用putTreeVal 方法将元素添加进入
如果不是就遍历链表插入(尾插法)
4. get 方法
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
hash(key) 方法同上,用于得到一个hash 值。java.util.HashMap#getNode方法如下: 这里就是根据hash 和 key 去获取节点Node, 分为直接从数组拿, 从树拿和从链表拿。
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
补充:Hash冲突的解决办法
(1)开放定址法:以发生冲突的哈希地址为自变量,通过哈希冲突函数得到一个新的空闲的哈希地址的方法。
线性探查:从冲突开始,找下一个地址,直到找到为空的地址
平方探查法:采用平方算法,假设do地址冲突,则找下一个地址为 d0 + (1^2)、d0 - (1^2)、d0 + (2^2)、d0 - (2^2)
伪随机序列和双哈希函数
(2)链地址法:再哈希方法。
(3)再哈希法:计算另一个哈希函数,直到不冲突为止
(4)公共溢出区域法:建立一个公共溢出区域,就是把冲突的都放在另一个地方,不在表里面。
HashMap 分析参考: https://juejin.cn/post/6931521369209470989#heading-19