笔记《Effective Java》02:对所有对象都通用的方法(文末抽奖)

目录

1、前言

2、在重写equals方法时要遵守通用约定

2.1、什么时候重写equals方法呢?

2.2、重写equals的注意事项

3、重写equals方法时应该总是重写hashCode方法

3.1、为什么要重写hashCode

3.2、推荐的hashCode方法

4、总是重写toString方法

5、谨慎重写clone方法

5.1、替代Cloneable的复制方法

5.2、小结

6、考虑实现Comparable接口

6.1、小结


1、前言

《Effective Java》这本书可以说是程序员必读的书籍之一。这本书讲述了一些优雅的,高效的编程技巧。对一些方法或API的调用有独到的见解,还是值得一看的。刚好最近重拾这本书,看的是第三版,顺手整理了一下笔记,用于自己归纳总结使用。建议多读一下原文。今天整理第二章节:对所有对象都通用的方法。

2、在重写equals方法时要遵守通用约定

要避免犯错,最简单的方式就是不重写。这样的话,该类的实例只会与自身相等。
如果满足下述条件中的一个,不重写equals方法就是合理的:
  1. 该类的每个实例在本质上都是唯一的。对于诸如Thread这样代表活动实体而不是值的类来说,这是成立的。
  2. 该类没有必要提供一个“逻辑相等”的测试。如java.util.regex.Pattern。
  3. 超类已经重写了equals方法,而且其行为适合这个类。
  4. 类是私有的或包私有的,我们可以确信其equals方法绝对不会被调用。

2.1、什么时候重写equals方法呢?

  1. 当一个类在对象相同之外还存在逻辑相等的概念,而且其上层超类都没有重写equals方法。这通常就是值类的情况。如Integer或String。
在重写equals方法时,必须遵守通用约定。equals方法用来判断等价关系,有如下属性:
在尝试重写equals方法时,千万不要忽视这个约定。如果违反了,可能程序就会表现的不正常,甚至崩溃。
  1. 自反性:对于任何非null的引用值x,x.equals(x)必须返回true
  2. 对称性:对于任何非null的引用值x和y,当且仅当y.eguals(x)返回true 时,x.equals(y)必须返回 true
  3. 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回 true,并且y.equals(z)返回true,那么x.equals(z)必须返回 true
  4. 一致性:对于任何非null的引用值x和y,只要equals 比较中用到的信息没有修改,多次调用x.equals(y)必须一致地返回 true 或一致地返回false
  5. 对于任何非 null的引用值 x,x.equals(nul1)必须返回 false

2.2、重写equals的注意事项

  1. 重写equals方法时,应该总是重写hashCode方法
  2. 不要自作聪明。不要过度考虑各种相等关系。如File类不应该将多个指向同一个文件的符号连接视为相等。
  3. 不要将equals方法声明中的Object替换为其他类型。如:
public boolean equals(MyClass o) {
    
}
该方法只是重载了equals方法,而不是重写。应该强制使用@Override注解,在编译期间会告知我们哪里出错了。
总而言之,除非迫不得已,否则不要重写equals方法。多数情况下,从Object继承的equals实现就能满足。

3、重写equals方法时应该总是重写hashCode方法

重写equals方法的每个类都必须重写hashCode。

3.1、为什么要重写hashCode

如果没有这样做,类就会违反hashCode的通用约定,这将使其实例无法正常应用与诸如HashMap和HashSet等集合中。
hashCode有其通用约定,如下:
  1. 当在一个对象上重复调用hashCode方法时,只要在equals的比较中用到的信息没有修改,他就必须一致的返回同样的值
  2. 如果根据equals方法,两个对象是相等的,那么在这两个对象上调用hashCode方法,必须产生同样的整数结果
  3. 如果根据equals方法,两个对象不相等,那么在这两个对象上调用hashCode方法,并不要求产生不同的结果,只是不相等的对象产生不同的结果,可能会提高哈希表的性能

3.2、推荐的hashCode方法

  • 声明一个名为result的int变量,将其初始化为对象中第一个重要字段的hash码
  • 对其余的每个重要字段,如果是基本类型,则使用Type.hashCode(x)来计算。如果是对象引用,则在该字段上递归调用hashCode。如果是数组类型,则调用Arrays.hashCode。然后按照如下方式合并到result中:
result = 31 * result + c  // c为该字段的hashCode
  • 返回result

4、总是重写toString方法

虽然Object类提供了toString方法的一个实现,但他返回的字符串通常不是类的用户所希望看到的。toString的约定指出:“建议所有的子类都重写这个方法”。
  1. 提供一个好的toString实现可以让类用起来更舒适,使用该类的系统也更容易调试
  2. 如果条件允许,toString方法应该返回当前对象中包含的所有有意义的信息
  3. 无论是否决定指定格式,都应该在文档中清晰地表明自己的意图
借助Google的AutoValue或IDE自带的生成toString方法也比Object自带的toString好得多。

5、谨慎重写clone方法

Cloneable 接口的设计初衷是作为一个混合接口,用来表明类支持克隆功能。然而,它没有达到这个目的,主要的缺陷是缺乏 clone 方法,而 Object 类中的 clone 方法是受保护的。你无法直接调用 clone 方法,即便对象实现了 Cloneable,除非借助反射。更糟糕的是,反射调用也不一定总能成功,因为对象可能没有可访问的 clone 方法。
由于clone() 方法并不保证正确行为,除非类路径上的每个类都正确覆盖了clone() 方法,并且正确调用了super.clone() ,因此在Effective Java中,推荐使用“复制构造器”或者“私有构造器和静态工厂方法”来替代clone() 方法。
public class Example implements Cloneable {
    private int[] values;
 
    public Example(int[] values) {
        this.values = values.clone();
    }
 
    // 复制构造器
    public Example(Example original) {
        this.values = original.values.clone();
    }
 
    // 如果确实需要实现Cloneable接口的clone方法,可以按照如下方式实现
    @Override
    public Example clone() {
        try {
            return (Example) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // Can never happen
        }
    }
}
以上代码中, 我们使用clone() 方法来复制对象时,如果对象中包含数组等引用类型,应当在复制构造器中对这些引用类型也进行深复制。同时,由于clone() 方法可能会抛出CloneNotSupportedException ,所以通常会像示例中那样通过AssertionError 来保证这个异常永远不会发生,因为如果Example 类不支持Cloneable 接口,那么super.clone() 调用时就会抛出异常。

5.1、替代Cloneable的复制方法

与 Cloneable  和 clone  方法相关的复杂性通常是不必要的。更好的方法是提供复制构造函数或复制工厂方法。复制构造函数是一种以自身类型作为参数的构造函数:
// 复制构造器
public Yum(Yum yum) { ... }

// 复制工厂
public static Yum newInstance(Yum yum) { ... }
与Cloneable/clone相比,复制构造器方式及其静态工厂变体有许多优点:他们不依赖于Java核心语言之外的、存在风险的对象创建机制;他们不需要遵守基本没有文档说明的约定,更何况这样的约定还没有办法强制实施;他们与final字段的正常使用没有冲突;他们不会抛出不必要的检查型异常;他们不需要类型转换。

5.2、小结

当你重写 clone  时,确保使用 super.clone()  并对任何可变对象进行深拷贝。如果类的所有字段都是不可变的,则无需额外处理。对于大多数情况下,使用复制构造函数或工厂方法代替 clone  是更好的选择。

6、考虑实现Comparable接口

compareTo 并非 Object 类中声明的,而是 Comparable 接口的唯一方法。compareTo 方法与 equals 类似,但它不仅支持相等性比较,还允许顺序比较,同时它是泛型的。通过实现 Comparable 接口,一个类表明其实例具有自然顺序。这使得对实现 Comparable 的对象数组进行排序变得非常简单:
Arrays.sort(a);
几乎Java平台类库中所有的值类,以及所有的枚举类型都实现了Comparable接口。实现Comparable接口如下:
public interface Comparable<T> {
    int compareTo(T t);
}
编写 compareTo 方法类似于编写 equals 方法,但有一些关键区别。由于 Comparable 是参数化接口,因此 compareTo 方法是静态类型化的,避免了类型检查和强制转换。如果参数类型错误,代码甚至无法编译。
在 compareTo 方法中,字段是按顺序比较的。对于对象引用字段,可以递归调用 compareTo 方法。如果字段没有实现 Comparable,或者需要非标准排序,可以使用 Comparator。例如,下面是一个比较 CaseInsensitiveString 类的 compareTo 方法:
// 使用对象引用字段的单字段 Comparable
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
    public int compareTo(CaseInsensitiveString cis) {
        return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
    }
    // 其他代码省略
}
effective java此前的几个版本推荐在compareTo方法中使用关系运算符<和>来比较整形的基本类型字段,使用Double.compare和Float.compare来比较浮点型基本类型字段。而在Java7中,所有基本类型封装类都添加静态的compare方法。在compareTo方法中使用<和>非常繁琐,而且容易出错,所以不再推荐。
Java8中,Comparator接口提供了一组比较构造器方法,这些比较器可以用来实现Comparable接口所要求的compareTo方法,不过性能上会稍微慢一些。使用比较器构造方法实现的Comparable如下:
// 使用比较器构造方法的 Comparable
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);
}
基于差的比较器,会破坏传递性,存在问题。应该避免使用:
// 错误的差值比较器 - 违反传递性
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();
    }
};
使用静态的compare方法的比较器来替代:
// 基于静态比较方法的比较器
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};
或者使用比较器构造方法的比较器:
// 基于比较器构造方法的比较器
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

6.1、小结

总而言之,每当要实现一个可以合理地进行排序地值类时,都应该让这个类实现Comparable接口,这样他的实例就可以轻松地被排序、查找和用在基于比较地集合中。在CompareTo方法地实现中,当比较字段的值时,应该避免使用<和>运算符。相反,请使用基本类型的封装类中的静态compare方法,或使用Comparator接口中的比较器构造方法。

7、粉丝福利


感谢您耐心阅读完这篇技术博客!希望这些内容能为您的技术探索之旅带来启发和帮助。

为了回馈一直支持我的粉丝朋友们,我特别准备了一份专属福利!🎉

《推荐系统:技术原理与实践》是一本深入浅出地讲解推荐系统核心原理与实战技巧的优质书籍。无论是初学者还是有一定基础的技术人员,都能从中获取宝贵的知识和灵感。

福利获取方式:

1. 关注我的公众号:柴犬说编程

2. 在公众号对话框中回复“抽奖”

3. 即可获得抽奖链接,参与抽奖,赢取这本书籍!

抽奖活动将持续到2025-02-28 22:00,中奖名单将在公众号公布,敬请关注!

再次感谢您的支持与陪伴,希望这本书能成为您技术成长路上的得力助手。祝您好运!😊

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有一只柴犬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值