【Java基础复习】集合框架:HashMap手写源码详解

一、介绍:

  HashMap是java集合框架中常用的数据结构,其本质是一个Entry结构的数组和链表组成,即主体是长度为2的幂的数组,里面的元素为链表结构。接下来,我们来分析他的源码组成。

二、源码分析:

  在阅读源码之前,我们先看看,再集合框架中,HashMap的继承关系。HashMap根据 key 的 hashCode 值l来定位存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度(数组的查找操作是线性的)。HashMap 非线程安全,举例子来说,当在查找hashcode时,若对某一线程返回true,如果此时恰好有一删除操作,会造成死锁。如果需要满足线程安全,可以用 Collections的synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用ConcurrentHashMap 。

   HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,为了方便分析这里依然采用JDK1.6。实现HashMap,首先定义一个Map类的接口,再集合框架中,HashMap引用Map接口。

 1 interface DIYMap<K, V>{
 2     public V put(K k,V v) ;
 3     public V get(K k,V v) ;
 4     
 5     
 6     public interface Entry<K,V>{
 7         Entry<?, ?> next = null;
 8         public K getKey() ;
 9         public V getValue() ;
10     } 
11 }

  接下来正式自定义HashMap实现类:

 1 public class DIYHashMap<K,V> implements DIYMap<K,V> {
 2     public  DIYHashMap(int defalutLength, double defalutAddSizeFactor){
 3         if(defalutLength < 0){
 4             throw new IllegalAccessError("数组长度为负数") ;
 5         }
 6         
 7         if(defalutAddSizeFactor<=0 || Double.isNaN(defalutAddSizeFactor)){
 8             throw new IllegalAccessError("扩容长度不能为非正数字");
 9         }
10         
11         this.defalutAddSizeFactor = defalutAddSizeFactor ;
12         this.defalutLength = tableSizeFor(defalutLength) ;
13         
14     }
15 
16 }

  这里是自定义HashMap的构造函数,在jdk源码中,总共有四个构造函数方便调用。里面的参数代表含义如下:

    //定义初始化默认数组长度 ;
    private int defalutLength = 16 ;

    //定义默认负载因子 ;
    private double defalutAddSizeFactor = 0.75 ;
    
    //使用数组位置的总数
    private int useSize ;
    
    //定义骨架Entry数组 ;
    private Entry<K,V>[] table ;

  tableSize()函数是为了保证数组长度为2的幂(即定义new DIYHashMap(5),得到的数组长度是8),为什么长度一定要是2的次幂?下文会总结一下,主要是为了hash散列的时候保持分布均匀,提高空间的利用效率。

 1     public int tableSizeFor(int n ){
 2         int num = n-1 ;
 3         num |= num>>>1;
 4         num |= num>>>2;
 5         num |= num>>>4;
 6         num |= num>>>8;
 7         num |= num>>>16;
 8         
 9         return (num<0) ? 1:((num>= 1<<30)? 1<<30 : num+1) ;
10     }

  在实现Map接口的同时,要实现内部接口Entry,即定义数组的存储类型。

 1     private class Entry<K,V> implements DIYMap.Entry<K,V>{
 2         Entry<K,V> next = null;
 3         K k;
 4         V v;
 5         public Entry(K k,V v,Entry<K,V> next){
 6             this.k = k;
 7             this.v = v;
 8             this.next = next ;
 9         }
10         @Override
11         public K getKey() {
12             // TODO Auto-generated method stub
13             return k;
14         }
15 
16         @Override
17         public V getValue() {
18             // TODO Auto-generated method stub
19             return v;
20         }
21         
22         
23     }

  以上就是HashMap的初始化,在jdk源码中,依然是按照这样的流程进行初始化构造。在一开始数组是没有分配长度的,在put的时候才会初始化数组长度。

 1     @Override
 2     public V put(K k, V v) {
 3         if(table.length==0 || useSize > defalutLength * defalutAddSizeFactor){
 4             up2Size() ;
 5         }
 6         
 7         int index=getIndex(k,table.length) ;
 8         Entry<K,V> entry =table[index] ;
 9         Entry<K,V> newEntry =new Entry<K, V>(k, v, null) ;        
10         
11         if(entry == null){
12             table[index] = newEntry ;
13             useSize++ ;
14         }else{
15             Entry<K,V> tmp;
16             while((tmp=table[index])!=null){
17                 tmp=tmp.next ;
18                 }
19             tmp.next = newEntry ;
20         }
21         return newEntry.getValue() ;
22 }

  首先,在put一个元素之前,进行检查,看当前已经使用的容量useSize 是否超过了当前的负载因子,或者是不是没有进行数组分配。如果是,会先进行扩容方法,将数组进行初始化,或者进行2倍扩展。up2Size()逻辑下面会解释。接下来会求出需要put的元素被放置的索引getIndex(),这里面就是主要的hash散列算法。如果求出来的索引所在的table[index]==null,就可以将put的(k,v)赋值给它,useSize++;如果不为空,证明发生了hash冲突,需要将新值放在table[index].next位置。

 1     //扩展数组长度 ;
 2     public void up2Size(){
 3         Entry<K,V>[] newTable = new Entry[defalutLength*2] ;
 4         
 5         ArrayList<Entry<K,V>> entryList = new ArrayList<Entry<K,V>>() ;
 6         for(int i=0;i<table.length;i++){
 7             if(table[i] == null){
 8                 continue ;
 9             }
10             //查找是否形成链表
11             findEntryByNext(table[i], entryList) ;
12         }
13         if(entryList.size() >0){
14             useSize =0;
15             defalutLength = defalutLength*2 ;
16             table=newTable ;
17             for(Entry<K,V> entry : entryList){
18                 if(entry.next != null){
19                     entry.next = null ;
20                 }
21                 put(entry.getKey(), entry.getValue()) ;
22             }
23         }else{
24             table = new Entry[defalutLength] ;
25         }
26         
27     }

  在up2Size()函数中,重点在于已经形成链表的数据如何进行重新划分。这里采用ArrayList将旧的所有的元素导出来,重新进行hash计算新的index进行导入到新的数组中。其中重点就是找到已经形成链表的数据。

1     public void findEntryByNext(Entry<K, V> entry, ArrayList<Entry<K, V>> entryList){
2         if(entry!= null && entry.next != null){
3             entryList.add(entry) ;
4             findEntryByNext(entry.next, entryList) ;
5         }else 
6             entryList.add(entry) ;
7     }

  写到这里,一个put函数其实已经差不多了,这里面的重点在于hash算法,如何保证分布的均匀性。

 1     private int getIndex(K k,int length ){
 2         int m=length-1 ;
 3         int hashCode = k.hashCode() ;
 4         hashCode = hashCode^((hashCode>>>7)^(hashCode>>>12)) ;
 5         hashCode = hashCode^(hashCode>>>7)^(hashCode>>>4) ;
 6         
 7         int index = hashCode & m ;
 8         
 9         return index >=0 ?index :-index ;
10     }

(分析待续。。。)

  get方法比put要简单一些,本质上也是计算key的hashcode,并且得到index是否有值。希望下来自己再写一下。

原文地址:https://www.cnblogs.com/panghaohan/p/7681536.html