对于所有对象都通用的方法⭐良好习惯总结(避免踩坑)

2024-07-25 08:59:34 浏览数 (3)


对于所有对象都通用的方法⭐良好习惯总结(避免踩坑)

Object 是每个类的父类,它提供一些非final方法:equals、hashCode、clone、toString、finalize...

这些方法在设计上是可以被子类重写的,但是重写前需要遵守相关的规定,否则在使用时就可能踩坑

为了避免业务开发踩坑,本文基于Effective Java中第三章节汇总出对于所有对象都通用方法的好习惯(文末附案例地址)

finalize方法上篇文章已经描述就不再讨论

思维导图如下:

image.pngimage.png
1.重写equals的通用规定

equals是Object中提供比较对象逻辑相等的方法

在Object中equals方法比较对象引用地址是否相同,相同则返回true

代码语言:java复制
public boolean equals(Object obj) {
    return (this == obj);
}

如果想让对象逻辑相等,则可以重写equals方法

但在重写equals方法前需要遵守一些规定:

  1. 自反性:x.equals(x)需要返回true
  2. 对称性:x.equals(y)返回true,那么y.equals(x)也要返回true
  3. 传递性:x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也要true
  4. 一致性:x.equals(y)返回true,只要xy都没被修改多次执行都返回true
  5. 非null的x: x.equals(null) 要返回 false
  6. 重写equals必须重写hashCode

如果要实现equals,通用情况可以使用以下总结:

  1. 先判断对象的引用地址是否相等,相等则返回true
  2. 判断两个对象是否为相同类型,不同类型则返回false
  3. 转换成相同类型后根据规定逻辑相等的关键字段进行比较,相等返回true

比如String中的equals就是这样重写的:

代码语言:java复制
public boolean equals(Object anObject) {
    //1.判断对象的引用地址是否相等
    if (this == anObject) {
        return true;
    }
    
    //2.判断两个对象是否为相同类型
    if (anObject instanceof String) {
        //3.转换成相同类型后根据规定逻辑相等的关键字段进行比较
        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

重写hashCode也要根据逻辑相等的关键字段进行,能够根据关键字段充分打乱哈希码

如果不遵循约定,那么在使用哈希表的数据结构时可能出现严重的问题

并且使用哈希表时,Key最好是不可变对象如String,或者保证哈希码不变

2.始终要重写toString

在Object的toString中返回:全限定类名 @ 哈希码的十六进制

代码语言:java复制
public String toString() {
    return getClass().getName()   "@"   Integer.toHexString(hashCode());
}

使用起来十分不方便,不好调试,查看对象信息

因此最好对其进行重写,返回容易阅读、有用的对象信息

3.谨慎重写clone

clone方法提供克隆一个新的对象,重写时使用super.clone()进行克隆

clone方法坑多,重写时需要谨慎

如果重写clone需要实现Cloneable接口(该接口是一个空接口)否则就会抛出不支持克隆的运行时异常(这是Cloneable设计上的缺陷)

代码语言:java复制
protected native Object clone() throws CloneNotSupportedException;

在clone的重写时,super.clone() 使用的是浅拷贝,如果字段存在对象,想要深拷贝对象,则对象也要重写clone方法

代码语言:java复制
    class CloneObject implements Cloneable {
            private int num;
            private CloneA cloneA = new CloneA(99);
    
            @Override
            protected CloneObject clone() throws CloneNotSupportedException {
                CloneObject res = (CloneObject) super.clone();
                //深拷贝:CloneA也要重写clone实现Cloneable
                res.cloneA = cloneA.clone();
                return res;
            }   
    }

如果字段是final的,则无法使用深拷贝

因为深拷贝时还需要去调用clone进行赋值:res.cloneA = cloneA.clone();

一个实体类携带克隆的方法,耦合性较高,违反单一职责

4.考虑实现Comparable接口

有的对象如果你需要对它进行排序,那么可以实现Comparable接口来进行排序,然后使用一些排序工具如:Arrays.sort

它是一个泛型接口,可以指定需要排序的类型,实现compareTo 负数为小于、正数为大于、零为等于

与其相似功能的另一个接口Comparator是外部比较器,常用于外部排序

  1. Comparator 外部比较器优先Comparable 内部比较器

有时候在一些容器中会需要排序,如果没提供外部比较器也没有实现内部比较器,会导致转换失败抛出异常

如红黑树实现的TreeMap中:

代码语言:java复制
Comparator<? super K> cpr = comparator;
//优先外部排序器
if (cpr != null) {
    //小于去左子树寻找、大于去右子树寻找、相等替换
    do {
        parent = t;
        cmp = cpr.compare(key, t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);
}
else {
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    //如果未实现内部排序器 则抛出异常
    Comparable<? super K> k = (Comparable<? super K>) key;
    do {
        parent = t;
        cmp = k.compareTo(t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);
}
  1. 使用某些需要排序的容器(TreeMap 红黑树)时,如果不实现比较器在转换时会发生异常
  2. 实现排序时,根据多个关键字段从重要程度依次排序,基本类型可以使用包装类的compare方法

比如需要按照学生年龄排序,那么可以先比较age,age相等再比较day

代码语言:java复制
public int compareTo(Student o) {
    // int res = this.age - o.age;
    // 使用包装类的compare
    int res = Integer.compare(this.age, o.age);

    if (0 == res) {
        return Integer.compare(this.day, o.day);
    }

    return res;
}
  1. 外部比较器还提供lambda表达式构造Comparator
代码语言:java复制
TreeSet<Student> students = new TreeSet<>(
        //先比较age再比较day
        Comparator
                .comparingInt(Student::getAge)
                .thenComparingInt(Student::getDay)
);
总结

equals表示逻辑相等,当需要判断对象逻辑相等时重写equals方法

重写equals通用方案一般为先判断对象引用是否相等,再判断对象是否为同类型,为同类型再根据关键字段进行比较

重写equals需要根据根据逻辑相等的字段重写hashCode,否则在使用哈希表实现的数据结构时会出现严重问题

使用哈希表时Key最好为不可变对象,或让对象的hashCode不会随着字段值改变,否则会出现严重问题

始终要重写toString,输出关键字段信息,方便阅读、调试

谨慎重写clone,clone用于对象的克隆,在设计上并不太好还存在一些缺点:

  1. 重写clone需要实现Cloneable空接口,否则会抛出 CloneNotSupportedException 异常
  2. 调用 super.clone 实现的是浅拷贝,如果要实现深拷贝,字段中的类也需要重写clone方法
  3. 如果字段是final的则无法实现深拷贝
  4. 实体类携带克隆方法,耦合性较高,违法单一职责

对于需要排序的对象,考虑实现Comparable或Comparator接口:

  1. Comparator 外部比较器一般优先 Comparable 内部比较器
  2. 使用某些需要排序的容器时(红黑树 TreeMap),如果不实现比较器在转换时会发生异常
  3. 实现排序时,根据多个关键字段重要程度进行排序,基本类型可以使用包装类的compare方法
  4. 外部排序器提供lambda表达式构造Comparator外部比较器
最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

0 人点赞