EffectiveJava——第三章 对于所有对象都通用的方法

2021/6/19 17:26:59

本文主要是介绍EffectiveJava——第三章 对于所有对象都通用的方法,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

对于所有对象都通用的方法

Object类中有很多通用方法,比如equalstoStringhashCode,还有实现了Comparable的类,它们的方法都有明确的约定,如果你想你的类能与其他类良好的工作在一起,请遵守这些约定。

覆盖equals方法

其实很多时候equals方法根本不需要被覆盖:

  • 当类的每个实例都是唯一的 很多时候我们设计的类就是这样的,只有相同的实例才相等,而不是依赖某个属性来判等,比如Thread类。
  • 类没有必要提供逻辑相等的测试功能 比如Pattern类,它没有实现equals,确实,仔细想想,判断两个正则表达式是否相等的需求真的没啥用。
  • 超类的equals实现正适用于本类 Java的集合类中的equals方法基本都是继承自祖先的。
  • 可以确保类的equals永远不会被调用 比如类是包级私有的,静态的等等。

只有当我们自己设计一个“值类”的时候,才需要实现equals方法。

equals方法规范

  • 自反性 对于任何非null的引用值xx.equals(x)==true
  • 对称性 对于任何非null的引用值x,yx.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的引用值xx.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,这已经违反了对称性。

原因不难看出,CaseInsensitiveStringequals方法第二行做了一个画蛇添足的操作,如果你传入一个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类型比较的时候,忽略了颜色信息。

这个问题似乎无法解决,如果你想让PointPoint的子类能够判等的话,那就永远无法绕过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;
}
  1. 判断this和传入类的引用是否一致,这对于大对象的比较将节省很多时间
  2. 判断是否是同类型
  3. 转换类型
  4. 将所有重要的属性比较

这也是很多IDE自带的生成工具的写法。

好习惯

  1. 覆盖equals时尽量覆盖hashCode
  2. 不要企图让equals过于智能,往往是负优化
  3. 不要将equals的参数改为其他类型,这样做是重载(Overload)不是重写(Override)。
  4. 尽可能使用ide自带的equals实现
  5. 如非必要请勿轻易覆盖equals

...未完



这篇关于EffectiveJava——第三章 对于所有对象都通用的方法的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程