大纲
1.为什么要对JDK源码剖析
2.ArrayList源码一:基本原理以及优缺点
3.ArrayList源码二:核心方法的原理
4.ArrayList源码三:数组扩容以及元素拷贝
5.LinkedList源码一:优缺点和使用场景
6.LinkedList源码二:双向链表数据结构
7.LinkedList源码三:插入元素的原理
8.LinkedList源码四:获取元素的原理
9.LinkedList源码五:删除元素的原理
10.Vector和Stack:栈的数据结构和源码
11.HashMap源码一:数组 + 链表 + 红黑树
12.HashMap源码二:核心成员变量的作用
13.HashMap源码三:降低哈希冲突概率的算法
14.HashMap源码四:put操作及哈希寻址算法
15.HashMap源码五:哈希冲突时的链表处理
16.HashMap源码六:引入红黑树优化哈希冲突
17.HashMap源码七:哈希冲突时插入红黑树
18.HashMap源码八:JDK 1.7的数组扩容原理
19.HashMap源码九:JDK 1.8的数组扩容原理
20.HashMap源码十:get与remove操作源码
21.LinkedHashMap有顺序的Map数据结构
22.TreeMap可自定义排序规则的红黑树Map
23.HashSet + LinkedHashSet + TreeSet简介
24.迭代器应对多线程并发修改的Fail-Fast机制
1.为什么要对JDK源码剖析
简单的框架源码有:Spring Cloud,ByteTcc分布式事务,Redisson以及Curator等框架源码。
较难的框架源码有:ZooKeeper,Dubbo、Netty、RocketMQ、Sharding-JDBC等框架源码。
看这些源码的时候,都有一些基础性的代码逻辑,比如:内存里的各种数据结构(List、Map、Set)、并发、网络请求、磁盘IO等。
其实对于各种各样的框架系统的底层,最核心的部分,基本都是:
一.集合(在内存里面存放数据)
二.并发(系统底层都会开多个线程进行并发处理,会涉及锁、同步等)
三.IO(读写磁盘上的文件数据、或者发起网络IO通过网络读写数据)
四.网络(在分布式系统中给各个机器建立网络连接,互相发送请求通信)
2.ArrayList源码一:基本原理以及优缺点
(1)数组长度固定需要扩容和拷贝
(2)添加元素导致大量移动元素
(3)基于数组非常适合随机读
(4)总结
(1)数组长度固定需要扩容和拷贝
由于Java里的数组都是定长数组,数组的长度是固定的。比如数组大小设置为100,此时不停的往ArrayList里面塞入数据,当元素数量超过了100以后,此时就会发生数组的扩容,就会申请更大的数组,把旧数组元素拷贝到新数组里去。
这个数组扩容 + 元素拷贝的过程,相对来说会慢一些。所以使用ArrayList时要注意,不要频繁往ArraList里面去塞数据,而导致频繁的数组扩容影响性能。
(2)添加元素导致大量移动元素
由于基于数组来实现,当往数组中添加元素,会导致移动元素。
(3)基于数组非常适合随机读
由于基于数组来实现,所以非常适合随机读。可以随机的去读数组中的某个元素,这个随机读的性能是比较高的,可以直接通过内存地址来定位某个元素。
(4)总结
如果不会频繁插入元素,导致频繁的移动元素位置、List扩容,而主要用于遍历集合或通过索引随机读取元素,那么可以用ArrayList。
如果会频繁插入元素到List中,那么尽量还是不要用ArrayList,因为很可能会造成大量的元素移动 + 数组扩容 + 元素拷贝。
3.ArrayList源码二:核心方法的原理
(1)构造方法的源码
(2)add()方法的源码
(3)set()方法的源码
(4)add(index, element)方法的源码
(5)get()方法的源码
(6)remove()方法的源码
(7)源码分析的总结
(1)构造方法的源码
如果不传参直接初始化一个ArrayList对象,则会执行默认的构造方法。默认的构造方法会将内部的数组设成一个默认的空数组。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData;...//Constructs an empty list with an initial capacity of ten.public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}...
}
ArrayList有一个默认的初始化数组大小的数值是10,可认为默认的数组初始化大小就只有10个元素,但这是往ArrayList添加元素时才触发的。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {//Default initial capacity.private static final int DEFAULT_CAPACITY = 10;private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData;...public boolean add(E e) {ensureCapacityInternal(size + 1);//Increments modCount!!elementData[size++] = e;return true;}private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}private void ensureExplicitCapacity(int minCapacity) {modCount++;//overflow-conscious codeif (minCapacity - elementData.length > 0) {grow(minCapacity);}}private void grow(int minCapacity) {//overflow-conscious codeint 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);}...
}
使用ArrayList的场景,应该不会有频繁的插入、移除元素的操作,基本可以知道它里面有多少元素,所以应该不会使用默认的构造方法。
一般给ArrayList构造时,会初始化一个合适的数组大小,避免数组太小插入塞入数据时导致频繁扩容。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData;...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);}}...
}
(2)add()方法的源码
每次往ArrayList添加元素时,都会判断一下当前数组元素是否满了。如果满了,此时就会对数组进行扩容。然后将旧数组中的元素拷贝到新数组中,确保数组能承受更多元素。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {//Default initial capacity.private static final int DEFAULT_CAPACITY = 10;private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData;...public boolean add(E e) {ensureCapacityInternal(size + 1);//Increments modCount!!elementData[size++] = e;return true;}private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}private void ensureExplicitCapacity(int minCapacity) {modCount++;//overflow-conscious codeif (minCapacity - elementData.length > 0) {grow(minCapacity);}}private void grow(int minCapacity) {//overflow-conscious codeint 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);}...
}
(3)set()方法的源码
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {transient Object[] elementData;...public E set(int index, E element) {rangeCheck(index);//检查是否数组越界E oldValue = elementData(index);elementData[index] = element;return oldValue;}private void rangeCheck(int index) {if (index >= size) {throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}}...
}
(4)add(index, element)方法的源码
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {transient Object[] elementData;...//往随机位置index插入元素public void add(int index, E element) {rangeCheckForAdd(index);//检查是否数组越界ensureCapacityInternal(size + 1);//System.arraycopy()方法会将elementData数组的index位置后的数据进行拷贝System.arraycopy(elementData, index, elementData, index + 1, size - index);elementData[index] = element;size++;}private void rangeCheck(int index) {if (index >= size) {throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}}...
}
(5)get()方法的源码
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {transient Object[] elementData;...public E get(int index) {rangeCheck(index);//检查是否数组越界return elementData[index];}private void rangeCheck(int index) {if (index >= size) {throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}}...
}
get()方法最简单,基于数组直接定位到元素,这是ArrayList性能最好的一个操作。
(6)remove()方法的源码
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {transient Object[] elementData;...public E remove(int index) {rangeCheck(index);modCount++;E oldValue = elementData(index);int numMoved = size - index - 1;if (numMoved > 0) {System.arraycopy(elementData, index + 1, elementData, index, numMoved);}elementData[--size] = null;return oldValue;}...
}
(7)源码分析的总结
一.remove()和add(index, element)
都会导致数组的拷贝(System.arraycopy()),因此性能都不是太高。所以基于ArrayList来进行随机位置的插入和删除,性能不会太高。
二.add()和add(index, element)
都可能会导致数组需要扩容(ensureCapacityInternal())。由于数组长度是固定的,默认初始大小是10。如果往数组里添加数据,可能会导致数组不停扩容,影响性能。
三.set()和get()
可以基于数组实现随机位置的直接定位,性能很高。
4.ArrayList源码三:数组扩容以及元素拷贝
(1)每次添加元素都判断是否扩容
(2)数组扩容一半时会进行数组拷贝
(1)每次添加元素都判断是否扩容
ArrayList里最关键的就是:如果数组满了,如何进行扩容,每当往ArrayList添加元素都会调用ensureCapacityInternal()方法。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {transient Object[] elementData;...public boolean add(E e) {ensureCapacityInternal(size + 1);//Increments modCount!!elementData[size++] = e;return true;}private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private void ensureExplicitCapacity(int minCapacity) {modCount++;if (minCapacity - elementData.length > 0) {grow(minCapacity);}}...
}
(2)数组扩容一半时会进行数组拷贝
假设ArrayList使用的是默认数组大小,也就是10。现已经往数组添加了10个元素,数组的size = 10,capacity = 10。此时再调用add()方法插入一个元素,也就是需要插入第11个元素,那么肯定是插入不进去的。此时执行的是ensureCapacityInternal(11)。
elementData已经填充了10个元素,minCapacity = 11。elementData.length是默认的值,也就是10。现在要放第11个元素,所以就会调用grow()方法对数组进行扩容。
根据"int newCapacity = oldCapacity + (oldCapacity >> 1);"以及"elementData = Arrays.copyOf(elementData, newCapacity);"可知:老的大小 + 老的大小 >> 1(相当于扩容一半),得出新数组大小。然后调用Arrays.copyOf()进行数组拷贝。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {transient Object[] elementData;...private void grow(int minCapacity) {//overflow-conscious codeint 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);}...
}
5.LinkedList源码一:优缺点和使用场景
(1)LinkedList的优缺点
(2)LinkedList的使用场景
(1)LinkedList的优缺点
LinkedList,底层是基于链表来实现的。LinkedList的优点就是非常适合频繁插入各种元素。LinkedList的缺点就是不太适合获取某个随机位置的元素。
比如LinkedList.get(10)这种操作,性能就较低。因为需要遍历链表,直到找到index = 10的这个元素为止。
而ArrayList.get(10),则不需要遍历,直接根据内存的地址,根据指定的index,直接定位到那个元素,不需要遍历数组。
ArrayList和LinkedList区别就是数组和链表的区别。
(2)LinkedList的使用场景
一.ArrayList的使用场景
一般会使用ArrayList来代表一个集合。只要别频繁插入大量元素即可,遍历或者随机查都可以。
二.LinkedList的使用场景
适合频繁在list中插入和删除元素,LinkedList可以当队列来使用。如果要在内存中实现一个内存队列,那么可以使用LinkedList。
6.LinkedList源码二:双向链表数据结构
(1)LinkedList的双向链表数据结构
(2)ArrayList和LinkedList的区别
(3)LinkedList的主要操作
(1)LinkedList的双向链表数据结构
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient int size = 0;transient Node<E> first;transient Node<E> last;protected transient int modCount = 0;...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;}}...
}
LinkedList的数据结构如下:
(2)ArrayList和LinkedList的区别
一般的回答:ArrayList是数组实现的,LinkedList是链表实现的。
深入的回答:结合ArrayList的源码,介绍add、remove、get、set方法的实现原理。介绍ArrayList数组扩容、元素移动的原理、以及优缺点是什么。LinkedList则是基于双向链表实现的,可以介绍一下它的数据结构。介绍LinkedList一些常见操作的原理、node是怎么变化的、以及优缺点,以及在哪个项目哪个业务场景下用过ArrayList和LinkedList。
(3)LinkedList的主要操作
在双向链表头部添加一个元素 / 获取一个元素 / 删除一个元素;
在双向链表尾部添加一个元素 / 获取一个元素 / 删除一个元素;
在双向链表中插入一个元素 / 获取一个元素 / 删除一个元素;
7.LinkedList源码三:插入元素的原理
(1)add()方法在尾部插入元素(尾插法)
(2)addFirst()方法在头部插入元素(头插法)
(3)add(index, e)方法在中间插入元素
(4)LinkedList的主要方法
(1)add()方法在尾部插入元素(尾插法)
add()方法会向双向链表尾部添加元素,add(2, e)会在index = 2的位置插入一个元素,都使用了尾插法。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient int size = 0;transient Node<E> first;transient Node<E> last;protected transient int modCount = 0;...public boolean add(E e) {linkLast(e);return true;}public void add(int index, E element) {checkPositionIndex(index);if (index == size) {linkLast(element);} else {linkBefore(element, node(index));}}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++;}void linkBefore(E e, Node<E> succ) {//assert succ != null;final Node<E> pred = succ.prev;final Node<E> newNode = new Node<>(pred, e, succ);succ.prev = newNode;if (pred == null) {first = newNode;} else {pred.next = newNode;}size++;modCount++;}...
}
(2)addFirst()方法在头部插入元素(头插法)
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient int size = 0;transient Node<E> first;transient Node<E> last;protected transient int modCount = 0;...public void addFirst(E e) {linkFirst(e);}private void linkFirst(E e) {final Node<E> f = first;final Node<E> newNode = new Node<>(null, e, f);first = newNode;if (f == null) {last = newNode;} else {f.prev = newNode;}size++;modCount++;}...
}
(3)add(index, e)方法在中间插入元素
add(index, e)方法在调用linkBefore()方法时会调用node()方法,这个node()方法就是用来返回位置为index的那个结点的。node()方法会根据index判断是在队列的前半部分还是后半部分,然后决定从头进行遍历还是从尾进行遍历。获取到位置为index的结点后,便可以通过linkBefore()方法插入元素。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient int size = 0;transient Node<E> first;transient Node<E> last;protected transient int modCount = 0;...public void add(int index, E element) {checkPositionIndex(index);if (index == size) {linkLast(element);} else {linkBefore(element, node(index));}}Node<E> node(int index) {//assert isElementIndex(index);if (index < (size >> 1)) {//如果index < size / 2,说明要找的结点是在队列的前半部分,从队头遍历Node<E> x = first;for (int i = 0; i < index; i++) {x = x.next;}return x;} else {//如果index >= size / 2,说明要找的结点是在队列的后半部分,从队尾开始遍历Node<E> x = last;for (int i = size - 1; i > index; i--) {x = x.prev;}return x;}}void linkBefore(E e, Node<E> succ) {//assert succ != null;final Node<E> pred = succ.prev;final Node<E> newNode = new Node<>(pred, e, succ);succ.prev = newNode;if (pred == null) {first = newNode;} else {pred.next = newNode;}size++;modCount++;}...
}
(4)LinkedList的主要方法
一.add()方法
在双向链表的尾部插入一个元素。
二.add(index, element)方法
在双向链表的中间插入一个元素。
三.addFirst()方法
在双向链表的头部插入一个元素。
四.addLast()方法
和add()方法一样也是在双向链表尾部插入一个元素。
五.offer()方法
在双向链表的尾部插入一个元素。
六.poll()方法
从双向链表头部获取一个元素并删除元素。
七.peek()方法
获取双向链表头部的元素,但是头部的元素不删除,所以LinkedList也可以作为一个队列来使用。
8.LinkedList源码四:获取元素的原理
(1)getFirst()方法和peek()方法
(2)getLast()方法
(3)get(int index)方法
(4)总结
(1)getFirst()方法和peek()方法
getFirst()方法获取头部的元素,直接返回first指针指向的那个Node元素。如果对空的LinkedList调用getFirst()方法,会抛出异常。
peek()方法也获取头部的元素,直接返回first指针指向的那个Node元素。如果对空的LinkedList调用peek()方法,会返回null。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient Node<E> first;...public E getFirst() {final Node<E> f = first;if (f == null) {throw new NoSuchElementException();}return f.item;}public E peek() {final Node<E> f = first;return (f == null) ? null : f.item;}...
}
(2)getLast()方法
getLast()方法会获取双向链表尾部的元素。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient Node<E> last;...public E getLast() {final Node<E> l = last;if (l == null) {throw new NoSuchElementException();}return l.item;}...
}
(3)get(int index)方法
get(int index)方法会随机获取双向链表在index位置上的元素。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient int size = 0;transient Node<E> first;transient Node<E> last;...public E get(int index) {checkElementIndex(index);return node(index).item;}Node<E> node(int index) {//assert isElementIndex(index);if (index < (size >> 1)) {//如果index < size / 2,说明要找的结点是在队列的前半部分,从队头遍历Node<E> x = first;for (int i = 0; i < index; i++) {x = x.next;}return x;} else {//如果index >= size / 2,说明要找的结点是在队列的后半部分,从队尾开始遍历Node<E> x = last;for (int i = size - 1; i > index; i--) {x = x.prev;}return x;}}...
}
(4)总结
对于ArrayList而言,如果要获取某个随机位置的元素,则其get(int index)方法会直接通过数组的index定位到该元素,性能超高。
对LinkedList而言,如果要获取某个随机位置的元素,则其get(int index)方法需调用node(index)这个方法,进行链表的遍历。也就是会比较index和size >> 1(链表元素一半)的大小,如果在前半部分就从头开始遍历,如果在后半部分就从尾开始遍历。
9.LinkedList源码五:删除元素的原理
(1)removeLast()方法
(2)removeFirst()方法和poll()方法
(3)remove(int index)方法
(4)总结
(1)removeLast()方法
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient int size = 0;transient Node<E> first;transient Node<E> last;...public E removeLast() {final Node<E> l = last;if (l == null) {throw new NoSuchElementException();}return unlinkLast(l);}private E unlinkLast(Node<E> l) {//assert l == last && l != null;final E element = l.item;final Node<E> prev = l.prev;l.item = null;l.prev = null;//help GClast = prev;if (prev == null) {first = null;} else {prev.next = null;}size--;modCount++;return element;}...
}
(2)removeFirst()方法和poll()方法
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient int size = 0;transient Node<E> first;transient Node<E> last;...public E removeFirst() {final Node<E> f = first;if (f == null) {throw new NoSuchElementException();}return unlinkFirst(f);}public E poll() {final Node<E> f = first;return (f == null) ? null : unlinkFirst(f);}private E unlinkFirst(Node<E> f) {//assert f == first && f != null;final E element = f.item;final Node<E> next = f.next;f.item = null;f.next = null;//help GCfirst = next;if (next == null) {last = null;} else {next.prev = null;}size--;modCount++;return element;}...
}
(3)remove(int index)方法
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {transient int size = 0;transient Node<E> first;transient Node<E> last;...public E remove(int index) {checkElementIndex(index);return unlink(node(index));}E unlink(Node<E> x) {//assert x != null;final E element = x.item;final Node<E> next = x.next;final Node<E> prev = x.prev;if (prev == null) {first = next;} else {prev.next = next;x.prev = null;}if (next == null) {last = prev;} else {next.prev = prev;x.next = null;}x.item = null;size--;modCount++;return element;}Node<E> node(int index) {//assert isElementIndex(index);if (index < (size >> 1)) {//如果index < size / 2,说明要找的结点是在队列的前半部分,从队头遍历Node<E> x = first;for (int i = 0; i < index; i++) {x = x.next;}return x;} else {//如果index >= size / 2,说明要找的结点是在队列的后半部分,从队尾开始遍历Node<E> x = last;for (int i = size - 1; i > index; i--) {x = x.prev;}return x;}}...
}
(4)总结
如果往LinkedList里插入大量数据(队头、队尾、队列中间),由于基于链表实现,不会出现大量的元素移动,也不会出现数组扩容。但在中间插入元素的性能没有在队头和队尾插入元素的性能好,因为需要遍历到指定的位置,然后才能完成元素的插入。
所以基于链表的LinkedList很适合做队列,缺点是随机位置获取一个元素会导致遍历。如果元素很多,遍历的性能可能比较差。
10.Vector和Stack:栈的数据结构和源码
(1)栈的数据结构
(2)栈的push()方法
(3)栈的pop()方法和peek()方法
(1)栈的数据结构
栈是由Vector和Stack这两个类来实现的。Stack代表了栈这种数据结构,它是继承自Vector的。
Vector是一种类似ArrayList的数据结构(基于数组实现),是有序的集合。
Stack是一种基于数组来实现的栈数据结构。栈是先进后出,队列是先进先出。栈的使用一般通过push()和pop(),将元素压入栈底和从栈顶弹出元素;
(2)栈的push()方法
栈的push()方法会将元素压入栈底。Stack.push()方法和ArrayList.add()方法的的实现源码几乎是一样的:就是放在数组按顺序排列的位置上。
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {protected Object[] elementData;protected int elementCount;protected int capacityIncrement;protected transient int modCount = 0;...public Vector() {this(10);}public Vector(int initialCapacity) {this(initialCapacity, 0);}public Vector(int initialCapacity, int capacityIncrement) {super();if (initialCapacity < 0) {throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);}this.elementData = new Object[initialCapacity];this.capacityIncrement = capacityIncrement;}...
}public class Stack<E> extends Vector<E> {public E push(E item) {addElement(item);return item;}public synchronized void addElement(E obj) {modCount++;ensureCapacityHelper(elementCount + 1);elementData[elementCount++] = obj;}private void ensureCapacityHelper(int minCapacity) {//overflow-conscious codeif (minCapacity - elementData.length > 0) {grow(minCapacity);}}private void grow(int minCapacity) {//overflow-conscious codeint oldCapacity = elementData.length;int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}if (newCapacity - MAX_ARRAY_SIZE > 0) {newCapacity = hugeCapacity(minCapacity);}elementData = Arrays.copyOf(elementData, newCapacity);}...
}
ArrayList默认每次扩容成原来的1.5倍,Vector默认每次扩容成原来的2倍。Vector将元素压入栈底时,elementData[elementCount++] = element。看这些源码可学习如何使用System.arraycopy()、Arrays.copyOf()方法。
(3)栈的pop()方法和peek()方法
Stack.pop()方法和Stack.peek()方法会从栈顶弹出一个元素。也就是最后一个压入栈的元素,会通过Stack.pop()方法从栈顶弹出。首先使用elementData[size - 1]获取最后一个元素,返回给用户。然后通过removeElementAt(size - 1)方法删除最后一个元素。
public class Stack<E> extends Vector<E> {...public synchronized E pop() {E obj;int len = size();obj = peek();removeElementAt(len - 1);return obj;}public synchronized E peek() {int len = size();if (len == 0) {throw new EmptyStackException();}return elementAt(len - 1);}public synchronized void removeElementAt(int index) {modCount++;if (index >= elementCount) {throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);} else if (index < 0) {throw new ArrayIndexOutOfBoundsException(index);}int j = elementCount - index - 1;if (j > 0) {System.arraycopy(elementData, index + 1, elementData, index, j);}elementCount--;elementData[elementCount] = null; /* to let gc do its work */}...
}
11.HashMap源码一:数组 + 链表 + 红黑树
(1)关于HashMap的问题
(2)HashMap的基本数据结构和原理
(3)红黑树的特点
(1)关于HashMap的问题
HashMap应该是整个JDK集合包源码剖析的重点,关于HashMap底层原理或者源码的问题如下。
一.基础问法
哈希冲突的时候怎么解决:用链表来处理。
二.初级问法
HashMap的原理:对key进行哈希运算,找到对应的位置进行存放。
三.中级问法
HashMap的扩容是怎么处理的,扩容的原理。
四.资深问法
JDK 1.8以后HashMap底层做了哪些优化,HashMap底层的源码。迭代集合时,Fail-Fast机制和ConcurrentModificationException。
(2)HashMap的基本数据结构和原理
一.HashMap设置元素和获取元素的流程
如果要对一个HashMap执行map.put(1, "张三"),map.put(2, "李四")。首先对一个key进行hashCode()运算,获取该key的哈希值。然后常规的做法是用这个哈希值对数组的长度进行取模。接着根据取模的结果,将key-value对放在数组中的某个元素上。
如果要对一个HashMap执行map.get(1),同理会先根据key获取哈希值。然后根据哈希值对数组长度取模,这样就知道key对应的value在哪里。
二.HashMap设置元素时出现哈希冲突的处理
如果某两个key对应的Hash值一样,该怎么处理?
在JDK 1.8以前,使用的是数组 + 链表来进行处理。如果出现大量哈希冲突,那么遍历长链表寻找key-value对时的复杂度是O(n)。
在JDK 1.8以后,使用的是数组 + 链表 + 红黑树来进行处理。如果链表长度超过8,会自动将链表转换为红黑树,那么查找key-value对时的复杂度是O(logn)。
下图要搜索map.get(38),如果是链表的话,必须要遍历5个结点。如果是红黑树的话,只要找3个结点,就可以找到那个38的值。
(3)红黑树的特点
一.红黑树是二叉查找树,左小右大,根据这个规则可以快速查找某个值。
二.普通的二叉查找树,有可能出现瘸腿的情况。也就是只有一条腿,出现不平衡了,导致查询的性能变成了O(n),退化成线性查询了。
三.红黑树,有红色和黑色两种结点。有一些条件限制来尽量保证树是平衡的,不会出现瘸腿的情况。
四.如果插入结点的时候破坏了红黑树的规则和平衡,那么会自动重新平衡。变色(红 <-> 黑)、旋转、左旋转、右旋转。
JDK 1.8+,在链表长度为8以后,链表 -> 红黑树。链表的遍历性能,时间复杂度是O(n),红黑树是O(logn)。所以如果出现大量哈希冲突后,红黑树的性能比链表高得多。
JDK 1.8+,HashMap的数据结构是:
数组 + 链表 + 红黑树。
12.HashMap源码二:核心成员变量的作用分析
(1)HashMap的数组默认大小16
(2)HashMap的默认负载因子0.75
(3)HashMap的结点内部类Node
(4)HashMap的Node数组table
(5)HashMap中的key-value对数量
(6)HashMap的扩容阈值
(7)HashMap的负载因子
(8)HashMap的构造方法
(1)HashMap的数组默认大小16
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {//The default initial capacity - MUST be a power of two.static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16...
}
HashMap的数组的默认初始大小是16;
ArrayList的数组的默认初始大小是10;
(2)HashMap的默认负载因子0.75
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {//The load factor used when none specified in constructor.static final float DEFAULT_LOAD_FACTOR = 0.75f;...
}
默认的负载因子为0.75。如果数组里的元素个数达到了数组大小(16) * 负载因子(0.75),也就是数组元素达到12个时就会进行数组扩容。
(3)HashMap的结点内部类Node
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...//Basic hash bin node, used for most entries.//(See below for TreeNode subclass, and in LinkedHashMap for its Entry subclass.)static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;...}
}
Node是HashMap的内部类,它代表了一个key-value对。一个Node结点里包含了:key的哈希值、key、value、一个next指针。这个next指针会指向下一个Node,也就是单向链表中的下一个结点。通过这个next指针就可以形成一个链表,来解决哈希冲突。
(4)HashMap的Node数组table
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {//The table, initialized on first use, and resized as necessary. //When allocated, length is always a power of two.//(We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed.)transient Node<K,V>[] table;...
}
Node[],这个数组就是HashMap里的核心数据结构的数组。数组的元素是Node类型的,天然就可以挂成一个单向链表,因为Node里面会有一个next指针。
(5)HashMap中的key-value对数量
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {//The number of key-value mappings contained in this map.transient int size;...
}
这个size就是当前HashMap中有多少个key-value对。如果该数量达到了指定大小 * 负载因子,那么就会进行数组扩容。
(6)HashMap的扩容阈值
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {//The next size value at which to resize (capacity * load factor).int threshold;final int capacity() {return (table != null) ? table.length : (threshold > 0) ? threshold : DEFAULT_INITIAL_CAPACITY;}...
}
扩容阈值threshold = 数组容量 * 负载因子。如果size达到threshold,那么HashMap就会进行数组扩容。
扩容原理涉及:负载因子、默认值、threshold、扩容、rehash的算法。
(7)HashMap的负载因子
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {//The load factor for the hash table.final float loadFactor;...
}
这就是负载因子,默认值是0.75f,也可以自己指定,一般不会修改。HashMap数组的初始容量,一般会手动指定。和使用ArrayList一样,需要预估会放多少key-value对,避免频繁扩容。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {//The load factor used when none specified in constructor.static final float DEFAULT_LOAD_FACTOR = 0.75f;//The load factor for the hash table.final float loadFactor;//Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75).public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}//Constructs an empty HashMap with the specified initial capacity and the default load factor (0.75).//@param initialCapacity the initial capacity.public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0) {throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);}if (initialCapacity > MAXIMUM_CAPACITY) {initialCapacity = MAXIMUM_CAPACITY;}if (loadFactor <= 0 || Float.isNaN(loadFactor)) {throw new IllegalArgumentException("Illegal load factor: " + loadFactor);}this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}//Returns a power of two size for the given target capacity.static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}...
}
(8)HashMap的构造方法
构造方法只是对HashMap的负载因子和扩容阈值进行赋值,具体的数组初始化则是在执行put()方法时才开始进行处理的。
13.HashMap源码三:降低哈希冲突概率的算法
(1)HashMap的put()方法会先对key进行哈希
(2)Hash算法的工作原理
(3)如何降低哈希冲突的概率
(4)总结
(1)HashMap的put()方法会先对key进行哈希
通过key的哈希值获取到对应数组中的index位置。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {//Associates the specified value with the specified key in this map.//If the map previously contained a mapping for the key, the old value is replaced.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);}...
}
(2)Hash算法的工作原理
HashMap里的Hash算法是经过优化的、高性能的。HashMap的hash()方法会对key执行具体的Hash算法来获取一个Hash值。
首先通过key.hashCode()获取key的HashCode,然后通过h >>> 16对HashCode进行右移16位,也就是把32位的二进制数字的所有bit往右侧移动16位,最后将右移16位的结果和HashCode进行异或运算。
假设h = 1111 1111 1111 1111 1111 1010 0111 1100
那么h >>> 16 = 0000 0000 0000 0000 1111 1111 1111 1111
接着h ^ (h >>> 16),异或也就是:
1111 1111 1111 1111 1111 1010 0111 1100
0000 0000 0000 0000 1111 1111 1111 1111
1111 1111 1111 1111 1111 0101 1000 0011
实际上就是将32位的key.hashCode()的高16位和低16位进行异或运算,这样可以降低哈希冲突的概率。
(3)如何降低哈希冲突的概率
为什么要将32位的key.hashCode()的高16位和低16位进行异或运算?
因为首先HashMap的数组的默认初始大小是16,然后在get()方法会用这个异或运算的结果值定位数组的index时,默认情况下就会将数组大小16和异或运算的结果值进行位与运算。当然随着数组扩容,之后可能用32和异或运算的结果值进行位与运算。
当使用数组大小16和异或运算的结果值进行位与运算时:由于HashCode是32位,所以运算结果最多只能利用HashCode低16位。因此为了尽量利用到HashCode的32位,降低哈希冲突的概率,才对HashCode进行高低16位的异或处理。否则如果直接使用HashCode的低16位进行位与运算,则冲突更多。
而当使用扩容后的数组大小32和异或运算的结果值进行位与运算时,即使不对HashCode进行高低16位异或,也能利用到32位的HashCode。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}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 && ((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;}...
}
(4)总结
在HashMap的hash()方法里,把HashCode的高低16位进行异或运算,可保证异或运算结果同时保留HashCode的高16位和低16位的特征。
于是在get()方法中通过位运算定位数组index时,即使只有低16位参与运算,HashCode的高低16位特征也参与到运算中。相比于直接使用HashCode的低16位去定位数组index,能减少哈希冲突。
14.HashMap源码四:put操作及哈希寻址算法
(1)HashMap.put()方法的源码
(2)HashMap数组的初始化
(3)通过位与运算的哈希寻址算法设置数组值
(1)HashMap.put()方法的源码
首先会通过HashMap.hash()方法的哈希算法根据key获取其哈希值,然后调用HashMap.putVal()方法把对应的键和值设置到HashMap数组。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;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);} else {Node<K,V> e; K k;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) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1sttreeifyBin(tab, hash);}break;}if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { break;}p = e;}}if (e != null) {// existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null) {e.value = value;}afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold) {resize();}afterNodeInsertion(evict);return null;}...
}
(2)HashMap数组的初始化
HashMap的构造方法只是对HashMap的负载因子和扩容阈值进行赋值,而具体的HashMap的数组初始化则是在执行put()方法时处理的。
假设一开始是通过HashMap的无参构造函数创建一个HashMap对象,然后第一次执行该HashMap对象的put()方法时HashMap的数组为空。
于是在执行"tab = resize()"代码,来对HashMap数组的初始化时,会初始化数组大小为默认16,负载因子为默认0.75,扩容阈值为12。也就是会初始化一个大小为16的、元素为Node的数组。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0) {n = (tab = resize()).length;}...}//Initializes or doubles table size. //If null, allocates in accord with initial capacity target held in field threshold.//Otherwise, because we are using power-of-two expansion, //the elements from each bin must either stay at same index, //or move with a power of two offset in the new table.final Node<K,V>[] resize() {//第一次执行HashMap的put()方法时,table为nullNode<K,V>[] oldTab = table;//原数组int oldCap = (oldTab == null) ? 0 : oldTab.length;//原数组大小int oldThr = threshold;//默认的无参构造函数下,扩容阈值threshold为nullint newCap, newThr = 0;//新数组大小,新扩容阈值if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {newThr = oldThr << 1;// double threshold}} else if (oldThr > 0) {// initial capacity was placed in thresholdnewCap = oldThr;} else {//默认的数组大小16newCap = DEFAULT_INITIAL_CAPACITY;//扩容阈值 = 16 * 1.75 = 12newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); }threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;...return newTab;}...
}
(3)通过位与运算的哈希寻址算法设置数组值
接着执行"hash & (n - 1)"代码,其中n是16,所以变成"15 & hash":
1111 1111 1111 1111 0000 0101 1000 0011
0000 0000 0000 0000 0000 0000 0000 1111
位与操作:就是必须都是1,才是1,否则就是0。所以"15 & hash"的结果是:
0000 0000 0000 0000 0000 0000 0000 0011
转成10进制就是3,因此index = 3。所以哈希寻址算法并不是直接用hash值对数组大小取模来实现的。因为取模的操作性能不高,而位运算的性能很高,一般会通过位与操作来实现取模的效果。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;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);}...}Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {return new Node<>(hash, key, value, next);}//Basic hash bin node, used for most entries. //(See below for TreeNode subclass, and in LinkedHashMap for its Entry subclass.)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;}...}...
}
JDK1.8对HashMap的一个优化就是:数组刚开始的初始值以及未来每次扩容的值,都是2的n次方。只要保证数组大小是2的n次方,就可以保证:"(数组大小 - 1) & hash"与"hash % 数组大小"的结果一样。
注意:a对2的n次方取模等价于a和2的n次方-1的结果进行位与。
通过hash & (n - 1),就能将任意一个hash值定位到数组的某个index里。直接取模的性能相对较低,所以这是HashMap提升性能的一个优化点,这也是HashMap底层原理里的重要部分。
15.HashMap源码五:哈希冲突时的链表处理
两个key的hash值不同,但通过哈希寻址算法定位到数组的同一个index。此时就会出现典型的哈希冲突,默认情况下会使用单向链表来处理。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {static final int TREEIFY_THRESHOLD = 8;//链表转红黑树的阈值...public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0) {n = (tab = resize()).length;}if ((p = tab[i = (n - 1) & hash]) == null) {//如果通过哈希寻址算法定位到的下标为i的数组元素为空(即tab[i]为空)//那么就可以直接将一个新创建的Node对象放到数组的tab[i]这个位置;tab[i] = newNode(hash, key, value, null);} else {Node<K,V> e; K k;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {//通过哈希寻址算法定位到的数组位置已有Node元素//那么判断是否为相同的key,如果是相同的key则进行value覆盖e = p;} else if (p instanceof TreeNode) {//通过哈希寻址算法定位到的数组位置已有Node元素,而且不是相同的key//那么通过"p instanceof TreeNode)",判断数组的tab[i]元素是否是一颗红黑树e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);} else {//如果通过哈希寻址算法定位到的数组位置已有Node元素,且判断出不是相同的key,且数组的tab[i]元素也不是一颗红黑树 //那么则说明数组的tab[i]元素是一个链表,于是通过"p.next = newNode()"这行代码将新元素串入到链表中for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//如果链表的长度大于等于8,则将这个链表转换成一个红黑树if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1sttreeifyBin(tab, hash);}break;}if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {break;}p = e;}}if (e != null) {//existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null) {e.value = value;}afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold) {resize();}afterNodeInsertion(evict);return null;}...
}
其中的分支代码"if ((p = tab[i = (n - 1) & hash]) == null)"的意思是:如果通过哈希寻址算法定位到的下标为i的数组元素为空(即tab[i]为空),那么就可以直接将一个新创建的Node对象放到数组的tab[i]这个位置。
如果通过哈希寻址算法定位到的数组位置已有Node元素,那么会判断是否为相同的key,如果是相同的key则进行value覆盖。如果不是相同的key,那么通过"p instanceof TreeNode)",判断数组的tab[i]元素是否是一颗红黑树。
如果通过哈希寻址算法定位到的数组位置已有Node元素,且判断出不是相同的key,且数组的tab[i]元素也不是一颗红黑树,那么则说明数组的tab[i]元素是一个链表,于是通过"p.next = newNode()"这行代码将新元素串入到链表尾部。
最后判断当前链表的长度是否已经大于等于8。如果是,则通过调用treeifyBin()方法将这个链表转换成一个红黑树。
16.HashMap源码六:引入红黑树优化哈希冲突
(1)红黑树的查找复杂度比链表的低
(2)链表转红黑树的处理
(3)总结
(1)红黑树的查找复杂度比链表的低
如果出现大量的哈希冲突后,某位置挂的一个链表可能会特别的长。如果链表长度太长,那么就会导致有一些get()操作的时间复杂度就是O(n)。虽然通过table[i]数组索引直接定位元素的时间复杂度是O(1),但如果链表存在大量元素,会是导致对该链表get()操作的性能急剧下降的。
所以JDK 1.8以后进行了HashMap优化:如果链表的长度达到8,那么就会将链表转换为红黑树。如果对红黑树进行get()操作,那么时间复杂度会变成O(logn)。红黑树查找的O(logn)远比链表查找的O(n)低,性能得到大幅提升。
(2)链表转红黑树的处理
//如果通过哈希寻址算法定位到的数组位置已有Node元素,且判断出不是相同的key,且数组的tab[i]元素也不是一颗红黑树
//那么则说明数组的tab[i]元素是一个链表,于是通过"p.next = newNode()"这行代码将新元素串入到链表中
for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//如果链表的长度大于等于8,则将这个链表转换成一个红黑树;if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1sttreeifyBin(tab, hash);}break;}if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {break;}p = e;
}
当遍历到链表的第7个结点时,binCount是6。当遍历到链表的第8个结点时,binCount是7。当向链表准备挂上第9个结点时,就会发现binCount >= 7了。达到了临界值,此时就会将链表转换为红黑树。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...//Replaces all linked nodes in bin at index for given hash unless table is too small, in which case resizes instead.final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {resize();} else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;//通过do while循环将Node类型的单向链表转换为TreeNode类型的双向链表do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null) {//设置双向链表的头结点hd = p;} else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);//调用TreeNode的treeify()方法将TreeNode类型的双向链表转换成一棵红黑树if ((tab[index] = hd) != null) {hd.treeify(tab);}}}//将Node类型的结点转换成TreeNode类型的结点TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {return new TreeNode<>(p.hash, p.key, p.value, next);}...
}
上面的do while循环执行完之后,只是将Node类型的单向链表转换为TreeNode类型的双向链表,接着会将TreeNode类型的双向链表的头结点设置为数组元素tab[index],然后执行双向链表头结点的treeify()方法将双向链表转为一棵红黑树。
(3)总结
一.出现哈希冲突的两种情况
情况一:key不一样但hashCode()方法重写出问题,导致Hash值一样
情况二:key不一样且Hash值也不一样,但定位到的数组位置一样
二.出现哈希冲突后的处理
首先会将元素放到单向链表中存放。如果链表长度超过8,则单向链表先变成双向链表,然后再转成红黑树。之后,会将元素放到红黑树中存放。以下是双向链表转红黑树的方法源码:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...//Entry for Tree bins. //Extends LinkedHashMap.Entry (which in turn extends Node) so can be used as extension of either regular or linked node.static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;//red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;//needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}//Forms tree of the nodes linked from this node.final void treeify(Node<K,V>[] tab) {TreeNode<K,V> root = null;for (TreeNode<K,V> x = this, next; x != null; x = next) {next = (TreeNode<K,V>)x.next;x.left = x.right = null;if (root == null) {x.parent = null;x.red = false;root = x;} else {K k = x.key;int h = x.hash;Class<?> kc = null;for (TreeNode<K,V> p = root;;) {int dir, ph;K pk = p.key;if ((ph = p.hash) > h) {dir = -1;} else if (ph < h) {dir = 1;} else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { dir = tieBreakOrder(k, pk);}TreeNode<K,V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {x.parent = xp;if (dir <= 0) {xp.left = x;} else {xp.right = x;}root = balanceInsertion(root, x);break;}}}}moveRootToFront(tab, root);}...}...
}public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {...static class Entry<K,V> extends HashMap.Node<K,V> {Entry<K,V> before, after;Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}...
}
17.HashMap源码七:哈希冲突时插入红黑树
假设现在某个table[i]已经是一颗红黑树了。如果此时在table[i]再次出现哈希冲突,则会在红黑树中插入一个元素,也就是在HashMap的putVal()方法中调用TreeNode的putTreeVal()方法。红黑树是一个平衡的二叉查找树,插入元素时需要变色、旋转。TreeNode.putTreeVal()方法会在保持平衡的前提下,插入元素到红黑树。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {static final int TREEIFY_THRESHOLD = 8;//链表转红黑树的阈值...public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0) {n = (tab = resize()).length;}if ((p = tab[i = (n - 1) & hash]) == null) {//如果通过哈希寻址算法定位到的下标为i的数组元素为空(即tab[i]为空)//那么就可以直接将一个新创建的Node对象放到数组的tab[i]这个位置;tab[i] = newNode(hash, key, value, null);} else {Node<K,V> e; K k;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {//通过哈希寻址算法定位到的数组位置已有Node元素//那么判断是否为相同的key,如果是相同的key则进行value覆盖e = p;} else if (p instanceof TreeNode) {//通过哈希寻址算法定位到的数组位置已有Node元素,而且不是相同的key//那么通过"p instanceof TreeNode)",判断数组的tab[i]元素是否是一颗红黑树e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);} else {...}...}++modCount;if (++size > threshold) {resize();}afterNodeInsertion(evict);return null;}...//Entry for Tree bins. //Extends LinkedHashMap.Entry (which in turn extends Node) so can be used as extension of either regular or linked node.static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;//red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;//needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}...//Tree version of putVal.final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {Class<?> kc = null;boolean searched = false;TreeNode<K,V> root = (parent != null) ? root() : this;for (TreeNode<K,V> p = root;;) {int dir, ph; K pk;if ((ph = p.hash) > h) {dir = -1;} else if (ph < h) {dir = 1;} else if ((pk = p.key) == k || (k != null && k.equals(pk))) {return p;} else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) {if (!searched) {TreeNode<K,V> q, ch;searched = true;if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) { return q;}}dir = tieBreakOrder(k, pk);}TreeNode<K,V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {Node<K,V> xpn = xp.next;TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);if (dir <= 0) {xp.left = x;} else {xp.right = x;}xp.next = x;x.parent = x.prev = xp;if (xpn != null) {((TreeNode<K,V>)xpn).prev = x;}moveRootToFront(tab, balanceInsertion(root, x));return null;}}}...}...
}
18.HashMap源码八:JDK 1.7的数组扩容原理
(1)HashMap的扩容会进行两倍扩容 + rehash
(2)HashMap的扩容原理总结
(1)HashMap的扩容会进行两倍扩容 + rehash
由于HashMap底层是基于数组来实现的,所以和ArrayList一样存在扩容的问题。也就是如果数组满了,那么就必须要扩容。
HashMap扩容的原理非常简单:2倍扩容 + rehash。每个key-value对,都会基于key的hash值重新寻址找到新数组的新位置。
原来数组的长度是16,现在新数组的长度是32。原来所有的key的hash对16取模是一个位置,比如index = 5。但是如果对32取模,可能就是index = 11,位置可能变化。
(2)HashMap的扩容原理总结
一.基于数组实现
二.一次扩容2倍
三.rehash的过程
在rehash的过程中,很多key在新数组的位置可能都不一样了,之前存在冲突的key可能在新的数组里分布在不同的位置上了。
上述便是JDK 1.7以前的扩容原理,通过取模实现哈希寻址。在JDK 1.8以后,则通过位与来实现哈希寻址。
19.HashMap源码九:JDK 1.8的数组扩容原理
(1)出现hash冲突的例子
(2)数组扩容的源码
(3)数组扩容后的例子
(4)HashMap的底层原理总结
JDK 1.8为提升rehash性能,不再使用key的hash值对新数组大小取模。而使用位与操作实现哈希寻址,因为直接取模的性能比较低。
(1)出现hash冲突的例子
如下两个hash值会出现哈希冲突,HashMap使用链表 + 红黑树解决。
n - 1 0000 0000 0000 0000 0000 0000 0000 1111
hash值1 1111 1111 1111 1111 0000 1111 0000 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)n - 1 0000 0000 0000 0000 0000 0000 0000 1111
hash值2 1111 1111 1111 1111 0000 1111 0001 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
(2)数组扩容的源码
每次执行HashMap的put()方法(也就是执行HashMap的putVal()方法),将一个新的key-value对放入到HashMap的数组之后,就会对size自增。
接着会判断数组大小size,是否已经达到了扩容阈值threshold大小。如果已经达到扩容阈值,那么就执行HashMap的resize()方法进行扩容。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {static final int TREEIFY_THRESHOLD = 8;//链表转红黑树的阈值...public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0) {n = (tab = resize()).length;}if ((p = tab[i = (n - 1) & hash]) == null) {//如果通过哈希寻址算法定位到的下标为i的数组元素为空(即tab[i]为空)//那么就可以直接将一个新创建的Node对象放到数组的tab[i]这个位置;tab[i] = newNode(hash, key, value, null);} else {Node<K,V> e; K k;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {//通过哈希寻址算法定位到的数组位置已有Node元素//那么判断是否为相同的key,如果是相同的key则进行value覆盖e = p;} else if (p instanceof TreeNode) {//通过哈希寻址算法定位到的数组位置已有Node元素,而且不是相同的key//那么通过"p instanceof TreeNode)",判断数组的tab[i]元素是否是一颗红黑树e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);} else {...}...}++modCount;//判断数组大小size,是否已经达到了扩容阈值threshold大小if (++size > threshold) {resize();}afterNodeInsertion(evict);return null;}final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//原数组int oldCap = (oldTab == null) ? 0 : oldTab.length;//原数组大小int oldThr = threshold;//原扩容阈值int newCap, newThr = 0;//新数组大小,新扩容阈值if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {//newCap = oldCap << 1,就是乘以2,新数组的大小是原数组的2倍newThr = oldThr << 1; // double threshold}} else if (oldThr > 0) {// initial capacity was placed in thresholdnewCap = oldThr; } else {// zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;//如果e.next是null的话,这个位置的元素不是链表也不是红黑树if (e.next == null) {//那么此时就是用e.hash & newCap(新数组的大小) - 1,进行与运算//直接定位到新数组的某个位置,然后直接就放在新数组里 newTab[e.hash & (newCap - 1)] = e;} else if (e instanceof TreeNode) {//如果这个位置是一个红黑树的话,此时会调用split()方法,去里面遍历这颗红黑树//然后将里面每个结点都进行重新hash寻址,找到新数组的位置 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);} else {//preserve order//进入这个else分支的话,证明是链表Node<K,V> loHead = null, loTail = null;//旧链表Node<K,V> hiHead = null, hiTail = null;//新链表Node<K,V> next;//对链表的处理逻辑do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null) {loHead = e;} else {loTail.next = e;}loTail = e;} else {if (hiTail == null) {hiHead = e;} else {hiTail.next = e;}hiTail = e;}} while ((e = next) != null);//要么直接放在新数组的原来的那个index,要么就是原来的index + oldCapif (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
}
(3)数组扩容后的例子
如果数组的长度扩容之后为32,需要重新对每个hash值进行寻址,也就是用每个hash值跟新数组的length - 1进行与操作。
n-1 0000 0000 0000 0000 0000 0000 0001 1111
hash值1 1111 1111 1111 1111 0000 1111 0000 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)n-1 0000 0000 0000 0000 0000 0000 0001 1111
hash值2 1111 1111 1111 1111 0000 1111 0001 0101
&结果 0000 0000 0000 0000 0000 0000 0001 0101 = 21(index = 21的位置)
可见hash2的位置,从原来的5变成了21 = 6 + 原数组大小16。也就是说,HashMap的数组大小每次扩容2倍后,比如从16到32到64,元素要么停留在原index位置,要么移动到原index + 原数组大小位置。以上就是JDK 1.8以后,数组扩容时元素重新寻址的过程。
(4)HashMap的底层原理总结
一.哈希算法
为什么要对key的HashCode进行高低位的异或运算?
因为可以降低数组大小为16时的哈希冲突的概率。(h = key.hashCode()) ^ (h >>> 16);
二.哈希寻址
为什么是hash值和数组.length - 1进行与运算?
因为位与的性能比取模要高:tab[(n - 1) & hash];
三.哈希冲突处理
首先将元素存到单向链表中,当单向链表的元素超8个时,再将单向链表转双向链表再转红黑树。
四.数组扩容机制
每次按原数组大小2倍扩容,并按hash & (n - 1)进行重新寻址(rehash)。重新寻址时,会判断hash & (n - 1)的二进制结果是否多出一个bit的1。如果是,那么重新寻址后的位置就是index + oldCap。如果否,那么重新寻址后的位置还是原来的index。通过这个方式,避免rehash时使用低效的每个hash值对新数组大小进行取模。
20.HashMap源码十:get与remove操作源码
(1)HashMap的get()方法源码
(2)HashMap的remove()方法源码
(1)HashMap的get()方法源码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}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 && ((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;}...static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;//red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;//needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}...//Calls find for root node.final TreeNode<K,V> getTreeNode(int h, Object k) {return ((parent != null) ? root() : this).find(h, k, null);}//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;}}//Finds the node starting at root p with the given hash and key.//The kc argument caches comparableClassFor(key) upon first use comparing keys.final TreeNode<K,V> find(int h, Object k, Class<?> kc) {TreeNode<K,V> p = this;do {int ph, dir; K pk;TreeNode<K,V> pl = p.left, pr = p.right, q;if ((ph = p.hash) > h) {p = pl;} else if (ph < h) {p = pr;} else if ((pk = p.key) == k || (k != null && k.equals(pk))) {return p;} else if (pl == null) {p = pr;} else if (pr == null) {p = pl;} else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) { p = (dir < 0) ? pl : pr;} else if ((q = pr.find(h, k, kc)) != null) {return q;} else {p = pl;}} while (p != null);return null;}}...
}
(2)HashMap的remove()方法源码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;}static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {//直接从定位到的数组位置中获取元素node = p;} else if ((e = p.next) != null) {if (p instanceof TreeNode) {//从红黑树中获取元素node = ((TreeNode<K,V>)p).getTreeNode(hash, key);} else {//从链表中获取元素do {if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) {//从红黑树中删除元素((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);} else if (node == p) {//从数组位置上删除元素tab[index] = node.next;} else {//从链表中删除元素p.next = node.next;}++modCount;--size;afterNodeRemoval(node);return node;}}return null;}...static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;//red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;//needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}...//Removes the given node, that must be present before this call.//This is messier than typical red-black deletion code //because we cannot swap the contents of an interior node with a leaf successor //that is pinned by "next" pointers that are accessible independently during traversal. //So instead we swap the tree linkages. //If the current tree appears to have too few nodes, the bin is converted back to a plain bin. //(The test triggers somewhere between 2 and 6 nodes, depending on tree structure).final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {int n;if (tab == null || (n = tab.length) == 0) {return;}int index = (n - 1) & hash;TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;if (pred == null) {tab[index] = first = succ;} else {pred.next = succ;}if (succ != null) {succ.prev = pred;}if (first == null) {return;}if (root.parent != null) {root = root.root();}if (root == null || (movable && (root.right == null || (rl = root.left) == null || rl.left == null))) {tab[index] = first.untreeify(map);//too smallreturn;}TreeNode<K,V> p = this, pl = left, pr = right, replacement;if (pl != null && pr != null) {TreeNode<K,V> s = pr, sl;while ((sl = s.left) != null) {//find successors = sl;}boolean c = s.red; s.red = p.red; p.red = c; // swap colorsTreeNode<K,V> sr = s.right;TreeNode<K,V> pp = p.parent;if (s == pr) {// p was s's direct parentp.parent = s;s.right = p;} else {TreeNode<K,V> sp = s.parent;if ((p.parent = sp) != null) {if (s == sp.left) {sp.left = p;} else {sp.right = p;}}if ((s.right = pr) != null) {pr.parent = s;}}p.left = null;if ((p.right = sr) != null) {sr.parent = p;}if ((s.left = pl) != null) {pl.parent = s;}if ((s.parent = pp) == null) {root = s;} else if (p == pp.left) {pp.left = s;} else {pp.right = s;}if (sr != null) {replacement = sr;} else {replacement = p;}} else if (pl != null) {replacement = pl;} else if (pr != null) {replacement = pr;} else {replacement = p;}if (replacement != p) {TreeNode<K,V> pp = replacement.parent = p.parent;if (pp == null) {root = replacement;} else if (p == pp.left) {pp.left = replacement;} else {pp.right = replacement;}p.left = p.right = p.parent = null;}TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);if (replacement == p) { // detachTreeNode<K,V> pp = p.parent;p.parent = null;if (pp != null) {if (p == pp.left) {pp.left = null;} else if (p == pp.right) {pp.right = null;}}}if (movable) {moveRootToFront(tab, r);}}}
}
21.LinkedHashMap有顺序的Map数据结构
(1)HashMap和LinkedHashMap的遍历顺序
(2)LinkedHashMap和TreeMap的区别
(3)HashMap的newNode()方法
(4)LinkedHashMap重写的newNode()方法
(5)LinkedHashMap覆盖key时是否改变顺序
(1)HashMap和LinkedHashMap的遍历顺序
HashMap添加一些key-value对后,如果要遍历这个HashMap,那么遍历的顺序并不是按照插入的key-value的顺序来进行遍历的;
LinkedHashMap是HashMap的一个子类。LinkedHashMap会记录添加key-value的顺序,在遍历LinkedHashMap时会按照添加key-value对的顺序进行遍历。
(2)LinkedHashMap和TreeMap的区别
LinkedHashMap和TreeMap都可以维持key的插入顺序。LinkedHashMap是基于链表来实现的,TreeMap是基于红黑树来实现的。
LinkedHashMap基本上与HashMap是差不多的。区别在插入、覆盖、删除时,会使用一个链表来记录key-value对的顺序。这样遍历LinkedHashMap时就可以按照这个链表的顺序来遍历;
LinkedHashMap重写了HashMap的newNode()方法。LinkedHashMap的newNode()方法会用一个链表来记录key-value对的顺序。
(3)HashMap的newNode()方法
HashMap在putVal()方法会调用newNode方法:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;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);} else {...}}Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {return new Node<>(hash, key, value, next);}...
}
(4)LinkedHashMap重写的newNode()方法
首先会将结点封装为一个LinkedHashMap.Entry对象,然后调用linkNodeLast()方法将这个结点挂到一个链表里去。
LinkedHashMap有两个指针head和tail,head和tail这两个指针都是LinkedHashMap.Entry类型的。
LinkedHashMap.Entry也有两个指针before和after。通过before和after两个指针,LinkedHashMap便可以将:LinkedHashMap.Entry类型的结点通过尾插法串成一个双向链表。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {//HashMap.Node subclass for normal LinkedHashMap entries.static class Entry<K,V> extends HashMap.Node<K,V> {Entry<K,V> before, after;Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}...//The head (eldest) of the doubly linked list.transient LinkedHashMap.Entry<K,V> head;//The tail (youngest) of the doubly linked list.transient LinkedHashMap.Entry<K,V> tail;Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);linkNodeLast(p);return p;}//通过尾插法维护一个双向链表private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {LinkedHashMap.Entry<K,V> last = tail;tail = p;if (last == null) {head = p;} else {p.before = last;last.after = p;}}
}
(5)LinkedHashMap覆盖key时是否改变顺序
一开始LinkedHashMap这个链表里只有一个结点,比如p。所以LinkedHashMap的tail和head两个指针,都会指向这个p。
LinkedHashMap的构造方法有一个参数accessOrder,默认是false。如果accessOrder是false,那么get一个key或者覆盖这个key,都不会改变它在链表里的顺序;如果accessOrder是true,那么get一个key或者覆盖这个key,就会改变它在链表里的顺序到尾部;
当删除LinkedHashMap的某个元素时,就会将那个元素从链表中删除。当迭代LinkedHashMap的元素时,就会从链表的头部head开始迭代。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {...public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder = accessOrder;}public V get(Object key) {Node<K,V> e;if ((e = getNode(hash(key), key)) == null) {return null;}if (accessOrder) {afterNodeAccess(e);}return e.value;}void afterNodeAccess(Node<K,V> e) {//move node to lastLinkedHashMap.Entry<K,V> last;if (accessOrder && (last = tail) != e) {LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null;if (b == null) {head = a;} else {b.after = a;}if (a != null) {a.before = b;} else {last = b;}if (last == null) {head = p;} else {p.before = last;last.after = p;}tail = p;++modCount;}}...
}
22.TreeMap可自定义排序规则的红黑树Map
TreeMap是基于红黑树实现的,不是传统意义上的HashMap。TreeMap天然可以按key的自然顺序来排序,即按照key的大小来排序。而且TreeMap也可以自定义比较方法来进行排序,也就是TreeMap可以定制Comparator,自己决定排序的算法和逻辑。TreeMap会基于红黑树来实现按key排序。
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable { ...static final class Entry<K,V> implements Map.Entry<K,V> {K key;V value;Entry<K,V> left;Entry<K,V> right;Entry<K,V> parent;boolean color = BLACK;}...
}
23.HashSet + LinkedHashSet + TreeSet简介
(1)HashSet基于HashMap来实现
(2)LinkedHashSet基于LinkedHashMap实现
(3)TreeSet基于TreeMap来实现
(4)Set基于Map也有数组扩容的问题
(1)HashSet基于HashMap来实现
ArrayList、LinkedList都比较简单,没什么太多复杂问题。HashMap和LinkedHashMap是基于HashMap来实现的,TreeMap和Set是直接基于Map来实现的。
HashSet是基于HashMap来实现的。因为HashMap不允许key重复,HashMap的底层是一个数组。如果key重复,那hash寻址会到数组的同一个位置,然后覆盖原值。
HashSet是一个集合,里面的元素是无序的,里面的元素没有重复。而HashMap的key就是无顺序的,插入顺序和迭代遍历的顺序不一样。而且HashMap的key也没有重复,所以HashSet可以直接基于HashMap来实现。
当不断地往HashSet里放入元素,底层就是不断put元素到HashMap里。如果要从HashSet里进行遍历,直接遍历HashMap的key即可。
(2)LinkedHashSet基于LinkedHashMap实现
LinkedHashSet是有顺序的Set,也就是维持了插入Set的顺序,迭代LinkedHashSet的顺序跟插入的顺序是一样的。LinkedHashSet可直接基于LinkedHashMap来实现。
(3)TreeSet基于TreeMap来实现
TreeSet默认是根据插入进去的元素的值来排序的,而且可以定制Comparator,自己决定排序的算法和逻辑,所以TreeSet底层可基于TreeMap来实现。
(4)Set基于Map也有数组扩容的问题
Set底层的Map,只有key是有值的,value都是null值都是空的。HashSet底层是基于HashMap来实现的,所以也有数组扩容的问题,可以在构造HashSet的时候就传入数组的大小。
问题:Set底层的实现原理是什么?
Set是基于Map来实现的。也就是在Map的key里放置值,但里面的value都是一个空对象。
比如,LinkedHashSet.add()方法,会调用LinkedHashMap.put()方法。此时在这个方法里就会记住加入元素的顺序。遍历LinkedHashSet时,直接遍历LinkedHashMap里维护好的链表。
24.迭代器应对多线程并发修改的Fail-Fast机制
(1)Java集合在迭代时的Fail-Fast机制
(2)通过modCount实现Java集合在迭代时的Fail-Fast机制
(1)Java集合在迭代时的Fail-Fast机制
也就是一个线程在迭代遍历集合,另一个线程在修改集合时:迭代的线程会快速报异常:ConcurrentModificationException。这个异常就是并发修改的异常,这种机制就叫做Fail-Fast机制。
(2)通过modCount实现Java集合在迭代时的Fail-Fast机制
modCount就是用来实现Fail-Fast机制的,各个集合里其实都有这个modCount的概念。只要这个集合被修改了,那么就会对modCount++。modCount就是修改次数之意,只要修改一次集合,那么就会更新modCount。这些修改集合的方法有:add()、remove()、set()等。
Java集合包下的类都是非线程安全的,都设计了针对并发修改集合的处理,也就是用modCount来实现Fail-Fast机制。
比如在迭代一个ArrayList前,已经插入4个元素,此时modCount = 4。获取和初始化一个迭代器时,其expectedModCount会设为modCount,迭代器每次迭代时都会比较expectedModCount和modCount是否相等。如果不相等,抛出并发修改冲突异常ConcurrentModificationException。
比如HashMap在迭代开始时,会将modCount赋值给mc。迭代完成后,会判断mc是否等于modCount。如果不相等,抛出并发修改冲突异常ConcurrentModificationException。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { ...@Overridepublic void forEach(Consumer<? super E> action) {Objects.requireNonNull(action);final int expectedModCount = modCount;@SuppressWarnings("unchecked")final E[] elementData = (E[]) this.elementData;final int size = this.size;for (int i=0; modCount == expectedModCount && i < size; i++) {action.accept(elementData[i]);}if (modCount != expectedModCount) {throw new ConcurrentModificationException();}}...
}public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...transient int modCount;@Overridepublic void forEach(BiConsumer<? super K, ? super V> action) {Node<K,V>[] tab;if (action == null) {throw new NullPointerException();}if (size > 0 && (tab = table) != null) {int mc = modCount;for (int i = 0; i < tab.length; ++i) {for (Node<K,V> e = tab[i]; e != null; e = e.next) {action.accept(e.key, e.value);}}if (modCount != mc) {throw new ConcurrentModificationException();}}}...
}