jdk7和jdk8版本的HashMap比较

1.HashMap

Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结构.

HashMap存储着Entry(hash, key, value, next)对象。

jdk7和jdk8版本的HashMap比较_第1张图片

 

2工作原理

jdk7和jdk8版本的HashMap比较_第2张图片

 

 

 

put函数大致的思路为:

  1. 对key的hashCode()做hash,然后再计算index;

注:相同的key,hash是相同的,不同的key,hash也可能相同,此时产生碰撞

2.如果没碰撞直接放到bucket里;

3.如果碰撞了,以链表的形式存在buckets后;

4.如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD= 8 ),就把链表转换成红黑树;

注: 即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法.

5.如果节点已经存在就替换old value(保证key的唯一性)

6.如果bucket满了(超过load factor*current capacity = 16 ),就要resize。

jdk7和jdk8版本的HashMap比较_第3张图片

jdk7和jdk8版本的HashMap比较_第4张图片

3equals()和hashCode作用

通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点

 

 

 

4hash计算

jdk7和jdk8版本的HashMap比较_第5张图片

在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。

 

3当两个对象的hashcode相同会发生什么?

因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

 

4如果两个键的hashcode相同,你如何获取值对象?

找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。因此,设计HashMap的key类型时,如果使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择

 

 

补充:

equalshashCode网上也有很多的资料。这里只是记录下我目前的理解与认识。 
大家会经常听到这样的话,当你重写equals方法时,尽量要重写hashCode方法,有些人却并不知道为什么要这样,待会就会给出源码说明这个原因。 

首先来介绍下ObjectequalshashCode方法。如下: 

Java代码  

  1. public native int hashCode();  
  2. public boolean equals(Object obj) {  
  3.         return (this == obj);  
  4.     }  


这里挺简单的,equals(obj)默认比较的是内存地址,hashCode()方法默认是native方法,看下它的文档说明: 

Java代码  

  1. /** 
  2. 关注点1 
  3.      * Returns a hash code value for the object. This method is 
  4.      * supported for the benefit of hash tables such as those provided by 
  5.      * {@link java.util.HashMap}. 
  6.      * 

     

  7.      * The general contract of {@code hashCode} is: 
  8.      * 
       
    •      * 
    • Whenever it is invoked on the same object more than once during 
    •      *     an execution of a Java application, the {@code hashCode} method 
    •      *     must consistently return the same integer, provided no information 
    •      *     used in {@code equals} comparisons on the object is modified. 
    •      *     This integer need not remain consistent from one execution of an 
    •      *     application to another execution of the same application. 
    • 关注点2 
    •      * 
    • If two objects are equal according to the {@code equals(Object)} 
    •      *     method, then calling the {@code hashCode} method on each of 
    •      *     the two objects must produce the same integer result. 
    •      * 
    • It is not required that if two objects are unequal 
    •      *     according to the {@link java.lang.Object#equals(java.lang.Object)} 
    •      *     method, then calling the {@code hashCode} method on each of the 
    •      *     two objects must produce distinct integer results.  However, the 
    •      *     programmer should be aware that producing distinct integer results 
    •      *     for unequal objects may improve the performance of hash tables. 
    •      * 
     
  9.      * 

     

  10. 关注点3 
  11.      * As much as is reasonably practical, the hashCode method defined by 
  12.      * class {@code Object} does return distinct integers for distinct 
  13.      * objects. (This is typically implemented by converting the internal 
  14.      * address of the object into an integer, but this implementation 
  15.      * technique is not required by the 
  16.      * JavaTM programming language.) 
  17.      * 
  18.      * @return  a hash code value for this object. 
  19.      * @see     java.lang.Object#equals(java.lang.Object) 
  20.      * @see     java.lang.System#identityHashCode 
  21.      */  
  22.     public native int hashCode();  


这里有三个关注点。 
关注点1:主要是说这个hashCode方法对哪些类是有用的,并不是任何情况下都要使用这个方法(此时是根本没有必要来复写此方法),而是当你涉及到像HashMapHashSet(他们的内部实现中使用到了hashCode方法)等与hash有关的一些类时,才会使用到hashCode方法。 

关注点2:推荐按照这样的原则来设计,即当equals(object)相同时,hashCode()的返回值也要尽量相同,当equals(object)不相同时,hashCode()的返回没有特别的要求,但是也是尽量不相同以获取好的性能。 

关注点3:默认的hashCode实现一般是内存地址对应的数字,所以不同的对象,hashCode()的返回值是不一样的。 

java
世界里的相同: 
Person类,含有nameage属性: 

Java代码  

  1. public class Person {  
  2.   
  3.     private String name;  
  4.     private int age;  
  5.       
  6.     public String getName() {  
  7.         return name;  
  8.     }  
  9.     public void setName(String name) {  
  10.         this.name = name;  
  11.     }  
  12.     public int getAge() {  
  13.         return age;  
  14.     }  
  15.     public void setAge(int age) {  
  16.         this.age = age;  
  17.     }  
  18.     @Override  
  19.     public boolean equals(Object obj) {  
  20.         if(!(obj instanceof Person)){  
  21.             return false;  
  22.         }  
  23.         Person tmp=(Person)obj;  
  24.         return name.equals(tmp.getName()) && age==tmp.getAge();  
  25.     }  
  26.   
  27. }  


我们认为当nameage值都相同时就是一个相同的person,所以我们可以重写equals方法如上所述,这样我们就可以调用perosn1.equals(person2)来判断他们是否相同。然而这样就完了吗?如果你不涉及其他有关hash方面的内容,这样的确可以满足你的需求了,也就是说这样做仅仅是针对部分情况是可以的,并没有针对全部情况,如若使用HashMapHashSet等还想实现person1person2相同,仅仅重写equals方法肯定是不够的,必须要重写hashCode方法。 

为什么会有Hash类型的Map 
简单理解:Map本身是存放keyvalue信息的地方,若想获取某个key1对应的value,即map.get(key1),常规思维就是拿key1和所有的key一个一个去比较,若相同,则返回对应的value。假如有10000key,要比较10000次吗?这样的效率难道不是很低下的吗?所以要改进,假如我们对key1进行某种运算直接能得到对应value的存储位置,来直接获取到value,这样不是最爽的吗?不再和其他key进行比较了,而是得到位置,直接获取对应的value。这就是HashMap等的基本原理,同时hashCode方法在得到位置信息上发挥着巨大的作用。 

接下来HashMap的源码分析这一具体过程: 

Java代码  

  1. static final Entry[] EMPTY_TABLE = {};  
  2. transient Entry[] table = (Entry[]) EMPTY_TABLE;  


HashMap内部是由Entry类型的数组table来存储数据的。来看下Entry的代码: 

Java代码  

  1. static class Entry implements Map.Entry {  
  2.         final K key;  
  3.         V value;  
  4.         Entry next;  
  5.         int hash;  
  6.   
  7.         /** 
  8.          * Creates new entry. 
  9.          */  
  10.         Entry(int h, K k, V v, Entry n) {  
  11.             value = v;  
  12.             next = n;  
  13.             key = k;  
  14.             hash = h;  
  15.         }  
  16.         //  
  17. }  


Entry有四个重要的属性,是一对keyvalue的结合,同时包含下一个Entry,就像链表一样,最后一个就是哈希值h(这个哈希值就是keyhashCode方法的返回值经过hash运算得到的值)。 
所以我们可以画出HashMap的存储结构: 

jdk7和jdk8版本的HashMap比较_第6张图片

图中的每一个方格就表示一个Entry对象,其中的横向则构成一个Entry[] table数组,而竖向则是由Entrynext属性形成的链表。 
假入我们想找编号为2value,如果我们能直接找到它所在数组中的索引便可以快速找到它,假如我们想找编号为73value,如果我们能直接找到编号7然后再继续沿着链表寻找,便可以快速找到它。 

首先看下它HashMap是如何来添加的,即 put(K key, V value)方法: 

Java代码  

  1. public V put(K key, V value) {  
  2.         if (table == EMPTY_TABLE) {  
  3.             inflateTable(threshold);  
  4.         }  
  5.         if (key == null)  
  6.             return putForNullKey(value);  
  7.         int hash = hash(key);  
  8.         int i = indexFor(hash, table.length);  
  9.         for (Entry e = table[i]; e != null; e = e.next) {  
  10.             Object k;  
  11.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  12.                 V oldValue = e.value;  
  13.                 e.value = value;  
  14.                 e.recordAccess(this);  
  15.                 return oldValue;  
  16.             }  
  17.         }  
  18.   
  19.         modCount++;  
  20.         addEntry(hash, key, value, i);  
  21.         return null;  
  22.     }  


现在先不管HashMap扩容的事情,我们重点关注它的存的过程,首先就是计算keyhash值,这个hash计算的过程便用到了key对象的hashCode方法,如下: 

Java代码  

  1. final int hash(Object k) {  
  2.         int h = hashSeed;  
  3.         if (0 != h && k instanceof String) {  
  4.             return sun.misc.Hashing.stringHash32((String) k);  
  5.         }  
  6.   
  7.         h ^= k.hashCode();  
  8.   
  9.         // This function ensures that hashCodes that differ only by  
  10.         // constant multiples at each bit position have a bounded  
  11.         // number of collisions (approximately 8 at default load factor).  
  12.         h ^= (h >>> 20) ^ (h >>> 12);  
  13.         return h ^ (h >>> 7) ^ (h >>> 4);  
  14.     }  


先不用看懂这个方法是怎么计算的,它的内容就是对keyhashCode方法返回值进行一系列的运算得到一个最终的值,这个值就是hash值,就是上文所说的Entry中的h属性的值。 
得到这个hash值后,紧接着执行了int i = indexFor(hash, table.length);就是找到这个hash值在table数组中的索引值,具体方法indexFor(hash, table.length)为: 

Java代码  

  1. static int indexFor(int h, int length) {  
  2.         return h & (length-1);  
  3.     }  


就是拿刚才生成的hash值和(table数组的长度减一)进行了相&操作,可以看到我们得到的hash值是一个很大很大的数字,和length-1&之后的值,必然是在0length-1之内,即在table数组的范围之内。得到的这个索引之后,接下来针对这个索引值对应的链表便进行放入或者替换操作。遍历这个链表,拿要放进来的key和这个链表上的每一对象的key进行下对比,看是否一致,若一致则进行替换操作,若都不一致则进行新的插入操作。 

判断是否一致的条件是:e.hash == hash && ((k = e.key) == key || key.equals(k)),一定要牢牢记住这个条件。 

必须满足的条件1hash值一样,hash值的来历就是根据keyhashCode再进行一个复杂的运算,当两个keyhashCode一致的时候,计算出来的hash也是必然一样的。 

必须满足的条件2:两个key的引用一样或者equals相同。 

综上所述,HashMap对于key的重复性判断是基于两个内容的判断,一个就是hash值是否一样(会演变成keyhashCode是否一样),另一个就是equals方法是否一样(引用一样则肯定一样)。它依据的是两个条件,所以对于上文的Person类,若想在HashMap中以person对象作为key,要满足person1对象和person2对象一样,则我们必须要重写equals方法和hashCode方法。若没有重写hashCode方法,则使用系统默认的本地hashCode方法,不同的对象的hashCode是不一样的,所以HashMap在判断时就会认为person1person2是不一样,造成了我们事与愿违的结果。 
HashMap
为什么要多引入keyhash是否一致的判断条件呢?为什么不仅仅判断equals方法是否一样? 
我认为原因如下 

好处1:当这个table数组特别大的时候,如长度为10000,根据hash&length-1这个计算的索引值,便很快的定位某一个链表下,过滤了很大一批数据,不需要一个一个遍历。仅仅依靠equals是无法实现这样的快速过滤的。 

好处2:不同的hash值得出的索引位置很可能是一样的,所以在这个链表下仍要进一步判断,此时就需要一个一个进行遍历。Entry对象中hash值是已经保存的数据,新的keyhash也已经计算出来,所以在遍历对比的过程中判断hash值是否一致是相当快的,如果不一致,则认为不相同继续下一个判断,就不会调用费时的equals方法。假如这个链表的数据也特别多,判断过程也是相当快的。也就是说,判断hash是否一致加快了在链表上的遍历的速度,减少了相对费时的equals调用次数。 

综上所述,为了实现HashMap的上述高效的存储操作,引入了hash这个重要的东西。同时带给我们的附加操作就是要满足key一致除了equals返回true外,还必须让hashCode一样。所以我们重写equals方法的时候尽量的重写hashCode方法,当用到HashMap或者HashSet等时必须要重写hashCode方法。 
hashCode的重写的原则:当equals方法返回true,则两个对象的hashCode必须一样。 
StringInteger等类都重写了equals方法和hashCode方法,都是遵循上述原则。所以我们在重写hashCode时也要遵循上述原则。 

接下来看下get(Object key)源代码的具体寻找过程: 

Java代码  

  1. public V get(Object key) {  
  2.        if (key == null)  
  3.            return getForNullKey();  
  4.        Entry entry = getEntry(key);  
  5.   
  6.        return null == entry ? null : entry.getValue();  
  7.    }  


就是找到对应keyEntry对象,有了这个对象我们便可以获取到value。继续看下是如何来找到key对应的Entry对象的: 

Java代码  

  1. final Entry getEntry(Object key) {  
  2.         if (size == 0) {  
  3.             return null;  
  4.         }  
  5.   
  6.         int hash = (key == null) ? 0 : hash(key);  
  7.         for (Entry e = table[indexFor(hash, table.length)];  
  8.              e != null;  
  9.              e = e.next) {  
  10.             Object k;  
  11.             if (e.hash == hash &&  
  12.                 ((k = e.key) == key || (key != null && key.equals(k))))  
  13.                 return e;  
  14.         }  
  15.         return null;  
  16.     }  


看到这里就会明白了这个过程,和上面put的过程类似的。 

hash&length-1结果相同我们称为冲突 
同时要思考什么样的情况下,getkey)过程是最快的?当然是hash&length-1的结果所在的数组索引下只有一个对象,还没有其他对象插入进来。也就是当所有的数据均匀分布在table上,而不是集中在table某个索引对应的连表上的时候此时get操作的效率是相当高的,为了达到这一个操作,就是要满足hash&length-1要尽可能的不同,减少冲突。 

首先看length-1:它的原因是因为要限制在table数组内,同时还有一个重要的作用就是减少冲突。首先要知道length的长度是2的幂级数,这个是HashMap来保证的,下一篇文章再说HashMap的大小及扩容。假如length7,3&(7-1) 即二进制的11&110等于10,2&(7-1),即二进制的10&11010,这就是说23这两个值不一样,却造成了一样的索引值,即产生了冲突,当length=8时,11&11111,10&11110所以避免了冲突。所以当length-1的二进制为全1时,会起到避免冲突的作用。 

接着看hash值,hash值是由keyhashCode经过hash运算得到的,为了让hash&length-1的结果尽量不产生冲突,hash的值也要尽量均匀,这就对hash算法提出了很高的要求,一个好的hash算法,会让不同的hashCode计算出来的hash值更加均匀分布。hash算法不在本文的范围之内,感兴趣的可以去研究。 

接下来顺便看看HashSet的原理: 
Set
List相比是无序的,不允许元素重复。元素重复的依据和HashMapkey的要求是一样的。即所存元素的hash值一样并且equals相同才是一样的元素。看下代码: 

Java代码  

  1. private transient HashMap map;  
  2.   
  3. private static final Object PRESENT = new Object();  


看到了没有,HashSet内部是有一个HashMap的,这个key就是HashSet的元素,而value始终是一个固定的值PRESENT 
看下HashSetadd方法: 

Java代码  

  1. public boolean add(E e) {  
  2.         return map.put(e, PRESENT)==null;  
  3.     }  


看到没有,HashSet就是依托HashMap中的key不能重复来实现HashSet中自身的元素不能重复的。

 

 

参考地址: http://blog.csdn.net/richard_jason/article/details/53887222

你可能感兴趣的:(Code编程基础知识)