第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;
}

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

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

    这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对 ...

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

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

  3. [Java读书笔记] Effective Java(Third Edition) 第 3 章 对于所有对象都通用的方法

    第 10 条:覆盖equals时请遵守通用约定 在不覆盖equals方法下,类的每个实例都只与它自身相等. 类的每个实例本质上都是唯一的. 类不需要提供一个”逻辑相等(logical equality ...

  4. [Effective Java]第三章 对所有对象都通用的方法

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

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

    第二十三条 请不要再新代码中使用原生态类型 1 使用原生态类型,就失去了泛型在安全性和表述性方面的所有优势,所以新代码中不要使用原生态类型 2 List<String>可以传递给List作 ...

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

    第二十条 用函数对象表示策略 函数指针(JAVA的函数指针,是指使用对象的引用来作为参数,传递给另一个对象的方法)主要用来实现策略模式,为了在JAVA中实现这种模式,要申明一个接口来表示该策略,并为每 ...

  7. [Effective Java 读书笔记] 第三章类和接口 第十三 -- 十四条

    第十三条 使类和成员的可访问性最小化 总得来说,我们应该尽量将成员的访问范围限制到最小!有利于解耦,开发.测试和优化都能够更加独立. 对于成员(域,方法,嵌套类和嵌套接口),有四种可能的访问级别,访问 ...

  8. [Effective Java 读书笔记] 第三章类和接口 第十八--十九条

    十八条 接口优于抽象类 接口的特点: 1.一个类可以实现多个接口,不能继承多个类(抽象类) 2.接口不能有具体的方法实现,只定义标准类型 骨架类: 即实现一个abstract类来实现接口,提供给其他类 ...

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

    第十六条 复合优先于继承 如果不确定B和A的关系是,is-a的关系,B确实也是A,那么久不应该使用B继承A,否则会暴露实现细节, 你的实现都会限制在原始的实现上. 书中举的第一个例子,实现了一个类ex ...

随机推荐

  1. 在Sql中将 varchar 值 '1,2,3,4,5,6' 转换成数据类型 int

    --问题:将aa转换为Int类型失败 string aa="3,5,11,56,88,45,23"; select * from ERPBuMen where ID in(aa) ...

  2. JavaFx EventHandler

    import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHan ...

  3. InstallShield详细制作说明(一)

    虽然网上关于InstallShield的制作说明已经很多,但是看的时候还是会有些晕乎乎的,不得不说很复杂.前段时候做了一次,后面需要升级,在重新做的时候发现有些地方自己又忘了,所以有必须将自己看的教程 ...

  4. ArcGIS Engine 线段绘制

    转自ArcGIS Engine 线段绘制研究 基本步骤 构建形状 1. 创建 IPoint IPoint m_Point = new PointClass(); m_Point.PutCoords(x ...

  5. Accelerated C++:通过演示样例进行编程实践——练习解答(第9章)

    我的Github地址:https://github.com/lanbeilyj/Accerlerated-C-plus-plus 9-0. Compile, execute, and test the ...

  6. cx_Oracle

    cx_Oracle 安装 pip install cx_Oracle 只是我没用那个安装成功过.我找了rpm 包. http://nchc.dl.sourceforge.net/project/cx- ...

  7. Impala基础认知与安装

    一.Impala简介 Cloudera Impala对你存储在Apache Hadoop在HDFS,HBase的数据提供直接查询互动的SQL.除了像Hive使用相同的统一存储平台,Impala也使用相 ...

  8. 图像处理 Mine

    1)中值滤波:排序取中间值.作用:去噪点 1.1)均值滤波; 1.2)高斯模糊:执行高斯模糊,然后改混合模式,改成叠加.柔光或者深色.就能得到平滑而不模糊的效果. 2)腐蚀.膨胀:开运算(腐蚀后膨胀) ...

  9. 忍者无敌-实例解说Cocos2d-x瓦片地图

    实例比較简单,如图所看到的,地图上有一个忍者精灵,玩家点击他周围的上.下.左.右,他能够向这个方向行走. 当他遇到障碍物后是无法穿越的,障碍物是除了草地以为部分,包含了:树.山.河流等. 忍者实例地图 ...

  10. Scala具体解释---------数组、元组、映射

    一.数组 1.定长数组 声明数组的两种形式: 声明指定长度的数组 val 数组名= new Array[类型](数组长度) 提供数组初始值的数组,无需newkeyword Scala声明数组时.须要带 ...