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

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

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

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

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

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

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

思维导图如下:

image.png

1.重写equals的通用规定

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

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

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,只要x\y都没被修改多次执行都返回true
  5. 非null的x: x.equals(null) 要返回 false
  6. 重写equals必须重写hashCode

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

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

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

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中返回:全限定类名 + @ + 哈希码的十六进制

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

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

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

3.谨慎重写clone

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

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

  1. 如果重写clone需要实现Cloneable接口(该接口是一个空接口)否则就会抛出不支持克隆的运行时异常(这是Cloneable设计上的缺陷)
protected native Object clone() throws CloneNotSupportedException;
  1. 在clone的重写时,super.clone() 使用的是浅拷贝,如果字段存在对象,想要深拷贝对象,则对象也要重写clone方法
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;
        }   
}
  1. 如果字段是final的,则无法使用深拷贝
    因为深拷贝时还需要去调用clone进行赋值:res.cloneA = cloneA.clone();
  2. 一个实体类携带克隆的方法,耦合性较高,违反单一职责

4.考虑实现Comparable接口

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

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

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

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

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

如红黑树实现的TreeMap中:

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

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
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-CaiCaiJavaGithub-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~

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

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

相关文章
|
5天前
|
前端开发
前端知识笔记(四)———深浅拷贝的区别,如何实现?
前端知识笔记(四)———深浅拷贝的区别,如何实现?
32 0
|
5天前
|
前端开发 数据处理
【前端学习】—多种方式实现数组拍平(十一)
【前端学习】—多种方式实现数组拍平(十一)
|
9月前
|
前端开发
前端学习案例8-数组上的方法总结8 原创
前端学习案例8-数组上的方法总结8 原创
31 0
|
12月前
|
存储 机器学习/深度学习 Rust
Rust 快速入门60分① 看完这篇就能写代码了
Rust 快速入门60分① 看完这篇就能写代码了
336 1
|
前端开发
前端学习案例7-数组上的方法总结7
前端学习案例7-数组上的方法总结7
31 0
前端学习案例7-数组上的方法总结7
|
前端开发
前端学习案例2-原型面试题2 原
前端学习案例2-原型面试题2 原
45 0
前端学习案例2-原型面试题2 原
|
前端开发
前端学习案例1-原型面试题1 原
前端学习案例1-原型面试题1 原
53 0
前端学习案例1-原型面试题1 原
|
JavaScript 前端开发
【重温基础】12.使用对象 上
【重温基础】12.使用对象 上
136 0
|
网络架构
【重温基础】12.使用对象 下
【重温基础】12.使用对象 下
90 0
|
网络架构 iOS开发