1. ArrayList のソースコード解析
1.1 ArrayList の基本原理
- 空のパラメーターで作成されたコレクションを使用して、elementData という名前のデフォルトの長さ 0 の配列が最下位レイヤーに作成されます。また、要素の数を記録するための最下位レベルの変数サイズもあります。
- 最初の要素を追加すると、長さ 10 の新しい配列が最下層に作成され、デフォルトの初期化値は空です。したがって、通常、ArrayList の基になる配列のデフォルトの長さは 10 であると考えられます。
- 基礎となるサイズ変数には 2 つの意味があります。1 つ目の意味は、現在の要素数 (コレクションの長さ) を表します。2 つ目は、次の格納場所を表します。サイズは、データが格納されるたびに増加します。
- コレクションの元の配列がいっぱいになって追加を続けると、配列は自動的に 1.5 倍に拡張され、この時点のサイズは 15 です。再び満杯になると、現在の長さの 1.5 倍まで拡張し続けます。
- 複数の要素を一度に追加し、1.5 倍に収まらない場合は、新しく作成される配列の長さは実際の長さに従うものとします。
1.2 ArrayList のソースコード解析
null パラメーター コンストラクターを使用して ArrayList コレクションを作成すると、ArrayList は下に長さ 0 の配列を作成します。
/**
* 当用空参构造创建ArrayList集合的时候,ArrayList在底层创建了一个长度为零的数组
*/
List<String> list = new ArrayList<>();
例えば、以下のソースコード解析では、ArrayListに分かりやすいソースコードをいくつか貼り付けています。
transient Object[] elementData;//集合底层的数组名
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
public ArrayList() {
//ArrayList空参构造
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
//这个就相当于,Object[] elementData = {};也是相当于创建了一个长度为零的数组的意思
}
list.add() を呼び出して最初の要素を追加すると、ArrayList はその下に長さ 10 の新しい配列を作成します。
内部のデフォルトの初期化値はすべて null です。
/**
* 当用空参构造创建ArrayList集合的时候,ArrayList在底层创建了一个长度为零的数组
*/
List<String> list = new ArrayList<>();
//当调用list.add()添加第一个元素的时候,ArrayList在底层创建了一个新的,长度为十的数组
list.add("nnn");
以下のソースコードステップの分析
transient Object[] elementData;//集合底层的数组名
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
private int size;//集合中元素个数,集合长度
private static final int DEFAULT_CAPACITY = 10;//默认初始容量
protected transient int modCount = 0;//记录的是list集合被修改的次数,例如每调用一次add方法,就会加一
public ArrayList() {
//ArrayList空参构造
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
//这个就相当于,Object[] elementData = {};创建了一个长度为零的数组
}
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//minCapacity为1
//这个minCapacity就是上面传下来的size,为1
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// Object[] elementData ={};
// private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//private static final int DEFAULT_CAPACITY = 10; int minCapacity = 1;
return Math.max(DEFAULT_CAPACITY, minCapacity);//俩者之间取最大值,返回10
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
//int minCapacity = 10;
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//minCapacity=10
// overflow-conscious code
int oldCapacity = elementData.length;//旧容量,0;
int newCapacity = oldCapacity + (oldCapacity >> 1);//这个>>相当于oldCapacity *1.5
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:
//创建一个新的长度为10的数组,把原来的元素拷贝进去
elementData = Arrays.copyOf(elementData, newCapacity);{
},10
}
1.3 ArrayList 展開メカニズム
展開ルール
-
ArrayList()
長さゼロの配列を使用します -
ArrayList(int initialCapacity)
指定された容量の配列が使用されます -
public ArrayList(Collection<? extends E> c)
c のサイズは配列の容量として使用されます -
add(Object o)
最初の拡張は 10 で、2 番目の拡張は前の容量の 1.5 倍です -
addAll(Collection c)
要素が無い場合はMath.max(実際の要素数の10)まで容量が拡張され、要素がある場合はMath.max(元の容量の1.5倍、実際の要素数)となります。
2. LinkedList ソースコード分析
2.1 LinkedList の概要
LinkedList は双方向循環リンク リスト (ソース コードから容易に確認できます) に基づいて実装されており、リンク リストとして動作するだけでなく、スタック、キュー、両端キューとしても使用できます。
LinkedList もスレッドセーフではないため、単一スレッドでの使用にのみ適しています。
LinkedList は Serializable インターフェイスを実装しているため、シリアル化をサポートし、シリアル化を通じて送信でき、Cloneable インターフェイスを実装し、クローンを作成できます。
2.2 LinkedList ソースコード分析
LinkedListのソースコードは以下の通りです(さらに詳細なコメントを追加しました)
package java.util;
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 链表的表头,表头不包含任何数据。Entry是个链表类数据结构。
private transient Entry<E> header = new Entry<E>(null, null, null);
// LinkedList中元素个数
private transient int size = 0;
// 默认构造函数:创建一个空的链表
public LinkedList() {
header.next = header.previous = header;
}
// 包含“集合”的构造函数:创建一个包含“集合”的LinkedList
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
// 获取LinkedList的第一个元素
public E getFirst() {
if (size==0)
throw new NoSuchElementException();
// 链表的表头header中不包含数据。
// 这里返回header所指下一个节点所包含的数据。
return header.next.element;
}
// 获取LinkedList的最后一个元素
public E getLast() {
if (size==0)
throw new NoSuchElementException();
// 由于LinkedList是双向链表;而表头header不包含数据。
// 因而,这里返回表头header的前一个节点所包含的数据。
return header.previous.element;
}
// 删除LinkedList的第一个元素
public E removeFirst() {
return remove(header.next);
}
// 删除LinkedList的最后一个元素
public E removeLast() {
return remove(header.previous);
}
// 将元素添加到LinkedList的起始位置
public void addFirst(E e) {
addBefore(e, header.next);
}
// 将元素添加到LinkedList的结束位置
public void addLast(E e) {
addBefore(e, header);
}
// 判断LinkedList是否包含元素(o)
public boolean contains(Object o) {
return indexOf(o) != -1;
}
// 返回LinkedList的大小
public int size() {
return size;
}
// 将元素(E)添加到LinkedList中
public boolean add(E e) {
// 将节点(节点数据是e)添加到表头(header)之前。
// 即,将节点添加到双向链表的末端。
addBefore(e, header);
return true;
}
// 从LinkedList中删除元素(o)
// 从链表开始查找,如存在元素(o)则删除该元素并返回true;
// 否则,返回false。
public boolean remove(Object o) {
if (o==null) {
// 若o为null的删除情况
for (Entry<E> e = header.next; e != header; e = e.next) {
if (e.element==null) {
remove(e);
return true;
}
}
} else {
// 若o不为null的删除情况
for (Entry<E> e = header.next; e != header; e = e.next) {
if (o.equals(e.element)) {
remove(e);
return true;
}
}
}
return false;
}
// 将“集合(c)”添加到LinkedList中。
// 实际上,是从双向链表的末尾开始,将“集合(c)”添加到双向链表中。
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
// 从双向链表的index开始,将“集合(c)”添加到双向链表中。
public boolean addAll(int index, Collection<? extends E> c) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Object[] a = c.toArray();
// 获取集合的长度
int numNew = a.length;
if (numNew==0)
return false;
modCount++;
// 设置“当前要插入节点的后一个节点”
Entry<E> successor = (index==size ? header : entry(index));
// 设置“当前要插入节点的前一个节点”
Entry<E> predecessor = successor.previous;
// 将集合(c)全部插入双向链表中
for (int i=0; i<numNew; i++) {
Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);
predecessor.next = e;
predecessor = e;
}
successor.previous = predecessor;
// 调整LinkedList的实际大小
size += numNew;
return true;
}
// 清空双向链表
public void clear() {
Entry<E> e = header.next;
// 从表头开始,逐个向后遍历;对遍历到的节点执行一下操作:
// (01) 设置前一个节点为null
// (02) 设置当前节点的内容为null
// (03) 设置后一个节点为“新的当前节点”
while (e != header) {
Entry<E> next = e.next;
e.next = e.previous = null;
e.element = null;
e = next;
}
header.next = header.previous = header;
// 设置大小为0
size = 0;
modCount++;
}
// 返回LinkedList指定位置的元素
public E get(int index) {
return entry(index).element;
}
// 设置index位置对应的节点的值为element
public E set(int index, E element) {
Entry<E> e = entry(index);
E oldVal = e.element;
e.element = element;
return oldVal;
}
// 在index前添加节点,且节点的值为element
public void add(int index, E element) {
addBefore(element, (index==size ? header : entry(index)));
}
// 删除index位置的节点
public E remove(int index) {
return remove(entry(index));
}
// 获取双向链表中指定位置的节点
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
// 获取index处的节点。
// 若index < 双向链表长度的1/2,则从前先后查找;
// 否则,从后向前查找。
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
// 从前向后查找,返回“值为对象(o)的节点对应的索引”
// 不存在就返回-1
public int indexOf(Object o) {
int index = 0;
if (o==null) {
for (Entry e = header.next; e != header; e = e.next) {
if (e.element==null)
return index;
index++;
}
} else {
for (Entry e = header.next; e != header; e = e.next) {
if (o.equals(e.element))
return index;
index++;
}
}
return -1;
}
// 从后向前查找,返回“值为对象(o)的节点对应的索引”
// 不存在就返回-1
public int lastIndexOf(Object o) {
int index = size;
if (o==null) {
for (Entry e = header.previous; e != header; e = e.previous) {
index--;
if (e.element==null)
return index;
}
} else {
for (Entry e = header.previous; e != header; e = e.previous) {
index--;
if (o.equals(e.element))
return index;
}
}
return -1;
}
// 返回第一个节点
// 若LinkedList的大小为0,则返回null
public E peek() {
if (size==0)
return null;
return getFirst();
}
// 返回第一个节点
// 若LinkedList的大小为0,则抛出异常
public E element() {
return getFirst();
}
// 删除并返回第一个节点
// 若LinkedList的大小为0,则返回null
public E poll() {
if (size==0)
return null;
return removeFirst();
}
// 将e添加双向链表末尾
public boolean offer(E e) {
return add(e);
}
// 将e添加双向链表开头
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
// 将e添加双向链表末尾
public boolean offerLast(E e) {
addLast(e);
return true;
}
// 返回第一个节点
// 若LinkedList的大小为0,则返回null
public E peekFirst() {
if (size==0)
return null;
return getFirst();
}
// 返回最后一个节点
// 若LinkedList的大小为0,则返回null
public E peekLast() {
if (size==0)
return null;
return getLast();
}
// 删除并返回第一个节点
// 若LinkedList的大小为0,则返回null
public E pollFirst() {
if (size==0)
return null;
return removeFirst();
}
// 删除并返回最后一个节点
// 若LinkedList的大小为0,则返回null
public E pollLast() {
if (size==0)
return null;
return removeLast();
}
// 将e插入到双向链表开头
public void push(E e) {
addFirst(e);
}
// 删除并返回第一个节点
public E pop() {
return removeFirst();
}
// 从LinkedList开始向后查找,删除第一个值为元素(o)的节点
// 从链表开始查找,如存在节点的值为元素(o)的节点,则删除该节点
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
// 从LinkedList末尾向前查找,删除第一个值为元素(o)的节点
// 从链表开始查找,如存在节点的值为元素(o)的节点,则删除该节点
public boolean removeLastOccurrence(Object o) {
if (o==null) {
for (Entry<E> e = header.previous; e != header; e = e.previous) {
if (e.element==null) {
remove(e);
return true;
}
}
} else {
for (Entry<E> e = header.previous; e != header; e = e.previous) {
if (o.equals(e.element)) {
remove(e);
return true;
}
}
}
return false;
}
// 返回“index到末尾的全部节点”对应的ListIterator对象(List迭代器)
public ListIterator<E> listIterator(int index) {
return new ListItr(index);
}
// List迭代器
private class ListItr implements ListIterator<E> {
// 上一次返回的节点
private Entry<E> lastReturned = header;
// 下一个节点
private Entry<E> next;
// 下一个节点对应的索引值
private int nextIndex;
// 期望的改变计数。用来实现fail-fast机制。
private int expectedModCount = modCount;
// 构造函数。
// 从index位置开始进行迭代
ListItr(int index) {
// index的有效性处理
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+size);
// 若 “index 小于 ‘双向链表长度的一半’”,则从第一个元素开始往后查找;
// 否则,从最后一个元素往前查找。
if (index < (size >> 1)) {
next = header.next;
for (nextIndex=0; nextIndex<index; nextIndex++)
next = next.next;
} else {
next = header;
for (nextIndex=size; nextIndex>index; nextIndex--)
next = next.previous;
}
}
// 是否存在下一个元素
public boolean hasNext() {
// 通过元素索引是否等于“双向链表大小”来判断是否达到最后。
return nextIndex != size;
}
// 获取下一个元素
public E next() {
checkForComodification();
if (nextIndex == size)
throw new NoSuchElementException();
lastReturned = next;
// next指向链表的下一个元素
next = next.next;
nextIndex++;
return lastReturned.element;
}
// 是否存在上一个元素
public boolean hasPrevious() {
// 通过元素索引是否等于0,来判断是否达到开头。
return nextIndex != 0;
}
// 获取上一个元素
public E previous() {
if (nextIndex == 0)
throw new NoSuchElementException();
// next指向链表的上一个元素
lastReturned = next = next.previous;
nextIndex--;
checkForComodification();
return lastReturned.element;
}
// 获取下一个元素的索引
public int nextIndex() {
return nextIndex;
}
// 获取上一个元素的索引
public int previousIndex() {
return nextIndex-1;
}
// 删除当前元素。
// 删除双向链表中的当前节点
public void remove() {
checkForComodification();
Entry<E> lastNext = lastReturned.next;
try {
LinkedList.this.remove(lastReturned);
} catch (NoSuchElementException e) {
throw new IllegalStateException();
}
if (next==lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = header;
expectedModCount++;
}
// 设置当前节点为e
public void set(E e) {
if (lastReturned == header)
throw new IllegalStateException();
checkForComodification();
lastReturned.element = e;
}
// 将e添加到当前节点的前面
public void add(E e) {
checkForComodification();
lastReturned = header;
addBefore(e, next);
nextIndex++;
expectedModCount++;
}
// 判断 “modCount和expectedModCount是否相等”,依次来实现fail-fast机制。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
// 双向链表的节点所对应的数据结构。
// 包含3部分:上一节点,下一节点,当前节点值。
private static class Entry<E> {
// 当前节点所包含的值
E element;
// 下一个节点
Entry<E> next;
// 上一个节点
Entry<E> previous;
/**
* 链表节点的构造函数。
* 参数说明:
* element —— 节点所包含的数据
* next —— 下一个节点
* previous —— 上一个节点
*/
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
// 将节点(节点数据是e)添加到entry节点之前。
private Entry<E> addBefore(E e, Entry<E> entry) {
// 新建节点newEntry,将newEntry插入到节点e之前;并且设置newEntry的数据是e
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
// 修改LinkedList大小
size++;
// 修改LinkedList的修改统计数:用来实现fail-fast机制。
modCount++;
return newEntry;
}
// 将节点从链表中删除
private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException();
E result = e.element;
e.previous.next = e.next;
e.next.previous = e.previous;
e.next = e.previous = null;
e.element = null;
size--;
modCount++;
return result;
}
// 反向迭代器
public Iterator<E> descendingIterator() {
return new DescendingIterator();
}
// 反向迭代器实现类。
private class DescendingIterator implements Iterator {
final ListItr itr = new ListItr(size());
// 反向迭代器是否下一个元素。
// 实际上是判断双向链表的当前节点是否达到开头
public boolean hasNext() {
return itr.hasPrevious();
}
// 反向迭代器获取下一个元素。
// 实际上是获取双向链表的前一个节点
public E next() {
return itr.previous();
}
// 删除当前节点
public void remove() {
itr.remove();
}
}
// 返回LinkedList的Object[]数组
public Object[] toArray() {
// 新建Object[]数组
Object[] result = new Object[size];
int i = 0;
// 将链表中所有节点的数据都添加到Object[]数组中
for (Entry<E> e = header.next; e != header; e = e.next)
result[i++] = e.element;
return result;
}
// 返回LinkedList的模板数组。所谓模板数组,即可以将T设为任意的数据类型
public <T> T[] toArray(T[] a) {
// 若数组a的大小 < LinkedList的元素个数(意味着数组a不能容纳LinkedList中全部元素)
// 则新建一个T[]数组,T[]的大小为LinkedList大小,并将该T[]赋值给a。
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
// 将链表中所有节点的数据都添加到数组a中
int i = 0;
Object[] result = a;
for (Entry<E> e = header.next; e != header; e = e.next)
result[i++] = e.element;
if (a.length > size)
a[size] = null;
return a;
}
// 克隆函数。返回LinkedList的克隆对象。
public Object clone() {
LinkedList<E> clone = null;
// 克隆一个LinkedList克隆对象
try {
clone = (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
// 新建LinkedList表头节点
clone.header = new Entry<E>(null, null, null);
clone.header.next = clone.header.previous = clone.header;
clone.size = 0;
clone.modCount = 0;
// 将链表中所有节点的数据都添加到克隆对象中
for (Entry<E> e = header.next; e != header; e = e.next)
clone.add(e.element);
return clone;
}
// java.io.Serializable的写入函数
// 将LinkedList的“容量,所有的元素值”都写入到输出流中
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// 写入“容量”
s.writeInt(size);
// 将链表中所有节点的数据都写入到输出流中
for (Entry e = header.next; e != header; e = e.next)
s.writeObject(e.element);
}
// java.io.Serializable的读取函数:根据写入方式反向读出
// 先将LinkedList的“容量”读出,然后将“所有的元素值”读出
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// 从输入流中读取“容量”
int size = s.readInt();
// 新建链表表头节点
header = new Entry<E>(null, null, null);
header.next = header.previous = header;
// 从输入流中将“所有的元素值”并逐个添加到链表中
for (int i=0; i<size; i++)
addBefore((E)s.readObject(), header);
}
}
2.3 いくつかの要約ポイント
LinkedList のソース コードに関して、重要な要約をいくつか示します。
1. ソース コードから明らかなように、LinkedList の実装は双方向循環リンク リストに基づいており、次の図に示すように、ヘッド ノードにはデータが格納されません。
2. 2 つの異なる構築方法に注目してください。引数なしの構築メソッドは、コレクション構築メソッドを含むヘッド ノードのみを含む空のリンク リストを直接作成します。最初に引数なしの構築メソッドを呼び出して空のリンク リストを作成し、次にコレクション内のデータを最後に追加します。リンクされたリストの。
3. 要素を検索して削除する場合、ソースコードは要素が null である場合と null でない場合に分けられますが、LinkedList では要素が null であっても構いません。
4. LinkedList はリンクされたリストに基づいて実装されているため、容量不足の問題が発生せず、容量を拡張する方法がありません。
5. ソース コード内の Entry<E>entry(int Index) メソッドに注目してください。このメソッドは、二重リンク リスト内の指定された位置にあるノードを返しますが、リンク リストには添字インデックスがありません。その位置の要素を指定するには、リンク リストをトラバースする必要があります。ソース コードの実装から、次のことがわかります。ここには加速アクションがあります。ソース コードでは、最初にインデックスを size の半分の長さと比較します。index<size/2 の場合、位置 0 からインデックスの位置までのみ移動します。index>size/2 の場合、位置 size から前方の位置までのみ移動します。 .インデックス。これにより、不必要なトラバーサルが削減され、一定の効率が向上します (実際には効率は依然として非常に低いです)。
6. リンクリストクラスに対応するデータ構造Entryに注目してください。次のように;
// 双向链表的节点所对应的数据结构。
// 包含3部分:上一节点,下一节点,当前节点值。
private static class Entry<E> {
// 当前节点所包含的值
E element;
// 下一个节点
Entry<E> next;
// 上一个节点
Entry<E> previous;
/**
* 链表节点的构造函数。
* 参数说明:
* element —— 节点所包含的数据
* next —— 下一个节点
* previous —— 上一个节点
*/
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
7. LinkedListはリンクリストをベースに実装されているため、挿入・削除効率は高いですが、検索効率は低いです(高速化作用はありますが)。
8. ソース コードにはスタックとキューの操作メソッドも実装されているため、スタック、キュー、両端キューとしても使用できることに注意してください。
3. LinkedHashMapのソースコード解析
3.1 LinkedHashMap の概要
LinkedHashMap は HashMap のサブクラスであり、HashMap と同じ記憶構造を持っていますが、二重リンク リストの先頭ノードを追加し、LinkedHashmap に入れられたすべてのノードを 1 つずつ二重循環リンク リストに文字列化し、ノードの挿入を保持します。 . 順序により、ノードの出力順序を入力順序と同じにすることができます。
LinkedHashMap を使用して LRU アルゴリズムを実装できます (これは以下のソース コードで分析されます)。
LinkedHashMap もスレッドセーフではないため、シングルスレッド環境でのみ使用できます。
3.2 LinkedHashMap のソースコード分析
LinkedHashMap のソースコードは次のとおりです (詳細なコメントを追加)。
package java.util;
import java.io.*;
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
private static final long serialVersionUID = 3801124242820219131L;
//双向循环链表的头结点,整个LinkedHashMap中只有一个header,
//它将哈希表中所有的Entry贯穿起来,header中不保存key-value对,只保存前后节点的引用
private transient Entry<K,V> header;
//双向链表中元素排序规则的标志位。
//accessOrder为false,表示按插入顺序排序
//accessOrder为true,表示按访问顺序排序
private final boolean accessOrder;
//调用HashMap的构造方法来构造底层的数组
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false; //链表中的元素默认按照插入顺序排序
}
//加载因子取默认的0.75f
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//加载因子取默认的0.75f,容量取默认的16
public LinkedHashMap() {
super();
accessOrder = false;
}
//含有子Map的构造方法,同样调用HashMap的对应的构造方法
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
//该构造方法可以指定链表中的元素排序的规则
public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//覆写父类的init()方法(HashMap中的init方法为空),
//该方法在父类的构造方法和Clone、readObject中在插入元素前被调用,
//初始化一个空的双向循环链表,头结点中不保存数据,头结点的下一个节点才开始保存数据。
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}
//覆写HashMap中的transfer方法,它在父类的resize方法中被调用,
//扩容后,将key-value对重新映射到新的newTable中
//覆写该方法的目的是为了提高复制的效率,
//这里充分利用双向循环链表的特点进行迭代,不用对底层的数组进行for循环。
void transfer(HashMap.Entry[] newTable) {
int newCapacity = newTable.length;
for (Entry<K,V> e = header.after; e != header; e = e.after) {
int index = indexFor(e.hash, newCapacity);
e.next = newTable[index];
newTable[index] = e;
}
}
//覆写HashMap中的containsValue方法,
//覆写该方法的目的同样是为了提高查询的效率,
//利用双向循环链表的特点进行查询,少了对数组的外层for循环
public boolean containsValue(Object value) {
// Overridden to take advantage of faster iterator
if (value==null) {
for (Entry e = header.after; e != header; e = e.after)
if (e.value==null)
return true;
} else {
for (Entry e = header.after; e != header; e = e.after)
if (value.equals(e.value))
return true;
}
return false;
}
//覆写HashMap中的get方法,通过getEntry方法获取Entry对象。
//注意这里的recordAccess方法,
//如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做,
//如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将e移到链表的末尾处。
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
//清空HashMap,并将双向链表还原为只有头结点的空链表
public void clear() {
super.clear();
header.before = header.after = header;
}
//Enty的数据结构,多了两个指向前后节点的引用
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
//调用父类的构造方法
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
//双向循环链表中,删除当前的Entry
private void remove() {
before.after = after;
after.before = before;
}
//双向循环立链表中,将当前的Entry插入到existingEntry的前面
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
//覆写HashMap中的recordAccess方法(HashMap中该方法为空),
//当调用父类的put方法,在发现插入的key已经存在时,会调用该方法,
//调用LinkedHashmap覆写的get方法时,也会调用到该方法,
//该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部,
//accessOrder为true时,get方法会调用recordAccess方法
//put方法在覆盖key-value对时也会调用recordAccess方法
//它们导致Entry最近使用,因此将其移到双向链表的末尾
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部,
//如果是按照插入的先后顺序排序,则不做任何事情。
if (lm.accessOrder) {
lm.modCount++;
//移除当前访问的Entry
remove();
//将当前访问的Entry插入到链表的尾部
addBefore(lm.header);
}
}
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
//迭代器
private abstract class LinkedHashIterator<T> implements Iterator<T> {
Entry<K,V> nextEntry = header.after;
Entry<K,V> lastReturned = null;
/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;
public boolean hasNext() {
return nextEntry != header;
}
public void remove() {
if (lastReturned == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
LinkedHashMap.this.remove(lastReturned.key);
lastReturned = null;
expectedModCount = modCount;
}
//从head的下一个节点开始迭代
Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (nextEntry == header)
throw new NoSuchElementException();
Entry<K,V> e = lastReturned = nextEntry;
nextEntry = e.after;
return e;
}
}
//key迭代器
private class KeyIterator extends LinkedHashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
//value迭代器
private class ValueIterator extends LinkedHashIterator<V> {
public V next() {
return nextEntry().value;
}
}
//Entry迭代器
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
// These Overrides alter the behavior of superclass view iterator() methods
Iterator<K> newKeyIterator() {
return new KeyIterator(); }
Iterator<V> newValueIterator() {
return new ValueIterator(); }
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator(); }
//覆写HashMap中的addEntry方法,LinkedHashmap并没有覆写HashMap中的put方法,
//而是覆写了put方法所调用的addEntry方法和recordAccess方法,
//put方法在插入的key已存在的情况下,会调用recordAccess方法,
//在插入的key不存在的情况下,要调用addEntry插入新的Entry
void addEntry(int hash, K key, V value, int bucketIndex) {
//创建新的Entry,并插入到LinkedHashMap中
createEntry(hash, key, value, bucketIndex);
//双向链表的第一个有效节点(header后的那个节点)为近期最少使用的节点
Entry<K,V> eldest = header.after;
//如果有必要,则删除掉该近期最少使用的节点,
//这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
//扩容到原来的2倍
if (size >= threshold)
resize(2 * table.length);
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//创建新的Entry,并将其插入到数组对应槽的单链表的头结点处,这点与HashMap中相同
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
//每次插入Entry时,都将其移到双向链表的尾部,
//这便会按照Entry插入LinkedHashMap的先后顺序来迭代元素,
//同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,符合LRU算法的实现
e.addBefore(header);
size++;
}
//该方法是用来被覆写的,一般如果用LinkedHashmap实现LRU算法,就要覆写该方法,
//比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中put
//Entry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
}
3.3 いくつかの要約ポイント
LinkedHashMap のソース コードに関して、次の重要な概要が示されています。
1. ソースコードからわかるように、LinkedHashMap にヘッドノードが追加され、LinkedHashMap に挿入されたすべてのエントリが、head をヘッドノードとして双方向循環リンクリストの末尾に挿入順に追加されます。
[外部リンク画像の転送に失敗しました。ソース サイトには盗難防止リンク メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-YlUzBgYY-1692927385674)(https://image.xiaoxiaofeng.site/blog/) 2023/05/18/xxf -20230518175942.png?xxfjava)]
1. 実際には、HashMap と LinkedList の 2 つのコレクション クラスのストレージ構造を組み合わせたものです。LinkedHashMapMap では、最初の図に示すように、すべての put エントリはハッシュ テーブルに格納されますが、head をヘッド ノードとする空の双方向循環リンク リストも定義されます。それをハッシュ テーブル内の対応する位置に挿入するだけでなく、二重循環リンク リストの末尾にも挿入します。
2. LinkedHashMap は HashMap を継承しているため、HashMap のすべての特性を備えており、キーと値を null にすることもできます。
3. ソース コードの accessOrder フラグに注目してください。これが false の場合、二重リンク リスト内の要素は、エントリが LinkedHashMap に挿入される順序に従ってソートされることを意味します。 LinkedHashMap へのデータは二重リンク リストの最後に配置されるため、二重リンク リストを走査するとき、Entry の出力順序は挿入順序と一致し、これは二重リンク リストのデフォルトの格納順序でもあります。 true の場合、二重リンクリスト内の要素がアクセス順に配置されていることを意味します。リンクリストにエントリが挿入される順序は、LinkedHashMap に配置された順序のままであることがわかります。ただし、put メソッドと get メソッドは両方とも、recordAccess メソッドを呼び出します (put メソッドは、キーが同じで元の Entry を上書きする場合に、recordAccess メソッドを呼び出します)。このメソッドは、accessOrder を決定します。これは true ですか? そうであれば、現在アクセスされている Entry (入ってくるエントリまたは出てくるエントリ)は二重リンクリストの最後に移動されます(キーが異なる場合、新しいエントリを置くときにaddEntryが呼び出され、それがcreatEntryを呼び出します。このメソッドはまた、新しいエントリを置きます)要素が二重リンクリストの最後に挿入されます。この時点でエントリにもアクセスされるため、挿入順序およびアクセス順序と一致します)。それ以外の場合は、何も行われません。
4. 構築メソッドに注意してください。最初の 4 つの構築メソッドはすべて、accessOrder を false に設定します。これは、デフォルトが挿入順序に従って並べ替えられることを示します。5 番目の構築メソッドは、受信する accessOrder の値をカスタマイズできるため、次のように指定できます。双方向循環リンク リスト要素の並べ替え規則では、通常、LinkedHashMap を使用して LRU アルゴリズムを実装し、この構築メソッドを使用して accessOrder を true に設定します。
5. LinkedHashMap は HashMap の put メソッドを上書きしませんが、put メソッド内で呼び出される addEntry メソッドと RecordAccess メソッドを上書きします。HashMap の put メソッドを見てみましょう。
// 将“key-value”添加到HashMap中
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
入れるEntryのキーがすでにハッシュテーブルに存在する場合はrecordAccessメソッドが呼び出され、キーが存在しない場合はaddEntryメソッドが呼び出され、新規Entryを単独リンクリストの先頭に挿入します。対応するスロット。
まず、recordAccess メソッドを見てみましょう。
//覆写HashMap中的recordAccess方法(HashMap中该方法为空),
//当调用父类的put方法,在发现插入的key已经存在时,会调用该方法,
//调用LinkedHashmap覆写的get方法时,也会调用到该方法,
//该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部,
//accessOrder为true时,get方法会调用recordAccess方法
//put方法在覆盖key-value对时也会调用recordAccess方法
//它们导致Entry最近使用,因此将其移到双向链表的末尾
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部,
//如果是按照插入的先后顺序排序,则不做任何事情。
if (lm.accessOrder) {
lm.modCount++;
//移除当前访问的Entry
remove();
//将当前访问的Entry插入到链表的尾部
addBefore(lm.header);
}
}
このメソッドは、accessOrder が true かどうかを判断します。true の場合、現在アクセスされているエントリ (ここでは put エントリ) を二重リンク リストの末尾に移動し、それによってアクセス順序に従って二重リンク リスト内の要素を並べ替えます。 ( 最後にアクセスしたエントリがリンク リストの最後に配置されます。これを何度も繰り返すと、前にある要素が最近アクセスされていない要素になります。 LRU アルゴリズムを実装する場合、エントリ内のノードの数が二重リンクされたリストが最大値に達すると、前の要素は削除されます。はい、前の要素が最も最近使用されていないためです)、それ以外の場合は何も行われません。
addEntry メソッドを見てみましょう。
//覆写HashMap中的addEntry方法,LinkedHashmap并没有覆写HashMap中的put方法,
//而是覆写了put方法所调用的addEntry方法和recordAccess方法,
//put方法在插入的key已存在的情况下,会调用recordAccess方法,
//在插入的key不存在的情况下,要调用addEntry插入新的Entry
void addEntry(int hash, K key, V value, int bucketIndex) {
//创建新的Entry,并插入到LinkedHashMap中
createEntry(hash, key, value, bucketIndex);
//双向链表的第一个有效节点(header后的那个节点)为近期最少使用的节点
Entry<K,V> eldest = header.after;
//如果有必要,则删除掉该近期最少使用的节点,
//这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
//扩容到原来的2倍
if (size >= threshold)
resize(2 * table.length);
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//创建新的Entry,并将其插入到数组对应槽的单链表的头结点处,这点与HashMap中相同
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
//每次插入Entry时,都将其移到双向链表的尾部,
//这便会按照Entry插入LinkedHashMap的先后顺序来迭代元素,
//同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,符合LRU算法的实现
e.addBefore(header);
size++;
}
新しいEntryは、テーブルの対応するスロットに対応する単連結リストの先頭ノードにも挿入されますが、createEntryでは、新たに置かれたEntryが二重連結リストの末尾にも挿入されることがわかります。挿入順序の観点から見ると、例えば、新しいエントリが二重リンクリストの最後に挿入される場合、エントリは挿入順序に従って反復されます。アクセス順序の観点から見ると、新しく配置されたエントリが最も多くなります。最近アクセスしたエントリなので、二重リンク リストの最後にも配置する必要があります。
上記には次のような RemoveEldestEntry メソッドもあります。
//该方法是用来被覆写的,一般如果用LinkedHashmap实现LRU算法,就要覆写该方法,
//比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中put
//Entry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
このメソッドはデフォルトで false を返します。通常、LinkedHashMap を使用して LRU アルゴリズムを実装するときに、このメソッドをオーバーライドします。一般的な実装では、設定されたメモリ (ここではノード数を指します) が最大値に達すると true を返すため、新しいメモリが追加されます。エントリ (エントリのキーがハッシュ テーブルにまだ存在していない) の場合、removeEntryForKey メソッドが呼び出され、最も最近使用されていないノード (ヘッドの後のノードは実際には最近使用されていません) が削除されます。
6. LinkedHashMap は HashMap の get メソッドをオーバーライドします。
//覆写HashMap中的get方法,通过getEntry方法获取Entry对象。
//注意这里的recordAccess方法,
//如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做,
//如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将e移到链表的末尾处。
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
最初に Entry を取得します。null でない場合は、recordAccess メソッドも呼び出します。これについては上で明確に説明されているため、ここではこれ以上説明しません。
7. 最後に、LinkedHashMap が LRU を実装する方法について話しましょう。まず、accessOrder が true の場合、アクセス順序によるソート モードが有効になり、LRU アルゴリズムの実装に使用できるようになります。put メソッドであっても get メソッドであっても、対象の Entry は最後にアクセスされた Entry となるため、Entry は二重リンクリストの末尾に追加されることがわかります (get メソッドは、recordAccess メソッドを呼び出すことによって実装されます)。 、putメソッドは既存のものを上書きします(キーの場合はrecordAccessメソッドを呼び出すことで実現します。新しいEntryを挿入する場合はcreateEntryのaddBeforeメソッドで実現します)ので、最後に使用したEntryが配置されます。二重リンクリストの最後尾のエントリを複数回操作後、二重リンクリストの先頭のエントリは最近使用されていないため、ノード数がいっぱいになった場合、最初のエントリ(次のエントリ)が削除されます。 head) は、最も最近使用されていないエントリになります。
4. HashMapのソースコード(JDK7)の解析
4.1 ハッシュマップの概要
HashMap はハッシュ テーブルに基づいて実装されます。各要素はキーと値のペアです。競合は単一リンク リストを通じて内部で解決されます。容量が不十分な場合 (しきい値を超える場合)、容量も自動的に拡張されます。
HashMap はスレッドセーフではなく、シングルスレッド環境でのみ使用されます。マルチスレッド環境では、concurrent パッケージの concurrentHashMap を使用できます。
HashMap は Serializable インターフェイスを実装しているため、シリアル化をサポートし、Cloneable インターフェイスを実装しており、クローンを作成できます。
4.2 HashMap に関する 4 つの懸念に対する回答
フォーカスポイント | 結論は |
---|---|
HashMap は空を許可しますか | キーと値は両方とも空にすることができます |
HashMap ではデータの重複は許可されますか? | キーの重複は上書きされます、値は重複を許可します |
HashMap は注文されていますか? | 順序なし: 具体的には、この障害は、HashMap を走査するときに、取得された要素の順序が put の順序になることが基本的に不可能であるという事実を指します。 |
HashMap はスレッドセーフですか? | スレッドセーフではありません |
4.3 HashMap のソースコード分析
HashMap のソース コードは次のとおりです (詳細なコメントが追加されています)。
package java.util;
import java.io.*;
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
// 默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必须是2的整数次幂。
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储数据的Entry数组,长度是2的幂。
// HashMap采用链表法解决冲突,每一个Entry本质上是一个单向链表
transient Entry[] table;
// HashMap的底层数组中已用槽的数量
transient int size;
// HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
int threshold;
// 加载因子实际大小
final float loadFactor;
// HashMap被改变的次数
transient volatile int modCount;
// 指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// HashMap的最大容量只能是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因此不能小于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 找出“大于initialCapacity”的最小的2的幂
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
// 设置“加载因子”
this.loadFactor = loadFactor;
// 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
threshold = (int)(capacity * loadFactor);
// 创建Entry数组,用来保存数据
table = new Entry[capacity];
init();
}
// 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 默认构造函数。
public HashMap() {
// 设置“加载因子”为默认加载因子0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
// 创建Entry数组,用来保存数据
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
// 包含“子Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 将m中的全部元素逐个添加到HashMap中
putAllForCreate(m);
}
//求hash值的方法,重新计算hash值
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 返回h在数组中的索引值,这里用&代替取模,旨在提升效率
// h & (length-1)保证返回值的小于length
static int indexFor(int h, int length) {
return h & (length-1);
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
// 获取key对应的value
public V get(Object key) {
if (key == null)
return getForNullKey();
// 获取key的hash值
int hash = hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值等于key”的元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//判断key是否相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
//没找到则返回null
return null;
}
// 获取“key为null”的元素的值
// HashMap将“key为null”的元素存储在table[0]位置,但不一定是该链表的第一个位置!
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
// HashMap是否包含key
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
// 返回“键为key”的键值对
final Entry<K,V> getEntry(Object key) {
// 获取哈希值
// HashMap将“key为null”的元素存储在table[0]位置,“key不为null”的则调用hash()计算哈希值
int hash = (key == null) ? 0 : hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值等于key”的元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
// 将“key-value”添加到HashMap中
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
// 将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
// putForNullKey()的作用是将“key为null”键值对添加到table[0]位置
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果没有存在key为null的键值对,则直接题阿见到table[0]处!
modCount++;
addEntry(0, null, value, 0);
return null;
}
// 创建HashMap对应的“添加方法”,
// 它和put()不同。putForCreate()是内部方法,它被构造函数等调用,用来创建HashMap
// 而put()是对外提供的往HashMap中添加元素的方法。
private void putForCreate(K key, V value) {
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
// 若该HashMap表中存在“键值等于key”的元素,则替换该元素的value值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
// 若该HashMap表中不存在“键值等于key”的元素,则将该key-value添加到HashMap中
createEntry(hash, key, value, i);
}
// 将“m”中的全部元素都添加到HashMap中。
// 该方法被内部的构造HashMap的方法所调用。
private void putAllForCreate(Map<? extends K, ? extends V> m) {
// 利用迭代器将元素逐个添加到HashMap中
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
putForCreate(e.getKey(), e.getValue());
}
}
// 重新调整HashMap的大小,newCapacity是调整后的容量
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果就容量已经达到了最大值,则不能再扩容,直接返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
// 然后,将“新HashMap”赋值给“旧HashMap”。
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
// 将HashMap中的全部元素都添加到newTable中
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
// 将"m"的全部元素都添加到HashMap中
public void putAll(Map<? extends K, ? extends V> m) {
// 有效性判断
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;
// 计算容量是否足够,
// 若“当前阀值容量 < 需要的容量”,则将容量x2。
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length)
resize(newCapacity);
}
// 通过迭代器,将“m”中的元素逐个添加到HashMap中。
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
put(e.getKey(), e.getValue());
}
}
// 删除“键为key”元素
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
// 删除“键为key”的元素
final Entry<K,V> removeEntryForKey(Object key) {
// 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
// 删除链表中“键为key”的元素
// 本质是“删除单向链表中的节点”
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
// 删除“键值对”
final Entry<K,V> removeMapping(Object o) {
if (!(o instanceof Map.Entry))
return null;
Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
Object key = entry.getKey();
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
// 删除链表中的“键值对e”
// 本质是“删除单向链表中的节点”
while (e != null) {
Entry<K,V> next = e.next;
if (e.hash == hash && e.equals(entry)) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
// 清空HashMap,将所有的元素设为null
public void clear() {
modCount++;
Entry[] tab = table;
for (int i = 0; i < tab.length; i++)
tab[i] = null;
size = 0;
}
// 是否包含“值为value”的元素
public boolean containsValue(Object value) {
// 若“value为null”,则调用containsNullValue()查找
if (value == null)
return containsNullValue();
// 若“value不为null”,则查找HashMap中是否有值为value的节点。
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
// 是否包含null值
private boolean containsNullValue() {
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (e.value == null)
return true;
return false;
}
// 克隆一个HashMap,并返回Object对象
public Object clone() {
HashMap<K,V> result = null;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// assert false;
}
result.table = new Entry[table.length];
result.entrySet = null;
result.modCount = 0;
result.size = 0;
result.init();
// 调用putAllForCreate()将全部元素添加到HashMap中
result.putAllForCreate(this);
return result;
}
// Entry是单向链表。
// 它是 “HashMap链式存储法”对应的链表。
// 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
// 指向下一个节点
Entry<K,V> next;
final int hash;
// 构造函数。
// 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断两个Entry是否相等
// 若两个Entry的“key”和“value”都相等,则返回true。
// 否则,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 实现hashCode()
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
// 当向HashMap中添加元素时,绘调用recordAccess()。
// 这里不做任何处理
void recordAccess(HashMap<K,V> m) {
}
// 当从HashMap中删除元素时,绘调用recordRemoval()。
// 这里不做任何处理
void recordRemoval(HashMap<K,V> m) {
}
}
// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex];
// 设置“bucketIndex”位置的元素为“新Entry”,
// 设置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}
// 创建Entry。将“key-value”插入指定位置。
void createEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex];
// 设置“bucketIndex”位置的元素为“新Entry”,
// 设置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
size++;
}
// HashIterator是HashMap迭代器的抽象出来的父类,实现了公共了函数。
// 它包含“key迭代器(KeyIterator)”、“Value迭代器(ValueIterator)”和“Entry迭代器(EntryIterator)”3个子类。
private abstract class HashIterator<E> implements Iterator<E> {
// 下一个元素
Entry<K,V> next;
// expectedModCount用于实现fast-fail机制。
int expectedModCount;
// 当前索引
int index;
// 当前元素
Entry<K,V> current;
HashIterator() {
expectedModCount = modCount;
if (size > 0) {
// advance to first entry
Entry[] t = table;
// 将next指向table中第一个不为null的元素。
// 这里利用了index的初始值为0,从0开始依次向后遍历,直到找到不为null的元素就退出循环。
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
// 获取下一个元素
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
// 注意!!!
// 一个Entry就是一个单向链表
// 若该Entry的下一个节点不为空,就将next指向下一个节点;
// 否则,将next指向下一个链表(也是下一个Entry)的不为null的节点。
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
// 删除当前元素
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
}
// value的迭代器
private final class ValueIterator extends HashIterator<V> {
public V next() {
return nextEntry().value;
}
}
// key的迭代器
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
// Entry的迭代器
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
// 返回一个“key迭代器”
Iterator<K> newKeyIterator() {
return new KeyIterator();
}
// 返回一个“value迭代器”
Iterator<V> newValueIterator() {
return new ValueIterator();
}
// 返回一个“entry迭代器”
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
}
// HashMap的Entry对应的集合
private transient Set<Map.Entry<K,V>> entrySet = null;
// 返回“key的集合”,实际上返回一个“KeySet对象”
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
// Key对应的集合
// KeySet继承于AbstractSet,说明该集合中没有重复的Key。
private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}
// 返回“value集合”,实际上返回的是一个Values对象
public Collection<V> values() {
Collection<V> vs = values;
return (vs != null ? vs : (values = new Values()));
}
// “value集合”
// Values继承于AbstractCollection,不同于“KeySet继承于AbstractSet”,
// Values中的元素能够重复。因为不同的key可以指向相同的value。
private final class Values extends AbstractCollection<V> {
public Iterator<V> iterator() {
return newValueIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsValue(o);
}
public void clear() {
HashMap.this.clear();
}
}
// 返回“HashMap的Entry集合”
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
// 返回“HashMap的Entry集合”,它实际是返回一个EntrySet对象
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
}
// EntrySet对应的集合
// EntrySet继承于AbstractSet,说明该集合中没有重复的EntrySet。
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<K,V> e = (Map.Entry<K,V>) o;
Entry<K,V> candidate = getEntry(e.getKey());
return candidate != null && candidate.equals(e);
}
public boolean remove(Object o) {
return removeMapping(o) != null;
}
public int size() {
return size;
}
public void clear() {
HashMap.this.clear();
}
}
// java.io.Serializable的写入函数
// 将HashMap的“总的容量,实际容量,所有的Entry”都写入到输出流中
private void writeObject(java.io.ObjectOutputStream s)
throws IOException
{
Iterator<Map.Entry<K,V>> i =
(size > 0) ? entrySet0().iterator() : null;
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
// Write out number of buckets
s.writeInt(table.length);
// Write out size (number of Mappings)
s.writeInt(size);
// Write out keys and values (alternating)
if (i != null) {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
s.writeObject(e.getKey());
s.writeObject(e.getValue());
}
}
}
private static final long serialVersionUID = 362498820763181265L;
// java.io.Serializable的读取函数:根据写入方式读出
// 将HashMap的“总的容量,实际容量,所有的Entry”依次读出
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold, loadfactor, and any hidden stuff
s.defaultReadObject();
// Read in number of buckets and allocate the bucket array;
int numBuckets = s.readInt();
table = new Entry[numBuckets];
init(); // Give subclass a chance to do its thing.
// Read in size (number of Mappings)
int size = s.readInt();
// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<size; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}
// 返回“HashMap总的容量”
int capacity() {
return table.length; }
// 返回“HashMap的加载因子”
float loadFactor() {
return loadFactor; }
}
4.4 いくつかの要約ポイント
1. まず、次の図に示すように、HashMap のストレージ構造を理解する必要があります。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムが組み込まれている可能性があります。画像を保存して直接アップロードすることをお勧めします (img-Aoj2barK-1692927385674)(https://image.xiaoxiaofeng.site/blog/2023) /05/18/xxf -20230518180052.png?xxfjava)]
図では、紫色の部分はハッシュ テーブル (ハッシュ配列とも呼ばれます) を表します。配列の各要素は、単一リンク リストのヘッド ノードです。リンク リストは、競合を解決するために使用されます。異なるキーが同じキーにマップされている場合、配列内の位置を指定して、それを単一リンクリストに追加します。
2. まず、リンク リスト内のノードのデータ構造を確認します。
// Entry是单向链表。
// 它是 “HashMap链式存储法”对应的链表。
// 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
// 指向下一个节点
Entry<K,V> next;
final int hash;
// 构造函数。
// 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断两个Entry是否相等
// 若两个Entry的“key”和“value”都相等,则返回true。
// 否则,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 实现hashCode()
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
// 当向HashMap中添加元素时,绘调用recordAccess()。
// 这里不做任何处理
void recordAccess(HashMap<K,V> m) {
}
// 当从HashMap中删除元素时,绘调用recordRemoval()。
// 这里不做任何处理
void recordRemoval(HashMap<K,V> m) {
}
}
その構造要素には、キー、値、ハッシュに加えて、次のノードを指す next も含まれます。さらに、ここでは、キーと値のペアが一意であることを保証するために、equals メソッドと hashCode メソッドがオーバーライドされます。
3. HashMap には 4 つの構築方法があります。建設方法では、初期容量と荷重係数という 2 つの非常に重要なパラメーターが言及されています。これら 2 つのパラメータは、HashMap のパフォーマンスに影響を与える重要なパラメータです。容量は、ハッシュ テーブル内のスロットの数 (つまり、ハッシュ配列の長さ) を表します。初期容量は、ハッシュ テーブルが作成されたときの容量です (コンストラクターから確認できます。指定されていない場合は、デフォルトで 16) 負荷係数は、ハッシュ テーブルの容量が自動的に増加する前に、ハッシュ テーブルがどの程度満たされるかを示す尺度です。ハッシュ テーブル内のエントリの数が積を超えると、負荷率と電流容量を考慮した場合、ハッシュ テーブルのサイズを変更する (つまり、拡張する) 必要があります。
負荷率について話しましょう。負荷率が大きければ、スペースはより完全に利用されますが、検索効率は低下します (リンクされたリストの長さがますます長くなります)。負荷率が小さすぎると、検索効率が低下します。 、テーブル内のデータがあまりにもまばらになり (スペースが使用される前に拡張されることが多くなります)、スペースの重大な無駄が発生します。構築方法で指定しない場合、システムのデフォルトの荷重係数は理想値である 0.75 であり、通常はこれを変更する必要はありません。
また、いくら容量を指定しても実際の容量は工法上指定容量の2の乗以上となり、最大値は2の30乗を超えることはできません。
4. HashMap のキーと値は両方とも null にすることができます。
5. HashMap で最も一般的に使用される 2 つのメソッド、put と get の分析に重点を置きます。より単純な get メソッドから始めましょう。ソース コードは次のとおりです。
// 获取key对应的value
public V get(Object key) {
if (key == null)
return getForNullKey();
// 获取key的hash值
int hash = hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值等于key”的元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
/判断key是否相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
没找到则返回null
return null;
}
// 获取“key为null”的元素的值
// HashMap将“key为null”的元素存储在table[0]位置,但不一定是该链表的第一个位置!
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
まず、キーがnullの場合、ハッシュテーブルの先頭位置table[0]に対応する連結リストから直接検索します。キーが null のキーと値のペアは常に table[0] をヘッド ノードとしてリンク リストに配置され、もちろん必ずしもヘッド ノード table[0] に格納されるわけではないことに注意してください。
キーが null でない場合は、まずキーのハッシュ値を検索し、ハッシュ値に従ってテーブル内のインデックスを検索し、インデックスに対応する単一リンク リスト内でキーが null であるキーと値のペアがあるかどうかを検索します。ターゲットキーと等しい場合は、対応する値を返します。そうでない場合は、null を返します。
put メソッドは少し複雑で、コードは次のとおりです。
// 将“key-value”添加到HashMap中
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
キーが null の場合、table[0] に対応するリンク リストに追加します。putForNullKey のソース コードは次のとおりです。
// putForNullKey()的作用是将“key为null”键值对添加到table[0]位置
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果没有存在key为null的键值对,则直接题阿见到table[0]处!
modCount++;
addEntry(0, null, value, 0);
return null;
}
キーが null でない場合は、最初にキーのハッシュ値も検索され、そのハッシュ値に基づいてテーブル内のインデックスが取得され、その後、対応する単一リンク リストが走査されます。単一リンク リストのターゲット キーと等しい場合、新しい値が古い値を上書きし、古い値が返されます。ターゲット キーと等しいキーと値のペアが見つからない場合、または単一リンク リストが空の場合、キーと値のペアが単一リンク リストのヘッド ノードに挿入されます (新しく挿入された各ノードはヘッド ノードの位置に配置されます)、この操作は addEntry メソッドによって実装され、そのソース コードは次のとおりです。
// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex];
// 设置“bucketIndex”位置的元素为“新Entry”,
// 设置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}
ここで最後から 3 行目の構築方法に注目し、キーと値のキーと値のペアを table[bucketIndex] に割り当て、その次を要素 e にポイントします。これにより、キーと値がヘッド ノードに配置され、前のヘッダー その後ろにノードが接続されています。このメソッドは、キーと値のペアが配置されるたびに、新しいキーと値のペアが常に table[bucketIndex] (つまり、ヘッド ノード) に配置されることも示しています。
コードの最後の 2 行にも注意してください。キーと値のペアを追加するたびに、現在使用されているスロットの数がしきい値 (容量 * 負荷率) 以上であるかどうかを判断する必要があります。値以上の場合、容量を元の2倍の容量に拡張します。
6.拡張について。上記では展開メソッドとサイズ変更メソッドについて説明しました。そのソースコードは次のとおりです。
// 重新调整HashMap的大小,newCapacity是调整后的单位
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
// 然后,将“新HashMap”赋值给“旧HashMap”。
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
明らかに、HashMap の新しい基になる配列が作成され、次に Transfer メソッドが呼び出されて、HashMap のすべての要素が新しい HashMap に追加されます (新しい配列内の要素のインデックス位置を再計算する必要があります)。転送メソッドのソースコードは以下のとおりです。
// 将HashMap中的全部元素都添加到newTable中
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
明らかに、拡張は、新しい配列内のこれらの要素の位置を再計算し、コピー処理を実行する必要があるため、かなり時間のかかる操作です。したがって、HashMap を使用する場合は、HashMap のパフォーマンスを向上させるために、事前に HashMap 内の要素の数を見積もることが最善です。
7. containsKey メソッドと containsValue メソッドに注目してください。前者はキーのハッシュ値から指定したインデックスに対応する連結リストまでを直接検索範囲とすることができるのに対し、後者はハッシュ配列の各連結リストを検索することができる。
8. ハッシュ値とインデックス値の計算方法の分析に焦点を当てましょう。これら 2 つの方法は HashMap 設計の中核部分であり、この 2 つの方法を組み合わせることで、ハッシュ テーブル内の要素が可能な限り均等にハッシュされるようにすることができます。
ハッシュ値の計算方法は次のとおりです。
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
これは単なる数式です。IDK がこのようにハッシュ値を計算する設計には、当然の利点があります。なぜこのように設計されているかについては、ここでは調査しません。使用されているビット演算がハッシュ値の計算は非常に効率的です。
ハッシュ値から対応するインデックスを見つける方法は次のとおりです。
static int indexFor(int h, int length) {
return h & (length-1);
}
ここに焦点を当てたいと思います. ハッシュ テーブルをハッシュするとき, 必然的にハッシュ値を使用して長さを剰余する (つまり除算ハッシュ法) ことを考えることになります. これは Hashtable にも実装されています. この方法は基本的に次のことを保証できます.要素 ハッシュ テーブル内のハッシュは比較的均一ですが、モジュラスでは除算演算が使用されるため、非常に非効率的です。HashMap では、モジュラスの代わりに h&(length-1) メソッドが使用され、これによっても均一なハッシュが実現されますが、効率ははるかに高く、これも Hashtable よりも HashMap の改良点です。
次に、ハッシュ テーブルの容量が 2 の整数乗でなければならない理由を分析してみましょう。まず第一に、 length が 2 の整数乗である場合、 h&(length-1) はモジュロ長と同等であり、均一なハッシュが保証され、効率が向上します。第 2 に、 length が 2 の整数乗である場合、 は偶数であるため、 length-1 は奇数で、奇数の最後の桁は 1 です。これにより、h&(length-1) の最後の桁が (h の値に応じて) 0 または 1 になることが保証されます。 AND の後の結果は偶数または奇数になる可能性があり、これによりハッシュの均一性が保証されます。長さが奇数の場合、長さ-1 が偶数であることは明らかであり、その最後のビットは 0 であるため、h&( length-1) の最後のビットは 0 である必要があります。つまり、偶数のみにすることができます。この方法では、ハッシュ値は配列の添字の偶数位置にのみハッシュされ、スペースのほぼ半分が無駄になります。したがって、長さの 2 の整数乗は、ハッシュ テーブル内で要素を均等にハッシュできるように、異なるハッシュ値の衝突の確率を小さくするためです。
5. HashMapのソースコード解析(JDK8)
5.1 HashMap の基礎となるデータ構造
HashMap の基礎となるデータ構造は配列 + チェーンです。以下に示すように:
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-892DV2KL-1692927385675)(https://image.xiaoxiaofeng.site/spider/2023) /7/20/xxf -1689833106507.png)]
次の 2 つの条件が満たされると、リンク リストは赤黒ツリーに変換されます。
1. 配列の長さが 64 以上である
2. リンクされたリストの長さが 8 以上である
配列の長さが 64 未満で、リンク リストの長さが 8 以上の場合、リンク リストは赤黒ツリーに変換されず、拡張されます。拡張により、リンクされたリストの長さも短縮される可能性があります。
5.2 HashMap のいくつかの重要なメンバー変数
// 底层数组,可自动扩容,但是HashMap不支持缩容,长度总是2的N次方
transient Node<K,V>[] table;
// 初始容量大小,1左移4位结果是10000,转为十进制是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 同时满足“数组长度等于或大于64”、“链表长度等于或大于8” 两个条件,才将链表转为红黑树
*/
// 树化阀值
static final int TREEIFY_THRESHOLD = 8;
// 最小树化容量(树化是指将链表转为红黑树)
static final int MIN_TREEIFY_CAPACITY = 64;
// HashMap的数组最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
// 扩容的阈值
int threshold;
// 负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
5.3 HashMap コンストラクター
HashMap には 4 つのコンストラクターがあります。説明するために public HashMap(intInitialCapacity, floatloadFactor) を選択します。
/**
* 构造函数解析
*/
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);
// 设置负载因子,默认是 0.75
this.loadFactor = loadFactor;
// 设置扩容阀值
// initialCapacity(初始容量大小)默认是16
// 用户设置的initialCapacity可以是任何大于0的数字,tableSizeFor(initialCapacity)返回结果是2的N次方。即HashMap的容量必然是2的N次方
this.threshold = tableSizeFor(initialCapacity);
}
/**
* tableSizeFor(int cap)方法解析
* 返回值大于等于cap,且一定是2的次方数
*
* 假设 cap = 10
* n = 10 - 1 => 9 => 0b1001(0b表示二进制数)
* n |= n >>> 1; 表示 n 等于 n 或上 n右移一位
* 0b1001 | 0b0100 => 0b1101 // n |= n >>> 1;
* 0b1101 | 0b0010 => 0b1111 // n |= n >>> 2;
* 0b1111 | 0b0100 => 0b1111 // n |= n >>> 4;
* 以此类推,最终 n = 15
*
* return 16
*/
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;
}
5.4 put(Kキー,V値)メソッド
HashMapのリンクリストNodeのデータ構造は以下の通りです
/**
* 链表的Node
*/
static class Node<K,V> implements Map.Entry<K,V> {
// key的hash值
final int hash;
// key
final K key;
// value
V value;
// 下一个元素
HashMap.Node<K, V> next;
}
ステップ2 hash(オブジェクトキー)メソッドのソースコード解釈
/**
* 如果key是null,则返回0
* 如果key不是null,则使用 key的hashCode 异或 key的hashCode右移16位
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
キーの hashCode を直接返すのではなく、それを 16 ビット右にシフトして、「XOR」結果を取得するのはなぜでしょうか。これは、キーのルーティング式 i = (table.length - 1) & node.hash に関連しています。
Java オブジェクトの hashCode() メソッドは int を返しますが、その int は 32 ビットを占有します。つまり、int は 32 ビットの 2 進数表現に変換できます。キーの hashCode が直接使用されると仮定すると、
ハッシュコード = 1111 0101 1100 0100 1111 0001 1101 0011
hashCodeをルーティング式に組み込む
i = (テーブルの長さ - 1) & 1111 0101 1100 0100 1111 0001 1101 0011
table.length は 2 の N 乗である必要があり、table.length - 1 をバイナリに変換した結果は、上位ビットがすべて 0、下位ビットがすべて 1 である必要があります。table.length が比較的小さい場合、たとえば table.length = 1024、table.length - 1 = 1023、1023 をバイナリに変換すると、0000 0000 000 0000 0000 0011 1111 1111 となります。
0000 0000 000 0000 0000 0011 1111 1111 1111 何か特別なことですか?0 と任意の数値の間の「AND」演算の結果は 0 であり、1 と任意の数値の間の「AND」演算の結果は変更されないため、キー ルーティング式ではハッシュ コードの低い値のみが使用され、ハッシュ コードは使用されません。高い値。特に、table.length が小さいほど、使用できる hashCode 桁数が少なくなります。高レベルの hashCode を使用するために、HashMap の作成者はこの操作 (h = key.hashCode()) ^ (h >>> 16) を実行し、hashCode と 16 ビット右シフトした hashCode の結果を実行させます。 XOR" " 演算により、下位 16 ビット データが上位 16 ビット データと混合され、下位 16 ビット データがよりハッシュ化されます。key.hash()で返される値の下位16ビットには、ルーティング式i = (table.length - 1) & node.hashの結果であるkey.hashCodeの情報がすべて混合されていると考えることもできます。私はよりハッシュ化されます。
Put(K key, V value) メソッドのソース コード分析。メソッドは内部で putVal(int hash, K key, V value, booleanonlyIfAbsent, boolean evict) メソッドを呼び出します。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* putVal方法分析
* @param hash key的hash值
* @param key key
* @param value value
* @param onlyIfAbsent key已经存在,是否改变value。如果为true,则不更改现有值;为false,修改value
* @param evict 如果为false,则表处于创建模式
* @return
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// tab: 引用当前HashMap的数组
// p: 数组的元素
// n:数组的长度
// i: 路由寻址的结果
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
// 执行 new HashMap() 的时候并不会创建数组,节约内存,等首次插入键值对,才创建数组,这属于延迟初始化,所以会有table==null的判断
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// i = (n - 1) & hash 是key路由公式,tab[i = (n - 1) & hash] 找到key在数组中的位置
// 如果 tab[i] == null 证明当前位置还没有键值对,创建Node放到tab[i]中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// tab[i]中已经有Node了
else {
// e: 一个临时的Node
// k: 一个临时的key
HashMap.Node<K,V> e; K k;
// key比较,桶位中的第一个元素与插入的key完全一致的情况
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// e后续要进行替换操作
e = p;
// p instanceof TreeNode 桶位是红黑树的情况
else if (p instanceof TreeNode)
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// tab[i]是链表,并且链表第一个元素key与插入的key不一致
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// (e = p.next) == null 表示迭代到了最后的元素
if ((e = p.next) == null) {
// 将插入的node放到链表末尾
p.next = newNode(hash, key, value, null);
// 新node插入到链表末尾,判断是否将链表转为红黑树
// 链表长度等于或大于8,执行treeifyBin(tab, hash);
// treeifyBin(Node<K,V>[] tab, int hash)方法中会判断数组长度小于MIN_TREEIFY_CAPACITY则执行扩容,否则执行链表变红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 在遍历链表的过程中,找到了key完全相等的node元素
// 退出循环,后续进行替换
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e != null 条件成立,说明插入的key在HashMap中已经存在,把值替换为新值即可,然后返回旧值
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 替换value
e.value = value;
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 散列表被修改的次数加一
// 替换node的value不算被修改,如果是替换操作,在上面的if (e != null)判断中return了,不会运行此处的代码
++modCount;
// HashMap的node数量到达阈值,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
5.5size()展開メソッド
前提知識: HashMap を展開した後、キーは 2 つの場所にのみ存在できます。
1. キーは元のバケットの位置に留まり、移動しない可能性があります。つまり、キーはまだ table[i] にあります。
2. キーは、元のバケットの添字 + 元のテーブルの長さにある可能性があります。
以下の例
table.length = 16 でキーが table[15] の位置にあると仮定すると、キーのルーティング式は i = (table.length - 1) & node.hash です。ルーティング式に代入
15 = (16 - 1) & ノード.ハッシュ
10 進数を 2 進数に変換します。複数を示すには「...」を使用します。複数桁の 2 進数を表すには、「xxxx」を使用します (0 または 1)。
00…00 1111 = 00…00 1111 & node.hash、式が確立される場合、node.hash の下位 4 ビットはすべて 1 である必要があり、1 & 1 の結果のみが 1 になります。node.hash は次のようにすることができます。 xxxxxxxx 1111 として表現されます
その後、拡張が発生し、table.length が 32 になり、table.length がルーティング式に代入されます。
i = 31 & node.hash、31 をバイナリに変換し、上記により、node.hash は xxxxxxxx 1111 であると結論付けられました。
i = 00…01 1111 & xxxxxxxx 1111
現在、node.hash = xxxxxxx0 1111 、i = 00…01 1111 & xxxxxxx0 1111 = 00…00 1111 = 15
現在、node.hash = xxxxxxx1 1111 、i = 00…01 1111 & xxxxxxx1 1111 = 00…01 1111 = 31 = 15 + 16
他のバケットの拡張も同様であり、上記の 2 つの点を満たします。
1. キーは元のバケットの位置に留まり、移動しない可能性があります。つまり、キーはまだ table[i] にあります。
2. キーは、元のバケットの添字 + 元のテーブルの長さにある可能性があります。
わかりにくい場合は、次の図を使って理解するとよいでしょう。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-dd6XWZaD-1692927385676)(https://image.xiaoxiaofeng.site/spider/2023) /7/20/xxf -1689833109430.png)]
展開の主要なルールを理解した後、もう 1 つの概念を覚えておく必要があります。resize() のソース コードでは、展開後の table[15] のリンク リストは低レベルのリンク リストと呼ばれ、展開後の table[31] を上位連結リストと呼びます。
size() ソースコード分析
// 扩容
final HashMap.Node<K,V>[] resize() {
// oldTab:引用扩容前的数组
HashMap.Node<K,V>[] oldTab = table;
// oldCap: 扩容前数组table的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr:扩容前的扩容阈值
int oldThr = threshold;
// newCap:扩容后数组table的大小,先给个初值0
// newThr:扩容后的扩容阈值,先给个初值0
int newCap, newThr = 0;
// oldCap > 0 表示数组table已经初始化过了,是一次正常的扩容
if (oldCap > 0) {
// oldCap >= MAXIMUM_CAPACITY 数组的长度已经到达最大值,没法扩容了,直接return
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// newCap = oldCap << 1 数字左移一位,等同于乘以2,但使用位运算更高效。新容量等于旧容量乘以2
// 例如:4 * 2 = 8 转为二进制左移操作:100 左移一位变为 1000
// (newCap = oldCap << 1) < MAXIMUM_CAPACITY -> 数组大小 < 最大限制值 ,这个判断条件基本都是true
// oldCap >= DEFAULT_INITIAL_CAPACITY 当前数组长度必须大于DEFAULT_INITIAL_CAPACITY
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 扩容阀值也要变化,新扩容阀值 = 旧扩容阀值左移一位,等同于乘以2
newThr = oldThr << 1; // double threshold
}
/**
* oldCap == 0 && oldThr > 0 的情况
* 通过 new HashMap(int initialCapacity, float loadFactor)
* new HashMap(int initialCapacity)
* new HashMap(Map<? extends K, ? extends V> m)
* 这三种方式创建HashMap,构造函数会初始化oldThr,且 oldThr >= 16
*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
/**
* oldCap == 0 && oldThr == 0 的情况
* 通过 new HashMap() 创建的HashMap,构造函数不会初始化oldThr
*/
else {
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
// 扩容阈值是 负载因子 * 默认初始容量 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/**
* else if (oldThr > 0) 条件成立
*
* else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY
* && oldCap >= DEFAULT_INITIAL_CAPACITY) 条件不成立
*
* 这两种情况下,newThr == 0,需要计算扩容阈值
*/
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 得到扩容阈值
threshold = newThr;
// 前面的代码主要做两件事
// 1、计算出本次扩容后,table数组的长度
// 2、计算出下一次扩容的阈值
// 创建一个更大的数组,一般情况下是原数组的两倍长度
@SuppressWarnings({
"rawtypes","unchecked"})
HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
table = newTab;
// oldTab != null 说明扩容前HashMap已经有数据
if (oldTab != null) {
// 遍历老数组
for (int j = 0; j < oldCap; ++j) {
// 临时节点变量
HashMap.Node<K,V> e;
// (e = oldTab[j]) != null 当前桶位有数据,但是不知道是 单个Node、链表、红黑树 中的哪一种情况
if ((e = oldTab[j]) != null) {
// 方便JVM回收内存
oldTab[j] = null;
// e.next == null 当前桶位只有一个node
if (e.next == null)
// e.hash & (newCap - 1) 是 key的路由算法
// 当前桶位只有一个元素,从未发生碰撞,可直接将当前元素放到新数组中
newTab[e.hash & (newCap - 1)] = e;
// e instanceof HashMap.TreeNode 桶位元素是红黑树
else if (e instanceof HashMap.TreeNode)
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 桶位元素是链表
else {
// preserve order
// 低位链表
// 扩容之后的数组下标位置,与当前数组的下标位置一致
// 假设原数组长度是16,table[15].hash = xxx0 1111,扩容后,还是在table[15]中
HashMap.Node<K,V> loHead = null, loTail = null;
// 高位链表
// 扩容之后的数组下标位置 = 原数组下标 + 扩容之前数组的长度
// 假设原数组长度是16,table[15].hash = xxx1 1111,扩容后,在table[31]中
// 扩容之后的数组下标位置 = 当前数组下标位置 + 扩容之前数组的长度 -> 31 = 15 + 16 -> 1 1111 = 1111 + 10000
HashMap.Node<K,V> hiHead = null, hiTail = null;
// 临时变量
HashMap.Node<K,V> next;
do {
next = e.next;
// 假设原数组长度oldCap是16 ,转为二进制是 10000
// 假设 e.hash = xxx0 1111 ,xxx0 1111 & 10000 = 0 ,扩容后node在低位链表中
// 假设 e.hash = xxx1 1111 ,xxx1 1111 & 10000 = 10000 ,扩容后node在高位链表中
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);
// loTail != null 低位链表有数据
if (loTail != null) {
// 新链表的最后一个node.next一定要设置为null
// 因为在原链表中node.next可能还指向一个node
loTail.next = null;
// 低位链表还在原桶位中,即还在table[j]中
newTab[j] = loHead;
}
// hiTail != null 高位链表有数据
if (hiTail != null) {
hiTail.next = null;
// 高位链表放在 数组下标位置 = 当前数组下标位置 + 扩容之前数组的长度 的位置,即在table[[j + oldCap]]中
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5.6 get(Object key) 方法
put メソッドを理解すると、get メソッドは比較的簡単になります。
public V get(Object key) {
HashMap.Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final HashMap.Node<K,V> getNode(int hash, Object key) {
// tab:HashMap底层数组
// first:桶位中的头元素
// e: 临时node元素
// n: table数组长度
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
// table不为null
// (n - 1) & hash 是key的路由算法,first = tab[(n - 1) & hash] 找到第一个桶元素
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;
// 桶位不是单个node
if ((e = first.next) != null) {
// 桶位是树
if (first instanceof HashMap.TreeNode)
return ((HashMap.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;
}
5.7 remove(Object key) 方法
Remove(Object key) メソッドは、HashMap の基になる配列を縮小しません。メソッドの詳細な分析については、コード コメントを参照してください。
final HashMap.Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// tab:HashMap底层数组
// p: 当前node元素
// n: 数组长度
// index: 寻址结果
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, index;
// 通过路由公式 (n - 1) & hash 查找到key所在桶位不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// node:查找到的结果
// e: 当前node的下一个元素
HashMap.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) {
// 红黑树查找node
if (p instanceof HashMap.TreeNode)
node = ((HashMap.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,下面执行删除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 红黑树删除元素
if (node instanceof HashMap.TreeNode)
((HashMap.TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// node == p ,则p必然是桶位第一个元素
// 删除桶位第一个元素
else if (node == p)
tab[index] = node.next;
// 链表删除node,此时p是node的前一个元素
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
6. JDK7とJDK8のHashMapの違い
jdk7 と 8 の HashMap の違いについて多くの記事を読みました。ここでは、詳細な分析と、これらの違いが存在する理由について説明します。
6.1 データ構造の違い
-
JDK7 の主なデータ構造は、配列 + リンク リストであり、配列とリンク リストのノードの実装クラスは Entry クラスです。
-
JDK8のデータ構造は主に配列+リンクリスト/赤黒ツリーとなっており、リンクリストの要素数が8以上の場合は赤黒ツリーに変換されます。が 6 以下の場合、赤黒ツリー構造はリンク リストと配列に復元されます。リンク リストのノードの実装クラスは Node クラスです。
分析:
1.红黑树是解决链表查询出现的O(n)情况,那么为什么不用其他树呢?
如平衡二叉树等,我们通过以下二方面分析:
平均插入效率:链表>红黑树>平衡二叉树
平均查询效率:平衡二叉树>红黑树>链表
可以看出红黑树介于二者之间,hashMap作为各种操作频繁的容器,自然选择综合性能较好的红黑树
2.为什么阈值是6和8呢?
1.为什么8转红黑树?
红黑树的平均查找次数是log2(n),
长度为8时:
红黑树平均查找次数为3,链表平均查找长度为8/2=4,此时选择红黑树优
长度为4为:
红黑树平均查找次数为2,链表平均长度为4/2=2,此时次数一样,红黑树开销大
至于567我们在这没有讨论的必要
2.为什么6转回链表?
若选择7,在7和8链表之间的增删元素,必然会导致频繁进行链表和红黑树的转换
6.2 ハッシュ値の計算の違い
-
JDK7:
h^ =(h>>>20)^(h>>>12) return h ^(h>>>7) ^(h>>>4)
-
JDK8:
(key==null)?0:(h=key.hashCode())^(h>>>16)
分析:
jdk7中因为要保持hash函数的散列性,所以进行了多次的异或和位运算而,
8中因为链表长度超过等于8会转红黑树,所以我们可以稍微减少元素的散列性,
从而避免很多异或和位运算操作
6.2 リンクリストデータ挿入の違い
-
JDK7: 拡張後の元の位置とは逆のヘッド挿入方法が使用されます (サイズを変更すると循環リンク リストが発生します)
-
JDK8:末尾挿入方式を使用、展開後の位置は元のリンクリストと同じ
分析:
jdk7插入链表头部,因为这样无需遍历链表(需要判断是否为尾部,然后插入尾部),可以直接插入头部
jdk8中插入元素时,要判断个数是否需要构造红黑树,这样已存在了遍历, 所以插入尾部方便,
并且解决了jdk7中头插法导致的环状链表问题
6.2 拡張機構の違い
- JDK7展開条件:要素数 > 容量(16) * 負荷率(0.75) && 挿入された配列位置に要素がある
- JDK8拡張条件:要素数>容量(16)×負荷率(0.75)
分析:
虽然都是进行2倍扩容,但是JDK1.7中扩容的时候,重新计算位置,
JDk8则不会,只要看看原hash值新增的那个bit位是1还是0就好了,是0的话索引没有变,
是1的话索引变成“原索引+oldCap(旧数组大小)
7. Hashtableの実装原理
Hashtable は HashMap に似ており、ハッシュ テーブルを使用してキーと値のペアを保存します。ハッシュ テーブルの定義: 設定されたハッシュ関数と競合処理方法 (オープン アドレス指定、パブリック オーバーフロー領域、チェーン アドレス、ヘビー ハッシュなど) に基づいて、一連のキーワードが限定された連続アドレス セット (つまり、バケット配列) にマッピングされます。バケット配列)を利用し、アドレスに設定されたキーの「イメージ」をテーブル内のレコードの格納場所として使用するテーブルをハッシュテーブルと呼びます。
ハッシュの競合が発生した場合、競合は「リンク リスト方式」または「ジッパー方式」を通じて処理されます。つまり、リンク リストを使用してキーと値のペア (Map.Entry) を保存します。各 Entry オブジェクトには、同じハッシュコード値を持つ次の Entry を指す次のポインターがあります。
8. HashSetの実装原理
HashSet は HashMap を通じて実装され、HashMap キーのみを使用し、HashMap 値は使用しません。
hashCode()、ハッシュ値、HashSet 要素はハッシュ値に従って格納され、同じハッシュ値を持つ要素は同じ領域に格納されます (バケット原則 (バケット) とも呼ばれ、これにより検索も大幅に効率化されます) 。
ただし、要素が HashSet コレクションに追加された後、ハッシュ値の計算に関与する要素の属性を変更して、remove() メソッドを呼び出すことは機能せず、メモリ リークが発生します。