注意 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 比较。