java集合系列之一HashMap

1. HashMap概述

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

2. HashMap的数据结构

本文涉及的图中,每个圆角方块代表一个Entry对象,里面包括key、value等元素,图中黄色方块内的值为Entry对象的key。

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap数据结构

HashMap实际上就是用table数组来进行存储数据的。可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表,即单向列表。
下面我们来看下Entry对象的代码:

static class Entry<K,V> implements Map.Entry<K,V> {  
    final K key;  //key值
    V value;  //value值
    Entry<K,V> next;  //指向下一个Entry对象的引用
    final int hash;  //hash值
  
    /** 
     * Creates new entry. 
     */  
    Entry(int h, K k, V v, Entry<K,V> n) {  
        value = v;  
        next = n;  
        key = k;  
        hash = h;  
    }  
  
    ......  
}  

每一个键值对都对应一个 Entry 对象。 Entry 对象中有一个next,指向另一个Entry对象的引用。这样就组成了一个单向列表。

2. HashMap初始化

默认初始化方法
/** 
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity 
 * (16) and the default load factor (0.75). 
 */  
public HashMap() {  
    this.loadFactor = DEFAULT_LOAD_FACTOR;  
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
    table = new Entry[DEFAULT_INITIAL_CAPACITY];  
    init();  
}  

从上面代码中可以看出,内部HashMap的负载因子(load factor )为 0.75 ,初始最大容量(initial capacity,即内部数组的长度)为 16,实际最大容量( threshold )为0.75*16=12

  • 负载因子(load factor ),表述为 实际最大容量 / 初始最大容量 的比值。简单来说,负载因子相当于table数组的填充率,默认为0.75。这个值是对空间和时间效率的一个平衡选择。
  • 初始最大容量(initial capacity,即内部数组的长度),内部数组的长度。
  • ** 实际最大容量( threshold )** ,定义为 负载因子与 初始最大容量 的乘积(求整)。当table数组的已使用数量大于 threshold 时,HashMap就会重新扩张table数组的长度,长度扩展为当前的两倍

我们可以通过代码来看看“实际最大容量( threshold )”是如何扩展的。调用HashMap.put(key,value)时,其中部分代码如下:

/** 
    * Adds a new entry with the specified key, value and hash code to 
    * the specified bucket.  It is the responsibility of this 
    * method to resize the table if appropriate. 
    * 
    * Subclass overrides this to alter the behavior of put method. 
    */  
   void addEntry(int hash, K key, V value, int bucketIndex) {  
       Entry<K,V> e = table[bucketIndex];  
       table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
       if (size++ >= threshold)  
           resize(2 * table.length);  
   }

从上面我们可以看出,初始化HashMap时,我们最好能预估到HashMap的大小,否则在put的过程中, 当table数组的已使用数量大于 threshold 时, HashMap就会自动扩展(我们把这种行为叫做rehash,调用resize(2 * table.length);方法),代码如下:

void resize(int newCapacity) {  
    Entry[] oldTable = table;  
    int oldCapacity = oldTable.length;  
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;  
        return;  
    }  
    //新建一个table数组  
    Entry[] newTable = new Entry[newCapacity];  
    transfer(newTable);//把当前数组元素移动到新数组中  
    table = newTable;  
    threshold = (int)(newCapacity * loadFactor);  
/** 
 * Transfers all entries from current table to newTable. 
 */  
void transfer(Entry[] newTable) {  
    Entry[] src = table;  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) {//循环old数组中所有元素  
        Entry<K,V> e = src[j];  
        if (e != null) {  
            src[j] = null;  
            do {//循环每个元素所属的单向链表  
                Entry<K,V> next = e.next;  
                int i = indexFor(e.hash, newCapacity);//根据Entry的hash重新计算它在新数组中的位置  
                e.next = newTable[i];//新数组之前移动的数组元素作为Entry的下一个Entry:即插入到当前链表的最顶端  
                newTable[i] = e;  
                e = next;  
            } while (e != null);  
        }  
    }  
}  

从上面代码可以看到,rehash导致所有已存入的元素都要根据key的hash与新数组的长度值,重新计算每个Entry在新数组中的位置,并存入新数组。这个动作是相当费时间的,所以我们要尽量避免让HashMap进行自动扩展。

指定参数初始化方法

实际上,我们可以通过 HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap;或者 HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。
一般情况下,我们不需要修改默认的负载因子,所以常用HashMap(int initialCapacity)。其初始化实际上是调用的HashMap(int initialCapacity, float loadFactor)方法,代码如下:

/** 
 * Constructs an empty <tt>HashMap</tt> with the specified initial 
 * capacity and load factor. 
 * 
 * @param  initialCapacity the initial capacity 
 * @param  loadFactor      the load factor 
 * @throws IllegalArgumentException if the initial capacity is negative 
 *         or the load factor is nonpositive 
 */  
public HashMap(int initialCapacity, float loadFactor) {  
    if (initialCapacity < 0)  
        throw new IllegalArgumentException("Illegal initial capacity: " +  
                                           initialCapacity);  
    if (initialCapacity > MAXIMUM_CAPACITY)  
        initialCapacity = MAXIMUM_CAPACITY;  
    if (loadFactor <= 0 || Float.isNaN(loadFactor))  
        throw new IllegalArgumentException("Illegal load factor: " +  
                                           loadFactor);  
  
    // Find a power of 2 >= initialCapacity数组的长度必须是2的n次方,假如初始化initialCapacity=6,实际table数组的长度是大于6、且2的n次方的最小值 ,即 2的3次方=8
    int capacity = 1;  
    while (capacity < initialCapacity)  
        capacity <<= 1;  
  
    this.loadFactor = loadFactor;  
    threshold = (int)(capacity * loadFactor);  
    table = new Entry[capacity];  
    init();  

这里,要求table的实际长度(初始最大容量(initial capacity,即内部数组的长度)),必须为2的n次方。原因是为了更高性能的计算出每个key-value对应的元素位置.

  • 情况1:我们如果需要使用HashMap保存20个key-value对,那我们就要Map map= HashMap(20),而实际上,20并不是2的n次方,所以初始化时,table实际的长度会被初始化2的5次方=32,而此map在不扩展的情况下最多可以容纳320.75=24个key-value对。*
  • 情况2:我们需要保存16个key-value对,如果我们Map map= HashMap(16)这样初始化,因为16正好的2的4次方,所以table数组的长度就会被初始化为16,而此map在不扩展的情况下最多可以容纳160.75=12个key-value对.而我们需要保存16个,这样就会导致map自动扩展table数组,非常影响性能。实际上,为了能够保存16个key-value对,考虑的负载因子0.75,我们应该这样初始化Map map= HashMap(32),(其实17-32之间的任何一个值都可以,在初始化时,最终都会被修正为2的5次方:32)*

参考链接:
http://blog.csdn.net/turkeyzhou/article/details/3197520
http://zhangshixi.iteye.com/blog/672697

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容