今天我们来看一下HashMap 一.Hash表的介绍 在学习数据结构的时候,我们有学过一种查找方式叫作哈希查找 我们举个例子说明: 例如内存中有这样的8个位置 这个8个位置就构成了一张hash表 3.0 1 2 3 4 5 6 7 现在我有一个对象需要存储在以上8个位置之一,该对象中有一个属性key,如果不用hashCode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。 但如果用hashCode那就会使效率提高很多。 我们定义我们的hashCode为key%8,然后把我们的类存放在取得得余数那个位置。比如我们的key为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果key是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过key除 8求余数直接找到存放的位置了。 这就是hash查找的原理。 但是如果连个对象的hashCode相同时就会造成hash冲突,10/8为2.18/8也为2,我们必须要解决hash冲突,将hash值一样的放在同一个hash“桶”里。 二.HashMap的数据结构 我们的HashMap的数据结构就是基于上述的列子形成的。 HashMap 的底层实现是数组和链表 链表使用头插法。 数组就是Hash表 链表就是为了防止Hash冲突 我们看代表HashMap的底层数据结构的内部类
/** Entry是单向链表。 * 它是 “HashMap链式存储法”对应的链表。 *它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数 **/ static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; // 指向下一个节点 Entry<K,V> next; final int hash; // 构造函数。 // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)" Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判断两个Entry是否相等 // 若两个Entry的“key”和“value”都相等,则返回true。 // 否则,返回false public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } // 实现hashCode() public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } // 当向HashMap中添加元素时,绘调用recordAccess()。 // 这里不做任何处理 void recordAccess(HashMap<K,V> m) { } // 当从HashMap中删除元素时,绘调用recordRemoval()。 // 这里不做任何处理 void recordRemoval(HashMap<K,V> m) { } }HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象, 它就是用来处理hash冲突的,形成一个链表。
HashMap中的关键属性 transient Entry[] table; transient int size;//存放元素的个数 int threshold;//临界值 当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量 final float loadFactor; transient int modCount; 其中loadFactor加载因子是表示Hsah表中元素的填满的程度.
若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高.
因此,必须在 “冲突的机会”与”空间利用率”之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的”时-空”矛盾的平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。 三.存储 我们来看一下HashMap的数据储存
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }我们来分析一下这段代码,HashMap中允许Null值存在,即key为null,至于Value是不是为null,HashMap不关心。null所对应的hashCode值默认为0,所以key为null时,对应hash表中为0的位置的hash桶中,由于HashMap中一个key(key.equals(key1)为true)只能对应一个Value,所以不用插入链表,而是直接替换掉原值。
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }如果key不为null,则调用hash方法,参数为key.hashCode返回的整数值
static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }使用hash方法,生成一个整数,降低不同key的hash值重复的几率,提高存储的效率。 之后接着执行indexFor(hash, table.length);
static int indexFor(int h, int length) { return h & (length-1); }得到该key对应的hash表的位置, 如果hash表中该索引已经有数据了,则用头插法将该数据插入链表中(即放到这个hash桶中) 如果有key.equals()也就是之前有这个key值了,替换原来的value的值 这里有一个细节我们需要注意 return h & (length-1); 一般来说,我们都是和length进行与操作,而这里是和length-1,为什么呢。 我们来分析一下,看HashMap源码可知,length是2的幂也就意味着length-1是一个奇数,我们知道,和一个奇数与,最后可能是奇数也可能是偶数,但是和偶数与就一定只能是偶数,因为最后一位肯定为0。而这就意味着浪费了差不多一半的空间。所以这里采用和length-1与。
get(Object)方法 寻找某个元素也是一样,先通过hashcode值找到元素所在的hash桶,然后通过key.equals()找到对应的Value值。
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }四.遍历 我们在遍历HashMap时一般采用以下方法
public class TestHashMap { public static void main(String[] args) { // TODO Auto-generated method stub Map<String, String> map = new HashMap<String, String>(); map.put("1", "kobe"); map.put("2", "james"); map.put("3", "paul"); Iterator<Map.Entry<String, String>> itEntry = map.entrySet().iterator(); while (itEntry.hasNext()) { Map.Entry<String, String> entry = itEntry.next(); System.out.println("KEY:" + entry.getKey() + "Value:" + entry.getValue()); } for (Map.Entry<String, String> myEntry : map.entrySet()) { System.out.println("KEY:" + myEntry.getKey() + "Value:" + myEntry.getValue()); } Iterator<String> itKeySet = map.keySet().iterator(); while (itKeySet.hasNext()) { String key = itKeySet.next(); System.out.println("KEY:" + key + "VALUE:" + map.get(key)); } for (String key : map.keySet()) { System.out.println("KEY:" + key + "VALUE:" + map.get(key)); } // Map<String, String> map1 = new TreeMap<String, String>(); }我们可以在上述代码中看出,HashMap利用的是Iterator迭代器进行的遍历,但是我们知道HashMap实现了Map接口并没有实现Collection接口(Map和Collection是平级的,Collection有List和Set),是没有重写iterator方法的。为什么可以用iterator()方法呢?我们观察HashMap的源码发现
public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); } private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } } public Set<Map.Entry<K,V>> entrySet() { return entrySet0(); } private Set<Map.Entry<K,V>> entrySet0() { Set<Map.Entry<K,V>> es = entrySet; return es != null ? es : (entrySet = new EntrySet()); } private final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public Iterator<Map.Entry<K,V>> iterator() { return newEntryIterator(); } public boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<K,V> e = (Map.Entry<K,V>) o; Entry<K,V> candidate = getEntry(e.getKey()); return candidate != null && candidate.equals(e); } public boolean remove(Object o) { return removeMapping(o) != null; } public int size() { return size; } public void clear() { HashMap.this.clear(); } }原来HashMap中有着两个内部类,分别代表这一个Set集合,都继承了AbstractSet类, 一个是装有key的keySet 集合,一个EntrySet集合 Set集合实现了iterator()方法。故可以通过Iterator迭代器遍历。 这里也说明了内部类的一个好处,一定程度打破了单继承的限制。对于内部类以后会写博客专门探讨一下。 我们具体看一下KeySet和EntrySet中迭代器的实现 先看KeySet
Iterator<K> newKeyIterator() { return new KeyIterator(); } Iterator<K> newKeyIterator() { return new KeyIterator(); } private final class KeyIterator extends HashIterator<K> { public K next() { return nextEntry().getKey(); } } private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } } public final boolean hasNext() { return next != null; } final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); Entry<K,V> e = next; if (e == null) throw new NoSuchElementException(); if ((next = e.next) == null) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } current = e; return e; } public void remove() { if (current == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); Object k = current.key; current = null; HashMap.this.removeEntryForKey(k); expectedModCount = modCount; } } final Entry<K,V> removeEntryForKey(Object key) { int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; //(k = e.key) == key || (key != null && key.equals(k))) //(k = e.key) == key 对应primitive type //(key != null && key.equals(k))) //防止空指针 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }我们来看看HashIterator这个类,该类页是HashMap的内部类,这个类是一个抽象类,实现了Iterator接口,因为是抽象类,所以不必显示的实现Iterator的方法,所以HashIterator在这里没有显式的实现Iterator的next()方法。而在KeyIterator中实现了next()方法
Iterator<Map.Entry<K,V>> newEntryIterator() { return new EntryIterator(); } private final class EntryIterator extends HashIterator<Map.Entry<K,V>> { public Map.Entry<K,V> next() { return nextEntry(); } }五.解答为什么要重写hashCode方法 **通过对HashMap的讲解,我相信对为什么要重写hashCode有了解了吧。在HashMap元素查找时,我们必须重写hashcode。因为我们要先通过hashCode找到hash桶,然后再从hash桶(链表中)通过equals找到我们要的Value。这时如果不重写hashCode,使用Object类的hashCode就无法找到hash桶了。 在元素插入时,如果不重写hashCode使用Object类的hashCode,就极易发生hash冲突,大大降低插入元素的效率。 所以我们一定要重写hashCode!!!!!!!**
