天天看点

Effective Java笔记第二章对所有对象都通用的方法第一节覆盖equals时请遵守通用约定

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对象替换为其他类型。