本文主要阐述代码中的判等问题。

equals

== 和 equals

概述

比较值的内容,基本类型使用 == ,引用类型使用 equals

整型

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**1**/
Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
a == b;//true
/**2**/
Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
c == d;//false
/**3**/
Integer e = 127; //Integer.valueOf(127)
Integer f = new Integer(127); //new instance
e == f;//false
/**4**/
Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
g == h;//false
/**5**/
Integer i = 128; //unbox
int j = 128;
i == j;//true

分析

  1. Integer a = 127转换为Integer.valueOf(127),转换在内部做了缓存,使得两个 Integer 指向同一个对象,所以 == 返回 true。

    1
    2
    3
    4
    5
    public static Integer valueOf(int i) { 
    if (i >= IntegerCache.low && i <= IntegerCache.high)
    return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
    }
  2. 由于默认情况下会缓存[-128, 127]的数值,而 128 处于这个区间之外,故此时 == 返回false。

  3. 实例化的对象与缓存,以及实例化的对象之间, == 都会返回false。

  4. 装箱的 Integer 会先拆箱再比较,即数值比较,故 == 返回true。

结论

  • 比较 Integer 的值请使用 equals,而不是 ==。
  • 注意 Integer 的 == 比较返回为真的范围是[-128, 127]。

字符串类型

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**1**/
String a = "1";
String b = "1";
a == b;//true
/**2**/
String c = new String("2");
String d = new String("2");
c == d;//false
/**3**/
String e = new String("3").intern();
String f = new String("3").intern();
e == f;//true
/**4**/
String g = new String("4");
String h = new String("4");
g.equals(h);//true

字符串常量池机制

字符串常量池机制的设计初衷是为了节省内存。当代码中出现双引号形式创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。这种机制,就是字符串驻留或池化。

分析

  1. 由于字符串驻留机制,故用双引号赋值的两个内容相等的字符串,== 返回true。
  2. 实例化的两个对象引用不同,故 == 返回false。
  3. 使用 String 提供的 intern 方法也会走常量池机制,所以同样能得到 true。

注意

虽然使用 new 声明的字符串调用 intern 方法,也可以让字符串进行驻留,但在业务代码中滥用 intern,可能会产生性能问题。

原因:字符串常量池是一个固定容量的 Map。如果容量太小(Number of buckets=60013)、字符串太多(1000 万个字符串),那么每一个桶中的字符串数量会非常多,所以搜索起来就很慢。

解决:解决方式是,设置 JVM 参数 -XX:StringTableSize,指定更多的桶。

结论:没事别轻易用 intern,如果要用一定要注意控制驻留的字符串的数量,并留意常量表的各项指标。

equals 的实现

概述

equals 在 Object 类中的实现方式,其实就是 == 判断;而 String 和 Integer 都重写了 equals 方法。因此,新定义的类如果不重写 equals 方法,则默认使用 == 判等,比较的是对象的引用。

实现方式

如何实现 equals?

  1. 指针判等,若相等则返回true;
  2. 判空,若比较对象为空则返回false;
  3. 判断两个对象类型,若不同则返回false;
  4. 进行类型转换,逐一判断所有字段。
1
2
3
4
5
6
7
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NewClass that = (NewClass) o;
return x == that.x && y == that.y;
}

hashCode

概述

由源码注释可知,hashCode 会返回对象的哈希值,该方法会影响散列表,如HashTable。

注意

注意 hashCode 要与 equals 配对实现

散列表需要使用 hashCode 来定位元素放到哪个桶。如果自定义对象没有实现自定义的 hashCode 方法,就会使用 Object 超类的默认实现,得到的两个 hashCode 是不同的,导致无法满足需求。

实现

重写 hashCode 方法:

1
2
3
4
@Override 
public int hashCode() {
return Objects.hash(x, y);
}

compareTo

概述

该方法由Comparable接口定义,用于做排序时的比较,返回值为负数、零和正数,分别代表少于、等于和多于。

注意

如果要使用 Collections 的排序,则自定义类就要实现 Comparable。

而重写 compareTo 时要注意其逻辑与 equals 保持一致。

实现

重写compareTo方法:

1
2
3
4
5
6
@Override 
public int compareTo(NewClass other) {
return Comparator.comparing(NewClass::getA)
.thenComparingInt(NewClass::getB)
.compare(this, other);
}

结论

对于自定义的类型,如果要实现 Comparable,请记得 equals、hashCode、compareTo 三者逻辑一致

还有点啥

  • 建议在 IDE 中安装阿里巴巴的 Java 规约插件
  • getClass 和 instanceof 的区别
    • instanceof进行类型检查规则是:判断是否为该类或者该类的子类;
    • getClass获得类型信息采用==来进行检查是否相等的操作是严格的判断。不会存在继承方面的考虑。