zoukankan      html  css  js  c++  java
  • 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析

      前面了解了jdk容器中的两种List,回忆一下怎么从list中取值(也就是做查询),是通过index索引位置对不对,由于存入list的元素时安装插入顺序存储的,所以index索引也就是插入的次序。
      Map呢是这样一种容器,它可以存储两个元素键和值,根据键这个关键字可以明确且唯一的查出一个值,这个过程很像查字典,考虑一下使用什么样的数据结构才能实现这种效果呢?
     
    1.自己实现一个Map
     
         先来看一下jdk中map的定义:     
     1 public interface Map<K,V> {
     2     int size();
     3     boolean isEmpty();
     4     boolean containsKey(Object key);
     5     boolean containsValue(Object value);
     6     V get(Object key);
     7     V put(K key, V value);
     8     V remove(Object key);
     9     void putAll(Map<? extends K, ? extends V> m);
    10     void clear();
    11     Set<K> keySet();
    12     Collection<V> values();
    13     Set<Map.Entry<K, V>> entrySet();
    14     interface Entry<K,V> {
    15        V getValue();
    16        V setValue(V value);
    17         boolean equals(Object o);
    18         int hashCode();
    19     }
    20     boolean equals(Object o);
    21     int hashCode();
    22 }
      可以看到Map并没有实现Collection接口,也没有实现List接口,因为它可以保存两个属性key-value,和List容器一样还是包含增删改查等基本操作,同时可以看到Map中还定义了一个用来表示键值K-V的接口Entry。
         
      在了解了map的概念和定义后,首先我们自己先来简单写一个Map的实现,看看会遇到什么样的问题。
     1 public class MyMap {
     2 
     3         private Entry[] data = new Entry[100];
     4         private int size;
     5 
     6         public Object put(Object key, Object value) {
     7                // 检查key是否存在,存在则覆盖
     8                for (int i = 0; i < size; i++) {
     9                       if (key.equals(data [i].key)) {
    10                            Object oldValue = data[i].value ;
    11                             data[i].value = value;
    12                             return oldValue;
    13                      }
    14               }
    15               
    16               Entry e = new Entry(key, value);
    17                data[size ] = e;
    18                size++;
    19               
    20                return null;
    21        }
    22 
    23         public Object get(Object key) {
    24                for (int i = 0; i < size; i++) {
    25                       if (key.equals(data [i].key)) {
    26                             return data [i].value;
    27                      }
    28               }
    29 
    30                return null;
    31        }
    32        
    33         public int size() {
    34                return size ;
    35        }
    36        
    37         private class Entry {
    38               Object key;
    39               Object value;
    40 
    41                public Entry(Object key, Object value) {
    42                       this.key = key;
    43                       this.value = value;
    44               }
    45 
    46        }
    47 }
      
      上面我们简单实现了一下map的put、get、size等方法,从代码可以看到底层是使用数组来存储数据的。
      测试一下上面的方法:
     1 public class Test {
     2 
     3         public static void main(String[] args) {
     4               MyMap map = new MyMap();
     5               map.put( "tstd", "angelababy" );
     6               map.put( "张三" , "李四");
     7               map.put( "tstd", "高圆圆" );
     8               
     9               System. out.println(map.size());
    10               System. out.println(map.get("tstd" ));
    11               System. out.println(map.get("张三" ));
    12        }
    13 }

      看下结果:

    2
    高圆圆
    李四
      结果好像是没有问题的对不对。但是这么简单嘛?我们来看一下上面的代码存在一些什么样的问题。
      观察代码可以看到,get方法中,通过key获取value的方式是通过遍历数组实现,这样显然是非常低效的,同样在put方法中由于要检查key是否已经存在也是通过遍历数组实现,想一下有没有更好的办法呢?能不能像数组那样直接通过下标就可以取得对应的元素呢?
      接下来,我们看下HashMap是怎么样实现的。
     
    2.HashMap的定义
      
      在看HashMap定义前,我们首先需要了解hash是什么意思,hash通常被翻译成“散列”,简单解析下(不对的话还请指出^_^),hash就是通过散列算法,将一个任意长度关键字转换为一个固定长度的散列值,但是有一点要指出的是,不同的关键字可能会散列出相同的散列值。什么意思呢?也就是关键字和散列值不是一一对应的,散列值会出现冲突。但是为什么会出现这种情况呢,原因是hash是一种压缩映射,举个例子就是将一个8个字节(二进制64位)的long值转换为一个4个字节(二进制32位)的int值,也就是说需要砍掉4个字节(32位),坑位有限,人太多,所以只能两个人一个坑喽。
         ok、了解了hash的概念和特点后,来看下HashMap的定义:
    1 public class HashMap<K,V>
    2     extends AbstractMap<K,V>
    3     implements Map<K,V>, Cloneable, Serializable
      可以看出HashMap集成了AbstractMap抽象类,实现了Map,Cloneable,Serializable接口,AbstractMap抽象类继承了Map提供了一些基本的实现。
     
    3.底层存储
     1 // 默认初始容量为16,必须为2的n次幂
     2     static final int DEFAULT_INITIAL_CAPACITY = 16;
     3 
     4     // 最大容量为2的30次方
     5     static final int MAXIMUM_CAPACITY = 1 << 30;
     6 
     7     // 默认加载因子为0.75f
     8     static final float DEFAULT_LOAD_FACTOR = 0.75f;
     9 
    10     // Entry数组,长度必须为2的n次幂
    11     transient Entry[] table;
    12 
    13     // 已存储元素的数量
    14     transient int size ;
    15 
    16     // 下次扩容的临界值,size>=threshold就会扩容,threshold等于capacity*load factor
    17     int threshold;
    18 
    19     // 加载因子
    20     final float loadFactor ;

      可以看出HashMap底层是用Entry数组存储数据,同时定义了初始容量,最大容量,加载因子等参数,至于为什么容量必须是2的幂,加载因子又是什么,下面再说,先来看一下Entry的定义。

     1 static class Entry<K,V> implements Map.Entry<K,V> {
     2         final K key ; 
     3         V value;
     4         Entry<K,V> next; // 指向下一个节点
     5         final int hash;
     6 
     7         Entry( int h, K k, V v, Entry<K,V> n) {
     8             value = v;
     9             next = n;
    10             key = k;
    11             hash = h;
    12         }
    13 
    14         public final K getKey() {
    15             return key ;
    16         }
    17 
    18         public final V getValue() {
    19             return value ;
    20         }
    21 
    22         public final V setValue(V newValue) {
    23            V oldValue = value;
    24             value = newValue;
    25             return oldValue;
    26         }
    27 
    28         public final boolean equals(Object o) {
    29             if (!(o instanceof Map.Entry))
    30                 return false;
    31             Map.Entry e = (Map.Entry)o;
    32             Object k1 = getKey();
    33             Object k2 = e.getKey();
    34             if (k1 == k2 || (k1 != null && k1.equals(k2))) {
    35                 Object v1 = getValue();
    36                 Object v2 = e.getValue();
    37                 if (v1 == v2 || (v1 != null && v1.equals(v2)))
    38                     return true;
    39             }
    40             return false;
    41         }
    42 
    43         public final int hashCode() {
    44             return (key ==null   ? 0 : key.hashCode()) ^
    45                    ( value==null ? 0 : value.hashCode());
    46         }
    47 
    48         public final String toString() {
    49             return getKey() + "=" + getValue();
    50         }
    51 
    52         // 当向HashMap中添加元素的时候调用这个方法,这里没有实现是供子类回调用
    53         void recordAccess(HashMap<K,V> m) {
    54         }
    55 
    56         // 当从HashMap中删除元素的时候调动这个方法 ,这里没有实现是供子类回调用
    57         void recordRemoval(HashMap<K,V> m) {
    58         }
    59 }

      Entry是HashMap的内部类,它继承了Map中的Entry接口,它定义了键(key),值(value),和下一个节点的引用(next),以及hash值。很明确的可以看出Entry是什么结构,它是单线链表的一个节点。也就是说HashMap的底层结构是一个数组,而数组的元素是一个单向链表。

      为什么会有这样的设计?我们上面自己实现的map存在一个问题就是查询时需要遍历所有的key,为了解决这个问题HashMap采用hash算法将key散列为一个int值,这个int值对应到数组的下标,再做查询操作的时候,拿到key的散列值,根据数组下标就能直接找到存储在数组的元素。但是由于hash可能会出现相同的散列值,为了解决冲突,HashMap采用将相同的散列值存储到一个链表中,也就是说在一个链表中的元素他们的散列值绝对是相同的。找到数组下标取出链表,再遍历链表是不是比遍历整个数组效率好的多呢?
      我们来看一下HashMap的具体实现。
     
    4.构造方法
     1 /**
     2      * 构造一个指定初始容量和加载因子的HashMap
     3      */
     4     public HashMap( int initialCapacity, float loadFactor) {
     5         // 初始容量和加载因子合法校验
     6         if (initialCapacity < 0)
     7             throw new IllegalArgumentException( "Illegal initial capacity: " +
     8                                                initialCapacity);
     9         if (initialCapacity > MAXIMUM_CAPACITY)
    10             initialCapacity = MAXIMUM_CAPACITY;
    11         if (loadFactor <= 0 || Float.isNaN(loadFactor))
    12             throw new IllegalArgumentException( "Illegal load factor: " +
    13                                                loadFactor);
    14 
    15         // Find a power of 2 >= initialCapacity
    16         // 确保容量为2的n次幂,是capacity为大于initialCapacity的最小的2的n次幂
    17         int capacity = 1;
    18         while (capacity < initialCapacity)
    19             capacity <<= 1;
    20 
    21         // 赋值加载因子
    22         this.loadFactor = loadFactor;
    23         // 赋值扩容临界值
    24         threshold = (int)(capacity * loadFactor);
    25         // 初始化hash表
    26         table = new Entry[capacity];
    27         init();
    28     }
    29 
    30     /**
    31      * 构造一个指定初始容量的HashMap
    32      */
    33     public HashMap( int initialCapacity) {
    34         this(initialCapacity, DEFAULT_LOAD_FACTOR);
    35     }
    36 
    37     /**
    38      * 构造一个使用默认初始容量(16)和默认加载因子(0.75)的HashMap
    39      */
    40     public HashMap() {
    41         this.loadFactor = DEFAULT_LOAD_FACTOR;
    42         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    43         table = new Entry[DEFAULT_INITIAL_CAPACITY];
    44         init();
    45     }
    46 
    47     /**
    48      * 构造一个指定map的HashMap,所创建HashMap使用默认加载因子(0.75)和足以容纳指定map的初始容量。
    49      */
    50     public HashMap(Map<? extends K, ? extends V> m) {
    51         // 确保最小初始容量为16,并保证可以容纳指定map
    52         this(Math.max(( int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
    53                       DEFAULT_INITIAL_CAPACITY ), DEFAULT_LOAD_FACTOR);
    54         putAllForCreate(m);
    55 }

      

      最后一个构造方法引入一下三个方法进行map元素添加,具体内容不多看了,逻辑和put一样但是少了数组扩容逻辑,直接跳过去看增加方法。

     1 private void putAllForCreate(Map<? extends K, ? extends V> m) {
     2       for(Iterator<?extendsMap.Entry<?extendsK, ?extendsV>> i = m.entrySet().iterator(); i.hasNext(); ) {
     3             Map.Entry<? extends K, ? extends V> e = i.next();
     4             putForCreate(e.getKey(), e.getValue());
     5         }
     6     }
     7 
     8     /**
     9      * This method is used instead of put by constructors and
    10      * pseudoconstructors (clone, readObject).  It does not resize the table,
    11      * check for comodification, etc.  It calls createEntry rather than
    12      * addEntry.
    13      */
    14     private void putForCreate(K key, V value) {
    15         int hash = (key == null) ? 0 : hash(key.hashCode());
    16         int i = indexFor(hash, table.length );
    17 
    18         for (Entry<K,V> e = table [i]; e != null; e = e. next) {
    19             Object k;
    20             if (e.hash == hash &&
    21                 ((k = e. key) == key || (key != null && key.equals(k)))) {
    22                 e. value = value;
    23                 return;
    24             }
    25         }
    26 
    27         createEntry(hash, key, value, i);
    28     }
    29    
    30    void createEntry(int hash, K key, V value, int bucketIndex) {
    31        Entry<K,V> e = table[bucketIndex];
    32         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    33         size++;
    34 }
      看完构造方法有一个疑问一直存在,代码一直确认初始容量和数组长度必须为2的n次幂,而加载因子是为了计算扩容临界值,那么到底HashMap是怎么进行扩容的呢?
     
    5.增加
     1 public V put(K key, V value) {
     2         // 如果key为null,调用putForNullKey方法进行存储
     3         if (key == null)
     4             return putForNullKey(value);
     5         // 使用key的hashCode计算key对应的hash值
     6         int hash = hash(key.hashCode());
     7         // 通过key的hash值查找在数组中的index位置
     8         int i = indexFor(hash, table.length );
     9         // 取出数组index位置的链表,遍历链表找查看是有已经存在相同的key
    10         for (Entry<K,V> e = table [i]; e != null; e = e. next) {
    11             Object k;
    12             // 通过对比hash值、key判断是否已经存在相同的key
    13             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    14                 // 如果存在,取出当前key对应的value,供返回
    15                 V oldValue = e. value;
    16                 // 用新value替换之旧的value
    17                 e. value = value;
    18                 e.recordAccess( this);
    19                 // 返回旧value,退出方法
    20                 return oldValue;
    21             }
    22         }
    23 
    24         // 如果不存在相同的key
    25         // 修改版本+1
    26         modCount++;
    27         // 在数组i位置处添加一个新的链表节点
    28         addEntry(hash, key, value, i);
    29         // 没有相同key的情况,返回null
    30         return null;
    31     }
    32 
    33     private V putForNullKey(V value) {
    34         // 取出数组第1个位置(下标等于0)的节点,如果存在则覆盖不存在则新增,和上面的put一样不多讲,
    35         for (Entry<K,V> e = table [0]; e != null; e = e. next) {
    36             if (e.key == null) {
    37                 V oldValue = e. value;
    38                 e. value = value;
    39                 e.recordAccess( this);
    40                 return oldValue;
    41             }
    42         }
    43         modCount++;
    44         // 如果key等于null,则hash值等于0
    45         addEntry(0, null, value, 0);
    46         return null;
    47 }

      增加和我们上面分析的一样,通过将key做hash取得一个散列值,将散列值对应到数组下标,然后将k-v组成链表节点存进数组中。

      上面有三个方法需要重点关注,计算hash值的hash方法,计算数组索引位置的indexFor方法,添加新链表节点的addEntry方法,下面我们逐一的看一下。

     1 /**
     2      * Applies a supplemental hash function to a given hashCode, which
     3      * defends against poor quality hash functions.  This is critical
     4      * because HashMap uses power -of- two length hash tables, that
     5      * otherwise encounter collisions for hashCodes that do not differ
     6      * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     7      */
     8     static int hash(int h) {
     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     }
    15 
    16     /**
    17      * Returns index for hash code h.
    18      */
    19     static int indexFor(int h, int length) {
    20         return h & (length-1);
    21 }
      
      
         上面这两个方法好难懂啊,又是位移又是异或又是与操作,如果让我们自己来写会怎么写呢,hash方法中直接使用hashCode就好了,indexFor直接取模(h % length)就好了,这两种有什么区别吗,哪个更好呢?来简单分析下(分析的不好请拍砖)。
         首先要明白&操作:把两个操作数分别转换为二进制,如果两个操作数的位都是1则为1,否则为0,举个例子:两个数8和9的二进制分别为1000和1001,1000 & 1001 = 1000。
         先看下indexFor方法中的h & (length-1) ,这是什么鬼东西。。。
         不懂原理只能反着推了。。。我们先来看下一个神奇的推论。。。
     
         2^n转换为二进制是什么样子呢:
         2^1 = 10
         2^2 = 100
         2^3 = 1000
         2^n = 1(n个0)
     
         再来看下2^n-1的二进制是什么样子的:
         2^1 - 1 = 01
         2^2 - 1 = 011
         2^3 - 1 = 0111
         2^n - 1 = 0(n个1)
     
      我们发现一个吃惊的结果,就是当length=2的n次幂的时候,h & (length-1)的结果,就是0~(length-1)之间的数,而这个结果和h % length是一样的,但当length!=2^n的时候,这个就特点不成立了。解释下就是:2^n - 1转换成二进制就是0+n个1,比如16的二进制10000,15的二进制01111,按照&操作,都是1则为1,否则为0,所以在低位运算的时候(小于等于2^n - 1),值总是与hash相同,而进行高位运算时(大于2^n - 1),其值等于其低位值。
         只要知道这个结果,就ok,如果还想更加深入,有个大神写了一篇文章可以参考下http://yananay.iteye.com/blog/910460
         但是为什么不直接取模呢,当然是因为&操作要比除法操作效率高了。
     
         知道了 h & (length-1)的结果等同于h % length后,再来看看上面的hash()方法是怎么回事呢?如果hashCode的低位相同(尤其是等于length位数的部分),那么经过散列后冲突的概率比较大,于是jdk给hash的各位加入了一些随机性。
     
         上面那两个还没懂的话,只要明白含义,然后忘掉他来看增加节点的方法。
     
     1     /**
     2      * 增加一个k-v,hash组成的节点在数组内,同时可能会进行数组扩容。
     3      */
     4     void addEntry( int hash, K key, V value, int bucketIndex) {
     5         // 下面两行行代码的逻辑是,创建一个新节点放到单向链表的头部,旧节点向后移
     6         // 取出索引bucketIndex位置处的链表节点,如果节点不存在那就是null,也就是说当数组该位置处还不曾存放过节点的时候,这个地方就是null,
     7        Entry<K,V> e = table[bucketIndex];
     8        // 创建一个节点,并放置在数组的bucketIndex索引位置处,并让新的节点的next指向原来的节点
     9         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    10        // 如果当前HashMap中的元素已经到达了临界值,则将容量扩大2倍,并将size计数+1
    11         if (size ++ >= threshold)
    12             resize(2 * table.length );
    13 }
      这里面有一个需要注意的地方,将新节点指向原来的节点,这里虽然是next,但是却是往回指向的,而不是像上面图中画的由数组第1个节点往后指向,就是说第1个节点指向null,第2个节点指向第1个,第3个节点指向第2个。也就是新节点一直插入在最前端,新节点始终是单向列表的头节点。
        
      再看下扩容的方法:
     1     /**
     2      * Rehashes the contents of this map into a new array with a
     3      * larger capacity.  This method is called automatically when the
     4      * number of keys in this map reaches its threshold.
     5      *
     6      * If current capacity is MAXIMUM_CAPACITY, this method does not
     7      * resize the map, but sets threshold to Integer.MAX_VALUE.
     8      * This has the effect of preventing future calls.
     9      *
    10      * @param newCapacity the new capacity, MUST be a power of two;
    11      *        must be greater than current capacity unless current
    12      *        capacity is MAXIMUM_CAPACITY (in which case value
    13      *        is irrelevant).
    14      */
    15     void resize( int newCapacity) {
    16         // 当前数组
    17         Entry[] oldTable = table;
    18         // 当前数组容量
    19         int oldCapacity = oldTable.length ;
    20         // 如果当前数组已经是默认最大容量MAXIMUM_CAPACITY ,则将临界值改为Integer.MAX_VALUE 返回
    21         if (oldCapacity == MAXIMUM_CAPACITY) {
    22             threshold = Integer.MAX_VALUE;
    23             return;
    24         }
    25 
    26         // 使用新的容量创建一个新的链表数组
    27         Entry[] newTable = new Entry[newCapacity];
    28         // 将当前数组中的元素都移动到新数组中
    29         transfer(newTable);
    30         // 将当前数组指向新创建的数组
    31         table = newTable;
    32         // 重新计算临界值
    33         threshold = (int)(newCapacity * loadFactor);
    34     }
    35 
    36     /**
    37      * Transfers all entries from current table to newTable.
    38      */
    39     void transfer(Entry[] newTable) {
    40         // 当前数组
    41         Entry[] src = table;
    42         // 新数组长度
    43         int newCapacity = newTable.length ;
    44         // 遍历当前数组的元素,重新计算每个元素所在数组位置
    45         for (int j = 0; j < src. length; j++) {
    46             // 取出数组中的链表第一个节点
    47             Entry<K,V> e = src[j];
    48             if (e != null) {
    49                 // 将旧链表位置置空
    50                 src[j] = null;
    51                 // 循环链表,挨个将每个节点插入到新的数组位置中
    52                 do {
    53                     // 取出链表中的当前节点的下一个节点
    54                     Entry<K,V> next = e. next;
    55                     // 重新计算该链表在数组中的索引位置
    56                     int i = indexFor(e. hash, newCapacity);
    57                     // 将下一个节点指向newTable[i]
    58                     e. next = newTable[i];
    59                     // 将当前节点放置在newTable[i]位置
    60                     newTable[i] = e;
    61                     // 下一次循环
    62                     e = next;
    63                 } while (e != null);
    64             }
    65         }
    66 }
      
      
       transfer方法中,由于数组的容量已经变大,也就导致hash算法indexFor已经发生变化,原先在一个链表中的元素,在新的hash下可能会产生不同的散列值,so所有元素都要重新计算后安顿一番。注意在do while循环的过程中,每次循环都是将下个节点指向newTable[i] ,是因为如果有相同的散列值i,上个节点已经放置在newTable[i]位置,这里还是下一个节点的next指向上一个节点(不知道这里是否能理解,画个图理解下吧)。
     
      Map中的元素越多,hash冲突的几率也就越大,数组长度是固定的,所以导致链表越来越长,那么查询的效率当然也就越低下了。还记不记得同时数组容器的ArrayList怎么做的,扩容!而HashMap的扩容resize,需要将所有的元素重新计算后,一个个重新排列到新的数组中去,这是非常低效的,和ArrayList一样,在可以预知容量大小的情况下,提前预设容量会减少HashMap的扩容,提高性能。
      再来看看加载因子的作用,如果加载因子越大,数组填充的越满,这样可以有效的利用空间,但是有一个弊端就是可能会导致冲突的加大,链表过长,反过来却又会造成内存空间的浪费。所以只能需要在空间和时间中找一个平衡点,那就是设置有效的加载因子。我们知道,很多时候为了提高查询效率的做法都是牺牲空间换取时间,到底该怎么取舍,那就要具体分析了。
     
    6.删除
     1 /**
     2      * 根据key删除元素
     3      */
     4     public V remove(Object key) {
     5         Entry<K,V> e = removeEntryForKey(key);
     6         return (e == null ? null : e. value);
     7     }
     8 
     9     /**
    10      * 根据key删除链表节点
    11      */
    12     final Entry<K,V> removeEntryForKey(Object key) {
    13         // 计算key的hash值
    14         int hash = (key == null) ? 0 : hash(key.hashCode());
    15         // 根据hash值计算key在数组的索引位置
    16         int i = indexFor(hash, table.length );
    17         // 找到该索引出的第一个节点
    18         Entry<K,V> prev = table[i];
    19         Entry<K,V> e = prev;
    20 
    21         // 遍历链表(从链表第一个节点开始next),找出相同的key,
    22         while (e != null) {
    23             Entry<K,V> next = e. next;
    24             Object k;
    25             // 如果hash值和key都相等,则认为相等
    26             if (e.hash == hash &&
    27                 ((k = e. key) == key || (key != null && key.equals(k)))) {
    28                 // 修改版本+1
    29                 modCount++;
    30                 // 计数器减1
    31                 size--;
    32                 // 如果第一个就是要删除的节点(第一个节点没有上一个节点,所以要分开判断)
    33                 if (prev == e)
    34                     // 则将下一个节点放到table[i]位置(要删除的节点被覆盖)
    35                     table[i] = next;
    36                 else
    37                  // 否则将上一个节点的next指向当要删除节点下一个(要删除节点被忽略,没有指向了)
    38                     prev. next = next;
    39                 e.recordRemoval( this);
    40                 // 返回删除的节点内容
    41                 return e;
    42             }
    43             // 保存当前节点为下次循环的上一个节点
    44             prev = e;
    45             // 下次循环
    46             e = next;
    47         }
    48 
    49         return e;
    50 }
     
    7.修改
         想一下Map中为什么没有修改方法,1,2,3想好了,对于Map,put相同的key,value会被覆盖掉,这是不是就相当于修改呀。
     
    8.查找
     1 public V get(Object key) {
     2         // 如果key等于null,则调通getForNullKey方法
     3         if (key == null)
     4             return getForNullKey();
     5         // 计算key对应的hash值
     6         int hash = hash(key.hashCode());
     7         // 通过hash值找到key对应数组的索引位置,遍历该数组位置的链表
     8         for (Entry<K,V> e = table [indexFor (hash, table .length)];
     9              e != null;
    10              e = e. next) {
    11             Object k;
    12             // 如果hash值和key都相等,则认为相等
    13             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
    14                 // 返回value
    15                 return e.value ;
    16         }
    17         return null;
    18     }
    19 
    20     private V getForNullKey() {
    21         // 遍历数组第一个位置处的链表
    22         for (Entry<K,V> e = table [0]; e != null; e = e. next) {
    23             if (e.key == null)
    24                 return e.value ;
    25         }
    26         return null;
    27 }
      从删除和查找可以看出,在根据key查找元素的时候,还是需要通过遍历,但是由于已经通过hash对key散列,要遍历的只是发生冲突后生成的链表,这样遍历的结果就已经少很多了,比我们自己写的完全遍历效率提升了n被。
     
    9.是否包含
     1 /**
     2      * Returns <tt>true</tt> if this map contains a mapping for the
     3      * specified key.
     4      *
     5      * @param   key   The key whose presence in this map is to be tested
     6      * @return <tt> true</tt> if this map contains a mapping for the specified
     7      * key.
     8      */
     9     public boolean containsKey(Object key) {
    10         return getEntry(key) != null;
    11     }
    12  
    13     /**
    14      * Returns the entry associated with the specified key in the
    15      * HashMap.  Returns null if the HashMap contains no mapping
    16      * for the key.
    17      */
    18     final Entry<K,V> getEntry(Object key) {
    19         int hash = (key == null) ? 0 : hash(key.hashCode());
    20         for (Entry<K,V> e = table [indexFor (hash, table .length)];
    21              e != null;
    22              e = e. next) {
    23             Object k;
    24             if (e.hash == hash &&
    25                 ((k = e. key) == key || (key != null && key.equals(k))))
    26                 return e;
    27         }
    28         return null;
    29 }

      containsKey的代码逻辑和get的代码逻辑90%是相同的啊,为什么没有封装下呢?

     
     1 /**
     2      * Returns <tt>true</tt> if this map maps one or more keys to the
     3      * specified value.
     4      *
     5      * @param value value whose presence in this map is to be tested
     6      * @return <tt> true</tt> if this map maps one or more keys to the
     7      *         specified value
     8      */
     9     public boolean containsValue(Object value) {
    10         if (value == null)
    11             return containsNullValue();
    12 
    13        Entry[] tab = table;
    14        // 遍历整个table查询是否有相同的value值
    15         for (int i = 0; i < tab. length ; i++)
    16             // 遍历数组的每个链表
    17             for (Entry e = tab[i] ; e != null ; e = e.next)
    18                 if (value.equals(e.value ))
    19                     return true;
    20         return false;
    21     }
    22 
    23     /**
    24      * Special -case code for containsValue with null argument
    25      */
    26     private boolean containsNullValue() {
    27        Entry[] tab = table;
    28         for (int i = 0; i < tab. length ; i++)
    29             for (Entry e = tab[i] ; e != null ; e = e.next)
    30                 if (e.value == null)
    31                     return true;
    32         return false;
    33 }

      可以看到针对指定key的查找,由于HashMap在结构上的优化,查找相对是十分高效的,而对于指定value的查找,要遍历整个hash表,这样是非常低效费时的。。。

    10.容量检查

     1     /**
     2      * Returns the number of key -value mappings in this map.
     3      *
     4      * @return the number of key- value mappings in this map
     5      */
     6     public int size() {
     7         return size ;
     8     }
     9 
    10     /**
    11      * Returns <tt>true</tt> if this map contains no key -value mappings.
    12      *
    13      * @return <tt> true</tt> if this map contains no key -value mappings
    14      */
    15     public boolean isEmpty() {
    16         return size == 0;
    17 }
    11.遍历
     
         关于Map的遍历,需要到后面再分析,因为它牵扯到另外一个容器Set,Set见。
     
     
         至此,HashMap也就分析的差不多了,我们应该已经明白HashMap能过做到快速查询时建立在其底层的存储结构只上的,学习HashMap也就是对前面的两种数据结构的综合运用,另外这里面有关hash的算法,扩容的方案也应该有所掌握,还是那句话学习应该是先观其大略,先从整体上了解他的实现,比如先了解它的存储结构,而不是一头扎进代码中,另外代码读不懂画画图唱唱歌吧,啦啦啦
     
         HashMap 完!
     
     
    参加:
     
     
    参考资料:
     
     
     
  • 相关阅读:
    滚动条滑至底部自动加载内容
    curl请求https请求
    JS根据经纬度获取地址信息
    php结合md5的加密解密算法实例
    php gzcompress() 和gzuncompress()函数实现字符串压缩
    html视频播放器的代码 及其参数详解
    phpcms 整合 discuz!
    phpcms V9 整合 Discuz! X2 教程
    中国各省打架排行榜
    jQuery获取输入框并设置焦点
  • 原文地址:https://www.cnblogs.com/tstd/p/5055286.html
Copyright © 2011-2022 走看看