Effective Java:对于所有对象都通用的方法

前言:

读这本书第1条规则的时候就感觉到这是一本很好的书,可以把我们的Java功底提升一个档次,我还是比较推荐的。这里我主要就关于覆盖equals、hashCode和toString方法来做一个笔记总结,希望能够与君共勉。

概述:

这一章主要是说明一些对于所有对象都通用的方法。我们知道Java的多态是其特色之一,而多态的体现方式中就有一种方式叫做“重写”。这些概念性的东西我想在大学我们学习Java的初期,老师就会如数家珍一样地灌输给我们,不过,在那个时候有多少人真的了解了什么是重载,什么是重写,什么是多态呢?

而对于现在的一些开发者而言,了解并使用它们是家常便饭,理所应当。但是,你真的是已经够了解吗?

相关内容:

1.覆盖equals时请遵守通用约定

我们知道Java中如果需要比较两个对象是否相等的时候,就会用到equals。对于初学者,可能遇到更多的是equals与"=="的区别,可能一开始大家都是一头雾水,傻傻分不清楚,这可能是因为你还没有地址和值的概念。关于equals与"=="的区别,大家可以看看这篇博客——Java中equals和==的区别

如果你还不是很清楚equals和"=="的区别,那么,你可以花几分钟看看上面的博客,以便你可以明白,我们为什么要覆盖equals方法。如果你已全然了解,那么便没有什么东西可以阻止你继续往下看。

我们知道equals要实现的是逻辑上的等。站在数学的角度来看,两个事物相等的条件,有如下几个:

1.自反性:对于任何非null的引用值x,x.equals(x)必须返回true.

2.对称性:对于非空的引用值x,y,当且仅当x.equals(y)返回true时,y.equals(x)必须返回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.

看完上面的这些数学式的规则,你是不是有一种哪要这么麻烦的事的感觉呢?从直观上来说,上面的这些规则的确是有一些麻烦,但你却不能忽视它们,不然麻烦的可就是你了。

下面我会通过一些实例的学习,来说明这些规则。

1.自反性:

<span style="font-family:Courier New;font-size:18px;">public static void equalsOppositeSelf() {
        String s = "ABC";
        Object o = new Object();

        System.out.println(s.equals(s));
       </span>

结果:

<span style="font-family:Courier New;font-size:18px;">true
true</span>

2.对称性:

对于对称性,可能你会感觉理所当然。这是因为在你看来,我们要比较的两者必定是同一类型,这个必定太过理想化了,如果我们比较的两个对象不是同一种类型呢?下面可以看看这个例子。

<span style="font-family:Courier New;font-size:18px;">public final class CaseInsensitiveString {

    private final String s;

    public CaseInsensitiveString(String s) {
        if (s == null) {
            throw new NullPointerException();
        }

        this.s = s;
    }

    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
        }

        if (o instanceof String) {
            return s.equalsIgnoreCase((String)o);
        }

        return false;
    }
}</span>

上面equals方法的代码实现了忽略大小写来比较字符串。我们先不考虑同类型的两个对象比较,对于不同类型的两个对象,从上面的代码中我们可以看出,如果被比较的对象是一个String类型的,那么我们就可以去忽视大小写进行比较,答案也是在情理之中。下面看看例证:

比较方法:

<span style="font-family:Courier New;font-size:18px;">public static void equalsSymmetric() {
        CaseInsensitiveString s1 = new CaseInsensitiveString("abc");
        String s2 = "abc";

        System.out.println("s1 == s2 ? " + s1.equals(s2));
        System.out.println("s2 == s1 ? " + s2.equals(s1));
    }</span>

比较结果:

<span style="font-family:Courier New;font-size:18px;">s1 == s2 ? true
s2 == s1 ? false</span>

这是为什么?不是说equals要满足对称性的吗?怎么这里又行不通了呢?

仔细推敲一番就可以发现了,我们在进行s1.equals(s2)的时候,是因为s1是CaseInsensitiveString类型的,它会执行到上面的代码,而s2是String类型的,s2.equals(s1)的比较自然是String中的equals方法。

那你又会问,既然这样我们总不能去修改String类中的代码吧。如果你这样想,那我就无言以对了。我们知道一件事,两个不同类型的对象我就让它不相同去吧。也就是说,我们要有一个判断告诉程序,如果被比较的对象不是CaseInsensitiveString类型,那我们就不用客气直接返回false就行了。修改后的代码如下:

<span style="font-family:Courier New;font-size:18px;">    public boolean equals(Object o) {
        return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
    }</span>

3.传递性

传递性的判断是x = y, y = z,那么就可以判断x = z了。

现在假设我们有一个类Point和一个Point的子类ColorPoint分别如下:

Point

<span style="font-family:Courier New;font-size:18px;">public class Point {

    private final int x;

    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Point)) {
            return false;
        }

        Point p = (Point) o;

        return p.x == x && p.y == y;
    }
}</span>

ColorPoint

<span style="font-family:Courier New;font-size:18px;">public class ColorPoint extends Point {

    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
}</span>

可以看到ColorPoint继承于Point,不过比Point类多一个颜色属性。当我们把ColorPoint与Point和Point与ColorPoint进行比较,如下:

<span style="font-family:Courier New;font-size:18px;">public static void equalsTransitivity() {
        Point p1 = new Point(1, 2);
        ColorPoint cp1 = new ColorPoint(1, 2, Color.BLACK);

        System.out.println("p1 == cp1 ? " + p1.equals(cp1));
        System.out.println("cp1 == p1 ? " + cp1.equals(p1));
    }</span>

会得到如下结果:

<span style="font-family:Courier New;font-size:18px;">p1 == cp1 ? true
cp1 == p1 ? true</span>

为什么两个都true呢?明明两个不同类型啊,如果真的要去考虑父类与子类的关系,也应该是一个true一个false啊。因为这里我们的ColorPoint本身没有重写Point的equals,它使用的是Point的equals,这时无论哪一次的比较中,都是去比较x和y,与color无关。

这样就会导致一个问题,如果我的两个比较对象都是ColorPoint呢?这样一来如果我的两个ColorPoint的x和y全都一样,只是color不同,那么无论怎么比较,其结果值都会是true.这里不会去检查color。那你可能就会说,那我们就重写ColorPoint的equals啊。

这里我们使用一条建议:复合优于继承(这一点在设计模式中也有体现)。

实例示范:

<span style="font-family:Courier New;font-size:18px;">public static void equalsTransitivity() {
        Point p1 = new Point(1, 2);
        ColorPoint cp1 = new ColorPoint(1, 2, Color.BLACK);
        ColorPoint cp2 = new ColorPoint(1, 2, Color.BLUE);

        ColorPointNew cpn1 = new ColorPointNew(1, 2, Color.BLACK);
        ColorPointNew cpn2 = new ColorPointNew(1, 2, Color.BLUE);

        System.out.println("p1 == cp1 ? " + p1.equals(cp1));
        System.out.println("cp1 == p1 ? " + cp1.equals(p1));
        System.out.println("cp1 == cp2 ? " + cp1.equals(cp2));

        System.out.println("cpn1 == cpn2 ? " + cpn1.equals(cpn2));
        System.out.println("cpn1 == cp1 ? " + cpn1.equals(cp1));
        System.out.println("cp1 == cpn1 ? " + cp1.equals(cpn1));
    }</span>

结果:

<span style="font-family:Courier New;font-size:18px;">p1 == cp1 ? true
cp1 == p1 ? true
cp1 == cp2 ? true
cpn1 == cpn2 ? false
cpn1 == cp1 ? false
cp1 == cpn1 ? false</span>

上面的代码看上去很简洁。

4.一致性

一致性的要求是,如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象被修改了。

2.覆盖equals时总要覆盖hashCode

为什么要说覆盖equals时总要覆盖hashCode呢?前面我们说的那些不都好好的么?一些equals必需的数学规则不是都已经满足了么?我们不是已经做得差不多了么?是的,的确是差不多了,不过我们还是要去覆盖hashCode方法。这是因为我们如果把我们的对象与HashMap之类的Hash值联系起来,有此时候可能会感到困惑,甚至大失所望。下面,我们就来列举一个例子,根据例子来说明再合适不过了。

我们有这样一个PhoneNumber类:

package com.java.effective.samples;

public final class PhoneNumber {

    private final short areaCode;

    private final short prefix;

    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999,  "area code");
        rangeCheck(prefix, 999,  "prefix");
        rangeCheck(lineNumber, 9999,  "line number");

        this.areaCode = (short)areaCode;
        this.prefix = (short)prefix;
        this.lineNumber = (short)lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || arg > max) {
            throw new IllegalArgumentException(name + ": " + arg);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof PhoneNumber)) {
            return false;
        }

        PhoneNumber pNumber = (PhoneNumber)o;

        return (pNumber.lineNumber == lineNumber) && (pNumber.prefix == prefix) && (pNumber.areaCode == areaCode);
    }
}

上面的代码对equals的处理完全OK,不过如果我们把PhoneNumber和HashMap放在一起使用,结果会如何?下面是我们的测试用例:

public static void hashCodePhoneNumber() {
        Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>();
        PhoneNumber phoneNumber = new PhoneNumber(707, 867, 9876);
        map.put(phoneNumber, "Jenny");

        System.out.println(map.get(new PhoneNumber(707, 867, 9876)));
        System.out.println(map.get(phoneNumber));
    }

结果:

null
Jenny

我们可以这样来理解上面的map.put()。如果我们不去覆盖hashCode,那么当我们使用map.put时,我们是把这些PhoneNumber对象放在各个不同的盒子里,而我们去map.get()的时候,只是去某一个盒子里去找(当然,如果map.get()和map.put()中的对象是同一个的话,当然可以找到)。

而如果我们覆盖了hashCode方法,这时,如果通过hashCode计算出来的值是相等的,就会放在同一个盒子里。这样,只要我们对象中保存的值是完全一致的,就会找到这个key所对应的value。不知道你发现没有,这个hashCode有点类似于分类,这样在数据量比较大的情况下就会大大提高效率。

我们可以通过以下两种方法来覆盖hashCode方法:

方法一:

@Override
    public int hashCode() {
        return 42;
    }

方法二:

@Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

首先两种方法都可以。通过上面的分析,从效率的角度来考虑,当然是第二种方法更为恰当。

所以在覆盖了equlas的同时,别忘了去覆盖hashCode.

3.始终要覆盖toString

承上,就拿PhoneNumber类来说,如果我们不去覆盖类的toString()方法,后果就是当我们需要去打印这个类的对象时,会有一些并非是我们想要的那种。类似这样的:[email protected]

有时我们不希望打印出这样的对象,那我们就要去覆盖它们的toString方法了。在这个方法里,我们可以按照我们自己的意愿来给类添加toString方法。对于PhoneNumber,我们可以这样来写:

@Override
    public String toString() {
        String result = "";
        result += (areaCode + "-");
        result += (prefix + "-");
        result += (lineNumber);

        return result;
    }

打印结果:

707-867-9876

总结:

在我们优化代码的时候不妨考虑一下去合理地覆盖这些方法,可以让我们的代码更加健壮。

版权声明:本文为博主原创文章,未经博主允许不得转载。http://blog.csdn.net/lemon_tree12138

时间: 2024-12-22 23:00:44

Effective Java:对于所有对象都通用的方法的相关文章

Effactive Java -- 对于所有对象都通用的方法

覆盖equb时请遵循通用约定: 自反性.对于任何非null的引用值x,xequals(x)必须返回true. 对称性.对于任何非null的引用值x和y,当且晋档y.equals(x)返回true的时候,x.equals(y)必须返回true. 传递性.对于任何非null的引用值x,y和z,如果x.equals(y),并且y.equals(z),则x.equals(z)为true. 一致性.对于非null的引用值x和y,只要equals的比较操作在对象所用的信息没有被修改,多次调用x.equals

Effective Java读书笔记(3对于所有对象都通用的方法)

3.1 覆盖equals时请遵守通用约定 什么时候应该覆盖Object.equals()方法呢? 如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法. Object.equals()方法具有自反性.对称性.传递性.一致性和与null比较返回false的特点. 实现高质量equals方法的诀窍: (1)使用==操作符检查"参数是否为这个对象的引用".如果是,则返回true,这

Effective java 第三章对于所有对象都通用的方法(一) 读书笔记

对于所有对象都通用的方法 覆盖equals时请遵守通用约定 类的每个实例本质上都是唯一的. 不关心类是否提供了逻辑相等的测试功能 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的. 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用. throw new AssertionError() 一般覆盖Object.equals都是值类 但有一种值类不需要覆盖equals方法,即实例受控,确保每个值至多只存在一个对象的类.如枚举 覆盖equals方法,通用约定. 自

[Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第八 ---- ?条

这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对对象进行相等的比较: public boolean equals(Object obj) { return (this == obj); } 那么什么时候需要覆盖equals呢? 当你的类有自己的逻辑相等,而不是对象相等时,应该自己实现equals,比如Date和Interger,他们的相等比较不仅

effective java-读书笔记-第三章 对于所有对象都通用的方法

个人博客同步发布:effective java-读书笔记-第三章 对于所有对象都通用的方法 第三章 对于所有对象都通用的方法 所有非final方法(equals.hashCode.toString.clone.finalize)都有明确的通用约定,因为它们被设计成是要被覆盖的,如果不遵守,基于散列的集合(HashMap.HashSet.HashTable)可能无法结合该类一起运作. 第8条 覆盖equals时请遵守通用约定 覆盖equals规范: 自反性(reflexive).对于任何非null

effective java读书笔记——对于所有对象都通用的方法

Java中的所有类都继承自Object类,Object类中有许多通用的方法,这一章要讨论的是:对于Object类中的通用方法,我们的类要不要继承,以及继承时需要注意的事项. 第1条:equals(),覆盖时请遵守通用约定 首先看一下不需要覆盖的情况: 1.类的每个实例本质上是唯一的.(比如Static的,单例的等等),这样不需要特意覆盖equals方法,用Object类的equals()方法就足够了 2.不关心类是否实现了“逻辑相等”的测试功能.我们用equals的目的就是判断两个对象是否是“逻

Effective Java读书笔记——第三章 对于所有对象都通用的方法

第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它所有的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定. 首先看看equals方法: 若满足下面的这些情况中的某一个,您可以直接使用Object类中的equals方法而不用覆盖: 类的每个实例本质上是唯一的.对于那些代表实例而不是值的类来说可以不用覆盖equals方法.比如Thread类.因为每一个Thread类的实例都表示一个线程,这与Thread某

Effective Java读书笔记——第三章 对于全部对象都通用的方法

第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它全部的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定. 首先看看equals方法: 若满足以下的这些情况中的某一个,您能够直接使用Object类中的equals方法而不用覆盖: 类的每个实例本质上是唯一的.对于那些代表实例而不是值的类来说能够不用覆盖equals方法.比方Thread类.由于每个Thread类的实例都表示一个线程,这与Thread某些

《Effective Java》第3章 对于所有对象都通用的方法

第8条:覆盖equals时请遵守通用约定 覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重.最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等. 那么,什么时候应该覆盖Object.equals呢?如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖 equals方法.这通常属于"值类(value class)"