[Java读书笔记] Effective Java(Third Edition) 第 3 章 对于所有对象都通用的方法
第 10 条:覆盖equals时请遵守通用约定
在不覆盖equals方法下,类的每个实例都只与它自身相等。
- 类的每个实例本质上都是唯一的。
- 类不需要提供一个”逻辑相等(logical equality)”的测试功能。
- 父类已经重写了 equals 方法,并且父类的行为完全适合于该子类。
- 类是私有的或包级私有的,并且可以确定它的 equals 方法永远不会被调用。
什么时候需要覆盖equals方法?
如果一个类包含一个逻辑相等( logical equality)的概念——此概念有别于对象同一性(object identity),而且父类还没有重写过 equals 方法。
这通常用在值类( value classes)的情况。
在覆盖equals 方法时,必须遵守它的通用规范。下面是 Object 类注释里的规范:
- 自反性:x.equals(x) 必须返回 true
- 对称性:x.equals(y) 返回 true 当且仅当 y.equals(x) 返回 true
- 传递性:如果 x.equals(y) 返回 true,y.equals(z) 返回 true,则x.equals(z) 必须返回 true
- 一致性:如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用必须始终返回true或始终返回false
- 对于任何非空引用 x,x.equals(null) 必须返回 false
编写高质量 equals 方法的秘诀:
- 使用 == 运算符检查参数是否为该对象的引用。如果是,返回true。
- 使用 instanceof 运算符来检查参数是否具有正确的类型。 如果不是,则返回 false。
- 将参数转换为正确的类型。
- 对于类中的每个关键域(属性),检查参数的属性是否与该对象对应的属性相匹配。
- 对于类型为非 float 或 double 的基本类型,使用 == 运算符进行比较;对于对象引用属性,递归地调用 equals 方法;对于 float 基本类型的属性,使用静态方法 Float.compare(float, float);对于 double 基本类型的属性,使用 Double.compare(double, double) 方法。
- equals 方法的性能可能受到属性比较顺序的影响。为了获得最佳性能,你应该首先比较最可能不同的属性和开销比较小的属性。
例如String 的例子:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
最后的告诫:
- 覆盖equals时总要覆盖hashCode。
- 不要企图让equals方法过于智能。
- 不要将equals声明中的Object对象替换为其他的类型。
总之,不要轻易覆盖equals方法,除非迫不得已。因为很多情况下,从Object继承的实现正是你想要的。
如果覆盖equals方法,一定要比较这个类的所有关键域,并且确保遵守equals合约的五个条款。
第 11 条:覆盖equals时总要覆盖hashCode
在每一个重写 equals 方法的类中,都要重写 hashCode 方法。
如果不这样做,你的类会违反 hashCode 的通用约定,这会阻止它在 HashMap 和 HashSet 这样的集合中正常工作。
Object源码约定内容:
- 在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法必须始终返回相同的值。从一个应用程序到另一个应用程序时返回的值可以是不一致的。
- 如果两个对象根据 equals(Object) 方法比较是相等的,那么在这两个对象上调用 hashCode 就必须产生相同的整数结果。
- 如果两个对象根据 equals(Object) 方法比较并不相等,不要求在每个对象上调用 hashCode 都必须产生不同的结果。 为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。
没有覆盖hashCode违反上述规约第二条:相等对象必须具有相等的散列码(hashCode)。
一个好的 hash 方法趋向于为不相等的实例生成不相等的哈希码。
理想情况下,hash 方法为集合中不相等的实例均匀地分配 int 范围内的哈希码。实现这种理想情况可能很困难。
简单步骤:
1. 声明一个 int 类型的变量 result,并将其初始化为对象中第一个重要属性 c 的哈希码,如下面步骤 2.a 中所计算的那样。
2. 对于对象中剩余的重要属性 f ,执行以下操作:
a. 为属性 f 与计算一个 int 类型的哈希码 c:
i. 如果这个属性是基本类型,使用 Type.hashCode(f) 方法计算,其中 Type 类是对应属性 f 的包装类。
ii. 如果该属性是一个对象引用,并且该类的 equals 方法通过递归调用 equals 来比较该属性,那么递归地调用 hashCode 方法。
如果需要更复杂的比较,则计算此字段的“范式”(canonical representation),并在范式上调用 hashCode 。
如果该字段的值为空,则使用 0(也可以使用其他常数,但通常使用 0 表示)。
iii. 如果属性 f 是一个数组,把数组中每个重要的元素都看作是一个独立的属性。
如果数组没有重要的元素,则使用一个常量,最好不要为0。如果所有元素都很重要,则使用 Arrays.hashCode 方法。
b. 将步骤 2.a 中计算出的哈希码 c 合并为如下结果:result = 31 * result + c;
3. 返回 result 值。
例子:
// Typical hashCode method
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
写完之后验证,问自己“相等的实例是否都具有相等的散列码”。
总之,每当覆盖equals方法时都必须覆盖hashCode,否则程序将无法正确运行。
还可以利用AutoValue(google)生成equals和hashCode方法,不必手工编写,可以省略测试。部分IDE也提供类似的部分功能。
第 12 条:始终要覆盖toString
提供好的易读的toString实现可以让使用这个类的系统更容易调试。
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。无论是否指定格式,应该在文档中明确地表明你的意图。
在静态工具类中编写toString方法时没有意义的,也不用在大多数枚举类型中编写toString方法。
Google开源的AutoValue会替你生成toString方法。
总之,要在你编写的每一个可实例化的类中覆盖Object的toString实现,除非已经在超类中这么做了。
这样会让类的使用易于调试。toString方法应该返回一个关于对象的简洁、有用的描述。
第 13 条:谨慎地覆盖clone
Cloneable接口的目的是作为对象的一个接口,表明这样的对象允许克隆(clone)。
实现Cloneable接口的类是为了提供一个功能适当的公有的clone方法。
假设你希望在一个类中实现Cloneable接口,它的父类提供了一个行为良好的 clone方法。
首先调用super.clone。 得到的对象将是原始的完全功能的复制品。 在你的类中声明的任何属性将具有与原始属性相同的值。
如果每个属性包含原始值或对不可变对象的引用,则返回的对象可能正是你所需要的,在这种情况下,不需要进一步的处理。(浅拷贝)
不可变的类永远都不应该提供clone方法。
如果对象包含引用可变对象的属性,则前面显示的简单clone实现可能是灾难性的。
例子:
ublic class Stack { private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
} public void push(Object e) {
ensureCapacity();
elements[size++] = e;
} public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference
return result;
} // Ensure space for at least one more element.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
上述例子希望做成可克隆的,如果clone方法仅返回super.clone()调用的对象,那么生成的Stack实例在其size 属性中具有正确的值,
但elements属性引用与原始Stack实例相同的数组。 修改原始实例将破坏克隆中的约束条件,反之亦然。
你会很快发现你的程序产生了无意义的结果,或者抛出NullPointerException异常。
实际上,clone方法就是另一个构造器,必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。
上述例子在elements数组中递归地调用clone:
// Clone method for class with references to mutable state
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
在数组上调用clone返回的数组,编译时的类型与被克隆数组的类型相同,这是复制数组的最佳习惯。
如果elements属性是final的,则以前的解决方案将不起作用,因为克隆将被禁止向该属性分配新的值。
这是一个基本的问题:像序列化一样,Cloneable体系结构与引用可变对象的final 属性的正常使用不兼容。
仅仅递归地调用clone方法并不总是足够的。
例如一个类包含一个散列桶数组,每个散列通都指向键-值对链表第一项,是一个单向链表。
如果仅克隆散列数组,但是这个数组引用的链表与原始对象一样,容易引起克隆对象和原始对象中不确定行为。所有必须单独地拷贝并组成每个桶的链表。
简而言之,实现Cloneable的所有类应该重写公共clone方法,而这个方法的返回类型是类本身。
这个方法应该首先调用super.clone,然后修复任何需要修复的属性。
通常,这意味着复制任何包含内部“深层结构”的可变对象,并用指向新对象的引用来代替原来指向这些对象的引用。
虽然这些内部拷贝通常可以通过递归调用clone来实现,但这并不总是最好的方法。
如果类只包含基本类型或对不可变对象的引用,那么很可能是没有属性需要修复的情况。
这个规则也有例外, 例如,表示序列号或其他唯一ID的属性即使是基本类型的或不可变的,也需要被修正。
对象拷贝的更好方法时提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。
// Copy constructor
public Yum(Yum yum) { ... };
// Copy factory
public static Yum newInstance(Yum yum) { ... };
总之,考虑到与Cloneable接口相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。
虽然实现Cloneable接口对于final类没有什么危害,但应该将其视为性能优化的角度,仅在极少数情况下才是合理的(条目67)。
通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone方法复制。
第 14 条:考虑实现Comparable接口
与本章讨论的其他方法不同,compareTo 方法并没有在 Object 类中声明。
相反,它是 Comparable 接口中的唯一方法。 通过实现 Comparable 接口,一个类表明它的实例有一个自然序( natural ordering )。
通过实现 Comparable 接口,可以让你的类与所有依赖此接口的泛型算法和集合实现进行交互操作。
Java 平台类库中几乎所有值类以及所有枚举类型(条款 34)都实现了 Comparable 接口。
如果你正在编写具有明显自然序(如字母顺序、数字顺序或时间顺序)的值类,则应该实现 Comparable 接口:
public interface Comparable<T> {
int compareTo(T t);
}
将此对象与指定的对象按照排序进行比较。返回值可能为负整数,零或正整数,对应此对象小于,等于或大于指定的对象。
compareTo不能跨越不同类型的对象进行比较,在比较不同类型的对象时,抛出ClassCastException异常。
考虑 BigDecimal 类,其 compareTo 方法与 equals 不一致。
如果你创建一个空的 HashSet 实例,然后添加 new BigDecimal("1.0") 和 new BigDecimal("1.00"),则该集合将包含两个元素,
因为用 equals 方法进行比较时,添加到集合的两个 BigDecimal 实例是不相等的。
但是,如果使用 TreeSet 而不是 HashSet 执行相同的过程,则该集合将只包含一个元素,因为使用 compareTo 方法进行比较时,两个 BigDecimal 实例是相等的。
在 Java 7 中,静态比较方法被添加到 Java 的所有包装类中。在 compareTo 方法中使用关系运算符 < 和 > 是冗长且容易出错的,不再推荐。
在 Java 8 中 Comparator 接口提供了一系列比较器方法,可以流畅地构建比较器。
许多程序员更喜欢这种方法的简洁性,尽管它会牺牲一定地性能。在使用这种方法时,考虑使用 Java 的静态导入,以便可以通过其简单名称来引用比较器静态方法。
// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
[Java读书笔记] Effective Java(Third Edition) 第 3 章 对于所有对象都通用的方法的更多相关文章
- [Effective Java]第三章 对所有对象都通用的方法
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- [Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第八 ---- 九条
这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对 ...
- [Java读书笔记] Effective Java(Third Edition) 第 7 章 Lambda和Stream
在Java 8中,添加了函数式接口(functional interface),Lambda表达式和方法引用(method reference),使得创建函数对象(function object)变得 ...
- [Java读书笔记] Effective Java(Third Edition) 第 6 章 枚举和注解
Java支持两种引用类型的特殊用途的系列:一种称为枚举类型(enum type)的类和一种称为注解类型(annotation type)的接口. 第34条:用enum代替int常量 枚举是其合法值由一 ...
- [Java读书笔记] Effective Java(Third Edition) 第 4 章 类和接口
第 15 条: 使类和成员的可访问性最小化 软件设计基本原则:信息隐藏和封装. 信息隐藏可以有效解耦,使组件可以独立地开发.测试.优化.使用和修改. 经验法则:尽可能地使每个类或者成员不被外界访问 ...
- [Java读书笔记] Effective Java(Third Edition) 第2章 创建和销毁对象
第 1 条:用静态工厂方法代替构造器 对于类而言,获取一个实例的方法,传统是提供一个共有的构造器. 类可以提供一个公有静态工厂方法(static factory method), 它只是一个返回类 ...
- Effective Java读书笔记——第三章 对于全部对象都通用的方法
第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它全部的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定 ...
- 《Effective Java》第3章 对于所有对象都通用的方法
第8条:覆盖equals时请遵守通用约定 覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重.最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每 ...
- 《Effective Java》第2章 对所有对象都通用的方法
第10条:覆盖equals时,请遵守通用约定 1.使用==来比较两个对象的时候,比较的是两个对象在内存中的地址是否相同(两个引用指向的是否为同一个对象):Object中定义的equals方法也是这样比 ...
随机推荐
- $(selector).each() 和$each() 的区别
今天在做项目的时候, 后台数据需要循环遍历出来, 想到each, 结果,竟然不记得语法了 现在来回顾一下,而搜索了一下,竟然发现有两种each 一种就是$(selector).each() ...
- 2.03_01_Python网络爬虫urllib2库
一:urllib2库的基本使用 所谓网页抓取,就是把URL地址中指定的网络资源从网络流中抓取出来.在Python中有很多库可以用来抓取网页,我们先学习urllib2. urllib2 是 Python ...
- springboot无法获取证书内容
最近项目里面在接第三方验证的时候,需要用到生成的公钥和私钥证书.在demo测试的时候,发现在resources里面直接建立一个key文件夹放入证书文件,然后使用文件方式去获取,大概代码如下: File ...
- cursor:not-allowed
今天发现了一个鼠标样式:not-allowed,是一个红色圈加一个斜杠,表示禁止的意思,似乎IE ,chrome firefox 都可以正常显示,很好用
- 如何决定使用 HashMap 还是 TreeMap?(未完成)
如何决定使用 HashMap 还是 TreeMap?(未完成)
- 洛谷P2664 树上游戏——点分治
原题链接 被点分治虐的心态爆炸了 题解 发现直接统计路径上的颜色数量很难,考虑转化一下统计方式.对于某一种颜色\(c\),它对一个点的贡献为从这个点出发且包含这种颜色的路径条数. 于是我们先点分一下, ...
- ProjectEuler237 Tours on a 4 x n playing board
思路是这样的 插头dp-->打表-->OEIS查表-->通项公式-->矩阵快速幂优化线性递推 OEIS竟然有这个东西的生成函数啊 答案为15836928 这是最终代码 #inc ...
- Docker清除容器镜像命令:
# ~/.bash_aliases # Kill all running containers. alias dockerkillall='docker kill $(docker ps -q)' # ...
- PHP即时通讯设计实现
详解即时通讯设计实现(PHP+GatewayWorker+Redis) 需要实现的功能 一对一聊天(私聊) 一对多聊天(群聊) 类似QQ,微信等聊天列表 实时消息 显示 工具选择 GatewayWor ...
- 牛客练习赛3 绝对半径 ——k尺取法
题目 链接 题意:一个n个数字的序列,最多去掉其中k个,使得连续相同数字序列的长度尽量长 分析 如果不考虑去掉元素,就是普通的尺取.考虑到去元素,则只需做一点修改. 先离散化,再把每种元素的位置用va ...