深入理解Java中的HashMap的实现原理
Posted zhchoutai
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解Java中的HashMap的实现原理相关的知识,希望对你有一定的参考价值。
HashMap继承自抽象类AbstractMap,抽象类AbstractMap实现了Map接口。关系图例如以下所看到的:
详细实现例如以下:
import java.util.*; public class SimpleMap<K,V> extends AbstractMap<K,V> { //keys存储全部的键 private List<K> keys = new ArrayList<K>(); //values存储全部的值 private List<V> values = new ArrayList<V>(); /** * 该方法获取Map中全部的键值对 */ @Override public Set entrySet() { Set<Map.Entry<K, V>> set = new SimpleSet<Map.Entry<K,V>>(); //keys的size和values的size应该一直是一样大的 Iterator<K> keyIterator = keys.iterator(); Iterator<V> valueIterator = values.iterator(); while(keyIterator.hasNext() && valueIterator.hasNext()){ K key = keyIterator.next(); V value = valueIterator.next(); SimpleEntry<K,V> entry = new SimpleEntry<K,V>(key, value); set.add(entry); } return set; } @Override public V put(K key, V value) { V oldValue = null; int index = this.keys.indexOf(key); if(index >= 0){ //keys中已经存在键key,更新key相应的value oldValue = this.values.get(index); this.values.set(index, value); }else{ //keys中不存在键key,将key和value作为键值对加入进去 this.keys.add(key); this.values.add(value); } return oldValue; } @Override public V get(Object key) { V value = null; int index = this.keys.indexOf(key); if(index >= 0){ value = this.values.get(index); } return value; } @Override public V remove(Object key) { V oldValue = null; int index = this.keys.indexOf(key); if(index >= 0){ oldValue = this.values.get(index); this.keys.remove(index); this.values.remove(index); } return oldValue; } @Override public void clear() { this.keys.clear(); this.values.clear(); } @Override public Set keySet() { Set<K> set = new SimpleSet<K>(); Iterator<K> keyIterator = this.keys.iterator(); while(keyIterator.hasNext()){ set.add(keyIterator.next()); } return set; } @Override public int size() { return this.keys.size(); } @Override public boolean containsValue(Object value) { return this.values.contains(value); } @Override public boolean containsKey(Object key) { return this.keys.contains(key); } @Override public Collection values() { return this.values(); } }
事实上我们仅仅要重写entrySet和put方法,该类就能够正确执行。那我们为什么还要重写剩余的那些方法呢?AbstractMap这种方法做了非常多处理操作。Map中的非常多方法在AbstractMap都实现了,而且非常多方法都依赖于entrySet方法,举个样例。Map接口中的values方法是让我们返回该Map中全部的值的Collection。我们能够看一下AbstractMap中对values方法的实现:
public Collection<V> values() { if (values == null) { values = new AbstractCollection<V>() { public Iterator<V> iterator() { return new Iterator<V>() { private Iterator<Entry<K,V>> i = entrySet().iterator(); public boolean hasNext() { return i.hasNext(); } public V next() { return i.next().getValue(); } public void remove() { i.remove(); } }; } public int size() { return AbstractMap.this.size(); } public boolean isEmpty() { return AbstractMap.this.isEmpty(); } public void clear() { AbstractMap.this.clear(); } public boolean contains(Object v) { return AbstractMap.this.containsValue(v); } }; } return values; }
大家能够看到。代码不少。主要的思路是先通过entrySet生成包括全部键值对的Set,然后通过迭代获取当中的value值。当中生成包括全部键值对的Set肯定须要开销。所以我们在自己的实现里面重写了values方法,就一句话,return this.values,直接返回我们的values字段。
所以我们重写大部分方法的目的都是让方法的实现更快更简洁。
下面是我们自己实现的键值对类SimpleEntry。实现了Map.Entry<K,V>接口,代码例如以下:
import java.util.Map; //Map中存储的键值对,键值对须要实现Map.Entry这个接口 public class SimpleEntry<K,V> implements Map.Entry<K, V>{ private K key = null;//键 private V value = null;//值 public SimpleEntry(K k, V v){ this.key = k; this.value = v; } @Override public K getKey() { return this.key; } @Override public V getValue() { return this.value; } @Override public V setValue(V v) { V oldValue = this.value; this.value = v; return oldValue; } }
下面是我们自己实现的集合类SimpleSet,继承自抽象类AbstractSet<K,V>,代码例如以下:
import java.util.AbstractSet; import java.util.ArrayList; import java.util.Iterator; public class SimpleSet<E> extends AbstractSet<E> { private ArrayList<E> list = new ArrayList<E>(); @Override public Iterator<E> iterator() { return this.list.iterator(); } @Override public int size() { return this.list.size(); } @Override public boolean contains(Object o) { return this.list.contains(o); } @Override public boolean add(E e) { boolean isChanged = false; if(!this.list.contains(e)){ this.list.add(e); isChanged = true; } return isChanged; } @Override public boolean remove(Object o) { return this.list.remove(o); } @Override public void clear() { this.list.clear(); } }
我们測试下我们写的SimpleMap这个类,測试包括两部分。一部分是測试我们写的SimpleMap是不是正确,第二部分測试性能怎样,測试代码例如以下:
import java.util.HashMap; import java.util.HashSet; import java.util.Map; public class Test { public static void main(String[] args) { //測试SimpleMap的正确性 SimpleMap<String, String> map = new SimpleMap<String, String>(); map.put("iSpring", "27"); System.out.println(map); System.out.println(map.get("iSpring")); System.out.println("-----------------------------"); map.put("iSpring", "28"); System.out.println(map); System.out.println(map.get("iSpring")); System.out.println("-----------------------------"); map.remove("iSpring"); System.out.println(map); System.out.println(map.get("iSpring")); System.out.println("-----------------------------"); //測试性能怎样 testPerformance(map); } public static void testPerformance(Map<String, String> map){ map.clear(); for(int i = 0; i < 10000; i++){ String key = "key" + i; String value = "value" + i; map.put(key, value); } long startTime = System.currentTimeMillis(); for(int i = 0; i < 10000; i++){ String key = "key" + i; map.get(key); } long endTime = System.currentTimeMillis(); long time = endTime - startTime; System.out.println("遍历时间:" + time + "毫秒"); } }
//创建HashMap的实例 HashMap<String, String> map = new HashMap<String, String>(); //測试性能怎样 testPerformance(map);
@Override public V put(K key, V value) { V oldValue = null; int index = this.keys.indexOf(key); if(index >= 0){ //keys中已经存在键key,更新key相应的value oldValue = this.values.get(index); this.values.set(index, value); }else{ //keys中不存在键key,将key和value作为键值对加入进去 this.keys.add(key); this.values.add(value); } return oldValue; }
须要性能开销的主要是this.keys.indexOf(key)这句代码,这句代码从ArrayList中查找指定元素的索引,本质就是从数组开头走,往后找。直至数组的末尾。例如以下图所看到的:
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; }
在put方法中,。调用了对象的hashCode方法,该方法返回一个int类型的值。是个初始的哈希值,这个值就相当于车牌号,比如"鲁E.DE829",HashMap中有个hash方法。该hash方法将我们得到的初始的哈希值做进一步处理。得到终于的哈希值,就好比我们将车牌号传入hash方法。然后返回该存放车辆的大桶,即返回"鲁",这样HashMap就把这辆车放到标有“鲁”的大桶里面了。上面说到的hash方法叫做哈希函数。专门负责依据传入的值返回指定的终于哈希值。详细实现例如以下:
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); }
这里简单说一下哈希函数,哈希函数有多种实现方式。比方最简单的就是取余法,比方对i%10取余,然后依照余数创建不同的区块或桶。
比方有100个数,各自是从1到100,那么分别对10取余,那么就能够把这100个数放到10个桶子里面了,这就是所谓的哈希函数。
仅仅只是HashMap中的hash函数看起来比較复杂,进行的是位操作,可是其作用与简单的取余哈希法的作用是等价的。就是把元素分类放置。
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
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; }
在get方法中。也是先调用了对象的hashCode方法。就相当于车牌号,然后再将该值让hash函数处理得到终于的哈希值,也就是桶的索引。
然后我们再去这个标有“鲁”的桶里面去找我们的键值对,首先先取出桶里面第一个键值对,比对一下是不是我们要找的元素,假设是就直接返回了。假设不是就通过键值对的next顺藤摸瓜通过单向链表继续找下去,直至找到。 例如以下图所看到的:
import java.util.HashMap; public class Car { private final String num;//车牌号 public Car(String n){ this.num = n; } public String getNum(){ return this.num; } @Override public boolean equals(Object obj) { if(obj == null){ return false; } if(obj instanceof Car){ Car car = (Car)obj; return this.num.equals(car.num); } return false; } public static void main(String[] args){ HashMap<Car, String> map = new HashMap<Car, String>(); String num = "鲁E.DE829"; Car car1 = new Car(num); Car car2 = new Car(num); System.out.println("Car1 hash code: " + car1.hashCode()); System.out.println("Car2 hash code: " + car2.hashCode()); System.out.println("Car1 equals Car2: " + car1.equals(car2)); map.put(car1, new String("Car1")); map.put(car2, new String("Car2")); System.out.println("map.size(): " + map.size()); } }我们在main函数中写了一些測试代码,我们创建了一个HashMap。该HashMap的用Car作为键,用字符串作为值。我们用同一个字符串实例化了两个Car,分别为car1和car2,然后将这两个car都放入到HashMap中,输出结果例如以下:
Car2 hash code: 2027651571
Car1 equals Car2: true
map.size(): 2
通过我们前面研究可知,假设是两个元素相等。那么这两个元素应该放到同一个HashMap的桶里。可是因为我们的car1和car2的hashCode不同,所以HashMap将car1和car2分别放到不同的桶子里面了,这就出问题了。相等(equals)的两个元素(car1和car2)假设hashCode返回值不同,那么这两个元素就会放到HashMap不同的区间里面。
所以我们写代码的时候要保证相互equals的两个对象的哈希值必然要相等。即必须保证hashCode的返回值相等。那怎样解决问题?我们仅仅须要重写hashCode方法就可以,代码例如以下:
@Override public int hashCode() { return this.num.hashCode(); }又一次执行main中的測试代码,输出结果例如以下:
Car2 hash code: 607836628
Car1 equals Car2: true
map.size(): 1
下次查找该对象的时候,还是计算其哈希值,依据哈希值确定区块或桶,然后在这个小范围内查找元素,这样就快多了。