Java中equals方法探讨
Java中equals()方法探讨
基本数据类型VS
对象引用类型的相等性判断
- 基本数据类型:
byte
、char
、short
、int
、long
、float
、double
、boolean
- 对象引用类型:简单来说就是
new
出来的对象的名称就是一个对象引用
基本数据类型的相等性判断
特点:直接比较值是否相等,使用==
运算符
1 | int a = 5; |
基本数据类型的变量直接存储值,
==
直接比较两个变量的二进制值是否相等
对象引用类型的相等性判断
对象引用类型的变量相等于一个指针,利用==
相当于比较“这个指针变量”的值(是一个“地址”)是否相同,而利用equals
方法类似于对C语言中指针解引用之后判断指针所指向的地方的内容是否相等,并且可以重写equals()
方法(个人理解,有错勿喷)
==
运算符:比较内存地址
1 | String s1 = new String("hello"); |
equals()
方法:比较对象内容
默认行为与==
相同,但可重写实现内容比较(重写见后续内容)
1 | System.out.println(s1.equals(s2)); // true(String 类默认重写了 equals) |
查看String类的源码,可以看到重写的equals()
逻辑
1 | public boolean equals(Object anObject) { |
- 常见错误
- 未重写
equals
的类:对于自行创建的类的对象,默认继承原始类Object
的equals方法,该方法的实现等同于==
,比较地址
即Object.java
中的如下方法:
1 | public boolean equals(Object obj) { |
- 数组的相等性判断:
arr1 == arr2
:比较两个数组对象的地址arr1.equals(arr2)
:数组对象的equals()
方法未重写,继承Object
类的方法Arrays.equals(arr1, arr2)
:利用内容比较工具方法,会逐个比较数组元素的值,以下是源码片段简化
1
2
3
4
5
6
7
8
9
10public static boolean equals(int[] a, int[] a2) {
if (a == a2) return true; // 地址相同直接返回 true
if (a == null || a2 == null) return false;
if (a.length != a2.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != a2[i]) return false; // 逐个比较元素值
}
return true;
}
1 | int[] arr1 = {1, 2}; |
值得注意的是,如果数组对象是自创建对象,即使对于
Arrays.equals()
内容比较工具方法,其底层也任然会调用自创建对象的equals()
方法进行内容比较。若为重写,依旧进行地址比较
对于其他容器,List/Set
,ArrayList.equals()
和HashSet.equals()
方法同样依赖元素的equals()
方法。**Map
**中HashMap
的键比较同时依赖于equals()
和hashCode
方法
Object.equals(person a, person b)
的空指针安全性
首先我们需要知道,如果不在使用a.equals(b)
之前进行空指针判断而直接调用的话,会出现空指针风险。
对于a.equals(b)
,当a
为null时,直接调用a.equals(b)
会抛出NullPointerException
java.util.Objects.equals(a, b)
是 Java 7 引入的静态工具方法,专门设计用于安全处理空指针。其核心逻辑如下:
1 | public static boolean equals(Object a, Object b) { |
equals()
与hashCode
的重写
equals()
和hashCode
方法一般同时重写
二者之间具有极强的依赖于契约关系:
- 哈希集合依赖:HashSet, HashMap等集合依赖hashCode定位对象,使用equals确认对象是否真正相等
- 若x.equals(y)为true,则x.hashCode() == y.hashCode()必须为true。(若
a.equals(b)
为false
,a.hashCode()
与b.hashCode()
可以相同,但应尽量避免)
以下展示了一个不同时重写的问题:
1 | class MyClass { |
正确重写方法
1 |
|
在重写时,应该尽量避免使用可变字段(无final标记的或者String等)参与
equals
和hashCode
方法比较
如果两个类相互依赖,再重写equals
方法时仍互相依赖,可能会陷入无线递归导致栈溢出,可以为对象设置唯一标识符进行equals比较或者采取其他方法打破循环依赖(如果有多个属性,可依据实际情况对造成依赖关系的属性进行地址比较等方法)
番外
字符串常量池
在Java中,创建的字符串字面常量会被虚拟机优化存储到常量池中,与创建对象所在的堆区并不在一个地方,可能会影响==
的结果。每次创建字面字符串常量时,JVM首先检查常量池中有无相同字符串常量,有的话直接用已经存在的,否则重新在常量池中创建对象,将引用传递回来。但是通过new
创建的对象,则会强制在堆区创建新的变量
1 | String str1 = "hello"; // 常量池中创建 |
此外,通过intern()
方法可以将字符串手动加入常量池
HashMap
方法探究
首先我们来看HashMap
中的equals()
方法,该方法继承自AbstractMap
类,源码如下:
1 | public boolean equals(Object o) { |
在比较时,一定要正确实现键和值的equals()
和hashCode
方法一般同时重写
- 键对象: 必须正确覆盖
hashCode()
和equals()
方法,确保相同的逻辑键能正确匹配。 - 值对象:如果值是自定义对象,需正确实现
equals()
,否则默认比较引用地址。
此外,hashMap的键对象最好采用不可变字段,否则键对象如果发生改变,哈希值变化,但是
HashMap
不会重新分配桶位置,导致后续如get()
操作通过hashCode
的值寻找键的时候无法找到该键
HashMap
数据结构原理:
数组(桶数组)HashMap
内部维护一个Node<K,V>[] table
数组,每个数组元素称为一个桶(Bucket)。
- 桶的初始容量默认为
16
,可通过构造函数调整,但始终是2
的幂次方(优化索引计算)。
链表与红黑树- 当多个键映射到同一桶时,会以链表形式存储(拉链法)。
- 当链表长度超过
8
且桶数组长度≥64
时,链表转为红黑树(时间复杂度从O(n)
降为O(log n)
)。- 当树节点数减少到
6
时,退化为链表。
HashMap
官方文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/HashMap.html
本文参考deepseek