java源码分析之集合框架HashMap 10

HashMap

  1. HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
  2. HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
  3. HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
  4. HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行
    rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
  5. 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生
    rehash 操作。

          注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedMap
方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

Map m = Collections.synchronizedMap(new HashMap(...))

hashMap存储结构:

Entry实体:

Entry其实是个单向链表:它是“HashMap链式存储法”对应的链表。它实现了Map.Entry接口,也就是实现了getKey()、getValue()、setValue(V value) 、equals(Object o)和hashCode()这些方法。

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next; //指向下一个节点
    int hash;  

    /**
     * 构造方法,创建一个Entry
     * 参数:哈希值h,键值k,值v和下一个节点n
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }  

    public final K getKey() {
        return key;
    }  

    public final V getValue() {
        return value;
    }  

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }  

    //判断两个Entry是否相等,必须key和value都相等,才返回true
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }  

    public final int hashCode() { //实现hashCode
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }  

    public final String toString() {
        return getKey() + "=" + getValue();
    }  

    /**
     * 当向HashMap中添加元素时,即调用put(k,v)时,
     * 对已经在HashMap中k位置进行v的覆盖时,会调用此方法
     * 这里没做任何处理
     */
    void recordAccess(HashMap<K,V> m) {
    }  

    /**
     * 当从HashMap中删除了一个Entry时,会调用该函数
     * 这里没做任何处理
     */
    void recordRemoval(HashMap<K,V> m) {
    }
}  

HashMap 的API:

HashMap的继承关系:

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

HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口

属性:

	// 默认的初始容量是16,必须是2的幂。
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 存储数据的Entry数组,长度是2的幂。
    // HashMap是采用拉链法实现的,每一个Entry本质上是一个单向链表
    transient Entry[] table;

    // HashMap的大小,它是HashMap保存的键值对的数量
    transient int size;

    // HashMap的阈值,用于判断是否需要调
    //整HashMap的容量(threshold = 容量*加载因子)
    int threshold;

    // 加载因子实际大小
    final float loadFactor;

    // HashMap被改变的次数
    transient volatile int modCount;

    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALU

	//hash种子
	transient int hashSeed = 0;



主要来看看loadFactor属性,loadFactor表示Hash表中元素的填满程度。

若加载因子设置过大,则填满的元素越多,无疑空间利用率变高了,但是冲突的机会增加了,冲突的越多,链表就会变得越长,那么查找效率就会变得更低;

若加载因子设置过小,则填满的元素越少,那么空间利用率变低了,表中数据将变得更加稀疏,但是冲突的机会减小了,这样链表就不会太长,查找效率变得更高。

这看起来有点绕口,我举个简单的例子,如果数组容量为100,加载因子设置为80,即装满了80个才开始扩容,但是在装的过程中,可能有很多key对应相同的hash值,这样就会放到同一个链表中(因为没到80个不能扩容),这样就会导致很多链表都变得很长,也就是说,不同的key对应相同的hash值比数组填满到80个更加容易出现。

但是如果设置加载因子为10,那么数组填满10个就开始扩容了,10个相对来说是很容易填满的,而且在10个内出现相同的hash值概率比上面的情况要小的多,一旦扩容之后,那么计算hash值又会跟原来不一样,就不会再冲突了,这样保证了链表不会很长,甚至就一个表头都有可能,但是空间利用率很低,因为始终有很多空间没利用就开始扩容。

因此,就需要在“减小冲突”和“空间利用率”之间寻找一种平衡,这种平衡就是数据结构中有名的“时-空”矛盾的平衡。如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没什么要求的话可以将加载因子设置大一点。一般我们都使用它的默认值,即0.75。

构造函数:

//initialCapacity初始容量,loadFactor加载因子,指定“容量大小”和“加载因子”的构造函数
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //Float.isNaN()判断loadFactor是否是非数字值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;

        //将阈值设置为初始容量,这里不是真正的阈值,是为了扩展table的,后面这个阈值会重新计算
        threshold = initialCapacity;
        init();//一个空方法用于未来的子对象扩展
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold); //扩展table  

        // 将m中的全部元素逐个添加到HashMap中
        putAllForCreate(m);
    }

在构造HashMap的时候,如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则就用默认的。默认的初始容量为16,加载因子为0.75。构造函数做了什么,保存加载因子,并将初始容量先赋给阕值,但是这里并不是真正的阕值,正真的阕值是在第一次put时,初始化的,即阕值=加载因子*容量。

HashMap 方法:

put(K key, V value) 

put(K key,V value)

在此映射中关联指定值与指定键。

public V put(K key, V value) {
    if (table == EMPTY_TABLE) { //如果哈希表没有初始化(table为空)
        inflateTable(threshold); //用构造时的阈值(其实就是初始容量)扩展table
    }
    //如果key==null,就将value加到table[0]的位置
    //该位置永远只有一个value,新传进来的value会覆盖旧的value
    if (key == null)
        return putForNullKey(value);  

    int hash = hash(key); //根据键值计算hash值  

    int i = indexFor(hash, table.length); //搜索指定hash在table中的索引  

    //循环遍历Entry数组,若该key对应的键值对已经存在,则用新的value取代旧的value
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue; //并返回旧的value
        }
    }  

    modCount++;
    //如果在table[i]中没找到对应的key,那么就直接在该位置的链表中添加此Entry
    addEntry(hash, key, value, i);
    return null;
}  

下面一步步分析put方法内部都干了些啥:

首先检测table是不是为空table,如果是空table,说明并没有给table初始化,所以调用inflateTable(threadshold)方法给table初始化。该方法如下:

//初始化table
    private void inflateTable(int toSize) {
        //获取和toSize最接近的2的幂作为容量
        int capacity = roundUpToPowerOf2(toSize);

        //重新计算阈值 threshold = 容量 * 加载因子
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

        //用该容量初始化table,创建一个定长数组table,table里存储的类型是Entry
	table = new Entry[capacity];

	//初始化HashSeed的值
        initHashSeedAsNeeded(capacity);
    }

//将初始容量转变成2的幂
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY//如果容量超过了最大值,设置为最大值
                //否则设置为最接近给定值的2的次幂数
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

扩展:

一、Integer.highestOneBit()返回具有至多单个 1 位的 int 值,在指定的 int 值中最高位(最左边)的 1 位的位置。如果指定的值在其二进制补码表示形式中不具有
1 位,即它等于零,则返回零。

比如 17, 二进制是

17 0000,0000,0000,0000,0000,0000,0001,0001

它返回的是最高位的1个1, 其它全是0

16 0000,0000,0000,0000,0000,0000,0001,0000

总之一句话:返回最高位为1, 其它位为0的数。

二、用指定初始容量和指定加载因子构造一个新的空哈希表。其中initHashSeedAsNeeded方法用于初始化hashSeed参数,其中hashSeed用于计算key的hash值,它与key的hashCode进行按位异或运算。这个hashSeed是一个与实例相关的随机值,主要用于解决hash冲突。

在inflateTable方法内,首先初始化数组容量大小,数组容量永远是2的幂(下面会分析为什么要这样)。所以调用roundUpToPowerOf2方法将传进来的容量转换成最接近2的次幂的值,然后重新计算阈值threadshold
= 容量 x 加载因子,最后初始化table。所以刚开始初始化table不是在HashMap的构造函数里,因为构造函数中仅仅简单的将传进去的容量作为阈值。真正初始化table是在第一次往HashMap中put数据的时候。

初始化好了table后,就开始往table中存入数据了,table中存的是Entry实体,而put方法传进来的是key和value,所以接下来要做两件事:

1. 找到table数组中要存入的位置;

2. 将key和value封装到Entry中存入。

我们再回到put方法中,先来分析第一步,找存储的位置就要依靠key的值了,因为需要用key的值来计算hash值,根据hash值来决定在table中的位置。首先当key为null时,调用putForNullKey方法,该方法内部实现如下:

//传进key==null的Entry
private V putForNullKey(V value) {
    //遍历一遍table数组
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;  

            /*对已经在HashMap中k位置进行v的覆盖时,会调用此方法,空函数 */
            e.recordAccess(this);  

            return oldValue;
        }
    }  

    //如果table为null  或者原table表里没有key==null的
    modCount++;
    addEntry(0, null, value, 0);//如果键为null的话,则hash值为0
    return null;
}  

从方法中可以看出,首先会定位到table[0]处,然后遍历table数组,依次查询是否有key==null的键,如果有,将对应的value用新的value值取代,同时返回旧的value值。除非table为空或者原来table里面没有key==null项,才会走下面的代码addEntry()。那么调用addEntry方法,将空键和值封装到Entry中放到table[0]的位置,addEntry方法如下:

//向HashMap中添加Entry
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length); //扩容2倍
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }  

    createEntry(hash, key, value, bucketIndex);
}
//创建一个Entry
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];//先把table中该位置原来的Entry保存
    //在table中该位置新建一个Entry,将原来的Entry挂到该Entry的next
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    //所以table中的每个位置永远只保存一个最新加进来的Entry,其他Entry是一个挂一个,这样挂上去的
    size++;
}  

从该方法中可以看出,第一个参数是hash值,中间两个是key和value,最后一个是插入table的索引位置。插入之前先判断容量是否足够并且table[bucketIndex] !=null,HashMap中是2倍扩容。resize()
,方法如下:

    //用新的容量来给table扩容
    void resize(int newCapacity) {
        Entry[] oldTable = table; //保存old table
        int oldCapacity = oldTable.length; //保存old capacity
        // 如果旧的容量已经是系统默认最大容量了,那么将阈值设置成整形的最大值,退出
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }  

        //根据新的容量新建一个table
        Entry[] newTable = new Entry[newCapacity];
        //将table转换成newTable
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        //设置阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }  

    //将所有的Entry移到新的table中
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//获得原来table中的所有Entry
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                //下面两句跟createEntry方法中原理一样的
                e.next = newTable[i];//设置e.next为newTable[i]保存的Entry
                newTable[i] = e; //将e设置为newTable[i]
                e = next; //设置e为下一个Entry,继续上面while循环
            }
        }
    }  

若table容量够,addEntry中先计算hash值,然后通过调用indexFor方法返回在索引的位置,这两个方法如下:

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // 预处理hash值,避免较差的离散hash序列,导致table没有充分利用
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    //这个方法有点意思,也是为什么容量要设置为2的幂的原因
    static int indexFor(int h, int length) {

        return h & (length-1);//h&(length-1)===h%length
    }

h & (length-1)等于h % length

length为2的次幂的话,是偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h & (length-1)的最后一位可能为0也可能为1(取决于h的值),即结果可能为奇数,也可能为偶数,这样便可以保证散列的均匀性,即均匀分布在数组table中;而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h
& (length-1)的最后一位肯定为0,级只能为偶数,这样任何hash值都会被映射到数组的偶数下标位置上,这便浪费了近一半的空间!因此,length去2的整数次幂,也是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀的散列。

具体解释请看代码:

public class Test {

	public static void main(String[] args) {

		System.out.println("^:" + (3^3) );
		System.out.println("---------------------------------");
		System.out.println("23%4:" + (23%4));
		System.out.println("23&(4-1):" + (23&(4-1)));
		System.out.println("23%3:" + (23%3));
		System.out.println("23&(3-1):" + (23&(3-1)));
		System.out.println("---------------------------------");
		System.out.println("26%4:" + (26%4));
		System.out.println("26&(4-1):" + (26&(4-1)));
		System.out.println("26%3:" + (26%3));
		System.out.println("26&(3-1):" + (26&(3-1)));

		System.out.println("i&偶数:----------------------------------------》");
		for(int i=10; i<30; i++){
			System.out.println(i+"&2:" + (i&2));
			System.out.println(i+"&2:" + (68&2));
		}
		System.out.println("i&奇数:----------------------------------------》");
		for(int i=10; i<30; i++){
			System.out.println(i+"&3:" + (i&3));
			System.out.println(i+"&3:" + (68&3));
		}

	}

}

输出结果:

^:0
---------------------------------
23%4:3
23&(4-1):3
23%3:2
23&(3-1):2
---------------------------------
26%4:2
26&(4-1):2
26%3:2
26&(3-1):2
i&偶数:----------------------------------------》
10&2:2
10&2:0
11&2:2
11&2:0
12&2:0
12&2:0
13&2:0
13&2:0
14&2:2
14&2:0
15&2:2
15&2:0
16&2:0
16&2:0
17&2:0
17&2:0
18&2:2
18&2:0
19&2:2
19&2:0
20&2:0
20&2:0
21&2:0
21&2:0
22&2:2
22&2:0
23&2:2
23&2:0
24&2:0
24&2:0
25&2:0
25&2:0
26&2:2
26&2:0
27&2:2
27&2:0
28&2:0
28&2:0
29&2:0
29&2:0
i&奇数:----------------------------------------》
10&3:2
10&3:0
11&3:3
11&3:0
12&3:0
12&3:0
13&3:1
13&3:0
14&3:2
14&3:0
15&3:3
15&3:0
16&3:0
16&3:0
17&3:1
17&3:0
18&3:2
18&3:0
19&3:3
19&3:0
20&3:0
20&3:0
21&3:1
21&3:0
22&3:2
22&3:0
23&3:3
23&3:0
24&3:0
24&3:0
25&3:1
25&3:0
26&3:2
26&3:0
27&3:3
27&3:0
28&3:0
28&3:0
29&3:1
29&3:0

再回到addEntry方法中,接下来就调用createEntry方法在table数组适当的位置开创一个Entry了,new Entry的时候,将next置为原本在该位置的Entry即可,这样,原来的Entry就挂到现在的Entry上了,以后只要在该位置新new一个Entry,就将原来的挂上去,这样一个挂一个,形成了一个链表。但是table中永远存储的是最新的Entry,并非一个真正的链表数据结构,只是这么多Entry是一个个连在一起的,跟链表很像而已。

现在往上回到put方法,我们刚刚分析完了key==null的情况,接着往下走,下面其实跟刚刚分析的一样了,先计算hash值,然后找到在table中的位置,然后开始判断是否已经有相同的key的Entry放在那了,如果有,用新的value取代旧的value,如果没有,用传进来的key和value新new一个Entry放到table中,并与原来的Entry挂上。过程跟上面分析的一模一样,唯一不同的就是key!=null。

get(Object key)

get(Object key)

返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

从源码中更可以看出,从HashMap中get元素时,先计算key的hashCode,找到数组中国对应的位置,然后通过key的equals在对应位置的链表中找到需要的元素。

其他方法:

//返回当前HashMap的key-value映射数,即Entry数量
public int size() {
    return size;
}  

//判断HashMap是否为空,size==0表示空
public boolean isEmpty() {
    return size == 0;
}  

//判断HashMap中是否包含指定键的映射
public boolean containsKey(Object key) {
    return getEntry(key) != null; //getEntry方法在上面已经拿出来分析了
}  

//根据已有的Map创建对应的Entry
private void putAllForCreate(Map<? extends K, ? extends V> m) {
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putForCreate(e.getKey(), e.getValue());
}  

//看看需不需要创建新的Entry
private void putForCreate(K key, V value) {
    // 如果key为null,则定义hash为0,否则用hash函数预处理
    int hash = null == key ? 0 : hash(key);
    //计算相应的索引
    int i = indexFor(hash, table.length);  

    //遍历所有的Entry
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //如果有hash相同,且key相同,那么则不需要创建新的Entry,
        //将新的value替换旧的value值退出
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }  

    createEntry(hash, key, value, i);//否则需要创建新的Entry
}  

//用新的容量来给table扩容
void resize(int newCapacity) {
    Entry[] oldTable = table; //保存old table
    int oldCapacity = oldTable.length; //保存old capacity
    // 如果旧的容量已经是系统默认最大容量了,那么将阈值设置成整形的最大值,退出
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }  

    //根据新的容量新建一个table
    Entry[] newTable = new Entry[newCapacity];
    //将table转换成newTable
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    //设置阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}  

//将所有的Entry移到新的table中
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {//获得原来table中的所有Entry
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            //下面两句跟createEntry方法中原理一样的
            e.next = newTable[i];//设置e.next为newTable[i]保存的Entry
            newTable[i] = e; //将索引下移
            e = next; //设置e为下一个Entry,继续上面while循环
        }
    }
}  

//将指定的Map中所有映射复制到现有的HashMap中,这些映射关系将覆盖当前HashMap
//中针对指定键相同的映射关系
public void putAll(Map<? extends K, ? extends V> m) {
    //统计下需要复制多少个映射关系
    int numKeysToBeAdded = m.size();
    if (numKeysToBeAdded == 0)
        return;
    //如果table还没初始化,先用刚刚统计的复制数去初始化table
    if (table == EMPTY_TABLE) {
        inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
    }  

    //如果要复制的数目比阈值还要大,判断是否需要扩容
    if (numKeysToBeAdded > threshold) {
        int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
        if (targetCapacity > MAXIMUM_CAPACITY)
            targetCapacity = MAXIMUM_CAPACITY;
        int newCapacity = table.length;
        while (newCapacity < targetCapacity)
            newCapacity <<= 1;
        if (newCapacity > table.length)
            resize(newCapacity);
    }
    //开始复制
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
    //其实就是一个个put进去,如果有相同的key则将value替换,否则创建新的Entry映射
        put(e.getKey(), e.getValue());
}  

//根据指定的key删除Entry,返回对应的value
public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}  

//根据指定的key,删除Entry,并返回对应的value
final Entry<K,V> removeEntryForKey(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    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中的第一项的引用
                table[i] = next;//直接将第一项中的next的引用存入table[i]中
            else
            //否则将table[i]中当前Entry的前一个Entry中的next置为当前Entry的next
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;  //移动索引
        e = next;
    }  

    return e;
}  

//根据Entry来删除HashMap中的值
final Entry<K,V> removeMapping(Object o) {
    if (size == 0 || !(o instanceof Map.Entry))
        return null;  

    Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
    Object key = entry.getKey();//第一步也是先获得该Entry中保存的key
    int hash = (key == null) ? 0 : hash(key);
    //接下来就和上面根据key删除Entry道理一样了
    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;
        if (e.hash == hash && e.equals(entry)) {
            modCount++;
            size--;
            if (prev == e)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }  

    return e;
}  

//清空HashMap中所有的Entry
public void clear() {
    modCount++;
    //将指定的 null 值分配给指定 Entry 型数组的每个元素
    Arrays.fill(table, null);//将table中存储的Entry全部置为null
    size = 0;//size置为0
}  

//判断HashMap中是否有key映射到指定的value
public boolean containsValue(Object value) {
    if (value == null)//如果value为空,调用下面特定的containsNullValue()方法
        return containsNullValue();  

    Entry[] tab = table;//否则遍历数组表table中的每个链表的Entry
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (value.equals(e.value))//如果有Entry中的value与指定的value相等
                return true;//返回true
    return false;
}  

//value为空时调用的方法  ,
//这里查找value要遍历两遍,是因为hashmap存储结构是数组加链表
private boolean containsNullValue() {
    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (e.value == null)//与上面的不为空时大同小异
                return true;
    return false;
}  

//克隆HashMap实例,这里是浅复制,并没有复制键和值的本身
public Object clone() {
    HashMap<K,V> result = null;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // assert false;
    }
    if (result.table != EMPTY_TABLE) {
        result.inflateTable(Math.min(
            (int) Math.min(
                size * Math.min(1 / loadFactor, 4.0f),
                // we have limits...
                HashMap.MAXIMUM_CAPACITY),
           table.length));
    }
    result.entrySet = null;
    result.modCount = 0;
    result.size = 0;
    result.init();
    result.putAllForCreate(this);  

    return result;
}  

Iterator方法:

int expectedModCount;   // 用于fail-fast机制 看http://blog.csdn.net/wangnanwlw/article/details/52293134

//抽象类hashIterator重写了Iterator接口的(hasnext(),remove())
//抽象类hashIterator 没有重写next()
private abstract class HashIterator<E> implements Iterator<E> {
    Entry<K,V> next;        // 下一个Entry
    int expectedModCount;   // 用于fail-fast机制
    int index;              // 当前索引
    Entry<K,V> current;     //当前的Entry  

    HashIterator() {
        expectedModCount = modCount;//保存modCount用于fail-fast机制
        //此处为next赋值,next为数组table中第一个不为空的值
        if (size > 0) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    }
    //判断有没有下一个Entry
    public final boolean hasNext() {
        return next != null;
    }
    //获得下一个Entry
    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)//在迭代的过程中发现被修改了,就会抛出异常
            throw new ConcurrentModificationException();//即fail-fast
        Entry<K,V> e = next;
        if (e == null) //没有就抛出异常
            throw new NoSuchElementException();
        //查找下一个不为空的Entry,防止本该数组索引对应的链表已经到链尾
        //index的值是紧接着HashIterator() 中的index的值,继续++
        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
        current = e;
        return e;
    }  

    public void remove() {//删除
        if (current == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Object k = current.key;
        current = null;
        HashMap.this.removeEntryForKey(k);
        expectedModCount = modCount;
    }
}
//内部class ValueIterator迭代器,它重写了next方法
private final class ValueIterator extends HashIterator<V> {
    public V next() {
        return nextEntry().value;
    }
}
//内部class KeyIterator迭代器,它重写了next方法
private final class KeyIterator extends HashIterator<K> {
    public K next() {
        return nextEntry().getKey();
    }
}
//内部class EntryIterator迭代器,它重写了next方法
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() {
        return nextEntry();
    }
}  

//定义上面三个对应的Iterator方法
Iterator<K> newKeyIterator()   {
    return new KeyIterator();
}
Iterator<V> newValueIterator()   {
    return new ValueIterator();
}
Iterator<Map.Entry<K,V>> newEntryIterator()   {
    return new EntryIterator();
}  

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

/**
 * keySet()返回此映射中所包含的键的 Set 视图。
 * 该 set 受映射的支持,所以对映射的更改将反映在该 set 中,
 * 反之亦然。如果在对 set 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作除外),
 * 则迭代结果是不确定的。该 set 支持元素的移除,通过
 * Iterator.remove、Set.remove、removeAll、retainAll 和 clear 操作
 * 可从该映射中移除相应的映射关系。它不支持 add 或 addAll 操作。
 */
public Set<K> keySet() {
    Set<K> ks = keySet;
    return (ks != null ? ks : (keySet = new KeySet()));
}  

private final class KeySet extends AbstractSet<K> {
    public Iterator<K> iterator() {
        return newKeyIterator();
    }
    public int size() {
        return size;
    }
    public boolean contains(Object o) {
        return containsKey(o);
    }
    public boolean remove(Object o) {
        return HashMap.this.removeEntryForKey(o) != null;
    }
    public void clear() {
        HashMap.this.clear();
    }
}  

/**
 * 返回此映射所包含的值的 Collection 视图。
 * 该 collection 受映射的支持,所以对映射的更改将反映在该 collection 中,
 * 反之亦然。如果在对 collection 进行迭代的同时修改了映射(通过迭代器自己的 remove 操作除外),
 * 则迭代结果是不确定的。该 collection 支持元素的移除,
 * 通过 Iterator.remove、Collection.remove、removeAll、retainAll 和 clear 操作
 * 可从该映射中移除相应的映射关系。它不支持 add 或 addAll 操作。
 */
public Collection<V> values() {
    Collection<V> vs = values;
    return (vs != null ? vs : (values = new Values()));
}  

private final class Values extends AbstractCollection<V> {
    public Iterator<V> iterator() {
        return newValueIterator();
    }
    public int size() {
        return size;
    }
    public boolean contains(Object o) {
        return containsValue(o);
    }
    public void clear() {
        HashMap.this.clear();
    }
}  

/**
 * 返回此映射所包含的映射关系的 Set 视图。
 * 该 set 受映射支持,所以对映射的更改将反映在此 set 中,
 * 反之亦然。如果在对 set 进行迭代的同时修改了映射
 * (通过迭代器自己的 remove 操作,或者通过在该迭代器返回的映射项上执行 setValue 操作除外),
 * 则迭代结果是不确定的。该 set 支持元素的移除,
 * 通过 Iterator.remove、Set.remove、removeAll、retainAll 和 clear 操作
 * 可从该映射中移除相应的映射关系。它不支持 add 或 addAll 操作。
 */
public Set<Map.Entry<K,V>> entrySet() {
    return entrySet0();
}  

private Set<Map.Entry<K,V>> entrySet0() {
    Set<Map.Entry<K,V>> es = entrySet;
    return es != null ? es : (entrySet = new EntrySet());
}  

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator();
    }
    public boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<K,V> e = (Map.Entry<K,V>) o;
        Entry<K,V> candidate = getEntry(e.getKey());
        return candidate != null && candidate.equals(e);
    }
    public boolean remove(Object o) {
        return removeMapping(o) != null;
    }
    public int size() {
        return size;
    }
    public void clear() {
        HashMap.this.clear();
    }
}
  

序列化

/************************** 序列化 *****************************/
private void writeObject(java.io.ObjectOutputStream s)
    throws IOException
{
    //将当前类的非静态和非瞬态(transient)字段写入此流。此字段只能从正在序
    //列化的类的 writeObject 方法中调用。如果从其他地方调用该字段,
    //则将抛出 NotActiveException。
    s.defaultWriteObject();  

    //将table.length写入流
    if (table==EMPTY_TABLE) {
        s.writeInt(roundUpToPowerOf2(threshold));
    } else {
       s.writeInt(table.length);
    }  

    //将size写入流
    s.writeInt(size);  

    //这里之所以不直接将table写出,而是分开写里面保存Entry的key和value的原因是:
    //table数组定义为了transient,也就是说在进行序列化时,并不包含该成员。
    //为什么将其设置为transient呢?因为Object.hashCode方法对于一个类的两个实例返回的是不同的哈希值。
    //即我们在机器A上算出对象A的哈希值与索引,然后把它插入到HashMap中,然后把该HashMap序列化后,
    //在机器B上重新算对象的哈希值与索引,这与机器A上算出的是不一样的,
    //所以我们在机器B上get对象A时,会得到错误的结果。
    //所以我们分开序列化key和value
    if (size > 0) {
        for(Map.Entry<K,V> e : entrySet0()) {
            s.writeObject(e.getKey());
            s.writeObject(e.getValue());
        }
    }
}  

private static final long serialVersionUID = 362498820763181265L;  

private void readObject(java.io.ObjectInputStream s)
     throws IOException, ClassNotFoundException
{
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new InvalidObjectException("Illegal load factor: " +
                                           loadFactor);
    }  

    // set other fields that need values
    table = (Entry<K,V>[]) EMPTY_TABLE;  

    // Read in number of buckets
    s.readInt(); // ignored.  

    // Read number of mappings
    int mappings = s.readInt();
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                           mappings);  

    // capacity chosen by number of mappings and desired load (if >= 0.25)
    int capacity = (int) Math.min(
                mappings * Math.min(1 / loadFactor, 4.0f),
                // we have limits...
                HashMap.MAXIMUM_CAPACITY);  

    // allocate the bucket array;
    if (mappings > 0) {
        inflateTable(capacity);
    } else {
        threshold = capacity;
    }  

    init();  // Give subclass a chance to do its thing.  

    // Read the keys and values, and put the mappings in the HashMap
    for (int i = 0; i < mappings; i++) {
        K key = (K) s.readObject();
        V value = (V) s.readObject();
        putForCreate(key, value);
    }
}  

// These methods are used when serializing HashSets
int   capacity()     { return table.length; }
float loadFactor()   { return loadFactor;   } 

hashMap 遍历:

package wn.comeOn.java.test;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

public class Test2 {

	public static void main(String[] args) {

		HashMap<Integer, Character> map = new HashMap<Integer, Character>();

		Character j = 'a';
		for (int i = 0; i < 10; i++, j++) {
			map.put(i, j);
		}

		// 遍历Entry
		System.out.println("遍历Entry:");
		Integer key = null;
		Character value = null;
		Iterator itr01 = map.entrySet().iterator();
		while (itr01.hasNext()) {
			Map.Entry entry = (Entry) itr01.next();
			key = (Integer) entry.getKey();
			value = (Character) entry.getValue();
			System.out.println(key + "--->" + value);
		}

		// 遍历key
		System.out.println("遍历key:");
		Integer key02 = null;
		Iterator itr02 = map.keySet().iterator();
		while (itr02.hasNext()) {
			key = (Integer) itr02.next();
			System.out.println(key );
		}

		// 遍历value
		System.out.println("遍历value:");
		Character value03 = null;
		Iterator itr03 = map.values().iterator();
		while (itr03.hasNext()) {
			value = (Character) itr03.next();
			System.out.println( value);
		}

	}

}

输出结果:

遍历Entry:
0--->a
1--->b
2--->c
3--->d
4--->e
5--->f
6--->g
7--->h
8--->i
9--->j
遍历key:
0
1
2
3
4
5
6
7
8
9
遍历value:
a
b
c
d
e
f
g
h
i
j

    最后给大家分享一个链接:https://www.cs.usfca.edu/~galles/visualization/OpenHash.html。可以很直观的看到HashMap添加和删除数据的过程。帮助我们更好的理解HashMap。

时间: 2024-10-15 13:23:16

java源码分析之集合框架HashMap 10的相关文章

JDK1.7源码分析【集合】HashMap的死循环

前言 在JDK1.7&1.8源码对比分析[集合]HashMap中我们遗留了一个问题:为什么HashMap在调用resize() 方法时会出现死循环?这篇文章就通过JDK1.7的源码来分析并解释这个问题. 如下,并发场景下使用HashMap造成Race Condition,从而导致死循环,现象是CPU 100%被占用. final HashMap<String, String> map = new HashMap<String, String>(); for (int i =

Java 集合源码分析(一)HashMap

目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 ConcurrentHashMap 6. JDK 1.8 的 ConcurrentHashMap 7. 最后补充一下 HashMap 中的一些属性和方法 附:更这个系列感觉自己像是又挖了一个坑??,不过趁自己刚好工作不太忙,有空闲期,静下心来研究学习源码也是一件很值得做的事,自己尽量会把这个坑填完??.

《Java源码分析》:HashMap

<Java源码分析>:HashMap 看过很多次HashMap的源码了,但是,每次都没有做记录,因此,每次记忆都不太深,今天在看别人博客时提到Hashtable是线程安全的,Hashtable中的方法都用了synchronized进行了同步,于是就看了下Hashtable的源码,在看的过程中,写了篇博客,现在2016年7月20日22:03:53,还在教研室,感觉回寝室还早,因此,决定再看下HashMap的源码,也随便以写博客的形式做点笔记. 还是很看其他类的源码一样,先看构造函数,然后看一些比

《Java源码分析》:Vector

<Java源码分析>:Vector 虽然,Vector集合在我们的编程中,使用的比较少,至少我使用的比较少,一般情况下,我都是倾向于使用List来存储一些同类型的元素. 其实,Vector的内部实现和ArrayList的内部实现基本一致,内部都是借助于数组来实现的.下面就一起来分析下. HashMap类的源码分析,博客在这里:http://blog.csdn.net/u010412719/article/details/51980632 Hashtable类的源码分析,博客在这里:http:/

《Java源码分析》:线程池 ThreadPoolExecutor

<Java源码分析>:线程池 ThreadPoolExecutor ThreadPoolExecutor是ExecutorService的一张实现,但是是间接实现. ThreadPoolExecutor是继承AbstractExecutorService.而AbstractExecutorService实现了ExecutorService接口. 在介绍细节的之前,先介绍下ThreadPoolExecutor的结构 1.线程池需要支持多个线程并发执行,因此有一个线程集合Collection来执行

《Java源码分析》:Java NIO 之 Buffer

<Java源码分析>:Java NIO 之 Buffer 在上篇博文中,我们介绍了Java NIO 中Channel 和Buffer的基本使用方法,这篇博文将从源码的角度来看下Buffer的内部实现. 在Java API文档中,对Buffer的说明摘入如下: Buffer:是一个用于特定基本数据类型的容器.这里的特定基本数据类型指的是:除boolean类型的其他基本上数据类型. 缓冲区是特定基本数据类型元素的线性有限序列.除内容外,缓冲区饿基本属性还包括三个重要的属性,如下: 1.capaci

Java集合源码分析(七)HashMap&lt;K, V&gt;

一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Coll

java源码分析之HashSet

http://blog.csdn.net/jzhf2012/article/details/8540696 Java容器类的用途是"保存对象",分为两类:Map--存储"键值对"组成的对象:Collection--存储独立元素.Collection又可以分为List和Set两大块.List保持元素的顺序,而Set不能有重复的元素. 本文分析Set中最常用的HashSet类,并简单介绍和对比LinkedHashSet. 首先对Set接口进行简要的说明. 存入Set的每

Retrofit源码分析以及MVP框架封装使用

阅读此文前请先阅读Retrofit+okhttp网络框架介绍 从上文中我们已经了解通过如下代码即可得到返回给我们call 以及 response对象,今天我们通过源码来分析这个过程是如何实现的. /** * 获取天气数据 * @param cityname * @param key * @return */ @GET("/weather/index") Call<WeatherData> getWeatherData(@Query("format") S