《Java-SE-第十八章》之HashMap(jdk8)

简介: 《Java-SE-第十八章》之HashMap(jdk8)

文章目录

Map

HashMap

HashMap概述

**HashMap属性**

构造方法

核心方法

1.put()

添加逻辑

2.resize()

扩容逻辑

3.get()

遍历

Map

Map是一种以键值对(key-value)进行存储的集合,Map集中的每一个元素都包含一个 键(key) 对象 和 一个值(value)对象。其其特点都是由键来决定的,Map集合的键都是无序,不重复,无索引,Map集合后面重复的键对应的值会覆盖前的重复键的值,并且键和值都允许为空。

HashMap

HashMap概述

HashMap是基于哈希表的Map接口实现的,该类存储的<k,v>结构的键值对,并且k是唯一的,不能重复,而v作为值可以重复。

数据结构

HashMap属性

1.序列化ID

    private static final long serialVersionUID = 362498820763181265L;

2.HaahMap对象被创建时,初始的默认容量是16(1<<4)。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

3.HashMap的最大容量是2的30次方。

  static final int MAXIMUM_CAPACITY = 1 << 30;

4.HashMap默认的负载因子为0.75。

 static final float DEFAULT_LOAD_FACTOR = 0.75f;

5.当HashMap底层数组中某一位置的链表长度等于8,该链表便满足了转换成红黑树的条件。

 static final int TREEIFY_THRESHOLD = 8;

6.HashMap底层数组长度等于64时,链表长度等于8时,链表就会转换成红黑树。

static final int MIN_TREEIFY_CAPACITY = 64;

7.当链表已经转换成红黑树时,当树节点少于6便会退化成链表。

 static final int UNTREEIFY_THRESHOLD = 6;

8.HashMap底层是一个链表数组

transient Node<K,V>[] table;

9.该集合存储的就是Node节点,便于遍历使用

 transient Set<Map.Entry<K,V>> entrySet;

 10.记录数组中节点的个数

 transient int size;

11.记录HashMap修改的次数

  transient int modCount;

12.threshold是阈值,为了减少哈希冲突,HashMap底层的数组不是等到存储空间都被利用完之后才扩容,而是根据当前的负载因子和数组长度计算出一个阈值,当超过该阈值就进行扩容。

 int threshold;

13.该属性也是负载因子,仅仅是一个成员变量,便于在方法中使用。

final float loadFactor;

构造方法

使用HashMap的无参构造方法构造HashMap时,其默认的初始化容量是16,负载因子为0.75。

源码:

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; //负载因子为0.75
    }

HashMap还提供了三个带参构造方法,本质上可以视作两个。因为HashMap(int initialCapacity)方法体中实际调用的是HashMap(int initialCapacity, float loadFactor),使用带参构造指定初始化容量时,最终的初始化容量不一定是一开始指定的,初始化容量必须是2的次幂。tableSizeFor(initialCapacity)会对传入的容量进行调整,最终的调整的结果是等于或者大于指定容量的一个最接近2的次幂数。

  //initialCapacity和loadFactor分别为创建HashMap时指定的容量和负载因子
  public HashMap(int initialCapacity, float loadFactor) {
        //容量小于0抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +  initialCapacity);
        //容量大于数组最大容量时,将initialCapacity调整为MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
       //如果指定的负载因子小于等于0,或者负载因子是非数字则抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //进行负载因子初始化
        this.loadFactor = loadFactor;
       //
        this.threshold = tableSizeFor(initialCapacity);
    }
    //创建HashMap指定容量initialCapacity
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上面的有参构造
    }

根其它Map集合构造hashMap

如果传入的集合中有元素,在添加元素成功之前就会开辟好内存,如果该集合没有元素,就还是不会开辟内存。

  //创建一个HashMap并将m中的元素存入其中 
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

获得一个大于cap又是最接近cap的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;
      //判断n是否小于0,如果小于0则返回1,否则就继续判断是否大于最大容量,是的话就返回最大容量,不是则返回n+1。
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

综上所述,创建HashMap对象时,只是确定好了初始化容量以及负载因子,底层的数组并没有分配内存。只有当添加元素时才会给数组分配内存。

核心方法

HashMap真正的分配内配内存,是在添加元素时。

1.put()

源码:

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//该方法根据key计算出该节点应该插入到数组的哪一个下标
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
 //插入元素
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab为哈希数组,p为节点,n为数组长度,i为数组下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       //判断table的引用是否为空以及table数组的长度,为null或者为0说明需要扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//将扩容之后的数组长度赋值给n
      //如果数组的i位置没有值,就将传入的key-value插入即可,如果插入成功则返回null。如果插入的节点已经存在则返回这个节点。
        if ((p = tab[i = (n - 1) & hash]) == null)//p为该索引位置的链表的第一个节点
            tab[i] = newNode(hash, key, value, null);//插入节点
        else {
            //发生哈希冲突的情况
            //定义临时变量
            Node<K,V> e; K k;
            //如果当前索引位置对应的链表第一个元素的hash与准备添加的key的hash相同
            //并且满足一下两个条件之一
            //1.准备加入的key与p.key相同
            //2.p指向的Node节点的key的equals()与准备加入的key比较后相同(如自定义类型作为key)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判断是否是红黑树的节点
            else if (p instanceof TreeNode)
                //添加到红黑树,如果该节点在红黑树中存在则返回该节点,不存在则返回null
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //hash不为链表首元素,不是红黑树的节点,就是链表中的节点,遍历链表,依次把该元素与链表中的每个元素比较后,都不相同则加到末尾
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//如果遍历到链表末尾说明已经没有重复的节点,此时直接添加到链表的末尾
                        p.next = newNode(hash, key, value, null);
             //如果链表长度已经达到了8个节点,就会进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;
                    }
                    //在遍历的过程中找到了重复的节点,直接break,e为重复的节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //有重复的key()旧节点和插入的节点
            if (e != null) { 
                V oldValue = e.value;//拿到旧节点的value
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//将待插入节点的value更新到旧节点
                afterNodeAccess(e);
                return oldValue;//返回
            }
        }
        ++modCount;
        if (++size > threshold)//添加成功size首先++,再和阈值进行判断,大于就要扩容
            resize();
        afterNodeInsertion(evict);
        return null;//成功添加
    }

注意

链表树化需要满足连个添加,第一是链表长度已经到了8,并且数组长度要大于等于64。如果仅仅是链表长度满足了添加,调用 treeifyBin(tab, hash)去树化,实际还是对数组进行了扩容。

源码如下:


添加逻辑

1.首先根据key计算出哈希值

2.判断table是否为null或者为长度为0,满足就扩容

3.计算出数组的下标,并判断该位置是否为null

4.如果table[i]位置为null插入即可

5.不为空说明发生了哈希冲突,再判断插入的节点与当前位置节点的key是否相同

6.如果相同直接覆盖元素

7.如果不是,在判断是否是树节点

8.如果是树节点,红黑树插入

9.如果不是,直接遍历链表寻找是否存在相同的key

10.如果存在直接覆盖节点

11.不存在将该节点添加到链表末尾

12.接着判断链表长度是否达到了8,达到了尝试树化

2.resize()

final Node<K,V>[] resize() {
        //将数组引用赋值该oldTab
        Node<K,V>[] oldTab = table;
        //得到得到当前数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
       //旧阈值
        int oldThr = threshold;
        //newCap为数组新的容量,newThr为新的数组长度,newThr为新的阈值
        int newCap, newThr = 0;
      //判断是需要初始化数组还是扩容,如果oldCap小于或者等于0则表示需要初始化数组,大于0 则需要扩容
        if (oldCap > 0) {
            //oldCap大于数组的最大容量则需要重置大小
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //将数组扩容到到原来2倍,阈值也扩大原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //说明调用的是有参构造方法,理由无参构造没有对threshold进行初始化
        else if (oldThr > 0) 
            newCap = oldThr;
        else { 
            //无参构造初始化数组
            newCap = DEFAULT_INITIAL_CAPACITY;
            //计算阈值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
      //如果阈值为空需要重新计算阈值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
      //将计算出来的阈值赋值给成员变量 threshold
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
         //扩容成功后,需要将旧数组的元素搬运到新数组去
         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;//更新数组引用
        //判断旧数组是否有元素,有的话就开始搬运
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;//返回新数组的引用
    }

扩容逻辑

1.首先为初始化和扩容准备好所需的变量

2.根据当前数组的长度(oldCap)进行判断数组是需要扩容还是初始化

3.如果oldCap大于0说明是需要扩容,将数组长度以及阈值都扩充到原来的2倍

4.如果oldCap小于等于0则说明数组需要初始化,

5.根据oldThr判读是调用那个构造方法进行初始化,然后为其分配初始容量以及负载因子

6.然后对新计算出来的阈值进行检查,如果为0则需要重新计算

7.最后如果是扩容,则将源数组的元素搬运到目标数组去。

3.get()

 public V get(Object key) {
        Node<K,V> e;
       //调用getNode()方法来完成
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
    //table为数组,first头节点,n是数组长度
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //判断数组是否null并且已经开辟空间了,同时得到计算出索引位置的节点(first)
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果该索引位置的头节点就是要找到的节点直接返回
            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 {
                    //不是红黑树节点,就是链表节点遍历链表查找
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //找不到该节点
        return null;
    }

查找逻辑

1.根据key计算出哈希值

2.判断数组是否为null,长度是否为0,如果是就直接返回null

3.如果不是,计算出数组的下标,判断该位置的节点的key是否和要查找的key相同,一致直接返回

4.不相同,判断第一个节点是否是红黑树节点,如果是则去红黑树里去查找

5.如果不是树节点,说明是链表节点,遍历链表即可。找到就返回,还没找到返回null。

遍历

1.键找值的方式的遍历

代码示例

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
 * @author 929KC
 * @date 2022/11/5 15:57
 * @description:
 */
public class Demo {
    public static void main(String[] args) {
        HashMap<String,Integer> map = new HashMap<>();
        map.put("a",1);
        map.put("b",2);
        map.put("c",3);
        // 1、键找值:第一步:先拿到集合的全部键。
        Set<String> key = map.keySet();
        // 2、第二步:遍历每个键,根据键提取值
        for (String s : key) {
            int  value = map.get(s);
            System.out.println(s+" "+value);
        }
    }
}
//a 1
//b 2
//c 3

2.键值对的方式遍历

代码示例

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
 * @author 929KC
 * @date 2022/11/5 15:57
 * @description:
 */
public class Demo {
    public static void main(String[] args) {
        HashMap<String,Integer> map = new HashMap<>();
        map.put("a",1);
        map.put("b",2);
        map.put("c",3);
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            Integer value = entry.getValue();
            String key = entry.getKey();
            System.out.println(key+" "+value);
        }
    }
}
//a 1
//b 2
//c 3

3.forEach遍历

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
/**
 * @author 929KC
 * @date 2022/11/5 15:57
 * @description:
 */
public class Demo {
    public static void main(String[] args) {
        HashMap<String,Integer> map = new HashMap<>();
        map.put("a",1);
        map.put("b",2);
        map.put("c",3);
        map.forEach(new BiConsumer<String, Integer>() {
            @Override
            public void accept(String key, Integer value) {
                System.out.println(key+" "+value);
            }
        });
    }
}
//a 1
//b 2
//c 3

相关文章
|
6天前
|
安全 Java API
【Java炸裂更新】JDK 22:区域锚定引领G1垃圾回收革命,性能飙升新高度!
【9月更文挑战第6天】JDK 22的发布,标志着Java在性能优化和垃圾回收技术上的又一次重大突破。区域锚定技术的引入,不仅提升了G1垃圾收集器的效率,也为Java应用的性能提升注入了新的动力。随着Java生态的不断发展和完善,我们有理由相信,Java将继续在编程界保持其铁打英雄的地位,为开发者们带来更多惊喜和可能。 让我们共同期待,Java在JDK 22的引领下,开启一个全新的性能飙升时代!
40 17
|
3天前
|
Java API 开发者
【Java模块化新飞跃】JDK 22模块化增强:构建更灵活、更可维护的应用架构!
【9月更文挑战第9天】JDK 22的模块化增强为开发者构建更灵活、更可维护的应用架构提供了强有力的支持。通过模块化设计、精细的依赖管理和丰富的工具支持,开发者可以更加高效地开发和管理应用,提高应用的性能和可维护性。
29 10
|
5天前
|
存储 Java 开发者
【Java新纪元启航】JDK 22:解锁未命名变量与模式,让代码更简洁,思维更自由!
【9月更文挑战第7天】JDK 22带来的未命名变量与模式匹配的结合,是Java编程语言发展历程中的一个重要里程碑。它不仅简化了代码,提高了开发效率,更重要的是,它激发了我们对Java编程的新思考,让我们有机会以更加自由、更加创造性的方式解决问题。随着Java生态系统的不断演进,我们有理由相信,未来的Java将更加灵活、更加强大,为开发者们提供更加广阔的舞台。让我们携手并进,共同迎接Java新纪元的到来!
29 11
|
3天前
|
监控 IDE Java
【Java性能调优新工具】JDK 22性能分析器:深度剖析,优化无死角!
【9月更文挑战第9天】JDK 22中的性能分析器为Java应用的性能调优提供了强大的支持。通过深度集成、全面监控、精细化分析和灵活报告生成等核心优势,性能分析器帮助开发者实现了对应用性能的全面掌控和深度优化。在未来的Java开发过程中,我们期待性能分析器能够继续发挥重要作用,为Java应用的性能提升贡献更多力量。
|
6天前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
29 11
|
3天前
|
监控 Java 大数据
【Java内存管理新突破】JDK 22:细粒度内存管理API,精准控制每一块内存!
【9月更文挑战第9天】虽然目前JDK 22的确切内容尚未公布,但我们可以根据Java语言的发展趋势和社区的需求,预测细粒度内存管理API可能成为未来Java内存管理领域的新突破。这套API将为开发者提供前所未有的内存控制能力,助力Java应用在更多领域发挥更大作用。我们期待JDK 22的发布,期待Java语言在内存管理领域的持续创新和发展。
|
5天前
|
Java API 数据处理
【Java的SIMD革命】JDK 22向量API:释放硬件潜能,让Java应用性能飙升!
【9月更文挑战第7天】 JDK 22向量API的发布标志着Java编程语言在SIMD技术领域的重大突破。这一新特性不仅释放了现代硬件的潜能,更让Java应用性能实现了飙升。我们有理由相信,在未来的发展中,Java将继续引领编程语言的潮流,为开发者们带来更加高效、更加强大的编程体验。让我们共同期待Java在SIMD技术的推动下开启一个全新的性能提升时代!
|
6天前
|
Java 开发者
【Java编程新纪元】JDK 22:超级构造函数来袭,super(...) 前导语句改写编程规则!
【9月更文挑战第6天】JDK 22的超级构造函数特性是Java编程语言发展史上的一个重要里程碑。它不仅简化了代码编写,还提升了代码的可读性和维护性。我们有理由相信,在未来的Java版本中,还将有更多令人兴奋的新特性等待我们去发现和应用。让我们共同期待Java编程新纪元的到来!
|
6天前
|
Oracle Java 关系型数据库
【颠覆性升级】JDK 22:超级构造器与区域锁,重塑Java编程的两大基石!
【9月更文挑战第6天】JDK 22的发布标志着Java编程语言在性能和灵活性方面迈出了重要的一步。超级构造器和区域锁这两大基石的引入,不仅简化了代码设计,提高了开发效率,还优化了垃圾收集器的性能,降低了应用延迟。这些改进不仅展示了Oracle在Java生态系统中的持续改进和创新精神,也为广大Java开发者提供了更多的可能性和便利。我们有理由相信,在未来的Java编程中,这些新特性将发挥越来越重要的作用,推动Java技术不断向前发展。
|
6天前
|
Java API 开发者
【Java字节码操控新篇章】JDK 22类文件API预览:解锁Java底层的无限可能!
【9月更文挑战第6天】JDK 22的类文件API为Java开发者们打开了一扇通往Java底层世界的大门。通过这个API,我们可以更加深入地理解Java程序的工作原理,实现更加灵活和强大的功能。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来!