EffectiveJava——第三章 对于所有对象都通用的方法
对于所有对象都通用的方法
Object类中有很多通用方法,比如equals
、toString
、hashCode
,还有实现了Comparable
的类,它们的方法都有明确的约定,如果你想你的类能与其他类良好的工作在一起,请遵守这些约定。
覆盖equals方法
其实很多时候equals
方法根本不需要被覆盖:
- 当类的每个实例都是唯一的 很多时候我们设计的类就是这样的,只有相同的实例才相等,而不是依赖某个属性来判等,比如Thread类。
- 类没有必要提供逻辑相等的测试功能 比如Pattern类,它没有实现equals,确实,仔细想想,判断两个正则表达式是否相等的需求真的没啥用。
- 超类的equals实现正适用于本类 Java的集合类中的equals方法基本都是继承自祖先的。
- 可以确保类的equals永远不会被调用 比如类是包级私有的,静态的等等。
只有当我们自己设计一个“值类”的时候,才需要实现equals方法。
equals方法规范
- 自反性 对于任何非null的引用值
x
,x.equals(x)==true
- 对称性 对于任何非null的引用值
x,y
,x.equals(y) == y.equals(x)
- 传递性 对于任何非null的引用值
x,y,z
,如果x.equals(y)==y.equals(z)==a
那么x.equals(z)==a
a是一个布尔值 - 一致性 对于任何非null的引用值
x,y
,只要多次调用过程中,equals使用的到的属性没有改变,那么多次调用的结果也不应该改变 - 对于任何一个非null的引用值
x
,x.equals(null)==false
这些规范看着有点让人感觉像是回到了数学课上,但是不遵循这些规范会带来一些潜在的后果。
违反自反性
对于自反性,如果一个类不能在equals中遵循自反性,那么Set的contains
方法就可能没法返回正常的值。集合中很可能包含很多完全相同的实例。
违反对称性
对于违反对称性,看下面的一个例子
class CaseInsensitiveString{
private final String s;
public CaseInsensitiveString(String s){
this.s = s;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
return false;
}
}
public class EqualsTest {
public static void main(String[] args) {
String string = "HelloWorld";
CaseInsensitiveString ciString = new CaseInsensitiveString("helloworld");
System.out.println(ciString.equals(string));
System.out.println(string.equals(ciString));
}
}
CaseInsensitiveString
使用委托实现了一个对大小写不敏感的字符串类。如果你运行这段程序,你会发现,主函数中的第一条输出语句是true
,第二条是false
,这已经违反了对称性。
原因不难看出,CaseInsensitiveString
的equals
方法第二行做了一个画蛇添足的操作,如果你传入一个String
对象,它仍然会按照忽略大小写的模式进行对比,但如果你用String
的实例去和CaseInsensitiveString
对比,显然,String
肯定不知道它是个什么牛马,直接返回false。看似一个聪明的,使该类支持原生String
的做法,却可能会酿成大祸。
CaseInsensitiveString
这个不明智的做法可能使他在不同的集合中产生不同的效果,例如如下的代码,它返回什么呢?
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(ciString);
System.out.println(list.contains(string));
完全取决于集合中contains
方法调用equals
的前后顺序。
解决问题很简单,别耍这种小聪明就行了。
违反传递性
违反传递性通常出现在子类和父类的比较中。
class Point{
private int x,y;
public Point(int x,int y){
this.x = x;this.y = y;
}
@Override public boolean equals(Object o){
if (!(o instanceof Point))
return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
}
class ColorPoint extends Point{
private int color;
public ColorPoint(int x, int y,int color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object o){
if (o instanceof ColorPoint)
return super.equals(o) && color == ((ColorPoint)o).color;
if (o instanceof Point)
return super.equals(o);
return false;
}
}
public class EqualsTest {
public static void main(String[] args) {
ColorPoint colorPoint1 = new ColorPoint(1,2,0xff0000);
Point point = new Point(1,2);
ColorPoint colorPoint2 = new ColorPoint(1,2,0xffffff);
System.out.println(colorPoint1.equals(point));
System.out.println(point.equals(colorPoint2));
System.out.println(colorPoint1.equals(colorPoint2));
}
}
这段代码违反了传递性,造成问题的原因是ColorPoint
在和Point
类型比较的时候,忽略了颜色信息。
这个问题似乎无法解决,如果你想让Point
和Point
的子类能够判等的话,那就永远无法绕过Point
没有子类新增加的属性的问题。
一个可选的办法就是不适用继承,而采取组合,并提供一个父类对象的视图,如何判断,全凭用户取舍:
class ColorPoint2 {
private final Point point;
private final int color;
public ColorPoint2(Point point,int color){
this.point = point;
this.color = color;
}
public Point asPoint(){
return point;
}
@Override
public boolean equals(Object o){
if (!(o instanceof ColorPoint))
return false;
ColorPoint2 c = (ColorPoint2) o;
return point.equals(c.point) && color == c.color;
}
}
在一个抽象类的子类中增加新的属性就不会出现这种问题。因为你无法创建这个抽象的父类。
违反一致性
java类库中URL类的实现就没遵循一致性,因为它比较时依赖了网络资源。
// URL.equals中调用了handler.equals进行判断两个URL是否相等
public boolean equals(Object obj) {
if (!(obj instanceof URL))
return false;
URL u2 = (URL)obj;
return handler.equals(this, u2);
}
// handler.equals 中调用了sameFile判断了是否是同一个文件
protected boolean equals(URL u1, URL u2) {
String ref1 = u1.getRef();
String ref2 = u2.getRef();
return (ref1 == ref2 || (ref1 != null && ref1.equals(ref2))) &&
sameFile(u1, u2);
}
// handler.sameFile 做了很多确认操作,我这里省略了,最后它使用hostEquals判断了两个URL的主机是否一致
protected boolean sameFile(URL u1, URL u2) {
// ...省略不重要代码
// Compare the hosts.
if (!hostsEqual(u1, u2))
return false;
return true;
}
// handler.hostEqual 中进行了一些网络操作,将URL转换成host地址
protected boolean hostsEqual(URL u1, URL u2) {
InetAddress a1 = getHostAddress(u1);
InetAddress a2 = getHostAddress(u2);
// if we have internet address for both, compare them
if (a1 != null && a2 != null) {
return a1.equals(a2);
// else, if both have host names, compare them
} else if (u1.getHost() != null && u2.getHost() != null)
return u1.getHost().equalsIgnoreCase(u2.getHost());
else
return u1.getHost() == null && u2.getHost() == null;
}
问题在于,随着时间,这个URL很可能被绑定到其它的主机上,原来的u1.equals(u2)
可能和之后的u1.equals(u2)
产生不同的结果。
所以equals
中不要依赖不确定不可靠的资源进行判断。
保证非空性
很多时候我们为了保证非空性会写这样的代码:
@Override public boolean equals(Object o){
if(o==null)return;
if(o instanceof Clz){...}
return false;
}
其实这个方法的第一行是没用的,因为instanceof已经会帮助你判空了。它在o为null的时候会返回false。
推荐的写法
@Override public boolean equals(Object o){
if (this == o)return true;
if (!(o instanceof Point))
return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
- 判断this和传入类的引用是否一致,这对于大对象的比较将节省很多时间
- 判断是否是同类型
- 转换类型
- 将所有重要的属性比较
这也是很多IDE自带的生成工具的写法。
好习惯
- 覆盖equals时尽量覆盖hashCode
- 不要企图让equals过于智能,往往是负优化
- 不要将equals的参数改为其他类型,这样做是重载(Overload)不是重写(Override)。
- 尽可能使用ide自带的equals实现
- 如非必要请勿轻易覆盖equals
覆盖equals时总覆盖hashCode
在每个覆盖了equals方法的类中,都必须覆盖hashCode方法。
因为hashCode方法有如下公用约定
- 只要两次调用对象的equals方法时,其中依赖的属性没有发生变化,hashCode应该返回相同的结果
- 对于两个对象a,b若
a.equals(b)==true
,那么他们应该有相同的hashCode - 如果两个对象equals方法的结果不相等,则hashCode不一定非要返回不同的结果,但不同的结果会提高该对象被用到散列表中时的性能
这个很好理解,就不过多阐述。实现hashCode一般的过程如下:
- 初始化result为第一个关键域(即equals依赖的属性)的散列码
- 对于余下每个关键域都计算他的散列码并与result进行一个线性运算f
- 将结果返回
举个例子:
@Override public int hashCode(){
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
上面的计算全部都是基本类型,对于一个域,如果它是基本类型,我们可以直接调用其包装类型中的hashCode
方法去计算哈希值,如果是引用类型,则递归调用其hashCode
,如是一个数组,则把每个元素作为一个单独的域来处理。null值可以用0代替,空数组可以用一个非零常数代替。
Objects.hashCode(Object...args)
可以传入任何的参数,然后返回一个hash码,你可以直接将全部关键域传入,只是这个方法比你自己实现的要慢一些,因为它包括一个可变长参数和数组之间的转换,基本类型的装箱和拆箱。
一些建议:
- 对于不可变类,缓存其hash码
- 不要试图从散列码计算中排除掉一个关键域来提高散列函数计算的性能,这样往往会在散列表查询时降低性能
- 不要对hashCode的返回值做规定,这样会降低灵活性
实现toString方法
toString方法应该被实现且显示为一个有用的,值得关注的信息。
谨慎的覆盖clone
考虑实现Comparable接口
...未完