Java集合源码剖析——基于JDK1.8中HashMap的实现原理(下)

简介: Java集合源码剖析——基于JDK1.8中HashMap的实现原理(下)

文章目录:


3.5 hash方法

3.6 resize方法

3.7 size方法

3.8 isEmpty方法

3.9 clear方法

3.10 containsKey方法

3.11 containsValue方法

3.12 replace方法

3.13 关于遍历map集合的三个方法

4.传统HashMap的缺点——引入红黑树


3.5 hash方法


get 方法和put方法中都需要先计算key映射到哪个桶上,然后才进行之后的操作,计算的主要代码如下:

(n - 1) & hash

上面代码中的 n 指的是哈希表的大小,hash 指的是 key 的哈希值,hash是通过下面这个方法计算出来的,采用了二次哈希的方式,其中 key hashCode 方法是一个native 方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


这个 hash 方法先通过 key hashCode 方法获取一个哈希值,再拿这个哈希值与它的高 16 位的哈希值做一个异或操作来得到最后的哈希值,计算过程可以参考下图。


为啥要这样做呢?注释中是这样解释的:如果当 n 很小,假设为 64 的话,那么 n-1 即为630x111111),这样的值跟 hashCode()直接做与操作,实际上只使用了哈希值的后 6 位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成冲突了,所以这里把高低位都利用起来,从而解决了这个问题。


正是因为与的这个操作,决定了 HashMap 的大小只能是 2 的幂次方,想一想,如果不是2的幂次方,会发生什么事情?即使你在创建HashMap的时候指定了初始大小,HashMap 在构建的时候也会调用下面这个方法来调整大小:

这个方法的作用看起来可能不是很直观,它的实际作用就是把 cap 变成第一个大于等于 2 的幂次方的数。

例如,16 还是 1613 就会调整为 1617 就会调整为 32

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

3.6 resize方法

HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize


那么HashMap 什么时候进行扩容呢

HashMap中的元素个数超过数组大小(数组总大小length不是数组中个数size*loadFactor loadFactor


(DEFAULT_LOAD_FACTOR)
0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。


在这里有一个需要注意的地方,有些文章指出当哈希表的桶占用超过阈值时就进行扩容,这是不对的;

实际上是当哈希表中的键值对个数超过阈值时,才进行扩容的。


3.7 size方法

返回map集合中键值对的个数。

 public int size() {
      return size;
  }

3.8 isEmpty方法

判断map集合是否为空。

  public boolean isEmpty() {
     return size == 0;
 }

3.9 clear方法

清空map集合,首先判断哈希表是否为空,为空的情况下,将size置为0,遍历哈希表依次清空每隔键值对。

public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

3.10 containsKey方法

判断map集合中是否包含指定的key。内部实现是调用了getNode方法,这个在上面将get方法的时候已经说过了。

  public boolean containsKey(Object key) {
      return getNode(hash(key), key) != null;
  }

3.11 containsValue方法

判断map集合中是否包含指定的value

public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}

3.12 replace方法

map集合中指定key对应的旧值替换为一个新值。

public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}

3.13 关于遍历map集合的三个方法

这三个方法分别是 keySetentrySetforEach,源码我这里就不再多说了,下面给出的是测试代码。

@Test
public void test1() {
    Map<String,Object> map = new HashMap<>();
    map.put("1001","张起灵");
    map.put("1002","小哥");
    map.put("1003","小宋");
    System.out.println("keySet方法遍历map集合: ");
    Set<String> stringSet = map.keySet();
    Iterator<String> iterator = stringSet.iterator();
    while (iterator.hasNext()) {
        String key = iterator.next();
        Object value = map.get(key);
        System.out.println("键: " + key + ", 值: " + value);
    }
    System.out.println("======================================");
    System.out.println("entrySet方法遍历map集合: ");
    Set<Map.Entry<String,Object>> entrySet = map.entrySet();
    Iterator<Map.Entry<String,Object>> entryIterator = entrySet.iterator();
    while (entryIterator.hasNext()) {
        Map.Entry<String, Object> entry = entryIterator.next();
        String key = entry.getKey();
        Object value = entry.getValue();
        System.out.println("键: " + key + ", 值: " + value);
    }
    System.out.println("======================================");
    System.out.println("forEach方法遍历map集合: ");
    map.forEach((key,value) -> System.out.println("键: " + key + ", 值: " + value));
}

4.传统HashMap的缺点——引入红黑树


(1)JDK 1.8 以前 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。

(2) HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。

(3)针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题。

HashMap 是数组+ 链表+红黑树(JDK1.8 增加了红黑树部分)实现的。

image.png

关于红黑树的三个关键参数。

目录
打赏
0
0
0
0
85
分享
相关文章
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
72 3
|
5月前
|
Java之HashMap详解
本文介绍了Java中HashMap的源码实现(基于JDK 1.8)。HashMap是基于哈希表的Map接口实现,允许空值和空键,不同步且线程不安全。文章详细解析了HashMap的数据结构、主要方法(如初始化、put、get、resize等)的实现,以及树化和反树化的机制。此外,还对比了JDK 7和JDK 8中HashMap的主要差异,并提供了使用HashMap时的一些注意事项。
220 2
Java之HashMap详解
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
78 17
Java HashMap详解及实现原理
HashMap的实现原理?
HashMap的数据结构: HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 HashMap 基于 Hash 算法实现的 1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数 组中的下标 2. 存储时,如果出现hash值相同的key,此时有两种情况。 a. 如果key相同,则覆盖原始值; b. 如果key不同(出现冲突),则将当前的key-value放入链表中 3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 HashMap JDK1.8之前 JDK1.8之前
|
15天前
|
java常见的集合类有哪些
Map接口和Collection接口是所有集合框架的父接口: 1. Collection接口的子接口包括:Set接口和List接口 2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等 3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
|
4月前
|
HashMap源码剖析-put流程
更好地掌握 `HashMap` 的内部实现原理,提高编写高效代码的能力。掌握这些原理不仅有助于优化性能,还可以帮助解决实际开发中的问题。
83 13
|
4月前
HashMap源码浅分析与解读
阿华代码解读,不是逆风就是你疯HashMap 和TreeMap都继承于Map,Map是一个接口在实现这个接口的时候,需要实例化TreeMap或者HashMap。
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
79 5
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
93 4
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。