HashMap结构图及特点

books HashMap特点

  • 允许空值和空键,一般情况下能直接根据hashcode找到元素(指除了hash冲突的情况),效率高
  • 线程不安全,可以采用ConcurrentHashMap或Collections.synchronizedMap来达到线程安全目的
  • 底层采用拉链法(数组+链表(java8中还包括红黑树))

对于HashMap和其它Map的区别,可大致参考如下:

截图取自《java编程思想》

 

books jdk1.7版本HashMap.put()方法

结合put源码来看下,调用map.put()方法时候到底做了哪些事情?

jdk1.7-HashMap源码

 核心步骤:

1、判断存放entry的数组table是否为空数组,为空则先初始化;

2、key如果为空,则统一放在数组中索引为0的位置,因为null的hash值总是0,在putForNullKey(value)方法中可以看到;

3、key不为空,对key进行hash得到"尽可能唯一"的hash值;

4、根据hash值和数组length通过&运算得到该entry在数组中的索引值 i;

5、判断hash值、key是否和数组中已有相同的key,相同则用新值覆盖旧值;

6、不相同则调用addEntry()方法,判断是否需要扩容,然后在table[i]对应位置上实例化Entry;

7、在上述步骤5的for循环判断中,把entry的next总是指向了下一个entry(没有的话就为entry.next=null);

put方法里面解决了两个关键性问题,其一是由hash值和数组长度经过&运算得到的索引值,通过这个索引能快速定位到该entry(key,value)。其二是解决hash冲突的问题。当生成了两个hash值相同但是它们对应的key又不相同时,就发生了hash冲突,这时hashmap的实现里面的链表就发挥作用了,通过代码可知hashmap集合里的数组结构中存放的是Entry,里面有key、value、next、hash属性,这里的next指向的就是下一个entry,也就是说,发生hash冲突的多个Entry,先进的会放在链表头部,通过next属性"链式的"指向下一个entry对象,简图如下:

put方法里面还有一个比较重要的是扩容,当hashmap里的数组长度超过threshold=loadFactory*capacity时(默认0.75*16=12)即开始扩容,核心即新建数组,把之前的数组通过transfer方法写入到新数组中:

jdk1.7-HashMap源码

books jdk1.7版本HashMap get()方法

get方法相对简单,好理解。如果key=null,直接获取数组下标为0的entry,否则根据hash值找到数组中的Entry,如果Entry中的next指向不为空,说明是链表结构,则循环链表判断key是否相等,相等直接返回对应Entry的value值,否则返回null。

这里可以看到,当链表长度非常大的时候(说明hash冲突较多),遍历链表取值操作是比较耗性能的,因为链表索引定位数据慢,修改或新增操作快,尤其是当元素位于链表的最末端时,复杂度是O(n)。

这里要注意的是,作为hashmap对象的key一定要同时重写hashcode和equals方法,,否则可能出现put成功但是却get不到数据的情况。原因很简单,要想保证key元素的唯一性,必须同时覆盖equals和hashcode方法,详见如下测试用例:

    public static void main(String[] args) {
        Map<User, Integer> map = new HashMap<>();
        map.put(new User("tom"), 20);
        map.put(new User("jack"), 25);
        map.put(new User("tom"), 20);
        Set<Map.Entry<User, Integer>> entries = map.entrySet();
        for (Map.Entry<User, Integer> entry : entries) {
            System.out.println(entry.getKey() + "-" + entry.getValue());
        }
    }

    public static class User {
        private String name;

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

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

输出结果为:(预期map中name=tom的key对象只有一个的,但是由于User类没有重写equals和hashCode方法……)

User{name='tom'}-20
User{name='tom'}-20
User{name='jack'}-25

books HashMap存在哪些问题?

由HashMap源码可知,hashmap存在几点不足的地方:

1、线程不安全,现象表现为

  • 并发put碰撞导致数据丢失:多线程同时put时,如果计算出来的hashcode相同就会放在数组的同一位置,可能造成数据丢失;
  • 并发put扩容导致数据丢失:多线程同时put发现同时需要扩容,扩容动作涉及到把数组拷贝到新数组中,扩容完成后只会有一个数组被保留下来,也就可能造成数据丢失;
  • 并发put导致死循环CPU100%:多线程并发扩容时导致循环链表

2、循环迭代中不允许修改内容;

3、扩容问题,hashmap resize时候涉及到数组拷贝等操作,比较耗资源,所以我们可以预估放在hashmap数据量并在初始化hashmap时候指定其数组容量。HashMap的扩容相关问题也经常在面试中出现:什么时候扩容? 如何扩容?;

books jdk1.7版本和1.8版本的HashMap结构图

HashMap从jdk1.7到1.8一个很重要的变化就是jdk8中引入红黑树的结构,由jdk7的数组+链表结构升级为jdk8的数组+链表+红黑树,当链表上的节点数超过了8(默认)会自动将链表结构转换成红黑树结构,用于解决链表过长的问题,查询遍历复杂度也由链表的O(N)降为了红黑树的O(logN)。

books 链表长度为什么超过8要转为红黑树?

  • 默认的是链表结构,并不是一开始就是红黑树结构,因为链表比红黑数占用的空间较少;
  • hash冲突导致链表长度真正达到8的概率极小,约为一千万分之一,同时也考虑到红黑树查询比链表快;

books 其它相关

1、如何解决hash冲突?

通过单链表结构,java8中如果链表长度超过8则转换为红黑树来加快查询

2、hash函数是如何降低hash冲突的?

通过"异或"操作,对hashcode的高16位与低16位进行异或求模,增加了散列度,降低冲突

3、为什么HashMap的初始化容量为2的倍数?

  • 和操作系统有关,申请内存时候避免内存碎片;
  • 计算机擅长移位操作,不擅长加减乘除操作,移位操作对计算机来说效率高,提高运算速度;
  • 参与到hash函数中,能提高hash函数的散列度,减少hash冲突;
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值