Joshua Bloch在国内出版的书包括《Effective Java2》《Java Puzzlers》《Java Concurrency in Practive》(合著)。特别是这本《Effective Java2》,每次重读都有新的收获,本文将过去和现在记录的一些读书笔记,感想整理了一下。
(标记说明∷表示笔记内容 ※为补充内容)
第1章 引言(略)
第2章 创建和销毁对象
第1条:考虑用静态工厂方法代替构造器
∷典型的方法名有:valueOf,newInstance,getInstance等,特别适用于,服务提供者框架上使用。
服务提供者框架又包含三个重要组件:服务接口(Connection),提供者注册API(DriverManager.registerDriver),服务访问API(DriverManager.getConnection),一个可选:服务提供者接口(Driver)
例:
Class.forName("oracle.jdbc.driver.OracleDriver");
String dbUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
Connection conn=DriverManager.getConnection(url,user,password);
Statement stmt = conn.createStatement(...);
※实现技巧:在OracleDriver的代码中有一个类级的static块代码,其中包含如下代码,即每次Class.forName()加载类后自动完成注册。
DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
第2条:遇到多个构造器参数时要考虑用构建器
∷当参数变多并且使用方式多样时,普通的构造器易用性会比较差(参数位置易错)。这时Builder模式应该是比较好的选择。(优雅的解决方法总是要多记一下)
例子:
public class NutritionFacts{
private final int servingSize;
private final int calories;
..
public static class Builder{
// Required parameters
private final int servingSize;
..
// Optional parameters - initialized to default values
private int calories =0;
..
public Builder( int servingSize, int servings ){
this.servingSize = servingSize;
..
}
public Bilder calories(int val)
{ calories = val; retrun this; }
..
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
//使用例
Facts cocaCola = new Facts.Builter(240,8).calories(100)...build();
第3条:用私有构造器或者枚举类型强化Singleton属性
∷单元素的枚举类型是实现 单例的最佳方式(绝对防止多次实例化,自带序列化),其次是私有构造器(静态公有域和静态公有
方法两种)
∷Singleton类做序列化时,为保证唯一性,必须声明所有实例域为瞬时(transient),并使用readResolve方法
例:
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis():
private Elvis() { ... }
..
public void leaveTheBuilding() { .. }
}
// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis():
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
..
public void leaveTheBuilding() { .. }
}
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { .. }
}
第4条:通过私有构造器强化不可实例化的能力
∷不希望实例化的工具类必须选择,副作用 是不会有子类
第5条:避免创建不必要的对象
∷与性能关系甚大。字符串拼接使用StringBulider,尽量使用常量static,避免自动装箱等。
第6条:消除过期的对象引用
∷JAVA内存泄露是比较隐蔽的,主要有堆栈,已废止的指针未清空,消除过期引用
∷缓存考虑使用WeakHashMap,或java.lang.ref。定时清理使用ScheduledThreadPoolExecutor,LinkedHashMap的removeEldestEntry()方法可移除其最旧的条目
∷监听器和回调,考虑使用弱引用(weak reference)如WeakHashMap
第7条:避免使用终结函数
∷不可预测,也是没有必要的一个方法
∷两种合法用途:充当安全网和本地方法回收(从来没用过啊)
第3章 对于所有对象都通用的方法
∷Java是单根系统,所以全体对象都要考虑Object暴露的方法(equals,hashCode,toString,clone,finalize)是否需要重写
∷否则的话,无法避免使用其他类库时,会不会发生问题。比如不重写equals,很多集合类库方法会出问题。
第8条:改写equals时请遵守通用约定
∷一个完善的equals方法要考虑自反性,对称性,传递性,一致性,特别是域为对象时,前面概念还要“递归进去”。
∷千万注意不要写成 public boolean equals(Myclass o)就悲剧了
※其实自己写很是麻烦的,考虑细节不少。根据不重复造轮子的原则,“聪明人”都用Apache Commons节省时间。包括下面的hashCode和toString同样适用,当然使用前研修一下里面的代码就更好了。
※从逻辑意义上,equals的间接地表明所有数据型对象必须要有key。类型和key能够唯一判定一个对象。常常在想,SUN当初怎么不弄一个Key关键字作为语言要素呢
第9条:改写equals时总要改写hashCode
∷hashCode涉及到集合类中为提高性能写的代码,如果hashCode不被正确地重写,很多集合类方法会出错。
第10条:始终要改写toString
∷应该作为一个良好的编程保持,一般将该对象的“KEY”内容格式化文字串
※主要应用在还是用Apache Commons吧
第11条:谨慎地改写clone
∷克隆主要关注的是可变类的复制(基本型和不可变型不需特别考虑)。当克隆对象含有数组,List等可迭代对象时,必须深入
到该结构中逐个克隆。
∷特别地Cloneable是一个标记接口,JDK1.6以后允许clone返回非Object对象(SUN公司给了特别通行证?)
@Override
public PhoneNumber clone(){
try{
return (PhoneNumber)super.clone();
} catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
※根据接口描述如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出
CloneNotSupportedException 异常。(又是一个挺奇怪的“约定”)
第12条:考虑实现Comparable接口
∷专为 Arrays.sort()排序而用,重点注意要与equals不矛盾(还是那句话懒人都用Apache Commons)
第4章 类和接口
第13条:使类和成员的可访问性最小化
∷为使访问级别(private,protected,public,包级)不会滥用一个简单方法是把说有成员都设为private,再逐步根据需要调整。
∷提放暴露数组,LIST等可从外部修改的变量,影响安全性(可用 Collections.unmodifiablelist做保护)
第14条:在公有类中使用访问方法而非公有域
∷JAVA Bean的典型特性
×(反例)java.awt中 Point和Dimension类 属性错误地使用public
第15条:使非可变性最小化
∷尽可能使用final
※方便的IDE工具容易生成出不该有的get,set方法,做这一步的时候需要慢一点再慢一点
第16条:复合优先于继承
∷继承虽然在重用方面效果显著,却有一些缺点:打破封装性,无法避免超类的“意外”扩展等。只有两者关系完全是“is-a”,即逻辑包含关系才可用继承。Java平台反例:Stack不应扩展Vector,Properties不应扩展Hashtable。
※实际项目中,继承类往往都是可高度重用的部分,应该都有底层开发人员或架构师统一考虑
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
∷强化了上一条,大多数继承都是精心策划为提高复用性而来。测试方法是编写子类,经验表明3个子类较合适。
一般性规则:构造器不可调用被覆盖的方法。
第18条:接口优于抽象类
∷相对于抽象类只能继承一个,接口可以有任意中组合,灵活性更强。
※JAVA的面向接口编程毋庸置疑所以这两个本不是一个“量级”的东西,不过抽象类在设计模式还是有用武之地的。
第19条:接口只用于定义类型
∷在接口中干任何别的事情(如定义写常量等)都会显得过于另类,不宜尝试
第20条:类层次优于标签类
※个人认为这个属于设计问题,一个类的杂合了太多东西肯定要重新设计或重构了
第21条:用函数对象表示策略
※与设计模式相关的条目,对java.util包里的 Arrays.sort(T[] a, Comparator super T> c) 及函数指针做了阐述。
第22条:优先考虑静态成员类
∷四种嵌套类
静态成员类(只有一个实例,比较节省资源),
非静态成员类(隐含外围实例,典型用法如Adapter),
匿名类(当且仅当在非静态环境中定义,才有外部实例。不能包含静态成员。典型用法创建函数对象如Compatator),
局部类(类匿名类,没有典型用法)
第5章 泛型
第23条:请不要在新代码中使用原生态类型
∷既然可以享受类型安全和消除类型强制转换的好处,没有理由不使用泛型。
∷两个例外:类文字必须使用原生态类型,关键字instanceof 可以也使用原生态
第24条:消除非受检警告
∷良好的编程习惯,并且竟可能小范围地使用@SuppressWarnings("unchecked"),同时加注释说明。
第25条:列表优先于数组
∷原因是数组的协变性(即类型检查在运行期做)与泛型相违,不宜一起使用。
∷用LIST
第26条:优先考虑泛型
※复用性增强的利器。实际项目中往往是做底层类和框架类考虑较多。
第27条:优先考虑泛型方法
∷泛型单例工厂(generic singleton factory)
例:
public interface UnaryFunction
T apply(T arg);
}
// Generic singleton factory pattern
private static UnaryFunction
@SuppressWarnings("unchecked")
public static
return (UnaryFunction
}
public static void main(String[] args){
String[] strings ={"jute","hemp","nylon"};
UnaryFunction
for (String s : strings)
System.out.println(sameString.apply(s));
Number[] numbers ={1, 2.0, 3L};
UnaryFunction
for (Number n : numbers)
System.out.println(sameNumber.apply(n));
}
※读泛型的程序特别是好的程序是一件非常有趣的事情,同时对提高编程能力又很有帮助,比如在JDK的集合框架中就有很多。
第28条:利用有限制通配符来提升API的灵活性
∷PECS(producer-extends,consumer-super) 生产者extends 消费者super(Comparable是典型的消费者)
∷类型推导有相当的复杂性(※本条似乎还不够深入)
例:显示类型参数
public static
Set
Set
Set
∷类型参数和通配符之间具有双重性,即以下两种声明皆可。
例:
public static
public static void swap(List> list,int i, int j);
=>虽然第二种更简单,但是开发时它还需要借助第一种作为辅助类才能编译。
public static void swap(List> list,int i, int j){
swapHelper(list, i, j);
}
private static
list.set(i, list.set(j, list.get(i)));
}
第29条:优先考虑类型安全的异构容器
∷以下的类所有键可是不同类型,并且类型安全。
∷局限性不能用在不可具体化类上(如:List
例:
public class Favorites{
private Map
public
if (type == null)
throw new NullPointerException("Type is null");
favorites.put(type, type.cast(instance));
}
public
return type.cast(favorites.get(type));
}
}
第6章 枚举和注解
※本章的enum的例子有很强参考价值。内容非常精彩,所以条目不做总结,看书就好了,几乎可以知晓enum能做的一切。
※enum内部可用抽象方法,外部可用接口实现,特别是还有EnumSet(代替位域,简单地说即多重选项),EnumMap(让分类代码更完美)协助。只有一个限制,无法再继承。
第30条:用enum代替int常量
第31条:用实例域代替序数
第32条:用EnumSet代替位域
第33条:用EnumMap代替序数索引
第34条:用接口模拟可伸缩的枚举
※是不错,不过一直没实际应用过
第35条:注解优先于命名模式
∷这里基本是在讲JUnit的发展史,JUnit3到JUnit4的主要实现区别。
第36条:坚持使用Override注解
※特别是equals上,简单有效防止误用
第37条:用标记接口定义类型
∷标记接口是没有包含方法声明的借口(如:Serializable)。
∷如果标记是应用到任何程序元素而不是类或接口,就必须用注解。如果标记只应用给类和接口,并且还要编写一个或多个只接受这种标记的方法就用标记接口,反之如果要永远限制这个标记只用于特殊接口的元素就使用改接口的子接口,都不是的场合用注解。
※这一条有点难,各人领悟
第7章 方法
第38条:检查参数的有效性
∷应提前构想方法的参数合法性,并反映到文档中,对尽可能对内部函数使用 Assert。
第39条:必要时进行保护性拷贝
∷与安全性相关的条目,当构造器参数为可变对象时,应复制后,对复制对象进行检查及后续操作
∷内部组件被返回给客户端之前,同样道理也应该做保护性拷贝。
第40条:谨慎设计方法签名
∷谨慎地选择方法的名称。不要过于追求提供便利的方法。避免过长的参数列表。
※貌似简单却见功力的一条,项目中方法名应当有详细的约定
第41条:慎用重载
∷特别注意参数有继承关系的方法,会导致隐晦的结果。编译期对类型的判定,不会再运行期改变。
×反例 String类导出两个重载的静态工厂方法: value(char[])和valueOf(Object)当他们被传递了同样的对象引用时,所做的事情完全不同。易引起混乱。
第42条:慎用可变参数
∷使用场合仅仅限于非常单一功能(如打印)
第43条:返回零长度的数组或者集合,而不是null
∷这个如果作为项目组一开始的约定可以节省很多代码,注意一些共通空集的常量定义必不可少。
第44条:为所有导出的API元素编写文档注释
∷优秀coder的必要条件
第8章 通用程序设计
第45条:将局部变量的作用域最小化
∷JAVA特色,也是面向对象语言的共性。变量声明尽量延后,用的时候现声明,现初始化。
第46条:for-each循环优先于传统的for循环
※这条与给懒人用得,和43条共同使用效果更佳
第47条:了解和使用类库
※哎,在所有里面最“废话”的一条
第48条:如果需要精确的答案,请避免使用float和double
※看到float和double就看到误差,简单1+1都算不对
第49条:原语类型优先于装箱的原语类型
∷主要是基于效率考虑,能不用对象时,就不要写Integr,Long等
第50条:如果其他类型更适合,则尽量避免使用字符串
∷笔者的意思是数据需要精确表现时,一定不会是字符串。换句话说,字符串使用于那些不太重要的项目。
第51条:了解字符串连接的性能
∷又是一个与性能相关的条目,拼字符串要用StringBuilder 性能高很多
第52条:通过接口引用对象
∷增强灵活性的法宝
第53条:接口优先于反射机制
∷接口应该是业务级的,反射是架构级的,应该予以区分。
第54条:谨慎地使用本地方法
∷当然是能不用就不用了。
第55条:谨慎地进行优化
∷重构应该随时进行,但是不是性能出现问题,不要太刻意去优化(存在风险不小)。设计和算法选择比优化更为重要。
第56条:遵守普遍接受的命名惯例
※coder等级越高越会遵守约定,好处也很明显,代码可读性更好
第9章 异常
第57条:只针对异常的条件才使用异常
∷使用异常来实现某种正常逻辑是得不偿失的。
※一般来讲正常与异常是完全并行的两条线路,这里Bloch委婉地再次重申良好的实现一定是“笨拙地”,即投机取巧的“创新代码”往往维护性差而走向反面。
第58条:对可恢复的条件使用受检异常,对编程错误使用运行时异常
∷难点在于如何判定是否是可恢复的,需要根据具体情况分析。
第59条:避免不必要地使用受检的异常
∷受检的异常一定会给使用者造成负担,应慎重考虑是否这样做。(还可考虑状态测试方法,非并发场合)
第60条:尽量使用标准的异常
∷Jdk中的异常的往往看起来更标准更易理解,经验者会把最常用的记在可查阅的地方。
第61条:抛出与抽象相对应的异常
∷本节涉及到异常转义,如果底层信息有保留价值则使用异常链(构造器参数为底层异常)
第62条:每个方法抛出的所有异常都要有文档
∷又一条与文档相关的良好习惯: 检查受检异常的@throws是否遗漏
第63条:在细节消息中包含失败-捕获信息
∷抛出异常时要考虑,看的人是否容易理解,同时也需要再异常构造器中设置足够的参数。
第64条:努力使失败保持原子性
∷比较考验设计功力,尽量将改变状态的部分放到异常可能出现之后的地方。异常后,保持可恢复状态。
∷一般性策略:充分的参数检查,缓存中间结果一次性提交。对于并发本分一般是不可恢复,就不需考虑了。
第65条:不要忽略异常
∷catch后别忘了处理,后果很严重
第10章 并发
※由于篇幅有限作者多次强调,更详细的内容参阅《Java Concurrency in Practive》。但是这几条内容少而精,还是非常值得留意的。
第66条:同步访问共享的可变数据
∷缩小同步代码范围有一些技巧:压缩共享可变数据的代码范围,计数使用AtomicXXX类,共享状态使用volatile变量
第67条:避免过多同步
∷同步代码中包含可能被客户端覆盖的方法,对应方法有:使用CopyOnWriteArrayList,给列表做快照
×反例 String是不可变对象,无需同步,StringBuffer却内部做同步,性能变差。一般使用 StringBuilder代替。
第68条:executor和task优先于线程
∷线程池框架的选择:
小程序考虑 Exectutors.newCachedThreadPool
大负载使用Exectutors.newFixedThreadPool
带定时功能使用ScheduledThreadPoolExecutor
第69条:并发工具优先于wait和notify
∷直接使用Thread如同直接使用汇编语言一样,java.util.concurrent才是“高级语言”。
∷并发集合性能更佳。ConcurrentHashMap 比起Collections.synchronizedMap和Hashtable性能更佳。
∷同步器各有擅长的场合:
CountDownLatch 倒计数锁存器,允许一个或多个线程等待一个或多个其他线程来作某些事情。
Semaphore 一个计数信号量,通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
CyclicBarrier 允许一组线程互相等待,直到到达某个公共屏障点。
Exchanger 可以在对中对元素进行配对和交换的线程的同步点。
∷System.nanoTime 比起System.currentTimeMillis更准确更精确,且不受系统的实时时钟的调整所影响。
∷如果用Thread,notifyAll较notify 保险一些。
第70条:线程安全性的文档化
∷线程安全级别有以下几种
不可变的(immutable)--不需要同步 如String,Long,BigInteger
无条件的线程安全(unconditionally thread-safe)--可变但是内部安全充分 如Random,ConcurrentHashMap
有条件的线程安全(conditionally thread-safe)-- 如:Collenctions.sysnchronized集合的迭代器要求外部同步
非线程安全(no thread-safe)--如:ArrayList,HashMap
线程对立的(thread-hostile)--即使外部同步还是不能安全并发。非常少见,如:System.runFinalizersOnExit
∷锁对象应当封装在同步对象内部,防止外部的拒绝服务攻击
第71条:慎用延迟初始化
∷对于实例域,使用双重检查模式,对于静态域使用lazy initialization holder calss idiom
例:
// lazy initialization holder calss idiom
private static class FieldHolder{
static final FieldType field = computeFieldValue();
}
static FieldType getField(){ return FieldHolder.field; }
// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField(){
FieldType result = field;
if ( result == null ){ // First check(no locking)
synchronized(this){
result = field;
if (result == null) // Second check(with locking)
field = result = computeFieldValue();
}
}
}
第72条:不要依赖于线程调度器
∷任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。
∷最好确保可运行线程的平均数量不明显多于处理器的数量。
∷如果线程没有在做有意义的工作,就该sleep。忙-等状态会使CUP效率低下。
∷Thread.yield没有可测试的语义。唯一的用途是在测试期间人为地增加程序的并发性。
第73条:避免使用线程组
∷即ThreadGroup,基本无用的类
第11章 序列化
※序列化最棘手的问题是考虑安全性问题,应为任何攻击者都可能会篡改这个序列。
※java.io.Serializable 接口的特殊方法:(个人认为这部分有点不规范)
//在序列化和反序列化过程中需要特殊处理的类必须使用下列准确签名来实现特殊方法
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
//在从流中读取类的一个实例时需要指定替代的类应使用的准确签名来实现此特殊方法
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
//发送者的类的版本号(版本不同可导致InvalidClassException) 这个eclipse的提示下,大多数人都知道
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
第74条:谨慎地实现Serializable
∷实现Serializable接口后的代价:类中私有级和包级的变量都编程导出API的一部分,违反了第13条。版本变更时,为使客户端兼容,必须要设计一种高质量的序列化形式(见第75,78条)。序列化机制是一种语言外的对象创建机制,所以要考虑由真正的构造器建立起来的约束关系。测试成本提高。
∷内部类不应该实现Serializable。然而,静态成员类却可以实现Serializable接口。
第75条:考虑使用自定义的序列化形式
∷默认的序列化有几个缺点:它使这个类的导出API永远地束缚在该类的内部表现法上,消耗过多的空间和时间,还会引起栈溢出。
∷注意如果读取对象的方法上有被同步的方法,必须在对象序列化上加同步。
第76条:保护性地编写readObject方法
∷readObject相当于一个公有的构造器,以下几条指导方针
▪对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的对象。不可变类的可变组件就属于这一类。
▪ 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟所在的保护性拷贝之后。
▪如果这个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
▪无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。
∷不要使用ObjectOutputString中的writeUnshared和readUnshared方法,不能提供必要的安全保护
第77条:对于实例控制,枚举类型优先于readResolve
∷应尽可能使用枚举类型来实施实例控制的约束条件。不行的话,需要实现readResolve方法,并确保该类的所有实力域都为基本型或是transient的。
第78条:考虑用序列化代理代替序列化实例
∷使用场合 必须在一个不能背客户端扩展的类上编写readObject或者writeObject方法的时候
∷实现方法:首先,为可序列话的类设计一个私有的静态嵌套类(即序列化代理),精确地表示外围类的实例的逻辑状态。这个类应该有一个单独的构造器,其参数类型就是外围类。并和外围类同时声明实现Serializable 接口。
∷优点:不必显示地执行有效性检查。允许反序列化类实例有与原始序列化实例不同的类,如EnumSet。
∷局限:它不能与可以被客户端扩展的类兼容。另外性能稍逊。
例:
private static class SerializationProxy implements Serializable{
private final Date start;
private final Date end;
SerializationProxy(Period p){
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID = ...;
}
private Object writeReplace(){
return new SerializationProxy(this);
}
// 防止攻击者伪造序列化系统
private void readObject(ObjectInputStream stream) throws InvalidObjectWxception {
throw new InvalidObjectException("Proxy required");
}