Java技术分享
剖析equals方法
1、对于Object来说,其equals()方法底层实现就是"==",都是比较对象的引用是否相等,下为JDK源码。
Object c = 1;
Object d = 1;
boolean equals = c.equals(d);
public boolean equals(Object obj) {
return (this == obj);
}
2、在JDK中其他类中通常会重写equals()方法,例如:Integer、String,源码如下。
Integer会先将Integer对象转换成基础类型int值来比较,所以此时就不再是比较两个对象引用,是比较两个对象的值是否相等。
Integer a = 1;
Integer b = 1;
boolean equals = a.equals(b);
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
@Override
public int hashCode() {
return Integer.hashCode(value); // 由于 Integer 是不可变类,其 hashCode 就是整数值本身。
}
String和Integer一样,引用比较重写成了值比较了。
String a = "a";
String b = "a";
boolean equals = a.equals(b);
public boolean equals(Object anObject) {
if (this == anObject) {
return true; // 引用相同返回 true,引用相同,那么值肯定相同了
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value); // equals 为下面的 equals 方法
}
@IntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) { // 循环每个字符对比,本质是值比较
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
// JDK17
public int hashCode() {
int h = hash; // 缓存的 hash 值
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true; // 标记 hashCode 为 0 的情况
} else {
hash = h; // 缓存 hashCode
}
}
return h;
}
static int hashCode(byte[] value) {
int h = 0;
for (int i = 0; i < value.length; i++) {
h = 31 * h + (value[i] & 0xff);
}
return h;
}
static int hashCode(byte[] value) {
int h = 0;
for (int i = 0; i < value.length; i += 2) {
h = 31 * h + getChar(value, i);
}
return h;
}
static char getChar(byte[] val, int index) {
return (char) (((val[index] & 0xff) << 8) | (val[index + 1] & 0xff));
}
JDK17对String的hashcode()方法与早期版本相比进行了优化,主要是为了更好地适应 String
的内部存储机制的变化以及性能提升。从 JDK9开始,String
使用了一种称为 Compact Strings
的机制。即:String
内部不再总是使用 char[]
存储,而是根据字符内容选择 byte[]
存储。如果所有字符是 Latin-1(单字节字符,0~255),则使用 byte[]
,以节省内存。如果有非 Latin-1 字符(需要双字节存储),则仍使用 UTF-16
编码。
1、hash
:用于缓存计算过的 hashCode
,避免重复计算。
2、hashIsZero
:标志变量,处理特殊情况:
- 如果字符串的
hashCode
计算结果为0
,会将hashIsZero
标记为true
,防止后续重复计算。 - 区分未计算和计算后结果为
0
的情况。 isLatin1()
:判断字符串是否为 Latin-1 编码。- 如果是 Latin-1,则调用
StringLatin1.hashCode(value)
。- Latin-1 的
hashCode
(StringLatin1.hashCode
): 遍历byte[]
数组,使用与原始实现相同的算法(31 为乘数)。
- Latin-1 的
- 否则调用
StringUTF16.hashCode(value)
。- UTF-16 的
hashCode
(StringUTF16.hashCode
): 遍历byte[]
数组(每两个字节表示一个char
),计算哈希值。
- UTF-16 的
- 如果是 Latin-1,则调用
JDK17的优点
支持 Compact Strings:
- 根据字符编码选择不同的计算方式,提高内存效率和性能。
性能优化:
- 使用
hashIsZero
减少重复计算。 - 针对 Latin-1 和 UTF-16 分开处理,减少不必要的判断和操作。
兼容性:
- 虽然内部实现发生了变化,但对外部来说行为与早期版本保持一致。
同时上述说了重写equals()方法,还需要重写hashCode
方法。
根据
Object
类的规范:如果两个对象根据
equals
方法比较是相等的,那么它们的hashCode
值也必须相等。原因:
如果只重写了
equals
而没有重写hashCode
,会导致在使用哈希表(如HashMap
或HashSet
)时出现问题,因为这些集合依赖于对象的hashCode
值来确定存储位置。