以下是本人在阅读《Effective Java》期间记录的笔记,未经整理。有兴趣的可以看看,有问题可以探讨。
第二章 创建和销毁对象
1. 考虑使用静态工厂方法代替构造方法
优点:
- 静态方法可以命名(见名知意)
- 单例模式(减少创建对象的开销)
- 可以返回当前类的子类而不是当前类
- 根据参数不同返回不同实现类
- 类不需要存在,可以反射生成(比如数据库的DriverManager)
缺点:
- 没有公共或受保护构造方法的类不能被子类化???
- 程序员很难找到它们
2. 当构造方法参数过多时使用 builder 模式
比如4个或者以上
优点:
- 减少冗余的构造方法
- 比set更简洁
缺点:
影响性能(多创建了一个Builder对象)
3. 使用私有构造方法或枚类实现 Singleton 属
性
- 序列化会破坏单例模式,解决方法
- 给实例化字段添加transient关键字,例如private transient String name;
将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化
-
给单例对象添加readResolve方法
private Object readResolve() { return instance; }
- 给实例化字段添加transient关键字,例如private transient String name;
- 枚举类型实现的单例模式是最佳的方式(链接)
5. 依赖注入优于硬连接资源
比如给适配器类注入需要适配的类,就叫做依赖注入。
不要用单例和静态工具类来实现依赖一个或多个底层资源的类(并发问题)
也没见过有静态的适配器类吧?
6. 避免创建不必要的对象
- 当创建的对象不可变时(比如正则表达式,一些地名等 或者Boolean.false好过new Boolean(“false”))
- 避免无意识的自动装箱
7. 消除过期的对象引用
- 集合和数组中不需要的对象记得置空(如果集合或者数组不会被销毁的情况下)
- WeakHashMap(当key不被其他对象引用时,会被GC)
- 第三个常见的内存泄漏来源是监听器和其他回调???
8. 避免使用 Finalizer 和 Cleaner 机制
把GC交给jvm就对了
- 这两个方法会造成性能影响
- 这两个方法被执行的时机不确定
- finalizer如果抛出异常不会被记录(问题:此时对象能被GC正确回收吗???)
- 使用AutoCloseable来替代这两个方法回收资源
AutoCloseable接口只有一个close方法,当对象实现了该接口,使用try-catch-resources语法创建的资源,正常退出try或者抛出异常,其close方法都会被调用,拿文件流举例
try (InputStream in = new FileInputStream("src"); OutputStream out = new FileOutputStream("dst")) { } catch (Exception e) { e.printStackTrace(); }
9. 使用 try-with-resources 语句替代 try-finally语句
见第8点的代码
第三章 对象通用方法
10. 重写 equals 方法时遵守通用约定
11. 重写 equals 方法时同时也要重写hashcode方法
默认的Object.hashCode()返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现,两个Object的hashCode值不一样
如果对性能有要求,不要使用Object.hash(Object… values)方法,因为
- 对基本类型做hashCode需要转换为包装类型
- 会创建一个Object[]数组
13. 谨慎地重写 clone 方法
- 不可变类永远不应该提供 clone 方法
- 一维数组的clone是深拷贝,二维数组是浅拷贝
- 类变量如果被final修饰,会和clone冲突???
- 其他接口不应该extend Clonable接口
- 为继承设计一个类时,不应该implement Clonable接口,就像直接继承 Object 一样。
答:如果父类实现了Clonable或Serializable接口,那么子类也应该相应的维护他,这会增加编写子类的成本。
-
实现 Cloneable 的所有类应该重写公共 clone 方法
- colne方法应该首先调用 super.clone,还需要注意浅拷贝问题
- 复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone 方法复制。
14. 考虑实现 Comparable 接口
- 如果你正在编写具有明显自然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现 Comparable 接口
- 请避免使用「<」和「>」运算符。 使用包装类中的静态 compare 方法或 Comparator 接口中的构建方法
public static class PhoneNumber {
int areaCode;
int prefix;
int lineNum;
// Comparable with comparator construction methods
private final Comparator<PhoneNumber> 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);
}
}
第四章类和接口
15. 使类和成员的可访问性最小化
除了作为常量的公共静态final 字段之外,公共类不应该有公共字段。 确保 public static final 字段引用的对象是不可变的。
16. 在公共类中使用访问方法而不是公共属性
就是属性私有化,提供getter和setter方法
17. 最小化可变性(不可变类)
不可变对象本质上是线程安全的;它们不需要同步。
例如:String、BigInteger、BigDecimal
- 不提供修改对象状态的方法(比如setter)
- 不被继承(final修饰类)
- 所有字段设置为final
- 所有字段设置为private
- 有非final修饰的变量,确保客户无法获取该变量的引用(不提供getter)
- 如implement Serializable,则必须提供readObject 或 readResolve 方法
- 如果一个类不能设计为不可变类,那么也要尽可能地限制它的可变性
- 构造方法应该创建完全初始化的对象,并建立所有的不变性
18. 组合优于继承
- 装饰器模式(使用属性注入然后调用的方式而不是直接继承)
- 包装类不适合在回调框架中使用,因为一个被包装的对象不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时并不记得外面的包装对象。
问题:如果传递的是被包装的对象,为什么不传递包装对象呢?
-
继承违反封装,当子类需要用到父类的每个属性和方法时,才适合继承。否则建议使用组合。
19. 要么设计继承并提供文档说明,要么禁用继承
-
可重写方法(public、protected)尽量少调用可重写方法(比如AbstractList.allAll调用了add)
因为如果子类重写了add,父类的addAll的逻辑将产生变化
-
如果调用了可重写方法(违反了第一点),请在注释中(@implSpec)明确说明调用顺序及方法
- 构造方法中不能直接或间接调用可重写的方法
- 测试为继承而设计的类的唯一方法是编写子类(非父类作者外的开发,3个子类足矣)
20. 接口优于抽象类
- 单继承(抽象类),多实现(接口)
- 推荐使用骨架抽象类(比如AbstractList)
- 抽象类实现了相关接口
- 抽象类实现了部分方法
21. 为后代设计接口
- 避免使用默认方法向现有的接口添加新的方法(新建接口时默认方法很好用)
- 应编写多个接口实现来测试接口
22. 接口仅用来定义类型
- 接口只能用于定义类型,不应该仅用于导出常量。
- 使用常量类代替常量接口(常量类应构造私有化)
23. 类层次结构优于标签类
-
不要在一个类中根据类型生成不同类
比如一个类根据条件生成长方形或正方形
而是应该抽出接口和抽象类,生产长方形子类和正方形子类
24. 支持使用静态成员类而不是非静态类
- 内部类如果不需要引用宿主实例,就应该改成静态内部类,因为存储这个引用会占用时间和空间(例如HashMap的Node)
- 非静态内部类可能会导致宿主类满足GC条件却无法被回收
-
非静态内部类通过(宿主类.this获取宿主类实例)
public class Outter { class inner { public Outter getOutter { // 获取当前Outter实例 return Outter.this; } } }
25. 将源文件限制为单个顶级类
就是一个java文件一个类或接口(内部类不限)
第五章 泛型
26. 不要使用原始类型
就是使用泛型,用List
27. 消除非检查警告
就是泛型的检查警告,例如使用List而不是List
使用@SuppressWarnings(“unchecked”)可以抑制警告,但是最好注解在变量上(最细粒度)以及注释说明为什么无法修改成泛型
28. 列表优于数组
- 集合有更好的类型安全性和互操作性
- 数组有更好的简洁性或性能
29. 优先考虑泛型
优先使用泛型类而不是Object类型,这样就可以在编译期间避免类型转换错误,也减少了强转类型的代码。比如ArrayList
ArrayList底层使用的是Object[]数组,取数据的时候会强转
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
31. 使用限定通配符来增加 API 的灵活性
Collection<? extend E>
Collection<? super E>
32. 合理地结合泛型和可变参数
可变参数和泛型不能很好地交互
此时toArray返回的是Object[],所以代码会报错
static <T> T[] toArray(T... args) {
return args;
}
static <T> T[] pickTwo(T a, T b, T c) {
return toArray(a, b);
}
public static void main(String[] args) {
String[] attributes = pickTwo("Good", "Fast", "Cheap");
}
33. 优先考虑类型安全的异构容器
在使用泛型的时候传入泛型的字节码类,可以确保类型安全
CheckedMap(Map<K, V> m, Class<K> keyType, Class<V> valueType) {
this.m = Objects.requireNonNull(m);
this.keyType = Objects.requireNonNull(keyType);
this.valueType = Objects.requireNonNull(valueType);
}
这样每次put的时候,都可以将value与valueType instance判断类型
get的时候可以valueType.case做类型转换
第六章 枚举和注解
34. 使用枚举类型替代整型常量
35. 使用实例属性替代序数
不要使用枚举类的默认序号,手动分配一个属性来存需要
// 这个不行
public enum Ensemble {
SOLO, DUET, TRIO;
public int numberOfMusicians() { return ordinal() + 1; }
}
// 这个ok
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
37. 使用 EnumMap 替代序数索引
39. 注解优于命名模式
A:规定类中以Tran结尾的所有方法开启事务
B:有@Transactional注解的方法开启事务
选择B比A更有灵活性,而且注解可以组合
40. 始终使用 Override 注解
41. 使用标记接口定义类型
不包含方法声明的接口,比如Serializable
思考(问题):什么情况下用注解合适,什么情况下用标记接口合适?
第七章 Lambda和Stream
42. lambda 表达式优于匿名类
- lambda 优于匿名类的主要优点是它更简洁
- lambda无法替代抽象类
- lambda最好一行,不超过3行
- lambda没有this引用
- lambda序列化
// java.util.Map中的comparingByKey // (Comparator<Map.Entry<K, V>> & Serializable)表示转换成可序列化的Comparator<Map.Entry<K, V>>对象 public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() { return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> c1.getKey().compareTo(c2.getKey()); }
43. 方法引用优于 lambda 表达式
方法引用就是
// ProjectEntity::getId和Function.identity()都是方法引用
// 方法引用和lambda哪个简洁用哪个(哪个短)
// 比如ProjectEntity::getId可以替换成x -> x.getId()
Map<String, ProjectEntity> projectMap = projectList.stream()
.collect(Collectors.toMap(ProjectEntity::getId, Function.identity()));
44. 优先使用标准的函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为 lambda 表达式。
使用@FunctionalInterface修饰
45. 明智审慎地使用 Stream
流的推荐使用场景
- 统一转换元素序列
- 过滤元素序列
- 使用单个操作组合元素序列 (例如添加、连接或计算最小值)
- 将元素序列累积到一个集合中,可能通过一些公共属性将它们分组
- 在元素序列中搜索满足某些条件的元素
46. 优先考虑流中无副作用的函数
forEach 操作应仅用于报告流计算的结果,而不是用于执行计算。
问题:forEach的应用场景是什么?
47. 优先使用 Collection 而不是 Stream 来作为方法的返回类型
- Collection 或适当的子类型通常是公共序列返回方法的最佳返回类型。
- 如果返回的序列小到足以容易地放入内存中,那么最好返回一个标准集合实现,例如 ArrayList 或 HashSet ???
- 如果在将来的 Java 版本中,Stream 接口声明被修改为继承 Iterable ,那么你就应该返回 Stream ,因为它可以同时被流和迭代处理。
48. 谨慎使用流并行
添加parallel关键字可以并行处理流。
但是作者认为甚至不要尝试并行化流管道,除非你有充分的理由相信它将保持计算的正确性并提高其速度。
第八章 方法
49. 检查参数有效性
public BigInteger mod(BigInteger m) {
if (m.signum <= 0) {
// 检查参数,主动抛出异常
throw new ArithmeticException("Modulus <= 0: " + m);
}
...
}
多用Objects.requireNonNull来判空
建议结合55点看
50. 必要时进行防御性拷贝
public class DefenseCopy {
private Date date;
// 1. 采用构造方法注入,不提供set
public DefenseCopy (Date date) {
// 2. 采用new Date拷贝
this.date = new Date(date.getTime());
}
public Date getDate() {
// 3. 采用new Date拷贝返回
return new Date(date.getTime());
}
public static void main(String[] args) {
Date date = new Date();
DefenseCopy defenseCopy = new DefenseCopy(date);
// 无法通过修改时间对defenseCopy产生影响
date.setTime(1622356419200L);
// 无法通过修改时间对defenseCopy产生影响
defenseCopy.getDate().setTime(1622356419200L);
}
}
推荐使用LocalDateTime取代Date
因为Data在实例化之后还允许改变,线程不安全
LocalDateTime只能实例化一次,线程安全。配合DateTimeFormatter使用
51. 仔细设计方法签名
- 仔细选择方法名名称。
- 不要过分地提供方便的方法。
方法过多使类的使用难度上升。只有经常使用的才考虑提供快捷方式。
-
避免过长的参数列表。
4个或更少的参数。
采用List、辅助类(类似vo)、Builder模式可以有效避免过长参数
-
对于参数类型,优先选择接口而不是类
-
与布尔型参数相比,优先使用两个元素枚举类型
枚举方便后续扩展新类型
52. 明智审慎地使用重载
一个安全和保守的策略是永远不要导出两个具有相同参数数量的重载。
否则考虑为方法赋予不同的名称。比如writeBoolean(boolean) 、 writeInt(int)而不是write(boolean) 、 write(int)
别杠String.valueOf,作者认为这方法不好
53. 明智审慎地使用可变参数
- 可变参数在调用时,可以不传参数进去,此时可变参数(数组)长度为0
- 可变参数本质上是数组,会影响性能
54. 返回空的数组或集合,不要返回 null
永远不要返回 null 来代替空数组或集合。它使你的 API 更难以使用,更容易出错,并且没有性能优势。
推荐使用Collections.emptyList、Collections.emptySet等
数组则推荐返回长度为0的静态数组(避免数组重复创建)
55. 明智审慎地返回 Optional
public People getMax(List<People> list) {
if (list.isEmpty()) {
// 返回空
return Optional.empty();
}
... do something
People p = list.xxxxxx;
// 使用of而不是ofNullable
return Optional.of(p);
}
- 如果可能无法返回结果,并且在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回 Optional 的方法(性能对比 返回null < Optional < 抛出异常 ???)
- 永远不要通过返回 Optional 的方法返回一个空值(用Optional.of而不是用Optional.ofNullable)
- 容器类型,包括集合、映射、Stream、数组和 Optional,不应该封装在 Optional 中。返回Collections.emptyList好过Optional.empty
- 永远不应该返回装箱的基本类型的 Optional(推荐用OptionalInt、OptionalLong等)
56. 为所有已公开的 API 元素编写文档注释
@param 参数注释
@throw 抛出的异常以及触发条件
@return
@implSpec 注释描述了方法与其子类之间的关系(重写了哪些方法以及调用链)
- 记录泛型类型或方法时,请务必记录所有类型参数
比如map的key和value都要写清楚
-
在记录枚举类型时,一定要记录常量(写清楚每个枚举的意思)
- 无论类或静态方法是否线程安全,都应该在文档中描述其线程安全级别
第九章 通用编程
57. 最小化局部变量的作用域
- 需要用到时再声明,避免过早声明。
- 尽量在声明变量的时候初始化
- 优先选择 for 循环而不是while 循环
58. for-each 循环优于传统 for 循环
59. 了解并使用库
- 使用ThreadLocalRandom代替Random
60. 若需要精确答案就应避免使用 float 和double 类型
- 使用 BigDecimal、int 或 long 进行货币计算
- 用BigDecimal(String)的构造,避免BigDecimal(double)这种损失精度
- 如果是金额(精度到分,即两位小数点),可以尝试用long运算,最后才除以100转成实际金额
61. 基本数据类型优于包装类
包装类不能使用 来做比较
想作比较的时候,推荐转成基本数据类型 Integer
62. 当使用其他类型更合适时应避免使用字符串
- 字符串是其他值类型的糟糕替代品。int、float、boolean、枚举等
63. 当心字符串连接引起的性能问题
StringBuffer Stringbuilder
64. 通过接口引用对象
// Good
Set<Son> sonSet = new LinkedHashSet<>();
// Bad
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类(顶级类)
65. 接口优于反射
反射用于创建实例,接口或超类用于访问
66. 明智审慎地本地方法
使用本地方法来提高性能的行为很少是明智的
67. 明智审慎地进行优化
- 代码简洁更重要
- 为了获得良好的性能而改变 API 是一个非常糟糕的想法
- 最后才是考虑性能优化
第十章 异常
69. 只针对异常的情况下才使用异常
- 异常应该只用于异常的情况下;他们永远不应该用于正常的程序控制流程
- 设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异常
比如假设iterator没有提供hasNext方法
70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常
区分清编译时异常和运行时异常
71. 避免不必要的使用受检异常
- 比如多加校验(索引越界前判断长度、if contain 等)
- 返回Optional
72. 优先使用标准的异常
- IllegalArgumentException 调用者传递的参数值不合适的时候
- IllegalStateException 因为接收对象的状态而使调用非法。
例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就
会抛出这个异常 -
NullPointerException
- ConcurrentModificationException 单线程对象被并发地修改
73. 抛出与抽象对应的异常
更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常
public E get(int index) {
try {
return listIterator(index).next();
// 捕获NoSuchElementException
} catch (NoSuchElementException exc) {
// 抛出IndexOutOfBoundsException
throw new IndexOutOfBoundsException("Index: "+index);
}
}
这比不加选择第从低层传递异常的做法更好
74. 每个方法抛出的异常都需要创建文档
注释中使用@throws
注释中写清楚会抛出的异常(包括运行时异常,虽然运行时异常不会显式包含在方法中)
75. 在细节消息中包含失败一捕获信息
异常抛出的信息应当简洁以及包含关键参数(敏感数据除外)
76. 保持失败原子性
失败原子性:失败的方法调用应该使对象保持在被调用之前的状态
解决方法:
- 设计一个不可变的对象
- 执行操作之前检查参数的有效性
- 对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容
77. 不要忽略异常
如果选择忽略异常, catch 块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为 ignored而不是e
第十一章 并发
78. 同步访问共享的可变数据
- 证读或者写一个变量是原子的( atomic ),除非这个变量的类型为 long 或者double
- 当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
// 因为nextSerialNumber++不是原子性的,所以改成如下
// 因为返回的方法已经被synchronized修饰,所以可以去除volatile关键字
private static int nextSerialNumber = 0;
public static synchronized int generateSerialNumber() {
return nextSerialNumber++;
}
当需要并发访问变量时,要么变量使用volatile修饰,要么提供被synchronized的get、set方法(两个方法都要)
79. 避免过度同步
在同步区域内做尽可能少的工作
80. executor 、task 和 stream 优先于线程
- 线程池比线程好
- 流的Parallel
81. 并发工具优于 wait 和 notify
比如, 应该优先使用 ConcurrentHashMap ,而不是使用 Collections.synchronizedMap
- 如果要维护需要调用wait或notify的旧版代码时候,wait标准格式如下
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
// (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}
- 优先使用notifyAll方法而不是notify方法
82. 文档应包含线程安全属性
Map<K, V> m = Collections.synchronizedMap(new HashMap<>());
Set<K> s = m.keySet(); // Needn't be in synchronized block
...
synchronized(m) { // Synchronizing on m, not s!
for (K key : s)
key.f();
}
Lock 字段应该始终声明为final
83. 明智审慎的使用延迟初始化
- 除非需要,否则不要延迟初始化
- synchronized
private FieldType field; private synchronized FieldType getField() { if (field == null) field = computeFieldValue(); return field; }
- lazy initialization holder class
// Lazy initialization holder class idiom for static fields private static class FieldHolder { static final FieldType field = computeFieldValue(); } private static FieldType getField() { return FieldHolder.field; }
- 双重检查(double check)
private volatile FieldType field; private FieldType getField() { FieldType result = field; if (result == null) { // First check (no locking) synchronized(this) { if (field == null) // Second check (with locking) field = result = computeFieldValue(); } } return result; }
- 单检查
// Single-check idiom - can cause repeated initialization! private volatile FieldType field; private FieldType getField() { FieldType result = field; if (result == null) field = result = computeFieldValue(); return result; }
84. 不要依赖线程调度器
- 不要依赖Thread.yield或线程优先级
- 线程不应该处于忙等待状态(busy-wait)
busy wait就是自旋等待
while True: if condition == True: do something break;
第十二章 序列化
85. 优先选择 Java 序列化的替代方案
- 没有理由在你编写的任何新系统中使用 Java 序列化
- 考虑JSON 或 protobuf
86. 非常谨慎地实现 Serializable
- 一旦类的实现被发布,它就会降低更改该类实现的灵活性
- 增加了出现 bug 和安全漏洞的可能性
- 增加了与发布类的新版本相关的测试负担
- 内部类(详见第 24 条)不应该实现 Serializable
87. 考虑使用自定义的序列化形式
简单的javaBean可以使用默认的序列化,当有类内部有复杂引用时,比如双向链表,则会大大增加序列化开销
- 必须在编写的每个可序列化类中声明显式的序列版本 UID
- 必须提供readObject方法
88. 保护性的编写 readObject 方法
// readObject method with validity checking - insufficient!
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
// 先调用defaultReadObject进行反序列化
s.defaultReadObject();
// 然后检查对象的有效性(自定义)
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +" after "+ end);
}
89. 对于实例控制,枚举类型优于readResolve
- 单例模式序列化需要用到readResolve保证单例
- 如果依赖 readResolve 进行实例控制,带有对象引用类型的所有实例字段都必须声明为transient
90. 考虑用序列化代理代替序列化实例
在执行out.write()方法时会出发这个方法,先调用writeReplace
如果没有writeReplace那么将会调用writeObject方法
private Object writeReplace() {
// 比如手动new一个相同的对象出来,并将this的属性赋值给新对象
return 代理对象;
}
其他
- 数字文字中使用下划线字符(_)
int num = 10_000.000_4; // 表示10000.0004