《Effective Java》读书笔记 - 3.对于所有对象都通用的方法
Chapter 3 Methods Common to All Objects
Item 8: Obey the general contract when overriding equals
以下几种情况不需要你override这个类的equals方法:这个类的每一个实例天生就是unique的,比如Thread;这个类的父类已经override了equals,并已经足够;这是个private或者package的class,你确定equals永远不会被调用,保险起见这时候你其实可以override一下equals然后throw new AssertionError();。再比如Enum types也不需要override equals,因为每一种enum值都只有一个实例。
而当你决定override equals时,你需要遵守以下几点general contracts:
一.Reflexive(自反性)对任何非null的x,x.equals(x)必须返回true。
二.Symmetric(对称性)对任何非null的x,y,x.equals(y)当且仅当y.equals(x)。
举个反例吧:
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {...}
// Broken - violates symmetry!
@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;
}
@Override public int hashcode(){...}
}
那么如果:
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();
list.add(cis);
cis.equals(s)和 s.equals(cis)的结果是不一样的,因为String的equals并不是Case Insensitive的。list.contains(s)的返回结果也是不确定的,可能true可能false,取决于其具体的实现。问题就在于这个equals太贪心了,不应该把String类型的对象也包括在可以跟自己比较的东西的范围里,正确做法是:
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
三.Transitive(传递性)x,y,z均非null,如果x.equals(y)和y.equals(z)返回true,那么x.equals(z)必返回true。
举个反例:现在有个Point类,它包括x和y两个信息,equals方法是上述标准写法,然后有个ColorPoint类继承了Point,它包括三个信息,x,y和Color。由于Point类中的equals方法有这么一句:if (!(o instanceof Point)) return false;,因为instanceof会考虑多态,所以如果point.equals(colorPoint)是可以的,而且会忽略Color信息(这里的小写字母开头的变量都是对象实例)。那么我们为了满足对称性,必须让colorPoint.equals(point)也返回相同结果,并且还要考虑Color信息在内,那么我们只能想出以下逻辑:
在ColorPoint里面重写equals方法,写成这样的:如果传过来的是个Point那就只比较x,y;如果传过来的是ColorPoint,那就x,y和Color都要比较。这时候虽然解决了对称性问题,但是会造成其他问题:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1和p2相等,p2和p3也相等,但p1和p3不相等。首先说一下违背了这个contract有什么不好?书上没说,但是我个人认为不好就在于背了这个contract就等于违背了基本的数学认知。
事实上,如果你想继承一个instantiable的类(就是可实例化的类,至于为什么后面会说),并加一个value component(上述例子里面就是加了个Color),那么无论你怎么重写equals方法,都必然会违背某个contract,除非你用 o.getClass() != getClass()来代替instanceof,也就是说传进来的o必须和当前对象是完全相同的类型,子类也不行,比如你写了个SonPoint extends Point{},注意除了继承啥都没有,那么sonPoint和point依然会被认为是不相等的,但是用instanceof就没这个问题。那么到底咋办?看下面:
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {...}
/**
* Returns the point-view of this color point.
*/
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
@Override public int hashcode(){...}
}
没错,这就是所谓的“Favor composition over inheritance.”。
在Java类库里面也有刚才那种继承了一个instantiable的类还增加了一个value component的反面教材:java.sql.Timestamp继承了java.util.Date并加了个nanoseconds,反正别在同一个集合里面混着用这两个就行了,否则会出现诡异的事情。
现在讲一下所谓的instantiable的类:你可以继承一个abstract class兵并加一个value component,因为你根本无法实例化得到一个abstract class的对象,所以abstract class里面根本没必要override equals。
四.Consistent(一致性)只要用来比较的信息没被修改,那么x.equals(y)总是返回相同的结果。举个反例:java.net.URL的equals会把host名翻译成IP地址,这需要访问网络,所以随着时间的迁移,不保证每次结果都一样。
五.对于非null的x,x.equals(null)必须返回false。
感觉只是个规定而已。注意如果o是null的话,o instanceof MyType会返回false,所以没必要再if (o == null)return false;这样检查一遍了。
下面总结一下如果你要override equals,该怎么做:
一.if(argument == this) return true;只是个优化而已。
二.用instanceof检查argument是不是正确的类型,一般来说,正确类型是指当前这个方法所在的class,有时候也可以是当前这个class实现的某个接口。
三.Cast成刚才那个“正确的类型”。
四.比较所有“重要的”field,这里的“重要的”就相当于一个表中的两行,你只需要比较他们的主键就行。对于primitive type的field,用==,float和double例外,要用Float.compare和Double.compare,因为Float.NaN, -0.0f的存在。对于数组field,如果其中每个元素都重要,可以用Arrays.equals。对于允许null的field,如果两个null可以看作相等的,记得(field == null ? o.field == null :field.equals(o.field)),
对于一些需要复杂计算的field,比如CaseInsensitiveString中对s的比较,也许你可以定义一个对应s的小写版本的field来提高性能,当然这一般适用于inmmutable class。另外,你应该先比较那些计算简单的,并且更容易不同的field,也是为了提高性能。
五.最后一定要检查是不是满足了上述的contracts,写一些单元测试进行测试。
小提醒:equals中的逻辑别太复杂,简简单单地比较field就好;记得用 @Override。
Item 9: Always override hashCode when you override equals
equals返回true的话,那么这俩objects的hashcode也必须返回一样的值;不equals的objects允许返回相同的hashcode,但是作为一个合格的coder,要返回分布均匀的才对。后面写了计算Hashcode的具体方法,貌似和Thinking in Java的差不多,需要的时候可以都参考一下,总之就是你在equals里面用到的field,在hashcode里面也要用。如果一个class是immutable的或者计算hashcode比较麻烦,你可以考虑把hashcode cache起来。书上提供的这个方法也并不是“最新式的;使用了最先进技术的;顶尖水准的(state-of-the-art)”,这问题最好留给数学家什么的。
Item 10: Always override toString
由toString()返回的字符串应该满足“concise but informative representation that is easy for a person to read”。当对象被pass到println, printf,字符串连接操作符,assert,或者被debugger显示的时候,toString都会自动被调用,client在记录一些诊断信息的时候就很有用,前提是你override了toString。
当override toString时,你要决定是否把return string的具体的Format写入文档,好处在于这样就是一种标准,可以用于各种输入输出,比如XML,同时最好提供static factory或者constructor来从某个string representation转换为对应的object,大多包装类都实现了。坏处就在于,一旦release后,就不能改了,否则会破坏client代码。但不管怎样,你都应该在文档中清楚地写明自己的意图,并且为所有toString方法中用到的字段提供getter。
Item 11: Override clone judiciously
首先看一下Object中的clone方法:
protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class doesn't implement Cloneable");
}
return internalClone((Cloneable) this);
}
Cloneable是个接口,然而它里面什么方法都没有,这个接口的唯一用处就是:如果实现了这个接口,就表示这个类可以用clone方法。这种用法并不是接口应有的用法,不应该被模仿。internalClone是个native的方法(不会调用constructor),它会创建一个新的实例并把所有field都拷贝过去(shallow copy)。当我们override clone时,正确的做法是要调用super.clone(),所以最终肯定能调用到Object的clone,让它来创建一个当前类(this)的实例(所以在任何clone方法里都不能调用constructor,以及其他任何非final的方法,因为你不知道“当前这个类有没有子类”),然后再初始化一些子类上的field。所以,如果实现了Cloneable,那么就应该写一个well-behaved的clone方法,前提是如果这个类所有的基类都提供了well-behaved的clone方法。但因为Cloneable里面啥都没有(按理说应该把clone方法放进去的),所以这些“正确的做法”并不是强制性的。如果所有的field都是primitive value或者指向immutable对象,那么Object的clone就足够了:
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch(CloneNotSupportedException e) {
throw new AssertionError(); //Can't happen,因为已经实现了Cloneable接口,所以不需要像Object中的clone那样,
//还要在方法中声明throws CloneNotSupportedException
}
}
上面的cast成PhoneNumber是利用了方法override中的covariant return types,这样就省得client再去cast了。如果用浅复制clone出来的对象会影响到原有对象,就需要用深复制。
比如有个数组的field叫elements,可以直接result.elements = elements.clone(),这时候(我猜的噢)elements.clone()会返回一个新的并且内容相同的数组对象:
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
但注意如果是包含reference元素的数组,其中的references就都还指着同样的对象,如果对于你的类来说这样不行的话,那就必须“把每个数组元素再都new一个”。但注意如果elements是final的,那么就不行了,clone方法和指向mutable对象的final field是冲突的。如果一个类是被设计为要被别人继承的,那么它就应该像Object中的clone方法那样(方法中声明throws,并不实现Cloneable接口),否则就像上面写得这样就行(不需要throws,并且实现Cloneable接口)。BTW,Object.clone不是线程安全的。
上面讲了很多的clone()的缺点,所以最好别用clone(),除非万不得已。比如,immutable的对象就不需要被clone。更好的办法是copy constructor或者copy factory,比如:
public Yum(Yum yum);
public static Yum newInstance(Yum yum);
这种方法不仅没有clone()的缺陷,而且你还可以把输入参数的类型定义成某个接口,如
HashSet s;
new TreeSet(s);
这里的TreeSet构造函数就接受一个Set类型的参数。
Item 12: Consider implementing Comparable
首先是接口定义:
public interface Comparable<T> {
int compareTo(T t);
}
有点像equals方法,但是它可以用来比较谁大谁小,而equals只能比较两个是不是相等。实现Comparable就表明这个类的实例有内在的排序关系(算法看了那么多了,应该很熟了)。compareTo的contract是:如果this object小于传进来的object,就返回一个小于零的数....。这里的泛型参数T,至少在Java类库里面,都是跟当前的类是同一个类,所以你也别弄太复杂,就把T写成当前类就行。还有compareTo的规范和equals的差不多,也是自反性、对称性、传递性什么的,而且和equals一样,就是“there is no way to extend an instantiable class with a new value component while preserving the compareTo contract,unless you are willing to forgo the benefits of object-oriented abstraction(用getclass)”。强烈建议让equals和compareTo一致,当然也有例外,比如BigDecimal("1.0")和BigDecimal("1.00"),,equals就是不相等,而compareTo就是相等,所以如果你把他俩传给一个HashSet,就会装着两个元素;传给TreeSet就会只装着一个元素(TreeSet好像是基于红黑树的,所以是有顺序的,所以肯定基于compareTo)对于double和float用Double.compare和Float.compare,其他primitive用大于小于号。如果有多个field,你应该先比较最重要的field,然后比较第二重要的,以此类推。
《Effective Java》读书笔记 - 3.对于所有对象都通用的方法的更多相关文章
- [Effective Java]第三章 对所有对象都通用的方法
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- Effective Java 读书笔记(一):使用静态工厂方法代替构造器
这是Effective Java第2章提出的第一条建议: 考虑用静态工厂方法代替构造器 此处的静态工厂方法并不是设计模式,主要指static修饰的静态方法,关于static的说明可以参考之前的博文&l ...
- Effective Java读书笔记--创建和销毁对象
1.优先考虑用静态工厂方法代替构造器2.遇到多个构造器参数时要考虑使用构建器Builder解决参数过多,不可变类型.私有构造方法,静态类的构造方法提供必要参数,剩下可选.new xxx.build() ...
- [Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第八 ---- 九条
这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对 ...
- Effective Java 读书笔记
创建和销毁对象 >考虑用静态工厂方法替代构造器. 优点: ●优势在于有名称. ●不必再每次调用他们的时候都创建一个新的对象. ●可以返回原返回类型的任何子类型的对象. ●在创建参数化类型实例的时 ...
- Effective Java:对于全部对象都通用的方法
前言: 读这本书第1条规则的时候就感觉到这是一本非常好的书.可以把我们的Java功底提升一个档次,我还是比較推荐的.这里我主要就关于覆盖equals.hashCode和toString方法来做一个笔记 ...
- Java高效编程之二【对所有对象都通用的方法】
对于所有对象都通用的方法,即Object类的所有非final方法(equals.hashCode.toString.clone和finalize)都有明确的通用约定,都是为了要被改写(override ...
- Effective Java 读书笔记之一 创建和销毁对象
一.考虑用静态工厂方法代替构造器 这里的静态工厂方法是指类中使用public static 修饰的方法,和设计模式的工厂方法模式没有任何关系.相对于使用共有的构造器来创建对象,静态工厂方法有几大优势: ...
- Effective Java读书笔记——第三章 对于全部对象都通用的方法
第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它全部的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定 ...
随机推荐
- AtCoder Beginner Contest 076
A - Rating Goal Time limit : 2sec / Memory limit : 256MB Score : 100 points Problem Statement Takaha ...
- $APIO~2019$ 游记
我是鸽子. Upd:我全国倒数第一稳了. Uupd:时间过去好久了,这段时间发生很多事,比如NOIP没了... APIO时候的事也记得不是很清楚了,随便写点颓废资料吧: 如果想吃离酒店最近的一家火锅店 ...
- Python类函数调用:missing 1 required positional argument
在Python中,应该先对类进行实例化,然后在应用类.注意,实例化的过程是应该加括号的.
- solve update pip 10.0.0
The bug is found in pip 10.0.0. In linux you need to modify file: /usr/bin/pip from: from pip import ...
- es6 filter() 数组过滤方法总结(转载)
1.创建一个数组,判断数组中是否存在某个值 var newarr = [ { num: 1, val: 'ceshi', flag: 'aa' }, { num: 2, val: 'ceshi2', ...
- css实现两个div并排等高(一个div高度随另一个高度变化而变化)
方法一.两个div都设置 display: table-cell; 方法二.父级div设置 display: -webkit-box;
- Delphi 7的特点
- AIX中卷组管理
1.创建卷组 使用mkvg指令创建卷组. mkvg 指令参数 -B 创建大型卷组,该卷组最大能容纳128个物理卷和512个逻辑卷 -C 创建增加型并发卷组 -f 强制创建卷组 -G 与-B一样,创 ...
- final修饰符—不可变
final 修饰符 修饰类 不可以有子类 修饰变量 变量一旦获得初始值就不可改变,不能被重新赋值 成员变量:初始值必须有程序员显式设置,系统不会对其隐式初始化 类变量:静态初始化块 | 声明该类变量时 ...
- echarts实现心脏图的滚动三种实现方法
1.改变dataset 2.移动scrollbar 3.修改echarts自带的dataZoom的start和end