前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >笔记《Effective Java》02:对所有对象都通用的方法

笔记《Effective Java》02:对所有对象都通用的方法

原创
作者头像
有一只柴犬
修改2025-02-07 19:39:29
修改2025-02-07 19:39:29
1430
举报
文章被收录于专栏:高效编程高效编程

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替换为其他类型。如:
代码语言:java
复制
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方法

  1. 声明一个名为result的int变量,将其初始化为对象中第一个重要字段的hash码
  2. 对其余的每个重要字段,如果是基本类型,则使用Type.hashCode(x)来计算。如果是对象引用,则在该字段上递归调用hashCode。如果是数组类型,则调用Arrays.hashCode。然后按照如下方式合并到result中:
代码语言:java
复制
result = 31 * result + c  // c为该字段的hashCode
return 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()方法。

代码语言:java
复制
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 方法相关的复杂性通常是不必要的。更好的方法是提供复制构造函数或复制工厂方法。复制构造函数是一种以自身类型作为参数的构造函数:

代码语言:java
复制
// 复制构造器
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 的对象数组进行排序变得非常简单:

代码语言:java
复制
Arrays.sort(a);

几乎Java平台类库中所有的值类,以及所有的枚举类型都实现了Comparable接口。实现Comparable接口如下:

代码语言:java
复制
public interface Comparable<T> {
    int compareTo(T t);
}

编写 compareTo 方法类似于编写 equals 方法,但有一些关键区别。由于 Comparable 是参数化接口,因此 compareTo 方法是静态类型化的,避免了类型检查和强制转换。如果参数类型错误,代码甚至无法编译。

在 compareTo 方法中,字段是按顺序比较的。对于对象引用字段,可以递归调用 compareTo 方法。如果字段没有实现 Comparable,或者需要非标准排序,可以使用 Comparator。例如,下面是一个比较 CaseInsensitiveString 类的 compareTo 方法:

代码语言:java
复制
// 使用对象引用字段的单字段 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如下:

代码语言:java
复制
// 使用比较器构造方法的 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);
}

基于差的比较器,会破坏传递性,存在问题。应该避免使用:

代码语言:java
复制
// 错误的差值比较器 - 违反传递性
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();
    }
};

使用静态的compare方法的比较器来替代:

代码语言:java
复制
// 基于静态比较方法的比较器
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};

或者使用比较器构造方法的比较器:

代码语言:java
复制
// 基于比较器构造方法的比较器
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

6.1、小结

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 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、小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档