HashMap源码分析 (JDK1.7)

看HashMap源码有一段时间了,但是一直没有写点什么,这几天趁着要换实习公司,没什么事做,就把自己对HashMap的理解写下来,边写边整理自己的思路。

这是借用别人画的理解HashMap的图,简单理解就是它结合了数组查找快和链表插入删除快的优势。

下面直接分析源码:

先从构造函数说起:

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);

	// 找到一个大于initialCapacity的且是2的幂的最小数
	int capacity = 1;
	while (capacity < initialCapacity)
		capacity <<= 1;

	this.loadFactor = loadFactor;
    //因为loadFactor可以是大于1的数,这里防止threshold 超出最大容量
	threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
	table = new Entry[capacity];
	init();
}

虽然另外还有几个重载的构造函数,但都是通过this关键字来调用这个构造方法。

所以其实HashMap中用来存储的数据结构就是一个Entry[],说白了就是个数组,那么我们来看看Entry这个类,这是一个静态内部类,注释都写了,有些没讲的源码就没贴上去,可以自己去看,比如equals()。下面把这个类的源码贴出来:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//保存的key,final关键字定义后一旦赋值就不能修改了,所以也没有setKey方法
        V value;//保存的value
        Entry<K,V> next;//指向下一个Entry的引用
        int hash;//保存的hash值

        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;
        }
        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }
        //重写的toString,便于打印显示
        public final String toString() {
            return getKey() + "=" + getValue();
        }
        //在put()方法中,当一个entry保存的key与放入的key相同时,现存的entry会被覆盖,并调用这个方法,我认为应该是供继承HashMap的类自己实现的。
        void recordAccess(HashMap<K,V> m) {
        }

        //与recordAccess类似,当entry被移除时,调用这个方法。
        void recordRemoval(HashMap<K,V> m) {
        }
}

在讲最常用的put()方法之前先科普下:Hash,一般翻译做"散列",也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。

Put()

//将特定的key与特定的value相关联,如果map中先存在一个key的映射,oldvalue将被替换
public V put(K key, V value) {
        //如果key为null,则调用putForNullKey方法
        if (key == null)
            return putForNullKey(value);
        //根据key计算出hash值
        int hash = hash(key);
        //根据hash值和数组大小,计算出数组下标
        int i = indexFor(hash, table.length);
        //注释1
        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;
            }
        }
        //记录的修改次数+1
        modCount++;
        //向数组中放入Entry对象
        addEntry(hash, key, value, i);
        return null;
}

注释1:

根据下标索引到e,如果e不为空,则对key和e中的key进行hash值判断。怎么判断呢?

1.先判断hash值,如果hash值不相同,那肯定不是同一个对象(反之不成立),没必要浪费时间了。

2.如果hash值相同,则进一步判断与e中的key是否为同一个,通过两种方式: “==”引用判断和.equals()值判断(可自己重写),如果真的那么巧是同一个,就将old value返回并覆盖,并调用前面所说的recordAccess()方法。

找到位置i后,addEntry()实现加入一个Entry:

void addEntry(int hash, K key, V value, int bucketIndex) {
        //如果entry数量大于阀值并且当前位置i已经存在元素,则调用resize()方法并传入一个两倍于当前数组大小的参数。
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //创建一个新的Entry
        createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
}

这个方法比较巧妙,创建一个Entry引用e指向原有Entry,然后将数组i位置的引用指向e(Entry的构造方法中这样实现的),使得新创建的Entry位于链表最前端,有点像栈的FIFO。最后将size++。

看一下其中的resize()方法:

void resize(int newCapacity) {
        Entry[] oldTable = table;
        //获得数组大小,如果大于最大容量,则将阀值设为Integer.MAX_VALUE,则以后不会再触发resize()操作,一了百了
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        //下面就看不懂了,1.6中没有rehash这个参数
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;

        //重点在这,在transfer()中可以看到,将table中所有的Entry转移到newTable中
        transfer(newTable, rehash);
        //改变table指向的数组,并重新设定阀值
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

重点看transfer():

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //根据数组中每个链表开头的Entry e,获得它的下一个next,如果key不为null(有的key确实为null),则更新hash值。根据新hash值计算在新数组中下标,然后指向那个位置。最后
        for (Entry<K,V> e : table) {
            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);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
}

最后三行代码比较需要花功夫理解,

第一行:e.next指向newTable[i],不再指向所属链表中下一个元素,但next引用保存了改元素

第二行:newTable[i]指向e,newTable[i]本来是null,现在指向了e所属链表中头一个元素

第三行:e指向next,意味着e不再指向链表中头一个元素,而指向了它的下一个元素

如果看不懂那也正常,多看两遍一定能明白,我也看了好几遍。

举个例子就好比开枪射击,每执行一次while,table中某一条链表的头一个元素e好像一粒子弹,射向newTable相应位置,e指向了被射击的元素(e.next = newTable[i]),那里被射了一个坑,原有的位置被e取代(newTable[i] = e),然后用next补上了e的空缺(e = next),像自动填装弹药一样。(想了很久才明白,这就是数据结构不好的后果)

还剩一个

private V putForNullKey(V value) {
    //所有key为null的Entry保存在数组下标为0的链表上
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            //找到不为null的Entry e(这个判断有必要吗)
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //同样调用addEntry(),增加一个Entry,hash值为0,key为null,下标为0
        addEntry(0, null, value, 0);
        return null;
}

put()方法讲差不多了,其实与之对应的另一个常用方法get()也类似,大家有兴趣可以自己阅读。

我们来看看删除操作:

remove()

public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
}

removeEntryForKey()返回一个要被移除的e,若e==null,则返回null,否则返回e中的value

final Entry<K,V> removeEntryForKey(Object key) {
    //这两行在put()中都见过
        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--;
                //如果删除头节点(table[i])
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            //对象引用向后移动,像C中指针向后移动
            prev = e;
            e = next;
        }
        return e;
}

上面的操作就跟删除链表节点一样(我数据结构学得不好,看起来比较吃力),不同点在于如果删除的是头节点(table[i])就有不同。

同样是删除的还有

public void clear() {
        modCount++;
        Entry[] tab = table;
        for (int i = 0; i < tab.length; i++)
            tab[i] = null;
        size = 0;
}

可见它是将每个指向链表头的引用table[i]都设为null,这样垃圾回收机制会在合适的时间把他们收集并释放空间。

时间: 2024-10-12 02:57:12

HashMap源码分析 (JDK1.7)的相关文章

HashMap源码分析--jdk1.8

JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8 HashMap概述 ??1. HashMap是可以动态扩容的数组,基于数组.链表.红黑树实现的集合.??2. HashMap支持键值对取值.克隆.序列化,元素无序,key不可重复value可重复,都可为null.??3. HashMap初始默认长度16,超出扩容2倍,填充因子0.75f.??4.HashMap当链表的长度大于8的且数组大小大于64时,链表结构

HashMap源码分析jdk1.6

 1.定义 public interface Map<K,V> { int size(); boolean isEmpty(); boolean containsKey(Object key); boolean containsValue(Object value); V get(Object key); V put(K key, V value); V remove(Object key); void putAll(Map<? extends K, ? extends V> m)

AQS源码分析--jdk1.8

JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8 AbstractQueuedSynchronizer概述 ??1. AQS是一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.??2. AQS提供了双向链表.??3. AQS分为共享模式和独占模式.??4.AQS基于volatile内存可见性和CAS原子性操作实现线程间通信操作. AbstractQueuedS

ReentrantLock源码分析--jdk1.8

JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分析--jdk1.8 ReentrantLock概述 ??1. ReentrantLock是独占锁.??2. ReentrantLock分为公平模式和非公平模式.??3. ReentrantLock锁可重入(重新插入) ReentrantLock源码分析 /** * @since 1.5 * @aut

【JAVA集合】HashMap源码分析(转载)

原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储的对象是一个键值对对象(Entry<K,V>): HashMap补充说明 基于数组和链表实现,内部维护着一个数组table,该数组保存着每个链表的表头结点:查找时,先通过hash函数计算hash值,再根据hash值计算数组索引,然后根据索引找到链表表头结点,然后遍历查找该链表: HashMap数据

HashMap源码分析二

jdk1.2中HashMap的源码和jdk1.3中HashMap的源码基本上没变.在上篇中,我纠结的那个11和101的问题,在这边中找到答案了. jdk1.2 public HashMap() { this(101, 0.75f); } public HashMap(Map t) { this(Math.max(2*t.size(), 11), 0.75f); putAll(t); } jdk1.3 public HashMap() { this(11, 0.75f); } public Has

Java集合系列之HashMap源码分析

一.HashMap简介 HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射.此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能. ps:本文中的源码来自jdk1.8.0_45/src. 1.重要参数 HashMap的实例有两个参数影响其性能. 初始容量:哈希表中桶的数量 加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度 当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的

Java集合之HashMap源码分析

一.HashMap简介 HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射.此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能. ps:本文中的源码来自jdk1.8.0_45/src. 1.重要参数 HashMap的实例有两个参数影响其性能. 初始容量:哈希表中桶的数量 加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度 当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的

[Java] HashMap源码分析

1.概述 Hashmap继承于AbstractMap,实现了Map.Cloneable.java.io.Serializable接口.它的key.value都可以为null,映射不是有序的. Hashmap不是同步的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Collections.synchronizedMap(new HashMap()); (除了不同步和允许使用 null 之