读第一版已经是好几年前的事儿了, 现在想起来也没什么印象, 也没什么收获, 估计那会儿刚接触java, 还是个菜鸟, 很多东东都不甚了解. 虽然一直在用java, 不过大部分都是用一些很常见的东西, 其实java还有很多细节和技巧值得我们去发掘和实践, 而这些内容却能真正体现一个人的Java水平. 看第二版收获非常多, 应该算是我看过的java书中最好的一本, 里面有不少使用Java的技巧, 并且可以在实际工作中大量采用, 应该作为桌面上常备的参考工具书.
这本书很少去讲java的一些用法, 因此针对的用户群不是java的初哥或菜鸟. 而更多的在讲为什么不能这样做, 为什么最好那样做, 解释原因, 提供参考和建议.
--------------我是读书笔记的分割线------------------
对于代码来说, 清晰和简洁是最重要的.
代码应该被重用, 而不应该被拷贝
模块之间的依赖应该尽可能的小.
错误应该尽早被检测出来, 最好是在编译时刻.
代码的清晰, 正确, 可用, 健壮, 灵活和可维护性比性能更重要.
编程的艺术应该是先学会基本规则, 然后才能知道在什么时候打破这些规则.
静态工厂方法惯用的名称:
valueOf
of
getInstance
newInstance
getType
newType
2:遇到多个构造器参数时, 要考虑采用构建器(Builder)
- public A
- private int a;
- private int b;
- private int c;
- private int d;
- public static class Builder{
- // 必须的
- private final int a;
- private final int b;
- private int c;
- private int d;
- public Builder(int a, int b){
- this.a=a;
- this.b=b;
- }
- public Builder setC(int c){
- this.c = c;
- return this;
- }
- public Builder setD(int d){
- this.d=d;
- return this;
- }
- public A build(){
- return new A(this);
- }
- private A(Builder builder){
- a = builder.a;
- b = builder.b;
- c = builder.c;
- d = builder.d;
- }
- }
- 使用:
- A a = new A.Builder(1, 2).setC(3).setD(4).build();
如果类的构造器或者静态工厂中具有多个参数, 设计这种类时, Builder模式就是一种不错的选择. 特别是大多数参数都是可选的时候. 与使用传统的重载构造器模式相比, 使用Builder模式的客户端将更易于阅读和编写.
3:使用私有构造器或者枚举类型强化Singleton属性.
- public class A{
- public static final A INSTANCE = new A();
- private A(){...}
- public void invoke(){...}
- }
另一种写法是采用枚举(因为枚举类型的构造函数天生是私有的, 而且外部也不能new一个枚举值):
public enum A{
INSTANCE;
public void invoke(){...}
}
4:通过私有构造器强化不可实例化的能力
有时候你可能需要编写只包含静态方法和静态域的类, 这种类就是典型的以面向对象之名行面向过程之实. 工具类除外, 最好给其提供一个私有构造器, 否则编译器会为其生成一个默认无参构造函数.
5:避免创建不必要的对象
对于同时提供了构造器和静态工厂方法, 通常使用静态工厂方法而不是构造器, 以避免创建不必要的对象,
比如Boolean.valueOf(String)比new Boolean(String)要好, 构造器每次被调用时要求创建一个新的对象,
而静态工厂方法则没有这种要求, 也不会这样做.
创建对象的代价非常昂贵, 应该尽可能不这样做, 不过小对象的构造器只做少量的工作, 所以小对象的创建和回收非常高效.
6:消除过期对象
清空引用对象应该是一种例外, 而不是一种规范行为
只要是类自己管理内存, 程序员就应该警惕内存泄露的问题.
对于缓存来说, 只要缓存之外存在对某个项的的键的引用, 该项就有意义, 那么就可以用WeakHashMap代表缓存; 当缓存中的项过期之后, 它们会自动删除.
8:覆盖equals时请遵守通用约定
当使用equals来比较对象, 是希望他们在逻辑上是否相等, 而不是指向同一对象, 或者用来作为Map的key以及集合Set中的元素时, 就必须复写equals方法.
对于枚举类型来说, 逻辑相等与对象相等是同一回事, 因此不需要覆盖equals方法.
类和结构
13:使类和成员的可访问性最小化
一个模块对于外部其他模块而言, 是否隐藏其内部数据和实现细节, 设计良好的模块很好的将对外API与内部实现很好的隔离开来. 模块之间只通过API进行通信, 而不需要知道内部工作情况, 即信息隐藏或封装, 也是软件设计的基本原则之一.
封装之所以非常重要, 源于这样一个事实: 它可以有效的解除组成系统的各个模块之间的耦合关系.
对于顶层类和接口, 只有两种可访问级别: 包级私有和公有.
受保护类成员应该是导出API的一部分, 必须永远得到支持, 同时它也代表了该类对于某个实现细节的公开承诺, 受保护的成员应该尽可能少用.
对于final域来说, 要么指向基本类型, 要么指向不可变类, 如果指向可变对象的引用, 它便具有了非final域的所有缺点, 它只保证引用本身不可修改, 而不能保证引用对象不被修改.
对于公有数组来说, 可以将其变为私有, 同时增加一个公有不可变的列表:
- private final static Foo[] FOOS = ...
- public final static List<FOO> MY_FOOS = Collections.unmodifiableList(Arrays.asList(FOOS));
15:使可变性最小化
不可变的类比可变类更加易于设计, 实现和使用, 它们不容易出错, 且更加安全.
不可变类本质上是线程安全的, 它们不需要要求同步.
如果类不能设计成不可变的, 那么应该尽可能的限制它的可变性.
除非有令人信服的理由需要将域变成非final的, 否则使每一个域都是final的.
16:复合优于继承
对普通的具体类, 进行跨越包间的继承是非常危险的
与方法调用不同的是, 继承打破了封装性. 即子类依赖于其超类中特定功能的实现细节, 而超类可能随着版本的发布而发生变化, 如果真的发生了变化, 子类可能遭到破坏, 即使代码完全没有改变. 子类必须跟随者超类的更新而演变, 除非超类是专门为了扩展而设计的.
只有子类真正是超类的子类型时, 才适合用继承, 如果要让类B扩展类A, 就应该问问自己, 每个B确实也是A吗? 如果你不能确定这个问题的答案是肯定的, 那么B就不应该扩展A.
如果子类和超类处在不同的包中, 并且超类并不是为了继承而设计的, 那么继承将会导致脆弱性. 继承机制会将超类API中的所有缺陷都传播到子类中, 而复合则允许设计新的API来隐藏这些缺陷.
17:要么为继承而设计, 并提供文档说明, 要么禁止继承
关于程序文档有句格言: API文档应该描述一个给定的方法做了什么工作, 而不是描述它是如何做到的.
为了允许继承, 类还必须遵守其他一些约束, 构造器决不能调用可被覆盖的方法, 因为超类的构造器在子类的构造器之前运行,
所以子类中覆盖的方法将会在子类的构造器运行之前先被调用, 如果该复写版本的方法依赖于子类构造器所执行的任何初始化工作,
该方法将不会如预期般地执行.
18:接口优于抽象
骨架实现被称之为AbstractInterface, 例如Collection Framework中的AbstractSet, AbstractList和AbstractMap. 他们是为了继承而设计的.
抽象类的演变比接口的演变更容易.
接口一旦公开, 并且被广泛实现, 再想改变这个接口几乎是不可能的.
19:接口只用于定义类型
21:用函数对象表示策略
当一个具体策略只使用一次时, 通常使用匿名类和实例化这个具体策略类, 当一个具体策略是设计用来重复使用的时候, 它的类通常就要被实现为私有的静态成员类, 并通过公有的静态final域被导出, 其类型为策略接口.
- class Host{
- private static class MyComparator implements Comparator<String>, Serializable{
- public int compare(String s1, String s2){
- return s1.length() - s2.length();
- }
- }
- public static final Comparator<String> STRING_LENGTH_COMPARATOR = new MyComparator();
- }
22: 优先考虑静态成员类
当非静态成员类实例被创建的时候, 它和外围实例之间的关联就随之建立起来了, 而且这种关联关系以后不能随之改变.
非静态成员类的一种常见的用法是定义一个Adapter, 它被允许外部类的实例被看做另一个不相关类的实例(这句没明白). 比如List, Set实现类中的Iterator实现
如果声明成员类不要求访问外围实例, 就要始终把static修饰符放在它的声明中.
静态成员类的一种常见用法是用来代表外围类所代表对象的组件, 比如Map类中的Entry类, 虽然每一个entry跟map实例关联, 但是getKey, getValue方法跟map无关.
匿名类应该尽可能简短, 比如10行以内.
匿名类的一种常见用法就是动态创建函数对象.
泛型
23:请不要在新代码中使用原生态类型
使用原生态类型, 会失去泛型在安全性和类型方面的所有优势. 而它的存在主要是为了向后兼容.
List和List<Object>之间的区别: 前者是为了逃避泛型检查, 后者明确的告诉编译器, 它能够支持任意类型的对象.
可以将String传给List, 但是不能传递给List<Object>,
因为List<String>是List的一个子类型, 但不是List<Object>的子类型.
如果要使用泛型, 但是不知道或者不关心实际的类型参数, 可以使用问号(无限制通配符)代替
在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的.
- if (o instanceof Set){
- Set<?> m = (Set<?>)o;
- ...
- }
一旦确定这个o是个Set就应该把它转换成通配符类型Set<?>, 而不是转换成原生态类型Set, 这个是checked的转换, 因此不会导致编译警告.
Set<Object>是个参数化类型, 表示可以包含任何对象类型的一个集合; Set<?>则是一个通配符类型,
表示只能包含某种未知类型对象类型的一个集合, Set是个原生态类型, 它脱离了泛型系统, 前两者是安全的, 后者是一种不安全的.
24:消除非受检警告
如果无法消除警告, 同时可以证明引起警告的代码是类型安全的, 可以使用@SuppressWarnings("uncheked")注解来消除警告.
25:列表优先于数组
数组是协变的, 即如果Sub是Super的子类型, 那么Sub[]是Super[]的子类型. 而泛型是非协变的,
即任意两个不同类型Type1, Type2, List<Type1> 既不是List<Type2>的子类型,
也不是List<Type2>的超类型.
对于数组:
- Object[] array = new Long[1];
- array[0] = "foo"; // 运行时报错
对于列表:
- List<Object> list = new ArrayList<Long>(); //编译时报错
- list.add("foo");
数组是具体化的, 因此数组会在运行时才知道并检查它们元素的类型约束. 泛型则是通过擦除来实现的, 因此泛型只在编译时强化他们的类型信息, 在运行时丢弃它们的元素类型信息.
数组和泛型无法很好的混合使用, 比如创建泛型数组是非法的.
从技术的角度来说, 像E, List<String>这样的类型应该成为不可具体化的类型.即在运行时表示法包含的信息比它编译时包含的信息更少. 唯一可以具体化的参数化类型是无限制通配符类型, 这种类型的数组是合法的.
当你无法创建泛型数组时, 最好的解决方法通常是优先使用结合类型List<E>而不是数组类型E[]
数组提供了运行时的类型安全, 但是没有提供编译时类型安全, 而泛型正好相反.
28:利用有限制通配符来提升API灵活性
为了获得最大的灵活性, 要在表示生产者和消费者的输入参数上使用通配符类型, 如果既是生产者又是消费者, 可以不使用任何通配符.
如果参数化类型表示一个T生产者, 就是用<? extends T>, 如果它表示一个消费者, 就是用<? super T>.
- public void popAll(Collection<? super E> dst){
- while(!isEmpty()){
- dst.add(pop());
- }
- }
不要用通配符类型作为返回类型, 除了为用户提供不必要的灵活性之外, 它还会强制用户在客户端代码中使用通配符类型.
如果编译器不能推断出你希望它拥有的类型, 可以通过一个限时的类型参数来告诉它要使用哪种类型.
- Set<Number> numbers = Union.<Number>union(integers, doubles)
另一种比较复杂的泛型使用场景:
- public static <T extends Comparable<? super T> T max(List <? extends T> list);
两种写法:
- public static <E> void swap(List<E> list, int i, int j)
- public static void swap<List<?> list, int i, int j)
第二种方法更简单, 公用API常用. 如果类型参数在方法声明中出现一次, 可以使用通配符取代. 如果是无限制的类型参数, 就可以用无限制的通配符取代之, 如果是有限制的类型参数, 那么就可以使用有限制的通配符取代之.
29:优先考虑类型安全的异构容器
- public class Favorites{
- private Map<Class<?>, Object> map = new HashMap<Class<?>, Object>();
- public <T> put(Class<T> type, T instance){
- map.put(type, instance);
- }
- public <T> t get(Class<T> type){
- return type.cast(map.get(type))
- }
- }
枚举和注解
30:使用enum代替int常量
java的枚举本质上是int值
因为没有可以访问的构造函数, 枚举类型是真正的final, 因为客户端不能创建枚举类型的实例, 也不能对它进行扩展.
枚举类型天生就是不可变的, 因此所有的域都应该是final的, 它们可以是公共的, 但是最好将他们做成私有的, 并提供私有的方法访问.
枚举类型有一个静态的values方法, 按照声明顺序返回它的值的数组.
一种不好的枚举实现:
- public enum Operation{
- PLUS, MINUS, TIMES, DIVIDE;
- double apply(double x, double y){
- switch(this){
- case PLUS: return x + y;
- case MINUS:return x - y;
- case TIMES:return x * y;
- case DIVIDE:return x / y;
- }
- }
- }
更好的一种枚举实现:
- 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);
- }
枚举类型有一个自动产生的valueOf(String)方法, 它将常量的名字转换为常量本身. 如果在枚举中重载了toString方法, 可以考虑编写一个fromString方法.
- public static final Map<String, Operation> map = new HashMap<String, Operation>();
- static {
- for(Operation op:values()){
- map.put(op.toString(), op);
- }
- }
- public static Operation fromString(String symbol){ return map.get(symbol);}
策略枚举的使用
- enum PayrollDay{
- Mon(PayType.WeekDay), TUR(PayType.WeekDay), WEN(PayType.WeekDay), THU(PayType.WeekDay), FIR(PayType.WeekDay), SAT(PayType.WeekEnd), SUN(PayType.WeekEnd);
- private PayType payType;
- PayrollDay(PayType payType){this.payType = payType;}
- double pay(double hoursWorkded, double payRate){return payType.pay(hoursWorked, payRate);}
- private enum PayType{
- WEEKDAY{
- double overtimePay(double hours, double payRate){
- return hours < HOURS_PRE_SHIFT ? 0 : (hours -HOURS_PRE_SHIFT)*payRate / 2;
- }
- },
- WEEKEND{
- double overtimePay(double hours, double payRate){
- return hours * payRate / 2;
- }
- };
- private static int HOURS_PRE_SHIFT = 8;
- abstract double overtimePay(double hours, double payRate);
- double pay(double hoursWorked, double, double payRate){
- double basePay = hoursWorkded * payRate;
- return basePay + overtimePay(hoursWorked, payRate);
- }
- }
- }
什么时候使用枚举? 每当需要一组固定常量的时候.
如果多个常量共享相同的行为, 可以考虑策略枚举.
31:用实例域代替序数
永远不要根据枚举的序数导出与它关联的值, 而是将它保留在一个实例域中.
在枚举中避免使用ordinal方法.
32:用EnumSet代替位域
- public class Text{
- public enum Style{ BOLD, ITALIC, UNDERLINE}
- public void applyStyle(Set<Style> styles){...}
- }
- 调用:
- txt.applyStyle(EnumSet.of(Style.BOLD, Style.ITALIC));
33:用EnumMap代替序数索引
- Map<Herb.Type, Set<Herb> herbsByType = new EnumMap(Herb.Type.class);
- for(Herb.Type t: Herb.Type.values()){
- herbByType.put(t, new HashSet<Herb>());
- }
34:用接口模拟可伸缩的枚举
- public interface Operation{double apply(double x , double y);}
- public enum BasicOperation implements Operation{
- PLUS("+"){
- public double apply(double x, double y){return x + y;}
- },
- ...
- private final String symbol;
- BasicOperation(String symbol){this.symbol = symbol;}
- }
- public enum ExtendedOperation implements Operation{
- EXP("^"){
- public double apply(double x, double y){return Math.pow(x, y)}
- },
- }
- 使用:
- test(ExtendedOperation.class, x, y);
- public <T extends Enum<T> & Operation> void test(Class<T> t, double x, double y){
- for(Operation o : t.getEnumConstants()){
- System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y))
- }
- }
- 另一种用法:
- test(Arrays.asList(ExtendedOperation.values()), x, y);
- public void test(Collection<? extends Operation> opSet, double x, double y){
- for(Operation op: opSet){
- ...
- }
- }
枚举无法从一种枚举继承到另一种枚举
35:注解优于命名模式
注解对类的语义没有直接的影响, 它们只负责提供信息供相关的程序使用, 更一般的讲, 注解永远不会改变被注解代码的语义, 但他们可以通过工具进行特殊的处理.
42:慎用可变参数
可变参数通过先创建一个数组, 数组大小为在调用位置所传递的参数数量, 然后将参数值传到数组中, 最后将数组传递给方法.
有时候, 有必要编写需要1个或多个某种类型参数的方法, 而不是需要0个或者多个. 针对这种情况, 可以声明该方法有两个参数, 一个是指定类型的正常参数, 一个是这种类型的varargs参数.
可变参数是为printf设计的.
在重视性能的情况, 使用可变参数要特别小心, 因为可变参数方法每一次调用都会进行数组分配和初始化.
如果对某个方法95%的调用会有3个或者更少的参数, 就声明该方法的5个重载, 每个重载方法带有0至3个普通参数, 当参数的数目超过3个时, 就是用一个可变的参数
- public void foo(){...}
- public void foo(int a1){...}
- public void foo(int a1, int a2){...}
- public void foo(int a1, int a2, int a3){...}
- public void foo(int a1, int a2, int a3, int... rest){...}
43:返回0长度的集合或数组, 而不是null
将一些元素中一个集合转到另一种类型数组时, 可以这样做:
- private List<Integer> list = ...
- rivate final Integer[] EMPTY_ARRAY = new Integer[0];
- public Integer getArray(){
- return list.toArray(EMPTY_ARRAY);
- }
Collection.toArray(T[])规范保证: 如果输入数组大到足够容纳这个集合, 他就返回这个输入数组, 因此这种做法永远不会分配零长度的数组. 正常情况下, toArray方法分配了返回的数组, 如果数组是空的, 那么将直接返回零长度的数组.
44:为所有导出的API元素编写文档注释
方法的文档注释, 应该简洁地描述出它和客户之间的约定, 这个约定说明这个方法做了什么, 而不是说明他是如何完成这项工作的.
方法除了说明前置条件和后置条件之外, 还需要描述它们的副作用, 如果有的话.
跟在@throws之后的文字应该包含单词"if" 紧接着是一个名词短语, 描述了这个异常将在什么情况下抛出
使用javadoc中的{@code}标签来代替html中的<code>标签
对于方法和构造器而言, 概要描述应该是一个完整的动词短语, 描述了该方法所执行的动作
例如:ArrayList(int intialCapacity) 用指定的初始容量构造一个空的列表.
对于类, 结构和域, 概要描述应该是一个名词短语, 它描述了该类或者借口的实例, 或者域本身所代表的事物
例如:TimeTask 可以调用一次的任务, 或者被Timer重复执行的任务.
45:将局部变量的作用域最小化
几乎没有局部变量的声明都应该包含一个初始化的表达式, 如果你没有足够的信息来嘴一个变量进行有意义的初始化, 就应该推迟这个声明, 知道可以初始化为止.
如果在循环终止之后, 不再需要循环变量的内容, for循环好于while循环.
- for(int i, n = expensiveComputation(); i < n; i++){
- foo(i);
- }
这种写法可以避免执行冗余的计算
使用for循环与使用while循环相比还有一个优势, 更简短, 从而增强了可读性.
46:for-each循环优于传统的for循环
如果你在编写的类型表示的是一组元素, 即使你选择不让他实现Collection, 也要让他实现Iterable, 这样可以允许用户使用for-each循环遍历你的类型.
47:了解和使用类库
如果你要做的事情非常常见, 有可能类库中已经有了类完整了这个工作.
48:如果需要精确的答案, 请避免使用float和double
对于涉及到金融计算, 最明显的做法就是用分为单位, 而不是用元.
对于任何需要精确答案的计算任务, 请不要使用float和double, 如果你想系统的记录十进制小数点, 并不介意因为不是用基本类型带来的不便, 请使用BigDecimal, 使用BigDecimal还有另外一个好处, 就是允许你完全控制舍入.
49:基本类型由于装箱基本类型
对装箱基本类型运用==操作符几乎总是错误的.
自动装箱减少了使用装箱基本类型的繁琐性, 但是没有减少使用它的风险.
55:谨慎的使用优化
在优化方面我们应该遵循两条原则:
1.不要进行优化
2.还是不要优化(针对专家), 在没有绝对清晰的优化方案之前, 请不要进行优化.
不要因为性能而牺牲合理的结构, 要努力编写好的程序, 而不是快的程序. 如果好的程序不够快, 它的结构可以使它得到优化
好的程序体现了信息隐藏的原则: 只要有可能, 它们就会把设计决策集中在单个模块中, 因此可以改变单个决策, 而不会影响到系统的其他部分.
不要费力的去编写快的程序, 而应该努力编写好的程序, 速度自然会随之而来.
56:遵守普通的命名惯例
包名称应该包含一个或多个描述该包的组成部分. 这些组成部分应该比较简短, 最好不要超过8个字, 鼓励使用有意义的缩写. 比如使用util而不是utilities, 首字母的缩写形式也是可以的, 比如awt.
某个动作的方法, 通常用动词或者动词短语来命名, 对于返回boolean值, 通常用is开头, 以及用has开头.
如果方法返回被调用对象的一个非boolean的函数或者属性, 它通常用名词, 名词短语, 或者以动词get为前缀的动词短语来命名. 例如size, hashCode,或者getTime
有些方法的名称值得专门提及, 转换对象类型的方法, 返回不同类型的独立对象方法, 通常被称为toType, 例如toArray,
toString等. 返回视图的方法, 通常被称为asType, 比如asList, 静态工厂常用的名称: valueOf, of,
getInstance, newInstance, getType, newType
57: 只针对异常的情况使用异常
异常应该只被用于异常的情况下, 它们永远不应该用到正常的控制流.
58:对可恢复的情况使用受检查异常, 对编程错误使用运行时异常
如果调用者能适当的做恢复, 对这种情况应该使用受检异常. 对于受检异常, 强迫使用者在一个catch中处理该异常, 或者将它传播出去.
方法中抛出的某种异常, 应该是对API使用者的一种潜在的指示: 异常是该方法的某种可能的结果
如果程序中抛出未受检异常或者错误, 往往就属于不可恢复的情形, 继续执行下去有害无益.
用运行时异常来表明编程错误, 或者提前违例, 即调用方没有遵守API规范建立的约定, 例如ArrayIndexOutOfBoundsException表明违反了数组下标必须位于0和数组长度减一之间的约定.
对于受检异常, 最好能提供可恢复的具体信息, 比如用户在余额不足的收费电话上打电话失败, 抛出受检异常, 该异常应该允许调用方获取当前缺少金额, 从而进行相应的处理.
59:避免不必要的使用受检的异常
API的制定者在声明一个方法签名时, 必须问问自己, 程序员该如何处理该异常?
60:优先使用标准异常
专家级程序员与缺乏经验的程序员最重要的一个区别在于, 专家追求并且通常也能够实现高度的代码重用.
IllegalStateException是一个常被重用的异常, 表明因为接收对象的状态而使调用非法, 通常会抛出这个异常.
所有错误的方法调用都可以被归结为非法参数或者或者非法状态.
如果参数中传了null值, 而方法又不允许null值时, 应该抛出NullPointerException,
而不是IllegalArgumentException,
同样数组越界不是抛IllegalArgumentException而是IndexOutOfBoundsException.
说明具体的异常要优于通用的异常.
61:抛出与抽象相对应的异常
如果方法抛出的异常与它所执行的任务没有明显的联系, 这种情形会使人不知所措, 当方法传递由底层抽象抛出的异常时, 除了让人困惑外, 还会让实现细节污染了更高层的API.
更高层的实现应该捕获底层的异常, 同时抛出可以按照高层抽象进行解释的异常. 这种做法称为异常转译:
- try{
- ...
- }catch(LowLevelException e){
- throw new HighLevelException(...);
- }
尽管异常转译与不加选择的从底层传递异常的做法相比有所改进, 但是它也不能被滥用, 如果有可能, 处理来自底层的异常的最好办法是, 在调用底层方法之前确保它会成功执行, 从而避免抛出异常.
如果无法避免底层异常, 次选方案, 让高层来悄悄绕开这些异常, 从而将高层方法调用者与底层的问题隔离开来, 并将异常信息通过日志的形式记录下来, 以方便问题查找.
63:在细节消息中包含能捕获失败的信息
不要在异常中包含大量的描述信息, 因为异常堆栈能很好的反映出现异常的轨迹和具体位置, 通过阅读源码就可以获得.
64:努力使失败保持原子性
失败的调用应该使对象保持调用之前的状态, 即失败原子性
对于在可变对象上执行的操作方法, 要获得失败原子性, 最常见的做法就是操作之前检查参数的有效性.
调整方法执行的顺序, 将任何可能导致导致失败的部分都在对象状态被修改之前发生.
66:同步访问共享的可变数据
许多程序员把同步的概念仅仅理解为一种互斥的方式, 即当一个对象被一个线程修改的时候, 可以阻止另一个线程观察到对象内部不一致的状态. 除此之外, 它还能保证进入同步块的每个线程都能看到同一个锁保护的之前所有的修改效果.
- private static boolean stop
- public static main(){
- Thread thread = new Thread(){
- public void run(){
- int i = 0;
- while(!stop){
- i++;
- }
- }
- }
- thread.start();
- TimeUnit.SECONDS.sleep(1);
- stop = true;
- }
这个在某种场景下永远不会停止
因为while(!stop){...}
被翻译成了if (!stop){while(true){...}}
这是jvm的一种优化, 称之为hoisting, 是HopSpot Server VM的工作, 结果造成活性失败.
解决方法是提供stop的带synchronized关键字的get和get方法即可
另一种解决方案是使用volatile修饰符, 虽然它不执行互斥访问, 但是它可以保证任何一个线程在读取该域的时候都能看到最近刚刚被写入的值
当多个线程共享可变数据的时候, 每个读取或者写入数据的线程都必须执行同步
67:避免过度同步
通常应该在同步区域内做尽可能少的工作. 获得锁, 检查共享数据, 执行操作, 放掉锁. 如果执行某个很耗时的动作, 应该设法将这个动作移动到同步区域的外面.
为了避免死锁和数据破坏, 千万不要从同步区域内部调用外来的方法.
68:executor和task优先于线程
你尽量不要编写自己的工作队列, 而且还应该尽量不要直接使用线程, 现在关键的抽象不再是Thread了, 它以前既充当工作单元,
又是执行机制. 现在的工作单元和执行机制是分离的. 现在关键的抽象是工作单元, 称为任务, 任务有两种Runnable和Callable,
执行任务的通用机制是executor service.
69:并发工具优于wait和notify
除非不得已, 优先考虑ConcurrentHashMap, 而不是使用Collections.synchronizedMap或者Hashtable.
同步器(Synchronizer)是使一些线程能等待另一个线程的对象, 允许他们协调动作, 最常用的同步器是CountDownLatch和Semaphore, 不常用的是CyclicBarrier和Exchanger.
一个使用CountDownLatch的例子
- public static long time(Executor executor, int concurrency, final Runnable action) throws Exception {
- final CountDownLatch ready = new CountDownLatch(concurrency);
- final CountDownLatch start = new CountDownLatch(1);
- final CountDownLatch done = new CountDownLatch(concurrency);
- for (int i = 0; i < concurrency; i++) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- ready.countDown(); // 打开第一道闸门进入准备状态
- try {
- start.await(); // 关闭第二道闸门等待各就各位
- action.run();
- } catch (Exception e) {
- } finally {
- done.countDown(); // 到达终点
- }
- }
- });
- }
- ready.await(); // 等待直到都进入第一道闸门进入准备阶段
- long startNanos = System.nanoTime();
- start.countDown(); // 打开闸门, 都开跑
- done.await(); // 等待直到都到达终点
- return System.nanoTime() - startNanos;
- }
对于间歇式的定时, 始终应该优先使用System.nanoTime(), 而不是System.currentTimeMills(), 前者更精确, 不受系统的实时时钟的调整所影响.
直接使用wait和notify就好像用汇编语言进行编程一样, 而直接用并发包则提供了更高级的语言.
71:慎用延迟初始化
当有多个线程时, 延迟初始化是需要技巧的. 如果两个或者多个线程共享一个延迟初始化的域, 采用某种形式的同步是很重要的, 否则可能造成严重的bug
如果出于性能考虑, 需要对静态域进行初始化可以这样做:
- private static class FieldHolder{
- static final FieldType field= computeFieldValue();
- }
- static FieldType getField(){FieldHolder.field;}
如果出于性能考虑, 需要对实体域进行初始化, 可以使用双重检查模式:
- private volatile FieldType field;
- FieldType getField(){
- FieldType result = field;
- if (result == null){
- synchronized(this){
- result = field;
- if (field == null){
- field = result = computeFieldValue();
- }
- }
- }
- }
72:不要依赖线程调度器
适当的规定线程池大小, 让任务保持适当的大小, 彼此独立, 但是任务不能太小, 否则分配的开销也会影响性能.
线程不应该一直处于忙等状态, 即反复地检查一个共享对象, 以等待某些事情的发生. 这种做法除了使程序易受线程调度器的变化影响之外, 同时也极大的增加了处理器的负担, 降低了同一台机器上其他进程可以完成的工作量.