HashMap HashTable 深刻理解

在java编程语言中,最基本的结构就是两种,一个是数组,另一个是模拟指针(引用),全部的数据结构均可以用这两个基本结构来构造的,hashmap也不例外。Hashmap其实是一个数组和链表的结合体(在数据结构中,通常称之为“链表散列“),请看下图(横排表示数组,纵排表示数组元素【其实是一个链表】)。 前端

从图中咱们能够看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组。咱们来看看java代码: 
java

Java代码  收藏代码算法

  1. /** 编程

  2.      * The table, resized as necessary. Length MUST Always be a power of two. 数组

  3.      *  FIXME 这里须要注意这句话,至于缘由后面会讲到 缓存

  4.      */  数据结构

  5.     transient Entry[] table;  多线程

Java代码  收藏代码并发

  1. static class Entry<K,V> implements Map.Entry<K,V> {  编程语言

  2.         final K key;  

  3.         V value;  

  4.         final int hash;  

  5.         Entry<K,V> next;  

  6. ..........  

  7. }  

        上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。 
         当咱们往hashmap中put元素的时候,先根据key的hash值获得这个元素在数组中的位置(即下标),而后就能够把这个元素放到对应的位置中了。若是这个元素所在的位子上已经存放有其余元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,而后经过key的equals方法在对应位置的链表中找到须要的元素。从这里咱们能够想象获得,若是每一个位置上的链表只有一个元素,那么hashmap的get效率将是最高的,可是理想老是美好的,现实老是有困难须要咱们去克服,哈哈~ 
二、hash算法 
咱们能够看到在hashmap中要找到某个元素,须要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,因此咱们固然但愿这个hashmap里面的元素位置尽可能的分布均匀些,尽可能使得每一个位置上的元素数量只有一个,那么当咱们用hash算法求得这个位置的时候,立刻就能够知道对应位置的元素就是咱们要的,而不用再去遍历链表。 

因此咱们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来讲是比较均匀的。可是,“模”运算的消耗仍是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样作的,
 

Java代码  收藏代码

  1. static int indexFor(int h, int length) {  

  2.        return h & (length-1);  

  3.    }  



首先算得key得hashcode值,而后跟数组的长度-1作一次“与”运算(&)。看上去很简单,其实比较有玄机。好比数组的长度是2的4次方,那么hashcode就会和2的4次方-1作“与”运算。不少人都有这个疑问,为何hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为何数组大小为2的幂时hashmap访问的性能最高。 
         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,可是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就须要遍历这个链表,获得8或者9,这样就下降了查询的效率。同时,咱们也能够发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费至关大,更糟的是这种状况中,数组可使用的位置比数组长度小了不少,这意味着进一步增长了碰撞的概率,减慢了查询的效率!
 
 
          因此说,当数组长度为2的n次幂的时候,不一样的key算得得index相同的概率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的概率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 
          说到这里,咱们再回头看一下hashmap中默认的数组大小是多少,查看源代码能够得知是16,为何是16,而不是15,也不是20呢,看到上面annegu的解释以后咱们就清楚了吧,显然是由于16是2的整数次幂的缘由,在小数据量的状况下16比15和20更能减小key之间的碰撞,而加快查询的效率。 
因此,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码以下(HashMap的构造方法中):
 

Java代码  收藏代码

  1. // Find a power of 2 >= initialCapacity  

  2.         int capacity = 1;  

  3.         while (capacity < initialCapacity)   

  4.             capacity <<= 1;  

三、hashmap的resize 
       当hashmap中的元素愈来愈多的时候,碰撞的概率也就愈来愈高(由于数组的长度是固定的),因此为了提升查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操做也会出如今ArrayList中,因此这是一个通用的操做,不少人对它的性能表示过怀疑,不过想一想咱们的“均摊”原理,就释然了,而在hashmap数组扩容以后,最消耗性能的点就出现了:原数组中的数据必须从新计算其在新数组中的位置,并放进去,这就是resize。 

         那么hashmap何时进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认状况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,而后从新计算每一个元素在数组中的位置,而这是一个很是消耗性能的操做,因此若是咱们已经预知hashmap中元素的个数,那么预设元素的个数可以有效的提升hashmap的性能。好比说,咱们有1000个元素new HashMap(1000), 可是理论上来说new HashMap(1024)更合适,不过上面annegu已经说过,即便是1000,hashmap也自动会将其设置为1024。 可是new HashMap(1024)还不是更合适的,由于0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 咱们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。 
四、key的hashcode与equals方法改写 
在第一部分hashmap的数据结构中,annegu就写了get方法的过程:首先计算key的hashcode,找到数组中对应位置的某一元素,而后经过key的equals方法在对应位置的链表中找到须要的元素。因此,hashcode与equals方法对于找到对应元素是两个关键方法。 
Hashmap的key能够是任何类型的对象,例如User这种对象,为了保证两个具备相同属性的user的hashcode相同,咱们就须要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就能够找到在hashmap数组中的位置了。若是这个位置上有多个元素,还须要用key的equals方法在对应位置的链表中找到须要的元素,因此只改写了hashcode方法是不够的,equals方法也是须要改写滴~固然啦,按正常思惟逻辑,equals方法通常都会根据实际的业务内容来定义,例如根据user对象的id来判断两个user是否相等。 
在改写equals方法的时候,须要知足如下三点: 
(1) 自反性:就是说a.equals(a)必须为true。 
(2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。 
(3) 传递性:就是说a.equals(b)=true,而且b.equals(c)=true的话,a.equals(c)也必须为true。 
经过改写key对象的equals和hashcode方法,咱们能够将任意的业务对象做为map的key。
 


HashTable和HashMap区别

第一,继承不一样。

public class Hashtable extends Dictionary implements Map
public class HashMap  extends AbstractMap implements Map

第二

Hashtable 中的方法是同步的,而HashMap中的方法在缺省状况下是非同步的。在多线程并发的环境下,能够直接使用Hashtable,可是要使用HashMap的话就要使用Collections.synchronizedMap()。实现Map同步操做。

第三

Hashtable中,key和value都不容许出现null值。

在HashMap中,null能够做为键,这样的键只有一个;能够有一个或多个键所对应的值为null。当get()方法返回null值时,便可以表示 HashMap中没有该键,也能够表示该键所对应的值为null。所以,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

第四,两个遍历方式的内部实现上不一样。

Hashtable、HashMap都使用了 Iterator。而因为历史缘由,Hashtable还使用了Enumeration的方式 。

第五

哈希值的使用不一样,HashTable直接使用对象的hashCode。而HashMap从新计算hash值。

第六

Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增长的方式是 old*2+1。HashMap中hash数组的默认大小是16,并且必定是2的指数。 

 LinkedHashMap

LinkedHashMap是HashMap的一个子类,它保留插入的顺序,若是须要输出的顺序和输入时的相同,那么就选用LinkedHashMap。默认指定排序模式为插入顺序。若是你想构造一个LinkedHashMap,并打算按从近期访问最少到近期访问最多的顺序(即访问顺序)来保存元素,那么请使用下面的构造方法构造LinkedHashMap:

  1. public LinkedHashMap(int initialCapacity,  

  2.          float loadFactor,  

  3.                      boolean accessOrder) {  

  4.     super(initialCapacity, loadFactor);  

  5.     this.accessOrder = accessOrder;  

  6. }  

该哈希映射的迭代顺序就是最后访问其条目的顺序,这种映射很适合构建LRU缓存。

Map<String, String> map = new LinkedHashMap<String, String>(160.75f, true);

LRU简单实现

import java.util.*; 
//扩展一下LinkedHashMap这个类,让他实现LRU算法 
class LRULinkedHashMap<K,V>  extends LinkedHashMap<K,V>{ 
     //定义缓存的容量 
     private int capacity; 
     private static final long serialVersionUID = 1L; 
     //带参数的构造器    
     LRULinkedHashMap( int capacity){ 
         //调用LinkedHashMap的构造器,传入如下参数 
         super ( 16 , 0 .75f, true ); 
         //传入指定的缓存最大容量 
         this .capacity=capacity; 
    
     //实现LRU的关键方法,若是map里面的元素个数大于了缓存最大容量,则删除链表的顶端元素 
     @Override 
     public boolean removeEldestEntry(Map.Entry<K, V> eldest){  
         System.out.println(eldest.getKey() +  "=" + eldest.getValue());   
         return size()>capacity; 
     }   
//测试类 
class Test{ 
public static void main(String[] args)  throws Exception{ 
   
     //指定缓存最大容量为4 
     Map<Integer,Integer> map= new LRULinkedHashMap<>( 4 ); 
     map.put( 9 , 3 ); 
     map.put( 7 , 4 ); 
     map.put( 5 , 9 ); 
     map.put( 3 , 4 ); 
     map.put( 6 , 6 ); 
     //总共put了5个元素,超过了指定的缓存最大容量 
     //遍历结果 
         for (Iterator<Map.Entry<Integer,Integer>> it=map.entrySet().iterator();it.hasNext();){ 
             System.out.println(it.next().getKey()); 
        
    

}

输出结果以下

9=3
9=3
9=3
9=3
9=3
7
5
3
6

removeEldestEntry方法中始终访问的是最后一次被访问的元素。当size大于设定的阈值的时候,最前端的元素被删除。

相关文章
相关标签/搜索