HashMap
由数组和链表两种基本数据类型组成的数据类型。
基本操作
import java.util.HashMap; // 引入 HashMap 类
1、创建hashmap对象:
Map<Key数据类型,Value数据类型> 标识符=new HashMap<Key数据类型,Value数据类型>();
2、主要函数
hashmap.put("Key",Value);//插入键值对为“Key”,Value。
hashmap.get("Key");
hashmap.remove("Key");
hashmap.clear();
hashmap.size();//键值对数量
hashmap.keySet();//只获取Key;
hashmap.values();//只获取Values;
来源于RUNOOB.COM
实现原理
hashmap的默认大小为16,自动扩展或者手动初始化时大小也必须是2的幂次方。理由是:服务于哈希函数,目的是为了相对平均地划分键值对到对应的 index 中。
具体的过程是:每次划分键值对时,是对Key的hashcode数值使用哈希函数,得出index位置。原作者使用的方法是位运算,将Key的hashcode数值与hashmap的大小进行与运算:
index=hashcode(Key)&(Length-1);//Length为hashmap大小;
当hashmap大小为16时,hashcode与15(二进制为1111)进行与运算,最后的结果为hashcode(Key)的最后四位二进制数。这样,键值对的index位置就完全由hashcode来决定。与其他大小的hashmap相比,2次幂大小的hashmap似乎更加平均划分键值对。
注意
1、为何hashmap在链表中使用头插法(jdk7及以前),而不用尾插法?
据说是因为原作者觉得后插入的更有可能使用到。但在jdk1.8中已改为尾插法。
2、为什么改成尾插法呢?
为了安全性,防止环化。hashmap在扩容时,是重新创建一个更大的hashmap,然后将旧hashmap中的全部键值对再次哈希函数计算index插入。若是头插法,在多线程的情况下,对旧hashmap的键值对再次哈希计算时可能会导致环化。
简单来说,若线程1在读取旧hashmap中的一个链表的第一个元素时,记录下了第一个元素的Key,Value和next。这时CPU调度线程2来运行,线程2将第一个元素哈希到新hashmap中,再将第二个元素哈希到新hashmap时,若采用头插法,会将第一个元素移到第二个元素的next位置上,但是线程1记下的顺序是第一个元素的next是第二个元素。这时候就出现了1.next=2; 2.next=1;的环化情况。
2、高并发中,hashmap可能会出现死循环。
原因:见问题2. 环化的hashmap链表在读取时会陷入死循环。
3、多线程访问hashmap是不安全的。
原因:hashmap不支持线程同步。在hashmap源码中没有对get、put操作加同步锁。
4、java8中,hashmap得到了怎样的优化?
(1)java8以前hashmap采用数组+单链表的形式存储,java8在采用数组+单链表的同时,当单链表中的元素数量超过8个时改用红黑树存储,将时间复杂度由O(n)降低到O(logn)。
(2)java7使用头插法;java8使用尾插法。
(3)java7在rehash扩容时会重新计算key的哈希值进行&操作;java8在rehash时不会再计算key的哈希值(我觉得奇怪,不重新计算的话,那是有存储之前计算过的哈希值吗?),直接与容量进行&操作,得到index。
疑惑
?????????????????
1.7中是通过更改hashSeed值修改节点的hash值从而达到rehash时的链表分散,而1.8中键的hash值不会改变,rehash时根据(hash&oldCap)==0将链表分散。
?????????????????
有一说,是扩容时已经存在的元素不用再进行rehash计算,直接通过:新的hash=原来的index+原来的table大小,计算出扩容后的hash值进而计算新的index。