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

第8条:覆盖equals时请遵守通用的约定

设计Object类的目的就是用来覆盖的,它所有的非final方法都是用来被覆盖的(equals、hashcode、clone、finalize)都有通用约定。

首先看看equals方法:

若满足下面的这些情况中的某一个,您可以直接使用Object类中的equals方法而不用覆盖:

  • 类的每个实例本质上是唯一的。对于那些代表实例而不是值的类来说可以不用覆盖equals方法。比如Thread类。因为每一个Thread类的实例都表示一个线程,这与Thread某些域的值没有关系(我们没有必要用equals判断Thread中两个实例的某个域相等而推断出Thread相等,这没有意义,因为每一个Thread实例都表示一个线程,它们都是唯一的)。
  • 不关心类是否提供了“逻辑相等”的测试功能。Random类覆盖了equals方法,以检查两个Random实例是否产生相同的随即序列。但这通常没有意义。
  • 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。如,大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承实现。
  • 类是私有的或是包级私有的,应该确定他的equals方法永不会被调用。这种情况下,equals方法应该被重写,以防意外被调用。
@Override
public boolean equals(Object o) {
    throw new AssertionError();
} 

那么合适应该重写equals方法呢?如果类具有自己特有的“逻辑相等”的概念(而不是对象的地址相等),而且这个类的超类并没有覆盖equals以实现期望的行为,这时应该覆盖equals方法,这通常属于“值类(value class)”的情形。 所谓的值类就是指类中仅有一个域的类。如包装类Integer,或者日期类Date。

当然还有一种类不用重写equals方法,即单例类。

重写equals方法的规范:

1、自反性:对于任意非null的引用x , 必有x.equals(x) == true.

2、对称性:

对于任何非null的引用值x和y,若x.equals(y) == true ,那么必有y.equals(x) == true。下面这个类重写了equals方法,但违反了对称性:

public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        if(s == null) {
            throw new NullPointerExecption();
        }
        this.s = s;
    }
    @Override
    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;
    }

}

在调用这个类的时候:

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

在调用cis.equals(s)时返回true,但是s.equals(cis)将返回false ,因为String类中的equals方法并不知道比较的是不区分大小写的字符串。这明显违反了自反性。

所以需要这么修改代码:

@Override
public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString && ((CaseInsensitiveString)o).s.equalsIsIgnore(s);
}


3、传递性:

如果第一个对象equals第二个对象,第二个对象equals第三个对象,那么第一个对象equals第三个对象:

public class Point {
    private final int x;
    private final int 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 p.x == x && p.y == y;
    }

}

下面实现了一个子类:

public class ColorPoint extends Point {
    private final Color color;
    public ColorPoint(int x,int y,Color color) {
        super(x,y);
        this.color = color;
    }

}

如果不重写equals方法,那么在比较时就忽略了颜色,这显然不可接受。

那么现在重写equals方法:

@Override
public boolean equals(Object o) {
    if(!(o instanceof ColorPoint)) {
        return false;

    }
    return super.equals(o) && ((ColorPoint)o).color == color;
}

但是这样重写有个问题,当我们实例化一个Point和一个ColorPoint时:

Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);

当调用p.equals(cp)时返回true,但是cp.equals(p)是返回false,原因是p并不是ColorPoint类型或是其子类型的。那么可修正这个问题,在ColorPoint.equals进行混合比较时忽略颜色信息:

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

    if(!(o instanceof ColorPoint))
        return ((Point)o).equals(this);

    return super.equals(o) && ((ColorPoint)o).color == color;
}

这种方式实现了对称性,却牺牲了传递性:

ColorPoint p1 = new ColorPoint(1,2,Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);

这种情况而来,p1.equals(p2) == true,且p2.equals(p3) == true,但是p1.equals(p3) == false,这违反了传递性。

如果这样写:

@Override
public boolean equals(Object o) {
    if(o == null || o.getClass() != getClass())
        return false;
    Point p = (Point)o;
    return p.x == x && p.y == y;
}

这牺牲了面向对象的优势,即动态绑定,这要求对象必须有相同实现。



要编写一个方法,用来判断整值点是否在单位圆中:

private static final Set<Point> unitCircle;

static {
    unitCircle = new HashSet<Point>();
    unitCircle.add(new Point(1,0));
    unitCircle.add(new Point(0,1));
    unitCircle.add(new Point(-1,0));
    unitCircle.add(new Point(0,-1));
}

public static boolean onUnitCircle(Point p) {
    return unitCircle.contains(p);
}

此时,如果扩展了一个新类:

public class CounterPoint extends Point {
    private static final AtomicInteger counter = new AtomicInteger();
    public CounterPoint(int x,int y) {
        super(x,y);
        counter.incrementAndGet();
    }
    public int numberCreated() {
        return counter.get();

    }

}

如果像上面一样,重写的equals方法中使用getClass()判断,那么无论如何将返回false,这违反了里氏替换原则。



解决办法是,用组合代替继承,即在ColorPoint类中加入一个私有的Point域,并增加一个方法用于返回该域:

public class ColorPoint {
    private final Point point;
    private final Color color;
    public ColorPoint(int x,int y,Color color) {
        if(color == null) {
            throw new NullPointerException();
        }
        point = new Point(x,y);
        this.color = color;
    }
    public Point asPoint() {
        return point;
    }
    @Override
    public boolean equals(Object o) {
        if(!(o instanceoc ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

4、一致性

相等的对象永远相等,不相等的永远不相等。即,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。

5、非空性:

所有对象都必须不为null。

@Override
public boolean equals(Object o) {
    if(o == null) {
        return false;

    }
} 

其实这一步是不需要的,直接用instanceof操作符就可以:

@Override
public boolean equals(Object o) {
    if(!(o instanceof MyType))
        return false;
    MyType mt  = (MyType)o;
}

如果o为null的话,那么方法直接返回false,如果o不是MyType类型(或其子类型的话),那么程序直接抛出ClassCastException异常。

根据上面的讨论,针对equals小结一下几点:

  • 使用==检查“参数是否为这个对象的引用”,若是,返回true。这是一种性能优化。若比较很昂贵,就值得这么做。
  • 使用instanceof操作符检查是否为正确的类型。
  • 把参数转换成正确的类型。由于之前使用了instanceof操作符,所以转换肯定可以成功。
  • 对于该类中的每个关键的域(significant),检查参数中的域是否与该对象中对应的域相匹配。——对于既不是float也不是double的基本类型域,可以使用==操作符,对于对象引用的域,可以递归调用equals方法,对于float域,可以使用Float.compare方法,对于double域,使用Double.compare方法。对于某些对象引用时null的域,可以用这样的比较方式(field == null ? o.field == null : field.equals(o.field));**
  • equals比较的顺序不同,效率可能不一样,所以应该先比较开销较低的域。
  • 覆盖equals是总要覆盖hashcode。(后面会讲)**
  • 不要将equals声明中的Object对象替换为其他类型。

第9条:覆盖equals时总要覆盖hashcode



对于equals和hashcode之间的关系,可以先参考这篇文章:

《Java中的equals和hashCode方法详解》



首先看看Object规范:

  • 如果两个对象根据equals(Object)方法比较是相等的。那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
  • 如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则有可能产生相同的结果。但不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。

考虑下面的类:

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,"lineNumber");
        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 pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
        //未重写hashCode方法
        ...

    } 

}


这时,若考虑:

Map<PhoneNumber,String> m = new HashMap<>();
m.put(new PhoneNumber(123,456,789),"Jenny");

如果期望调用:

m.get(new PhoneNumber(123,456,789));

返回的是“Jenny”的话,实际上无法做到,因为它返回的是null,因为这里有两个PhoneNumber实例,第一个被插入到Map的散列桶中,第二个用于获取该对象,但两个对象的散列码不同,因为hashCode默认返回的是对象的地址值,get方法会首先判断Map中是否有与目标对象的hashCode相同的对象,显然,这是两个对象,hashCode明显不同,于是返回的结果为false,也就找不到了。所以需要重写hashCode方法,好的重写方式是为不相等的对象产生不相等的散列码,为相等的对象产生相等的散列码,即如果两个对象equals为true,那么两个对象的hashCode必相等,如果两个对象equals为false,那么两个对象的hashCode不相等。

重写hashCode:

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

如果计算散列码的开销较大,可以考虑把hashCode值存储于对象内部,等需要计算的时候再计算,即懒加载的模式:

private volatile int hashCode;

...

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

第10条:始终要覆盖toString方法

Object的toString方法默认返回一个“类的名称@对象散列码的无符号十六进制数”,这看起来没什么意义,所以建议所有的子类都应该覆盖这个方法。

第11条:谨慎地覆盖clone方法

如需要克隆对象,需要实现Cloneable接口。

有关clone方法的详解,可以参考这篇文章:

详解Java中的clone方法 – 原型模式


第12条:考虑实现Comparable接口

Comparable接口中唯一方法是compareTo(),该方法允许简单的比较,而且允许执行顺序比较。如果某个类实现了Comparable接口,就表明它的实例具有内在的排序关系,对该对象组成的数组(或是List)进行排序只需调用:

Arrays.sort(a);

Comparable接口的原形:

public interface Comparable<T> {
    int compareTo(T t);

}

将这个对象与指定的对象进行比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负数、零、正整数。如果指定的对象的类型与本对象的类型不匹配,则抛出ClassCastException异常。

建议(x.compareTo(y) == 0) == (x.equals(y))

在使用Comparable接口进行对象之间的比较时,如果该类中有多个域,那么比较的时候应该按照从最重要的域开始比较,如果不相等则比较结束,返回;如果相等,在比较次要的域,以此类推:

public int compareTo(PhoneNumber pn) {
    int areaCodeDiff = areaCode - pn.areaCode;
    if(areaCode != 0)
        return areaCodeDiff;

    int prefixDiff = prefix - pn.prefix;
    if(prefixDiff != 0)
        return prefixDiff;

    return lineNumber - pn.lineNumber;
}
时间: 2024-10-20 02:20:57

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

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

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

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

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

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

第十条 始终覆盖toString() toString的实现可以使类使用起来更加舒适,在执行println等方法时打印出定制信息. 一单实现了自己的toString,指定输出的固定格式,在方法的文档说明中应该做好注释说明! 第十一条 谨慎覆盖clone

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

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

[Effective Java 读书笔记] 第三章类和接口 第二十-二十一条

第二十条 用函数对象表示策略 函数指针(JAVA的函数指针,是指使用对象的引用来作为参数,传递给另一个对象的方法)主要用来实现策略模式,为了在JAVA中实现这种模式,要申明一个接口来表示该策略,并为每个具体策略申明一个实现了该接口的类. 如果这个策略只被执行一次,使用匿名类,如果重复使用,则通常实现为私有的静态成员类,并通过共有的静态final域导出(最后一个例子),其类型为该策略接口. 第二十一条 优先考虑静态成员类 嵌套类主要有四种:静态成员类,非静态成员类,匿名类,局部类 静态成员类,一般

[Effective Java 读书笔记] 第三章类和接口 第十二条

第十二条 使类和成员的可访问性最小化 总得来说,我们应该尽量将成员的访问范围限制到最小!有利于解耦,开发.测试和优化都能够更加独立. 对于成员(域,方法,嵌套类和嵌套接口),有四种可能的访问级别,访问范围从小到大: 1. private,只有声明成员的类的内部才能访问 2. 包级私有的,声明成员的类所在的包内的任何类都可以访问,如果成员不显示声明访问级别,就是这种级别,所以也成为缺省访问级别 3.protected,声明成员的类及其子类可以访问,并且声明类所在包中的任何类也可以访问 4.publ

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

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

[Effective Java 读书笔记] 第三章类和接口 第十六条

第十六条 复合优先于继承 如果不确定B和A的关系是,is-a的关系,B确实也是A,那么久不应该使用B继承A,否则会暴露实现细节, 你的实现都会限制在原始的实现上. 书中举的第一个例子,实现了一个类extends HashSet类,因为缺少对HashSet类的addAll方法的理解(addAll会重复调用add方法),导致多统计了一倍的调用次数,这就是自己的实现限制在父类的实现上. 同时书中还有一个说明,如果超类在后续的版本中增加了一个新的方法, 正好和你的类里新增的方法签名一致(参数列表和函数名

[Effective Java 读书笔记] 第三章类和接口 第二十三-- ??条

第二十三条 请不要再新代码中使用原生态类型 1 使用原生态类型,就失去了泛型在安全性和表述性方面的所有优势,所以新代码中不要使用原生态类型 2 List<String>可以传递给List作为的参数,但是不能传递给List<Object>,因为泛型有子类型化的规则,List<String>是原生态类型List的一个子类型,而不是参数化类型List<Object>的子类型 3. 如果不确定类型,可以使用通配符类型 Set<?>,