手写一个简单的HashMap 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 package com.thx;import java.util.ArrayList;import java.util.LinkedList;import java.util.List;public class MyHashMap <K, V> { private static final int DEFAULT_CAPACITY = 16 ; private static final float DEFAULT_LOAD_FACTOR = 0.75f ; private List<LinkedList<Node<K, V>>> buckets; private int size; private float loadFactor; private static class Node <K, V> { public K key; public V value; public Node (K key, V value) { this .key = key; this .value = value; } public Node () {} } public MyHashMap () { this (DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); } public MyHashMap (int capacity) { this (capacity, DEFAULT_LOAD_FACTOR); } public MyHashMap (int capacity, float loadFactor) { this .loadFactor = loadFactor; buckets = new ArrayList <>(capacity); for (int i = 0 ; i < capacity; i++) { buckets.add(new LinkedList <>()); } } public int size () { return this .size; } private int calHash (K key) { return Math.abs(key.hashCode() % buckets.size()); } public V get (K key) { int index = calHash(key); LinkedList<Node<K, V>> bucket = buckets.get(index); Node<K, V> node = this .getNode(bucket, key); return node == null ? null : node.value; } private Node<K, V> getNode (LinkedList<Node<K, V>> bucket, K key) { for (Node<K, V> node : bucket) { if (node.key.equals(key)) { return node; } } return null ; } public void put (K key, V value) { int index = calHash(key); LinkedList<Node<K, V>> bucket = buckets.get(index); Node<K, V> node = this .getNode(bucket, key); if (node == null ) { bucket.add(new Node <>(key, value)); size++; } else { node.value = value; } if ((double ) size / buckets.size() > loadFactor) { rehash(); } } private void rehash () { int newCapacity = buckets.size()*2 ; List<LinkedList<Node<K, V>>> newBuckets = new ArrayList <>(newCapacity); for (int i = 0 ; i < newCapacity; i++) { newBuckets.add(new LinkedList <>()); } for (LinkedList<Node<K, V>> bucket : buckets) { for (Node<K, V> node : bucket) { int index = Math.abs(node.key.hashCode() % newCapacity); newBuckets.get(index).add(node); } } this .buckets = newBuckets; } public void remove (K key) { int index = this .calHash(key); LinkedList<Node<K, V>> bucket = buckets.get(index); Node<K, V> node = this .getNode(bucket, key); if (node != null ) { bucket.remove(node); size--; } } public String toString () { StringBuilder sb = new StringBuilder (); sb.append("{" ); for (LinkedList<Node<K, V>> bucket : buckets) { for (Node<K, V> node : bucket) { sb.append("(" +node.key.toString()+"," +node.value.toString()+")," ); } } sb.setCharAt(sb.length()-1 , '}' ); return sb.toString(); } public boolean containsKey (K key) { int index = this .calHash(key); LinkedList<Node<K, V>> bucket = buckets.get(index); return this .getNode(bucket, key) != null ; } }
代码测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.thx;import java.lang.reflect.Field;import java.util.ArrayList;import java.util.LinkedList;public class MyHashMapTest { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException { MyHashMap<String, Integer> map = new MyHashMap <String, Integer>(); map.put("apple" , 2 ); map.put("banana" , 3 ); map.put("orange" , 5 ); map.put("apple" , 6 ); map.remove("banana" ); System.out.println(map); System.out.println(map.size()); for (int i = 0 ; i < 20 ; i++) { map.put("apple_" +i, i); } System.out.println(map); System.out.println(map.size()); Field buckets = map.getClass().getDeclaredField("buckets" ); buckets.setAccessible(true ); ArrayList o = (ArrayList) buckets.get(map); System.out.println(o.size()); System.out.println(map.containsKey("apple" )); System.out.println(map.containsKey("banana" )); } }
测试结果 1 2 3 4 5 6 7 8 9 {(orange,5),(apple,6)} 2 {(apple_10,10),(apple_11,11),(apple_12,12),(apple_13,13),(apple_14,14),(apple_15,15),(apple_16,16),(apple_17,17),(apple_9,9),(apple_18,18),(apple_8,8),(apple_19,19),(apple_7,7),(apple_6,6),(apple_5,5),(apple_4,4),(orange,5),(apple_3,3),(apple_2,2),(apple_1,1),(apple_0,0),(apple,6)} 22 32 true false 进程已结束,退出代码0
★还可以改进的地方 数据结构还可以再灵活一点 可以不用Java自带的LinkedList,而是定义Entry<K, V>
这样一个数据结构,并且用Node、TreeNode去继承Entry<K, V>
,从而实现程序的可扩展性。在源码中,Entry 是一个接口,用于表示键值对,Node 是一个单链表节点的内部类,用于在 HashMap 的桶中存储键值对,TreeNode 是一个红黑树节点的内部类,用于在 HashMap 的桶中存储键值对 。
当链表长度超过阈值时进行树化 还是接着上面那点,如果哈希冲突过多,就会影响访问效率,因此将链表转为红黑树是合理的。在HashMap源码中,首先会判断table的长度是否大于64,如果小于64,就通过扩容的方式来解决,避免红黑树结构化。
以下是它的getNode方法的源码,在遍历的时候根据first instanceof TreeNode
来判断当前是链表还是红黑树。
1 2 3 4 5 6 7 8 9 10 11 12 13 if ((e = first.next) != null ) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null ); }
当然了,如果嫌手写红黑树麻烦的话,那就用TreeMap吧!
哈希算法还可以再灵活一点 我们可以参考一下源码是怎么实现的。
哈希值的计算 在源码中,会使用”扰动函数” 的算法对初始哈希值进行变换,以消除低位的取模运算时丢掉高位所引起的误差 。
1 2 3 4 5 6 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向0靠拢,采用|运算计算出来的值会向1靠拢。
定位函数
本质上还是取余的操作。只不过n始终是2的幂,所以每一种情况出现的可能性都是相等的。
迭代器遍历 迭代器模式是设计模式的一种,它可以对外提供统一的访问方式,而不用关心底层的实现。在HashMap源码中,定义了Iterator这个接口类型 的成员变量,在HashMap中进行了实现,名字叫做HashIterator,是一个抽象类。EntryIterator、KeyIterator、ValueIterator是它的子类,对外提供三种不同的访问方式。HashIterator的源码如下,它虽然没有直接继承Iterator,但还是直接重写了Iterator中的next和hasNext方法。另外,还有modCount这种快速失败机制,之前在将List的时候说过,在这里不再赘述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 abstract class HashIterator { Node<K,V> next; Node<K,V> current; int expectedModCount; int index; HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null ; index = 0 ; if (t != null && size > 0 ) { do {} while (index < t.length && (next = t[index++]) == null ); } } public final boolean hasNext () { return next != null ; } final Node<K,V> nextNode () { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException (); if (e == null ) throw new NoSuchElementException (); if ((next = (current = e).next) == null && (t = table) != null ) { do {} while (index < t.length && (next = t[index++]) == null ); } return e; } public final void remove () { Node<K,V> p = current; if (p == null ) throw new IllegalStateException (); if (modCount != expectedModCount) throw new ConcurrentModificationException (); current = null ; removeNode(p.hash, p.key, null , false , false ); expectedModCount = modCount; } } final class KeyIterator extends HashIterator implements Iterator <K> { public final K next () { return nextNode().key; } } final class ValueIterator extends HashIterator implements Iterator <V> { public final V next () { return nextNode().value; } } final class EntryIterator extends HashIterator implements Iterator <Map.Entry<K,V>> { public final Map.Entry<K,V> next () { return nextNode(); } }
使用设计模式的思想来优化程序的扩展性 我们可以从源码的实现来学习到如下的软件设计原则:
依赖倒置原则 正如上面提到的,Map是一种映射的关系,实现有很多,所以大部分情况下都是依赖的是抽象的Map接口,而不是HashMap这样的实现类,体现的了依赖倒置的原则。再比如迭代器的访问都是统一的,大多数情况下我们都直接使用统一的访问方式,所以依赖的是抽象的Iterator接口而不是像EntryIterator这样具体的接口。还有,哈希桶依赖的是Node这样抽象的父类,可以使得 HashMap 可以灵活地调整内部实现。
开闭原则 HashMap 类是开放扩展的,可以通过继承或实现接口来定制自己的 HashMap 实现。例如,LinkedHashMap 类就是继承自 HashMap,增加了按照插入顺序或访问顺序迭代的功能。
总结 Map可以说是Java工程中最常用的数据结构了,因为像映射关系、缓存、去重等使用的场景有很多。只有在了解了它的底层原理之后,我们才能更高效、更合理地去使用它。我们不仅要弄明白它的设计思想,更要将其运用在实际的代码当中。