HashMap源码解析 给jdk写注释系列之jdk1.6容器(4)

Java容器之ConcurrentHashMap源码分析(JDK 1.7与JDK 1.8对比)   在前面两篇博客 Java容器之HashMap源码分析、Java容器之Hashtable源码分析分别对JDK1.8中的HashMap、Hashtable的源码进行一些分析,在本篇博客将对ConcurrentHashMap容器的源码进行一些分析。 申明:在前两篇博客介绍了两个容器的增、删、改、查相关的API,并且给源码加上了中文注释,在本篇博客将只介绍一些关键的API,比如hash值的计算,put... 阅读详情
  前面了解了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 完!
当GO.db安装失败时,生物信息学新手应该知道的5个底层逻辑 本文为生物信息学新手提供了解决GO.db安装失败的5个关键底层逻辑,包括网络连接与镜像源设置、版本兼容性检查、依赖关系管理、SQLite存储机制理解以及调试技巧。特别针对Bioconductor的GO.db包安装问题,详细介绍了如何通过切换国内镜像、管理版本冲突和解决依赖问题来成功安装,帮助用户高效克服常见安装障碍。 阅读详情

相关推荐

AniME框架深度体验:用AI自动生成《修真小说》动画全流程记录

本文详细记录了使用开源多智能体框架AniME,结合其核心动画生成模型AniSora V3,将一段修真小说文本自动转化为风格统一动画视频的全流程实战。文章深入解析了从环境搭建、导演智能体任务分解,到多智能体协作生成分镜、角色、动画及最终合成的每一步操作与挑战,为AI驱动的长视频创作提供了宝贵的实践经验与流程优化思路。

ol789012345的博客 625

给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析

  前面了解了jdk容器中的两种List,回忆一下怎么从list中取值(也就是做查询),是通过index索引位置对不对,由于存入list的元素时安装插入顺序存储的,所以index索引也就是插入的次序。   Map呢是这样一种容器,它可以存储两个元素键和值,根据键这个关键字可以明确且唯一的查出一个值,这个过程很像查字典,考虑一下使用什么样的数据结构才能实现这种效果呢? 1.自己实...

anxiang3546的博客 174

【VTK】VTK中的光标样式

本文分享VTK中的光标设置相关内容技术,希望对各位小伙伴有所帮助!

qq_40041064的博客 1931

jdk源码解析(十)——晚期(运行期)优化

上一节我们讲了早期的优化(编译器),这一节我们看一看晚期的优化(运行期的优化) 1、概述 java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just in Time Compile

敬业小码哥的博客 1198

AIGC算法工程师 面试八股文

1、简述DDPM的算法原理 2、什么是重参数化技巧?Diffusion Models和VAE中的重参数化技巧是如何使用的? VAE中的重参数化技巧 Diffusion Models中的重参数化技巧 3、什么是马尔可夫过程?DDPM中的马尔可夫链是如何定义的? 马尔可夫过程 DDPM马尔可夫链 4、为什么DDPM前向过程中前期加噪少,后期加噪多? 5、VAE和Diffusion Models中的变分推断是什么? 6、Negative Prompt实现的原理是什么?

沉迷单车的追风少年 1万+

【异常】解决 Claude Code 及第三方插件提示 “API Error: 404“ 的配置修复指南

在使用 Claude 相关的 CLI 工具(如)或 IDE 插件进行开发辅助时,配置错误的模型参数是导致连接失败的常见原因。本文将针对这一典型报错进行复盘与解决。

奶爸的编程之路,也就一周冷个三天 7747

给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析

给jdk写注释系列之jdk1.6容器(2):LinkedList源码解析 给jdk写注释系列之jdk1.6容器(3):Iterator设计模式 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析 给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析 给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器工作中经常听到别人讲“

Leeon的博客 1391

RT-DETR算法优化改进:Backbone改进 | HGBlock完美结合PPHGNetV2 GhostConv

GhostConv助力RT-DETR ,HGBlock与PPHGNetV2 GhostConv完美结合

①答疑群聊服务;②YOLO大模型知识问答系统;③计算机视觉论文生成智能体; 2910

java中定义一个栈容器,给jdk写注释系列之jdk1.6容器(10):Stack&Vector源码解析,jdkjdk1.6...

给jdk写注释系列之jdk1.6容器(10):Stack&Vector源码解析,jdkjdk1.6本系列:给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析给jdk写注释系列之jdk1.6容器(2):LinkedList源码解析给jdk写注释系列之jdk1.6容器(3):Iterator设计模式给jdk写注释系列之jdk1.6容器(4):HashMap源码解析给jdk写...

weixin_36473057的博客 202

【Bug已解决】Anthropic tool_result 找不到对应 tool use id 解决方案

方案适用场景推荐指数每个tool_use必须生成对应tool_result最基础的原则,所有Harness都必须遵守⭐⭐⭐⭐⭐裁剪历史时成对处理实现了上下文裁剪/压缩逻辑的Harness⭐⭐⭐⭐⭐并行工具调用结果数量校验支持多工具并行调用的场景⭐⭐⭐⭐使用官方SDK高层封装不需要特别定制消息组装逻辑⭐⭐⭐⭐请求前主动校验中间件追求生产级健壮性,提前拦截问题⭐⭐⭐⭐⭐这类报错,本质上是。

一键难忘的博客 1万+

【JAVA】海康威视门禁机ISAPI接口开发,人脸上传更新接口

本文实现了一个HTTP工具类HTTPClientUtil,提供GET/POST/PUT/DELETE请求方法,支持摘要认证。工具类封装了Apache HttpClient,设置连接超时、最大连接数等参数,通过DeviceInfoDTO传递设备IP、端口和认证信息。文中还展示了两个应用示例:userInfoAdd()用于添加人员信息,FaceDataRecordAdd()用于添加人脸数据,均通过POST请求与设备ISAPI接口交互。该工具类简化了HTTP请求处理,支持设备管理系统的集成开发。

qq_27246521的博客 588

JDK7的HashMap源码和注释

前段时间给jdk14中的HashMap加过注释,最近看到6000多行的ConcurrentHashMap有点畏惧了,再则了解到 jdk8 开始ConcurrentHashMap取消了分段锁机制,然后看到jdk7的ConcurrentHashMap大概有1600行去掉自带的注释就大概1000来行,nice,打算看下jdk7的ConcurrentHashMap然后再过度到jdk8以后的版本。 进行前后对比学习,顺便看下7中的HashMap源码,后期再把7和14的源码加注释 看过源码后自己总结一些知识点: 底层采

诗水人间 887

Spring AI Alibaba Agent开发实战:DeepResearch Agent调度系统

智能Agent调度系统通过专业化分工解决传统AI系统的三大痛点:通用模型专业性不足、处理效率低下和资源浪费。系统采用分层架构设计,核心组件包括智能调度器、Agent选择辅助器和集成工具类,实现问题分类、专业Agent匹配和搜索策略优化的全流程自动化。系统特点包括:1)配置驱动的灵活扩展;2)分层回退的容错机制;3)支持工具调用与传统搜索的智能切换。该架构通过策略模式和依赖注入等技术,在保证稳定性的同时显著提升了研究质量和效率,为企业级多Agent协作提供了完整解决方案。(149字)

wjianwei666的专栏 504

基于JDK 8的HashMap源码解析

看再多的源码解析,自己不亲身去阅读源码,都是徒劳无功。 为了我们能轻松地阅读源码,JDK的开发者们已经为我们准备了许多详尽的注释,阅读这些注释对我们理解代码起着事半功倍的效果,那么我们就先来看看HashMap在JDK8中的注释吧! Hash table based implementation of the Map interface. This implementation provides ...

清风阁 1239

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

基于JDK1.8源码分析HashMap容器

前言 基于JDK1.8源码解析Map集合类下的HashMap,以及HashMap与HashTable的对比。 要点总结: HashMap底层是数组+链表/红黑树(在JDK1.7及之前是数组+链表); HashMap根据哈希算法,计算key的HashCode(具体可参考Object类的HashCode()方法),为了减少冲突概率,在与数组长度做与运算之前,将高16位与低16位做异或运算,高位...

蚩尤 381

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

IDEA编译JDK1.8源码及运行测试--终于可以debug进带注释的JDK源码了

一、JDK源码源码导入IDEA前资源准备 在自己安装好的JDK1.8版本目录下,找到src.zip,这个就是源码(没有JDK1.8的自行去官网下载,我的版本是jdk1.8.0_271)。 2.在自己电脑磁盘新建项目目录JDK1.8.0_271_source,将src解压到项目目录下,然后删掉src.zip。 二、导入IDEA并配置 1.File-Open,找到自己项目导入 最终项目目录效果 2.导进入后,你打开一个java文件会发现这个不可识别。此时需要将src标记为资源..

神韵 4058

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

HashMap源码分析,基于jdk1.8,附源码注释(一)

HashMap源码详解 jdk1.8中HashMap是基于数组+链表+红黑树完成的 。 1.当我们执行 创建 HashMap 实例时 只是初始化的一些参数,并没有进行数组的创建; 初始化的参数: (1) 、当使用HashMap 空构造器时,只是初始化了 “负载系数”,默认值为 0.75f 源码: public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } (2) 、当使用HashMap(int 容量) 一个参数构造器时,初始化了 “负载

weixin_38002107的博客 837

JDK11源码--HashMap源码分析

文章目录概述hashmap数据结构基本参数`MAXIMUM_CAPACITY`为什么设置成1 &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt; 30为什么map的容量要限制为2的整数次方`DEFAULT_LOAD_FACTOR`负载因子为什么是0.75 概述 本文介绍JDK11中HashMap的源码实现。 hashmap数据结构 map中存储的是key,value键

刘本龙的专栏 2112

给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析

  前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表)。接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序。 来具体看下LinkedHashMap的...

anxiang3546的博客 209

给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器

  今天的主角是HashSet,Set是什么东东,当然也是一种java容器了。 现在再看到Hash心底里有没有会心一笑呢,这里不再赘述hash的概念原理等一大堆东西了(不懂得需要先回去看下HashMap了),需要在啰嗦一句的是hash表是基于快速存取的角度设计的,也是一种典型的空间换时间的做法(这个在分析HashMap中都有讲过)。那么今天的HashSet它又是怎么一回事的,...

anxiang3546的博客 238

给jdk写注释系列之jdk1.6容器(11)-Queue之ArrayDeque源码解析

  前面讲了Stack是一种先进后出的数据结构:栈,那么对应的Queue是一种先进先出(First In First Out)的数据结构:队列。 对比一下Stack,Queue是一种先进先出的容器,它有两个口,从一个口放入元素,从另一个口获取元素。如果把栈比作一个木桶,那么队列就是一个管道。   是不是很容易理解,因为队列有两个口,一个负责入队另一个负责出队,所以会...

anxiang3546的博客 240

给jdk写注释系列之jdk1.6容器(8)-TreeSet&NavigableMap&NavigableSet源码解析

  TreeSet是一个有序的Set集合。   既然是有序,那么它是靠什么来维持顺序的呢,回忆一下TreeMap中是怎么比较两个key大小的,是通过一个比较器Comparator对不对,不过遗憾的是,今天仍然不会讲Comparator,但是需要明白的是TreeSet要实现信息也必须依靠于Comparator接口。 关于Set,在前面我们讲过一个HashSet,是不是想起了什...

anxiang3546的博客 205

HashMap源码阅读分析(JDK1.8)

HashMap是JDK提供的经典容器之一,最近刚好时间充裕,于是自己看了一遍hashMap的源码实现,不同版本的JDK,HashMap的实现方式有所不同,本文主要针对JDK1.8的源码进行分析,至于各版本实现方式的不同,本文不做讨论,下面直接开始。 一、hash函数分析 map的put方法,首先调用的就是hash函数,返回key的hash值,其函数方法如下: ...

165

linkedhashmap扩容_给jdk写注释jdk1.6容器LinkedHashMap源码

我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序。来具体看下LinkedHashMap的实现。 1.定义 1 public class LinkedHashMap2 extends HashMap3 ...

weixin_39812065的博客 395

java 容器 源码_java容器源码

给jdk写注释系列之jdk1.6容器(13)-总结篇之Java集合与数据结构给jdk写注释系列之jdk1.6容器(12)-PriorityQueue源码解析给jdk写注释系列之jdk1.6容器(11)-Queue之ArrayDeque源码解析给jdk写注释系列之jdk1.6容器(10)-Stack&Vector源码解析给jdk写注释系列之jdk1.6容器(9)-Strategy设计模式之C...

weixin_35282085的博客 126

给jdk写注释系列之jdk1.6容器(13)-总结篇之Java集合与数据结构

是的,这篇blogs是一个总结篇,最开始的时候我提到过,对于java容器或集合的学习也可以看做是对数据结构的学习与应用。在前面我们分析了很多的java容器,也接触了好多种常用的数据结构,今天我们就来总结下这些内容。 下面我们以数据结构的维度来总结下,在Java集合的实现过程中,底层到底使用了哪些常用的数据结构中,他们分别又有什么特点。 1. ...

anxiang3546的博客 218
上一篇: Iterator的几个方法及意义
下一篇: LinkedHashMap源码解析 给jdk写注释系列之jdk1.6容器(5)
ProfeSir
博客等级 码龄11年 5粉丝 11原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值