Android HashMap

1.HashMap

HashMap是一个散列表,它存储的内容是键值对(key-value)映射。HashMap底层是基于数组+链表组成的,不过在jdk1.7和jdk1.8中具体实现稍有不同。

①HashMap根据键的HashCode值存储数据,具有很快的访问速度,最多只允许一条记录的键为null,允许多条记录的值为null。

②HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用Collection的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

③HashMap是无序的,即不会记录插入的顺序,插入的顺序和迭代输出的数组不一样。

④HashMap如果插入相同的Key,则后面的value将会覆盖前面的value。

⑤HashMap和HashTable的区别:HashMap允许空(null)键或值,而Hashtable不允许空键或值。

⑥HashMap的key与value类型可以相同也可以不同,可以是String类型的key和value,也可以是Integer的key和String类型的value。

HashMap中的元素实际上是对象,一些常见的基本类型可以使用它的包装类。基本类型对应的包装类表如下:

基本类型引用类型
booleanBoolean
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter

 

2.HashMap的用法

重点看HashMap的迭代。可以使用for-each来迭代HashMap中的元素。

①如果只想获取key,可以使用keySet()方法,然后可以通过get(key)获取对应的value;如果只想获取value,可以使用values()方法。

HashMap<Integer, String> sites = new HashMap<Integer, String>();

sites.put(1, "Google");

sites.put(6, "Runoob");

// 输出key和value

for (Integer i : sites.keySet()) {

    System.out.println("key: " + i + " value: " + sites.get(i));

}

// 返回所有value值

for(String value: sites.values()) {

  System.out.print(value + ", ");

}

输出结果如下:

 key: 1 value: Google

 key: 6 value: Runoob

 Google, Runoob, 

HashMap 常用方法列表如下:

方法描述
clear()删除hashMap中的所有键/值对
clone()复制一份hashMap
isEmpty()判断hashMap是否为空
size()计算hashMap中键/值对的数量
put()将键/值对添加到hashMap中
putAll()将所有键/值对添加到hashMap中
putIfAbsent()如果hashMap中不存在指定的键,则将指定的键/值对插入到hashMap中。
remove()删除hashMap中指定键key的映射关系
containsKey()检查hashMap中是否存在指定的key对应的映射关系。
containsValue()检查hashMap中是否存在指定的value对应的映射关系。
replace()替换hashMap中是指定的key对应的value。
replaceAll()将hashMap中的所有映射关系替换成给定的函数所执行的结果。
get()获取指定key对应对value
getOrDefault()获取指定key对应的value,如果找不到key ,则返回设置的默认值
forEach()对hashMap中的每个映射执行指定的操作。
entrySet()返回hashMap中所有映射项的集合集合视图。
keySet()返回hashMap中所有key组成的集合视图。
values()返回hashMap中存在的所有 value 值。
merge()添加键值对到hashMap中
compute()对hashMap中指定key的值进行重新计算
computeIfAbsent()对hashMap中指定key的值进行重新计算,如果不存在这个key,则添加到hasMap中
computeIfPresent()对hashMap中指定key的值进行重新计算,前提是该key存在于hashMap中。

 

3.HashMap原理

HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供Map接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实AbstractMap类已经实现了Map。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

(1)构造函数

HashMap提供了三个构造函数:

①HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空HashMap。

②HashMap(int initialCapacity):构造一个指定初始容量和默认加载因子 (0.75) 的空HashMap。

③HashMap(int initialCapacity, float loadFactor):构造一个指定初始容量和指定加载因子的空HashMap。

在这里提到了两个参数:初始容量、加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量;加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下是无需修改的。

可能有人要问:为什么要加载因子?这里就涉及到HashMap的原理了。HashMap中存储元素的时候,首先得先通过它自己的hash算法找到存储在table数组的索引值。但是这个hash算法并不能保证每一个元素对应table数组中不同的索引值,当放入HashMap的元素过多时就容易出现相同的索引值,在算法里叫冲突,这时元素就会被加到该索引值下的链表中,这样查找的效率就会大大降低,显然违背了HashMap快速查找的初衷。所以HashMap在设计的时候就用了这样一个加载因子,如果存储的元素个数占table长度的比例大于loadFactor加载因子的时候,冲突加剧,这时就得扩容解决问题了。 

所以总结影响HashMap效率的两个因素:①初始容量 ②加载因子。解决的本质就是减少hash冲突。

(2)数据结构

HashMap是一种支持快速存取的数据结构,要了解它的性能必须要了解它的数据结构。

在Java中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap也是如此。实际上HashMap是一个“链表散列”,以jdk1.7为例看一下它数据结构:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_13,color_FFFFFF,t_70,g_se,x_16

HashMap底层实现还是数组,只是数组的每一项都是一条链。参数initialCapacity就代表了该数组的长度。

HashMap构造函数的源码:

public HashMap(int initialCapacity, float loadFactor) {

        if (initialCapacity < 0) //初始容量不能<0

            throw new IllegalArgumentException( "Illegal initial capacity: " + initialCapacity);

        //初始容量不能 > 最大容量值,则设置最大容量值为2^30

        if (initialCapacity > MAXIMUM_CAPACITY)

            initialCapacity = MAXIMUM_CAPACITY;

        //负载因子不能 < 0

       if (loadFactor <= 0 || Float.isNaN(loadFactor))

            throw new IllegalArgumentException( "Illegal load factor: " + loadFactor);

        // 计算出大于initialCapacity的最小的2的n次方值

        int capacity = 1;

        while (capacity < initialCapacity)

            capacity <<= 1;       

        this.loadFactor = loadFactor;

        //设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作

        threshold = (int) (capacity * loadFactor);

        //初始化table数组

        table = new Entry[capacity];

        init();

    }

每次新建一个HashMap时都会初始化一个table数组。table数组的元素为Entry节点。

static class Entry<K,V> implements Map.Entry<K,V> {

    final K key;

    V value;

    Entry<K,V> next;

    final int hash;

    Entry(int h, K k, V v, Entry<K,V> n) {

        value = v;

        next = n;

        key = k;

        hash = h;

    }

    .......

}

Entry为HashMap的内部类,它包含了键key、值value、下一个节点next(用于实现链表结构),以及hash值(当前key的hashcode),这是非常重要的,正是由于Entry才构成了table数组的项为链表。

(3)存储实现:put(key,vlaue)

下面将探讨HashMap是如何实现快速存取的。

public V put(K key, V value) {

    //key为null时,调用putForNullKey方法,保存null于table第一个位置中,这是HashMap允许为null的原因

    if (key == null)

        return putForNullKey(value);

    //计算key的hash值

    int hash = hash(key.hashCode()); ------(1)

    //根据key的hash值计算在table数组中的位置

    int i = indexFor(hash, table.length); ------(2)

    //迭代table[i]位置的链表,找到key保存的位置

   for (Entry<K, V> e = table[i]; e != null; e = e.next){

        Object k;

         //判断该条链上是否有key相同的,若存在相同则直接覆盖value,返回旧value

        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

            V oldValue = e.value; 

            e.value = value;

            e.recordAccess(this);

            return oldValue; //返回旧值

        }

    }

    modCount++; //修改次数增加1

    //table[i]位置的链表不存在相同的key,则直接插入

    addEntry(hash, key, value, i);

    return null;

}

HashMap保存数据的过程为:首先判断key是否为null,若为null则直接调用putForNullKey方法。若不为null则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置。如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。这个过程看似比较简单,其实深有内幕。有如下几点:

①先看迭代处。此处迭代原因是为了防止存在相同的key值,若发现两个hash值(key)相同时,HashMap的处理方式是用新value替换旧value,这里并没有处理key,这就解释了HashMap中没有两个相同的key。

②再看(1)、(2)处。首先是hash方法,该方法为一个纯粹的数学计算,就是计算h的hash值。

static int hash(int h) {

        h ^= (h >>> 20) ^ (h >>> 12);

        return h ^ (h >>> 7) ^ (h >>> 4);

    }

对于HashMap的table而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算hash值后,怎么才能保证table元素分布均与呢?首先会想到取模,但是由于取模的消耗较大,HashMap是这样处理的:调用indexFor方法。

static int indexFor(int h, int length) {

    return h & (length-1);

}

HashMap的底层数组table长度总是2的n次方,在构造函数中存在:capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。

回到indexFor方法,该方法仅有一条语句:h&(length - 1),这句话除了上面的取模运算外,还有一个非常重要的责任:均匀分布table数据和充分利用空间。

假设length为16(2^n)和15,h为5、6、7。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

当n=15时,6和7的结果一样,表示他们在table存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,这样就会导致查询速度降低。诚然这里只分析三个数字不是很多,那再看0-15。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

看到总共发生了8次碰撞,同时发现浪费的空间非常大,有1、3、5、7、9、11、13、15处没有记录,也就是没有存放数据。这是因为他们在与14进行&运算时,得到的结果最后一位永远都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的,空间减少,进一步增加碰撞几率,这样就会导致查询速度慢。而当length = 16时,length – 1 = 15 即1111,那么进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

现在再来看put的流程:向HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素则直接插入,否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。具体的实现过程见addEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {

        //获取bucketIndex处的Entry

        Entry<K, V> e = table[bucketIndex];

        //将新创建的Entry放入bucketIndex索引处,并让新的Entry指向原来的 Entry (也就是将新元素加到该元素对应table[bucketIndex]链表的表头)

        table[bucketIndex] = new Entry<K, V>(hash, key, value, e);

        //若HashMap中元素的个数超过极限了,则容量扩大两倍

        if (size++ >= threshold)

            resize(2 * table.length);

    }

这个方法中有两点需要注意:

①一是链的产生。

这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链;但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

②扩容问题。

随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点是HashMap中元素的数量等于table数组长度*加载因子。

注意:扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

(4)扩容

在元素添加方法addEntry()中,添加完元素后,有下面两行代码: 

if (size++ >= threshold)  

    resize(2 * table.length);  

size表示的是HashMap中有多少个元素,当元素的个数超过临界值时会自动调用扩容方法,可以看出HashMap的扩容是翻番的扩2 * table.length。现在来看看resize扩容方法。

void resize(int newCapacity) {  

    Entry[] oldTable = table;  

    int oldCapacity = oldTable.length;  

    if (oldCapacity == MAXIMUM_CAPACITY) {  

        threshold = Integer.MAX_VALUE;  

        return;  

    }  

    Entry[] newTable = new Entry[newCapacity];  

    transfer(newTable);  

    table = newTable;  

    threshold = (int)(newCapacity * loadFactor);  

}  

前面几行是判断扩容后是否过了最大的int值。后面几行是将原来的table中的元素重新hash放到新的扩容后的table中。重点在transfer(newTable)这个方法。

void transfer(Entry[] newTable) {  

    Entry[] src = table;  

    int newCapacity = newTable.length;  

    for (int j = 0; j < src.length; j++) {  

        Entry<K,V> e = src[j];  

        if (e != null) {  

            src[j] = null;  

            do { 

               //将第一个元素e后的链表截取出来

                Entry<K,V> next = e.next; 

               //找到e对应新table的下标索引

                int i = indexFor(e.hash, newCapacity);  

               //将e插入到新table i下标索引链表的表头 

               e.next = newTable[i];  

             //将新table下标索引重新定位为e,这样就完成了一个元素的重新hash 

                newTable[i] = e; 

               //将截取的剩余的链表继续hash 

                e = next;  

            } while (e != null);  

        }  

    }  

}  

这个方法的主要作用就是,将老的table中的所有不为空的元素,重新hash放到新的table中去。在do之前就是遍历table中不为空的元素。这时候找出来的e = src[j]是一个Entry链表。所以,如果不为空,还要遍历这个链表中的每一个元素,并将这些元素重新hash到新table中。

示意图如下: 

①Entry<K,V> next = e.next; 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_15,color_FFFFFF,t_70,g_se,x_16

 ②e.next = newTable[i]; 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_18,color_FFFFFF,t_70,g_se,x_16

 即这里的e就是Entry[j],也就是 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_18,color_FFFFFF,t_70,g_se,x_16

 ③newTable[i] = e; 

因为newTable[i]本身是一个指向浅蓝色Entry[i]的引用,这个时候再将这个引用指向红色Entry[j],这样就完成了老table中一个元素的重新hash到新table中。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

(5)key为null,存到哪去了 

在put方法里头,第一行就处理了key=null的情况:

if (key == null)  

    return putForNullKey(value);  

那就看看这个putForNullKey是怎么处理的吧。  

private V putForNullKey(V value) {  

    for (Entry<K,V> e = table[0]; e != null; e = e.next){  

        if (e.key == null) {  

            V oldValue = e.value;  

            e.value = value;  

            e.recordAccess(this);  

            return oldValue;  

        }  

    }  

    modCount++;  

    addEntry(0, null, value, 0);  

    return null;  

}  

前面那个for循环是在talbe[0]链表中查找key为null的元素,如果找到就将value重新赋值给这个元素的value并返回原来的value。如果上面for循环没找到,则将这个元素添加到talbe[0]链表的表头。 

(6)读取实现:get(key)
相对于HashMap的存而言,取就比较简单了。通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。

public V get(Object key) {

    // 若为null,调用getForNullKey方法返回相对应的value

    if (key == null)

        return getForNullKey();

   // 根据key的hashCode值计算它的hash码  

    int hash = hash(key.hashCode());

    // 取出table数组中指定索引处的值

    for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {

        Object k;

        //若搜索的key与查找的key相同,则返回相对应的value

        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

            return e.value;

    }

    return null;

}

首先是找key为null的元素,通过putForNullKeykey方法可知,为null的元素是放在table[0]这个链表的。所以要找的话,直接到table[0]中查找就行了。如果没找到的话,则根据key的hash值找到元素所在table中下标索引,根据其再找到元素所在链表,在遍历链表,找到该元素并返回其value,否则返回null。

在这里能够根据key快速的取到value,除了和HashMap的数据结构密不可分外,还和Entry有很大的关系:HashMap在存储过程中并没有将key和value分开来存储,而是当做一个整体,这个整体就是Entry对象。同时value也只相当于key的附属而已。在存储的过程中,系统根据key的hashcode来决定Entry在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象。

(7)删除元素

public V remove(Object key) {  

    Entry<K,V> e = removeEntryForKey(key);  

    return (e == null ? null : e.value);  

}  

调用的还是下面的方法  :

final Entry<K,V> removeEntryForKey(Object key){  

    int hash = (key == null) ? 0 : hash(key.hashCode());  

    int i = indexFor(hash, table.length);  

    Entry<K,V> prev = table[i];  

    Entry<K,V> e = prev;  

    while (e != null) {  

        Entry<K,V> next = e.next;  

        Object k;  

        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {  

            modCount++;  

            size--;  

            if (prev == e)  

                table[i] = next;  

            else  

                prev.next = next;  

            e.recordRemoval(this);  

            return e;  

        }  

        prev = e;  

        e = next;  

    }  

    return e;  

}  

while循环外面的很简单,来看看while循环里的。 

Entry<K,V> next = e.next;把原有的链表截出表头元素,然后判断这个表头元素的key是不是要找的key。如果找出的第一个元素就是的话,直接将这个链表的第一个元素删除就OK。

if (prev == e) 

      table[i] = next; 

如果不是,则遍历这个链表,下图展示了这个过程:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_17,color_FFFFFF,t_70,g_se,x_16

 步骤1、初始情况 

Entry<K,V> prev = table[i]; 

Entry<K,V> e = prev; 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_18,color_FFFFFF,t_70,g_se,x_16

 步骤2、没找到 

Entry<K,V> next = e.next; 

…….. 

prev = e; 

e = next; 

如果e这个元素不是要删除的话,则遍历下一个元素。 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_14,color_FFFFFF,t_70,g_se,x_16

 步骤3、找到 

prev.next = next; 

return e; 

将prev的下一个元素指向e.next。这样就相当于删除了e 

最后的结果如下: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_15,color_FFFFFF,t_70,g_se,x_16

 

4.线程安全性

在多线程使用场景中,应尽量避免使用线程不安全的HashMap,而使用线程安全的ConcurrentHashMap。为什么说HashMap是线程不安全的,下面举例说明在并发的多线程使用场景中使用HashMap可能造成死循环。

代码例子如下(JDK1.7的环境):

public class HashMapInfiniteLoop {  

    private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f);  

    public static void main(String[] args) {  

        map.put(5, "C");  

        new Thread("Thread1") {  

            public void run() {  

                map.put(7, "B");  

                System.out.println(map);  

            };  

        }.start();  

        new Thread("Thread2") {  

            public void run() {  

                map.put(3, "A);  

                System.out.println(map);  

            };  

        }.start();        

    }  

}

map初始化为一个长度为2的数组,负载因子loadFactor为0.75,threshold=2*0.75=1,也就是说当put第二个key的时候,map就需要进行resize。

通过设置断点让线程1和线程2同时debug到transfer方法的首行。注意此时两个线程已经成功添加数据。放开thread1的断点至transfer方法的“Entry next = e.next;” 这一行;然后放开线程2的的断点,让线程2进行resize。结果如下图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

注意,Thread1的 e 指向了key(3),而next指向了key(7),其在线程2rehash后,指向了线程2重组后的链表。

线程一被调度回来执行,先是执行 newTalbe[i] = e, 然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 e.next = newTable[i] 导致 key(3).next 指向了 key(7)。注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

于是,当用线程一调用map.get(11)时,悲剧就出现了——Infinite Loop。

 

5.jdk1.7和1.8的区别

jdk1.7 中hashMap的数据结构图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

jdk 1.7 的实现中有一个很明显需要优化的地方就是:当Hash冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。

因此1.8中重点优化了这个查询效率。

1.8 HashMap 结构图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

先来看看几个核心的成员变量:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final int MAXIMUM_CAPACITY = 1 << 30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

static final int TREEIFY_THRESHOLD = 8;

transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;

transient int size;

和1.7大体上都差不多,有几个重要的区别:

①TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。

②HashEntry修改为Node。

Node的核心组成其实也是和1.7中的HashEntry一样,存放的都是key、value、hashcode、next等数据。

与TreeNode相关的三个参数: TREEIFY_THRESHOLD=8 和 UNTREEIFY_THRESHOLD=6 以及 MIN_TREEIFY_CAPACITY=64

TREEIFY_THRESHOLD=8 指的是链表的长度大于8的时候进行树化。

UNTREEIFY_THRESHOLD=6 指的是当元素被删除,链表的长度小于6 的时候进行退化,由红黑树退化成链表。

MIN_TREEIFY_CAPACITY=64 意思是数组中元素的个数必须大于等于64之后才能进行树化。

再来看看核心方法。

(1)put 方法

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 ①判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。

②根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。

③如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。

④如果当前桶为红黑树,那就要按照红黑树的方式写入数据。

⑤如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。

⑥接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。

⑦如果在遍历过程中找到 key 相同时直接退出遍历。

⑧如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。

⑨最后判断是否需要进行扩容。

(2)get 方法

public V get(Object key) {

    Node<K,V> e;

    return (e = getNode(hash(key), key)) == null ? null : e.value;

}

final Node<K,V> getNode(int hash, Object key) {

    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {

        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))

            return first;

        if ((e = first.next) != null) {

            if (first instanceof TreeNode)

                return ((TreeNode<K,V>) first ).getTreeNode(hash, key);

            do {

                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))

                    return e;

            } while ((e = e.next) != null);

        }

    }

    return null;

}

①首先将 key hash 之后取得所定位的桶。

②如果桶为空则直接返回 null 。否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。

③如果第一个不匹配,则判断它的下一个是红黑树还是链表。红黑树就按照树的查找方式返回值,不然就按照链表的方式遍历匹配返回值。

从get/put两个核心方法可以看出 1.8 中对链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。

但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。

final HashMap<String, String> map = new HashMap<String, String>();

for (int i = 0; i < 1000; i++) {

    new Thread(new Runnable() {

        @Override

        public void run() {

            map.put(UUID.randomUUID().toString(), "");

        }

    }).start();

}

还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种:

Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator();

        while (entryIterator.hasNext()) {

            Map.Entry<String, Integer> next = entryIterator.next();

            System.out.println("key=" + next.getKey() + " value=" + next.getValue());

        }

 

Iterator<String> iterator = map.keySet().iterator();

        while (iterator.hasNext()){

            String key = iterator.next();

            System.out.println("key=" + key + " value=" + map.get(key));

        }

强烈建议使用第一种 EntrySet 进行遍历。

第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。

无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至 1.7 中出现死循环导致系统不可用(1.8 已经修复死循环问题)。

因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。

 

6.各种Map映射

Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 下面针对各个实现类的特点做一些说明:

①HashMap:根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

②Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

③LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

④TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值