Java集合——HashMap底层实现(学习分享)
文章目录
- HashMap
- 介绍
- HashMap简介?
- 继承关系
- 基本属性
- 源码解析
- HashMap的数据结构
- JDK1.7(数组+链表)
- JDK1.8(数组+链表+红黑树)
- 方法介绍
- 构造函数
- 常用方法
- put方法介绍
- get方法介绍
- 删除方法介绍
- containsKey方法介绍
- containsValue方法介绍
- HashMap的遍历方式
- 常见面试题
- HashMap特性
- HashMap的底层原理
- 哈希冲突还有那些解决方式
- 为什么String、Integer适合做键
- HashMap 和 ConcurrentHashMap的区别?
- jdk1.7和jdk1.8中HashMap的实现有哪些区别?
- HashMap和HashTable的区别
HashMap
介绍
HashMap简介?
基于哈希表的Map实现;存储键值对映射。
允许null键、null值。
键不能重复。
无序,不能保证存入元素的顺序。
线程不安全。
默认的初始容量16,负载因子0.75,阈值12。
2倍扩容。散列表(也叫哈希表):
- 是依据关系码值(key value)来直接进行访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。映射到表中位置的操作是执行了映射函数,叫做散列函数;存放记录的数组叫做散列表。
继承关系
- 继承java.util.AbstractMap
抽象类,AbstractMap实现了Map接口中的一些方法,减少重复代码的使用。 - 继承java.util.HashMap
类。 - 实现Map
接口。 - 实现Cloneable接口,覆写clone()方法,能被克隆。
- 实现java.io.Serializable接口,支持序列化。
基本属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//默认初始化大小 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;//负载因子0.75
static final Entry<?,?>[] EMPTY_TABLE = { };//初始化的默认数组
transient int size;//HashMap中元素的数量
int threshold;//阈值,DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR=12,判断是否需要调整HashMap的容量
注意: 1、HashMap的扩容操作耗时比较严重,如果能估算Map的容量,最好给一个默认初始值,避免进行多次扩容。 2、HashMap线程不安全,多线程环境推荐使用ConcurrentHashMap
源码解析
点击这里可以里一片关于源码的内容
HashMap的数据结构
JDK1.7(数组+链表)
hashMap的底层数据结构是数组+链表。
HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
- 数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;
- 链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
数组是hashMap的主体,链表主要是为了解决哈希冲突;如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
JDK1.8(数组+链表+红黑树)
当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。
put操作
public V put(K key, V value) {
//调用putVal()方法完成
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table是否初始化,否则初始化操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算存储的索引位置,如果没有元素,直接赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//节点若已经存在,执行赋值操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断链表是否是红黑树
else if (p instanceof TreeNode)
//红黑树对象操作
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//为链表,
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表长度8,将链表转化为红黑树存储
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//key存在,直接覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//记录修改次数
++modCount;
//判断是否需要扩容
if (++size > threshold)
resize();
//空操作
afterNodeInsertion(evict);
return null;
}
方法介绍
构造函数
//构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity){
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造一个带指定初始容量和加载因子的空 HashMap。
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);
// Find a power of 2 >= initialCapacity
int capacity = 1; //初始容量
while (capacity < initialCapacity) //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
//构造一个映射关系与指定 Map 相同的新 HashMap。
HashMap(Map<? extends K,? extends V> m)
常用方法
//从此映射中移除所有映射关系。
//清空HashMap。通过将所有的元素设为null来实现。
void clear() {
modCount++;
Entry[] tab = table;
for (int i = 0; i < tab.length; i++)
tab[i] = null;
size = 0;
}
Object clone()//返回此 HashMap 实例的浅表副本:并不复制键和值本身。
boolean containsKey(Object key)//如果此映射包含对于指定键的映射关系,则返回 true。
boolean containsValue(Object value)//如果此映射将一个或多个键映射到指定值,则返回 true。
Set<Map.Entry<K,V>> entrySet()//返回此映射所包含的映射关系的 Set 视图。
V get(Object key)//返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。
boolean isEmpty()//如果此映射不包含键-值映射关系,则返回 true。
Set<K> keySet()//返回此映射中所包含的键的 Set 视图。
V put(K key, V value)//在此映射中关联指定值与指定键。
void putAll(Map<? extends K,? extends V> m)//将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射中所有键的所有映射关系。
V remove(Object key)//从此映射中移除指定键的映射关系(如果存在)。
int size()//返回此映射中的键-值映射关系数。
Collection<V> values()//返回此映射所包含的值的 Collection 视图。
put方法介绍
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //是否初始化
inflateTable(threshold);
}
if (key == null) //放置在0号位置
return putForNullKey(value);
int hash = hash(key); //计算hash值
int i = indexFor(hash, table.length); //计算在Entry[]中的存储位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i); //添加到Map中
return null;
}
添加键值对时,首先进行table是否初始化的判断,如果没有进行初始化(分配空间,Entry[]数组的长度)。然后进行key是否为null的判断,如果key==null ,放置在Entry[]的0号位置。计算在Entry[]数组的存储位置,判断该位置上是否已有元素,如果已经有元素存在,则遍历该Entry[]数组位置上的单链表。判断key是否存在,如果key已经存在,则用新的value值,替换点旧的value值,并将旧的value值返回。如果key不存在于HashMap中,程序继续向下执行。将key-vlaue, 生成Entry实体,添加到HashMap中的Entry[]数组中。
//*
* hash hash值
* key 键值
* value value值
* bucketIndex Entry[]数组中的存储索引
* /
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); //扩容操作,将数据元素重新计算位置后放入newTable中,链表的顺序与之前的顺序相反
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
添加到方法的具体操作,在添加之前先进行容量的判断,如果当前容量达到了阈值,并且需要存储到Entry[]数组中,先进性扩容操作,空充的容量为table长度的2倍。重新计算hash值,和数组存储的位置,扩容后的链表顺序与扩容前的链表顺序相反。然后将新添加的Entry实体存放到当前Entry[]位置链表的头部。在1.8之前,新插入的元素都是放在了链表的头部位置,但是这种操作在高并发的环境下容易导致死锁,所以1.8之后,新插入的元素都放在了链表的尾部。
get方法介绍
public V get(Object key) {
if (key == null)
//返回table[0] 的value值
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : 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;
}
在get方法中,首先计算hash值,然后调用indexFor()方法得到该key在table中的存储位置,得到该位置的单链表,遍历列表找到key和指定key内容相等的Entry,返回entry.value值。
删除方法介绍
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
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;
}
删除操作,先计算指定key的hash值,然后计算出table中的存储位置,判断当前位置是否Entry实体存在,如果没有直接返回,若当前位置有Entry实体存在,则开始遍历列表。定义了三个Entry引用,分别为pre, e ,next。 在循环遍历的过程中,首先判断pre 和 e 是否相等,若相等表明,table的当前位置只有一个元素,直接将table[i] = next = null 。若形成了pre -> e -> next 的连接关系,判断e的key是否和指定的key 相等,若相等则让pre -> next ,e 失去引用。
containsKey方法介绍
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());
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;
}
containsKey方法是先计算hash然后使用hash和table.length取摸得到index值,遍历table[index]元素查找是否包含key相同的值。
containsValue方法介绍
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
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;
}
containsValue方法就比较粗暴了,就是直接遍历所有元素直到找到value,由此可见HashMap的containsValue方法本质上和普通数组和list的contains方法没什么区别,不高效。
HashMap的遍历方式
- iterator迭代器
- entrySet()
- keySet()
- for-each
- entrySet
- keySet
- lambda表达式(JDK1.8)
- streams API(JDK1.8)
- 单线程
- 多线程
public static void main(String[] args){
Map<Integer,String> map = new HashMap<Integer,String>();
map.put(1, "Monday");
map.put(2, "Tuesday");
map.put(3, "Wednesday");
map.put(4, "Thursday");
map.put(5, "Friday");
map.put(6, "Saturday");
map.put(7, "Sunday");
System.out.print("方式一:");
//方式一 Iterator:entrySet()方法
Iterator<Entry<Integer, String>> iterator1 = map.entrySet().iterator();
while (iterator1.hasNext()) {
Entry<Integer, String> entry1 = iterator1.next();
System.out.print(entry1.getKey()+"-"+entry1.getValue()+" ");
}
System.out.println();
System.out.print("方式二:");
//方式二 Iterator:keySet()方法
Iterator<Integer> iterator2 = map.keySet().iterator();
while (iterator2.hasNext()) {
Integer key = iterator2.next();
System.out.print(key+"-"+map.get(key)+" ");
}
System.out.println();
System.out.print("方式三:");
//方式三 for-each:entrySet()方法
for (Entry<Integer, String> entry : map.entrySet()) {
System.out.print(entry.getKey()+"-"+entry.getValue()+" ");
}
System.out.println();
System.out.print("方式四:");
//方式四 for-each:keySet()方法
for (Integer key : map.keySet()) {
System.out.print(key+"-"+map.get(key)+" ");
}
System.out.println();
System.out.print("方式五:");
//方式五 lambda表达式
map.forEach((key,value) -> {
System.out.print(key+"-"+value+" ");
});
System.out.println();
System.out.print("方式六:");
//方式六 streams API:单线程
map.entrySet().stream().forEach((entry) -> {
System.out.print(entry.getKey()+"-"+entry.getValue()+" ");
});
System.out.println();
System.out.print("方式七:");
//方式七 streams API:多线程
map.entrySet().parallelStream().forEach((entry) -> {
//每次的输出可能不一样
System.out.print(entry.getKey()+"-"+entry.getValue()+" ");
});
}
整体运行结果为:
方式一:1-Monday 2-Tuesday 3-Wednesday 4-Thursday 5-Friday 6-Saturday 7-Sunday
方式二:1-Monday 2-Tuesday 3-Wednesday 4-Thursday 5-Friday 6-Saturday 7-Sunday
方式三:1-Monday 2-Tuesday 3-Wednesday 4-Thursday 5-Friday 6-Saturday 7-Sunday
方式四:1-Monday 2-Tuesday 3-Wednesday 4-Thursday 5-Friday 6-Saturday 7-Sunday
方式五:1-Monday 2-Tuesday 3-Wednesday 4-Thursday 5-Friday 6-Saturday 7-Sunday
方式六:1-Monday 2-Tuesday 3-Wednesday 4-Thursday 5-Friday 6-Saturday 7-Sunday
方式七:1-Monday 2-Tuesday 4-Thursday 5-Friday 6-Saturday 7-Sunday 3-Wednesday
常见面试题
HashMap特性
- 用来存储key-value键值对。
- 允许null键null值。
- 基于哈希的Map实现。
- 无序。
- 非线程安全。
HashMap的底层原理
底层是数组+链表的形式。JDK1.8做了新的调整,当链表的数量超过8以后会自动转为红黑树存储,来提高速度。
当put元素时,会先判断是否需要扩容,当达到阈值(=初始容量*加载因子)时,就会自动扩容为原来的2倍,并重新对计算hash值;
调用key的hashCode,利用hash值与数组的长度取模,计算bucket(桶)位置,也就是要把这个键值对放在数组中的位置;若当前位置为空,则可以直接放进去;若存在元素,在调用key的equals方法判断,相同则覆盖,不同则使用链表放在头部。
get元素的时候,调用key的hashCode找到具体的bucket(桶)位置,其实就是通过key的hash值与数组长度取模找到在数组中的具体位置,然后再调用key的equals方法找到具体的key所对应的value值。
哈希冲突还有那些解决方式
- 再哈希法。
- 链地址法。
- 开放定址法。
为什么String、Integer适合做键
String、Integer都重写了hashMap中要用到的HashCode()方法喝equals()方法。两个不相等的对象返回不同的hashCode的话,发生碰撞的几率更小,能够提高hashMap的性能。
String是不可变的,被final修饰,这样能保证在计算hashCode时防止键值改变。
HashMap 和 ConcurrentHashMap的区别?
- HashMap是线程不安全的,单线程情况下使用;
- 而ConcurrentHashMap是线程安全的,多线程使用!
- 可以使用 Collections.synchronizedMap(new HashMap
());将HashMap封装成线程安全的,其内部实现原理是使用了关键字synchronized。
jdk1.7和jdk1.8中HashMap的实现有哪些区别?
- 存储结构
jdk7内部使用使用Entry
而jdk1.8内部使用Node ,都是实现Map.Entry ,最主要的区别就是列表长度大于8时转为红黑树!
//在JDK1.7版本中.不管负载因子和Hash算法设计的再合理,也免不了会出现拉链(单链表)
//过长的情况,一旦出现拉链(单链表)过长,会严重影响HashMap的性能。
jdk1.7 :static class Entry<K,V> implements Map.Entry<K,V> {
//而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点
//提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。
jdk1.8 :static class Node<K,V> implements Map.Entry<K,V> {
- 一些操作方法
比如说resize()方法,这个方法是第一次初始化或者put操作扩容的。
- jdk1.7 直接扩容两倍,table.length * 2; 源码中使用resize(2 * table.length);
- jdk1.8 优化数组下标计算: index = (table.length - 1) & hash ,由于 table.length 也就是capacity 肯定是2的N次方,使用 & 位运算意味着只是多了最高位, 这样就不用重新计算 index,元素要么在原位置,要么在原位置+ oldCapacity
HashMap和HashTable的区别
- 线程安全
hashMap非线程安全,hashTable线程安全。
- hashTable的实现方法里添加了synchronized关键字来确保线程同步,相对而言hashMap的性能会高些。多线程环境下可以使用Collections.synchronizedMap()方法获取一个线程安全的结合。
Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理。
- 初始容量和扩容
- hashMap初始容量16,hashTable初始容量11,负载因子默认都是0.75。
- hashMap扩容为原来的2倍;hashTable扩容为原来的2倍+1。
- hash计算
Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
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);
}
- 继承结构
HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类。
- 其他
hashMap可以一个null键多个null值;null键时存储在数组的第一个接点上。
hashTable不允许null键null值。
参看地址:
https://blog.csdn.net/u010648555/article/details/60324303
https://www.cnblogs.com/scorates/p/11595930.html
https://blog.csdn.net/woshimaxiao1/article/details/83661464
https://www.cnblogs.com/aflyun/p/7701059.html
还没有评论,来说两句吧...