Java类--HashMap

1、概述我们都知道HashMap是一个非常常用的数据结构,在java8之前是有数组+链表组合构成,在java8及之后都用的是数组+链表+红黑树组合构成。数组的每个地方都存储了kv键值对即Key-Value,这些键值对在java7中叫Entry,在java8中叫Node。其实HashMap的底层原理就是哈希表,我么都知道哈希表是通过哈希函数计算元素应该放在数组的哪个位置,如果有两个元素通过哈希函数计算出来的值相同就会产生哈希冲突,而链表以及红黑树就是我们来解决哈希冲突的方法。我们通过HashMap的源码来
摘要由CSDN通过智能技术生成

1、概述

我们都知道HashMap是一个非常常用的数据结构,在java8之前是有数组+链表组合构成,在java8及之后都用的是数组+链表+红黑树组合构成。数组的每个地方都存储了kv键值对Key-Value,这些键值对在java7中叫Entry,在java8中叫Node

其实HashMap的底层原理就是哈希表,我么都知道哈希表是通过哈希函数计算元素应该放在数组的哪个位置,如果有两个元素通过哈希函数计算出来的值相同就会产生哈希冲突,而链表以及红黑树就是我们来解决哈希冲突的方法。我们通过HashMap的源码来了解一下HashMap的底层实现原理。

2、成员变量

//默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量2^30
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;
//多有node节点
transient Node<K,V>[] table;
//所有的键值对
transient Set<Map.Entry<K,V>> entrySet;
//元素的个数
transient int size;
//操作次数,fail-fast机制
transient int modCount;
//扩容阈值
int threshold;
//负载因子
final float loadFactor;
//链表节点
static class Node<K,V> implements Map.Entry<K,V> {
   
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ....
}
//红黑树节点
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;
    ...
}

3、构造方法

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);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
   //给定容量
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
   //无参构造,将默认负载因子0.75赋值给负载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

public HashMap(Map<? extends K, ? extends V> m) {
   //默认负载因子,将给定的map的值复制到现在的map中
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);//这个就是先根据m.size以及负载因子确定容量,然后扩容,遍历赋值
}
//tableSizeFor方法就是获取大于cap的最小2的n次方,至于为什么我们往后看
static final int tableSizeFor(int cap) {
   //如果cap为14即1110
    int n = cap - 1;//n为13即1101
    n |= n >>> 1;//1101 >>> 1 = 0110,1101 | 0110 = 1111 = 15
    n |= n >>> 2;//1111 >>> 2 = 0111, 1111 | 0111 = 1111 = 15
    n |= n >>> 4;//...
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//判断是否容量超过最大容量
}

在这里面我们其实可以发现,除了用map构造,其他构造方法只是给负载因子loadFactorthreshold扩容阈值也可以理解为容量。没有进行任何初始化操作。HashMap把初始化操作放在put方法中,也就是第一次put的时候进行初始化操作,给table等内容赋值。注意一点:我们在无参构造的时候只是给loadFactor赋初值,而threshold没有复制,有参构造的时候两个全部复制了,这点区别我们放在put方法中研究。

4、原理解析

4.1、hash值计算

static final int hash(Object key) {
   
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//这是getNode方法中的部分代码
if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
   

我们可以看到如果key为空,计算出来的hash值为0,说明HashMapkey值允许为空。

正常情况我们计算hash值直接获取key对象的hashCode就可以了,为什么还要与hashCode的高16位进行异或运算?

首先我们看getNode方法中给定一个key值,HashMap是通过(n-1) & hash(key)来确定的,为什么用这个式子嘞:

  • ntable长度也就是2的n次幂,我们看下面一些例子

    14          = 00001100
    18          = 00010010
    24          = 00011000
    16 - 1 = 15 = 00001111
    14 & 15     = 00001100 = 14 = 14 % 16
    18 & 15     = 00000010 = 2 = 18 % 16
    24 & 15     = 00001000 = 8 = 24 % 16
    
  • 我们可以看出(n - 1) & hash(key)即求hash(key)n进行取模运算。那我们再看这样一些运算

    hash1                  = 10110010 00000000 10011101 11100011;
    hash1 >>> 16           = 00000000 00000000 10110010 00000000;
    hash1 ^ (hash1 >>> 16) = 10110010 00000000 00101111 11100011 = h1;
    hash2                  = 11111010 00000000 10011101 11100011;
    hash2 >>> 16           = 00000000 00000000 11111010 00000000;
    hash2 ^ (hash2 >>> 16) = 11111010 00000000 01100111 11100011 = h2;
    n - 1                  = 00000000 00000000 11111111 11111111;
    hash1 & (n - 1)        = 00000000 00000000 10011101 11100011;
    h1 & (n - 1)           = 00000000 00000000 00101111 11100011;
    hash2 & (n - 1)        = 00000000 00000000 10011101 11100011;
    h2 & (n - 1)           = 00000000 00000000 01100111 11100011;
    
  • 我们看上述运算,hash1与hash2的低16位完全相同,不同的是高16位。此时n = 2 ^ 16,这是如果直接对hash1与hash2进行取模运算,那么结果一定是相同的,在哈希表中相同就代表发生了哈希冲突。看到这里详细大家就知道了为什么要与高16位进行异或运算了,就是为了降低发生哈希冲突的概率

  • 那么问题又来了,为什么要用异或运算而不是与、或、非运算?

    1010 ^ 1100 = 0110
    1010 & 1100 = 1000
    1010 | 1100 = 1110
    
  • 大家是不是有那么一丢丢的想法了,那就是异或的0、1个数相比于与、或运算接近于1 : 1。至于非运算?别开玩笑了,你能对两个对象使用非运算?

4.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;
    //判断数组是否为空、长度是否为零、该key对应的位置是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
   
        //是否当前位置的头结点key与hash值与给定的一致
        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 {
   
                //是否当前位置的头结点key与hash值与给定的一致
                if (e.hash == hash &&
                    ((k &#
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值