Effective Java 2th Part III

通用程序设计

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

  • 第一次使用的地方声明
  • 每个局部变量的声明都应该有一个初始化表达式
  • for(int i=0,j=getMax();i>j;i++)使用这种方式避免每次迭代的冗余计算
  • 方法小而集中

46、for-each优先

  • 没有性能损失(集合数只取了一次,见45条)
  • 删除元素无法使用
  • 转换元素无法使用
  • 多个集合平行迭代无法使用

    47、了解和使用类库

    • Random.nextInt

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

  • BigDecimal,int(9位数字)或long(18位数字)
  • int或long需要手动控制小数位数
  • BigDecimal功能齐全但用法不方便,速度慢

49、基本类型优先于装箱基本类型

  • ==操作符执行同一性比较
  • 操作中混合装箱与基本类型,都会进行拆箱比较
  • 循环中注意装箱类型反复计算导致的大量装箱拆箱操作
    1
    2
    3
    4
    Long sum=0L;
    for(long i=0L;i<1000000;i++){
    sum+=i;//装箱拆箱
    }

50、如果其他类型更适合,尽量避免使用字符串

  • 不要代替其他类型,如int等数字形式,应当做类型转换
  • 不适合代替枚举
  • 不适合代替聚集类型(以分隔符表示一串有含义的信息”Name-kkk#Age-12”)
  • 不适合代替能力表(用于授权的某个唯一变量标识)

51、当心字符串拼接性能

  • 拼接字符串时间复杂度n平方
  • 使用StringBuilder

52、通过接口引用对象

  • 如果没有合适的接口存在,使用类引用

53、接口优先于反射机制

  • 编译检查失效
  • 代码冗长难懂
  • 性能损失

54、谨慎的使用本地方法

  • 性能逐渐不是问题
  • 本地语言不是安全的
  • 不可再自由移植

55、谨慎优化

  • 不要进行优化
  • 还是不要进行优化-在未找到清晰的优化方案之前
  • 设计时要考虑性能(算法,API,数据格式,线路层)
  • 避免限制性能的设计决策
  • 优化之前与之后,测量性能

56、遵守普遍接受的命名惯例

  • 详细内容见alibaba Java开发手册

异常

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

  • 不要使用异常用于正常的控制流

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

  • 不要使用error
  • 未受检的抛出结构使用RuntimeException的子类

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

60、优先使用标准异常

61、抛出与抽象相对应的异常

  • 高层的实现捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常
  • 尽量处理或阻止来自底层的异常

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

  • 记录异常与条件
  • 只记录未受检异常

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

  • 异常信息应该包含所有对该异常有贡献的参数和值
  • 可以通过构造特定的异常类构造函数去规范的产生高质量的帮助信息

64、努力使失败保持原子性

  • 异常出现后,尽量使得处理的对象保持之前的状态而不要修改属性等信息
  • 所有可能的检查在修改对象之前进行

65、不要忽略异常

  • catch语句不要空置

并发

66、同步访问共享的可变数据

  • 注意”活性失败”, JVM优化可能将局部变量修改读取方式导致无法看到关键的开关变量
  • 读写都需要同步
  • “安全性失败”,常规的并发问题,如i++

67、避免过度同步

  • 同步区域尽可能小
  • 同步区域不可调用外来方法(接口,可覆盖的方法等),会导致死锁或异常

68、Executor和task优于线程

  • 使用线程池而不是简单的new Thread(){}.start();

69、并发工具优先于wait和notify

  • 永远不要在循环之外调用o.wait()
  • 新代码中不要使用wait和notify,而是用并发工具替代(Executor,并发集合,同步器)
  • 优先使用notifyAll();

70、线程安全文档化

  • 不同级别的线程安全要有文档说明
  • 锁封装在同步的对象中

71、慎用延迟初始化

  • 静态域使用内部类实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class A{
    private static class B{
    static final Object field=getField();
    }
    private A(){};
    public Object getField(){
    return B.field;
    }
    }
  • 实例域使用双重检查

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Object getField(){
    Object res=field;
    if(null==field){
    synchronized(this){
    res=field;
    if(null==res){
    field=res=getField();
    }
    }
    }
    return res;
    }

72、不要依赖于线程调度器

  • Thead.yield 不靠谱,只用于手动的增加并发用以复现测试bug
  • 线程优先级不靠谱

73、避免使用线程组

  • 禁用

序列化

74、谨慎地实现Serializable接口

  • 大大降低修改灵活性
  • serialVersionUID如果不提供,系统会调用复杂运算过程生成
  • 序列化构造器覆盖了默认的构造器行为
  • 新版本发布,测试负担加重
  • 为继承而设计的类尽量少的实现序列化接口
  • 使用AtomicReference实现线程安全状态机表示类已经初始化等信息
  • 内部类不要实现(内部类有另外的合成域用于指向外部类成员,序列化形式不清楚)
  • 静态成员类可以实现

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

  • 如果对象的物理表示法和逻辑数据内容有实质性的区别,不建议使用默认序列化形式
  • 加序列化UID(性能提升一些)

76、保护性的编写readobject方法

  • readObject方法中尽量对字段进行保护性拷贝后赋值,序列化字节流可能存在对对象内部字段的引用,进而再反序列化之后调用readObject之后对内部字段进行修改

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

  • 对于单例模式实例化,使用枚举类型作为实例控制

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

Effective Java 2th Part II

泛型

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

  • 尽量使用泛型

24、消除非受检警告

25、列表优先于数组

  • 不要混用数据与列表
  • 数据是具体化的,运行时才知道类型,泛型是擦除式的,运行时会丢弃类型,在编译时强化类型

26、优先考虑泛型

27、优先考虑泛型方法

  • 静态工具尤其适合泛型化

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

  • PECS, 用于生产者使用<? extends T>, 用于消费者使用<? super T>
  • 所有的comparable和comparator都是消费者

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

  • Map中存储不同类型的对象可以用类型对象Class作为Key,get时候使用Class.cast进行转换

枚举和注解

30、用enum代替常量int

  • 枚举是不可变的,每个枚举都有自己的命名空间,不担心重名问题
  • 如果每个枚举对应不同的行为,使用特定于常量的方法实现

    1
    2
    3
    4
    5
    6
    7
    public enum Oper{
    PLUS{double apply(double x,double y){return x+y;}},
    MINUS{double apply(double x,double y){return x-y;}};
    abstract double apply(double x,double y);
    }
  • 多个枚举常量同时共享相同的行为,则考虑策略枚举(枚举套枚举,里面的枚举提供方法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //伪代码
    public enum A{
    X1(B.Y1),
    X2(B.Y1),
    X3(B.Y2);
    enum B{
    Y1(void do(){}),
    Y2(void do(){});
    abstract void do();
    }
    }

31、用实例域代替序数

  • 尽量不要使用ordinal方法获取序号
  • 显示指定一个字段表示值

32、EnumSet代替位域

  • EnumSet.of(X1,X2),表示某几个枚举值的Set

33、用EnumMap代替序数索引

  • 当枚举作为Key时,使用EnumMap作为其集合载体,替代枚举数组

34、用接口模拟可伸缩的枚举

  • 利用枚举实现某接口可以动态的扩展枚举,调用使用接口调用即可
  • 调用端的参数两种声明方式<T extends Enum<T> & AInterface>Collection<? extends AInterface>

35、注解优先于命名模式

  • 使用注解而不是使用具体名称去判断执行逻辑

36、坚持使用Override注解

  • 使得编译器提前得到错误

37、用标记接口定义类型

  • 定义某一个类型使用接口

方法

38、检查参数的有效性

  • 断言或条件判断
  • 对于所有参数如果可以合理完成逻辑,限制越少越好

39、必要时进行保护性拷贝

  • 对外提供的服务,要做保护性的设计
  • 保护性拷贝在检查有效性之前进行
  • 参数可以被不信任的子类化,勿使用clone拷贝
  • 自身组件返回考虑做保护性拷贝

40、谨慎设计方法签名

  • 名称谨慎
  • 不过于追求便利方法
  • 参数列表<=4,过多则拆分方法或创建辅助类作为参数,或者采用builder模式
  • 参数优先使用接口
  • boolean优先使用二值枚举setType(Person.MALE)

41、慎用重载

  • 调用重载方法在编译时决定,而覆盖方法在运行时决定
  • 不要导出两个具有相同参数数目的重载方法
  • 构造器重载可考虑导出静态工厂
  • 多个重载方法,一般将具体化的重载方法转发给通用的重载方法以保证结果的一致性
  • 传递同样的参数时,所有的重载方法必须一致

42、慎用可变参数

  • 每次调用均会导致一次数据分配和初始化
  • 可以创建一系列接收参数数量的方法重载组,最后一个再使用可变参数
    1
    2
    3
    4
    void foo()
    void foo(int a1)
    void foo(int a1,int a2)
    void foo(int a1,int a2,int ...rest)

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

  • 返回类型是数组或者集合的方法返回零长度对象

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

  • <pre> {@code int i=1;}<pre/> 用于导出代码
  • {@literal Map<String,String>} 用于原样输出内容并且转移html字符

Effective Java 2th Part I

创建与销毁对象

1、静态工厂代替构造器
优点:

  • 命名:方法名称表示对象含义
  • 复用:不需要每次都创建新对象
  • 返回子类对象:可以返回该类的子类对象
  • 创建参数化类型对象简洁:new HashMap>().
    缺点:
  • 没有默认或保护的构造方法无法子类化
  • 只是一个静态方法而不像构造函数有明确的语法含义

2、多个构造参数考虑使用构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class App{
private int p1;
private int p2;
private int p3;
private int p4;
private int p5;
public static class Builder{
private int p1=0;
private int p2=0;
private int p3=0;
private int p4=0;
private int p5=0;
public Builder(int p1,int p2){
this.p1=p1;
this.p2=p2;
}
public Builder setP3(int p3){
this.p3=p3;
return this;
}
public Builder setP4(int p4){
this.p4=p4;
return this;
}
public Builder setP5(int p5){
this.p5=p5;
return this;
}
public App build(){
return new App(this);
}
}
public App(Builder builder){
p1=builder.p1;
p2=builder.p2;
p3=builder.p3;
p4=builder.p4;
p5=builder.p5;
}
}
//client
App app=new App.Builder(1,2).setP3(3).setP4(4).setP5(5).build();

优点:

  • 灵活
  • 具名
  • 安全
    缺点:
  • 需要创建构造器,性能开销

3、私有构造器强化单例属性

  • 见单例模式
  • 唯一元素的枚举类也是一种单例的最好方法

4、私有构造函数使得某些类不可实例化(工具类等)

5、不要创建多余的对象

  • 避免频繁使用装箱基本类型
    1
    2
    3
    4
    Long s=0L;
    for(long i=0;i<100000;i++){
    s+=i;//warning!每次都创建一个Long对象
    }

6、消除过期的对象引用

  • 如果类自己管理内存,注意内存泄漏,比如自己实现的堆栈
  • 缓存要注意清理问题
  • 监听器与回调

7、避免使用终结方法

对于所有对象都通用的方法

8、覆盖equals方法注意:

  • 自反性、对称性、传递性、一致性、非空性(x.equals(null)==false)
  • 使用instanceof检查参数是否为正确类型
  • 注意方法的入参类型是Object,否则是重载
  • ==比较不是float与double的基本类型,引用使用equals,float使用Float.compare(),double使用Double.compare()

9、覆盖equals时候始终覆盖hashcode

  • 否则影响hash

10、始终覆盖toString()

11、谨慎实现cloneable接口

  • 一般不要实现,否则需要提供良好的实现,深度拷贝与同步

12、考虑实现comparable接口

  • 获得更多的内置集合功能,如排序等

类和接口

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

  • 封装

14、访问属性使用访问方法而不是直接访问

  • 私有的嵌套类可以直接访问,危害性较小

15、尽量使用不可变性

  • 线程安全,内部信息共享,提供大量构件

16、使用复合而不是继承

  • 复合和转发机制(装饰器模式)
  • 只有在is-a关系时使用继承

17、要么为继承而设计,要么禁止继承

  • 需要提供详细的文档说明
  • 好的API文档描述一个操作做了什么,而不是如何做的
  • 如果允许继承,构造器不可以调用可覆盖的方法

18、接口优于抽象类

  • 接口利于实现混合类型(某个类具有某种特定的功能,如Comparable接口)
  • 利于实现非层次的类结构,比如歌唱家与作家接口
  • 一般导出接口的骨架实现抽象类或简单实现(用于直接使用或继承)
  • 抽象类对比接口更容易演变
  • 公开接口一旦发布,几乎无法修改,建议同时发布骨架抽象类

19、接口只用于定义类型

  • 导出常量使用枚举,而不是包含一堆static final变量

20、类层次优于标签类

  • 不要在一个类中完成相似而又不同的多套逻辑(三角形与原形面积计算,一个类中使用switch类的逻辑判断)
  • 针对相似而不同的逻辑关系,考虑拆成子类父类的类层次关系

21、用函数对象标识策略

  • 策略模式

22、优先考虑静态成员类

  • 非静态的嵌套类每个实例都会持有一个外围类的引用
  • 如果嵌套类不需要外围实例的引用,就使用静态,否则使用非静态