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

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

相关文章
|
5天前
|
存储 Java
Java学习笔记 List集合的定义、集合的遍历、迭代器的使用
Java学习笔记 List集合的定义、集合的遍历、迭代器的使用
|
6天前
|
存储 安全 Java
java集合框架复习----(4)Map、List、set
这篇文章是Java集合框架的复习总结,重点介绍了Map集合的特点和HashMap的使用,以及Collections工具类的使用示例,同时回顾了List、Set和Map集合的概念和特点,以及Collection工具类的作用。
java集合框架复习----(4)Map、List、set
|
6天前
|
存储 Java
java集合框架复习----(3)Set
这篇文章详细介绍了Java集合框架中的Set集合,包括HashSet和TreeSet的特点、实现原理和使用示例,展示了Set集合的无序性、元素唯一性以及如何通过自定义比较器实现元素的排序。
|
3月前
|
存储 安全 Java
java集合框架及其特点(List、Set、Queue、Map)
java集合框架及其特点(List、Set、Queue、Map)
|
9月前
|
Java
Java集合框架“List“
Java集合框架“List“
56 1
|
6天前
|
存储 安全 Java
java集合框架复习----(2)List
这篇文章是关于Java集合框架中List集合的详细复习,包括List的特点、常用方法、迭代器的使用,以及ArrayList、Vector和LinkedList三种实现类的比较和泛型在Java中的使用示例。
java集合框架复习----(2)List
|
3月前
|
Java 程序员
Java集合框架:List、Set、Map类型及泛型详解
Java集合框架:List、Set、Map类型及泛型详解
|
1月前
|
存储 安全 Java
详解Java中集合的List接口实现的ArrayList方法 | Set接口实现的HashSet方法
详解Java中集合的List接口实现的ArrayList方法 | Set接口实现的HashSet方法
|
2月前
|
存储 Java 索引
告别Java集合小白!一文读懂List的精髓
【6月更文挑战第17天】Java中的List接口作为有序集合,允许存储和操作有序元素,支持重复值。ArrayList和LinkedList是常见实现类:ArrayList基于数组,适合快速访问但插入删除慢;LinkedList基于链表,插入删除快但访问慢。了解其核心概念、方法及泛型使用,能提升编程效率和代码质量。示例代码展示了添加和访问元素。通过深入学习,可以更好地掌握List的高级用法。
25 1
|
2月前
|
存储 安全 Java
Java集合详解:Set, Map, Vector, List的对比与联系
Java集合框架核心包括List、Set、Map和Vector。List允许重复元素,如ArrayList(适合读取)和LinkedList(适合插入删除)。Set不允许重复,有HashSet(无序)和TreeSet(排序)。Map存储键值对,HashMap(无序)和TreeMap(排序)。Vector是线程安全的ArrayList替代品,但在多线程环境下使用。选择集合类型应根据应用场景,如有序、无序、键值对需求及线程安全考虑。