《开发实战》08 | 判等问题:程序里如何确定你就是你?

28 阅读6分钟

注意 equals 和 == 的区别

对基本类型,比如 int、long,进行判等,只能使用 ==,比较的是直接值。因为基本类型的值就是其数值。
对引用类型,比如 Integer、Long 和 String,进行判等,需要使用 equals 进行内容判等。因为引用类型的直接值是指针,使用 == 的话,比较的是指针,也就是两个对象在内存中的地址,即比较它们是不是同一个对象,而不是比较对象的内容。
比较值的内容,除了基本类型只能使用 ==外,其他类型都需要使用 equals

Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
// a == b true
编译器会把 Integer a = 127 转换为 Integer.valueOf(127)
这个转换在内部其实做了缓存,使得两个 Integer 指向同一个对象

Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
// c == d false
默认情况下 IntegerCache 会缓存[-128,127]的数值 所以false
如果设置 JVM 参数加上 -XX:AutoBoxCacheMax=1000 就是true了

Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
// g == h false
New 出来的 Integer 始终是不走缓存的新对象

Integer i = 128; //unbox
int j = 128;
// i == j  true
Integer 会先拆箱再比较,比较的肯定是数值而不是引用,因此返回 true

只需要记得比较 Integer 的值请使用 equals,而不是 ==

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

String a = "1";
String b = "1";
a == b //true
都是在常量池里

String c = new String("2");
String d = new String("2");
c == d  //false
new出来的是两个不同的对象,引用肯定不同

String e = new String("3").intern();
String f = new String("3").intern();
e == f  //true
使用 String 提供的 intern 方法也会走常量池机制,所以同样能得到true。

String g = new String("4");
String h = new String("4");
g.equals(h) //true
通过 equals 对值内容判等,是正确的处理方式,

虽然使用 new 声明的字符串调用 intern 方法,也可以让字符串进行驻留,但在业务代码中滥用 intern,可能会产生性能问题。
没事别轻易用 intern,如果要用一定要注意控制驻留的字符串的数量,并留意常量表的各项指标

实现一个 equals 没有这么简单

对象的判断,默认的equals比较的是对象的引用。一般来说 需要重写equals。

class Point {
  private int x;
  private int y;
  private final String desc;
  public Point(int x, int y, String desc) {
    this.x = x;
    this.y = y;
    this.desc = desc;
  }
}

如果直接对象之间equals那么肯定是false的。
但是对象怎么比较呢?
如果我出规则,x和y相同,那么对象就是相同的。这样写 就可以了

@Override
public boolean equals(Object o) {
  if (this == o) return true;
  if (o == null || getClass() != o.getClass()) return false;
  PointRight that = (PointRight) o;
  return x == that.x && y == that.y;
}

hashCode 和 equals 要配对实现

定义两个 x 和 y 属性值完全一致的 Point 对象 p1 和 p2,把p1 加入 HashSet,然后判断这个 Set 中是否存在 p2,结果返回的是false。

PointWrong p1 = new PointWrong(1, 2, "a");
PointWrong p2 = new PointWrong(1, 2, "b");
HashSet<PointWrong> points = new HashSet<>();
points.add(p1);
log.info("points.contains(p2) ? {}", points.contains(p2));

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

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

这些都可以利用Lombok和IDEA代码自动生成实现。

注意 compareTo 和 equals 的逻辑一致性

案例:代码里本来使用了 ArrayList 的 indexOf 方法进行元素搜索,但是一位好心的开发同学觉得逐一比较的时间复杂度是 O(n),效率太低了,于是改为了**排序后通过 Collections.binarySearch **方法进行搜索
定义一个 Student 类

@Data
@AllArgsConstructor
class Student implements Comparable<Student>{
  private int id;
  private String name;
  @Override
  public int compareTo(Student other) {
    int result = Integer.compare(other.id, id);
    if (result==0)
    log.info("this {} == other {}", this, other);
    return result;
  }
}

分别通过 indexOf 方法和 Collections.binarySearch 方法进行搜索

List<Student> list = new ArrayList<>();
list.add(new Student(1, "zhang"));
list.add(new Student(2, "wang"));

Student student = new Student(2, "li");
log.info("ArrayList.indexOf");
int index1 = list.indexOf(student);

Collections.sort(list);
log.info("Collections.binarySearch");
int index2 = Collections.binarySearch(list, student);

log.info("index1 = " + index1); -1
log.info("index2 = " + index2); 1

binarySearch 方法内部调用了元素的 compareTo 方法进行比较;
indexOf 的结果没问题,列表中搜索不到 id 为 2、name 是 li 的学生;
binarySearch 返回了索引 1,代表搜索到的结果是 id 为 2,name 是 wang 的学生。
解决方案 : 确保 compareTo 的比较逻辑和 equals 的实现一致即可

@Override
public int compareTo(StudentRight other) {
  return Comparator.comparing(StudentRight::getName)
    .thenComparingInt(StudentRight::getId)
    .compare(this, other);
}

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

小心 Lombok 生成代码的“坑”

Lombok 的 **@Data 注解会帮我们实现 equals 和 hashcode **方法,但是有继承关系时,Lombok 自动生成的方法可能就不是我们期望的了.

@Data
class Person {
  private String name;
  private String identity;
  public Person(String name, String identity) {
    this.name = name;
    this.identity = identity;
  }
}

比较身份证相同,姓名不同的对象,那么肯定是返回false的。但是我们如果希望身份证一致就认为是同一个人的话,可以使用 @EqualsAndHashCode.Exclude 注解来修饰 name 字段。

@EqualsAndHashCode.Exclude
private String name;

如果类型之间有继承,Lombok 会怎么处理子类的 equals 和 hashCode呢?

@Data
class Employee extends Person {
  private String company;
  public Employee(String name, String identity, String company) {
    super(name, identity);
    this.company = company;
  }
}

声明两个 Employee 实例,它们具有相同的公司名称,但姓名和身份证均不同

Employee employee1 = new Employee("zhuye","001", "bkjk.com");
Employee employee2 = new Employee("Joseph","002", "bkjk.com");
log.info("employee1.equals(employee2) ? {}", employee1.equals(employee2));

结果是 true,显然是没有考虑父类的属性,而认为这两个员工是同一人,说明**@EqualsAndHashCode 默认实现没有使用父类属性**。
可以手动设置 callSuper 开关为 true,来覆盖这种默认行为:

@Data
@EqualsAndHashCode(callSuper = true)
class Employee extends Person {

关于equals其实还有一个大坑,equals比较的对象除了所谓的相等外,还有一个非常重要的因素,就是该对象的类加载器也必须是同一个,不然equals返回的肯定是false;之前遇到过一个坑:重启后,两个对象相等,结果是true,但是修改了某些东西后,热加载(不用重启即可生效)后,再次执行equals,返回就是false,因为热加载使用的类加载器和程序正常启动的类加载器不同

HashSet 的 contains 方法判断元素是否在HashSet 中,同样是 Set 的 TreeSet 其 contains 方法和 HashSet 有什么区别吗?
HashSet 的本质是 HashMap,会通过 hash 函数来比较值,TreeSet 的本质是 TreeMap 会通过 compareTo 比较。