HashMap底层原理与面试题

article/2025/9/21 23:38:50

文章目录

    • 1 类图
    • 2 属性
      • 2.1 常见属性介绍
      • 2.2 底层数据结构
      • 2.3 重要常量
      • 2.4 链表和树的问题
        • 1 为什么不直接采用红黑树?
        • 2 为什么采用红黑树,而不是AVL平衡树?
    • 3 构造器
      • 3.1 常用构造器
    • 4 put方法
      • 4.1 put操作的整体思路
      • 4.2 树操作(略)
    • 5 get方法
    • 6 哈希处理
      • 6.1 如何有效较少碰撞
      • 6.2 hash的实现
      • 6.2 HashMap底层数组为什么总是2的n次方?
    • 7 扩容resize
      • 7.1 代码分析
      • 7.2 扩容的问题
    • 源码分析地址
    • 参考资料

1 类图

HashMap是非常常用的工具类,实现Map接口,存储key-value键值对,功能与HashTable类似,但是线程不安全,允许空键和空值。
在这里插入图片描述

2 属性

2.1 常见属性介绍

// 存放数据的数组
transient Node<K,V>[] table;// 缓存entrySet()值,用于keySet()和values()
transient Set<Map.Entry<K,V>> entrySet;// map中键值对个数
transient int size;// HashMap结构修改的次数,用于快速失败
transient int modCount;//下次resize操作的阈值,值等于capacity * load factor
//此外,数组还未分配时,本字段存放初始数组容量,0表示DEFAULT_INITIAL_CAPACITY
int threshold;// 负载因子--由final修饰,必须在构造器中初始化。
final float loadFactor;

初始容量和载入因子影响HashMap性能。容量即桶的数量,初始容量就是Node数组初始大小。载入因子是哈希表有多满的度量,当map中Entry个数大于当前容量与载入因子乘积时,进行rehash操作。

loadFactor值默认为0.75,是均衡时间和空间成本的折中值。loadFactor高会较少空间开销(扩容次数减少),但是增加了查询成本(因为hash冲突变长,导致桶中Node较多)。

2.2 底层数据结构

HashMap底层采用的数据结构是数组、链表和红黑树。数组元素(称为桶)可能是链表,也可能是红黑树,对于null和单个node可以视为是链表。链表长度大于等于8(TREEIFY_THRESHOLD)并且数组容量大于64时,转化为红黑树;红黑树元素数小于等于6(UNTREEIFY_THRESHOLD)时,转化为链表。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200909234736156.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xJWkhPTkdQSU5HMDA=,size_16,color_FFFFFF,t_70#pic_center,width=“50%” height=“50%”)

HashMap在Java8之前底层结构是数组+链表结构,我们知道数组查询快、增删慢,而链表增删快、查询慢,实现整合了数组和链表的优点;同时是非线程安全的,查询速率快。

当hash位运算后总是得到同一个值,会使得某个桶链表很长。链表查找是从头遍历,因此HashMap最坏时间复杂度是O(n)。为了解决掉这个问题,Java8设置TREEIFY_THRESHOLD(树化)值,超过阈值便转为红黑树,最坏时间复杂度降低为O(logn)。

2.3 重要常量

// 默认的初始化容量--必须是2的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 最大容量 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;// 链表长度树化的最小长度
static final int TREEIFY_THRESHOLD = 8;// 树桶元素个数小于等于6,转化为链表
static final int UNTREEIFY_THRESHOLD = 6;// 数组容量大于等于64时,才允许链表转化为红黑树,否则对table数组进行resize操作。
static final int MIN_TREEIFY_CAPACITY = 64;

2.4 链表和树的问题

1 为什么不直接采用红黑树?

因为红黑树需要进行左旋,右旋操作, 而单链表不需要,以下都是单链表与红黑树结构对比。
如果元素小于8个,查询成本高,新增成本低
如果元素大于8个,查询成本低,新增成本高

因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

2 为什么采用红黑树,而不是AVL平衡树?

主要数据结构的差别。

(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。

3 构造器

3.1 常用构造器

我们主要介绍默认构造器和map参数构造器,实际使用很少修改loadFactor值。

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

无参构造器中只初始化了loadFactor,也就是说,使用了懒加载,在添加元素时初始化数组。

public HashMap(Map<? extends K, ? extends V> m) {// 设置负载因子this.loadFactor = DEFAULT_LOAD_FACTOR;// 批量添加元素putMapEntries(m, false);
}final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {// 若是在构造器中调用if (table == null) { // 计算满足负载因子的最小容量float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);if (t > threshold)// 取大于阈值的2的n次幂作为阈值threshold = tableSizeFor(t); }// 若是在map.putAll()中调用else if (s > threshold)  resize(); // 逐个放入元素for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();// put操作putVal(hash(key), key, value, false, evict);}}
}// jdk8
// 获取大于cap的最小2的n次幂
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;
}

使用现有map创建新HashMap,主要分为两步:(1)计算阈值;(2)迭代元素处理。

  • threshold的处理:
    • 此处采用tableSizeFor()方法来计算阈值,得到大于并最接近初始容量的2的n次幂值;
    • 后续的阈值则由capacity * loadFactor计算得到。
  • 构造器只是初始化loadFactor和计算阈值,延时初始化。

4 put方法

4.1 put操作的整体思路

步骤总结:

  1. 计算hash,进行put操作
  2. 如果数组为null或者空数组,直接resize,进行初始扩容,得到一个容量为16,阈值为12的Node数组。
  3. 计算索引位置,存入值到数组
    1. 如果索引位置处Node为null,直接初始化新Node;
    2. 如果不为空,则可能存在hash冲突
      1. 若索引位置节点key与新key的hash和值相等,则直接替换。
      2. 若为红黑树,以红黑树形式新增。
      3. 若为链表,自旋判断替换旧节点,还是添加新节点到尾部。
      4. 若找到老节点,进行只替换,返回老值。
  4. 进行到这一步,说明存在新增节点,调整modCount。
  5. 判断阈值,是否需要扩容操作(resize)。
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
// 入参 hash:通过 hash 算法计算出来的值。
// 入参 onlyIfAbsent:false 表示即使 key 已经存在了,仍然会用新值覆盖原来的值,默认为 false
// 入参 evict:false表示table是创造器初始化模式
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K, V>[] tab; // 数组Node<K, V> p; // i位置节点int n, i; // n:数组长度;i:索引位置//如果数组为空,使用 resize 方法初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 如果当前索引位置是空的,直接生成新的节点在当前索引位置上if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { // 索引位置节点不为空,可能存在hash冲突Node<K, V> e; // 存放老节点K k;// 如果 key 的 hash 和值都相等,直接把当前下标位置的 Node 值赋值给临时变量if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果是红黑树,使用红黑树的方式新增else if (p instanceof TreeNode)e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);else { // 是个链表// 自旋for (int binCount = 0; ; ++binCount) {// 如果是新节点,加到链表尾部if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 当链表的长度大于等于 8 时,链表转红黑树(内部会判断数组大小)if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash); // 树化操作break;}// 链表遍历过程中,发现有元素和新增的元素相等,结束循环if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e; //更新指针}}// 如果找到匹配的老节点,则更新值,返回老值if (e != null) { // existing mapping for keyV oldValue = e.value;// 当 onlyIfAbsent 为 false 时,才会覆盖值if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount; // 更新结构修改次数//如果 HashMap 的实际大小大于扩容的门槛,开始扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

4.2 树操作(略)

5 get方法

理解了put方法后,get方法浅显易懂。

public V get(Object key) {Node<K,V> e;// hash(key) 哈希值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;// 查找 hash 对应 table 位置的 p 节点if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 如果找到的 first 节点,就是要找的,则则直接使用即可if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;if ((e = first.next) != null) {// 如果找到的 first 节点,是红黑树 Node 节点,则直接在红黑树中查找if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 如果找到的 e 是 Node 节点,则说明是链表,需要遍历查找do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;
}

注意:

  • get方法得到的值是null,除了不包含key,也可能是原本值就是null的情况。可以通过containsKey判断。

6 哈希处理

6.1 如何有效较少碰撞

HashMap通过树化(treeify)被动提升性能,hash元素也是提升性能的关键。方法主要有两个:

(1)使用扰动函数:不同的对象生成不同的hashcode,促使位置分布均匀,减少碰撞。

(2)使用final对象,并采用合适的hashcode()和equals()方法。final对象hashcode不会改变,并且通常会缓存hashcode值,例如String、Integer。

6.2 hash的实现

static final int hash(Object key) {int h;// h = key.hashCode() 计算哈希值// ^ (h >>> 16) 高 16 位与自身进行异或计算,保证计算出来的 hash 更加离散return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash过程,取hashcode值,高位右移16位,然后异或。

在这里插入图片描述

右移16位与异或的目的是,混合原始hashcode值的高位与低位,以此加大低位的随机性,同时也掺杂了部分高位特征,使散列结果更加均匀。

6.2 HashMap底层数组为什么总是2的n次方?

HashMap基于桶思想,为了存取高效,要尽量较少碰撞,就是要尽量把每个桶的数据分配均匀;

将数据分配到哪个桶的算法,就是取模,即hash%length。由于在计算机内部取余效率不如位运算,HashMap源码中将其优化为hash&(length-1),hash%length==hash&(length-1)的前提是length等于2的n次方。

为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;
例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;

7 扩容resize

7.1 代码分析

final Node<K, V>[] resize() {Node<K, V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 1. 计算扩容后的新容量if (oldCap > 0) { // oldCap 大于 0 ,说明 table 非空// 超过最大容量,则直接设置 threshold 阀值为 Integer.MAX_VALUE ,不再允许扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 扩容一倍} else if (oldThr > 0) // 初始容量被设置在threshold字段(即非默认构造器)newCap = oldThr;else {               // 初始threashold为0,表示默认构造器,采用默认值初始化数组newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 2. 上述逻辑中,未计算新阈值的情况,采用newCap * loadFactor 作为新的阀值if (newThr == 0) { // 看上去,也就是非默认构造float ft = (float) newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?(int) ft : Integer.MAX_VALUE);}threshold = newThr; // 将 newThr 赋值给 threshold 属性// 3. 数据迁移@SuppressWarnings({"rawtypes", "unchecked"})Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; // 创建新数组table = newTab; // table指向新数组if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K, V> e; // 当前节点if ((e = oldTab[j]) != null) {oldTab[j] = null; // 置空,便于GC// 桶内只有一个节点,直接放到新tableif (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// HashMap 是成倍扩容,这样原来位置的链表的节点们,会被分散到新的 table 的两个位置中去// 通过 e.hash & oldCap 计算,根据结果分到高位、和低位的位置中。// 1. 如果结果为 0 时,则放置到低位// 2. 如果结果非 1 时,则放置到高位// 举个例子,数组大小是 8 ,在数组索引位置是 1 的地方挂着两个值,两个值的 hashcode 是9和33。// 当数组发生扩容时,新数组的大小是 16,此时 hashcode 是 33 的值计算出来的数组索引位置仍然是 1Node<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;elseloTail.next = e;loTail = e;} else { // 满足高位if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 设置低位到新的 newTab 的 j 位置上if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 设置高位到新的 newTab 的 j + oldCap 位置上if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}
  1. 如果是初始化,使用threshold值分配初始容量
  2. 否则,扩展2的次幂扩展。每个桶或者留在原位置,或者移动到高位倍位置上

7.2 扩容的问题

(1)当多个线程同时发现hashMap需要调整大小,容易导致条件竞争,进而死锁。

(2)rehash操作是耗时操作。

源码分析地址

Git源码地址

参考资料

  1. https://github.com/luanqiu/java8/blob/master/src/main/java/java/util/HashMap.java
  2. http://svip.iocoder.cn/JDK/Collection-HashMap/

http://chatgpt.dhexx.cn/article/cbbr2gkF.shtml

相关文章

HashMap底层原理全解析

作为面试中的高频题目&#xff0c;我相信每一个java程序员都有必要搞懂HashMap的底层原理和实现细节&#xff0c;废话不多说直接开撸。 首先简单说一下HashMap的实现原理&#xff1a; 首先有一个Node<k,v>类型的数组&#xff0c;当添加一个键值对(key-value)时&#xf…

HashMap 底层原理///HashMap详解

HashMap超详细探讨 总述从集合出发探讨HashMapCollection接口继承树Map接口继承树 从Map接口来&#xff0c;一步一步深入其中Map接口概述Map接口&#xff1a;常用方法Map接口 Map实现类之一&#xff1a;HashMapHashMap的存储结构HashMap源码中的重要常量JDK 1.8之前JDK 1.8总结…

HashMap底层原理(图文并茂,简单易懂)

大家先看下这张图片&#xff1a; 1、HashMap是基于哈希表实现的&#xff0c;而哈希表的底层是数组加上链表的形式。 2、数组内存连续查询效率高&#xff0c;链表内存分散增删改效率高&#xff0c;哈希表采用此种存储数据的形式极大的提高操作数据的效率。 3、哈希表的默认长…

HashMap底层数据结构

HashMap集合&#xff1a; 底层是哈希表/散列表的数据结构 HashMap集合&#xff1a;1、HashMap集合底层是哈希表/散列表的数据结构。2、哈希表是一个怎样的数据结构呢&#xff1f;哈希表是一个数组和单向链表的结合体。数组&#xff1a;在查询方面效率很高&#xff0c;随机增删方…

HashMap底层数据结构(数组+链表+红黑树)

回顾一下HashMap的底层数据结构 HashMap底层实现JDK<1.7数组链表&#xff0c;JDK>1.8数组链表红黑树&#xff1b;HashMap这一个类型底层涉及到3中数据类型&#xff0c;数组、链表、红黑树&#xff0c;其中查询速度最快的是数组&#xff0c;时间复杂度是O(1),链表数据量少…

Java HashMap底层实现

HashMap 是 Java 使用频率最高的用于映射&#xff08;键值对&#xff09;处理的数据类型。JDK1.8 对 HashMap 底层的实现进行了优化&#xff0c;例如引入红黑树的数据结构和扩容的优化等。在JDK1.8以前HashMap是由数组链表的数据结构组成的。 Java为数据结构中的映射定义了一个…

java----hashmap底层原理

概述 在Java集合中&#xff0c;Map是一种特殊的集合&#xff0c;原因在于这种集合容器并不是保存单个元素&#xff0c;而是保存一个一个的Key-Vaue键值对.HashMap是基于哈希表的Map接口的实现,在项目开发中使用广泛,下面就对HashMap的源码进行解析. Hashmap的特点 1.HashMap…

HashMap底层数据结构详解

一、HashMap底层数据结构 JDK1.7及之前&#xff1a;数组链表JDK1.8&#xff1a;数组链表红黑树 关于HashMap基本的大家都知道&#xff0c;但是为什么数组的长度必须是2的指数次幂&#xff0c;为什么HashMap的加载因子要设置为0.75&#xff0c;为什么链表长度大于等于8时转成了…

复习一波HashMap底层实现原理解析

HashMap是JAVA中最常见的集合类框架&#xff0c;也是java语言中非常典型的数据结构&#xff0c;同时也是我们需要掌握的数据结构&#xff0c;更重要的也是面试题必问之一。 我们常见的有集合数据有三种结构&#xff1a;1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各…

HashMap的底层实现

1. HashMap概述&#xff1a; HashMap是基于哈希表的Map接口的非同步实现&#xff08;Hashtable跟HashMap很像&#xff0c;唯一的区别是Hashtalbe中的方法是线程安全的&#xff0c;也就是同步的&#xff09;。此实现提供所有可选的映射操作&#xff0c;并允许使用null值和null键…

HashMap底层特性全解析

文章目录 一、前言二、HashMap2.1 HashMap数据结构2.2 HashMap线程不安全2.3 哈希冲突 三、JDK1.7中HashMap的实现3.1 基本元素Entry3.2 插入逻辑3.2.1 插入逻辑3.2.2 新建节点添加到链表 3.3 数组扩容逻辑3.4 null处理3.5 辨析扩容、树化和哈希冲突 四、JDK1.8中HashMap的实现…

HashMap底层

1、HashMap底层数据结构 JDK1.7的底层是 数组链表&#xff1b; JDK1.8之后 数组 链表 红黑树&#xff1b; 数组特点&#xff1a;具有随机访问的特点&#xff0c;能达到O(1)的时间复杂度&#xff0c;数组查询快&#xff0c;增删比较麻烦&#xff1b; 链表特点&#xff1a;…

HashMap底层实现和原理(源码解析)

Note&#xff1a;文章的内容基于JDK1.7进行分析&#xff0c;1.8做的改动文章末尾进行讲解。 大家可以看一下:https://www.imooc.com/article/267756 一、先来熟悉一下我们常用的HashMap 1、概述 HashMap基于Map接口实现&#xff0c;元素以键值对的方式存储&#xff0c;并且…

HashMap 底层原理

前言 HashMap 源码和底层原理在现在面试中是必问的。因此&#xff0c;我们非常有必要搞清楚它的底层实现和思想&#xff0c;才能在面试中对答如流&#xff0c;跟面试官大战三百回合。文章较长&#xff0c;介绍了很多原理性的问题&#xff0c;希望对你有所帮助~ 正文 **说明&a…

HashMap底层原理剖析(面试收藏!!!)

HashMap HashMap底层原理剖析(超详细&#xff01;&#xff01;&#xff01;)一、散列表结构二、什么是哈希&#xff1f;三、HashMap原理讲解3.1继承体系图3.2Node数据结构分析3.3底层存储结构3.4put数据原理分析3.5什么是哈希碰撞&#xff1f;3.6JDK8为什么引入红黑树&#xff…

HashMap底层原理

文章目录 1.HashMap的概念2.底层数据结构2.JDK1.8之前存在的问题&#xff1f;3.问题&#xff1a;加载因子为什么默认值为0.75f &#xff1f;4.问题&#xff1a;如果得到key的hash值&#xff08;哈希码&#xff09;5.问题&#xff1a;如何得到插入元素在数组中的下标6.问题&…

HashMap 的底层结构和原理

1. 讲讲 HashMap 的底层结构和原理 HashMap 就是以 Key-Value 的方式进行数据存储的一种数据结构嘛&#xff0c;在我们平常开发中非常常用&#xff0c;它在 JDK 1.7 和 JDK 1.8 中底层数据结构是有些不一样的。总体来说&#xff0c;JDK 1.7 中 HashMap 的底层数据结构是数组 …

HashMap底层原理(详细介绍)

数组&#xff1a;其实所谓的数组指的就是一组相关类型的变量集合&#xff0c;并且这些变量彼此之间没有任何的关联。存储区间连续&#xff0c;占用内存严重&#xff0c;数组有下标&#xff0c;查询数据快&#xff0c;但是增删比较慢&#xff1b; 链表&#xff1a;一种常见的基…

HashMap底层详解

1、HashMap底层存储原理详解 HashMap存储原理 ☆获取到传过来的key&#xff0c;调用hash算法获取到hash值 ☆获取到hash值之后调用indexFor方法&#xff0c;通过获取到的hash值以及数组的长度算出数组的下标 (把哈希值和数组容量转换为二进&#xff0c;再在数组容量范围内与哈…

Java基础——工厂模式、单例模式、懒汉模式、饿汉模式

案例&#xff1a; 这里有Factory类、Goods接口、Foods类、Drink类以及Others类。其中&#xff0c;Foods类、Drink类和Others类继承Goods接口&#xff0c;实现各自对应的方法。然后&#xff0c;在测试类中&#xff0c;创建Goods接口指向三个子类中的某一个&#xff0c;通过Facto…