effective Java 学习笔记(第二弹)

news/2025/3/31 1:50:44/

effective Java 学习笔记(第一弹)

整理自《effective Java 中文第3版》

本篇笔记整理第3,4章的内容。

重写equals方法需要注意的地方

  1. 自反性:对于任何非空引用 x,x.equals(x) 必须返回 true。
  2. 对称性:对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true。
  3. 传递性:对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,则 x.equals(z) 必须返回 true。
  4. 一致性:对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用必须始终返回 true 或始终返回 false。
  5. 对于任何非空引用 x,x.equals(null) 必须返回 false。

在equals方法声明中,不要将参数Object替换成其他类型!可以用instanceof来判断类型是否一致。
重写equals方法时同时也要重写hashcode方法。相等的对象必须要具有相等的哈希码(hash code)。
@EqualsAndHashCode(callSuper = false) 是 Lombok 库中的一个注解,用于自动生成 equals 和 hashCode 方法。这个注解可以帮助开发者减少样板代码的编写。
callSuper = false 参数表示在生成的 equals 和 hashCode 方法中不调用父类的 equals 和 hashCode 方法。这意味着生成的方法将仅基于当前类的字段来实现相等性和哈希值的计算。这样可以确保子类在继承父类时,不会因为父类的 equals 和 hashCode 方法而影响子类的行为。

始终重写toString方法

虽然Object类提供了toString方法的实现,但它返回的字符串是它由类名后跟一个「at」符号(@)和哈希码的无符号十六进制表示组成。若想输出需要的易懂的内容,需要重写。

@Data 注解是 Lombok 库提供的一个注解,用于简化 Java 类的编写。使用 @Data 注解后,Lombok 会自动生成以下内容:

  • 生成 getter 和 setter 方法:为类中的所有字段自动生成 getter 和 setter 方法。
  • 生成 toString 方法:为类生成 toString 方法,包含所有字段的值。
  • 生成 equals 和 hashCode 方法:为类生成 equals 和 hashCode 方法,基于所有字段。
  • 生成 toString 方法:为类生成 toString 方法,包含所有字段的值。
  • 生成无参构造函数:为类生成一个无参构造函数。
  • 生成全参构造函数:为类生成一个包含所有字段的全参构造函数。
java">import lombok.Data;@Data
public class User {private String name;private int age;private String email;
}

使用 @Data 注解后,Lombok 会为 User 类生成以下内容:

public String getName()
public void setName(String name)
public int getAge()
public void setAge(int age)
public String getEmail()
public void setEmail(String email)
public String toString()
public boolean equals(Object obj)
public int hashCode()
无参构造函数 public User()
全参构造函数 public User(String name, int age, String email)

考虑实现Comparable接口

无论何时实现具有合理排序的值类,你都应该让该类实现Comparable接口,以便在基于比较的集合中轻松对其实例进行排序,搜索和使用。 比较 compareTo 方法的实现中的字段值时,请避免使用「<」和「>」运算符。 相反,使用包装类中的静态compare方法或Comparator接口中的构建方法。
如下是反例,可能导致整形最大长度移除和IEEE754浮点运算失真危险。

java">static Comparator<Object> hashCodeOrder = new Comparator<>() {public int compare(Object o1, Object o2) {return o1.hashCode() - o2.hashCode();}
};

可以使用如下两种方式替代

java">static Comparator<Object> hashCodeOrder = new Comparator<>() {public int compare(Object o1, Object o2) {return Integer.compare(o1.hashCode(), o2.hashCode());}
};static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

额外话题:IEEE754浮点运算失真
IEEE754浮点运算失真(或精度丢失)是指在使用IEEE754标准表示和计算浮点数时,由于二进制存储和有限位数限制,导致无法精确表示某些十进制小数或运算结果出现微小误差的现象。这是计算机科学中浮点数处理的固有挑战。
核心原因

  1. 二进制与十进制的进制差异
  • 许多十进制小数(如0.1)无法用有限位二进制精确表示(类似1/3无法用有限十进制表示)。
  • 例如,0.1的二进制表示是无限循环小数:0.0001100110011…,存储时会被截断。
  1. IEEE754的存储结构限制
  • 浮点数按三部分存储:符号位、指数位(控制范围)、尾数位(控制精度)。
  • 单精度(32位):1符号位 + 8指数位 + 23尾数位 → 约6-9位有效十进制数字。
  • 双精度(64位):1符号位 + 11指数位 + 52尾数位 → 约15-17位有效十进制数字。
  1. 舍入规则的影响
  • IEEE754默认使用“向最近偶数舍入”(Round to Nearest, Ties to Even),可能导致累积误差。

典型表现

console.log(0.1 + 0.2); // 输出:0.30000000000000004(非精确0.3)
console.log(0.3 - 0.2 === 0.1); // 输出:false
>>> 1.0000000000000001 == 1.0
True  # 双精度无法区分过小的差异

实际影响场景

  • 科学计算:迭代计算中误差累积可能影响结果可靠性。
  • 金融系统:货币计算要求精确到分,浮点误差可能导致账务错误。
  • 游戏物理引擎:微小误差可能引发碰撞检测异常。
    解决方案
  1. 整数替代法
    用整数表示最小单位(如分而不是元):
price_cents = 1000  # 表示10.00元,避免浮点运算
  1. 高精度计算库
    Python:decimal 模块(基于十进制的精确计算):
    from decimal import Decimal
    print(Decimal(‘0.1’) + Decimal(‘0.2’)) # 输出精确0.3
  2. 误差容忍比较
    使用极小值(epsilon)判断近似相等:
function areEqual(a, b, epsilon = 1e-10) {return Math.abs(a - b) < epsilon;
}
  1. 特殊场景处理
    避免超大数与超小数直接相加(会丢失小数部分):
double big = 1e20;
double small = 1.0;
printf("%f\n", big + small - big);  // 输出0.0(small被吞没)

虽然IEEE754的精度问题无法彻底消除,但通过合理的设计(如定点数、符号处理、误差控制)可将其影响降至最低。理解这一机制是开发可靠数值计算程序的关键基础。


使类的成员的可访问性最小化

非零长度的数组总是可变的,所以类具有公共静态 final 数组属性,或返回这样一个属性的访问器是错误的。

如果一个类有这样的属性或访问方法,客户端将能够修改数组的内容。 这是安全漏洞的常见来源:

public static final Thing[] VALUES = { ... };

有两种方法可以解决这个问题。

  1. 可以使公共数组私有并添加一个公共的不可变列表:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
  1. 可以将数组设置为 private,并添加一个返回私有数组拷贝的公共方法:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {return PRIVATE_VALUES.clone();
}

应该尽可能地减少程序元素的可访问性(在合理范围内)。 在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分。 除了作为常量的公共静态 final 属性之外,公共类不应该有公共属性。 确保 public static final 属性引用的对象是不可变的。

我自己也犯过这样的错误,在某个枚举中创建private static final Set的集合,枚举中加了静态的方法返回这个数组,在外层删除这个集合的某个值,这个集合的值就彻底变了。大致代码如下:

java">@Getter
public enum VIPTypeEnum {BRONZE(0,"bronze"),SILVER(1,"silver"),GOLD(2,"gold"),SUPPER(999,"supper"),;private int code;private String desc;VIPTypeEnum(int code, String desc) {this.code = code;this.desc = desc;}private static final Set<VIPTypeEnum> showSet = Sets.newHashSet(BRONZE,SILVER,GOLD);public static Set<VIPTypeEnum> getShowSet(){return showSet;}
}
java">public static void main(String[] args)  {Set<VIPTypeEnum> showSet = VIPTypeEnum.getShowSet();System.out.println(showSet);showSet.remove(VIPTypeEnum.SILVER);System.out.println(VIPTypeEnum.getShowSet());
}

输出如下:

[BRONZE, GOLD, SILVER]
[BRONZE, GOLD]

使用static final修饰的成员变量值改变了。上面说的书中的方法也是可以的:
修改为:

java">    public static Set<VIPTypeEnum> getShowSet(){return Collections.unmodifiableSet(showSet);}

那么运行main方法,会抛出异常:

[BRONZE, GOLD, SILVER]
Exception in thread "main" 与目标 VM 断开连接, 地址为: ''127.0.0.1:52720',传输: '套接字''
java.lang.UnsupportedOperationExceptionat java.util.Collections$UnmodifiableCollection.remove(Collections.java:1058)at com.example.demo.DemoApplication.main(DemoApplication.java:121)

书中的第二种方法改后会报错:‘clone()’ 在 ‘java.lang.Object’ 中具有 protected 访问权限,因为Set接口并没有定义clone()方法。这通常会导致编译错误,提示无法找到符号或类似的问题。为了解决这个问题,我们可以使用其他方式来复制集合,例如通过构造函数创建一个新的HashSet实例。

java">    public static Set<VIPTypeEnum> getShowSet(){return new HashSet<>(showSet);}public static void main(String[] args)  {Set<VIPTypeEnum> showSet = VIPTypeEnum.getShowSet();System.out.println(showSet);showSet.remove(VIPTypeEnum.GOLD);System.out.println(showSet);System.out.println(VIPTypeEnum.getShowSet());}

输出结果:

[BRONZE, GOLD, SILVER]
[BRONZE, SILVER]
[BRONZE, GOLD, SILVER]

关于深、浅拷贝的操作,见:Java 对实例进行深拷贝操作

最小化可变性

“最小化可变性”强调设计不可变类(Immutable Class)的重要性。不可变类的实例一旦创建,状态就不可修改,这能显著提升代码的线程安全性、可维护性和可靠性。以下是关键原则及示例:

核心原则

  1. 不提供修改状态的方法(Mutators)
  • 如 setXxx() 方法,禁止直接修改对象属性。
  1. 确保类不可被继承
  • 避免子类破坏不可变性,通常用 final 修饰类或私有化构造函数。
  1. 保护对可变组件的访问
  • 如果类持有可变对象(如数组、集合),需防御性拷贝(Defensive Copy),避免外部修改影响内部状态。

示例1:简单的不可变类

java">public final class ImmutablePoint {private final int x;private final int y;public ImmutablePoint(int x, int y) {this.x = x;this.y = y;}// 只有getter,没有setterpublic int getX() { return x; }public int getY() { return y; }
}

不可变性体现:

  • x 和 y 被声明为 final,只能在构造函数中初始化。
  • 没有提供修改字段的方法(如 setX())。
  • 类为 final,不允许子类覆盖行为。

示例2:处理深层次可变对象
若类中包含可变对象(如 Date、数组),需确保外部无法修改其内部状态:

java">public final class ImmutableEvent {private final Date eventDate;  // Date本身是可变的!public ImmutableEvent(Date date) {this.eventDate = new Date(date.getTime());  // 防御性拷贝,避免外部修改原Date}public Date getEventDate() {return (Date) eventDate.clone();  // 返回拷贝,避免外部修改内部Date}
}

构造函数中创建 Date 的副本存储,而非直接引用外界传入的 Date。getEventDate() 返回克隆对象,防止外部通过获取引用修改内部状态。

示例3:Java标准库中的不可变类
String 类:

java">String s = "Hello";
s = s.concat(" World");  // 返回新对象,原s未被修改

所有看似修改的操作(如 concat()、substring())都返回新对象,原始字符串不变。
BigInteger、BigDecimal:
数值运算(如 add())均返回新实例,确保原有对象不变。

为何不可变类更安全?

  • 线程安全:无需同步,多线程共享时不会出现竞态条件。
  • 缓存友好:可安全复用对象(如 String 常量池)。
  • 防御性拷贝不必要:不可变对象本身无法被修改,传递时无需复制。
  • 可靠的哈希键:作为 HashMap 的键时,哈希值不会改变,避免定位错误。

何时使用可变类?

不可变类的缺点是频繁创建对象可能影响性能,此时可选择可变配套类:String(不可变) ➔ StringBuilder(可变,用于高效拼接字符串)。复杂计算中,若需频繁修改状态,可使用可变对象临时操作,最终生成不可变结果。


http://www.ppmy.cn/news/1583729.html

相关文章

Qt信号与槽高级特性与项目实战:原理剖析与工程化应用指南

文章目录 五、信号与槽的高级特性1. 信号与信号的连接(1) 实现信号的转发(2) 信号与信号连接的应用场景 2. 带参数的信号与槽(1) 带参数的信号声明与使用(2) 参数类型的匹配问题 3. 信号与槽的断开连接(1) 为什么需要断开连接(2) 如何使用 QObject::disconnect() 小结 六、信号…

cmd命令查看电脑的CPU、内存、存储量

目录 获取计算机硬件的相关信息的命令分别的功能结果展示结果说明获取计算机硬件的相关信息的命令 wmic cpu get name wmic memorychip get capacity wmic diskdrive get model,size,mediaType分别的功能 获取计算机中央处理器(CPU)的名称 获取计算机内存(RAM)芯片的容量…

贪心算法(12))(java)坏了的计算器

题目&#xff1a;在显示着数字 startValue的坏计算器上&#xff0c;我们可以执行以下两种操作&#xff1a; 双倍(Double):将显示屏上的数字乘2; 递减(Decrement):将显示屏上的数字减1. 给定两个整数 startValue 和 target。返回显示数字target所需的最小操作数。 示例1: 输…

el-select 可搜索下拉框 在ios、ipad 无法唤出键盘,造成无法输入

下一篇&#xff1a;el-select 可搜索下拉框&#xff0c;选中选项后&#xff0c;希望立即失去焦点&#xff0c;收起键盘&#xff0c;执行其他逻辑 【效果图】&#xff1a;分组展示选项 >【去界面操作体验】 首先&#xff0c;通过 夸克浏览器的搜索: el-select 在 ipad 输入框…

Docker镜像相关命令(Day2)

文章目录 前言一、问题描述二、相关命令1.查看镜像2.搜索镜像3.拉取镜像4.删除镜像5.镜像的详细信息6.标记镜像 三、验证与总结 前言 Docker 是一个开源的容器化平台&#xff0c;它让开发者能够将应用及其依赖打包到一个标准化的单元&#xff08;容器&#xff09;中运行。在 D…

前端初级面试20道核心题+详细思路解析

一、HTML/CSS 基础篇 ​1. 如何让一个 div 水平居中&#xff1f;至少写出 3 种方法 答题思路&#xff1a; ​方法1&#xff1a;margin: 0 auto; 设置宽度&#xff08;块级元素&#xff09;。​方法2&#xff1a;父级 text-align: center;&#xff0c;子级 display: inline-…

ArkUI之常见基本布局(下)

6.轮播(Swiper) 1.概述 Swiper组件提供滑动轮播显示的能力。Swiper本身是一个容器组件&#xff0c;当设置了多个子组件后&#xff0c;可以对这些子组件进行轮播显示。通常&#xff0c;在一些应用首页显示推荐的内容时&#xff0c;需要用到轮播显示的能力。 针对复杂页面场景…

Modbus协议编程读写流程图大全

读离散量输入 读保持寄存器 读输入寄存器 写单个线圈 写单个寄存器 写多个线圈 写多个寄存器 (0x14) 读文件记录 写文件记录 (0x16) 屏蔽写寄存器 (0x17) 读/写多个寄存器