《Effective Java》——对所有对象都通用的方法

Chapter 3 Methods Common to All Objects

Item 10: Obey the general contract when overriding equals

重写 equals() 方法时要遵守通用的约定。

Cases where avoid overriding euqals

当某个类的实例只与自己相等的时候,不要重写equals方法。例如下面的情况:

  • Each instance of the class is inherently unique. 每个实例本质上是独一无二的,例如Thread的实例。
  • There is no need for the class to provide a "logical equality" test. 某个类不需要提供【逻辑相等】的测试。
  • A superclass has already overridden equals, and the superclass behavior is appropriate for this class. 父类已经重写了equals方法,并且父类的行为与子类适配。
  • The class is private or package-private, and you are certian that its equals method will never be invoked. 类是私有的或者是包私有的,并且确定它的equals方法永远也不会被调用。

occasions where sholud to override equals

  • 某个类包含逻辑相等(logical equality)的概念,并且父类没有重写equals方法。例如,StringInteger等等。
  • 当值类(Value Class)使用了实例控制(Instance Control)来保证每个值仅有一个实例的时候,例如Enum枚举类,不需要重写equals方法

Propertis of equals method

equals方法的性质:

  • 自反性 (Reflexive):对于非空的引用o,表达式o.equals(o)的值必须为true
  • 对称性(Symmetric): 对于非空的引用x | y,表达式x.equals(y)y.equals(x)的值相同
  • 传递性(Transitive): 对于非空引用x, y, z,如果x.equals(y) && y.equals(z),那么x.equals(z)
  • 一致性(Consistence): 对于任意的非空引用x, y,如果用于比较的equals的内容没有修改,那么无论经过多少次调用x.equals(y)返回值都应当保持不变
  • 非空性(Non-nullity): 所有的非空对象都不应该和null相等

无论一个类是否是可变的,都不应该写一个依赖于不可靠资源的equals方法。

Recipe for a high-quailty equals method

  1. Use the == operator to check if the argument is a reference to this object. 使用==运算符检查此参数是否为该对象的引用
  2. Use the instanceof operator to check if the argument has the correct type. 使用instanceof运算符检查参数是否是正确的类型。
  3. Cast the argument to the correct type. 将参数转换为正确的类型。
  4. For each "significant" field in the class, check if that field of the argument mathes the corresponding field of this object. 对于类中的每个重要 字段 都要检查参数的字段和对象中相应的字段是否匹配。
  • 对于基本数据类型,除了double & float,使用==比较两个字段的值
  • 对于引用数据类型,使用equals()方法标胶
  • 对于double & float,使用Double.compare(double, double) or Float.compare(float, float)
  • 对于数组,将这些准则应用于数组中的每个元素。
  • 如果某些引用字段中合法含有null值,使用静态方法Objects.equals(o1, o2)
  1. First compare fields that are more likely to differ, less expensive to compare.比较的顺序会影响equals的性能,所以优先比较那些最可能不同或者比较代价很小的字段。
  2. 吾日三省吾身:这个equals方法具有对称性吗?具有传递性吗?具有一致性吗?
  3. Always override hashCode when you override equals. 重写equals方法之后,一定要重写hashCode方法
  4. Do not try be too clever. 不要让equals方法太聪明。
  5. Do not substitute another type for Object in the equals declaration. 在equals方法的声明中,不要将参数Object替换为其他类型的参数。

Item 11: Always override hashcode when you override equals

重写 equals方法的时候,也要重写 hashCode方法

The contract, adapted from the Object specification.

  • the hashCode method must consistently return the same value when it is invoked repeatedlly
  • the hashCode method must produce the same Integer result if two objects are equal accrodiang to the euqals method
  • If two object are unequal according to the equals method, it is not required that their hashcode method return the same value

write a good hash function

  • A good one tends to produce unequal hash codes to unequal instances
Approach I

steps to get a hash funciton

  1. 声明一个int类型的result
  2. 对于对象中的所有重要属性,一一遍历得到其哈希码
    1. 如果该属性是基本数据类型,使用Type.hashCode(T),其中T为基本类型的包装类
    2. 如果该字段是一个引用数据类型,并且引用对象递归调用equals来比较是否相同,则递归调用其hashCode方法。如果该引用为null,使用0值作为哈希值
    3. 如果字段是一个array,则递归计算其中每个元素的hashCode
  1. 将第二步计算得出的哈希码计算为:
  2. 返回result

编写完哈希函数之后,一定要使用单元测试验证两个相等的对象是否相等。

更加优质的哈希函数可以参阅com.google.common.hash.Hashing[Guava]类。

Approach II

Object类中有一个静态方法hash,可以为任意数量的参数返回一个哈希码。在性能要求不是很高的情况下,可以说调用此函数来重写对象中的hashCode方法

typescript 复制代码
@Override
public int hashCode() {
    return Object.hash(field1, field2, ...);
}
Approach III

如果一个类是不可变的, 并且其哈希码的计算复杂度比较高,可以设法使用懒加载的方式,在首次调用hashCode方法的时候,计算出哈希码,并缓存在对象中。

Some importance tips

  • Do not be tempted to exclude significant fields from the hash code computation to improve performance. 不要省略重要字段的哈希值计算。
  • Do not provide a detailed specification for the value returned by hashCode , so clients can not reasonably depend on it; this give you the flexibility to change it. 不要给hashCode提供具体的规范,应当保持hashCode方法的灵活性。

Item 12: Always override toString

始终重写toString方法

toString的约定规范为:

a concise but informative representation that is easy for a person to read.

  • 提供一个优秀的toString实现可以让你的类更加易于使用和调试。
  • the toString method should return all of the interesting information contained in the objejct.toString方法应该返回对象包含的所有感兴趣的信息。

决定是否为toString的返回值指定格式:

  • 好处
    • 可读性比较好,可以用于输出以及持久化数据对象
    • 编码的时候,可以在字符串与对象之间随意切换
  • 坏处
    • 丢失了灵活性,一旦未来想要重新修改格式,会破坏现有的数据和代码

在静态工具类、枚举类中重写toString是没有意义的,但是可以在抽象类中重写toString方法,子类可以共享父类的公共字符串。

arduino 复制代码
abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    // 抽象类中的抽象方法
    abstract void makeSound();

    // 重写toString方法
    @Override
    public String toString() {
        return "Animal{" +
               "name='" + name + ''' +
               ", sound='" + makeSound() + ''' +
               '}';
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        return "Bark";
    }
}

Item 13 Override clone judiciously

谨慎重写 clone方法

Cloneable接口的目的就是作为对象的一个mixin接口,标识该对象是允许克隆的。

实现Cloneable接口的类就是为了提供一个功能适当的公有clone方法。

一个对象的克隆(clone)应当满足如下的表达式:

  1. x.clone != x
  2. x.cone().getClass() == x.getClass()
  3. x.clone.equals(x)

实现对象拷贝的方式

Approach I Implement Cloneable interface and override clone method

所有实现了Cloneable接口的类都应该覆盖clone方法,并且是公有的方法,他的返回类型为类本身。

  1. 调用super.clone()方法
  2. 修正任何应该修正的域

Approach II-Provide a copy constructor or copy factory

arduino 复制代码
public Yum(Yum yum) { ... } // Copy Constructor

public static Yum newInstance(Yum yum) { ... } // Copy static factory

优点

  • 不依赖域某种有风险的、语言之外的创建机制
  • 不需要强制存收文档规约
  • 不会和final类型的属性发生冲突
  • 不会抛出不必要的受检异常
  • 不需要进行强制类型转换
  • 可以接受接口类型的参数

建议

除了复制数组之外,其他的对象都建议使用拷贝构造器或者静态工厂方法完成对象的拷贝。

Item 14: Consider implementing Comparable

考虑实现 Comparable接口

一个类实现了Comparable接口,就表明他的实例具有内在的排序关系。

实现Comparable接口的好处

  1. 集合中的实例可以方便地进行搜索、排序、计算
  2. 可以和泛型算法(generic algorithm)以及依赖于该接口的集合实现进行协作

需要遵循的规约

  1. 该对象大于、等于、小于指定对象的时候,分别返回一个正整数、零、负整数
  2. 指定对象的类型无法与该对象进行比较的时候,抛出ClassCastException异常
  3. 确保自反性、对称性和传递性

一些比较好的建议

  1. 建议(x.compareTo(y) == 0) == (x.equals(y))
  2. 对于基本数据类型的比较,使用包装类的Box.compare静态方法,避免出错
  3. 如果一个类中多个字段需要比较,需要从最重要的字段开始比较,直到某一项结果的值不为0

TIPS

1)通过 Comparator构建 CompareTo方法

可以在类中构建Comparator静态内部类,然后在CompareTo方法中调用静态方法,即可实现多个字段的比较,例如

arduino 复制代码
// 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);
}

(2)比较两个整数的大小的时候,不要直接相减,这会造成整数的溢出,应该用静态的 compare方法

vbnet 复制代码
// Comparator based on static compare method
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};
相关推荐
技术的探险家5 分钟前
R语言的文件操作
开发语言·后端·golang
violin-wang6 分钟前
SpringBoot的Bean-高级-第三方Bean以及Bean管理
java·spring boot·后端·bean
m0_7482302113 分钟前
从 0 开始实现一个 SpringBoot + Vue 项目
vue.js·spring boot·后端
计算机学姐14 分钟前
基于SpringBoot的健身房管理系统
java·vue.js·spring boot·后端·mysql·spring·mybatis
BinaryBardC1 小时前
Go语言的文件操作
开发语言·后端·golang
bing_1581 小时前
Spring Boot 中使用 ShardingSphere-Proxy
java·spring boot·后端
十二同学啊1 小时前
Spring Boot 整合 Knife4j:打造更优雅的 API 文档
java·spring boot·后端
老猿讲编程1 小时前
详解Rust 中 String 和 str 的用途与区别
开发语言·后端·rust
Q_27437851092 小时前
springboot基于微信小程序的智慧小区管理系统
spring boot·后端·微信小程序
007php0072 小时前
深入了解计算机网络中的路由协议与性能优化
java·开发语言·数据库·后端·python·计算机网络·golang