Effective Java笔记第二章对所有对象都通用的方法
第一节覆盖equals时请遵守通用约定
如果对于不同类型的常,变量在JVM中的存储位置或者equals与==的区别不太清楚的话,强烈建议先读或者遇到不懂的情况下读一下这篇文章不同类型的常,变量在JVM中的存储位置和equals与==的区别,相信一定会对你有一些帮助。
1.最容易避免覆盖equals方法错误的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只会与它自身相等,满足以下任何一个条件都不需要覆盖equals方法:
1)类的每个实例本质上都是唯一的:对于代表活动实体而不是值的类来说就是这样。
2)不关心类是否提供了"逻辑相等"的测试功能。
3)超类(被继承的类)已经覆盖了equals,从超类继承过来的行为对于子类也是适合的。
4)类是私有的或者包级私有的(未指定访问级别,没有被private,protected或public修饰的类,接口,枚举,字段以及方法等),访问的级别为(public > 包级私有 > protected > private),可以确定他的equals方法永远不会被调用。
2.如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时候我们就需要覆盖equals方法,这通常属于"值类"(仅仅是一个表示值的类,如Integer或Date等)的情形。程序员在利用equals方法来比较值对象的引用时,希望知道他们在逻辑上是否相等,而不是想了解他们是否指向同一个对象。
有一种"值类"不需要覆盖equals方法,即实例受控确保"每个值至多只存在一个对象"的类。枚举类型就属于这种类,对于这种类而言,逻辑相同和对象等同是一回事。
3.覆盖equals方法须遵守的规范,equals方法实现了等价关系:
1)自反性:对于任何非null的引用值x,x.equals(x)必须返回true。
2)对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
3)传递性:对于任何非null的引用值x,y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
4)一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
5)对于任何非null的引用值x,x.equals(null)必须返回false。
4.逐一详细叙述覆盖equals方法所需遵守的规范。
1)自反性:对象必须等于其自身,这一点很难无意识地违反。假如违背了这一条,可以把该类的实例添加到集合中,该集合的contains方法会果断的告诉你,该集合不包含你刚刚添加的实例。
2)对称性:关于两个对象的"他们是否相等"的问题必须保持一致性,无意识的违反这一条不难想象,例如以下这个例子:
//对称性
public final class DemoSymmetry {
private final String s;
public DemoSymmetry(String s) {
if (null == s) {
throw new NullPointerException();
}
this.s = s;
}
/**
* 违反对称(当且仅当x.equals(y)返回true时,y.equals(x)必须也返回true)
* 如果这样写的话调用DemoSymmetry对象的equals方法时,传入的是字符串的话会返回true,但是反过来
* 字符串.equalsDemoSymmetry对象的时候,调用的是String类的方法,返回false,违反对称性。
* @param o
* @return
*/
// @Override
// public boolean equals(Object o) {
// if (o instanceof DemoSymmetry) {
// return s.equalsIgnoreCase(((DemoSymmetry) o).s);
// }
// //单向的互操作性
// //instanceof是java中的二元运算符,左边是对象,右边是类。作用为判断左边的对象是否是右边的类的实例,但是要注意,左边的对象实例不能是基础数据类型。
// //类的实例包含本身的实例以及所有直接或间接子类的实例
// if (o instanceof String) {
// //equalsIgnoreCase忽略大小写,equals不忽略大小写
// return s.equalsIgnoreCase((String) o);
// }
// return false;
// }
//修改后的写法
@Override
public boolean equals(Object o) {
//只能这样写,会先判断左侧的o不是DemoSymmetry的实例,所以不是直接false,跳过右边的判断。
//判断顺序不能颠倒,不然o不是DemoSymmetry的实例的话,会爆类型转换错误,
return o instanceof DemoSymmetry && ((DemoSymmetry) o).s.equalsIgnoreCase(s);
}
public static void main(String[] args) {
//demo是一个类的实例
DemoSymmetry demo = new DemoSymmetry("Polish");
//s是一个字符串
String s = "polish";
//实例和字符串的值不对等
//这里是调用DemoSymmetry类中的equals方法进行判断
boolean equals = demo.equals(s);
System.out.println(equals);
//这里是调用String类的equals方法进行判断
boolean equals1 = s.equals(equals);
System.out.println(equals1);
}
}
记住一旦违反了equals约定,当其他对象面临你的对象时,你完全不知道这些对象的行为会怎么样。
3)传递性:关于第一个对象等于第二个对象,第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。无意识的违反这条也不难想象,例如以下这个例子:
超类(被继承的类)
//传递性
public class DemoTransitivity {
private final int x;
private final int y;
public DemoTransitivity(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DemoTransitivity)) {
return false;
}
DemoTransitivity demo = (DemoTransitivity) o;
return demo.x == x && demo.y == y;
}
}
子类
//子类
public class DemoTransitivitySon extends DemoTransitivity {
private final Color color;
public DemoTransitivitySon(int x, int y, Color color) {
super(x, y);
this.color = color;
}
/**
* 违反对称性
* 父类equals子类的时候会忽略颜色,所以是true,
* 但是子类equals父类的时候,因为颜色参数没有,所以总是false
*
* @param o
* @return
*/
// @Override
// public boolean equals(Object o) {
// if (!(o instanceof DemoTransitivity)) {
// return false;
// }
// return super.equals(o) && ((DemoTransitivitySon) o).color == color;
// }
/**
* 违反传递性
* 父类equals子类的时候会忽略颜色,所以是true,
* 子类equals父类的时候,调用return o.equals(this),就等于o(父类)equals(this)(子类),不区分颜色,所以还是true
* 但是子类equals子类的时候,颜色不同就会是是false
*
* @param
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof DemoTransitivity)) {
return false;
}
if (!(o instanceof DemoTransitivitySon)) {
return o.equals(this);
}
return super.equals(o) && ((DemoTransitivitySon) o).color == color;
}
public static void main(String[] args) {
//测试对称性
// DemoTransitivitySon ds = new DemoTransitivitySon(1, 2, Color.red);
// DemoTransitivity df = new DemoTransitivity(1, 2);
// System.out.println(df.equals(ds));//true
// System.out.println(ds.equals(df));//false
//测试传递性
DemoTransitivitySon demo1 = new DemoTransitivitySon(1, 2, Color.red);
DemoTransitivity demo2 = new DemoTransitivity(1, 2);
DemoTransitivitySon demo3 = new DemoTransitivitySon(1, 2, Color.blue);
System.out.println(demo1.equals(demo2));//true
System.out.println(demo2.equals(demo3));//true
System.out.println(demo1.equals(demo3));//false
}
这是面向对象语言中关于等价关系的一个基本问题:我们无法在扩展可实例化的类的同时,既增加新的值组件(属性),同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。
你可能听说,在equals方法中用getClass测试代替instanceof测试,可以扩展可实例化的类和增加新的值组件,同时保留equals约定:
@Override
public boolean equals(Object o) {
//getClass返回调用该方法的对象的类
if (null == o || o.getClass() != getClass()) {
return false;
}
DemoTransitivity demo = (DemoTransitivity) o;
return demo.x == x && demo.y == y;
}
注意,这段程序只有当对象具有相同的实现时,才能使对象等同。这样虽然不算太糟糕,但是结果却是无法接受的。
假设我们要编写一个方法,用来检测某个整值点是否在单位圆中,采用以下的方法:
public class DemoTransitivityTest {
private static final Set<DemoTransitivity> unitCircle;
static {
unitCircle = new HashSet<>();
unitCircle.add(new DemoTransitivity(1, 0));
unitCircle.add(new DemoTransitivity(0, 1));
unitCircle.add(new DemoTransitivity(-1, 0));
unitCircle.add(new DemoTransitivity(0, -1));
}
public static boolean onUnitCircle(DemoTransitivity dt) {
return unitCircle.contains(dt);
}
}
假设你通过某种不添加值组件的方式扩展了DemoTransitivity方法,比如说让他的构造器记录创建了多少个实例:
public class DemoTransitivityCounter extends DemoTransitivity {
private static final AtomicInteger counter = new AtomicInteger();
public DemoTransitivityCounter(int x, int y) {
super(x, y);
counter.incrementAndGet();
}
public int numberCreated() {
return counter.get();
}
}
首先根据里氏替换原则,一个类型的任何重要属性也将适用于他的子类型,因此为该类型编写的任何方法,在他的子类型上也应该同样运行的很好。但是假设我们把DemoTransitivityCounter类的实例传给了DemoTransitivityTest类的onUnitCircle方法,如果DemoTransitivity类使用了基于getClass的equals方法时,该方法只有当对象具有相同的实现时,才能使对象等同。无论DemoTransitivityCounter实例的x和y传入什么值,onUnitCircle方法都会返回false,因为onUnitCircle方法所用的是HashSet集合,利用equals方法检验包含条件,没有任何DemoTransitivityCounter实例与任何DemoTransitivity对应。但是如果DemoTransitivity中使用适当的基于instanceof的equals方法时,当遇到
DemoTransitivityCounter时,相同的onUnitCircle方法就会工作的很好。
虽然没有一种令人满意的方法可以既扩展不可实例化类,有增加值组件,但是有一种不错的权宜之计:复合优先于继承,我们不再让DemoTransitivitySon扩展DemoTransitivity,而是在DemoTransitivitySon中加入一个私有的DemoTransitivity域,以及一个共有的视图方法,此方法返回一个与该有色点处在相同位置的普通DemoTransitivity对象。如下:
public class DemoTransitivityModified {
//加入私有的DemoTransitivity域
private final DemoTransitivity dt;
private final Color color;
public DemoTransitivityModified(int x, int y, Color color) {
if (null == color) {
throw new NullPointerException();
}
dt = new DemoTransitivity(x, y);
this.color = color;
}
/**
* 创建公有视图
* @return
*/
public DemoTransitivity returnDemoTransitivity() {
return dt;
}
/**
* 这样写相当于只有对象是DemoTransitivityModified类的实例化时才有可能使用equals判断相同
* dtm.dt.equals(dt)相当于调用DemoTransitivity的equals方法进行判断,
* dtm.color.equals(color)相当于调用Color的equals方法进行判断。
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof DemoTransitivityModified)) {
return false;
}
DemoTransitivityModified dtm = (DemoTransitivityModified) o;
return dtm.dt.equals(dt) && dtm.color.equals(color);
}
}
注意,你可以在一个抽象类的子类中增加新的值组件,而不会违反equals约定。对于用类层次代替标签类而得到的那种类层次结构来说,这一点很重要。
4)一致性:如果两个对象相等,他们必须始终保持相等,除非他们中有一个(或者两个都)被修改了。可变对象在不同的时候可以与不同的对象相等,而对于不可变对象来说相等的对象永远相等,不相等的对象永远不相等。
无论类是否是不可变的,都不要使equals方法依赖于不可靠的资源。
5)非空性:所有的对象都必须不等于null。这个一般没有必要,因为为了测试参数的等同性,equals方法必须先把参数转换成合适的类型,以便可以调用他的访问方法,或者访问他的域,在进行转换之前,equals方法必须使用instanceof操作符,对参数进行判断,比如:
@Override
public boolean equals(Object o) {
if (!(o instanceof DemoTransitivity)) {
return false;
}
DemoTransitivity demo = (DemoTransitivity) o;
return demo.x == x && demo.y == y;
}
5.实现高质量equals方法的诀窍。
1)使用"==“操作符检查"参数是否为这个对象的引用”。如果是,则返回true。这是一种性能优化,如果操作比较昂贵,就值得这么做。
2)使用instanceof操作符检查"参数是否为正确的类型",如果不是则返回false。
3)把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
4)对于该类中的每个"关键域",检查参数中的域(域是一种属性,可以是一个类变量,一个对象变量,一个对象方法变量或者是一个函数的参数)是否与该对象中对应的域匹配。
对于既不是float也不是double类型的基本类型域,可以使用" = ="操作符进行比较,对于对象引用域,可以递归地调用equals方法,对于float域,可以使用Float.compare方法,对于double域,则使用Double.compare。
域的比较顺序可能会影响到equals方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者开销最低的域,最理想的情况是两个条件同时满足的域。
5)当你编写完成了equals方法之后,应该问自己三个问题:他们是否是对称的,传递的,一致的?
6.告诫:
1)覆盖equals时总要覆盖hashCode。
2)不要企图让equals方法过于智能。想要多度的寻求各种等价关系,很容易就陷入麻烦之中。
3)不要将equals声明中的Object对象替换为其他类型。