基于散列的集合中,HashMap应该是用的最多的键值对容器,既然用的这么频繁,我觉得还是很有必要搞清楚原理,而写出来,思路会更清晰。 先看下简单的继承体系: 从图上看,继承体系很简单,2个标记性接口,然后就是作为Map的功能,很单纯。 然后再来看看数据结构图: 图上一个空格就代表一个节点,而每个key-value对都保存在一个节点中,下面的代码正式HashMap每个节点的数据结构。 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; }
从数据结构图判断,HashMap中肯定维护着一个数组,用来存储每个节点,现在就有2个问题,如何在数组中定位自己的位置(行话叫:桶),万一桶被占用了怎么办? 既然叫HashMap,通过名字大概能够猜想得出了,是通过hash值来定位的,通过hash算出自己桶的位置,然而位置是有限的,那么通过函数映射(hash与数组长取模)到其中的位置时,必然的会出现冲突,解决冲突办法,就如同上图所示,链表法。 优化性能的设计 HashMap设计时候需要考虑速度的问题,链表连接的越长性能就越差,极端的例子,退化成单链表,时间复杂度O(N),这样就失去了快速访问的特性。 如果为了速度最快,最好的办法就是没有链表,既然是通过取模的方式进行定位,要达到最少冲突,肯定需要很长的数组,而数组越长,就越容易浪费。 为了达到理想的效果,又不浪费太多的空间,hashmap使用了一个阀值,当对象的数量达到阀值,才会进行扩容,这样就能够保证浪费的空间是在可控的有限范围内。通过调节阀值,可以使得链表的链接的节点尽量的少。 如何定义阀值?hashmap中用了负载因子这么个概念,用数组的容量*负载因子作为阀值,默认负载因子是0.75。可以自行调整,但是一般不建议,优化有时候是个很棘手的问题,不成熟的优化是所有罪恶之源,最好的策略就是置之不顾,直到你发现真正需要优化他。
基于散列的集合,除了HashMap外,常用的还有HashSet、LinkedHashSet、LinkedHashMap。而这三个都是在HashMap的基础上进行改造的,只要明白了HashMap的原理,其他的非常容易清楚。 HashSet 我们来简单分析下,在jdk中的源码:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{ static final long serialVersionUID = -5024744406713321676L; PRivate transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>();//实例化时,同时实例化HashMap }上面是摘抄HashSet源码的一段,发现没,这是组合,是代理,用hashMap进行代理,下面最后列举下最常用的功能:
public boolean add(E e) { return map.put(e, PRESENT)==null; //PRESENT为常量 } public Iterator<E> iterator() { return map.keySet().iterator(); }发了没,是委托给HashMap的。 LinkedHashMap 估计 应该都知道,它既能拥有hashmap的速度,又能有序遍历,直接看源码:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>发现没,是继承自HashMap,可以说,复用了大部分的HashMap功能。 思考 :它是如何在hashmap的基础上进行改造,使的元素有序? 我们先看下面这幅图: 大家应该都知道了,左边的是hashmap的元素节点,而右边的正是LinkedHashMap的元素节点。发现多了什么了吗?2个指针,指向前驱和指向后继。想下,如果每个元素都能够知道后面是谁,从头开始按照顺序进行元素遍历,这个还难吗?看下面创造节点的代码:
void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value, old); table[bucketIndex] = e; e.addBefore(header); size++; } private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; //取前一个节点,对于header参数来说就是最后一个节点。 before.after = this; after.before = this; }看见没,createEntry比hashmap多了一个重定位指针的操作。header节点中保存有第二个和最后一个节点的指针(不懂看上图),保证有序,只需要把现有最后一个节点的after 指向本节点,就保证新加入的元素是有序,现有最后一个节点可以通过header.before获取,同时为了下一次新加入节点能正常有序,需要把header的前节点重新地位到新的最后一个节点(也就是说header.before指向新节点)。这样再通过after指针就能顺藤摸瓜,顺序遍历了。 LinkedHashSet 看源码:
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable {好像和我们想象的不一样,不应该是LinkedHashMap吗?不急继续看其构造函数
public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }发现没,HashSet这种构造方法用的是LinkedHashMap,现在还需要写下去吗,如同hashset复用hashMap原理一样。
新闻热点
疑难解答