《Effective Java》笔记(下)
Enums and Annotations
- Item 30: Use enums instead of int constants
- 类型安全
- 可以为常量提供数据和方法的绑定
- 可以遍历
- 实现建议
- 如果是通用的,应该定义为top level enum,否则应定义为内部类
- constant-specific method implementations
```java
// Enum type with constant-specific method implementations
public enum Operation {
PLUS { double apply(double x, double y){return x + y;} },
MINUS { double apply(double x, double y){return x - y;} },
TIMES { double apply(double x, double y){return x * y;} },
DIVIDE { double apply(double x, double y){return x / y;} };
abstract double apply(double x, double y);
}
```
+ 结合constant-specific data
```java
// Enum type with constant-specific class bodies and data
public enum Operation {
PLUS("+") {
double apply(double x, double y) { return x + y; }
},
MINUS("-") {
double apply(double x, double y) { return x - y; }
},
TIMES("*") {
double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
abstract double apply(double x, double y);
}
```
+ If switch statements on enums are not a good choice for implementing con- stant-specific behavior on enums, what are they good for? Switches on enums are good for augmenting external enum types with constant-specific behavior.
- A minor performance disadvantage of enums over int constants is that there is a space and time cost to load and initialize enum types.
- 所以,在安卓设备(手机、平板)上,应该避免使用enum,减小空间和时间的开销
- Item 31: Use instance fields instead of ordinals
- 每个enum的常量都有一个
ordinal()
方法获取其在该enum类型中的位置,但该方法只应该在实现EnumSet
,EnumMap
等类型的时候被使用,其他情形都不应该被使用 - 如果需要为每一个常量绑定一个数据,可以使用instance field实现,如果需要绑定方法,则可以用constant-specific method implementations,参考上一个item
- Item 32: Use EnumSet instead of bit fields
- bit fields的方式不优雅、容易出错、没有类型安全性
- EnumSet则没有这些缺点,而且对于大多数enum类型来说,其性能都和bit field相当
- 通用建议:声明变量时,不要用实现类型,应该用接口类型,例如,应该用
List<Integer>
而不是ArrayList<Integer>
- EnumSet并非immutable的,可以通过
Conllections.unmodifiableSet
来封装为immutable,但是代码简洁性与性能都将受到影响 - Item 33: Use EnumMap instead of ordinal indexing
- 同前文所述,应该避免使用ordinal。当需要用enum作为下标从数组获取数据时,可以换个角度思考,以enum作为key从map里面获取数据。
- 数组和泛型不兼容,因此使用数组也会导致编译警告;而且ordinal的值本来就不是表达index含义的,极易导致隐蔽错误
- EnumMap内部使用数组实现,因此性能和数组相当
- 使用数组也会导致程序可扩展性下降,考虑以下两种实现
// Using ordinal() to index array of arrays - DON'T DO THIS!
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// Rows indexed by src-ordinal, cols by dst-ordinal
private static final Transition[][] TRANSITIONS = {
{ null, MELT, SUBLIME },
{ FREEZE, null, BOIL },
{ DEPOSIT, CONDENSE, null }
};
// Returns the phase transition from one phase to another
public static Transition from(Phase src, Phase dst) {
return TRANSITIONS[src.ordinal()][dst.ordinal()];
}
}
}
// Using a nested EnumMap to associate data with enum pairs
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
final Phase src;
final Phase dst;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase,Transition>> m =
new EnumMap<Phase, Map<Phase,Transition>>(Phase.class);
static {
for (Phase p : Phase.values())
m.put(p,new EnumMap<Phase,Transition>(Phase.class));
for (Transition trans : Transition.values())
m.get(trans.src).put(trans.dst, trans);
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
}
当需要增加Phase
时,前者需要谨慎地修改TRANSITIONS
数组的内容(这一步骤容易出错),而后者则只需要增加相应Transition
即可,from
函数的逻辑完全不受影响。
- Item 34: Emulate extensible enums with interfaces
- 当enum遇到可扩展性时,总是一个糟糕的问题;扩展类是基础类的实例,但反过来不是,这一点很让人困惑;想要枚举所有基础类和扩展类的enum对象时,并没有一个很好地办法;
- 而对于可扩展性的需求,是真实存在的,例如:operation codes (opcodes)
- 实现方式是通过定义一个接口,enum类型(基础与扩展)均实现该接口,而在使用enum的地方,接收这个接口作为参数
- enum类型是不可扩展的,但是interface具备可扩展性,如果API使用接口而非实现去代表operation,API就有了可扩展性
- 泛型高级用法:
<T extends Enum<T> & Operation> ... Class<T>
,T类型是enum类型,且是Operation
子类 - 这一方式的不足:enum类型对接口的实现是不能继承的
- Item 35: Prefer annotations to naming patterns
- 在1.5之前,naming patterns很常见,在JUnit中都是这样,例如要求测例方法一
test
开头 - naming patterns有很多问题
- 拼写错误不能及时发现
- 无法保证naming patterns只在正确的场景使用,例如可能有人以
test
开头命名测例类,方法却没有,JUnit则不会运行测例 - 没有值/类型信息,编译器无法提前发现问题
- 使用annotations可以很好的解决这些问题,但是annotations的功能也是有限的
-
@Retention(RetentionPolicy.RUNTIME)
能限定其保留时期 -
@Target(ElementType.METHOD)
能限定其应用的程序元素 - 还有其他meta-annotations,如
@IntDef
- annotations接收的参数如果是数组,为其赋值一个单独的元素也是合法的
- Item 36: Consistently use the Override annotation
-
@Override
会使得重写的准确性得到检查 - 重载和重写的区别:一个只是函数名一样,通过参数列表决定执行哪个版本,是编译时多态;一个是通过虚函数机制实现,是运行时多态;
- Item 37: Use marker interfaces to define types
- 定义一个空的接口,表明某个类型的属性,例如
Serializable
- 另一种方式是使用annotation,表明者其具有某种属性
- marker interface的优点
- 定义了一个类型,可以进行instanceof判断,可以声明参数类型
- 比annotation更简洁
- marker annotation的优点
- 当一个类型(通过interface或者annotation)被声明后,如果想要加入更多的信息,annotation更方便,即annotation对修改是开放的,因为它的属性可以有默认值,而interface则不行,定义了方法就必须实现
- annotation可以被应用到更多代码的元素中,不仅仅是类型
- 实现建议
- 如果仅仅只应用于类型,则应该优先考虑annotation
- 如果希望mark的对象被限定于某个接口的实例(即为一个接口增加另外一种语义,却不改变其API),可以考虑使用marker interface
函数
- Item 38: Check parameters for validity
- 一个函数(包括构造函数)首先要做的事情就是验证参数合法性,如果不合法则应该抛出相应异常,这是对“尽早发现错误尽早抛出”原则的遵循,否则等到错误发生时将可能难以判断错误的根源所在,甚至程序不会显式报错,而是执行了错误的行为,导致更严重的后果
- 不由被调用函数使用,而是存起来留作后用的参数,更加要检查其合法性
- Javadoc里面应该注明
@throw
项,并说明原因 - 非公开的API(private或package private),则不应该通过抛异常来报错,应该采用
assert
,assert可以通过配置虚拟机参数开启或关闭,如果关闭则不会被执行 - 灵活运用,设计API时,就应该尽量设计得通用一些,即可以接受更大范围的参数,毕竟检查参数也是有开销的
- 另外可以考虑抛出
RuntimeException
的子类,因为这样的异常不用放到函数的异常表中,函数的使用者也不用必须try-catch
或者throw
,但doc一定要写明 - Item 39: Make defensive copies when needed
- 编码一大原则:永远不要信任用户(调用方)输入的数据,也不要信任它们不会篡改返回的数据,因此defensive copy很有必要
- 编写一个类时,如果成员变量是mutable的,那么就需要在构造函数(或者setter)中进行深拷贝,并且,先拷贝,再验证已拷贝数据的合法性(既不是先验证,也不是验证传入的数据,避免TOCTOU attack)
- 另外深拷贝时,传入对象的类如果不是final的,就不能用clone方法进行拷贝,因为不能保证clone方法返回的就正好是这个类的实例(有可能会是恶意的子类)
- 为mutable成员提供getter方法时,返回前也要进行深拷贝,但此时可以用clone方法,因为我们确定成员就是我们想要的类的对象
- java内建的Map, Set等容器,实现上是没有进行深拷贝的,因为是泛型,所以put进去或者get出来的时候,编译期都不知道具体是什么类型,是无法调用构造函数的,如果想要测试这一问题,需要确定key和value的类型都是mutable的,如果测
Map<String, Integer>
,那结果肯定是错误的,但如果测Map<StringBuilder, Date>
,就可以知道确实如此;所以如果要把用户传入的数据放入Map,且key/value是mutable的,那么就需要在put之前进行深拷贝,否则可能会被用户attack - 长度非零的数组都是mutable的
- 尽量使用immutable的成员就可以省去深拷贝带来的性能开销
- 如果确实信任用户,就可以把深拷贝省去,但一定要在文档内说明,例如:wrapper模式,如果用户恶意,那损害的也就仅仅是其自身;或者用户都是自己的代码,可以确信安全。
- Item 40: Design method signatures carefully
- 命名要合理,可理解:清除表达函数的功能;符合常识;保持风格一致;
- 类/接口的成员方法数量不要太多,否则会令人难以理解,而且不利于测试、维护
- 不要随便提供helper方法,只有当很有必要时才提供
- 避免过长参数列表(不多于4个),尤其是参数类型相同,否则既难记(倒还好),又可能引起隐晦的bug(传入参数顺序错了,编译不报错,运行时行为确是错的)
- 可以通过把参数列表过长的方法拆分为几个方法,但要避免导致方法过多
- 创建helper类,容纳作用相关联的的参数
- 类似于构造对象的Builder模式,为函数的调用创建一个builder
- 参数类型,使用interface,而不是实现类
- 对于起控制作用的参数,使用二值enum,而不是boolean,便于扩展;对于安卓来说,可以通过
@IntDef
辅助定义int常量,模拟enum - Item 41: Use overloading judiciously
- 慎用重载,重载(overload)与重写(override)的区别可以见上文,简言之,前者编译时多态,后者运行时多态
- 重载是编译时多态,版本选择在编译期完成,根据编译期参数的类型信息来进行决策
- 建议不要用参数类型来设计不同的重载版本,应该通过参数列表长度,或者没有父子类关系的不同参数类型,例如接受int和float的类型,后者也还是可能会有问题
- Item 42: Use varargs judiciously
- varargs的原理是调用时首先创建一个数组,然后把传入的参数放入数组,数组长度为参数个数
- 一个方法需要0或多个同类型数据这个需求很常见,然而也有另一个很常见的需求:需要一个或多个同类型数据,此时单纯用varargs不太优雅,可以让方法先接受一个数据,在接受一个varargs
- varargs最初是为了printf和反射设计的
- 可以通过把传入参数从一个数组改为varargs,来改良该方法(变得更灵活),而且对已有代码“无影响”,
Arrays.asList
便是一个例子,但接受varargs最初是为了打印数组内容设计的,而不是为了把多个数据变成一个List - Don’t retrofit every method that has a final array parameter; use varargs only when a call really operates on a variable-length sequence of values.
- 以下两种函数声明都可能会产生问题:
```java
ReturnType1 suspect1(Object... args) { }
<T> ReturnType2 suspect2(T... args) { }
```
如果传入一个基本类型的数组进去(例如int[]),那么这两个方法接受的都是一个int[][],即相当于接受了一个只有一个元素的数组,而这个数组的数据类型是int[]!而如果传入一个对象的数组,则相当于传入了数组长度个数的varargs。`Arrays.asList`方法就存在这个问题!
- varargs也存在性能影响,因为每次调用都会创建、初始化一个数组。如果为了不失API灵活性,同时大部分调用的参数个数都是有限个,例如03个,那么可以声明5个重载版本,分别接受03个参数,另外加一个3个参数+varargs的版本
- Item 43: Return empty arrays or collections, not nulls
- 可能有人认为返回null能减小内存开销,然:
- 永远不要过度考虑性能问题,只有当profiling显示瓶颈就是这里的时候,再考虑性能优化与代码优雅性的牺牲,当然,无副作用的优化肯定尽早采纳
- 可以每次需要返回空数组/集合时,返回同一个空数组/集合,这样就只需要一次内存分配
- Collection的
<T> T[] toArray(T[] a)
方法,可以每次调用时传入一个空数组,因为该方法保证如果集合元素可以被放入提供的参数数组中,将不会分配新内存,当放不下时才会分配 - 下面实现返回集合的值的方式也是值得借鉴的
// The right way to return a copy of a collection
public List<Cheese> getCheeseList() {
if (cheesesInStock.isEmpty())
return Collections.emptyList(); // Always returns same list
else
return new ArrayList<Cheese>(cheesesInStock);
}
- Item 44: Write doc comments for all exposed API elements
- 对于API暴露的部分(类、接口、方法、成员等),都应该先写好文档;为了提高代码的可维护性,未暴露的部分也应该写好文档;
- 每个方法的文档的内容,应该是描述该方法与调用者之间的约定,不必是实现细节,细节可以看代码,约定则是使用者关心的东西;设计为被继承的类,方法文档应该描述该方法做了什么,而不是怎么做的;
- 方法的文档中,应该描述约定的前提条件,执行后产生的影响,尤其是对于“系统”(或者说这个对象)状态的影响;不符合前提条件的情形将抛出异常;
- 更多细节
-
@param
,@return
,@throws
描述不要句号结尾 -
@throws
的描述应该以if开头,其他都应该是名词描述 -
@{code}
与@{literal}
- 有泛型时,需要说明每个类型参数
- enum类型要为每个常量注释含义
- annotation的定义,要为每个成员/参数注释含义
- 线程安全性说明,可见性说明,序列化说明
编程通用
- Item 45: Minimize the scope of local variables
- 在变量第一次使用的时候进行声明,声明时尽量就进行初始化
- 因此也更倾向于使用for-loop,而不是while-loop,因为后者需要使用while-loop外定义的控制变量
- for-loop的终结条件变量n,也应该在循环变量i初始化时计算,避免重复计算
- 保持方法简短,一个方法只做一件事
- Item 46: Prefer for-each loops to traditional for loops
- 优点之一:可以避免一些容易犯的bug
```java
// Can you spot the bug?
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING }
...
Collection<Suit> suits = Arrays.asList(Suit.values());
Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<Card>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
```
`i.next()`在内层循环被调用了多次。以下写法则直观且不易出错:
```java
// Preferred idiom for nested iteration on collections and arrays
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));
```
- 此种写法不仅可用于集合和数组,任何实现
Iterable
接口的类都可以用于冒号后面的部分 - 缺点
- 有性能代价!一定会创建Iterator,对于安卓开发,不建议如此。
- 不能在for-each语法中进行remove,用Iterator遍历时,能remove
- 遍历过程中替换原有元素
- Parallel iteration
- Item 47: Know and use the libraries
- don't reinvent the wheel
- 视野!
- Item 48: Avoid float and double if exact answers are required
- float和double设计为用于科学计算,“精确近似”,需要确切结果的,不要使用,例如:货币相关!应该使用BigDecimal, int, 或者long。
- BigDecimal使用有些不方便,性能也比primitive类型低
- Item 49: Prefer primitive types to boxed primitives
- 两种类型的区别
- boxed类型,除了包含数值外,还有不同的唯一标示,即值一样,对象可以不一样,这一点很重要!
- boxed类型,比primitive类型多一个值,null
- boxed类型,时间、空间效率均低一些
- caveats
- 有些操作会auto-unbox,例如:加减乘除,大小比较,但判等(
==
)不会! - Applying the
==
operator to boxed primitives is almost always wrong. - boxed类型,值为null时,会unbox为什么呢?会抛出
NullPointerException
- 当boxed和primitive出现在同一个运算中,boxed类型会auto-unbox(包括判等)
- 大量重复的box/unbox会导致性能大幅下降
- 使用场景与注意事项
- 放到标准集合里面,必须是boxed类型
- 作为类型参数(泛型),必须是boxed类型
- auto-box是安全的,也能省去繁琐的代码,但是auto-unbox则可能引起隐蔽的错误
- Item 50: Avoid strings where other types are more appropriate
- Strings are poor substitutes for other value types. 只有当数据确实就是文本时,才适合用String。
- Strings are poor substitutes for enum types.
- Strings are poor substitutes for aggregate types. 把一系列数据转化为一个String(序列化),然后再反序列化,也应该用Json,如果自定义分隔符,既不优雅,也不安全。
- Strings are poor substitutes for capabilities. capability是一种称呼,通常就是说不同的对象,凭借一个key去同一个地方保存、获取数据;如果用String,那么如果内容相同,那key就会冲突,不安全;ThreadLocal的发展史*。
- Item 51: Beware the performance of string concatenation
- 用
+
连接n个String,时间复杂度为O(n^2)
,因为String是immutable的,所以每次拼接都会拷贝两者的内容 - 使用
StringBuilder
进行拼接操作;不过对于安卓开发来说,基本没什么影响,因为在打包的过程中,这一优化会自动完成; - Item 52: Refer to objects by their interfaces
- 如果有接口,那么函数参数、返回值、成员变量、局部变量,都应该使用接口来保持对象的引用,只有在通过构造函数创建对象时才应该引用具体的实现类型;面向接口编程更广义的实践;
- 面向接口编程使得程序更加灵活,切换实现类非常简单;但如果代码功能/正确性依赖于实现类特有的特性,那么切换时就需要仔细考虑一下;
- 当然,如果对应功能的接口不存在,那直接引用该类当然是可以的;value type; class-based framework; 或者实现类提供了接口不存在的功能
- Item 53: Prefer interfaces to reflection
- 反射可以访问私有成员
- 反射可以调用编译时不存在的类的方法,当然需要运行时已经加载
- 但是反射也是有代价的
- 编译期的类型检查完全失效,类型安全性丧失
- 反射代码繁琐且易出错,当然这一点有一些好的框架可以避免,例如JOOR
- 性能下降,反射调用性能会低很多
- 反射常用的场景
- class browsers, object inspectors, code analysis tools, and interpretive embedded systems, remote procedure call (RPC) systems
- 反射功能强大,也有一些不足,如果合适利用,还是非常方便的
- 例如编译期有些类尚未获得,但是如果有其父类/接口,则可以声明为父类/接口,只通过反射创建实例,其余代码都无需反射
- Item 54: Use native methods judiciously
- 设计之初的三大用途
- 访问平台相关的功能,例如registries and file locks
- 访问老的C/C++版本的库,访问老的数据
- 追求性能
- 近年来JVM/Java的发展,性能已有很大改善,追求性能而使用JNI通常来说都已经没必要了
- JNI的劣势
- 不安全,内存管理不受JVM控制了,溢出等问题都有可能发生了
- 平台相关
- 难以调试
- Java和native层的交互是有开销的
- native代码比Java代码更难懂
- 对于安卓应用开发来说,JNI还有一点就是隐藏实现,Java代码反编译非常容易,而native代码则难一些
- Item 55: Optimize judiciously
- 只有当确实需要时,才考虑性能优化,当然一些常见的范式,初次编码时就应该遵循
- Strive to write good programs rather than fast ones; speed will follow.
- Strive to avoid design decisions that limit performance.
- Consider the performance consequences of your API design decisions.
- It is a very bad idea to warp an API to achieve good performance.
- 当确实需要优化性能时:measure performance before and after each attempted optimization.
- 找到原因后,首先考虑的是算法的优化,然后是上层的优化
- 在进行优化前,对程序进行profiling,确定瓶颈,否则可能浪费精力反而性能下降
- Item 56: Adhere to generally accepted naming conventions
- 包名要体现出组件的层次结构,全小写
- 公布到外部的,包名以公司/组织的域名开头,例如:edu.cmu, com.sun
- ...
异常处理
- Item 57: Use exceptions only for exceptional conditions
- exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow.
- A well-designed API must not force its clients to use exceptions for ordinary control flow.
- 如果一个类的某个方法,依赖于该类当前处于某个特定状态,则应该提供一个单独的状态检查方法,例如Iterator的next和hasNext方法
- 另外如果不提供状态检查方法,也可以让方法在异常状态下,返回一个特定的非法值
- 如果该类被并发访问,且访问时未进行互斥处理,则必须使用返回非法值的方式;另外考虑到性能因素,也更倾向于返回非法值;其他情况下,都应该使用状态检查方法,可读性更好,更容易检查错误;
- Item 58: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
- use checked exceptions for conditions from which the caller can reasonably be expected to recover.
- unchecked exception:
RuntimeException
,Error
通常都不需要、也不应该catch - Use runtime exceptions to indicate programming errors. 通常用于表示程序运行的状态违背了前提条件,违背了API的约定
- all of the unchecked throwables you implement should subclass RuntimeException
- Item 59: Avoid unnecessary use of checked exceptions
- 如果即便合理的调用了API也会遇到异常情形,并且捕获异常之后能够进行一些有意义的操作,才应该使用checked exception,其他情况下都应该使用RuntimeException
- 通常,如果一个方法会抛出checked exception,都可以将其拆分为两个方法,一个用于判断是否会抛出异常,另一部分用于处理正常情况,如果不符合约定,就抛出RuntimeException,这样使得API更易用,也更灵活;但是要考虑状态检查和执行之间,是否可能从外部其他线程修改对象的状态;
- Item 60: Favor the use of standard exceptions
- IllegalArgumentException, IllegalStateException, NullPointerException, IndexOutOfBoundsException, ConcurrentModificationException, UnsupportedOperationException
- Item 61: Throw exceptions appropriate to the abstraction
- exception translation: higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction.
```java
// Exception Translation
try {
// Use lower-level abstraction to do our bidding
...
} catch(LowerLevelException e) {
throw new HigherLevelException(...);
}
```
- While exception translation is superior to mindless propagation of excep- tions from lower layers, it should not be overused.
- Item 62: Document all exceptions thrown by each method
- Always declare checked exceptions individually, and document precisely the conditions under which each one is thrown using the Javadoc @throws tag. 不要通过声明抛出多个异常的父类来实现抛出多种异常的效果。
- 要为每个方法可能抛出的unchecked exception写文档,但是不要将这些异常放到方法声明的异常表中去。便于API使用者区分checked和unchecked exception。
- 如果一个类的很多方法都抛出同一个异常,那么可以将文档放到class doc中,而不是method doc中。
- Item 63: Include failure-capture information in detail messages
- To capture the failure, the detail message of an exception should contain the values of all parameters and fields that “contributed to the exception.”
- 良好设计的Exception类,应该把它需要的详细信息都作为构造函数的参数,而不是统一接收String参数;这样将把生成有意义的detail信息的任务集中在了Exception类本身,而不是其使用者。
- checked exception可以为failure-capture information提供访问方法,以便于使用者在程序上进行恢复处理;虽然unchecked exception通常不会在程序中进行恢复,但是提供同样的方法也是建议的做法。
- Item 64: Strive for failure atomicity
- Generally speaking, a failed method invocation should leave the object in the state that it was in prior to the invocation. 满足此属性的方法称为 failure atomic。
- immutable对象是最简单的实现方法
- mutable对象要达到此效果,就需要在进行操作前,对所有的参数、field进行检查
- 有可能无法在函数的第一部分进行检查,但是一定要在对对象进行修改之前进行检查
- 还有一种不太常见的方式:函数内部捕获异常,异常发生之后先回退对象的状态,再把异常抛出去
- 还可以先创建一个临时的对象,在临时对象上进行操作,成功后替换原对象的值
- 有的情况下,failure atomic是不可能的,所以也就没必要为此做出努力了
- 有的情况下,为了failure atomic,会增加很多额外的开销、复杂度,也就不太必要了
- 当方法不满足failure atomic时,需要在文档中进行说明
- Item 65: Don’t ignore exceptions
- An empty catch block defeats the purpose of exceptions
- At the very least, the catch block should contain a comment explaining why it is appropriate to ignore the exception.
- 忽略异常,可能导致程序在其他不相关的地方失败/崩溃,这时将很难找到/解决根本问题
并发
- Item 66: Synchronize access to shared mutable data
-
synchronized
不仅是为了保证每个线程访问/执行时,看到的都是“正常状态”的对象(所谓正常就是没有发生多线程同时未加同步的写同一个对象,导致其状态不一致);还能保证每个线程看到的都是最新的对象; - Java语言保证了基本类型中除了long和double的访问都是原子性的,并发写这些类型的数据而不进行同步控制,也不会有问题
- 有人建议访问具有原子性操作属性的对象无需进行同步控制,还能提升性能,纯属一派胡言
- Java语言不会保证并发访问时,其他线程写的值能立即被读的线程感知,所以同步操作不仅仅是为了互斥访问,也是为了保证多线程之间看到的始终是最新的值
- 上述问题的根本原因就是Java memory model
- 一个简单、常见、易错的例子
- 如何停止后台线程?首先不能调用
Thread.stop
方法,这个方法会导致data corruption - 常用的方法就是用一个
boolean
变量,后台线程根据其值决定是否停止,而主线程想要停止后台线程时,修改这个变量的值即可 -
boolean
的读写操作是原子性的,并发访问不加同步,不会导致data corruption,但是却无法保证主线程对变量的修改能及时被后台线程感知,甚至无法保证能被感知 - 指令重排,如果
done
就是个普通声明的boolean
,以下变换在Java memory model下是允许的while (!done) i++; //==> if (!done) while (true) i++;
- 可想而知,如果未进行同步操作,后台线程将永远不会停止
- 解决方法有两种
- 为
done
的读写访问都加上synchronized
,注意,读写都需要,否则没有数据同步(communication)的效果;由于boolean
的读写访问是原子性的,所以这里的synchronized
仅仅起数据同步的作用; - 声明
done
的时候加上volatile
关键字,volatile
没有互斥的作用,仅仅是起数据同步的作用,在这里正好满足需求;这种方式性能比上一种要好一些; - 使用
volatile
需要格外谨慎,因为它并没有互斥作用,如果声明一个volatile int
,然后对其进行++
操作,那将会导致data corruption,因为++
不是原子性的 - 对于这种需求,可以声明为
synchronized int
;更好的方式是使用java.util.concurrent.atomic
包下的类,安全,高效; - 更根本的解决方式就是不要多线程共享mutable对象,而是共享immutable对象;甚至不要多线程共享数据;
- 引入框架/库时,需要考虑一下它们是否会引入多线程问题
- effectively immutable:对象不是真的immutable,但是对象分享出去之后,就不会再改变了;当然这个还是很危险的,因为并没有强制的机制保证不会被修改;
- 小结:多线程访问共享变量时,读和写都需要进行同步操作
- Item 67: Avoid excessive synchronization
- 在同步代码块中,不要调用可能被重写的方法,更不要调用使用者传入对象的方法,因为这些代码是不可控的,可能导致异常、死锁、data corruption
- 对于Observer模式中的observer list,Java 1.5之后有一个单独优化的高效并发容器:
CopyOnWriteArrayList
,每次写(添加、删除)操作都会从内部的数组创建一份新的拷贝,读(遍历)操作时完全不用加锁,对于读多写少的场景性能很好 - 一个总的原则是,在同步代码块中,执行尽可能少的操作;如果有耗时操作,应该在保证安全的前提下,尝试各种手段,将其移出同步块;
- 过度同步的性能影响
- 丧失了多核CPU的并行性,获得锁的开销倒是其次
- 任何时刻都需要保证每个CPU核心之间的数据同步,这有不小的开销
- 限制了JVM的代码优化空间
- 共享数据的并发访问,一定要保证线程安全;如果可以在类内部,通过少量/高效的同步块保证,就不要把整个类的任何操作都加锁;如果做不到,那就不要进行任何同步,把这个责任交给使用者,给他们优化的空间,但一定要在文档中说明;
- 如果
static
成员可以被某些方法修改,那一定要为它们加锁,因为这种情况下使用者无法保证线程安全性 - Item 68: Prefer executors and tasks to threads
- Executor Framework
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(runnable);
executor.shutdown();
-
Executors
提供了多个工厂方法,创建ExecutorService
,还可以直接使用ThreadPoolExecutor
,对线程池做更精细的控制 - 如果程序负载轻,可以使用
Executors.newCachedThreadPool
,任务提交时如果没有空闲线程,将创建新的线程;如果负载重,用Executors.newFixedThreadPool
更合适; - 不仅不应该自己实现任务队列,甚至都应该避免直接使用线程,而是使用Executor Framework;
- 任务和机制被分别抽象了,前者为
Runnable
和Callable
,后者则是executor service; -
java.util.Timer
也尽量不要用了,可以使用ScheduledThreadPoolExecutor
; - Item 69: Prefer concurrency utilities to wait and notify
- 正确使用
wait
和notify
有难度,而Java又提供了更高层的抽象,何乐而不用呢? -
java.util.concurrent
包主要包含三块: - Executor Framework
- concurrent collections
- synchronizers
- concurrent collections提供了标准容器的多线程高性能版本,它们内部进行了同步互斥操作,保证正确性;外部使用的时候,无需加锁,否则只会导致性能下降;
- concurrent collections中的每一种实现,可能都有性能优化的侧重点,可能有的是多读少写高效,例如
CopyOnWriteArrayList
,所以使用时需要了解清楚其试用场景; - 除非有明确的理由,否则,优先使用
ConcurrentHashMap
,而不是Collections.synchronizedMap
或者Hashtable
;也尽量避免在使用者那端进行同步操作; - 有的concurrent collections提供了block操作接口,例如
BlockingQueue
,从中取数据的时候,如果队列为空,线程将等待,新的数据加入后,将自动唤醒等待的线程;大部分的ExecutorService
都是采用这种方式实现的; - Synchronizers:
CountDownLatch
,Semaphore
,CyclicBarrier
,Exchanger
-
CountDownLatch
: 多个线程等待另外一个或多个线程完成某种工作 - 注意thread starvation deadlock问题
-
Thread.currentThread().interrupt()
idiom:异常可能从其他线程抛出?用此方法回到原来的线程? - 计时的话,用
System.nanoTime()
而不是System.currentTimeMillis()
,前者更准确,更明确 - 如果非要用
wait
和notify
,注意以下几点: - Always use the wait loop idiom to invoke the wait method; never invoke it outside of a loop.
- wait前的条件检查可以保证不会死锁,wait后的检查可以保证安全
- 通常情况下都应该使用
notifyAll
- Item 70: Document thread safety
- 一个方法的声明中加了
synchronized
并不能保证它是线程安全的,并且Javadoc也不会把这个关键字输出到文档中 - 线程安全也分好几个层次,文档中应该说明类/方法做到了何种程度上的线程安全
- 线程安全的分类
- immutable,对象创建后不可修改,无需进行外部的同步操作(互斥访问控制或许更恰当);例如:
String
,Long
,BigInteger
; - unconditionally thread-safe,对象可变,但是其内部进行了正确的同步操作,无需外部进行同步;例如:
ConcurrentHashMap
; - conditionally thread-safe,和绝对线程安全类似,但是有些方法需要进行外部的同步操作;例如:
Collections.synchronized
返回的容器,它们的iterator使用时需要进行同步; - not thread-safe,类自身没有任何同步操作,需要使用者自己保证线程安全;例如:
ArrayList
; - thread-hostile,由于类的实现原因,使用者无论如何也无法保证线程安全,例如未加同步的修改static成员;例如:
System.runFinalizersOnExit
; - jsr-305引入了几个注解:
Immutable
,ThreadSafe
,NotThreadSafe
,对应上述前四种情形,绝对线程安全与条件线程安全都属ThreadSafe
,对于条件线程安全还应在文档中说明何种情况下是需要外部进行同步的; - 如果一个类,将它用于
synchronized
的对象暴露出去了,那是很危险的,通常的做法是,内部创建一个Object
实例,将其用于synchronized
,但这种方式通常只适用于unconditionally thread-safe的实现。
```java
// Private lock object idiom - thwarts denial-of-service attack
private final Object lock = new Object();
public void foo() {
synchronized(lock) {
...
}
}
```
- Item 71: Use lazy initialization judiciously
- don’t do it unless you need to
- 如果使用lazy initialization,那这个成员的访问方法要用
synchronized
修饰 - 静态成员实现lazy initialization且希望高性能,使用lazy initialization holder class idiom,例如:
```java
// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() { return FieldHolder.field; }
```
- 实例成员要实现lazy initialization且希望高性能,使用double-check idiom,但是注意,double-check并非严格意义的线程安全,例如:
```java
// 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();
}
}
return result;
}
```
`result`这个局部变量的作用是,通常情况下,`field`已经初始化过了,这时将只会对其产生一次读操作,性能会有所提升
- double-check idiom还有两个变体,各有其使用场景:single-check idiom,racy single-check idiom;前者忍受多次赋值,后者忍受多次赋值且field的操作具有原子性(primitive类型且不是long和double);
- Item 72: Don’t depend on the thread scheduler
- 依赖线程调度器的正确性、性能的程序,很可能是无法移植的
- 好的多线程程序,同时运行的线程数不应该多于CPU内核数
- 线程无法进行有意义的工作时,就不应继续运行,忙等是不好的实现方式
- 另外一个线程(task)的工作也不能太少,否则线程切换的开销都会大于线程执行的时间,此时性能可想而知很低
-
Thread.yield
has no testable semantics. 所以不要用Thread.yield
,当程序的有些线程因为线程过多而无法获得CPU时间时,应该减少线程数。 - 线程优先级是Java平台中移植性最差的部分,所以也不要用
- Item 73: Avoid thread groups
- 如果设计的类需要处理一些逻辑上有关联的线程,应该考虑 thread pool executors
Serialization
- Item 74: Implement Serializable judiciously
- 实现
Serializable
接口之后,一旦类发布出去,就不能随意更改实现方式了,否则序列化-反序列化时可能失败,这降低了灵活性 - 序列化-反序列化的格式也是暴露的API之一,而默认的格式是和内部具体实现细节绑定的,所以默认格式把内部实现细节也暴露出去了
- 自定义序列化-反序列化格式(
ObjectOutputStream.putFields
,ObjectInputStream.readFields
),可以缓解上述问题,但是这又带来了新的实现复杂度 -
serialVersionUID
问题 - 会增加bug、安全漏洞的可能性,因为反序列化得到的对象,其状态是无法保证的
- 会增加发布新版时的测试工作
- 被设计于用来被继承的类,谨慎实现
Serializable
接口,同样,设计的接口也谨慎继承Serializable
接口 - 内部类不应该实现
Serializable
接口 - Item 75: Consider using a custom serialized form
- Do not accept the default serialized form without first considering whether it is appropriate.
- The default serialized form is likely to be appropriate if an object’s physical representation is identical to its logical content.
- Even if you decide that the default serialized form is appropriate, you often must provide a readObject method to ensure invariants and security.
- Regardless of what serialized form you choose, declare an explicit serial version UID in every serializable class you write.
- Item 76: Write
readObject
methods defensively -
readObject
方法的功效和public的构造函数一样 - 反序列化的时候,
readObject
如果不进行深拷贝、以及数据合法性验证,就会导致生成的对象数据非法,同时,也有可能获得反序列化后对象内部成员的引用(rogue object reference attacks) - 不要使用
writeUnshared
和readUnshared
方法,它们并不安全 - 前文应该提到过,非final类,构造函数以及
readObject
方法中,不能调用可重载的方法 - Item 77: For instance control, prefer enum types to readResolve
- if you depend on readResolve for instance control, all instance fields with object reference types must be declared transient. 否则可能会无法达到实例控制的目的。
- The accessibility of readResolve is significant.
- final类,应该置为private
- Item 78: Consider serialization proxies instead of serialized instances
- 为需要实现
Serializable
的类添加一个内部类,它的构造函数接收外部类的实例,并将其field拷贝到自身的field,并且实现readResolve
方法,创建外部类实例,创建方法可以是构造函数、static factory函数,在其中就可以进行实例控制了
```java
// Serialization proxy for Period class
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;
}
// readResolve method for Period.SerializationProxy
private Object readResolve() {
return new Period(start, end); // Uses public constructor
}
private static final long serialVersionUID = 234098243823485285L; // Any number will do (Item 75)
}
```
- 外部类实现一个
writeReplace
方法
```java
// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
return new SerializationProxy(this);
}
```
- 外部类实现
readObject
方法,并在其中抛出异常
```java
// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
```
摘录来源:https://notes.piasy.com/Android-Java/EffectiveJava.html