不需覆盖:
- 类的每个实例本质上都是唯一的
- 类没有必要提供“逻辑相等”( logical equality )的测试功能
- 超 类 已经覆盖 了 equals , 超类的行为对于这个 类也是合适的
- 类是私有的,或者是包级私有的 ,可以确定它的 equals 方法永远不会被调用 。
equals实现等价关系,其属性:
- 自反性
- 对称性
- 一致性
- 对于任何非null的引用值x,x.equals(null)必须返回false
实现高质量equals方法:
- 使用==操作符检查“参数是否为这个对象的引用”
- 使用 i nstanceof 操作符检查“参数是否为正确的类型” 。
- 把参数转换成正确的类型
- 对于该类中的每个“关键”( significant )域,检查参数中的域是否与该对象中对应的域相匹配
- 在编写完 equals 方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的 ?
eg:
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(short areaCode, short prefix, short lineNum) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNum = lineNum;
}
private static short rangeCheck(int val, int max, String arg){
if(val < 0 || val > max){
throw new IllegalArgumentException(arg + ":" + val);
}
return (short)val;
}
@Override
public boolean equals(Object o){
if(o == this){
return true;
}
if(!(o instanceof PhoneNumber)){
return false;
}
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}
告诫:
- 覆盖 equals 时总要覆盖 hashCode
- 不要企图让 equals 方法过于 智能
- 不要将 equals 声 明 中 的 Object 对象替换为其他的类型
Object 规范
- 在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用, hashCode 方法都必须始终返回同一个值 。在一个应用程序与另一个程序的执行过程中,执行 hashCode 方法所返回的值可以不一致 。
- 如果两个对象根据 equals(Object )方法比较是相等的,那么调用这两个对象中的 hashCode 方法都必须产生同样的整数结果 。
- 如果两个对象根据 equals(Object )方法比较是不相等的,那么调用这两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果 。 但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表( hashtable )的性能 。
解决方法:
-
声明一个 int 变量并命名为 result ,将它初始化为对象中第一个关键域的散列码c 。
-
对象中剩下的每一个关键域 f 都完成以下步骤:
a . 为该域计算 int 类型的散列码 C:
I . 如果该域是基本类型,则计算 Type. hashCode ( f ),这里的 Type 是装箱基本类型的类,与 f 的类型相对应 。
II .如果该域是一个对象引用,并且该类的 equals 方法通过递归地调用 equals的方式来比较这个域,则同样为这个域递归地调用 hashCode 。 如果需要更复杂的比较,则为这个域计算一个“范式”,然后针对这个范式调用 hashCode 。 如果这个域的值为 null , 则 返回0(或者其他某个常数,但通常是 0)
III. 如果该域是一个数组,则要把每一个元素当作单独的域来处理 。 也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤 2.b中的做法把这些散列值组合起来 。 如果数组域中没有重要的元素,可以使用一个常量,但最好不要用 0。 如果数组域中的所有元素都很重要,可以使用Arrays.hashCode方法
b. 按照下面公式,把2.a中计算得到的散列码c合并到result中
result = 31 * result + c;
3 返回 result 。
之所以选择 31 ,是因为它是一个奇素数 。 如果乘数是偶数,并且乘法、溢出的话,信息就会丢失,因为与 2 相乘等价于移位运算 。 使用素数的好处并不很明显,但是习惯上都使用素数来计算散列结果 。 3 1 有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能 : 31 * i = = ( i < < 5 ) - i. 现代的虚拟机可以自动完成这种优化 。
@Override
public int hashCode(){
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
Object方法:
@Override
public int hashCode() {
return Objects.hash(areaCode, prefix, lineNum);
}
如果一个类是不可变的,并且计算散列码的开销也比较大 , 就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码 。
如果你觉得这种类型的大多数对象会被用作散列键( hash keys ),就应该在创建实例的时候计算散列码 。 否则,可以选择“延迟初始化”( lazi ly initialize )散列码,即一直到 hashCode 被第一次调用的时候才初始化
12.始终要覆盖toString建议所有的子类都覆盖这个方法
eg
@Override
public String toString(){
return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
13.谨慎地覆盖clone
Cloneable接口的目的是作为对象的一个mixn接口,表明这样的对象允许克隆,它的主要缺陷是少一个clone方法,而Object的clone方法是受保护的。
Cloneable接口并没有包含任何方法,作用为决定了Object中受保护的clone方法实现的行为,如果一个类实现类Cloneable,Object的clone方法就返回该对象的逐域看吧,否则就会抛出CloneNotSupportedException异常。
拷贝的含义
- 对于任何对象x:x.clone != x -------->true
- x.clone.getClass() == x.getClass() -------> true
- x.clone.equals(x) -----> true (不是绝对的要求)
不可变的类永远不应该提供clone方法
clone 方法就是另一个构造器; 必须确保它不会伤害到原始的对象, 并确保正确地创建被克隆对象中的
约束条件 。
公有的 clone 方法应该省略 throws 声明 ,因为不会抛出受检异常的方法使用起来更加轻松 。
所有实现了 Cloneable 接口的类都应该覆盖 clone 方法,并且是公有的方法,它的返回类型为类本身 。 该方法应该先调用 super.clone 方法,然后修正任何需要修正的域 。 一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用 。 虽然,这些内部拷贝操作往往可以通过
递归地调用 clo口e 来完成,但这通常并不是最佳方法 。 如果该类只包含基本类型的域,或者指向不可变对象的引用,那么多半的情况是没有域需要修正 。
eg
@Getter
@Setter
public final class PhoneNumber implements Cloneable {
private final short areaCode, prefix, lineNum;
private Entry[] buckets;
private List lists;
private Object[] elements;
private Person person;
private class Entry{
final Object key;
Object value;
Entry next;
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
// Entry deepCopy(){
// return new Entry(key, value, next == null ? null: next.deepCopy());
// }
//可以用迭代代替递归
Entry deepCopy(){
Entry result = new Entry(key, value, next);
for(Entry p = result; p.next != null; p = p.next){
p.next = new Entry(p.next.key, p.next.value, p.next.next);
}
return result;
}
}
public PhoneNumber(short areaCode, short prefix, short lineNum) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNum = lineNum;
}
private static short rangeCheck(int val, int max, String arg){
if(val < 0 || val > max){
throw new IllegalArgumentException(arg + ":" + val);
}
return (short)val;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PhoneNumber that = (PhoneNumber) o;
return areaCode == that.areaCode &&
prefix == that.prefix &&
lineNum == that.lineNum;
}
@Override
public String toString(){
return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
@Override
public PhoneNumber clone(){
try {
PhoneNumber phoneNumber = (PhoneNumber)super.clone();
//对object[] 的clone
phoneNumber.elements = elements.clone();
//对键值对 clone
phoneNumber.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++) {
if(buckets[i] != null){
phoneNumber.buckets[i] = buckets[i].deepCopy();
}
}
return phoneNumber;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
对象拷贝的更好的办法是提供一个拷贝构造器( copyconstructor)或拷贝工厂( copy factory ) 。
和JAVA开发者对话 Bill Venners: 在你的书中,你建议使用复制构造函数而不是实现Cloneable和编写clone。你能详细说明吗? Josh Bloch:如果你已经阅读了我的书中关于克隆的章节,特别是如果你看得仔细的话,你就会知道我认为克隆已经完全坏掉的东西。有一些设计缺陷,其中最大的一个是 Cloneable 接口没有 clone方法。这意味着它根本不起作用:实现了 Cloneable 接口并不说明你可以用它做什么。相反,它说明了内部可能做些什么。它说如果通过super.clone 反复调用它最终调用 Object 的 clone 方法,这个方法将返回原始的属性副本。 但它没有说明你可以用一个实现 Cloneable 接口的对象做什么,这意味着你不能做多态 clone 操作。如果我有一个 Cloneable 数组,你会认为我可以运行该数组并克隆每个元素以制作数组的深层副本,但不能。你不能强制转换对象为 Cloneable 接口并调用 clone 方法,因为 Cloneable 没有public clone 方法,Object 类也没有。如果您尝试强制转换 Cloneable 并调用该 clone 方法,编译器会说您正在尝试在对象上调用受保护的clone方法。 事实的真相是,您不通过实施 Cloneable 和提供 clone 除复制能力之外的公共方法为您的客户提供任何能力。如果您提供具有不同名称的copy操作, 怎么也不次于去实现 Cloneable 接口。这基本上就是你用复制构造函数做的事情。复制构造方法有几个优点,我在本书中有讨论。一个很大的优点是可以使副本具有与原始副本不同的实现。例如,您可以将一个 linkedList 复制到 ArrayList。 Object 的 clone 方法是非常棘手的。它基于属性复制,而且是“超语言”。它创建一个对象而不调用构造函数。无法保证它保留构造函数建立的不变量。多年来,在Sun内外存在许多错误,这源于这样一个事实,即如果你只是super.clone 反复调用链直到你克隆了一个对象,那么你就拥有了一个浅层的对象副本。克隆通常与正在克隆的对象共享状态。如果该状态是可变的,则您没有两个独立的对象。如果您修改一个,另一个也会更改。突然之间,你会得到随机行为。 我使用的东西很少实现 Cloneable。我经常提供实现类的 clone 公共方法,仅是因为人们期望有。我没有抽象类实现 Cloneable,也没有接口扩展它,因为我不会将实现的负担 Cloneable 放在扩展(或实现)抽象类(或接口)的所有类上。这是一个真正的负担,几乎没有什么好处。 Doug Lea 走得更远。他告诉我 clone 除了复制数组之外他不再使用了。您应该使用 clone 复制数组,因为这通常是最快的方法。但 Doug 的类根本就不再实施 Cloneable了。他放弃了。而且我认为这并非不合理。 这是一个耻辱, Cloneable 接口坏掉了,但它发生了。最初的 Java API在紧迫的期限内完成,以满足市场窗口收紧的需求。最初的 Java 团队做了不可思议的工作,但并非所有的 API 都是完美的。 Cloneable 是一个弱点,我认为人们应该意识到它的局限性。 ———————————————— 版权声明:本文为CSDN博主「weixin_39624700」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_39624700/article/details/114058196
public class Parents {
private int id;
private Child child;
public Parents(int id, Child child) {
this.id = id;
this.child = child;
}
//实现对象的复制
public Parents(Parents parents){
id = parents.id;
child = new Child(parents.child);
}
}
public class Child {
public String name;
public Child(String name){
this.name = name;
}
//实现对象的复制
public Child(Child child){
name = child.name;
}
}
14.考虑实现Comparable接口
- compare To 方法并没有在 Object 类中声明 。
- 它是 Comparable 接口中唯一的方法 。
- compare To 方法不但允许进行简单的等同性比较,而且允许执行顺序比较,
- 它与 Object 的 equals 方法具有相似的特征,它还是个泛型( generic ) 。
Comparable接口
public interface Comparable{ public int compareTo(T o); }
compareTo约定:
当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数 。 如果由 于指定对象的类型而无法与该对象进行比较,则抛出 ClassCastException 异常 。
自反性,传递性,对称性
依赖于比较关系的类包括有序集合类 TreeSet 和 TreeMap ,以及工具类 Collections 和 Arrays ,它们内部包含有搜索和排序算法 。
public class CaseInsensitiveString implements Comparable{ private final String s; public CaseInsensitiveString(String s) { this.s = Objects.requireNonNull(s); } public int compareTo(CaseInsensitiveString cis){ return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s); } }
多个关键域,从最关键的域开始,产生非0结果,比较操作结束
@Getter @Setter public final class PhoneNumbers implements Cloneable, Comparable{ private final short areaCode, prefix, lineNum; public PhoneNumbers(short areaCode, short prefix, short lineNum) { this.areaCode = areaCode; this.prefix = prefix; this.lineNum = lineNum; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PhoneNumbers that = (PhoneNumbers) o; return areaCode == that.areaCode && prefix == that.prefix && lineNum == that.lineNum; } @Override public String toString(){ return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum); } @Override public int compareTo(PhoneNumbers ph) { int result = Short.compare(areaCode, ph.areaCode); if(result == 0){ result = Short.compare(prefix, ph.prefix); if(result == 0){ result = Short.compare(lineNum, ph.lineNum); } } return result; } }
java8 比较器构造方法 —> 简洁,但付出性能成本
@Getter @Setter public final class PhoneNumbers implements Cloneable, Comparable{ private static final Comparator COMPARATOR = comparingInt((PhoneNumbers pn) ->pn.areaCode) .thenComparingInt(pn -> pn.prefix) .thenComparingInt(pn -> pn.lineNum); private final short areaCode, prefix, lineNum; public PhoneNumbers(short areaCode, short prefix, short lineNum) { this.areaCode = areaCode; this.prefix = prefix; this.lineNum = lineNum; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PhoneNumbers that = (PhoneNumbers) o; return areaCode == that.areaCode && prefix == that.prefix && lineNum == that.lineNum; } @Override public String toString(){ return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum); } @Override public int compareTo(PhoneNumbers ph) { return COMPARATOR.compare(this, ph); } }
- 每当实现一个对排序敏感的类时,都应该让这个类实现 Comparable 接口,以便其实例可以轻松地被分类 、 搜索,以及用在基于比较的集合中 。
- 每当在 compareTo 方法的实现中比较域值时,都要避免使用 < 和>操作符,而应该在装箱基本类型
的类中使用静态的 compare 方法,或者在 Comparator 接口中使用比较器构造方法 。



