但实际上更好地写法是:
@Override
public boolean equals(Object o) {
if (!(o instanceof Type))
return false;
Type t = (Type) o;
…
}
这样既可以类型检查,也可以保证传入null时返回false
综上所述,实现一个高质量的equals分以下四步即可:
-
用==检查入参是否是本对象的引用
-
用instanceof检查参数类型
-
将参数转换成正确的类型
-
对于类中的每个属性,检查入参的每个属性和自身是否匹配
-
不比较不属于对象逻辑状态的属性,比如保证同步的Lock(这种情况比较少)
有些对象的引用属性中包含null可能是合法的,为了避免出现空指针,建议使用
Objects.equals(Object, Object)
来检查是否相同
一个标准的equals示例:
public class PhoneNumber {
private final int areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum){
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNum = lineNum;
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
}
}
代替手工编写和测试equals和hashCode方法的最佳途径是使用Google开源的AutoValue框架。它可以通过注解自动生成这两个方法。
注意:
-
覆盖equals时还要覆盖hashCode
-
equals的逻辑不要太复杂
-
equals的入参类型只能是Object
这是一个老生常谈的话题,没有覆盖hashCode违反的是相等的对象必须具有相等的hash code这一约定。
例如下面的代码:
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), “Jenny”);
如果没有覆盖hashCode方法,m.get(new PhoneNumber(707, 867, 5309))
会返回null,这里涉及到两个PhoneNumber对象,因为在put和get操作里都使用new关键字,因为没有覆盖hashCode方法,所以这两个实例有不同的hash code,所以返回结果是null
即使这两个实例正好被放到同一个桶中,get方法返回的也是null,因为HashMap会将每一项的hash code缓存起来,如果code不匹配则不会去检查对象的等同性
下面给出一种简单且有效的hashCode计算方法:
-
初始化
int result = c
,其中c是对象中第一个关键属性的散列码 -
对于剩下的每一个属性f,循环进行以下步骤:
a. 为该属性计算散列码c
① 如果是基本数据类型,直接计算hashCode(f)
② 如果是引用,则针对这个属性递归调用hashCode
③ 如果是数组,对其中的元素单独求hash code
b. 按照如下公式将上面算出来的c合并的result里
- 最终的result就是求出的hash code
result = 31 * result + c;
31有一个很好的特性:
31 * i == (i << 5) - i
,使用移位和减法可以代替乘法,在硬件层面上执行速度更快
上面给出的hashCode实现方法可以满足日常需要,如果执意不想让散列函数有冲突,可以使用Guava的com.google.common.hash.Hashing
如果是不太注重性能的情况,可以在上面定义的PhoneNumber类里使用下面这种简单的实现方式:
@Override
public int hashCode(){
return Objects.hash(lineNum, prefix, areaCode);
}
如果计算hash code的开销较大的话,可以考虑使用缓存 + 延迟加载技术来提高性能:
private int hashCode;
@Override
public int hashCode(){
if(this.hashCode == 0){
// 计算散列码的逻辑
this.hashCode = XXX;
}
return this.hashCode;
}
最后要提醒注意一点:不要在计算hash code是尝试排除掉类的任一个属性,否则在某些场景下会以平方级的时间运行。
当对象传递给println
、printf
、字符串联接操作符+,assert
或者被调试器打印出来时,toString
方法会被自动调用。
所以提供好的toString实现可以使类用起来更舒服,也更容易调试。
例如有一个电话号码类PhoneNumber
,没有覆盖和覆盖了toString方法分别会输出:
PhoneNumber@163b91
18373xxxxxx
我们肯定希望看到后面一种输出
对此,阿里巴巴开发规约里面也专门做了要求:
在实现toString时,还要决定是否要返回指定的格式,比如对于一般的类而言,使用json格式输出就很直观;但是对于一些特殊的类,比如电话号码类,它是有自己固定的格式的
@Override
public String toString(){
return String.format(“%03d-%03d-%04d”, areaCode, prefix, lineNum);
}
注意点:
-
无论是否指定格式,toString中返回的所有属性都应该有一个getter方法
-
如果很多子类都是同一个字符串表示法,在其抽象类里一定要写一个toString
比如大多数集合实现的toString都是继承自抽象集合类
这里主要讨论何时,以及如何实现一个较好的clone方法。
Java的Cloneable
接口没有任何方法:如果一个类实现了Cloneable
,Object的clone方法就返回该对象的拷贝,否则会抛CloneNotSupportedException
异常。
这是接口一种不常用的用法,依据子类是否实现这个接口来决定返回什么。不值得提倡!
我们实现Cloneable接口的目的就是为了提供一个public的clone方法(clone方法来自于Object类,
实现了这个接口也意味着要提供一个合适的clone方法),此外,不可变的类不应该提供clone方法(鼓励尽可能复现现有实例)。
由于类里面定义的属性可能是变的或不变的,所以还要分情况讨论:
1. 如果每个属性都是基本数据类型或指向一个不可变对象的引用,这种情况不需要特殊处理。
例如之前的电话号码类PhoneNumber:
@Override
public PhoneNumber clone(){
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
2. 如果对象中的域引用了可变对象,用上面简单的clone会产生问题。
如果要对一个栈做克隆操作:
package com.wjw;
import java.util.Arrays;
import java.util.EmptyStackException;
/**
-
2 * @Author: 小王同学
-
3 * @Date: 2021/11/23 20:50
-
4
*/
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size ++] = e;
}
public Object pop(){
if (size == 0)
throw new EmptyStackException();
Object result = elements[-- size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
如果仅仅返回super.clone()
,这样得到的新Stack实例的elements属性与原来的Stack指向的其实是同一个数组,这两个对象任意一个修改了elements都会影响另外一个。解决方案是使用深拷贝技术,递归地拷贝栈的内部信息:
@Override
public Stack clone(){
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
如果elements被final修饰,上面的代码会报错,clone方法禁止给final域赋新值。
递归调用clone了可能还不够,比如HashTable
这种引用套引用再套引用的数据结构:
package com.wjw;
public class HashTable implements Cloneable{
private Entry[] buckets = new Entry[10];
private static class Entry{
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next){
this.key = key;
this.value = value;
this.next = next;
}
}
}
如果向上面clone栈一样,仅仅递归地克隆buckets
@Override
public HashTable clone(){
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
这样一来,这个数组引用的链表与原始对象又是一样的了,继续出现上面的问题。
为了解决这个问题,必须单独拷贝buckets每个位置的链表:
package com.wjw;
public class HashTable implements Cloneable{
private Entry[] buckets = new Entry[10];
private static class Entry{
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next){
this.key = key;
this.value = value;
this.next = next;
}
Entry deepCopy(){
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
@Override
public HashTable clone(){
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++) {
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
}
return result;
} catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
}
这样一来,HashTable的clone方法重新new了一个新的buckets数组,并且遍历原先的buckets,对其中的非空位置进行深度的拷贝。
需要注意的点:
1. public的clone方法应该省略throws声明
Object的clone方法声明时抛了一个异常
我们覆盖clone之后可以忽略这个声明(继承后,不能抛出比父类更多的异常)
2. 如果是线程安全的类要实现Cloneable接口,它的clone方法必须保证严格的同步。
Object的clone方法没有同步,所以必须实现synchronized clone()
方法来调用super.clone()
3. 对象拷贝的更好方法是实现一个 拷贝构造器 或 拷贝工厂
public Yum(Yum yum){
this.属性 = yum.属性;
}
或
public static Yum newInstance(Yum yum){
…
}
这样做有很多优势:
a. 不依赖于语言之外的对象创建机制;
b. 不会与final域发生冲突;
c. 不会抛不必要的异常;
d. 不需要进行类型转换。
假设希望将一个HashSet对象s拷贝成TreeSet对象,clone方法无法完成,但用构造器就容易实现:
new TreeSet<>(s)
综上所述:除了数组最好用clone方法复制之外,其余的类不要实现Cloneable接口
这一条主要说明实现了Comparable接口后可以获得非常强大的功能。
比如下面一段代码就实现了args数组去重并打印的功能:
public class WordList {
Set s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
可以看到String类实现了Comparable接口后上面的代码量其实非常少。
如果一个类具有按照字母、数字、年代等排序的需求,就强烈建议实现这个接口
compareTo方法的约束与之前的equals方法的约束类似,这里需要借助数学里面的sgn函数:
-
sgn(x.compareTo(y)) == - sgn(y.compareTo(x))
-
如果
x.compareTo(y) > 0 && y.compareTo(z) > 0
,则有x.compareTo(z) > 0
-
如果
x.compareTo(y) == 0
,则对于任意的z
,sgn(x.compareTo(z)) == sgn(y.compareTo(z))
-
最后一条是一个建议,最好满足
x.compareTo(y) == 0
的结果和x.equals(y)
相同
compareTo和equals的限制也类似,子类继承父类并扩展了新的属性时,同时保持compareTo约定,所以上面equals章节里的解决方案同时也适用于这里。
最后一条是一个强烈的建议,例如Java里面有一个BigDecimal
类,它的compareTo与equals方法不一致。如果创建两个对象new BigDecimal("1.0")
和new BigDecimal("1.00")
,并添加到HashSet
里,这个集合里面将会有两个元素,因为这两个对象通过equals方法比较结果是不同的。但将他们添加到TreeSet
里,这个集合里将只包含一个元素,这又是因为这两个BigDecimal
实例在通过compareTo方法进行比较时是相等的。
需要注意的是,不要在compareTo方法中使用操作符<
,>
下面介绍Comparator
接口配置比较器构造方法,这样做会使得构造工作变得流畅,还是以手机号码的比较为例:
private static final Comparator COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
comparingInt
返回一个按areaCode
对手机号进行排序的Comaprator<PhoneNumber>
,如果两个手机号的areaCode
相同,需要进行prefix
和lineNum
的比较。
如果要比较两个hashCode值的话,千万不要使用减法运算符:
static Comparator hashCodeOrder = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
总结
总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。
如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-rGVuZdSH-1713684782115)]
[外链图片转存中…(img-I8DZjsZf-1713684782116)]
[外链图片转存中…(img-17tRZtde-1713684782116)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
总结
总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。
如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。
[外链图片转存中…(img-LoFXqUi3-1713684782116)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!