HashMap存储结构

   1.1简介

  • 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
  • 所有的key构成的集合是Set:无序的、不可重复的。所以, key所在的类要重写:equals()和hashCode()
  • 所有的value构成的集合是Collection:无序的、可以重复的。所以, value所在的类要重写: equals()
  • 一个key-value构成一个entry
  • 所有的entry构成的集合是Set:无序的、不可重复的
  • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
  • HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。

 

1.2 HashMap存储结构

    1.2.1JDK1.7前存储结构

JDK 7及以前版本: HashMap是数组+链表结构(即为链地址法)

  • HashMap的内部存储结构其实是数组和链表的结合。 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组, 这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以根据索引快速的查找bucket中的元素。
  • 每个bucket中存储一个元素, 即一个Entry对象, 但每一个Entry对象可以带一个引用变量, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Entry链。而且新添加的元素作为链表的head。
  • 添加元素的过程:向HashMap中添加entry1(key, value), 需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到), 此哈希值经过处理以后, 得到在底层Entry[]数组中要存储的位置i。 如果位置i上没有元素, 则entry1直接添加成功。 如果位置i上已经存在entry2(或还有链表存在的entry3, entry4), 则需要通过循环的方法, 依次比较entry1中key和其他的entry。 如果彼此hash值不同, 则直接添加成功。 如果hash值相同, 继续比较二者是否equals。 如果返回值为true, 则使用entry1的value去替换equals为true的entry的value。 如果遍历一遍以后, 发现所有的equals返回都为false,则entry1仍可添加成功。 entry1指向原有的entry元素。
  • HashMap的扩容: 当HashMap中的元素越来越多的时候, hash冲突的几率也就越来越高, 因为数组的长度是固定的。 所以为了提高查询的效率, 就要对HashMap的数组进行扩容, 而在HashMap数组扩容之后, 最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置, 并放进去, 这就是resize。
  • 那么HashMap什么时候进行扩容呢? 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值(DEFAULT_LOAD_FACTOR)为0.75, 这是一个折中的取值。 也就是说, 默认情况下, 数组大小(DEFAULT_INITIAL_CAPACITY)为16, 那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值, 也叫做临界值) 的时候, 就把数组的大小扩展为 2*16=32, 即扩大一倍, 然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

 

1.2.2 JDK 8存储结构

JDK 8版本发布以后: HashMap是数组+链表+红黑树实现。

  • HashMap的内部存储结构其实是数组+链表+树的结合。 当实例化一个HashMap时, 会初始化initialCapacity和loadFactor, 在put第一对映射关系时, 系统会创建一个长度为initialCapacity的Node数组, 这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以根据索引快速的查找bucket中的元素。
  • 每个bucket中存储一个元素, 即一个Node对象, 但每一个Node对象可以带一个引用变量next, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Node链。 也可能是一个一个TreeNode对象, 每一个TreeNode对象可以有两个叶子结点left和right, 因此, 在一个桶中, 就有可能生成一个TreeNode树。 而新添加的元素作为链表的last, 或树的叶子结点。
  • HashMap扩容: 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值(DEFAULT_LOAD_FACTOR)为0.75, 这是一个折中的取值。 也就是说, 默认情况下, 数组大小(DEFAULT_INITIAL_CAPACITY)为16, 那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值, 也叫做临界值)的时候, 就把数组的大小扩展为 2*16=32, 即扩大一倍, 然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。
  • HashMap树化和链化: 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
  • 扩容和树化举个例子:
    • 初始情况下,hashmap 的capacity为16,因子为0.75。
    • 当hashmap桶内元素小于等于8,且size小于12时,不进行扩容和树化的操作。
    • 当hashmap桶内元素为9,因为capacity为16,因此不进行树化,而选择扩容,将capacity扩容为32。
    • 当hashmap桶内元素为10,因为capacity为32,因此不进行树化,而选择扩容,将capacity扩容为64。
    • 当hashmap桶内元素大于10,由于capacity已经达到64,此时进行树化。
    • 最后当HashMap中元素个数超过48(64*0.75=48),进行扩容
  • JDK1.8HashMap新变化
    • HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
    • 当首次调用map.put()时,再创建长度为16的数组
    • 数组为Node类型,在jdk7中称为Entry类型
    • 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
    • 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。

 

1.3HashMap源码中的重要常量

  • DEFAULT_INITIAL_CAPACITY : 默认16,HashMap的默认容量
  • MAXIMUM_CAPACITY :默认2^30, HashMap的最大支持容量
  • DEFAULT_LOAD_FACTOR:默认0.75, HashMap的默认加载因子
  • TREEIFY_THRESHOLD:默认8 Bucket中链表长度大于该默认值,转化为红黑树
  • UNTREEIFY_THRESHOLD: 默认6,Bucket中红黑树存储的Node小于该默认值,转化为链表
  • MIN_TREEIFY_CAPACITY: 默认64,桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
  • table: 存储元素的数组,总是2的n次幂
  • entrySet: 存储具体元素的集合
  • size: HashMap中存储的键值对的数量
  • modCount: HashMap扩容和结构改变的次数。
  • threshold: 扩容的临界值, =容量*填充因子
  • loadFactor: 填充因子

 

1.3.1HashMap源码的重要方法

  • resize:扩容
  • treeifyBin:树化
  • untreeify:链化

    1.扩容

  • 默认HashMap产生,capacity默认容量0,第一次使用时,扩容成16
  • 当元素个数超过12,会扩容resize
  • 每次扩容一倍

 

     2.树化

  • TREEIFY_THRESHOLD:树化的阈值,默认8
  • UNTREEIFY_THRESHOLD: 链化的阈值,默认6
  • MIN_TREEIFY_CAPACITY: 最小树化的容量,默认64,如果元素个数没有超过这个阈值(64),即使链超过8,会先进行扩容,超过64才进行树化
  • 比如下面所有元素的hashCode相同
    • 第一个元素,扩容到16
    • 第二个元素,形成链,链里面2个元素
    • ...一直到链中有8个元素
    • 第9个元素,执行resize扩容到32
    • 第10个元素,执行resize扩容到64
    • 第11个元素,执行treeifyBin()方法树化
      • Node(next) 链
      • TreeNode(left,right)红黑树

 

     3.链化

  • 链的数量小于6,执行untreeify方法变成链
public class User {

    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

//    @Override
//    public boolean equals(Object o) {
//        System.out.println("User equals()....");
        return super.equals(o);
//        if (this == o) return true; //内存地址相同返回true
//        if (o == null || getClass() != o.getClass()) return false;
//
//        User user = (User) o;
//
//        if (age != user.age) return false;
//        return name != null ? name.equals(user.name) : user.name == name;
//    }

    /**
     *      * 注:**为什么hashCode中,用31这个数子?
     *      *
     *      * - 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
     *      * - 并且31只占用5bits,相乘造成数据溢出的概率较小。
     *      * - 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。 (提高算法效率)
     *      * - 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除! (减少冲突)
     * @return
     */
    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }


}

 

测试:

public class TreeifyDemo1 {

    public static void main(String[] args) {
        Map map = new HashMap();
        User u1 = new User("马云", 45);
        User u2 = new User("马云", 45);
        User u3 = new User("马云", 45);
        User u4 = new User("马云", 45);
        User u5 = new User("马云", 45);
        User u6 = new User("马云", 45);
        User u7 = new User("马云", 45);
        User u8 = new User("马云", 45);
        User u9 = new User("马云", 45);
        User u10 = new User("马云", 45);
        User u11 = new User("马云", 45);
        User u12 = new User("马云", 45);

        map.put(u1, "a");
        map.put(u2, "a");
        map.put(u3, "a");
        map.put(u4, "a");
        map.put(u5, "a");
        map.put(u6, "a");
        map.put(u7, "a");
        map.put(u8, "a");
        map.put(u9, "a");
        map.put(u10, "a");
        map.put(u11, "a");
        map.put(u12, "a");


        map.remove(u12);
        map.remove(u11);
        map.remove(u10);
        map.remove(u9);
        map.remove(u8);
        map.remove(u7);
        map.remove(u6);
        map.remove(u5);
        map.remove(u4);
        map.remove(u3);
        map.remove(u2);
        map.remove(u1);


        System.out.println(map);

    }
}

1.3.2哈希取模算法

  • 调用对象的hashCode(),确定数组中的槽位,确定数组中的槽位,采用的哈希取模算法
  • put(key) -> (n-1) & hash
  • 要求容量必须是2的n次方
  1. 面试题:HashMap的容量,为什么必须是2的n次幂

HashMap使用n-1 & hash得到槽位地址,这个运算n必须是2的n次幂,结论当容量是2的n次幂的时候(16,32...)

hash % n = (n-1) & hash,因为位运算性能高

如果初始容量不是2的n次幂,HashMap调用tableSizeFor自动转换成大于这个数最小的2的n次幂

 

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高级的南松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值