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

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