Effective Java读书笔记

读书笔记说明

effective java是按照这种条目来罗列知识点的。本读书笔记不对每条知识点做详细解释(甚至某些是没解释的)。本系列读书笔记只记录一些我认为有必要记录的重点。

第2章 创建和销毁对象

用静态工厂方法代替构造器

优点:

  • 有名称方便理解
    • 例:BigInteger.probablePrime
  • 对象可以复用,每次调用不用创建新对象
    • 例:Boolean.valueOf()在调用时就不会每次都创建新对象
  • 方便返回任何子类型对象
    • 例:java.util.Collections 可以避免导出所有具体集合类,具体的集合类在源码中是private static或者默认包权限的
  • 返回的类可以随着每次调用而发生变化:
    • 例:EnumSet,根据不同的参数返回不同的子类型对象,和第3点比较接近。例如EnumSet可以根据参数返回RegalarEnumSet或者JomboEnumSet。客户端不需要关心实现类,只需要关心返回的是个父类EnumSet。这样方便未来和增删具体的实现。也就是说利用静态工厂方法可以评估具体子类实现从而方便未来更好的拓展。
  • 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在,这是构成服务提供框架的基础:
    • 例:JDBC中DriverManager.registerDriver
    • 例:java.util.ServiceLoader

缺点:

  • 没有public或者protected的构造器就无法被子类化:不过这个可以鼓励多用组合而不是继承
  • 程序员很难发现他们:静态工程方法可以采用如下命名习惯:
    • from: 类型转换方法来获取实例,例如Date.from(instant)
    • of: 聚合方法,通过组合参数来获取实例,例如Set faceCards = EnumSet.of(JACK,QUEEN,KING);
    • valueOf: 根据值转换成实例
    • getInstance: 通过实例的参数来获取实例
    • newInstance: 确保返回的是新实例
    • getType: 以具体type命名返回别的类型例如Files.getFileStore
    • newType: 返回的是新的类型
    • 直接以type:Collections.list()

使用builder来创建很多参数的对象

优点:

  • 便于阅读
  • 传参不容易出现因为传参顺序不对导致的初始化问题
  • 参数比较多,并且一些是可选的并且未来还会变动参数的情况下,使用Builder模式易于改动
  • 此外builder也可以用在继承的结构中

缺点:

  • 核心对象,量比较大的情况下不要使用Builder模式,会有额外的内存占用(不过由于中间对象一般用完就销毁的,实际对内存影响是非常小的)

用私有构造器或者美剧类型强化Singleton属性

静态工厂方法或者公有成员变量都可以实现单例。
以下实现方式称为静态内部类单例模式,它具有如下优点:

  1. 线程安全:在Java中,静态内部类只会在被调用时才会被加载,因此可以保证在多线程环境下的线程安全性。
  2. 懒加载:由于静态内部类只会在被调用时才会被加载,因此可以实现懒加载,即在需要时才会创建单例对象。
  3. 代码优雅:相较于其他实现方式,静态内部类单例模式代码更加优雅、简洁、易于理解和维护。

需要注意的是,这种实现方式基于Java语言本身的特性,不需要使用synchronized或者volatile等关键字来保证线程安全性,因此性能较好,适用于高并发的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13


public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

private Singleton() {}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
1
2
3
4
5
6
publicenum Elvis{
INSTANCE;

//build方法中返回所需对象
publicvoidleaveTheBuilding(){...}
}

仅作工具类使用的类,可以使用私有构造器防止被实例化

提供私有构造函数强制类不被实例化

有限考虑依赖注入来引用资源

可以考虑通过构造函数注入依赖的对象或者传递依赖对象的工厂类。例如使用Supplier接口就很适合表示工厂。另外可以考虑dagger2这样的依赖注入框架。

避免创建不必要的对象

例如自己维护对象池、尽量使用基本类型而不是装箱基本类型。

消除过期对象引用

实践:
1.对象用完手动指定为null,例如栈对象pop时应该确保对应index的指向为null
2.缓存可以考虑使用WeakHashMap使得其没有引用时自动删除
3.添加新条目的时候清理,例如LinkedHashMap提供了removeEldestEntry来清除过期条目
4.例如注册回调,但是没有显示取消注册,这时候可以考试采用弱引用,自动在没有引用时删除。

避免使用终结(finalizer)方法

合理应用情况有:
1.充当安全网,检验之前的终止方法(例如FileInputStreamConnection,FileOutputStrean的close方法和timer的cancel方法)是否被及时调用。如果发现资源未被终结可以记录日志。
2.终结本地对等体,本地对等体即指通过本地方法将对象委托给本地对象,本地对象不会被JVM回收,就依赖这个显示终结方法来回收。

有限使用try-with-resource

第3章 所有对象都通用的方法

覆盖Object的equals时请遵守通用约定

满足下列四个条件之一的就不需要覆盖equals方法:
(1).类的每个实例本质上都是唯一的,如枚举等。
(2).不关心类是否提供了“逻辑相等”的测试功能。
(3).超类已经覆盖了equals方法,从超类集成过来的行为对于子类也是合适的。
(4).类是私有的或者包访问权限的,可以确定它的equals方法永远不会被调用。
当类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖equals方法以实现期望的行为时,就需要覆盖equals方法。在覆盖equals方法时,必须遵循自反性、对称型、传递性、一致性、非空性

违反对称性场景:equals传递的参数没有实现相同的equals方法,比较的仍然是引用地址,导致违反对称性
违反传递性场景:父类覆盖了equals,子类没有覆盖equals,调用了父类的equals方法,从而少比较了一些属性导致出错

实现高质量equals方法的诀窍:
(1).使用==操作符检查参数是否为这个对象的引用。
(2).使用instanceof操作符检查参数是否为正确的类型。
(3).把参数转换成正确的类型。
(4).对于要比较类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。
(5).编写完equals方法后需要测试是否满足对称性、传递性和一致性。
覆盖equals方法时特别要注意:不要将equals生命中的Object对象替换为其他类型

覆盖equals时总要覆盖hashCode方法

反正这两个就是要绑定在一起覆盖就对了

建议任何时候都覆盖toString

覆盖toString提供可读性更好的信息

谨慎覆盖clone

cloneable只是个不包含接口定义的标记接口,有不少副作用。建议使用拷贝工厂做深拷贝,例如newInstance静态工厂方法

考虑实现Comparable接口

JAVA类库中的值类都实现了该接口中的compareTo方法,可以进行比较。如果自己设计的类有内在排序关系,强烈建议实现该接口。
实现Comparator接口时,基本装箱类型推荐使用compare方法进行比较

第4章 类和接口

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

实例域绝不能是公有的。包含公有可变域的类并不是线程安全的。
公有的静态final域可以来暴露常量,但是一定要注意不能包含指向不可变对象的引用。
类具有公有的静态final数组域是很愚蠢的,客户端将能够修改数组的内容。

在公有类中使用访问方法而非公有域

使可变性最小化

为了使类成为不可变,要遵循下面五条规则:
1.不要提供任何会修改对象状态的方法
2.保证类不会被扩展
3.使所有域都是final的
4.使所有的域都成为私有的
5.确保对于任何可变组件的互斥访问:永远不要向客户端提供对象的引用来初始化域。可以采用“保护性拷贝”技术

实现方法:让类的所有构造器都变成私有的或者包级私有的,并添加静态工厂来代替公有的构造器。

如果选择让自己的不可变类实现序列化接口,并且包含指向可变对象的域,则需要显示的提供readObject和 readResolve方法

总之,在实际中尽可能把类做成是不可变的,能使用final就尽量使用final;构造器应该创建完全初始化对象,建立起所有的约束关系。

复合优先于继承

同个包内合理的设计,来通过继承是比较安全的(但仍然有风险)。跨包继承则很危险。

超类在后续发行中修改了功能,会导致子类功能的异常。避免这样的情况就是采用复合。新类中通过一个私有域引用现有类的一个实例,这称为复合。现有类添加新方法也不会影响新类。这样设计得到的类会更加稳固。

只有当两个类确实存在 is-a关系的时候,才适用继承。注意不要盲目的使用继承。

要么为继承而设计并提供文档说明,要么就禁止继承

在做继承时要写完善的文档。
继承会打破封装

为了继承而设计的类,唯一测试方法就是编写子类。

接口优于抽象类

使用设计接口要谨慎,一旦发行和大量实现,就肯定无法再修改了,而抽象类确可以。

为后代设计接口

  • 接口中使用default实现

接口只用于定义类型

避免使用常量接口(接口里面定义大量常量)。应该使用枚举类型或者不可实例化的工具类来导出这些常量。
总结:不要使用接口来导出常量。

类层次优于标签类

充斥样板代码、标签类的域就是标签类。例如一个Point类就是一个标签类。利用抽象类抽象出一些共用的方法,做好类层次

用函数对象表示策略

即策略模式。一些策略(例如比较功能),需要设计一个策略接口,再设计一个策略实现类,来供其他类调用。

静态成员类优于非静态成员类

不需要关联一个外部实例,保存这份引用要消耗时间和空间,还有可能影响垃圾回收。

限制源文件为单个顶级类

一般也不会这么做

第5章 泛型

不要在新代码中使用原生态类型

意思就是尽量使用泛型,可以提前做类型检查,防止出错。

消除非受检警告

消除警告,保证不出现由于类型造成的ClassCastException

列表优于数组

优先考虑泛型

优先考虑泛型方法

利用有限制通配符来提升API灵活性

? extends 和 ? super T

谨慎将泛型和可变参数一起使用

主要可变参数是数组,值保存在泛型可变参数数组中不是类型安全的。简单来说就是泛型中的类型混合可变参数时类型不安全。JDK中的实现都是当做整体处理的,可以参考Arrays.asList和EnumSet.of的实现

优先考虑类型安全的异构容器

jdk提供的容器基本都是类型安全的,异构的话可以用泛型的class作为key

第6章

用enum代替int常量

枚举是单例的。类型检查等措施保证其安全可靠。

枚举类中需要引用自有的枚举值做判断,建议使用策略枚举。即将引用枚举值的行为方法定义成一个内部枚举类,传递给枚举类的构造器中。例子见书P135。策略枚举是策略模式的一种延伸。

用实例代替序数

枚举是有个int值序数来维护顺序的。但是避免用这ordinal()方法来获取序数。该方法仅在设计像EnumSet和EnumMap这样的类的时候才使用

用EnumSet来代替位域

有时候需要将多个枚举值按照位运算的方式来做并交的操作,这个可以交个EnumSet来做,其本身内部实现也是采用位运算的方式。

EnumMap来代替序数索引

需要键值对来索引枚举值时使用该数据结构

用接口模拟可伸缩的枚举

枚举可以实现接口。

多使用注解

坚持使用Override注解

  1. 编译时检查:使用 @Override 注解可以让编译器在编译时检查是否存在方法重写错误。如果子类重写了父类或接口中的方法,但是没有使用 @Override 注解,那么编译器就无法检查出这个错误,导致程序运行时出现问题。
  2. 提高可读性:使用 @Override 注解可以使代码更加清晰易懂,让其他开发人员更容易理解代码的含义。
  3. 防止误重写:使用 @Override 注解可以防止开发人员在重写方法时误用了不同的方法名或参数列表,从而导致出现一些隐藏的错误。
  4. API 文档生成:使用 @Override 注解可以帮助自动生成 API 文档,这对于大型项目非常有用。

用标记接口定义类型

需要定义类型的时候就使用标记接口。注意和标记注解的区别。
java.io.Serializable就是使用标记接口的例子。因为需要这个类型。

当前主流来看,用标记注解更好点

第7章 Lambda和Stream

用lambda替换匿名类

方法引用优先于lamba

方法引用够简洁应该优先使用方法引用

坚持使用标准函数接口

有限使用java.util.Function下的接口

谨慎使用Stream

  • stream是延迟计算的,没有遇到终止操作前相当于无操作指令
  • 避免使用stream处理char,会输出整型

优先使用Stream中无副作用的函数

主要用.collect中toList,toSet,toMap,groupingBy,joining等收集器即可

Stream优先使用Collection作为返回类型

谨慎使用Stream并行

没有充分信心以及相关测试不要使用paralle,容易出错。parallel只适合用在ArrayList,HashMap,HashSet这种能被精确分工的,

第8章 方法

检查参数有效性

在编程中养成对参数做检查的习惯。可以用Objects.requireNonNull

必要时进行保护性拷贝

对可变对象的引用可能造成错误的答案。这时候可以考虑使用保护性拷贝的方式。例如重新使用构造器生成对象。

谨慎设计方法签名

方法名要易于理解;避免过长的参数列表。
参数类型优先使用接口;
boolean参数优先使用枚举类型
几个减少方法参数的技巧:

  • 拆成多个方法:尽量提升拆开的方法的正交性
  • 创建辅助类:如果多个方法的参数都有共性,可以定义个辅助类保存这些参数

参数类型接口优先于类,利于扩展

对于boolean参数有限考虑使用枚举,可读性更加好

慎用重载

最佳实践:永远不要导出两个具有相同参数的重载方法

慎用可变参数

如果参数也只有0,3个的情况可以不用可变参数,避免性能开销。可变参数每次调用会有一次数组分配和初始化,对性能略微有一些影响。

返回零长度的数组或者集合,而不是null

返回类型为数组或集合的方法没理由返回null,避免上层还需要对null做额外的处理

谨慎返回optional

  • 容器结合类的不应该放在optional内,避免外部额外处理optional
  • optional除了作为返回值不要用在别的地方,没好处,例如作为key

为所有导出的API元素编写文档注释

第9章 通用程序设计

将局部变量的作用域最小化

在第一次使用它的地方声明

for-each循环优于传统的for循环

性能更好;
避免使用iterator时候产生的一些迭代问题

以下情况无法使用for-each
1.过滤:需要在遍历时删除元素
2.转换
3.平行迭代

了解和使用类库

避免重复造轮子

需要精确的答案,避免使用float和double

货币计算可以使用BigDecimal、int和long

基本类型优于装箱基本类型

避免对象的NullPointerException方法,同时性能也会好点

如果其他类型更适合,则避免使用字符串

例如值类型(用基本类型)、枚举类型(用枚举)

当心字符串连接的性能

如果频繁拼接,请使用StringBuffer(线程安全)或者StringBuilder。使用+会产生大量中间对象有额外的性能和内存开销

通过接口引用对象

如果养成用接口作为类型的习惯,程序会更加灵活。返回的结果赋值给接口。

接口优先于反射机制

反射有几个问题:
1.丧失了编译时类型检查的好处
2.执行反射访问锁需要的代码非常笨拙和荣昌
3.性能损失

反射仅仅用在实例化对象还是比较安全的

谨慎使用本地方法

一般情况下还是别用为好。如果需要使用高性能的高精度算数运算可以考虑本地方法的GUN高精度运算库GMP

谨慎地进行优化

避免过度优化:

  • 不要为了性能而牺牲合理的结构
  • 努力编写好的程序而不是快的程序
  • 初期重视那些难以更改的组件,例如API、交互层设计、永久数据格式对性能的影响,因为后续难以改变了
  • 用JMH剖析性能

遵守普遍接受的命名惯例

第10章 异常

只针对异常的情况才使用异常

这个我相信正常人都是这么想的。确实认为需要处理异常的地方才加try-catch

对可恢复的情况使用受检异常,对编程错误使用运行时异常

避免不必要地使用受检异常

有时候如果受检异常没有提供较好的恢复,还不如不使用反而会对程序更加有帮助

优先使用标准的异常

尽量代码重用。不要直接重用Exception、RuntimeException、Throwable或者Error

抛出与抽象想对应的异常

也就是说更高层的实现应该捕获更低层的异常,同时抛出可以按照高层抽象进行解释的异常。

异常链:如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。

1
2
3
4
5
6
//Exception Chaining
try{
... //use lower-level abstraction to do our bidding
}catch(LowerLevelException cause){
thrownew HigherLevelException(cause); //关键
}

最佳实践:
1.主选方案:能在低层处理异常则在低层处理异常,实在不行就使用异常转译
2.次选方案:低层不能处理异常,则使用日志记录,方便后面来定位问题

这个辩证的看吧,我觉得最好抛root cause,中间的代码如果有利于排查问题才做异常转译

每个方法抛出的异常都要有文档

可以用Javadoc的@throw标签记录下一个方法可能抛出的每个未受检异常;

不要使用throws关键字来将未受检的异常包含在方法的声明中

在细节消息中包含能捕获失败的信息

异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值

努力失败保持原子性

失败的时候,操作已经进行了一半,通过编写恢复代码、调整处理过程顺序等方式使得代码能回滚到失败之前的状态。

不要忽略异常

除非很明确自己为什么这么做,否则不要吞异常

第11章 并发

这一章还是比较简单。因为之前看过并发编程实战这本书的内容。如果需要完整了解并发,还是需要看下那本书。

同步访问共享的可变数据

避免过度同步

在同步区域内做尽量少的工作。例如调用外星方法以及引用可变对象都会造成线程不安全。

executor和task优先于线程

自己去通过Thread编写多线程任务是不太好的,因为要考虑很多细节。采用并发包中的ExecutorService来管理线程任务是最佳实践

并发工具优先于wait和notify

并发操作,都优先使用并发包里面的工具和数据结构。

线程安全性的文档化

提供一些线程安全相关的注解。在JAVA并发实战里面也有提及.详情可以查看JAVA并发编程读书笔记

慎用延迟初始化

绝大部分情况都应该使用正常的初始化。

为了达到性能目标而进行延迟初始化有以下选择(P250):
1.实例域:双重检查模式
2.静态域:使用lazy initialization holder class idiom
3.对于可以接受重复初始化的实例域也可以考虑使用单重检查模式

不要依赖于线程调度器

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。

不要依赖Thread.yield或者线程优先级(最不可移植特征)来控制线程调度,这种仅仅在测试期间,可以帮助来发现一些隐蔽的BUG。

第12章:序列化

其他方法优先于Java序列化

主要是反序列化天然就容易被攻击。对不被信任的流进行反序列化和反序列化很容易被攻击。例如下面的代码层次很深,有100层。反序列化HashSet需要计算其元素的散列码,hashCode方法会被调用2^100次方。

谨慎地实现Serializable接口

实现序列化接口带来的问题:
1.灵活性:实现序列化接口会导致程序发布后灵活性变差(导出后要永远支持这种序列化形式)。
2.安全性和BUG:增加了BUG和安全漏洞的可能性:反序列化机制是一个“隐蔽的构造器”。依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭到非法访问
3.测试负担:随着类发型新的版本,相关的测试负担也增加了。

最佳实践:为了继承而设计的类,应尽可能少地去实现序列化接口。

考虑使用自定义序列化形式

保护性地编写readObeject方法

主要是为了避免由于反序列化攻击而导致的安全问题。具体来说,建议在readObject()方法中实现一些安全性检查和防御措施,以确保反序列化得到的对象是安全和正确的。
保护性地编写readObject()方法的主要思想是,不信任反序列化得到的数据,要对其进行检查和验证,以确保反序列化得到的对象是安全和正确的。一般来说,可以采取以下几个措施:

  1. 对反序列化得到的对象进行校验:在readObject()方法中,可以对反序列化得到的对象进行校验,确保它们符合要求。例如,可以对序列化数据进行数字签名验证,以确保序列化数据没有被篡改。
  2. 对反序列化得到的对象进行安全性检查:在readObject()方法中,可以对反序列化得到的对象进行安全性检查,以确保它们不会带来安全问题。例如,可以检查反序列化得到的对象是否包含敏感信息,是否存在恶意代码等。
  3. 对反序列化得到的对象进行过滤和转换:在readObject()方法中,可以对反序列化得到的对象进行过滤和转换,以确保它们符合要求。例如,可以将反序列化得到的对象转换为安全的Java对象,或者过滤掉不符合要求的数据。

总之,保护性地编写readObject()方法的目的是为了确保反序列化得到的对象是安全和正确的。在编写readObject()方法时,需要考虑具体的反序列化需求和安全要求,采取相应的措施来保证对象的安全性和正确性。

对于实例控制,枚举类型优先于readResolve

Effective Java中提到了“对于实例控制,枚举类型优先于readResolve”这句话,意思是说在需要实现实例控制(如单例模式)时,枚举类型是更好的选择,而不是使用readResolve()方法。

这是因为枚举类型在Java中是实现单例模式的最佳方式之一,它具有以下几个优点:

  1. 简单明了:枚举类型实现单例模式非常简单明了,不需要考虑线程安全问题和反序列化攻击等问题。
  2. 线程安全:枚举类型天生就是线程安全的,它们的单例实例在JVM中只会被实例化一次,不需要考虑线程同步的问题。
  3. 防止反序列化攻击:枚举类型天生就是防止反序列化攻击的,因为枚举类型的实例是通过类加载器来实现的,不会受到反序列化攻击的影响。

相比之下,使用readResolve()方法实现单例模式存在一些问题:

  1. 反序列化攻击:使用readResolve()方法实现单例模式容易受到反序列化攻击的影响,需要在readResolve()方法中对序列化数据进行校验和过滤,增加了额外的复杂度。
  2. 线程安全:使用readResolve()方法实现单例模式需要考虑线程安全问题,需要使用同步机制来保证线程安全,影响性能。

因此,当需要实现实例控制时,枚举类型是更好的选择,它具有简单明了、线程安全和防止反序列化攻击等优点,可以更好地保证代码的安全性和可维护性。

考虑用序列化代理代替序列化实例

相当于实现序列化器精细控制序列化,可以用三方库达成类似目的