HashMap<K,V>源码解析

源码

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

HashMap继承了AbstractMap,AbstractMap是Map接口的一个实现,继承后,能使用Map的部分方法,节省map方法的工作。实现Cloneable, Serializable方法,说明可克隆和序列化。

官方文档1:

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

翻译:

基于哈希表的映射接口实现。该实现提供了所有可选的映射操作,并允许null值和null键。(HashMap类大致相当于Hashtable,除了它是不同步的,并且允许null。)这个类不能保证地图的顺序;特别是,它不能保证订单会随时间而保持不变。 这个实现为基本操作(get和put)提供了常量时间性能,假设哈希函数在bucket中正确地分散了元素。对集合视图的迭代要求时间与HashMap实例(桶数)的“容量”和它的大小(键值映射的数量)成比例。因此,如果迭代性能很重要,那么不将初始容量设置得太高(或者负载因素过低)是非常重要的。

官方文档2:

Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more mappings; merely changing the value associated with a key that an instance already contains is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the map. If no such object exists, the map should be “wrapped” using the Collections.synchronizedMap method. This is best done at creation time, to prevent accidental unsynchronized access to the map:
Map m = Collections.synchronizedMap(new HashMap(…));

翻译:

注意,这个实现不是同步的。如果多个线程并发地访问一个散列映射,并且至少有一个线程在结构上修改了映射,那么它必须在外部同步。(结构修改是任何添加或删除一个或多个映射的操作;仅仅更改与一个实例已经包含的键相关联的值不是结构修改。)这通常是通过在某些对象上同步完成的,这些对象自然地封装了映射。如果不存在这样的对象,则应该使用集合对映射进行“包装”。synchronizedMap方法。这是在创建时最好的做法,以防止意外的不同步访问映射:


HashMap并没有使用“synchronized”关键字修饰,说明hashMap本身并不是线程安全的。文档中也说明了这一点,hashMap不是同步的,当需要线程同步时,文档也给出了解决方法,即使用“synchronizedMap” 方法进行包装。

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

成员变量

这里写图片描述

DEFAULT_INTIAL_CAPACITY: 默认的初始容量为16
MAXIMUM_CAPACITY:最大的容量为 2 ^ 30
DEFAULT_LOAD_FACTOR:默认的加载因子为 0.75f
Entry< K,V>[] table:Entry类型的数组,HashMap用这个来维护内部的数据结构,它的长度由容量决定
size:HashMap的大小
threshold:HashMap的极限容量,扩容临界点(容量和加载因子的乘积)

HashMap的四个构造函数
 

public HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap   public
HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap
public HashMap(Map< ? extends K, ? extends V> m):构造一个映射关系与指定 Map 相同的新 HashMap

其中“初始容量(initialCapacity)”和“加载因子(loadFactor)”是主要影响HashMap的性能。

文档中这样解释:

HashMap的一个实例有两个影响其性能的参数:初始容量和负载因素。容量是哈希表中的bucket的数量,初始容量就是创建哈希表时的容量。加载因子是一个度量,在它的容量自动增加之前,哈希表是如何被允许的。当哈希表中条目的数量超过了加载因子和当前容量的乘积时,哈希表就会被重新处理(也就是说,内部数据结构被重新构建),这样哈希表的数量大约是bucket的两倍。
作为一般规则,默认的加载因子(.75)在时间和空间成本之间提供了一个很好的权衡。更高的值可以减少空间开销,但增加查找成本(在HashMap类的大多数操作中都反映了这一情况,包括get和put)。在设置初始容量时,应该考虑到map中的条目及其负载因素的预期数量,以便将rehash操作的数量最小化。如果初始容量大于最大条目数除以负载因子,则不会发生任何重新哈希操作。

数据结构

在java中最常见的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以使用这种组合来实现,HashMap也是如此,HashMap实际是一个“链表散列”,数据结构如下:

这里写图片描述

因此,hashMap的底层实现还是数组,数组的每一项都是一条链,其中“initialCapacity”为该数组的长度。
在构造函数“HashMap(int initialCapacity,float loadFactor)”中,会对“容量和加载因子”进行处理,设置hashMap的容量和hash长度,创建底层实现Entry数组(table)。
Entry是HashMap的一个内部类,用于维护key-value映射关系,除了key和value,还有next引用(该引用指向当前table位置的链表),hash值(用来确定每一个Entry链表在table中位置)

HashMap的储存

源码:

public V put(K key, V value) {
        //如果key为空的情况
        if (key == null)
            return putForNullKey(value);
        //计算key的hash值
        int hash = hash(key);
        //计算该hash值在table中的下标
        int i = indexFor(hash, table.length);
        //对table[i]存放的链表进行遍历
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断该条链上是否有hash值相同的(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;
            }
        }
        //修改次数+1
        modCount++;
        //把当前key,value添加到table[i]的链表中
        addEntry(hash, key, value, i);
        return null;
    }

1.传入key和vaule,判断key是否为NULL,如果为NULL,则调用putForNullKey,以NULL值作为key储存到hash表中。
2. 然后计算key的hash值,根据hash值搜索在哈希表table中的索引位置,若当前索引位置不为null,则对该位置的Entry链表进行遍历,如果链中存在该key,则用传入的value覆盖掉旧的value,同时把旧的value返回,结束;
3. 否则调用addEntry,用key-value创建一个新的节点,并把该节点插入到该索引对应的链表的头部

HashMap的读取实现get(key,value)

 public V get(Object key) {
        //如果key为null,求null键
        if (key == null)
            return getForNullKey();
        // 用该key求得entry
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
final Entry<K,V> getEntry(Object key) {
        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;
    }

调用hash(key)求得key的hash值,然后调用indexFor(hash)求得hash值对应的table的索引位置,然后遍历索引位置的链表,如果存在key,则把key对应的Entry返回,否则返回null

HashMap键的遍历,keySet()

HashMap遍历时,按哈希表的每一个索引的链表从上往下遍历,由于HashMap的存储规则,最晚添加的节点都有可能在第一个索引的链表中,这就造成了HashMap的遍历时无序的

参考博客HashMap


THE END

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@SokachWang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值