对于所有对象都通用的方法⭐良好习惯总结(避免踩坑)
Object 是每个类的父类,它提供一些非final方法:equals、hashCode、clone、toString、finalize...
这些方法在设计上是可以被子类重写的,但是重写前需要遵守相关的规定,否则在使用时就可能踩坑
为了避免业务开发踩坑,本文基于Effective Java中第三章节汇总出对于所有对象都通用方法的好习惯(文末附案例地址)
finalize方法上篇文章已经描述就不再讨论
思维导图如下:
1.重写equals的通用规定
equals是Object中提供比较对象逻辑相等的方法
在Object中equals方法比较对象引用地址是否相同,相同则返回true
public boolean equals(Object obj) { return (this == obj); }
如果想让对象逻辑相等,则可以重写equals方法
但在重写equals方法前需要遵守一些规定:
- 自反性:x.equals(x)需要返回true
- 对称性:x.equals(y)返回true,那么y.equals(x)也要返回true
- 传递性:x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也要true
- 一致性:x.equals(y)返回true,只要x\y都没被修改多次执行都返回true
- 非null的x: x.equals(null) 要返回 false
- 重写equals必须重写hashCode
如果要实现equals,通用情况可以使用以下总结:
- 先判断对象的引用地址是否相等,相等则返回true
- 判断两个对象是否为相同类型,不同类型则返回false
- 转换成相同类型后根据规定逻辑相等的关键字段进行比较,相等返回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方法坑多,重写时需要谨慎
- 如果重写clone需要实现Cloneable接口(该接口是一个空接口)否则就会抛出不支持克隆的运行时异常(这是Cloneable设计上的缺陷)
protected native Object clone() throws CloneNotSupportedException;
- 在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; } }
- 如果字段是final的,则无法使用深拷贝
因为深拷贝时还需要去调用clone进行赋值:res.cloneA = cloneA.clone();
- 一个实体类携带克隆的方法,耦合性较高,违反单一职责
4.考虑实现Comparable接口
有的对象如果你需要对它进行排序,那么可以实现Comparable接口来进行排序,然后使用一些排序工具如:Arrays.sort
它是一个泛型接口,可以指定需要排序的类型,实现compareTo 负数为小于、正数为大于、零为等于
与其相似功能的另一个接口Comparator是外部比较器,常用于外部排序
- 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); }
- 使用某些需要排序的容器(TreeMap 红黑树)时,如果不实现比较器在转换时会发生异常
- 实现排序时,根据多个关键字段从重要程度依次排序,基本类型可以使用包装类的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; }
- 外部比较器还提供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用于对象的克隆,在设计上并不太好还存在一些缺点:
- 重写clone需要实现Cloneable空接口,否则会抛出
CloneNotSupportedException
异常 - 调用
super.clone
实现的是浅拷贝,如果要实现深拷贝,字段中的类也需要重写clone方法 - 如果字段是final的则无法实现深拷贝
- 实体类携带克隆方法,耦合性较高,违法单一职责
对于需要排序的对象,考虑实现Comparable或Comparator接口:
- Comparator 外部比较器一般优先 Comparable 内部比较器
- 使用某些需要排序的容器时(红黑树 TreeMap),如果不实现比较器在转换时会发生异常
- 实现排序时,根据多个关键字段重要程度进行排序,基本类型可以使用包装类的compare方法
- 外部排序器提供lambda表达式构造Comparator外部比较器
最后(不要白嫖,一键三连求求拉~)
本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜