jdk 1.7HashMap 底层实现是数组+链表(为什么用链表呢?详情看问题五中)。
存储结构
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理就是基于此。
几种数据结构之间的情况对比:
1、数组:采用一段连续的内存空间来存储数据。对于指定下标的查找,时间复杂度为O(1),对于给定元素的查找,需要遍历整个数据,时间复杂度为O(n)。但对于有序数组的查找,可用二分查找法,时间复杂度为O(logn),对于一般的插入删除操作,涉及到数组元素的移动,其平均时间复杂度为O(n)。对应到集合实现,代表就是ArrayList。
2、二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。对应的集合类有TreeSet和TreeMap。
3、线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历整个链表,复杂度为O(n)。对应的集合类是LinkedList。
4、哈希表:也叫散列表,用的是数组支持元素下标随机访问的特性,将键值映射为数组的下标进行元素的查找。所以哈希表就是数组的一种扩展,将键值映射为元素下标的函数叫做哈希函数,哈希函数运算得到的结果叫做哈希值。哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀。
存储位置 = f(关键字)
其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。这会涉及到哈希冲突。
哈希冲突(也叫哈希碰撞):不同的键值通过哈希函数运算得到相同的哈希值。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址)、再散列函数法、链地址法。ThreadLocalMap由于其元素个数较少,采用的是开放寻址法,而HashMap采用的是链表法来解决哈希冲突,即所有散列值相同的元素都放在相同槽对应的链表中(也就是数组+链表的方式)
HashMap是由数组+链表构成的,即存放链表的数组,数组是HashMap的主体,链表则是为了解决哈希碰撞而存在的,如果定位到的数组不包含链表(当前的entry指向为null),那么对于查找,删除等操作,时间复杂度仅为O(1),如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先需要遍历链表,存在相同的key则覆盖value,否则新增;对于查找操作,也是一样需要遍历整个链表,然后通过key对象的equals方法逐一比对,时间复杂度也为O(n)。所以,HashMap中链表出现的越少,长度越短,性能才越好,这也是HashMap设置阀值即扩容的原因。
一、前期
参数说明
1、加载因子loadFactor
- 默认0.75,哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。threshold=(int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1)。
- 表示Hsah表中元素的填满的程度。若加载因子越大,填满的元素越多。好处是空间利用率高了,但冲突的机会加大了。反之加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了。因此必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷。
2、threshold
- 当HashMap的size大于threshold(且null != table[bucketIndex])时会执行resize操作,threshold会重新赋值(int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1)。
- 初始化时默认16,构造方法中threshold = initialCapacity,但在put初始化数组时threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1)。即作用之一用于判断是否需要扩容的条件之一。
3、修改次数modCount
- 在迭代map过程中会判断modCount 和expectedModCount相等,不等抛出 ConcurrentModificationException
关于faild-fast机制:java.util.HashMap 不是线程安全的,因此如果在使用迭代器iterator的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:注意到 modCount 声明为 volatile,保证线程之间修改的可见性。
4、hash种子hashSeed
在hash计算时用到,默认为0。当capacity >= Integer的最大值,且重新随机赋值hashSeed。
默认参数配置
/** 初始容量,2^4,默认16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** 最大初始容量,2^30 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/** 临界值(HashMap 实际能存储的大小),公式为(threshold = capacity * loadFactor) */
int threshold; //默认构造时默认为DEFAULT_INITIAL_CAPACITY = 16
//加载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;
//默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
构造方法
无参最终传入默认的初始容量,加载因子
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);
// 设置负载因子,临界值此时为容量大小,后面第一次put时由inflateTable(int toSize)方法计算设置
this.loadFactor = loadFactor;
//初始阈值为初始容量,与JDK1.8中不同,JDK1.8中调用了tableSizeFor(initialCapacity)得到大于等于初始容量的一个最小的2的指数级别数,比如初始容量为12,那么threshold为16,;如果初始容量为5,那么初始容量为8
threshold = initialCapacity;
init();//空实现
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
计算hash值 - jdk 1.8有变动
final int hash(Object k) {
int h = hashSeed;//默认为0
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);
}
initHashSeedAsNeeded()方法,
final boolean initHashSeedAsNeeded(int capacity) {
//当我们初始化的时候hashSeed为0,0!=0 这时为false.
boolean currentAltHashing = hashSeed != 0;
//isBooted()这个方法里面返回了一个boolean值,我们看下面的代码
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
默认返回false,但是测试时发现vm启动后赋值为true,所以在上面initHashSeedAsNeeded()方法中,主要看capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD,然后决定是否需要重新赋值hashSeed。否则默认为0。
private static volatile boolean booted = false;
public static boolean isBooted() {
return booted;
}
关于变量Holder.ALTERNATIVE_HASHING_THRESHOLD,看静态块中发现其值就是判断是否自定义altThreshold,一般不会定义,所以是null。那么主要看该等式是否成立:capacity >= Integer的最大值。所以一般不会rehash
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;//这里为2147483647,它是HashMap的属性,初始化的时候就已赋值
//Holder这个类是HashMap的子类,
private static class Holder {
//这里定义了我们需要的常量,但是它没赋值,我们看看它是怎么赋值的?
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
//是否有自定义传参threshold,一般不会,所以altThreshold==null
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
计算key的位置,此时length是2^x价值体现出来(减少hash冲突,值能均匀分布)
static int indexFor(int h, int length) {
return h & (length-1); // 与运算,相当于取模运算,等价于 h%length
}
二、增/改
put
1、第一次put,调用inflateTable(threshold)进行初始化
2、对key为null的处理,将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头。所以table[0]的位置上,永远最多存储1个Entry对象,该链表只有一个元素。
3、hash处理:1.重复,替换旧值;2. 对hash进行索引计算indexFor(hash, table.length),索引且hash不同,hash冲突,头插法。否则新建Entry对象
4、如果是改key的value,put后返回oldValue
public V put(K key, V value) {
// 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
// 默认容量16,threshold = loadfactor
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
// 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值
int hash = hash(key);
// 搜索指定hash值在对应table中的索引
int i = indexFor(hash, table.length);
// 循环遍历table数组上存在的Entry对象的链表,判断该位置上hash是否已存在
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))) {
// 如果这个key对应的键值对已经存在,就用新的value代替老的value,并返回老的value!
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 修改次数+1
modCount++;
// table数组中没有key对应的键值对,就将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
懒加载,新增的时候初始化hash map
//初始化HashMap
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize - 找到一个 >= toSize的2^x次方数,即是2的次幂增长的
int capacity = roundUpToPowerOf2(toSize); // 实现了增长为2的幂运算. 实现也比较简单
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);//初始化hashSeed变量
}
找到一个 >= number的2^x次方数,即是2的次幂
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
// 方法Integer.highestOneBit((number - 1) << 1) 即2^x <= ((number-1)<<1);如number = 7, (6<<1) =6*2^1=12 ,需要(2^3 = 8) >= 6,返回2^3
//即该方法会返回一个大于等于(number-1)的2^x数,而Integer.highestOneBit(number) 返回的是小于等于number的2^x数。造成这个原因的是<<1将number的值变大
//为什么要number-1?如果number是2^x的值,那么返回的是number。如number=8,Integer.highestOneBit(number << 1)返回16,不合需要做的含义
//,Integer.highestOneBit((number - 1) << 1)返回的是8
}
插入键为null的值
private V putForNullKey(V value) {
//可以看到键为null的值永远被放在哈希表的第一个桶中,即永远放在table[0]中,再次入值时,只会覆盖原来的,不会形成链表
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//一旦找到键为null,替换旧值
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//如果第一个桶中为null或没有节点的键为null的,插入新节点
modCount++;
addEntry(0, null, value, 0);
return null;
}
新增一个Entry
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果尺寸已将超过了阈值并且桶中索引处不为null
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容2倍
resize(2 * table.length);
//重新计算哈希值
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];
//将该节点作为头节点,此时如果hash冲突,则头插法,next指向原来的头
table[bucketIndex] = new Entry<>(hash, key, value, e);
//尺寸+1,这个值在获取、删除时都有用到
size++;
}
三、查
get
1、处理key为null的情况,取table[0]的value
2、查找key不为null的value
注:都需要判断size是否为0,即是否为空map
获取key的的value值
public V get(Object key) {
//如果Key值为空,则获取对应的值,这里也可以看到,HashMap允许null的key,其内部针对null的key有特殊的逻辑(详细看插入时的操作)
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);//获取实体
return null == entry ? null : entry.getValue();//判断是否为空,不为空,则获取对应的值
}
获取key为null的值
private V getForNullKey() {
//如果元素个数为0,则直接返回null;说明没有值插入或者已删除完
if (size == 0) {
return null;
}
//key为null的元素存储在table的第0个位置;这个循环最多做一次操作,因为插入时值存储最新key为null的value
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)//判断是否为null
return e.value;//返回其值
}
return null;
}
获取键值为key的元素
final Entry<K,V> getEntry(Object key) {
if (size == 0) {//元素个数为0
return null;//直接返回null
}
int hash = (key == null) ? 0 : hash(key);//获取key的Hash值
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))))//判断Hash值和对应的key,合适则返回值
return e;
}
return null;
}
关于e.hash == hash在代码中是否加入问题?详情看
四、删
remove
1、size为0,即空map,返回null。
2、查找key不为null且hash值相同的key并remove,返回删除的entry对象,否则返回null。
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) { //map中无值,直接返回null
return null;
}
//计算hash值
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--; //数目减1
//在链表中的操作
if (prev == e) // 如果恰好是prev,则将下一结点作为头结点
table[i] = next;
else
prev.next = next; //否则指向删除结点的子结点
e.recordRemoval(this);
return e;
}
//不匹配,继续遍历
prev = e;
e = next;
}
// 不存在,返回null
return e;
}
五、扩容
1.7的扩容是插入之前之前判断,而1.8是插入之后再判断是否需要扩容,不过都是扩容2倍 resize(2 * table.length)。
扩容条件:
-
size >= threshold
size:当前map中存在的所有元素,数组+链表
初始化hash map时:threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1)
再次扩容时:threshold = capacity * loadFactor -
null != table[bucketIndex]
扩容到新容量 - 扩容容易出现并发问题,线程不安全
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果旧容量已经达到了最大,将阈值设置为最大值,与1.8相同
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建新哈希表
Entry[] newTable = new Entry[newCapacity];
//将原数组中的元素迁移到扩容后的数组中
//死循环就是在这个方法中产生的
transfer(newTable, initHashSeedAsNeeded(newCapacity)); // boolean initHashSeedAsNeeded()判断是否重新获取随机的hashSeed,这个值在hash()中用到
table = newTable;
//更新阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
如果hashSeed变了,那么rehash为true,否则为false。
计算得到新表中的索引,i的值有可能不同,所以可能和原来的存储位置不同,不同的newIndex = oldIndex + oldCapacity
比如:
hash值 1010 1010
容量16,则
15: 0000 1111
& 1010 1010
= 0000 1010
=10
扩容到32
31:0001 1111
& 1010 1010
= 0000 1010
=10
此时下标不变。
如果hash为1011 1010
则:
容量16,则
15: 0000 1111
& 1011 1010
= 0001 1010
= 16+10=26
扩容到32
31:0001 1111
& 1011 1010
= 0001 1000
= 16+10=26
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历旧表
for (Entry<K,V> e : table) {
//当桶不为空
while(null != e) { //循环遍历链表中的entry
Entry<K,V> next = e.next;
//如果hashSeed变了,需要重新计算hash值
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//计算得到新表中的索引,i的值有可能不同,所以可能和原来的存储位置不同,不同的newIndex = oldIndex + oldCapacity
int i = indexFor(e.hash, newCapacity);
//将新节点作为头节点添加到桶中
//采用链头插入法将e插入i位置,最后得到的链表相对于原table正好是头尾相反的(新值总是插在最前面)
e.next = newTable[i];
newTable[i] = e;
//下一轮循环
e = next;
}
}
}
boolean initHashSeedAsNeeded()判断是否重新获取随机的hashSeed,这个值在hash()中用到。
五、问题
1、为什么增删改查时,判断时e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))),为什么加入e.hash == hash??
有人觉得上面在定位到数组位置之后然后遍历链表的时候,e.hash == hash这个判断没必要,仅通过equals判断就可以。其实不然,试想一下,如果传入的key对象重写了equals方法却没有重写hashCode,而恰巧此对象定位到这个数组位置,如果仅仅用equals判断可能是相等的,但其hashCode和当前对象不一致,这种情况,根据Object的hashCode的约定,不能返回当前对象,而应该返回null。
2、为什么容量大小设置2^x次幂形式?
计算存放位置的公式为h & (length-1),如果length的值是2^x,如length为32,二进制为0010 0000,length - 1二进制为0001 1111,此时如果和有序hash进行与运算,总是有序且均匀获取到下标。从而减少碰撞,即减少hash冲突且值能均匀分布。如果不是2^x,那么hash冲突变大,链表变长,性能就会下降。
该计算方式等价h & (length-1) == h%length。
link:https://blog.csdn.net/eaphyy/article/details/84386313
3、为什么底层用链表?
链地址法处理hash冲突,形成链表,且单向链表的速度高于数组。对于hash相同的key,取模获取的数组下标index肯定也相同,所以此时用链表存储不同的value。当get(key)时,即使获取的数组下标index相同,比较hash值是否相同而获取value。
4、为什么说HashMap线程不安全?不安全造成什么后果?
在扩容时多线程会出现死链。
两个线程A,B同时对HashMap进行resize()操作,在执行transfer方法的while循环时,若此时当前槽上的元素为a–>b–>null
1.线程A执行到 Entry<K,V> next = e.next;时发生阻塞或cpu调度挂起,此时e=a,next=b
2.线程B完整的执行了整段代码,此时新表newTable元素为b–>a–>null
3.线程A继续执行后面的代码,执行完一个循环之后,newTable变为了a<–>b,此时退出该循环,造成数据丢失(只剩2个数据)
4.当get(key)时,取该下标值,hash冲突进来,由get(key)代码可知,for循环取,此时如果获取不到hash相等等符合获取条件,将一致循环下去。 一直死循环,CPU飙升,可能会造成宕机。 – 形成循环链的后果。
5、多线程下扩容可能会出现数据丢失
同样在resize的transfer方法上
1.当前线程迁移过程中,其他线程新增的元素有可能落在已经遍历过的哈希槽上;在遍历完成之后,table数组引用指向了newTable,这时新增的元素就会丢失,被无情的垃圾回收。
2.如果多个线程同时执行resize,每个线程又都会new Entry[newCapacity],此时这是线程内的局部变量,线程之前是不可见的。迁移完成后,resize的线程会给table线程共享变量,从而覆盖其他线程的操作,因此在被覆盖的new table上插入的数据会被丢弃掉。
6、为什么hash冲突时,新数据放在链表头部?
头部快,如果放在其他地方,还需要检索,链表过长,性能底下
7、多个key为null的情况,hashmap怎么处理?
对于key为null的值,默认放在table[0]位置。新增时先判断原先是否有值,没有则新建一个entry对象;有则判断key是否为null,不为null,则覆盖value值。
8、加载因子loadfactor为什么时0.75?
默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。理想状态下,在随机哈希值的情况,对于loadfactor = 0.75 ,虽然由于粒度调整会产生较大的方差,桶中的Node的分布频率服从参数为0.5的泊松分布。
扩容时:threshold=(int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1)
link:link:https://www.cnblogs.com/liyus/p/9916562.html
六、总结
1、HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变(发生扩容时,元素位置会重新分配)
2、对key进行hash计算,并int i = hashKey % table.length计算存在数组中的下标(下标范围[0, table.length -1])
3、JDK1.7和JDk1.8
对JDK1.7和JDk1.8中HashMap的相同与不同点做出总结。
首先是相同点:
- 默认初始容量都是16,默认加载因子都是0.75。容量必须是2的指数倍数
- 扩容时都将容量增加1倍,原来的2倍
- 根据hash值得到桶的索引方法一样,都是i=hash&(cap-1)
- 初始时表为空,都是懒加载,在插入第一个键值对时初始化
- 键为null的hash值为0,都会放在哈希表的第一个桶中
接下来是不同点,主要是思想上的不同,不再纠结与实现的不同:
- 最为重要的一点是,底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构
- 主要区别是插入键值对的put方法的区别。1.8中会将节点插入到链表尾部,而1.7中会将节点作为链表的新的头节点
- JDk1.8中一个键的hash是保持不变的,JDK1.7时resize()时有可能改变键的hash值
- rehash时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序
- JDK1.8是通过hash&cap==0将链表分散,而JDK1.7是通过更新hashSeed来修改hash值达到分散的目的
6.关于扩容,jdk1.7在插入前检查;jdk1.8在插入后检查
link:
https://blog.csdn.net/qq_19431333/article/details/61614414
https://blog.csdn.net/xiaokang123456kao/article/details/77503784