Java 基础体系 · 第 46/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。
Java equals、hashCode 与比较:等价关系、排序和集合正确性
对象比较在 Java 中不是一个单独的问题,而是三套相互关联的规则:
equals判断两个对象是否具有相同的逻辑值;hashCode把逻辑值映射为哈希值,供哈希集合和哈希映射定位候选对象;Comparable或Comparator定义对象的顺序,供排序和有序集合使用。
这三套规则如果相互矛盾,程序通常不会立即抛出异常,而是表现为查找失败、集合大小异常、元素“消失”、排序结果不符合预期等隐蔽问题。
1. 先区分三种“相同”
Java 中至少存在三种容易混淆的相同性。
1.1 引用相同:==
对于对象引用,== 判断两个引用是否指向同一个对象:
String a = new String("java");
String b = new String("java");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
a 和 b 的内容相同,但它们是两个不同对象,因此 == 为 false。
对于基本类型,== 比较值;对于对象引用,== 比较身份。对象的逻辑相等通常应使用 equals,而不是 ==。
1.2 逻辑相等:equals
equals 表达的是类定义的“值相同”或“状态相同”。例如:
- 两个
String的字符序列相同; - 两个表示用户标识的对象,其标识字段相同;
- 两个日期对象表示同一个日期。
equals 的具体含义由类决定。Object.equals 的默认实现本质上是身份比较,因此没有重写 equals 的普通类,默认行为接近:
this == other
1.3 顺序相同:比较结果为零
排序接口中,compareTo 或 Comparator.compare 返回 0 表示两个对象在该排序规则下处于同一位置:
Comparator<String> byLength = Comparator.comparingInt(String::length);
byLength.compare("ab", "CD") == 0
这并不自动意味着:
"ab".equals("CD")
事实上结果为 false。
因此,“比较结果为零”和“equals 为 true”是两个独立概念。工程中最容易出错的地方,正是把这两个概念默认成同一个概念。
2. equals 的形式化契约:它必须构成等价关系
等价关系是一种满足特定数学性质的关系。对于非空对象,equals 应满足以下条件。
设关系 a ~ b 表示 a.equals(b) 为 true。
2.1 自反性
对任意非空对象 a:
也就是:
a.equals(a) == true
如果一个对象的 equals 依赖会变化的外部状态,或者实现中错误地拒绝自身,就会破坏自反性。
2.2 对称性
对任意非空对象 a 和 b:
代码形式是:
a.equals(b) == b.equals(a)
继承体系中最常见的对称性问题如下:
class Money {
final int amount;
Money(int amount) {
this.amount = amount;
}
@Override
public boolean equals(Object other) {
return other instanceof Money money
&& amount == money.amount;
}
@Override
public int hashCode() {
return Integer.hashCode(amount);
}
}
class Voucher extends Money {
final String issuer;
Voucher(int amount, String issuer) {
super(amount);
this.issuer = issuer;
}
@Override
public boolean equals(Object other) {
return other instanceof Voucher voucher
&& amount == voucher.amount
&& issuer.equals(voucher.issuer);
}
@Override
public int hashCode() {
return 31 * Integer.hashCode(amount) + issuer.hashCode();
}
}
此时:
Money money = new Money(100);
Voucher voucher = new Voucher(100, "A");
System.out.println(money.equals(voucher)); // true
System.out.println(voucher.equals(money)); // false
Money.equals 只检查 amount,而 Voucher.equals 还要求对方是 Voucher,于是对称性被破坏。
解决方式不是简单地选择 instanceof 或 getClass 中的一个,而是要让整个继承设计与相等语义一致:
- 如果相等只适用于完全相同的具体类,使用
getClass(); - 如果允许父类与子类互相相等,所有参与比较的类必须遵循同一规则;
- 需要扩展值语义时,优先使用不可变、非继承的值类型,或者重新设计类型层次。
例如,一个只允许同一具体类相等的实现:
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
Money that = (Money) other;
return amount == that.amount;
}
2.3 传递性
如果:
则必须有:
如果 equals 根据多个字段判断,而子类、代理类或不同实现类分别采用不同字段,传递性可能被破坏。
2.4 一致性
只要参与比较的对象状态没有发生变化,多次调用结果应保持一致:
a.equals(b)
不应在没有相关状态变化时,一次返回 true,另一次返回 false。
这里的“状态变化”不仅包括对象自身字段,也包括 equals 实现读取的外部可变状态。一个依赖当前时间、随机数、数据库查询结果或可变全局配置的 equals,通常无法满足这个要求。
2.5 与 null 的关系
对于任何非空对象 a:
a.equals(null) == false
equals 不应因为传入 null 而抛出异常。典型的安全结构是:
if (this == other) {
return true;
}
if (!(other instanceof User that)) {
return false;
}
instanceof 对 null 返回 false,因此这个写法天然处理了 null。
3. 正确重写 equals 和 hashCode
hashCode 的核心契约只有一个方向:
反方向不成立:
不同对象允许拥有相同哈希值,这叫哈希冲突。哈希值不是对象的唯一身份。
3.1 为什么相等对象必须有相同哈希值
哈希集合通常先根据哈希值定位候选区域,再使用 equals 确认对象。
抽象流程如下:
put(key, value)
│
├─ 计算 key.hashCode()
├─ 根据哈希值定位桶
├─ 检查桶中的候选键
└─ 使用 equals 判断是否为同一个逻辑键
查找时同样如此:
get(queryKey)
│
├─ 计算 queryKey.hashCode()
├─ 定位桶
├─ 在候选键中调用 equals
└─ 返回匹配值或报告不存在
如果两个逻辑相等的对象哈希值不同,它们可能被定位到不同的桶,查找流程甚至不会比较它们的 equals。
3.2 一个完整的值对象实现
import java.util.Objects;
public final class UserId {
private final String value;
public UserId(String value) {
this.value = Objects.requireNonNull(value, "value");
}
public String value() {
return value;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof UserId that)) {
return false;
}
return value.equals(that.value);
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public String toString() {
return value;
}
}
这里的关键关系是:
new UserId("u-1").equals(new UserId("u-1")) // true
因此两个对象必须产生相同的哈希值。
多个字段通常使用:
@Override
public int hashCode() {
return Objects.hash(field1, field2, field3);
}
或者显式组合字段哈希值。哈希算法可以不同,但必须与 equals 使用相同的逻辑字段。
3.3 equals 与 hashCode 必须使用同一组字段
错误示例:
@Override
public boolean equals(Object other) {
return other instanceof User user
&& id.equals(user.id)
&& region.equals(user.region);
}
@Override
public int hashCode() {
return id.hashCode(); // 忽略了 region
}
这个实现并不违反“相等对象哈希值相同”的必要条件,因为相等时 id 一定相同;但它可能产生更多哈希冲突,降低性能。
反过来,如果 equals 只比较 id,而 hashCode 同时加入 region,则可能违反硬性契约:
a.equals(b) == true
a.hashCode() != b.hashCode()
这会导致 HashSet 或 HashMap 查找失败。
4. 可变字段为什么会让哈希集合中的对象“消失”
哈希集合要求:对象作为键或元素存入集合后,参与 equals 和 hashCode 的状态不能改变。
下面的示例可以直接运行:
import java.util.HashMap;
import java.util.Map;
public class MutableKeyDemo {
static final class Key {
String value;
Key(String value) {
this.value = value;
}
@Override
public boolean equals(Object other) {
return other instanceof Key key
&& value.equals(key.value);
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public String toString() {
return value;
}
}
public static void main(String[] args) {
Map<Key, String> map = new HashMap<>();
Key key = new Key("before");
map.put(key, "data");
System.out.println(map.get(key)); // data
key.value = "after";
System.out.println(map.get(key)); // null
System.out.println(map.containsKey(key)); // false
System.out.println(map.size()); // 1
}
}
插入时,映射根据 "before" 的哈希值把节点放入某个桶。修改后,get 使用 "after" 的哈希值定位另一个桶,因而找不到原节点。
原节点仍然保留在 HashMap 内部,所以 size() 仍为 1;但按照当前键状态,正常查找已经无法定位它。这不是 HashMap 自动修复失败,而是键违反了集合对稳定性的前提。
因此,适合作为哈希键的对象通常具有以下特征:
- 参与相等判断的字段不可变;
- 对外暴露的可变对象不会影响这些字段;
- 对象构造完成后,其逻辑身份不再变化。
如果必须修改对象,应先移除,再修改,再重新放入:
map.remove(key);
key.value = "after";
map.put(key, "data");
但这要求 remove 发生在修改之前;修改之后可能已经无法可靠移除。
5. Comparable:对象的自然顺序
Comparable<T> 定义对象自身的自然顺序:
public interface Comparable<T> {
int compareTo(T other);
}
返回值的语义不是必须为 -1、0 或 1,而是看符号:
- 小于
0:当前对象排在参数之前; - 等于
0:两者在该顺序下等价; - 大于
0:当前对象排在参数之后。
应使用符号判断:
int result = a.compareTo(b);
if (result < 0) {
// a 在 b 之前
}
不能依赖具体返回值:
if (a.compareTo(b) == -1) {
// 错误假设:compareTo 不保证返回 -1
}
5.1 compareTo 的数学性质
令:
至少需要满足以下顺序性质。
反对称性
比较结果的符号应相反:
如果 a 小于 b,那么 b 必须大于 a。
传递性
如果:
则必须有:
排序算法和有序集合依赖这一性质。如果比较器不具备传递性,排序结果可能不可预测,某些排序实现还可能抛出比较契约相关的异常。
零值的一致性
如果:
那么对任意 z,a 与 b 相对于 z 的比较方向应一致:
直觉上,既然排序规则认为 a 和 b 在同一位置,它们就不能对第三个对象产生矛盾的排序关系。
5.2 不要用减法实现比较
以下实现可能溢出:
@Override
public int compareTo(Task other) {
return this.priority - other.priority;
}
例如:
int a = Integer.MAX_VALUE;
int b = -1;
System.out.println(a - b); // 溢出为负数
结果会错误地认为 a 小于 b。
应使用:
@Override
public int compareTo(Task other) {
return Integer.compare(this.priority, other.priority);
}
多个字段可以使用比较器组合:
@Override
public int compareTo(Task other) {
return java.util.Comparator
.comparingInt(Task::priority)
.thenComparing(Task::name)
.compare(this, other);
}
或者在类外定义:
Comparator<Task> order =
Comparator.comparingInt(Task::priority)
.thenComparing(Task::name);
5.3 compareTo 可以与 equals 不一致,但要明确后果
Comparable 的文档建议自然顺序与 equals 一致,也就是:
x.compareTo(y) == 0
通常应当等价于:
x.equals(y)
这不是所有类都强制满足的绝对要求。BigDecimal 是标准库中的典型例外:
import java.math.BigDecimal;
BigDecimal x = new BigDecimal("1.0");
BigDecimal y = new BigDecimal("1.00");
System.out.println(x.equals(y)); // false
System.out.println(x.compareTo(y)); // 0
BigDecimal.equals 同时比较数值和 scale:
1.0的 scale 是1;1.00的 scale 是2。
而 compareTo 只比较数值大小,所以认为它们相等。
这意味着“数值相等”和“对象值相等”在 BigDecimal 中不是同一个概念。业务代码必须先确定需要哪一种:
// 需要数值相等
x.compareTo(y) == 0
// 需要表示形式也相同
x.equals(y)
6. Comparator:把顺序从对象中分离出来
Comparator<T> 表示外部提供的排序规则:
@FunctionalInterface
public interface Comparator<T> {
int compare(T first, T second);
}
它适合以下情况:
- 一个类型需要多种排序方式;
- 类型本身不应承担自然顺序;
- 排序规则由业务场景决定;
- 需要按组合字段排序。
例如:
import java.util.Comparator;
record Person(String name, int age) {}
Comparator<Person> byAgeThenName =
Comparator.comparingInt(Person::age)
.thenComparing(Person::name);
Person a = new Person("Bob", 30);
Person b = new Person("Alice", 30);
System.out.println(byAgeThenName.compare(a, b) > 0); // true
年龄相同后再按姓名比较,因此 "Bob" 排在 "Alice" 之后。
6.1 Comparator 的空值策略必须明确
自然比较通常不能比较 null。如果业务允许空值,应显式指定:
Comparator<String> order =
Comparator.nullsFirst(String::compareTo);
或:
Comparator<String> order =
Comparator.nullsLast(String::compareTo);
不要让排序过程中偶发的 NullPointerException 才暴露出空值策略未定义的问题。
6.2 比较字段必须覆盖业务需要的区分度
下面的比较器只比较年龄:
Comparator<Person> byAge = Comparator.comparingInt(Person::age);
两个年龄相同的人比较结果为 0,即使姓名不同。作为排序规则,这可能完全正确;但作为 TreeSet 的比较器,它会让同年龄的人只能保留一个。
如果业务需要保留所有人,应补充稳定的区分字段:
Comparator<Person> byAgeThenName =
Comparator.comparingInt(Person::age)
.thenComparing(Person::name);
如果姓名也可能重复,还需要继续加入唯一业务标识。
7. HashSet、HashMap、TreeSet 的“相等”不是同一种相等
这是理解集合正确性的核心。
7.1 哈希集合使用 hashCode 加 equals
对于 HashSet:
set.add(element);
set.contains(query);
逻辑上会先利用哈希值缩小候选范围,再使用 equals 判断是否为同一元素。
对于 HashMap:
map.put(key, value);
map.get(queryKey);
键的判定同样依赖 hashCode 和 equals。
7.2 有序集合使用比较结果
TreeSet 和 TreeMap 使用自然顺序或传入的 Comparator:
TreeSet<T> set = new TreeSet<>(comparator);
如果:
comparator.compare(a, b) == 0
那么对这个有序集合而言,a 和 b 占据同一个排序位置。即使:
a.equals(b) == false
集合也可能只保留一个元素。
下面的程序可以直接运行:
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class CollectionEqualityDemo {
public static void main(String[] args) {
BigDecimal first = new BigDecimal("1.0");
BigDecimal second = new BigDecimal("1.00");
Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(first);
hashSet.add(second);
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(first);
treeSet.add(second);
System.out.println(first.equals(second)); // false
System.out.println(first.compareTo(second)); // 0
System.out.println(hashSet.size()); // 2
System.out.println(treeSet.size()); // 1
}
}
结果的因果链是:
HashSet通过hashCode和equals判断重复;BigDecimal("1.0")与BigDecimal("1.00")的equals为false;- 所以
HashSet保留两个元素; TreeSet通过compareTo判断排序位置;- 两者的
compareTo为0; - 所以
TreeSet只认为有一个排序元素。
这不是 TreeSet 违反自身规则,而是两种集合采用了不同的等价定义。
7.3 不一致时可能出现的失败表现
import java.util.Comparator;
import java.util.TreeSet;
public class ComparatorSetDemo {
public static void main(String[] args) {
Comparator<String> byLength = Comparator.comparingInt(String::length);
TreeSet<String> set = new TreeSet<>(byLength);
set.add("ab");
set.add("CD");
System.out.println(set.size()); // 1
System.out.println(set.contains("xy")); // true
}
}
"ab"、"CD" 和 "xy" 长度都为 2,比较结果都为 0。因此:
- 第二次
add不会增加集合大小; contains("xy")可能返回true,尽管集合中没有字符串"xy";first()返回的可能是"ab",但它代表的是“长度为 2 的等价类”。
因此,传给 TreeSet 或 TreeMap 的比较器必须与集合的业务唯一性一致。若比较器只表达“排序优先级”,却没有表达“元素是否应视为重复”,就不应直接用它定义集合唯一性。
8. 有序关系与等价类
可以更形式化地理解 TreeSet 的行为。
给定比较器 C,定义:
TreeSet 实际上按照这个关系划分元素。每个 \approx_C 等价类只保留一个代表元素。
如果比较器满足完整的比较契约,那么这个关系通常具有等价关系的性质:
C.compare(a,a) == 0:自反;C.compare(a,b) == 0时,C.compare(b,a) == 0:对称;- 零值关系满足传递性:传递。
但这个等价类不一定等于 equals 等价类。
例如,按长度排序时:
{ "a", "B", "x" } // 长度为 1 的一个等价类
{ "ab", "CD", "xy" } // 长度为 2 的一个等价类
如果集合的业务定义是“每个字符串都必须保留”,按长度的比较器不适合作为 TreeSet 的唯一性规则;如果业务定义是“每种长度只保留一个代表”,它就可能是合理的。
9. 数组、记录类和包装类型的边界
9.1 数组的 equals 和 hashCode 不是按内容定义
数组继承自 Object,其 equals 默认是引用比较:
int[] a = {1, 2};
int[] b = {1, 2};
System.out.println(a.equals(b)); // false
按内容比较应使用:
import java.util.Arrays;
System.out.println(Arrays.equals(a, b)); // true
System.out.println(Arrays.hashCode(a)); // 内容哈希
System.out.println(Arrays.deepEquals(...)); // 多维对象数组
如果把数组直接放进自定义值对象的 equals 和 hashCode,必须相应使用 Arrays.equals、Arrays.hashCode 或深度版本。
9.2 记录类不会自动把数组变成深度值对象
记录类会根据组件生成 equals、hashCode 和访问器:
record Packet(byte[] payload) {}
但数组组件本身的相等语义仍是数组引用相等,而不是数组内容相等。因此:
new Packet(new byte[] {1, 2})
.equals(new Packet(new byte[] {1, 2})) // 通常为 false
如果记录表示的是不可变值,通常应在边界复制并转换为合适的值类型,例如 List<Integer>,或显式实现基于 Arrays.equals 的方法。还要注意:即使自定义了 equals,也必须同步自定义 hashCode。
9.3 Double、Float 的特殊值
浮点包装类的相等和排序要遵循其 API 规定,不能简单套用数学直觉:
Double.NaN的处理与普通 IEEE 数学比较不同;+0.0和-0.0在某些比较语义中存在区别;Double.compare、Double.equals和==的行为并不完全相同。
因此,涉及浮点值排序、集合键或去重时,应明确采用哪一种语义,不能混用:
double a = Double.NaN;
double b = Double.NaN;
System.out.println(a == b); // false
System.out.println(Double.valueOf(a).equals(b)); // true
10. 继承、代理和类型判断的设计选择
实现 equals 时,下面两种检查表达不同的类型语义。
getClass():严格的具体类型相等
if (other == null || getClass() != other.getClass()) {
return false;
}
优点是容易保持对称性,适合不可扩展值对象。
缺点是两个不同实现类即使拥有完全相同的状态,也不会相等。
instanceof:允许子类型参与相等
if (!(other instanceof BaseValue that)) {
return false;
}
优点是支持接口或父类抽象下的值比较。
风险是子类可能加入新的参与字段,导致父类和子类使用不同标准,从而破坏对称性或传递性。
使用 instanceof 时,必须确保子类不能改变相等语义,或者采用专门的继承相等设计。对于持久化代理、ORM 实体、接口多实现类,尤其需要把“类型身份”纳入设计,而不是只比较字段。
11. Objects.equals 解决的是空值处理,不是语义设计
Objects.equals(a, b) 的逻辑大致是:
a == b || (a != null && a.equals(b))
它适合简化空值安全的字段比较:
return Objects.equals(name, that.name)
&& Objects.equals(email, that.email);
但它不会解决以下问题:
- 两个字段是否真的属于对象逻辑身份;
- 子类和父类是否允许相等;
- 数组是否应按内容比较;
BigDecimal是否应忽略 scale;- 时间值是否应按时区归一化;
- 比较器是否与
equals一致。
空值安全只是实现细节,不能替代相等语义的定义。
12. 诊断集合问题的有效路径
遇到“明明存在却查不到”“集合大小不对”时,应先判断集合使用的是哪套规则。
12.1 检查哈希集合
针对 HashSet 或 HashMap,检查:
System.out.println(key.hashCode());
System.out.println(other.hashCode());
System.out.println(key.equals(other));
重点确认:
- 逻辑相等的对象是否具有相同哈希值;
- 插入后参与计算的字段是否发生变化;
equals和hashCode是否使用同一组字段;- 是否存在数组直接参与比较;
- 是否使用了可变集合、日期或其他可变对象作为键。
12.2 检查有序集合
针对 TreeSet 或 TreeMap,检查:
int result = comparator.compare(a, b);
System.out.println(result);
System.out.println(a.equals(b));
重点确认:
compare(a, b) == 0是否意味着业务上的重复;- 比较器是否遗漏了必要的区分字段;
- 是否使用减法导致整数溢出;
- 比较字段是否包含
null; - 比较器是否具有传递性。
12.3 不要依赖集合遍历顺序判断正确性
HashSet 和 HashMap 的遍历顺序不是键的自然顺序,也不应依赖具体实现的桶布局、哈希扰动或扩容行为。即使某次运行中顺序看起来稳定,也不构成 API 保证。
需要顺序时,应明确使用:
TreeSet/TreeMap:按比较规则排序;LinkedHashSet/LinkedHashMap:保持插入顺序;- 排序后的列表:显式调用排序逻辑。
这三者解决的是不同问题,不能通过观察某次 HashMap 输出顺序来替代顺序设计。
13. 线程安全不会修复错误的相等语义
HashMap、HashSet、TreeMap 和 TreeSet 本身不是用于无协调并发修改的容器。并发访问可能产生数据竞争、可见性问题或结构性错误。
即使使用并发集合,也不能修复以下逻辑错误:
- 键在插入后发生变化;
equals与hashCode不一致;- 比较器不具备传递性;
- 比较器把业务上不同的元素判断为相等。
并发安全解决的是多个线程如何访问集合,equals、hashCode 和比较器解决的是集合如何定义和定位元素。这是两个不同层次的问题。
14. 一套可执行的验证思路
为值对象编写测试时,不能只测试几个“相等”和“不相等”的例子,还应直接验证契约:
import static org.junit.jupiter.api.Assertions.*;
class UserIdTest {
@org.junit.jupiter.api.Test
void equalObjectsHaveEqualHashCodes() {
UserId a = new UserId("u-1");
UserId b = new UserId("u-1");
assertEquals(a, b);
assertEquals(b, a);
assertEquals(a.hashCode(), b.hashCode());
}
@org.junit.jupiter.api.Test
void differentValuesAreUsableAsDifferentMapKeys() {
var map = new java.util.HashMap<UserId, String>();
map.put(new UserId("u-1"), "Alice");
assertEquals("Alice", map.get(new UserId("u-1")));
assertNull(map.get(new UserId("u-2")));
}
}
对于比较器,还应验证:
assertTrue(comparator.compare(a, b) < 0);
assertTrue(comparator.compare(b, a) > 0);
assertEquals(0, comparator.compare(a, a));
若比较器包含多个字段,应构造:
- 主字段相同、次字段不同的对象;
- 所有字段都相同但对象引用不同的对象;
- 极端整数值;
null值;- 多个对象组成的三元组,用于检查传递性。
这些测试直接针对失败机制,而不是仅验证某一个排序输出。
15. 选择集合前先确定“唯一性”和“顺序”
可以用以下逻辑区分集合类型:
| 需求 | 适合的机制 |
|---|---|
按 equals 去重,并通过哈希快速查找 |
HashSet |
按 equals 判断键,并通过哈希快速定位 |
HashMap |
| 按比较器去重并保持排序 | TreeSet |
| 按比较器定位键并保持排序 | TreeMap |
保持插入顺序,同时按 equals 去重 |
LinkedHashSet |
| 只需要排序,不需要依靠排序规则去重 | List 加排序 |
关键问题不是“哪个集合性能更好”,而是:
对两个对象来说,什么条件下它们应该被视为同一个元素?
如果答案是 equals,使用哈希集合通常更自然;如果答案是比较器返回 0,使用有序集合才符合语义;如果排序只是展示需求,就不应让排序规则意外决定数据是否丢失。
equals 定义逻辑相等,hashCode 必须支持这种相等在哈希结构中的定位,Comparable 和 Comparator 定义顺序,而有序集合会把比较结果为零的对象视为同一个排序元素。只有先明确这三套关系各自表达什么,才能让对象、排序和集合在 Java 中保持一致。
系列导航与关联阅读
- 系列入口:Java 完整学习路线:从 Java 25 语言与 JVM 到 Spring、微服务和生产交付
- 上一篇:Java 25 不可变对象:防御复制、深浅不可变、线程安全和 Builder
- 下一篇:Java Optional:创建、组合、空值边界、性能与错误用法
官方资料
本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论