Java集合框架之HashMap
我们知道 Java 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。今天我们就来介绍一下最常见的HashMap。
什么是HashMap?
简单的来说,HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。他通过给定的key来取得对应的value值。
如何使用HashMap?
Talk is cheap,show me the code.接下来就进入代码环节。
public static void main(String[] args) { HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("222", "bbb"); hashMap.put("111", "aaa"); hashMap.put("333", "ccc"); for (Map.Entry<String, String> entry : hashMap.entrySet()) { System.out.println("Key: " + entry.getKey() + " Value: " + entry.getValue()); } }
在上述的代码中,我们通过简单的对hashmap存入值并遍历,来简单的体会了它的用法。
那么上述HashMap的key是简单类型的,那么如果key是自定义的类应该怎么办呢?
使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals()。为什么呢?这是因为我们如果不重写这两个方法,无论什么情况下两个对象都会被判定为不同,我们也就无法进行我们需要的业务操作。
其实HashMap用途多种多样,我们如果想要深入的理解HashMap就必须深入研究。
HashMap的原理?
JDK1.8之前
JDK1.8之前JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。Hashmap基于数组实现的,通过对key的hashcode & 数组的长度得到在数组中位置,如当前数组有元素,则数组当前元素next指向要插入的元素,这样来解决hash冲突的,形成了拉链式的结构。put时在多线程情况下,会形成环从而导致死循环。数组长度一般是2n,从0开始编号,所以hashcode & (2n-1),(2n-1)每一位都是1,这样会让散列均匀。
JDK1.8之后
HashMap在JDK1.8的版本中引入了红黑树结构做优化,当链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
我们通过HashMap中put方法来解析一下HashMap的源码。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ 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 = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof 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) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。
- 首先会判断桶是否为空,如果为空就执行resize方法。
-
根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
-
如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e。
-
如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
-
如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
-
接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
-
如果在遍历过程中找到 key 相同时直接退出遍历。
-
如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
-
最后判断是否需要进行扩容。
以上就是这篇文章的内容,希望对你有所帮助。