HashMap

129 阅读22分钟

1.实现方式

  • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
  • HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。
  • HashMap 是无序的,即不会记录插入的顺序。
  • HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

无法复制加载中的内容

那为什么hashmap不直接使用红黑树呢?

从时间复杂度来分析,红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。同时树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。

public class HashMap<K,V> extends AbstractMap<K,V>

    implements Map<K,V>, Cloneable, Serializable
变量术语说明
size大小HashMap的存储大小
threshold临界值HashMap大小达到临界值,需要重新分配大小。
loadFactor负载因子HashMap大小负载因子,默认为75%。
modCount统一修改HashMap被修改或者删除的次数总数。
Entry实体HashMap存储对象的实际实体,由Key,value,hash,next组成。

HashMap的实例有两个影响其性能的参数: 初始容量负载因子

  • 容量是哈希表中的桶数,初始容量只是创建哈希表时的容量。
  • 加载因子(默认加载因子0.75) 是在自动增加容量之前允许哈希表获取的完整程度的度量。

当哈希表中的条目数超过加载因子和当前容量的乘积时,哈希表将被重新哈希(即,重建内部数据结构),以便哈希表具有大约两倍的桶数。

请注意,此实现不同步。 如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。)这通常通过同步自然封装映射的某个对象来完成。 如果不存在此类对象,则应使用Collections.synchronizedMap方法“包装”地图HashMap。 这最好在创建时完成,以防止意外地不同步访问地图HashMap:

Map m = Collections.synchronizedMap(new HashMap(...));

DEFAULT_INITIAL_CAPACITY

默认初始容量-必须是2的幂。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

<< 1 相当于乘以2

>> 1 相当于除以2

DEFAULT_LOAD_FACTOR

在构造函数中未指定时使用的负载因子。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

2.映射entrySet

public abstract class AbstractMap<K,V>extends Objectimplements Map<K,V>要实现不可修改的映射,程序员只需扩展此类并提供 entrySet 方法的实现即可,该方法将返回映射的映射关系 Set 视图。

HashMap qq=new HashMap();

qq.put("1","qq");

qq.put("2","ww");

qq.put("3","ee");

qq.put("4","rr");

System.out.println(qq.entrySet());

qq.clear();

System.out.println(qq.entrySet());

entrySet定义为Map.Entry,实际存放的还是HashMap$Node

Set(集合):

Set是最简单的一种集合。集合中的对象不按特定的方式排序,并且没有重复对象。

3.Cloneable克隆

  • 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。 换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
  • 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。

那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。

HashMap qq=new HashMap();

qq.put("1","qq");

qq.put("2","ww");

qq.put("3","ee");

qq.put("4","rr");

System.out.println(qq.entrySet());

Object ww =qq.clone();

System.out.println(ww);

public Object clone() {

    HashMap<K,V> result;

    try {

        result = (HashMap<K,V>)super.clone();

    } catch (CloneNotSupportedException e) {

        // this shouldn't happen, since we are Cloneable

        throw new InternalError(e);

    }

    result.reinitialize();

    result.putMapEntries(this, false);

    return result;

}

克隆后引用比较

但此克隆是属于浅拷贝。返回此 HashMap实例的浅表副本:未克隆键和值本身。

4.实现Serializable接口的作用

  • 序列化:把对象转换为字节序列的过程称为对象的序列化。
  • 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。

在程序中为了能直接以 Java 对象的形式进行保存,然后再重新得到该 Java 对象,这就需要序列化能力。序列化其实可以看成是一种机制,按照一定的格式将 Java 对象的某状态转成介质可接受的形式,以方便存储或传输。其实想想就大致清楚基本流程,序列化时将 Java 对象相关的类信息、属性及属性值等等保存起来,反序列化时再根据这些信息构建出 Java 对象。而过程可能涉及到其他对象的引用,所以这里引用的对象的相关信息也要参与序列化。

序列化的作用:

  • 提供一种简单又可扩展的对象保存恢复机制。
  • 对于远程调用,能方便对对象进行编码和解码,就像实现对象直接传输。
  • 可以将对象持久化到介质中,就像实现对象直接存储。
  • 允许对象自定义外部存储的格式。

虽然Serializable接口的源码是个空接口,但当我们让实体类实现Serializable接口时,其实是在告诉JVM此类可被序列化,可被默认的序列化机制序列化。

即使没有声明该接口也是可以序列化的

  1. 默认方式,Java对象中的非静态和非transient的字段都会被定义为需要序列的字段。
  2. 另外一种方式是通过 ObjectStreamField 数组来声明类需要序列化的对象。

对于不想序列化的信息,可以通过transient关键字让敏感信息不被序列化

5.key与value的类型

HashMap qq = new HashMap();

qq.put("1", "qq");

qq.put("2", "ww");

qq.put("3", "ee");

qq.put("3",4);

System.out.println(qq);

可以看到put方法对于相同key值的填入采用的是覆盖



HashMap qq = new HashMap();

qq.put("1", "qq");

qq.put("2", "ww");

qq.put("3", "ee");

qq.put('3',"ss");

qq.put(3,"vv");

Integer n=3;

qq.put(n,"cc");

System.out.println(qq);

HashMap 中的元素实际上是对象,一些常见的基本类型可以使用它的包装类。

可以看到字符串类型的3并没有被字符类型的3覆盖,但基本int型的3被其包装类覆盖,因此key值不能为基本类型,因为基本类型没有equals(),对于赋值时的基本型会转换为其包装类,即对象。



qq.put("1","ww");

System.out.println(qq.get(1));

可以看到如果存储的key是String型,用Integer是取不出来的,因此在定义的时候可以指定key的类型

这时放入其他类型将会报错

value也不能是基本类型,对于输入的会自动转换为其包装类。



HashMap qq = new HashMap();

qq.put(1, "qq");

qq.put(2,1);

Integer q=1;

qq.put(3,q);

System.out.println(qq);

System.out.println(qq.get(2).getClass());

System.out.println(qq.get(3).getClass());



HashMap qq = new HashMap();

qq.put(1, "qq");

qq.put(null,"ww");

qq.put(null,"ee");

qq.put(2,null);

qq.put(3,null);

qq.put(4,null);

System.out.println(qq);

可以看到只允许一个key为null,但可以多个value为null

6.构造方法

构造器描述异常
HashMap()使用默认初始容量(16)和默认加载因子(0.75)构造一个空 HashMap 。
HashMap(int initialCapacity)使用指定的初始容量和默认加载因子(0.75)构造一个空 HashMapIllegalArgumentException - 如果初始容量为负数。
HashMap(int initialCapacity, float loadFactor)使用指定的初始容量和加载因子构造一个空 HashMap 。IllegalArgumentException- 如果初始容量为负或负载因子为非正数
HashMap(Map<? extends K,? extends V> m)构造一个新的 HashMap ,其映射与指定的 Map相同。NullPointerException- 如果指定的映射为null

引用:

www.runoob.com/manual/jdk1…

7.方法

  1. 添加键值对(key-value)可以使用 put() 方法

  1. 使用 get(key) 方法来获取 key 对应的 value:

  1. 使用 remove(key) 方法来删除 key 对应的键值对(key-value):

remove参数可以同时填入key和value,避免错误删除

  1. 删除所有键值对(key-value)可以使用 clear 方法:

  1. 计算 HashMap 中的元素数量可以使用 size() 方法:

6.迭代 HashMap

直接调用方法输出全部

但应该注意其类型是不同的

无法复制加载中的内容

源码分析:

www.jianshu.com/p/003256ce4…

8.扩容resize()

Jdk8

HashMap的容量变化通常存在以下几种情况:

  1. 空参数的构造函数:实例化的HashMap默认内部数组是null,即没有实例化。第一次调用put方法时,则会开始第一次初始化扩容,长度为16。
  2. 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数设置赋值给阈值(threshold)。第一次调用put方法时,会将阈值赋值给容量,然后让

新阈值=容量X负载因子。(因此并不是我们手动指定了容量就一定不会触发扩容,超过阈值后一样会扩容)元素数量超过阈值便会触发扩容。

选择2的幂数的原因:

  1. 运算速度快

对于数组下标的计算方式采用“ (n - 1) & hash”;这里实际时想取余运算。但是:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方)。在计算机中&的速度远比%快。

  1. 方便扩容

具体见下方,扩容时可以将新下标在原位置,或在原长度+原位置的位置。

  1. 可以减少冲突

length 为偶数时,length-1 为奇数,奇数的二进制最后一位是 1,这样便保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性

而如果 length 为奇数的话,很明显 length-1 为偶数,它的最后一位是 0,这样 hash & (length-1) 的最后一位肯定为 0,即只能为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间

  1. 如果不是第一次扩容,则容量变为原来的2倍,阈值也变为原来的2倍。 (容量和阈值都变为原来的2倍时,负载因子还是不变)

此外还有几个细节需要注意:

  • 首次put时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容;
  • 不是首次put,则不再初始化,直接存入数据,然后判断是否需要扩容;(懒加载 lazy load)

由于数组的容量是以2的幂次方扩容的,那么一个Entity在扩容时,新的位置要么在原位置,要么在原长度+原位置的位置。

将hash和n-1做与运算

数组长度变为原来的2倍,表现在二进制上就是多了一个高位参与数组下标确定(上图第五个数字) 。此时,一个元素通过hash转换坐标的方法计算后,恰好出现一个现象:最高位是0则坐标不变,最高位是1则坐标变为“10000+原坐标”,即“原长度+原坐标”。如下图:

最高位是1与运算后保留1,而该位扩大的量恰好就是原数组的长度n

  • JDK8在迁移元素时是正序的,不会出现链表转置的发生。
  • 如果某个桶内的元素超过8个并且table的大小大于 MIN_TREEIFY_CAPACITY = 64,会将链表转化成红黑树,加快数据查询效率。小于64的时候只会扩容。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)

    resize();

9. Hsah()

objects类

public static int hashCode(Object o) {

    return o != null ? o.hashCode() : 0;

}

Object 类中hashCode() 是一个native方法,意味着方法的实现和硬件平台有关,默认实现和虚拟机有关,对于有些JVM,hashCode()返回的是对象的地址,大多时候JVM根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,并返回。

//重写的hashCode

public final int hashCode() {

    return Objects.hashCode(key) ^ Objects.hashCode(value);

}

  /**

 此处是重写Object类中的hashCode值,为了是分布均匀,尽可能减少冲突,不保证唯一,但同一个类

 ,必须有唯一的hashcode,不同的对象可以有相同的hashcode值

 使用key和value的值进行hashcode值的异或决定放到桶中的哪个位置上

 */ 
static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//^ 按位异或

}

//此处是获取hash值的方法,若key值不为空时,使用key值与key无符号右移16位的值进行按位异或

//的操作得到的结果。可以进来得到高低hash值的分布,高位与地位都有数据
  1. 计算hash:hashcode32位,然后低16位异或高16位,得到hash值

为什么要右移16位?

其实是为了减少碰撞,进一步降低hash冲突的几率。int类型的数值是4个字节的,右移16位异或可以同时保留高16位于低16位的特征。

为什么要异或运算?((n -1 )& hash)

首先将高16位无符号右移16位与低十六位做异或运算。如果不这样做,而是直接做&运算那么高十六位所代表的部分特征就可能被丢失 将高十六位无符号右移之后与低十六位做异或运算使得高十六位的特征与低十六位的特征进行了混合得到的新的数值中就高位与低位的信息都被保留了 ,而在这里采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢。

  1. 计算hash表索引方式indexFor():hash&(length-1)

因为length为2^n,所以length-1换算成二进制,其全部位数均为1。按位与计算的原则是两位同时为“1”,结果才为“1”,否则为“0”。所以对于计算表达式h & (length-1) 来说,等同于返回h的低(length-1)位,与h%length相同,但是快很多。

10.Entry

static class Node<K,V> implements Map.Entry<K,V> {

    final int hash;

    final K key;

    V value;

    Node<K,V> next;



    Node(int hash, K key, V value, Node<K,V> next) {

        this.hash = hash;

        this.key = key;

        this.value = value;

        this.next = next;

    }

Node类实现了Map的Entry接口,存储对应的hash值,<key,value>,因为HashMap是数组加链表(大于8个为红黑树)实现,因此还有下个节点的引用next。

public final K getKey()        { return key; }

public final V getValue()      { return value; }

public final String toString() { return key + "=" + value; }



public final int hashCode() {

    return Objects.hashCode(key) ^ Objects.hashCode(value);

}

返回相应的数据



public final boolean equals(Object o) {

        if (o == this)

            return true;

        if (o instanceof Map.Entry) {//判断o是否是Map.Entry的实例

            Map.Entry<?,?> e = (Map.Entry<?,?>)o;

            if (Objects.equals(key, e.getKey()) &&

                Objects.equals(value, e.getValue()))//比较o和node的key,value是否相等

                return true;

        }

        return false;

    }

}

/*一般重写hashcode方法后,都会重写equals方法,2个都一起进行修改,equals具体是比较两个对

象是否相等,==是比较地址是否相等的,而不同的对象一定会有不同的内存地址,导致2对象的比较一定

不等,重写Object类中的equals方法后(Object类中直接比较地址),会在instanceof判断是同一个

类型后,比较对象内部的属性,若属性相同,则判断这两个对象是相等的,equals方法返回true,

当然前提是hashcode定位到桶的哪个位置,在桶的链表存储中使用equals进行具体的对象比较。

*/

重写equals方法

11. get()

//返回map中key对象的value值

public V get(Object key) {

    Node<K,V> e;

    return (e = getNode(key)) == null ? null : e.value;

}
final Node<K,V> getNode(Object key) {

    Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;

    if ((tab = table) != null && (n = tab.length) > 0 &&

        (first = tab[(n - 1) & (hash = hash(key))]) != null) {

        if (first.hash == hash && // always check first node

            ((k = first.key) == key || (key != null && key.equals(k))))

            return first;

        if ((e = first.next) != null) {

            if (first instanceof TreeNode)

                return ((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;

}

  /**具体实现怎么根据key值获取具体的value值,传入的有hash值和key,根据hash值可以计算出

  具体的table中的哪个下标,使用的计算方式是:(n - 1) & hash,n代表数组的长度,效果和

  hash%n是一样的效果,不过移位计算的效果更好,先查找第一个结点是不是相等的,若相等,则直接

  返回,若不是则判断是不是树结点,若是,则循环树,获取具体的结点信息最后,循环遍历链表,

  在链表中返回对应key的结点信息

  */

12. put()

/**

     *这个方法主要是实现在map中存入key-value键值对,调用putVal方法实现的,在调用的方法里

     面会把已存在的key键对应的值进行替换,下面会在具体的方法中标注出来怎么具体实现

     * @param key 与value值关联的value值,此值是唯一的,在map集合中不能重复

     * @param value:可以通过key键过去对应的值,这个是随着key一起存入map中的,不是唯一的

     ,但通过key可以找到一一对应的value值

*/

    public V put(K key, V value) {

        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;

        if ((tab = table) == null || (n = tab.length) == 0)  //此处是判断表是否

        //初始化,若没有初始化,则初始化并获取表的长度n

            n = (tab = resize()).length; //具体调用resize进行表的大小设置

        if ((p = tab[i = (n - 1) & hash]) == null)//根据hash%n计算出新结点的下标,

        //hash%n和hash&(n-1)是一样的效果,不过取模比移位计算要慢,若table中的位置为空

            tab[i] = newNode(hash, key, value, null);//直接新建一个结点Node,放到

            //对应的table中,table的下标是根据hash值按位与上(表的长度-1)

        else {//else表示此处是出现冲突的情况进行的处理

            Node<K,V> e; K k;

            if (p.hash == hash &&  //此处是表示表中的位置出存放的是hash值相同,

            //若table表中已存在的结点key值相等(地址相等或者值相等),

            //则把第一个结点Node赋值为e结点

                ((k = p.key) == key || (key != null && key.equals(k))))

                e = p;

            else if (p instanceof TreeNode)//此处判断table中的结点Node是不是

            //树结点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);  //把新建的结点

                        //添加到链表的最后

                        if (binCount >= TREEIFY_THRESHOLD - 1) // 此处是判断链表

                      //的长度是否是等于8,若是,则把链表转换为树结构,树可以增加查询效率

                            treeifyBin(tab, hash);//此处具体实现怎么把链表转换为

                            //树结构

                        break;

                    }

                    if (e.hash == hash &&  //此处是判断,当循环结点不为空时,判断链

                    //表中的key值是否和put的key值相等,若是,则直接退出,

                        ((k = e.key) == key || (key != null && key.equals(k))))

                        break;

                    p = e;//把p结点的下一个结点赋值为p结点,进行下一次的循环

                }

            }

            if (e != null) { // 此处表示表中若已经存在新结点中的key值,则添加新

            //Node的时候,会返回map集合中已经存在的key的value值

                V oldValue = e.value;

                if (!onlyIfAbsent || oldValue == null)

                    e.value = value;

                afterNodeAccess(e);

                return oldValue;//put进集合中的key值在原有集合中已存在,则会返回

                //集合中已存在的value的值

            }

        }

        ++modCount;

        if (++size > threshold)//添加元素后,会判断表的大小是否已经超过限定的大小

        //(这个大小不是集合容量的大小,而是真正的大小*加载因子的值),一般会预留一部分

        //用于缓冲

            resize(); //此处表示,表容量的大小超过限定的大小时,对容器进行扩容。

            //扩容是向左移一位,容量变为原来的2倍,不过有一些校验,后面会有分析

        afterNodeInsertion(evict);

        return null;//若插入的结点key值不存在table中,则插入成功后,返回null

    }

www.cnblogs.com/guopengxia0…

13.HashTable与HashMap对比

  1. 线程安全:HashMap是线程不安全的类,多线程下会造成并发冲突,但单线程下运行效率较高;HashTable是线程安全的类,很多方法都是用synchronized修饰,但同时因为加锁导致并发效率低下,单线程环境效率也十分低;
  2. 插入null:HashMap允许有一个键为null,允许多个值为null;但HashTable不允许键或值为null;
  3. 容量:HashMap底层数组长度必须为2的幂,这样做是为了hash准备,默认为16;而HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;
  4. Hash映射:HashMap的hash算法通过非常规设计,将底层table长度设计为2的幂,使用位与运算代替取模运算,减少运算消耗;而HashTable的hash算法首先使得hash值小于整型数最大值,再通过取模进行散射运算;
  5. 扩容机制:HashMap创建一个为原先2倍的数组,然后对原数组进行遍历以及rehash;HashTable扩容将创建一个原长度2倍的数组,再使用头插法将链表进行反序;
  6. 结构区别:HashMap是由数组+链表形成,在JDK1.8之后链表长度大于8时转化为红黑树;而HashTable一直都是数组+链表;
  7. 继承关系:HashTable继承自Dictionary类;而HashMap继承自AbstractMap类;
  8. 迭代器:HashMap是fail-fast;而HashTable不是。

fail-fast的字面意思是“快速失败”。当我们在遍历集合元素的时候,经常会使用迭代器,但在迭代器遍历元素的过程中,如果集合的结构被改变的话,就会抛出异常,防止继续遍历。这就是所谓的快速失败机制。

结构上的改变

例如集合上的插入和删除就是结构上的改变,但是,如果是对集合中某个元素进行修改的话,并不是结构上的改变哦。

HashMap的fail-fast

迭代器的快速失败行为无法得到保证,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证。失败快速迭代器以尽力而为的方式抛出ConcurrentModificationException 。因此,编写依赖于此异常的程序以确保其正确性是错误的: 迭代器的快速失败行为应该仅用于检测错误。

14. Compare different JDK version(JDK7和JDK8)

大部分内容在上面叙述细节,因此这里只有提纲

JDK7中的HashMap

基于链表+数组实现,底层维护一个Entry数组

Entry<K,V>[] table;

JDK8中的HashMap

基于位桶+链表/红黑树的方式实现,底层维护一个Node数组(继承于Entry)

Node<K,V>[] table;

这么做主要是再查询的时间复杂度上进行优化,链表为O(n),而红黑树一直是O(logn),冲突(即为相同的hash值存储的元素个数) 超过8个,可以大大的提高查找性能。因此节点数大于8时会将链表转化为红黑树。

共同点

1.容量(capacity) :容量为底层数组的长度,JDK7中为Entry数组,JDK8中为Node数组

a. 容量一定为2的次幂

static int indexFor(int h, int length) {      return h & (length-1);  }

这段代码是用来计算出键值对存放在一个数组的索引,h是int hash = hash(key.hashCode())计算出来的,“当容量一定是2^n时,h & (length - 1) = h % length” ,按位运算特别快 。

b. 默认初始容量16(容量为低层数组的长度,JDK7中为Entry数组,JDK8中为Node数组)

c.最大容量1<<30,即2的30次方

static final int MAXIMUM_CAPACITY = 1 << 30;

2.加载因子(Load factor) :HashMap在其容量自动增加前可达到多满的一种尺度

a. 默认加载因子 = 0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f
  • 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
  • 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长) 0.75是一个"冲突的机会"与"空间利用率"之间寻找一种平衡与折衷的选择

3.扩容机制:扩容时resize(2 * table.length),扩容到原数组长度的2倍。

4.key为null:若key == null,则hash(key) = 0,则将该键-值 存放到数组table 中的第1个位置,即table [0]

 static final int hash(Object key) {

        int h;

        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

    }

不同点

1.发生hash冲突时

  • JDK7: 发生hash冲突时,新元素插入到链表头中,即新元素总是添加到数组中,旧元素移动到链表中。
  • JDK8: 发生hash冲突后,会优先判断该节点的数据结构式是红黑树还是链表,如果是红黑树,则在红黑树中插入数据;如果是链表,则将数据插入到链表的尾部并判断链表长度是否大于8,如果大于8要转成红黑树。

2.扩容时

  • JDK7: 在扩容resize()过程中,采用单链表的头插入方式,在将旧数组上的数据转移到新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 。 多线程下resize()容易出现死循环。此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现环形链表,从而在获取数据、遍历链表时形成死循环(Infinite Loop),即 死锁的状态 。
  • JDK8: 由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况 ,但jdk1.8仍是线程不安全的,因为没有加同步锁保护。

建议:

1.使用时设置初始值,避免多次扩容的性能消耗

2.使用自定义对象作为key时,需要重写hashCode和equals方法

3.多线程下,使用CurrentHashMap代替HashMap