快速带你看完《Effective Java》—— 对象通用方法篇

上面的三个实例中,p1.equals(p2)p2.equals(p3)都是true,但p1.equals(p3)返回的却是false。这里就引出了面向对象语言中关于等价关系的一个基本问题:无法在继承可实例化类的同时,即增加新的类属性,同时又保留equals约定。好在是有一个其他的方案,即遵循复合优先于继承的原则:

public class ColorPoint {

private final Point point;

private final Color color;

public ColorPoint(int x, int y, Color color){

point = new Point(x, y);

this.color = color;

}

public Point asPoint(){

return point;

}

@Override

public boolean equals(Object o) {

if (!(o instanceof ColorPoint))

return false;

ColorPoint cp = (ColorPoint) o;

return cp.point.equals(point) && cp.color.equals(color);

}

}

此时ColorPoint就不继承Point了,而是在类的定义里添加一个Point属性,并提供getter方法

此时由于没有了继承关系,所以Point instanceof ColorPointColorPoint instanceof Point结果都是false,所以在前面对称性和传递性的例子里都返回false,相应的也满足了这两条性质

Java标准类库里甚至也有违反对称性的例子,java.sql.Timstampjava.util.Date扩展了一个属性,所以也违反了对称性,Timstamp和Date混用时会出现错误,当然这一点也被作者狠狠吐槽,我们不要模仿 ~~

  • 如果类是abstract的,我们可以在其子类李加新属性且不违反equals约定

只要不能创建超类的实例,前面的问题就没有。

4. 一致性:若x != nully != null,只要对象没有被修改,多次执行x.equals(y)都是相同的结果

equals方法禁止依赖于不可靠的资源,否则就会违反一致性。

例如:java.net.URL的equals方法依赖于对URL中主机IP地址的比较。将一个主机名转变成IP地址可能会有不同的结果,这也是一个大错误不值得提倡!

5. 非空性:若x != nullx.equals(null)=false

为了避免抛出空指针异常,通常会有这样的写法:

@Override

public boolean equals(Object o) {

if (o == null)

return false;

}

但实际上更好地写法是:

@Override

public boolean equals(Object o) {

if (!(o instanceof Type))

return false;

Type t = (Type) o;

}

这样既可以类型检查,也可以保证传入null时返回false


综上所述,实现一个高质量的equals分以下四步即可:

  1. 用==检查入参是否是本对象的引用

  2. 用instanceof检查参数类型

  3. 将参数转换成正确的类型

  4. 对于类中的每个属性,检查入参的每个属性和自身是否匹配

  5. 不比较不属于对象逻辑状态的属性,比如保证同步的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框架。它可以通过注解自动生成这两个方法。

注意:

  1. 覆盖equals时还要覆盖hashCode

  2. equals的逻辑不要太复杂

  3. equals的入参类型只能是Object

11 覆盖equals时总要覆盖hashCode


这是一个老生常谈的话题,没有覆盖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计算方法:

  1. 初始化int result = c,其中c是对象中第一个关键属性的散列码

  2. 对于剩下的每一个属性f,循环进行以下步骤:

a. 为该属性计算散列码c

① 如果是基本数据类型,直接计算hashCode(f)

② 如果是引用,则针对这个属性递归调用hashCode

③ 如果是数组,对其中的元素单独求hash code

b. 按照如下公式将上面算出来的c合并的result里

  1. 最终的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是尝试排除掉类的任一个属性,否则在某些场景下会以平方级的时间运行。

12 始终要覆盖toString


当对象传递给printlnprintf、字符串联接操作符+,assert或者被调试器打印出来时,toString方法会被自动调用。

所以提供好的toString实现可以使类用起来更舒服,也更容易调试。

例如有一个电话号码类PhoneNumber,没有覆盖和覆盖了toString方法分别会输出:

PhoneNumber@163b91

18373xxxxxx

我们肯定希望看到后面一种输出

对此,阿里巴巴开发规约里面也专门做了要求:

在这里插入图片描述


在实现toString时,还要决定是否要返回指定的格式,比如对于一般的类而言,使用json格式输出就很直观;但是对于一些特殊的类,比如电话号码类,它是有自己固定的格式的

@Override

public String toString(){

return String.format(“%03d-%03d-%04d”, areaCode, prefix, lineNum);

}

注意点:

  1. 无论是否指定格式,toString中返回的所有属性都应该有一个getter方法

  2. 如果很多子类都是同一个字符串表示法,在其抽象类里一定要写一个toString

比如大多数集合实现的toString都是继承自抽象集合类

13 谨慎地覆盖clone


这里主要讨论何时,以及如何实现一个较好的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接口

14 考虑实现Comparable接口


这一条主要说明实现了Comparable接口后可以获得非常强大的功能。

比如下面一段代码就实现了args数组去重并打印的功能:

public class WordList {

Set s = new TreeSet<>();

Collections.addAll(s, args);

System.out.println(s);

}

可以看到String类实现了Comparable接口后上面的代码量其实非常少。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

分享一套我整理的面试干货,这份文档结合了我多年的面试官经验,站在面试官的角度来告诉你,面试官提的那些问题他最想听到你给他的回答是什么,分享出来帮助那些对前途感到迷茫的朋友。

面试经验技巧篇
  • 经验技巧1 如何巧妙地回答面试官的问题
  • 经验技巧2 如何回答技术性的问题
  • 经验技巧3 如何回答非技术性问题
  • 经验技巧4 如何回答快速估算类问题
  • 经验技巧5 如何回答算法设计问题
  • 经验技巧6 如何回答系统设计题
  • 经验技巧7 如何解决求职中的时间冲突问题
  • 经验技巧8 如果面试问题曾经遇见过,是否要告知面试官
  • 经验技巧9 在被企业拒绝后是否可以再申请
  • 经验技巧10 如何应对自己不会回答的问题
  • 经验技巧11 如何应对面试官的“激将法”语言
  • 经验技巧12 如何处理与面试官持不同观点这个问题
  • 经验技巧13 什么是职场暗语

面试真题篇
  • 真题详解1 某知名互联网下载服务提供商软件工程师笔试题
  • 真题详解2 某知名社交平台软件工程师笔试题
  • 真题详解3 某知名安全软件服务提供商软件工程师笔试题
  • 真题详解4 某知名互联网金融企业软件工程师笔试题
  • 真题详解5 某知名搜索引擎提供商软件工程师笔试题
  • 真题详解6 某初创公司软件工程师笔试题
  • 真题详解7 某知名游戏软件开发公司软件工程师笔试题
  • 真题详解8 某知名电子商务公司软件工程师笔试题
  • 真题详解9 某顶级生活消费类网站软件工程师笔试题
  • 真题详解10 某知名门户网站软件工程师笔试题
  • 真题详解11 某知名互联网金融企业软件工程师笔试题
  • 真题详解12 国内某知名网络设备提供商软件工程师笔试题
  • 真题详解13 国内某顶级手机制造商软件工程师笔试题
  • 真题详解14 某顶级大数据综合服务提供商软件工程师笔试题
  • 真题详解15 某著名社交类上市公司软件工程师笔试题
  • 真题详解16 某知名互联网公司软件工程师笔试题
  • 真题详解17 某知名网络安全公司校园招聘技术类笔试题
  • 真题详解18 某知名互联网游戏公司校园招聘运维开发岗笔试题

资料整理不易,点个关注再走吧
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
如何回答非技术性问题

  • 经验技巧4 如何回答快速估算类问题
  • 经验技巧5 如何回答算法设计问题
  • 经验技巧6 如何回答系统设计题
  • 经验技巧7 如何解决求职中的时间冲突问题
  • 经验技巧8 如果面试问题曾经遇见过,是否要告知面试官
  • 经验技巧9 在被企业拒绝后是否可以再申请
  • 经验技巧10 如何应对自己不会回答的问题
  • 经验技巧11 如何应对面试官的“激将法”语言
  • 经验技巧12 如何处理与面试官持不同观点这个问题
  • 经验技巧13 什么是职场暗语

[外链图片转存中…(img-rlVTNpxv-1713684819994)]

面试真题篇
  • 真题详解1 某知名互联网下载服务提供商软件工程师笔试题
  • 真题详解2 某知名社交平台软件工程师笔试题
  • 真题详解3 某知名安全软件服务提供商软件工程师笔试题
  • 真题详解4 某知名互联网金融企业软件工程师笔试题
  • 真题详解5 某知名搜索引擎提供商软件工程师笔试题
  • 真题详解6 某初创公司软件工程师笔试题
  • 真题详解7 某知名游戏软件开发公司软件工程师笔试题
  • 真题详解8 某知名电子商务公司软件工程师笔试题
  • 真题详解9 某顶级生活消费类网站软件工程师笔试题
  • 真题详解10 某知名门户网站软件工程师笔试题
  • 真题详解11 某知名互联网金融企业软件工程师笔试题
  • 真题详解12 国内某知名网络设备提供商软件工程师笔试题
  • 真题详解13 国内某顶级手机制造商软件工程师笔试题
  • 真题详解14 某顶级大数据综合服务提供商软件工程师笔试题
  • 真题详解15 某著名社交类上市公司软件工程师笔试题
  • 真题详解16 某知名互联网公司软件工程师笔试题
  • 真题详解17 某知名网络安全公司校园招聘技术类笔试题
  • 真题详解18 某知名互联网游戏公司校园招聘运维开发岗笔试题

[外链图片转存中…(img-qFYqX0Yy-1713684819995)]

资料整理不易,点个关注再走吧
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值