Java常用容器源码(部分),个人笔记(解析仅个人理解不一定准)
一、List
ArrayList
成员变量
// 默认初始容量。
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享空数组实例。
private static final Object[] EMPTY_ELEMENTDATA = {};
// 共享空数组实例用于默认大小的空实例。我们将它与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时要膨胀多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 数组缓冲区,数组列表的元素存储在其中。数组列表的容量是这个数组缓冲区的长度。任何带有
// elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组列表在添加第一个元素时将被扩展为DEFAULT_CAPACITY。
transient Object[] elementData; // non-private to simplify nested class access
// 数组列表的大小(它包含的元素的数量)。
private int size;
构造器
//无参构造器: 构造一个初始容量为10的空列表,且当添加第一个元素时数组才初始容量为10。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 构造具有指定初始容量的空列表。initialCapacity 指定的初始容量。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 如果指定的初始容量大于0,则创建一个指定初始容量的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 否则如果指定的初始容量为0,则创建一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 其它情况抛出异常
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
// 按照集合的迭代器返回元素的顺序构造一个包含指定集合元素的列表。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray(); // 集合转数组
// 如果指定的集合元素个数不等于0个,则按顺序拷贝原先元素到新数组中(新数组容量为size)
if ((size = elementData.length) != 0) {
// c.toArray可能返回的不是Object类型数组
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 替换为空数组。
this.elementData = EMPTY_ELEMENTDATA;
}
}
add方法
------------------------- 无参add方法---------------
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) {
// 从最小需要容量 和 默认容量间返回较大值作为数组的初始容量(即10)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 确保容量,判断是否需要进行扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code 判断当前数组容量是否大于最小需要容量,如果不满足则对数组进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity); // 扩容方法
}
// 核心方法,数组扩容
private void grow(int minCapacity) {
// 拿到原数组容量
int oldCapacity = elementData.length;
// 扩容为原数组容量的1.5倍 (10 + (10 / 2))
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 判断扩容后的数组新容量是否小于 最小需要容量,如果小于则以最小需要容量作为数组新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 判断数组新容量是否大于整形最大值-8 (2147483647 - 8 = 2147483639),如果大于则重新计算数组新容量
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果最小需要容量大于 2147483639,则以整形最大值作为数组新容量,否则以 2147483639作为数组新容量
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
------------------------- 有参add方法---------------
public void add(int index, E element) {
// 检查是否添加
rangeCheckForAdd(index);
// 确保容量,同无参add方法逻辑
ensureCapacityInternal(size + 1); // Increments modCount!!
// 1.elementData数组 2.移动元素从index起 3.从index + 1起依次插入这些元素 4.size - index要移动的元素个数
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将新增元素添加到数组指定索引位置
elementData[index] = element;
// 元素个数+1
size++;
}
remove方法
------------------------- 按索引下标删除---------------
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);
// GC回收
elementData[--size] = null; // clear to let GC do its work
// 删除成功返回删除的元素
return oldValue;
}
// 检查删除的元素索引是否越界,越界则抛出IndexOutOfBoundsException异常
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
------------------------- 按对象删除---------------
public boolean remove(Object o) {
// 要删除对象不为空
if (o == null) {
// 循环遍历比较
for (int index = 0; index < size; index++)
// 删除第一个为空值的元素
if (elementData[index] == null) {
fastRemove(index);
// 删除成功返回true
return true;
}
} else {
for (int index = 0; index < size; index++)
// 删除第一个与要删除元素相同的元素
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
// 其它情况删除失败
return false;
}
// 按索引删除元素,不做边界检查
private void fastRemove(int index) {
modCount++;
// 要移动元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 将删除指定元素之后的元素(index+1) 从要删除元素位置(index)依次插入,移动个数为numMoved个
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
LinkedList
成员变量
// 链表节点个数
transient int size = 0;
// 指向链表的头节点
transient Node<E> first;
// 指向链表的尾节点
transient Node<E> last;
// 私有静态内部类(每个节点对象)
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;
}
}
构造器
// 构造一个空列表。
public LinkedList() { }
// 按照集合的迭代器返回元素的顺序构造一个包含指定集合元素的列表。
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
add、addAll
// 添加一个元素
public boolean add(E e) {
linkLast(e);
return true; // 添加成功
}
// 实际添加方法。添加第一个元素时头尾节点都指向它
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
// 设置当前节点为链表的尾节点
last = newNode;
// 在第一次添加时为null,即添加的第一个元素在第一次添加完成后链表头尾节点都是它(第二次添加尾节点改变)
if (l == null)
// 设置链表的头节点为当前节点
first = newNode;
else
// 第二次添加起,原链表的尾节点的下一个节点为当前添加节点
l.next = newNode;
// 元素+1
size++;
modCount++;
}
---------------------------addAll-------------
// 一次添加一个集合元素。将指定集合中的所有元素从指定位置开始插入到此列表中。
// 将当前位于该位置的元素(如果有的话)和所有后续元素向右移动(增加它们的索引)。
// 新元素将按照指定集合的迭代器返回它们的顺序出现在列表中。
public boolean addAll(int index, Collection<? extends E> c) {
// 校验索引是否越界,越界抛出异常
checkPositionIndex(index);
Object[] a = c.toArray();
// 数组长度,如果为0则说明添加的是空集合,无需处理。
int numNew = a.length;
if (numNew == 0)
return false;
// 插入位置的前驱节点 和 后继节点
Node<E> pred, succ;
if (index == size) {
// 如果插入位置是链表的尾部,则前驱节点为last,后继节点为null
succ = null;
pred = last;
} else {
// 否则调用node方法获得后继节点,再通过后继节点获得前驱节点
succ = node(index);
pred = succ.prev;
}
// 遍历插入
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// 新节点
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
// 如果插入位置在链表的头部,即链表还没有元素,则将这个节点设置为链表的头节点
first = newNode;
else
// 否则链接前驱节点
pred.next = newNode;
// 修改下一个要插入元素的前驱节点为当前节点
pred = newNode;
}
if (succ == null) {
// 如果不是在链表尾部插入的话后继节点为空,则将前驱节点设置为链表的尾节点
last = pred;
} else {
// 否则不是在链表尾部插入的话,则将插入的链表与先前链表连接起来
pred.next = succ;
succ.prev = pred;
}
// 原长度 + 插入的集合长度
size += numNew;
modCount++;
return true;
}
addFirst 头插入、addLast 尾插入
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++;
}
----------尾插入与头插入做法相似-------------
public void addLast(E e) {
linkLast(e);
}
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++;
}
remove
// 按索引进行删除
public E remove(int index) {
// 校验索引是否越界,越界则抛出异常
checkElementIndex(index);
return unlink(node(index));
}
// 得到删除节点
Node<E> node(int index) {
// assert isElementIndex(index);
// 折半判断删除节点是否在链表前半部分,如果是则从前往后遍历查找更快
if (index < (size >> 1)) { // size >> 1 => size/2
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;
}
}
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;
}
// 让GC回收
x.item = null;
size--;
modCount++;
return element;
}
----------------- 根据元素删除------------
public boolean remove(Object o) {
// 如果要删除元素为空,则从前往后找到第一个为null的节点,进行删除,删除成功返回true。
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 从前往后找到第一个为null的节点,使用equals比较找到第一个匹配的元素删除,成功返回true
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
// 其它情况返回false
return false;
}
Vector
Vector与ArrayList最大的区别就是,Vector的操作相关方法都使用了synchronized对操作方法进行修饰。所以Vector是一个线程安全的容器。
成员变量
// 数组缓冲区,其中存储向量的分量。vector的容量是数组缓冲区的长度,至少要足够大,可以包含vector的所有元素。
// Vector中最后一个元素后面的任何数组元素都是空的。
protected Object[] elementData;
// Vector对象中有效组件的数量。组件elementData[0]到elementData[elementCount-1]是实际的项。
protected int elementCount;
// 当向量的大小大于其容量时,其容量自动增加的量。如果容量增量小于或等于零,则每次需要增长时,向量的容量都会翻倍。
protected int capacityIncrement;
构造器
// 构造一个具有指定初始容量和容量增量的空向量。
// initialCapacity—向量的初始容量capacityIncrement—当向量溢出时容量增加的量
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 创建一个初始容量为initialCapacity的数组
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
// 构造一个具有指定初始容量且容量增量为零的空向量。initialCapacity—向量的初始容量
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
// 构造一个空向量,使其内部数据数组的大小为10,其标准容量增量为0。
public Vector() {
this(10);
}
// 按照集合的迭代器返回元素的顺序构造一个包含指定集合元素的向量。c:其元素被放置到该向量中的集合
public Vector(Collection<? extends E> c) {
elementData = c.toArray();
elementCount = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
add
public synchronized boolean add(E e) {
modCount++;
// 确保容量
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
// 这实现了ensureCapacity的非同步语义。该类中的同步方法可以在内部调用此方法,以确保容量,而不会产生额外的同步成本。
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
// 如果最小需要容量大于当前数组容量,则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
// 拿到当前数组容量
int oldCapacity = elementData.length;
// 如果扩容的增量大于0,则每次扩容为(原数组容量+增量)为新容量,否则扩展为原容量的2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// 如果扩容后新容量小于最小需要容量则,以需要最小容量作为数组新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于2147483639,则在hugeCapacity中对新容量进一步修改
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 对数组扩容和元素的拷贝操作
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果最小需要容量大于 2147483639,则以整形最大值作为数组新容量,否则以2147483639(整形最大值-8)作为数组新容量
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
其它和ArrayList几乎差不多,就不分析了。总的来说就是和ArrayList大同小异。
二、Set
HashSet底层是基于HashMap实现的;
LinkedHashSet底层是基于LinkedHashMap实现的;
TreeSet底层是基于TreeMap实现的;
所以对于Set容器就不多解析了,具体可以看Map中的分析;
HashSet
成员变量
// HashSet底层是基于HashMap实现的,管理着一个map对象
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
// 与底层 Map 中的 Object 关联的虚值(即map中的value)
private static final Object PRESENT = new Object();
构造器
// 构造一个新的空集合;支持的HashMap实例具有默认的初始容量(16)和负载因子(0.75)。
public HashSet() {
map = new HashMap<>();
}
// 构造包含指定集合中的元素的新集合。创建HashMap时使用默认负载因子(0.75)和足以包含指定集合中的元素的初始容量。
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
// 构造一个新的空集合;后台HashMap实例具有指定的初始容量和指定的负载因子。
// initialCapacity:哈希映射的初始容量、loadFactor:哈希映射的负载因子(加载因子)
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 构造一个新的空集合;支持的HashMap实例具有指定的初始容量和默认负载因子(0.75)。initialCapacity:哈希表的初始容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 构造一个新的空链接散列集。(这个包的私有构造函数只被LinkedHashSet使用。)后台HashMap实例是一个LinkedHashMap,具有指定的初始容量和指定的负载因子。
// initialCapacity:哈希映射的初始容量、loadFactor:哈希映射的负载因子、dummy被忽略(只是将此构造函数与其他int、float构造函数区别开来)。
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
add方法
// 添加成功返回true
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
remove方法
// 删除成功返回true
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
HashSet 具体实现查看HashMap处解析
LinkedHashSet
LinkedHashSet继承了HashSet,LinkedHashSet中并没实现什么方法,其底层是基于LinkedHashMap实现的。
继承体系
- 继承了 HashSet
- 实现了Set、Cloneable、 java.io.Serializable
构造器
// 构造具有指定初始容量和负载因子的新的空链接散列集。
// initialCapacity:链接哈希集的初始容量、loadFactor:链接哈希集的负载因子
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
// 构造一个新的空链接散列集,具有指定的初始容量和默认负载因子(0.75)。
// initialCapacity:LinkedHashSet的初始容量
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
// 构造一个新的空链接散列集,具有默认初始容量(16)和负载因子(0.75)。
public LinkedHashSet() {
super(16, .75f, true);
}
// 构造一个新的空链接散列集,具有默认初始容量(16)和负载因子(0.75)。
public LinkedHashSet() {
super(16, .75f, true);
}
// 构造具有与指定集合相同元素的新链接散列集。创建链接散列集时,其初始容量足以容纳指定集合中的元素和默认负载因子(0.75)。
// c:其元素将被放入此集合的集合
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
TreeSet
TreeSet底层是基于TreeMap实现的;
构造器
// 构造由指定的可导航映射支持的集合。
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
// 构造一个新的空树集,根据其元素的自然顺序进行排序。插入到集合中的所有元素都必须实现Comparable接口。
public TreeSet() {
this(new TreeMap<E,Object>());
}
// 构造一个新的空树集,根据指定的比较器排序。插入到集合中的所有元素必须由指定的比较器进行相互比较:comparator.compare(e1, e2)不能为集合中的任何e1和e2元素抛出ClassCastException。
// 如果用户试图向违反此约束的集合中添加元素,则add调用将抛出ClassCastException。
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
// 构造一个包含指定集合中的元素的新树集,根据其元素的自然顺序进行排序。插入到集合中的所有元素都必须实现Comparable接口。
// 此外,所有这样的元素必须是相互比较的:e1. compareto (e2)不能对集合中的任何元素e1和e2抛出ClassCastException。
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
// 构造一个包含相同元素并使用与指定排序集相同顺序的新树集。
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
add、remve
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
具体实现看TreeMap
三、Map
HashMap
成员变量
// 默认的初始容量-必须是2的幂。如果数组长度不是2的N次幂,则元素计算出的索引 特别容易相同,容易发生hash碰撞
// 导致数组其它空间很大程度上并没有存储数据,或者导致链表或者红黑树效率降低。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量,如果更高的值由任何一个构造函数用参数隐式指定,则使用该值。必须是二的幂<= 1<<30。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 在构造函数中没有指定时使用的负载因子。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 对于bin使用树而不是列表时的bin计数阈值。当向至少有这么多节点的bin中添加元素时,bin将转换为树。
// 该值必须大于2,并且应该至少为8,以便与树移除中关于收缩时转换回普通容器的假设相匹配。
static final int TREEIFY_THRESHOLD = 8;
// 在调整大小操作期间取消树化(分割)料仓的料仓数阈值。应小于TREEIFY_THRESHOLD,且最多为6以去除收缩检测相匹配。
static final int UNTREEIFY_THRESHOLD = 6;
// 可以对容器进行树化的最小表容量。(否则,如果一个bin中有太多的节点,则会调整表的大小。)
// 应该至少是4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间的冲突。
static final int MIN_TREEIFY_CAPACITY = 64;
// 表,在第一次使用时初始化,并根据需要调整大小。分配时,长度总是2的幂。(我们还允许某些操作的长度为0,以允许当前不需要的引导机制。)
transient Node<K,V>[] table;
// 保存缓存的entrySet()。注意,AbstractMap字段用于keySet()和values()。
transient Set<Map.Entry<K,V>> entrySet;
// 此映射中包含的键-值映射的数量。
transient int size;
// 阈值,元素达到此阈值则扩容
int threshold;
// 加载因子
final float loadFactor;
为什么HashMap链表长度超过8会转成树结构?
HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构”这个转换的前提是map的长度大于64才会发生转换,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
虽然红黑树的查找效率O(logN)高于链表O(N),但红黑树节点占用空间是链表节点的2倍。当链表长度较小时,即使全部遍历,效率也不会太差。所以需要找一种时间和空间都平衡的方法,根据泊松分布得知,当加载因子是0.75时,在同一个hash槽上链表长度达到8的可能性达到了最低(百万分之一)。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
HashMap的加载因子为什么是 0.75?
当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生 Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;
而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。
所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子
构造器
// 构造一个具有指定初始容量和负载因子的空HashMap。
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);
// 设置加载因子 和 返回给定目标容量的2次幂大小的临界值。 如指定容量给10那么返回16,给17返回32.
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 设置默认加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 将key的hash值高16位和低16位进行按位异或的运算,这样可以保证hash值的高低16位都可以参与运算,可以更好的保留hash值每一位数值的特征,从而可以更好的降低hash冲突。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 真正的添加操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果数组(hash表?)为空或长度为0,则进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过数组长度-1 与 计算得hash值做与运算得到存储得索引位置,如果该索引位置上元素为空,则直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 否则索引位置上有元素
Node<K,V> e; K k;
// 判断该索引位置上的元素hash值是否相等并且key是否一样,如果一样则对e做一个赋值
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);
// 判断链表长度是否达到临界值8,达到则进行链表转红黑树操作(树化操作感兴趣自己查看)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 遍历每个节点,对比当前插入节点与遍历到的节点的hash值是否一样并且内容是否相同,如果相同说明存在相同key的元素了则跳出链表元素比较。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 指向链表下一个节点
p = e;
}
}
// 如果e不等于空,则覆盖原来的value值,并且返回旧值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 条件永远成立,当前插入元素的value覆盖旧值
e.value = value;
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
++modCount;
// 元素+1,判断当前元素个数是否大于临界值,大于则进行扩容
if (++size > threshold)
resize();
// 该方法是留给子类实现的,不必理会
afterNodeInsertion(evict);
return null;
}
resize 扩容机制
// 扩容方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 原Node数组容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 原临界值
int oldThr = threshold;
// 新Node数组容量,新临界值
int newCap, newThr = 0;
// 如果原数组容量大于0,说明不是第一次扩容,数组中已经有值了。
if (oldCap > 0) {
// 判定Node数组是否已达到最大容量大小,若判定成功将不再扩容,直接将原Node数组返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 否则如果将旧容量扩容为原来的2倍后小于Node数组最大容量 且 旧容量大于等于默认初始容量,则将临界值也设置为原临界值的2倍(扩容阈值)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 否则如果初始化的时候用户传入了容量参数,则此时oldThr(原扩容阀值)大于0,那么将(原扩容阀值)oldThr 赋值给 (新Node数组的长度)newCap
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 如果初始化的时候用户什么都没传,那么此时oldCap和oldThr都==0:则
else { // zero initial threshold signifies using defaults
// 将默认初始容量(16)赋值作为数组容量
newCap = DEFAULT_INITIAL_CAPACITY;
// 将 加载因子(0.75)*默认初始容量(16) 作为数组扩容临界值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果初始化的时候用户传入了容量参数和负载因子或者只传入了容量参数,
//那么意味着oldCap==0、oldThr>0,那么上边的else里边是不会进入的,那么此时newThr(新的扩容阀值)仍然==0:
if (newThr == 0) {
//所以,将用户传入的容量参数 * 用户传入的负载因子loadFactor(也可能是默认的负载因子),将计算结果赋值给newThr(新的扩容阀值);
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将newThr(新的扩容阀值)赋给threshold临界值
threshold = newThr;
// 创建新的Node数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果原Node数组不为空,遍历原数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 如果该数组索引元素不为空
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果该索引元素的下一个节点为空,则说明该槽位只有一个元素(不是链表、不是红黑树)
if (e.next == null)
// 将该元素重新计算放到新数组中
newTab[e.hash & (newCap - 1)] = e;
// 否则判断是不是红黑树
else if (e instanceof TreeNode)
// 在新的Node数组中,将该红黑树进行拆分,(如果拆分后的子树过小(子树的节点小于等于6个),则取消树化,即将其转为链表结构)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 否则这是链表,对链表进行元素转移
else { // preserve order
// 两种类别的头、尾节点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表
do {
// 链表元素有两种可能:1 需要改变在数组中的位置、2 不需要改变在数组中的位置
next = e.next;
// 如果对元素索引位置进行重新计算结果等于0,则说明不需要改变位置
if ((e.hash & oldCap) == 0) {
// 如果loTail等于空,说明链表为空(当前节点属于第一个节点),设置头节点
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);
// 该Node节点所对应的数组下标不需要改变,直接把数组下标对应的节点指向新Node数组下标位置链表的头节点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 该Node节点所对应的数组下标需要改变,重新计算出所对应的数组下标值,然后指向新Node数组下标位置链表的头节点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回扩容后的数组
return newTab;
}
remove方法
// 根据key删除元素
public V remove(Object key) {
Node<K,V> e;
// 删除成功返回被删除元素的value值
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// 真正删除操作方法
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;
// 如果数组不为空 且 数组长度大于0 且 删除key的数组索引位置上节点不为空
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;
// 如果删除key在数组索引位置上的元素的hash值等于 删除key的hash值,且内容相同
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);
}
}
// 如果node不为空 且 [!matchValue为真(根据key删除时永远成立)或 满足其它几个条件之一]
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)
// 只有一个节点间接说明它后继节点为null,将当前删除位置设置为null(GC会回收?)
tab[index] = node.next;
// 链表?(不太看得懂了【狗头】)
else
// 将当删除节点的 前驱节点的后继节点 设置为被删除节点的后继节点
p.next = node.next;
++modCount;
// 元素-1
--size;
afterNodeRemoval(node);
// 返回被删除节点
return node;
}
}
return null;
}
Hashtable
TODO
//有时间再看
成员变量
构造器
put方法
LinkedHashMap
TODO
//有时间再看
成员变量
构造器
put方法
TreeMap
TODO
//有时间再看
成员变量
构造器
put方法