Effective Java读书笔记

读书笔记说明

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

第 2 章 创建和销毁对象

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

优点:

  • 有名称方便理解
  • 对象可以复用
  • 方便返回任何子类型对象
  • 创建参数化类型实例时简化代码

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

这个感觉可以依靠 spring 来完成。看示例代码见 P9

采用单元素的枚举类型实现 Singleton

1
2
3
4
5
6
publicenum Elvis{
INSTANCE;

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

调用:
Elvis e = Elvis.INSTANCE

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

避免创建不必要的对象

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

消除过期对象引用

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

避免使用终结(finalizer)方法

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

第 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 接口才可以使用 Object 当中的 clone 方法
如果你覆盖了非 final 类中的 clone 方法,则应该返回一个通过调用 super.clone 而得到的对象。
clone 时确保不会伤害到原始对象(很多时候克隆的时候如果只克隆了引用地址,就会有问题)
综上:尽量避免去覆盖 clone,需要拷贝对象采用深拷贝技术。

考虑实现 Comparable 接口

JAVA 类库中的值类都实现了该接口中的 compareTo 方法,可以进行比较。如果自己设计的类有内在排序关系,强烈建议实现该接口。

第 4 章 类和接口

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

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

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

使可变性最小化

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

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

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

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

复合优先于继承

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

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

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

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

在做继承时要写完善的文档。

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

接口优于抽象类

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

接口只用于定义类型

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

类层次优于标签类

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

用函数对象表示策略

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

有限考虑静态成员类

第 5 章 泛型

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

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

消除非受检警告

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

列表优于数组

优先考虑泛型

优先考虑泛型方法

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

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

第 6 章

用 enum 代替 int 常量

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

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

用实例代替序数

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

用 EnumSet 来代替位域

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

EnumMap 来代替序数索引

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

用接口模拟可伸缩的枚举

枚举可以实现接口。

多使用注解

坚持使用 Override 注解

用比较接口定义类型

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

第 7 章 方法

检查参数有效性

在编程中养成对参数做检查的习惯

必要时进行保护性拷贝

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

谨慎设计方法签名

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

慎用重载

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

慎用可变参数

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

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

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

第 8 章 通用程序设计

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

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

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

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

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

了解和使用类库

避免重复造轮子

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

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

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

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

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

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

当心字符串连接的性能

如果频繁拼接,请使用 StringBuffer(线程安全)或者 StringBuilder

通过接口引用对象

如果养成用接口作为类型的习惯,程序会更加灵活

接口优先于反射机制

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

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

谨慎使用本地方法

一般情况下还是别用为好

谨慎地进行优化

避免过度优化

遵守普遍接受的命名惯例

第 9 章 异常

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

这个我相信正常人都是这么想的。书上竟然举例用异常来做性能优化,反而把代码可读性弄的更差了。

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

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

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

优先使用标准的异常

抛出与抽象想对应的异常

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

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

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.次选方案:低层不能处理异常,则使用日志记录,方便后面来定位问题

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

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

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

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

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

努力失败保持原子性

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

不要忽略异常

第 10 章 并发

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

同步访问共享的可变数据

避免过度同步

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

executor 和 task 优先于线程

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

并发工具优先于 wait 和 notify

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

线程安全性的文档化

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

慎用延迟初始化

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

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

不要依赖于线程调度器

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

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

避免使用线程组

第 11 章:序列化

谨慎地实现 Serializable 接口

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

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

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

保护性地编写 readObeject 方法

针对 74 条中的安全性问题,可以使用 readObject 方法来获取对象,保证构造对象时的一些约束被满足

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

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