java 集合整理归纳
1. 概述:
- List, Set, Map都是接口,前两个继承collenction 接口,Map为独立接口
- Set 下有HashSet, LinkedHashSet,TreeSet 几个类
- List 下有ArrayList, Vector,LinkedList 几个类
- Map 下有Hashtable , LinkedHashMap,HashMap,TreeMap 几个类
- collection 接口下还有个Queue接口,有PriorityQueue类
- Queue 队列的实现方式常见有两种: 一种使用循环数组;另一种使用链表
2. connection接口 小结
2.1. List
特点: 有序,可重复
-
ArrayList
特点: 底层数据结构是数组,查找快,增删慢,效率高 (查询多使用)
线程安全性: 线程不安全
-
Vector
特点: 底层数据结构是数组,查询快,增删慢,效率低
线程安全性: 线程安全
-
LinkedList
线程安全性: 线程不安全
特点:
- 底层数据结是链表,查询慢,增删快,效率高 (增删多使用)
- LinkedList还实现了Deque接口,可以被当作成双端队列来使用,因此既可以被当成“栈"来使用,也可以当成队列来使用。
- LinkedList的工作原理: LinkedList调用默认构造函数,创建一个链表。由于维护了一个表头,表尾的Node对象的变量。可以进行后续的添加元素到链表中的操作,以及其他删除,插入等操作。也因此实现了双向队列的功能,即可向表头加入元素,也可以向表尾加入元素
public boolean add(E e) {
linkLast(e);
return true;
}
调用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++;
}
更新表尾节点,建立连接。其他操作类似,维护了整个链表。
如何将“双向链表和索引值联系起来的”?
public E get(int index) {
checkElementIndex(index);//检查索引是否有效
return node(index).item;
}
调用了node(index)方法返回了一个Node对象,其中node(index)方法具体如下
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
首先会比较“index”和“双向链表长度的1/2”;若前者小,则从链表头开始往后查找,直到index位置;否则,从链表末尾开始先前查找,直到index位置。这就是“双线链表和索引值联系起来”的方法。
2.1.1. ArrayList与Vector区别
相同点:
-
ArrayList和Vector都是基于数组实现的List类,这两个类封装了一个动态的,允许再分配的Object[]数组. ArrayList和Vector对象使用initalCapacity (默认容量10)参数来设置该数组的长度,当向ArrayList和Vector中添加元素超过了该数组的长度时,他们的initalCapacity会自动增加.
-
自动增加容量思路是: 先调用了一个ensureCapacityInternal()方法,顾名思义:该方法用来确保数组中是否还有足够容量。经过一系列方法(不必关心),最后有个判断:如果剩余容量足够存放这个数据,则进行下一步,如果不够,则需要执行一个重要的方法:
private void grow(int minCapacity) {
//......省略部分内容 主要是为了生成大小合适的newCapacity
//下面这行就是进行了数组扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 此外,ArrayList还提供了两个额外的方法来调整其容量大小
- void ensureCapacity(int minCapacity): 如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。
- void trimToSize():将此 ArrayList 实例的容量调整为列表的当前大小。
不同点:
- ArrayList是线程不安全的,Vector是线程安全的。
- Vector的性能比ArrayList差。
- Vector 有Stack子类, Stack与Vector一样,是线程安全的,但是性能较差, 可以使用LinkedList替代Stack
- ArrayList的遍历方式 有三种:
2.1.2. ArrayList与LinkedList区别
相同点:
- ArrayList和LinkedList都是List接口的实现类,都可以通过索引来随机访问集合中的元素
- 二者都是线程不安全的类
不同点:
- LinkedList还实现了Deque接口,可以被当作成双端队列来使用,因此既可以被当成“栈"来使用,也可以当成队列来使用。
- ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。ArrayList应使用随机访问(即,通过索引序号访问)遍历集合元素。
- LinkedList的实现机制与ArrayList完全不同。ArrayList内部是以数组的形式来保存集合中的元素的,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色。
- 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。
2.1.3. List 常用遍历方法
第一种,通过迭代器遍历 ; 效率最低
Integer value = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
value = (Integer)iter.next();
}
第二种,随机访问,通过索引值去遍历; 效率最高
Integer value = null;
int size = list.size();
for (int i=0; i<size; i++) {
value = (Integer)list.get(i);
}
第三种,for循环遍历
Integer value = null;
for (Integer integ:list) {
value = integ;
}
2.2. Set
特点: 无序,唯一
-
HashSet
特点: 底层数据结构是哈希表,无序,唯一
保证元素唯一性: hashCode() 和 eques() 两个方法
-
LinkedHashSet
特点: 底层数据结构是链表和哈希表(FIFO 插入有序,唯一)
保证元素唯一性: 链表保证元素有序,哈希表保证元素唯一
-
TreeSet
特点: 底层数据结构是红黑树,有序,唯一
保证元素唯一性: 根据比较的返回值是否是0来决定
如何保证元素排序: 通过 自然排序 和 比较器排序
2.2.1 HashSet, TreeSet, LinkedHashSet区别
相同点:
- 三者都实现Set的数据结构,所有元素都是唯一的, 三者都不是线程安全的,如果要使用线程安全的Set可以使用collections.synchronizedSet()
不同点:
- TreeSet 主要功能用于排序,HashSet只是通用的存储数据的集合,LinkedHashSet主要功能用于保证有序的集合(先进先出).
- HashSet插入数据最快,其次是LinkedHashSet,最慢是TreeSet因为内部实现了排序
- HashSet 和LinkedHashSet 语序存在null数据,但TreeSet插入null数据会报空指针异常
2.3. Map
Map实现类用于保存有映射关系的数据,内部保存的每项数据都是k-v对,Map里的Key不能重复. 接口有三个比较重要的实现类,分别是HashMap,TreeMap和Hashtable
2.3.1. TreeMap
-
TreeMap不是直接实现Map接口,而是继承于AbstractMap,实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
-
TreeMap基于红黑树(Red-Black tree)实现,它包含几个重要的成员变量: root, size, comparator。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 comparator 进行排序,具体取决于使用的构造方法。
-
红黑树排序简介
root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。
红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的。size是红黑数中节点的个数。
-
-
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
-
另外,TreeMap是非同步的同时也是非线程安全的。 它的iterator 方法返回的迭代器是fail-fastl的。
2.3.2. HashMap
-
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
-
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
-
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
-
除了使用迭代器的remove方法外其的其他方式删除,都会抛出ConcurrentModificationException. 原因如下:
- 使用iterator迭代删除时没有问题的,迭代器用了自己封装的remove方法,最后一步多了一个操作 expectedModCount = modCount,在每一次迭代时都会调用hasNext()方法判断是否有下一个,是允许集合中数据增加和减少的。
/**
* 正确操作例子
**/
Map<String, Integer> map = new HashMap<>();
map.put("GoddessY", 1);
map.put("Joemsu", 2);
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<String, Integer> entry=iterator.next();
String name=entry.getKey();
if(name.equals("GoddessY")){
//特别注意:不能使用map.remove(name) 否则会报同样的错误
iterator.remove();
}
}
//使用迭代器的remove不会抛出ConcurrentModificationException异常,原因如下:
/**
*迭代器中remove源码
***/
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
//注意这里:对expectedModCount重新进行了赋值。所以下次比较的时候还是相同的
expectedModCount = modCount;
//此处不+1
}
- 使用forEach删除时,会报错ConcurrentModificationException,因为在forEach遍历时,是不允许map元素进行删除和增加。forEach遍历的时候,会初始化expectedModCount=modCount,这时候对HashMap进行修改操作,modCount会+1,继续遍历的时候expectedModCount!=modCount,继而抛出java.util.ConcurrentModificationException异常。
//如果HashMap中modCount和expectedModCount不相等,则会抛出异常
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
modCount具体用途是记录该HashMap修改次数,比如在对一个HashMap put或者remove操作时,会对modCount进行++modCount操作
expectedModCount它是HashIterator中的一个变量,在对HashMap迭代的时候,将modCount赋给expectedModCount而HashMap中的entrySet()迭代时候会创建 HashIterator子类对象EntryIterator
- 解决办法:
1) 通过Iterator修改Hashtable
while(it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
it.remove();//增加此操作
}
2) 根据实际程序,您自己手动给Iterator遍历的那段程序加锁,给修改HashMap的那段程序加锁。
3) 使用“ConcurrentHashMap”替换HashMap,ConcurrentHashMap会自己检查修改操作,对其加锁,也可针对插入操作。
2.3.2.1 HashMap构造函数及源码分析
-
4个构造函数
// 默认构造函数。 HashMap() // 指定“容量大小”的构造函数 HashMap(int capacity) // 指定“容量大小”和“加载因子”的构造函数 HashMap(int capacity, float loadFactor) // 包含“子Map”的构造函数 HashMap(Map<? extends K, ? extends V> map)
-
HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
- table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
- size是HashMap的大小,它是HashMap保存的键值对的数量。
- threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=“容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
- loadFactor就是加载因子。
- modCount是用来实现fail-fast机制的。
-
HashMap源码摘要
package java.util;
import java.io.*;
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
// 默认的初始容量是16,必须是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储数据的Entry数组,长度是2的幂。
// HashMap是采用拉链法实现的,每一个Entry本质上是一个单向链表
transient Entry[] table;
// HashMap的大小,它是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;
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() {
// 设置“加载因子”
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);
}
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 返回索引值
// 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;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
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++;
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;
}
}
// 这里的完全不会被执行到!
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”插入指定位置,bucketIndex是位置索引。
// 它和addEntry的区别是:
// (01) addEntry()一般用在 新增Entry可能导致“HashMap的实际容量”超过“阈值”的情况下。
// 例如,我们新建一个HashMap,然后不断通过put()向HashMap中添加元素;
// put()是通过addEntry()新增Entry的。
// 在这种情况下,我们不知道何时“HashMap的实际容量”会超过“阈值”;
// 因此,需要调用addEntry()
// (02) createEntry() 一般用在 新增Entry不会导致“HashMap的实际容量”超过“阈值”的情况下。
// 例如,我们调用HashMap“带有Map”的构造函数,它绘将Map的全部元素添加到HashMap中;
// 但在添加之前,我们已经计算好“HashMap的容量和阈值”。也就是,可以确定“即使将Map中
// 的全部元素添加到HashMap中,都不会超过HashMap的阈值”。
// 此时,调用createEntry()即可。
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; }
}
2.3.2.2 HashMap的“拉链法”相关内容
- HashMap数据存储数组
transient Entry[] table;
HashMap中的key-value都是存储在Entry数组中的。
-
数据节点Entry的数据结构
Entry 实际上就是一个单向链表。这也是为什么我们说HashMap是通过拉链法解决哈希冲突的。Entry 实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数。这些都是基本的读取/修改key、value值的函数。
2.3.2.3 HashMap 主要对外接口
-
clear()
clear() 的作用是清空HashMap。它是通过将所有的元素设为null来实现的。
-
containsKey()
containsKey() 首先通过getEntry(key)获取key对应的Entry,然后判断该Entry是否为null。
getEntry() 的作用就是返回“键为key”的键值对
HashMap将“key为null”的元素都放在table的位置0处,即table[0]中;“key不为null”的放在table的其余位置!
-
containsValue()
containsValue()分为两步进行处理:第一,若“value为null”,则调用containsNullValue()。第二,若“value不为null”,则查找HashMap中是否有值为value的节点。
containsNullValue() 的作用判断HashMap中是否包含“值为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;
}
-
entrySet()、values()、keySet()
它们3个的原理类似,这里以entrySet()为例来说明。
entrySet()的作用是返回 HashMap中所有Entry的集合 它是一个集合。而每一个Entry本质上又是一个单向链表
HashMap是如何通过entrySet()遍历的。entrySet()实际上是通过newEntryIterator()实现的。 下面我们看看它的代码:
// 返回一个“entry迭代器”
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
}
// Entry的迭代器
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
// 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;
}
}
当我们通过entrySet()获取到的Iterator的next()方法去遍历HashMap时,实际上调用的是 nextEntry() 。而nextEntry()的实现方式,先遍历Entry(根据Entry在table中的序号,从小到大的遍历);然后对每个Entry(即每个单向链表),逐个遍历。
-
put()
若要添加到HashMap中的键值对对应的key已经存在HashMap中,则找到该键值对;然后新的value取代旧的value,并退出!
若要添加到HashMap中的键值对对应的key不在HashMap中,则将其添加到该哈希值对应的链表中,并调用addEntry()。
addEntry()的代码:
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);
}
addEntry() 的作用是新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
说到addEntry(),就不得不说另一个函数createEntry()。createEntry()的代码如下:
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++;
}
它们的作用都是将key、value添加到HashMap中。而且,比较addEntry()和createEntry()的代码,我们发现addEntry()多了两句:
if (size++ >= threshold)
resize(2 * table.length);
addEntry() 与 createEntry()区别如下
-
(01) addEntry()一般用在 新增Entry可能导致“HashMap的实际容量”超过“阈值” 的情况下。
例如,我们新建一个HashMap,然后不断通过put()向HashMap中添加元素;put()是通过addEntry()新增Entry的。在这种情况下,我们不知道何时“HashMap的实际容量”会超过“阈值”; 因此,需要调用addEntry()。
-
(02) createEntry() 一般用在 新增Entry不会导致“HashMap的实际容量”超过“阈值” 的情况下。
例如,我们调用HashMap“带有Map”的构造函数,它绘将Map的全部元素添加到HashMap中;但在添加之前,我们已经计算好“HashMap的容量和阈值”。也就是,可以确定“即使将Map中的全部元素添加到HashMap中,都不会超过HashMap的阈值”。此时,调用createEntry()即可。
2.3.2.4 HashMap 遍历方式
-
遍历HashMap的键值对
第一步:根据entrySet()获取HashMap的“键值对”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。 -
遍历HashMap的键
第一步:根据keySet()获取HashMap的“键”的Set集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。 -
遍历HashMap的值
第一步:根据value()获取HashMap的“值”的集合。
第二步:通过Iterator迭代器遍历“第一步”得到的集合。
2.3.2.5 HashMap的hashCode()方法特点
在HashMap类中, 如果HashMap对象的内部对象是空的, 则hashcode 一定是 0,如果非空, 则遍历容器中的全部对象然后取key的hashcode和value的hashcode 按位异或运算,然后把他们依次相加,也就是说, 如果你的两个HashMap的key和value全部相同的话, 那么, 它们的hashcode就是相同的
所以在任何时候不要使用HashMap或一些特殊容器的Hashcode作为key来进行缓存
- map通常情况下都是hash桶结构,但是当桶太大的时候,会转换成红黑树,可以增加在桶太大情况下访问效率 桶变成红黑树的代码如下
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//这里MIN_TREEIFY_CAPACITY派上了用场,及时单个桶数量达到了树化的阈值,总的容量没到,也不会进行树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
// 返回树节点 return new TreeNode<>(p.hash, p.key, p.value, next);
TreeNode<K,V> p = replacementTreeNode(e, null);
//为空说明是第一个节点,作为树的根节点
if (tl == null)
hd = p;
//设置树的前后节点
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//对整棵树进行处理,形成红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
2.3.2.5 HashMap在JDK7,JDK8 区别
-
数据结构方面: JDK 1.6,1.7 的HashMap 使用 Node数组+链表; JDK1.8 HashMap是使用Node数组+链表+红黑树的数据结构来实现。
在JDK 1.7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n),在JDK 1.8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)
-
新节点插入到链表是的插入顺序不同
jdk7插入在头部,jdk8插入在尾部。为什么jdk7要插入在头部呢?因为插入头部效率高,不需要遍历整个链表。jdk8中为什么新来的元素是放在节点的后面?我们知道jdk8链表大于8的时候就要树化,本身就要算这个链表的长度有多大,链表算大小就要一个个去遍历了,遍历到了才能知道数量,也可以直接把数据放到后面了,这样也是很方便,减少了把数据移动到头数组位置这一步,效率也提高了。jdk7中出现的HashMap死循环bug不会有了,因为jdk8这里是只是平移,没有调换顺序。 -
hash算法有所简化
jdk7默认初始化大小16,加载因子0.75。如果传入了size,会变为大于等于当前值的2的n次方的最小的数。为什么是2次方数?因为indexFor方法的时候,h&(length-1),length是2的次方,那么length-1总是00011111等后面都是1的数,h&它之后,其实就相当于取余,与的效率比取余高,所以用了这种方式达到高效率。hash函数里面方法里面,数会经过右移,为什么要右移?因为取余操作都是操作低位,hash碰撞的概率会提高,为了减少hash碰撞,右移就可以将高位也参与运算,减少了hash碰撞。
jdk7 hash函数
/**
* Retrieve object hash code and applies a supplemental hash function to the
* result hash, which defends against poor quality hash functions. This is
* critical because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
*/
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
jdk8 hash函数
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么jdk8的hash函数会变简单?jdk8中用的是链表过度到红黑树,效率会提高,所以jdk8提高查询效率的地方由红黑树去实现,没必要像jdk7那样右移那么复杂。
-
HashMap扩容条件不同
JDK7中HashMap扩容是要同时满足两个条件:- 当前数据存储的数量(即size())大小必须大于等于阈值
- 当前加入的数据是否发生了hash冲突
因为上面这两个条件,所以存在下面两种情况:
- 就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。
- 当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。
JDK8中,扩容的条件只有一个,就是当前容量大于阈值(阈值等于当前hashmap最大容量乘以负载因子)
-
扩容计算新索引方法不一样 详解并发下的HashMap以及JDK8的优化
JDK7中通过transfer方法将旧数组中的元素复制到新数组,在这个方法中进行了包括释放旧的Entry中的对象引用,该过程中如果需要重新计算hash值就重新计算,然后根据indexfor()方法计算索引值。而索引值的计算方法为: h & (length-1) ,即hashcode计算出的hash值和数组长度进行与运算。
JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,因为他采用的是头插法,先拿出旧链表头元素。一般认为,Java的%、/操作比&慢10倍左右,因此采用&运算而不是h % length会提高性能。
JDK8中扩充HashMap的时候不用重新计算hash,只要要看看原来的hash值新添加的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+“oldCap”。
这样做既省去了重新计算hash值的时间,而且同时,因为新添加的1bit是0还是1可以认为是随机的,因而resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。 -
扩容机制有所优化
在JDK7中,对所有链表进行rehash计算;在JDK8中,实际上也是通过取余找到元素所在的数组的位置,取余的方式在putVal里面:i = (n - 1) & hash。我们假设,在扩容之前,key取余之后留下了n位。扩容之后,容量变为2倍,所以key取余得到的就有n+1位。在这n+1位里面,如果第1位是0,那么扩容前后这个key的位置还是在相同的位置(因为hash相同,并且余数的第1位是0,和之前n位的时候一样,所以余数还是一样,位置就一样了);如果这n+1位的第一位是1,那么就和之前的不同,那么这个key就应该放在之前的位置再加上之前整个数组的长度的位置。
hiTail.next = null;
newTab[j + oldCap] = hiHead;
通过上面的操作就减少了移动所有数据带来的消耗。
图解上述步骤可能更清楚一点:
-
节点表示不同
在jdk7中,节点是Entry,在jdk8中节点是Node,其实是一样的。 -
关于jdk7HashMap的一些细节
1、jdk7里面addEntry方法扩容的条件size>threshold,还有一个很容易忽略的,就是null!=table[bucketIndex],这个是什么意思?意思是如果当前放进来的值的那个位置也被占用了,才进行扩容,否则还是放到那个空的位置就行了,反正不存在hash冲突。(但是在jdk8里面取消了这个限制,只要达到了threshold,不管原来的位置空不空,还是要扩容)2、jdk7 resize方法多线程死循环的bug HashMap 在高并发下引起的死循环 在jdk8的这个bug已经解决了 【JDK 8 用 head 和 tail 来保证链表的顺序和之前一样;JDK 7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)】。
-
关于jdk8HashMap的一些细节
1、jdk8 默认初始化大小16,加载因子0.75。还有一个默认的树化的大小8。非树化大小为6,也就是红黑树的元素小于6的时候,又会变回一个链表。为什么是8和6?
解释仅供参考:
HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
2、jdk8的putVal方法:i = (n - 1) & hash这个其实也就是取余操作了。
2.3.3. Hashtable
- Hashtable 也是一个散列表,它也是通过“拉链法”解决哈希冲突的。
- 它存储的内容是键值对(key-value)映射。Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
- Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。
- Hashtable 的实例有两个参数影响其性能:初始容量 和 加载因子。容量 是哈希表中桶 的数量,初始容量 就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。
- 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数 Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。
2.3.3.1 Hashtable的主要对外接口
-
elements()
elements() 的作用是返回“所有value”的枚举对象
public synchronized Enumeration<V> elements() {
return this.<V>getEnumeration(VALUES);
}
// 获取Hashtable的枚举类对象
private <T> Enumeration<T> getEnumeration(int type) {
if (count == 0) {
return (Enumeration<T>)emptyEnumerator;
} else {
return new Enumerator<T>(type, false);
}
}
(01) 若Hashtable的实际大小为0,则返回“空枚举类”对象emptyEnumerator;
(02) 否则,返回正常的Enumerator的对象。(Enumerator实现了迭代器和枚举两个接口)
emptyEnumerator对象是如何实现的
private static Enumeration emptyEnumerator = new EmptyEnumerator();
// 空枚举类
// 当Hashtable的实际大小为0;此时,又要通过Enumeration遍历Hashtable时,返回的是“空枚举类”的对象。
private static class EmptyEnumerator implements Enumeration<Object> {
EmptyEnumerator() {
}
// 空枚举类的hasMoreElements() 始终返回false
public boolean hasMoreElements() {
return false;
}
// 空枚举类的nextElement() 抛出异常
public Object nextElement() {
throw new NoSuchElementException("Hashtable Enumerator");
}
}
Enumerator的作用是提供了“通过elements()遍历Hashtable的接口” 和 “通过entrySet()遍历Hashtable的接口”。因为,它同时实现了 “Enumerator接口”和“Iterator接口”。
private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
// 指向Hashtable的table
Entry[] table = Hashtable.this.table;
// Hashtable的总的大小
int index = table.length;
Entry<K,V> entry = null;
Entry<K,V> lastReturned = null;
int type;
// Enumerator是 “迭代器(Iterator)” 还是 “枚举类(Enumeration)”的标志
// iterator为true,表示它是迭代器;否则,是枚举类。
boolean iterator;
// 在将Enumerator当作迭代器使用时会用到,用来实现fail-fast机制。
protected int expectedModCount = modCount;
Enumerator(int type, boolean iterator) {
this.type = type;
this.iterator = iterator;
}
// 从遍历table的数组的末尾向前查找,直到找到不为null的Entry。
public boolean hasMoreElements() {
Entry<K,V> e = entry;
int i = index;
Entry[] t = table;
/* Use locals for faster loop iteration */
while (e == null && i > 0) {
e = t[--i];
}
entry = e;
index = i;
return e != null;
}
// 获取下一个元素
// 注意:从hasMoreElements() 和nextElement() 可以看出“Hashtable的elements()遍历方式”
// 首先,从后向前的遍历table数组。table数组的每个节点都是一个单向链表(Entry)。
// 然后,依次向后遍历单向链表Entry。
public T nextElement() {
Entry<K,V> et = entry;
int i = index;
Entry[] t = table;
/* Use locals for faster loop iteration */
while (et == null && i > 0) {
et = t[--i];
}
entry = et;
index = i;
if (et != null) {
Entry<K,V> e = lastReturned = entry;
entry = e.next;
return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
}
throw new NoSuchElementException("Hashtable Enumerator");
}
// 迭代器Iterator的判断是否存在下一个元素
// 实际上,它是调用的hasMoreElements()
public boolean hasNext() {
return hasMoreElements();
}
// 迭代器获取下一个元素
// 实际上,它是调用的nextElement()
public T next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
return nextElement();
}
// 迭代器的remove()接口。
// 首先,它在table数组中找出要删除元素所在的Entry,
// 然后,删除单向链表Entry中的元素。
public void remove() {
if (!iterator)
throw new UnsupportedOperationException();
if (lastReturned == null)
throw new IllegalStateException("Hashtable Enumerator");
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
synchronized(Hashtable.this) {
Entry[] tab = Hashtable.this.table;
int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index], prev = null; e != null;
prev = e, e = e.next) {
if (e == lastReturned) {
modCount++;
expectedModCount++;
if (prev == null)
tab[index] = e.next;
else
prev.next = e.next;
count--;
lastReturned = null;
return;
}
}
throw new ConcurrentModificationException();
}
}
}
-
Hashtable实现的Serializable接口
Hashtable实现java.io.Serializable,分别实现了串行读取、写入功能。
串行写入函数: 就是将Hashtable的“总的容量,实际容量,所有的Entry” 都写入到输出流中
串行读取函数: 根据写入方式读出将Hashtable的“总的容量,实际容量,所有的Entry”依次读出
-
其余对外接口原理和HashMap一样
2.3.4. WeakHashMap
-
WeakHashMap是基于java弱引用实现的HashMap
-
WeakHashMap 继承于AbstractMap,实现了Map接口。WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
-
在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。
-
这个“弱键”的原理呢?
大致上就是,通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。“弱键”如何被自动从WeakHashMap中删除的实现步骤是:
(01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。 实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
(02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
(03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。 -
和HashMap一样,WeakHashMap是不同步的。可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap。 在结构上,基本和HashMap一致。在Java8中,唯一存储上的不同点就是,当冲突的key变多时,HashMap引入了二叉树(红黑树)进行存储,而WeakHashMap则一直使用链表进行存储。
-
在WeakHashMap中,就只有key被回收,而value,则是通过expungeStaleEntries赋值为null。 参考介绍在WeakHashMap中 ReferenceQueue (引用队列) 作用.
2.3.5. LinkedHashMap
-
LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。
-
LinkedHashMap 基本数据结构是相同的数组、链表、红黑树
-
LinkedHashMap 内部类 Entry 继承自 HashMap 内部类 Node,并新增了两个引用,分别是 before 和 after。这两个引用的用途不难理解,也就是用于维护双向链表。
-
HashMap 的内部类 TreeNode 不继承它的一个内部类 Node,却继承自 Node 的子类 LinkedHashMap 内部类 Entry 原因如下:
-
在 HashMap 的设计思路注释中解释了原因: TreeNode 对象的大小约是普通 Node 对象的2倍,我们仅在桶(bin)中包含足够多的节点时再使用。当桶中的节点数量变少时(取决于删除和扩容),TreeNode 会被转成 Node。当用户实现的 hashCode 方法具有良好分布性时,树类型的桶将会很少被使用。
-
也就是说 TreeNode 使用的并不多,浪费那点空间是可接受的。假如 TreeNode 机制继承自 Node 类,那么它要想具备组成链表的能力,就需要 Node 去继承 LinkedHashMap 的内部类 Entry。这个时候就得不偿失了,浪费很多空间去获取不一定用得到的能力。
-
链表的建立过程
- 链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新。
- LinkedHashMap 本身并没有覆写父类的 put 方法,而是直接使用了父类的实现,但也有不同,不同在于LinkedHashMap实现了afterNodeAccess,afterNodeInsertion方法
-
链表节点获取
- 使用get()方法,get进行了重写
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
//唯一需要关注的就是在插入模式中,获取值后又一次进行把当前节点移到链表尾部操作
2.3.5. ConcurrentHashMap
2.3.6. ConcurrentHashMap JDK6、JDK7和JDK8总结
2.3.7. HashMap 与Hashtable 区别
-
HashMap:
a. 是非线程安全的;
b. HashMap可以使用null作为key。
c. 遍历使用的是Iterator迭代器;
d. HashMap是对Map接口的实现
e. HashMap的初始容量为16,填充因子默认都是0.75,HashMap扩容时是当前容量翻倍即:capacity*2
f. 计算hash的方法: HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
g. 底层实现都是数组+链表结构实现
h. 添加、删除、获取元素时都是先计算hash
i. 在插入元素时,可能会扩大数组的容量,在扩大容量时须要重新计算hash,并复制对象到新的数组中;
-
HashTable:
a. 是线程安全的(Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些);
b. 无论是key还是value都不允许有null值的存在;在HashTable中调用put方法时,如果key为null,直接抛出NullPointerException异常;
c. 遍历使用的是Enumeration列举;
d. HashTable实现了Map接口和Dictionary抽象类
e. Hashtable初始容量为11,填充因子默认都是0.75,Hashtable扩容时是容量翻倍+1即:capacity*2+1
f. 计算hash的方法: Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
g. 底层实现都是数组+链表结构实现
3. Java里面引用分类 WeakHashMap的延伸
- 此处涉及到JVM虚拟机部分知识,WeakHashMap涉及到一个叫做弱引用的东西,干脆在此处延伸介绍一下
3.1 java中的四种引用方式如下:
-
强引用: Java默认支持的操作,即使进行了多次的GC回收,即使JVM真的已经不够用了,即使JVM最终不得已抛出了OOM错误,那么该引用继续抢占。
在多线程状态下,即使产生多个引用对象,空间也不会被回收。每个线程都会出现OOM ,强引用并不是造成OOM的关键性因素 -
软引用: 当内存空间不足时,可以回收此内存空间。如果充足则不回收,可以用其完成缓存的一些处理操作开发。缓存:保证数据更快的操作(读取),比如网页缓存,图片缓存等. 是不重要的数据。可以作为牺牲来释放空间。
- 软引用指的是当内存不足的时候才进行GC的空间释放,但是如果要想使用软引用必须单独使用特殊的处理类 java.lang.ref.SoftReference
- 软引用与强引用相比,最大的特点在于:软引用中保存的内容如果在内存富裕的时候会继续保留,内存不足会作为第一批的丢弃者进行垃圾空间的释放。
-
弱引用: 不管内存是否紧张,只要一出现GC处理,则立即回收。
- 弱引用使用“WeakReference”类来完成。 java.lang.ref.WeakReference
- 一旦执行了GC,那么就需要进行内存空间的释放,在类集里面有一个与弱引用功能相似的Map集合–WeakHashMap
- WeakHashMap最大的好处是可以用它保存一些共享数据,这些共享数据如果长时间不使用,可以将其清空
-
虚引用: 和没有引用是一样的。 比如HashMap根据key取得值,设置key值为null和不设置key值的效果是一样的。
- JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
-
构造方法中的 ReferenceQueue (引用队列) 使用方法
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
与软引用、弱引用不同,虚引用必须和引用队列一起使用。
3.1.1 在WeakHashMap中 ReferenceQueue (引用队列) 作用
queue是用来存放那些,被jvm清除的entry的引用,因为WeakHashMap使用的是弱引用,所以一旦gc,就会有key键被清除,所以会把entry加入到queue中。在WeakHashMap中加入queue的目的,就是为expungeStaleEntries所用。
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
在构造每一个Entry时,都将它与queue绑定,从而一旦被jvm回收,那么这个Entry就会倍加如到queue中。
expungeStaleEntries方法具体意思
方法里面就仅仅是释放value值。由前面的Entry的构造方法可知, super(key, queue); 传入父类的仅仅是key,所以经过仔细阅读jdk源码开始部分分析后,得出结论,在WeakHashMap中,有jvm回收的,仅仅是Entry的key部分,所以一旦jvm强制回收,那么这些key都会为null,再通过私有的expungeStaleEntries 方法,把value也制null,并且把size–。
/**
* 从ReferenceQueue中取出过期的entry,从WeakHashMap找到对应的entry,逐一删除
* 注意,只会把value置为null。
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
//遍历queue
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
//遍历table[i]所在链表
Entry<K,V> next = p.next;
if (p == e) {
//queue里面有e,那就删了。
if (prev == e)
//e就是当前的p.next
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
//置为null,帮助gc。只制null了value。
e.value = null; // Help GC
//设置e的value,但是没看到设置e的key。
size--;
break;
}
prev = p;
p = next;
}
}
}
}
上面代码逻辑为,当在table中找到queue中存在元素时,就把value制空,然后size–。
所以在WeakHashMap中,就只有key被回收,而value,则是通过expungeStaleEntries赋值为null。