Map源码解析之HashMap

Map使我们在开发过程中经常使用的一个数据结构,其主要有3个特点:键值对key-value、键key唯一且不可重复,每个键key都能且只能映射到一个value值。
HashMap又是Map中最常被使用的一个实现类,除了Map的3个基础特点外还具备无序性、key和value都可以为null、非线程安全等特点。
HashMap由数组组成,每个数组的元素都是一条链表,在jdk1.8中当链表超出一定长度会树化成一颗红黑树。为了保证HashMap的分布尽量均衡分散,HashMap采用了hash值与容量-1(即数组长度-1)进行按位与运算来实现取余的效果,从而确定元素在数组中的下标。而为了解决散列冲突,HashMap采用了拉链法进行处理,相同下标的元素组成链表放在数组中,当链表长度过长,又会树化成红黑树以提高存取效率。同时,当元素数量大于容量*负载因子时,HashMap会进行扩容,将容量变为原有的两倍。
接下来我们基于jdk1.8的源码对HashMap进行分析。

一. 主要的成员变量

  1. 静态成员变量
    默认的初始容量: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;
    去树化阈值:static final int UNTREEIFY_THRESHOLD = 6;
    树化的最小容量:static final int MIN_TREEIFY_CAPACITY = 64;
  2. 普通成员变量
    由链表的头结点组成的数组:transient Node<K,V>[] table;
    key-value的元素集:transient Set<Map.Entry<K,V>> entrySet;
    当前的元素数量:transient int size;
    修改次数:transient int modCount;
    扩容的阈值:int threshold;
    负载因子:final float loadFactor;

二. 主要的成员类

  1. Node<K, V>类,实现了Map.Entry<K, V>接口
    Node类表示HashMap中的每一个节点,由hash值、key值、value值和指向下一个Node的next组成。
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        //省略其它方法
}    
  1. TreeNode<K, V>类,继承了LinkedHashMap.Entry<K, V>类
    LinkedHashMap.Entry<K, V>类继承了Node<K, V>类,相比Node类多了指向前一个LinkedHashMap.Entry和后一个LinkedHashMap.Entry的before和after属性。
    TreeNode<K, V>类又继承了LinkedHashMap.Entry<K, V>类,相比LinkedHashMap.Entry类多了指向父TreeNode、左儿子TreeNode、右儿子TreeNode和前一个TreeNode的parent、left、right、prev属性,以及表示是节点颜色的布尔型的red属性。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        //省略其它方法
}        

三. 基础方法

在开始分析HashMap的主要方法之前先分析一下HashMap中被广泛使用的基础方法。

  1. tableSizeFor方法
    HashMap采用了hash值与容量-1(即数组长度-1)进行按位与运算来实现取余的效果,从而确定元素在数组中的下标,因此要求容量值必须是2的次幂。tableSizeFor实现了根据参数获取到一个不小于参数的2的次幂的值的效果。
    tableSizeFor采用了位运算来实现效果,位运算在CPU即可处理,运行速度更快。
    对于n |= n >>> 1运算,由于n的最左一位必然是1,n>>>1的的最左一位也是1,且长度比n少一位,因此n | n >>> 1的最左边两位必然都是1,如假设n是一个6位数,设为1xxxxx, 1xxxxx | 1xxxx = 11xxxx,后续步骤同理,算到 n |= n >>> 16保证了32位数都由1组成。
    由于MAXIMUM_CAPACITY = 1 << 30,因此保证了n每一位都是1,最后在进行n+1运算即保证了结果是一个2的次幂。
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
  1. hash方法
    hash方法用于获取key值的hash值,由key值的hashCode的高16位与低16位进行异或运算得到,以保证在HashMap中的元素分布尽量均衡。
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

三. 构造方法

HashMap的构造方法由4种,分别如下:

  1. 无参构造器
    无参构造器最简单,负载因子的值取默认值0.75。
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
  1. 带容量和负载因子参数的构造方法
    对初始容量做校验,小于零时抛出异常,大于最大容量时取最大容量,然后分别进行赋值。
    注意,HashMap并没有存储容量capacity的成员变量,也不会立即新建一个对应长度的数组,此时传入的初始容量根据tableSizeFor方法取到合适的容量值后存储在扩容阈值threshold中。
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initi
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值