当在 Set 中存储对象时同样一个对象是不能够存储两次的,这是 Set 的核心定义。在 Java 中,有两个方法分别用来决定两个引用对象是否相同和它们是否都能存在于 Set 中,这两个方法是 equals() and hashCode() 。接下来我将解释下相等性和一致性之间的区别以及它们各自相对来说的优点。
Java 为这两个方法都提供了标准的实现,不过标准 equals() 方法定义成一个做相似性比较的方法,也就是说是通过两块内存引用来判定是否两者相同。两个一致的对象但是存储于内存中的不同位置下会不认为是不等的。这个比较是通过 == 操作符完成的,源代码中 Object 类有如下的代码片段:
public boolean equals(Object obj) {
return (this == obj);
}
hashCode() 方法是由虚拟机将其作为 native 操作来实现的因此在代码中无法看到,不过通常是返回内存引用地址(在 32 位架构) 或者返回内存地址取模 32 后的值(在 64 位架构).
在设计类的时候程序员通常会做的一件事就是以一个不同的相等性定义来覆盖 equal() 方法,取代了原有的内存引用地址的比较,通过比较两个实例的值来判定相等性。下面就是一个不错的例子:
import java.util.Objects;
import static java.util.Objects.requireNonNull;
public final class Person {
private final String firstname;
private final String lastname;
public Person(String firstname, String lastname) {
this.firstname = requireNonNull(firstname);
this.lastname = requireNonNull(lastname);
}
@Override
public int hashCode() {
int hash = 7;
hash = 83 * hash + Objects.hashCode(this.firstname);
hash = 83 * hash + Objects.hashCode(this.lastname);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final Person other = (Person) obj;
if (!Objects.equals(this.firstname, other.firstname)) {
return false;
} else return Objects.equals(this.lastname, other.lastname);
}
}
这个比较就叫做"相等"(和前一个"一致"相较而言)。只要两个人的名和姓相同就认为他们是相等的,从输入流中区分出重复值就是"相等"的一个例子。不过一定要记住当你覆盖 equals() 方法的时候,一定也要覆盖 hashCode() 方法。
相等性
现在,如果你选择相等性而不是一致性,那么有些事需要你去思考。首先你需要问自己的是:这个拥有相同属性的两个类实例有必要一致吗?拿上边的 Person 来说,应该不是的。在某一天你的系统中很有可能会有两个人的姓和名都相同的人存在,而且无论再添加多少 Person 属性比如生日、最喜欢的颜色等等,始终迟早会发生相等的情况。或者可以这么说,如果你的系统是用来处理汽车的,每个汽车会有一个 "model" 的引用,那么如果两辆车拥有同样的黑色 Tesla model S,它们就是相等的,即使它们的对象在内存中位置不同它们也是拥有相同的 model。这个例子就是相等性发挥功用的时候。
import java.util.Objects;
import static java.util.Objects.requireNonNull;
public final class Car {
private final String color;
private final Model model;
public Car(String color, Model model) {
this.color = requireNonNull(color);
this.model = requireNonNull(model);
}
public Model getModel() {
return model;
}
public static final class Model {
private final String name;
private final String version;
public Model(String name, String version) {
this.name = requireNonNull(name);
this.version = requireNonNull(version);
}
@Override
public int hashCode() {
int hash = 5;
hash = 23 * hash + Objects.hashCode(this.name);
hash = 23 * hash + Objects.hashCode(this.version);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final Model other = (Model) obj;
if (!Objects.equals(this.name, other.name)) {
return false;
} else return Objects.equals(this.version, other.version);
}
}
}
也只有当两辆车在内存中拥有同一个地址的时候才会被认为是一致的,也即同一辆车。也就是它们的 model 被认为是相同的只需要有难过有同样的名字和版本即可。下边是一个例子:
final Car a=new Car("black",new Car.Model("Tesla","Model S"));
final Car b=new Car("black",new Car.Model("Tesla","Model S"));
System.out.println("Is a and b the same car? "+a.equals(b));
System.out.println("Is a and b the same model? "+a.getModel().equals(b.getModel()));
// Prints the following:
// Is a and b the same car? false
// Is a and b the same model? true
一致性
不过偏向选择相等性而非一致性有一个风险就是在堆中创建了许多实际不必要的实例。只要看上边车的例子就知道了。对于每辆我们所创建的车在内存中都会有个对应的 model,即使 java 通常会优化 string 存储来阻止重复的发生,但是那些总是相同的对象仍然会存在一定程度的浪费。这里有个比较不错的小技巧,通过将内部类转变成能够使用一致性来判断而且又能够避免不必要的对象内存占用,enum 在此就发挥了用武之地:
public final class Car {
private final String color;
private final Model model;
public Car(String color, Model model) {
this.color = requireNonNull(color);
this.model = requireNonNull(model);
}
public Model getModel() {
return model;
}
public enum Model {
TESLA_MODEL_S("Tesla", "Model S"),
VOLVO_V70("Volvo", "V70");
private final String name;
private final String version;
Model(String name, String version) {
this.name = name;
this.version = version;
}
}
}
现在我们就能够确保每个 model 在内存中只会占用一个地址因此能够安全地使用一致性判定。不过这样也带来了一个问题,这个方式限制了代码的扩展性。之前我们在不更改 Car.java 原文件的情况下就能够随意创建新的 models,但是现在却把我们限制在了 enum 里边而它通常都是保持不变的。不过如果这些条件都可能出现的话,那么相等性比较可能就更适合你了。
最后需要注意,如果你的类已经实现了 equals() 和 hashCode() 方法而后又需要基于一致性来存入 Map 中的话,推荐使用IdentityHashMap存储结构。它是通过内存地址来引用键集合的,无论是否有覆盖过 equals() 或 hashCode() 。