《Effective Java》读书笔记

引言

Java 语言支持四种类型

  • 接口(包括注释)
  • 类(包括enum)
  • 数组
  • 基本类型

创建和销毁对象

静态工厂方法代替构造器

优点:

  • 静态工厂方法有名称
  • 不必每次使用的时候都创建一个新对象
  • 可以返回原类型的任何子类型
  • 返回的对象的类可以随着每次调用的参数不同而发生变化
  • 方法返回的对象所属的类,在编写包含静态工厂方法的类时可以不存在

缺点:

  • 类如果不含有公有的或者受保护的构造器,就不能被子类化
  • 程序员很难发现静态工厂方法

类的构造器具有多个参数时可考虑使用构建器

建造者模式: 不直接生成想要的对象,而是让调用者使用必要的参数调用构造器(或者静态工厂),
得到一个builder对象,然后客户端在builder对象上调用类似setter的方法,来设置相关参数。最后,
使用者调用无参的build方法生成通常是不可变的对像。
优点: 既能保证像重叠构造器模式的安全性,又能有JavaBeans模式的可读性。
代码实例,静态类构造器:

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
51
52
53
54
55
56
57
58
59
60
61
//Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;

public static class Builder{
//Required parameters
private final int servingSize;
private final int servings;

//Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize,int servings){
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val){
calories = val;
return this;
}
public Builder fat(int val){
fat = val;
return this;
}
public Builder sodium(int val){
sodium = val;
return this;
}
public Builder carbohydrate(int val){
carbohydrate = val;
return this;
}
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder){
servings = builder.servings;
servingSize = builder.servingSize;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}

}
//Builder Test
public static void Test_NutritionFacts(){
NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35)
.carbohydrate(27).build();
System.out.println(cocaCola.GetCalories());
System.out.println(cocaCola.GetServings());
System.out.println(cocaCola.GetServingSize());
};

代码实例,使用类层次根部的抽象类表示各式各样的pizza(类层次结构的builder模式):

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//Builder pattern for class hierarchies
public class Pizza {
public enum Topping{HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
final Set<Topping> toppings;

// 泛型类,带有一个递归类型参数,和抽象的self方法一样,
// 允许在子类适当地地方进行方法链接,不需要转换类型
abstract static class Builder<T extends Builder<T>>{

//creating one empty set
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

public T addTopping(Topping topping){
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();

// Subclasses must overrite this method to return "this"
// 针对Java缺乏self类型的解决方案
protected abstract T self();

}
Pizza(Builder<?> builder){
toppings = builder.toppings.clone(); //
}

}

//subclass
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder>{
private final Size size;
public Builder(Size size){
this.size = Objects.requireNonNull(size);
}
@Override public NyPizza build(){
return new NyPizza(this);
}
@Override protected Builder self(){
return this;
}
}
private NyPizza(Builder builder){
super(builder);
size = builder.size;
}
public Size GetSize(){
return this.size;
}
}

//subclsss
public class Calzone extends Pizza{
private final boolean sauceInside;

public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; //Default

public Builder sauceInside(){
sauceInside = true;
return this;
}
@Override public Calzone build(){
return new Calzone(this);
}
@Override protected Builder self(){
return this;
}

}
private Calzone(Builder builder){
super(builder);
sauceInside = builder.sauceInside;
}
public boolean GetSauce(){
return this.sauceInside;
}


}

//Builder Test
public static void Test_Pizza(){
NyPizza pizza = new NyPizza.Builder(NyPizza.Size.SMALL)
.addTopping(Pizza.Topping.SAUSAGE).addTopping(Pizza.Topping.ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(Pizza.Topping.HAM).sauceInside().build();
System.out.println(pizza.GetSize());
System.out.println(calzone.GetSauce());
}

用私有构造器或者枚举类型强化Singleton属性

单例实现的三种方法:

final类型的公有静态成员

1
2
3
4
5
6
7
//Singleton with public field
public class Elvis{
public static final Evis INSTANCE = new Elvis();
private Elvis(){ ... }

public void leaveTheBuilding() { ... }
}

缺点:特权用户可通过AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。
优点:1. 简单;2. 清晰的向用户表达出该类是单例模式。

公有静态成员工厂

1
2
3
4
5
6
7
8
//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() { ... }
}

缺点:1.实现稍复杂;2.具有同上相同风险;
优点:1.灵活,不改变接口的情况下,修改器内部实现;2.实现泛型单例工厂;3.通过方法引用提供实例。
(以上2种方法实现,排除使用第二种方法优点的情况下,优先使用方法1)

声明单个元素的枚举类型

1
2
3
4
5
6
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;

public void leaveTheBuilding(){ ...}
}

缺点:若单例必需扩展一个超类,而不是Enum,则不宜使用该方法;
优点:1.更加简洁,无偿提供序列化机制;2.防止任何形式的多次序列化攻击;
(单元素的枚举是实现常用单例模式的最佳方法)

优先考虑依赖注入来引用底层资源

及时消除过期的对象引用,防止内存泄漏

Stack类易发生内存泄漏

需要主动销毁引用,通过stack[size] = null

缓存易发生内存泄漏

用WeakHashMap代表缓存,当缓存过期会被自动消除;或者使用后台线程定期清理。

监听器和其他回调易发生内存泄漏

回调注册后需要显式的取消注册
最佳方法是保存对回调的弱引用(weak reference)

try-with-resources 优先于try-finally用于关闭资源

要使用try-with-resources这个构造资源必需先实现AutoCloseable接口

1
2
3
4
5
6
7
//try-with-resources -the best way to close resources.
static String firstLineOfFile(String path) throws IOException{
try(BufferedReader br = new BufferedReader(
new FileReader(path))){
return br.readLine();
}
}

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

覆盖equals时的通用约定

对称的、传递的和一致的

实现高质量equals方法诀窍

  1. 使用==操作符检查”参数是否为这个对象的引用”,如果是返回true;
  2. 使用instanceof操作符检查 “参数是否为正确的类型,不是返回false”;
  3. 把参数转换成正确的类型;
  4. 对于该类中的每个”关键”域,检查参数中的域是否与该对象中对应的域相匹配,如果匹配怎返回true。
    实例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //Class with a typical equals method
    public final class PhoneNumber {
    private final short areaCode, prefix,lineNum;

    public PhoneNumber(int areaCode, int prefix, int lineNum) {
    this.areaCode = rangeCheck(areaCode, 999, "area code");
    this.prefix = rangeCheck(prefix, 999, "prefix");
    this.lineNum = rangeCheck(lineNum, 999, "line num");
    }
    private static short rangeCheck(int val, int max, String arg){
    if(val < 0 || val > max){
    throw new IllegalArgumentException(arg + ":" + val);
    return (short)val;
    }

    @Override public boolean equals(Object o){
    if(o == this)
    return true;
    if(!(o instanceof PhoneNumber))
    return false;
    PhoneNumber pn = (PhoneNumber)o;
    return pn.lineNum == lineNum && pn.prefix ==prefix && pn.areacode == areaCode;
    }
    }

警告:
覆盖equals方法时总要覆盖hashCode;
不要企图让equals方法过去智能;
不要将equals声明中的Object类替换为其他的类型。
Auto Value框架提供了很好的替代方法,可以不必手工编写equals和hashcode方法

始终要覆盖toString

谨慎覆盖clone方法

对象拷贝应选择提供一个拷贝构造器或者拷贝工厂

1
2
3
4
//copy constructor
public Yum(Yum yum){ ... }
//Copy factory
public static Yum newInstance(Yum yum){ ... }

Comparable接口实现

每档实现一个对排序敏感的类时,都应该让这个类实现Comparable接口。

类和接口

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

成员的四种可访问性

  1. 私有的(private),显示声明;
  2. 包级私有(package-private)默认;
  3. 受保护的(protected),显示声明;
  4. 公有的(public)任何地方都可以访问成员。

让类具有公有的静态final数组域,后者返回这种域的访问方法,是错误的(客户端能够修改数组中的内容)

解决方法:

  1. 使公有数组编程私有的,并增加一个公有的不可变列表:

    1
    2
    3
    private static final Thing[] PRIVATE_VALUES = { ... };
    public static final List<Thing> VALUES =
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
  2. 使数组变成私有的,并添加一个公有方法,返回私有数组的一个拷贝:

    1
    2
    3
    4
    private static final Thing[] PRIVATE_VALUES = { ... };
    public static final Thing[] values(){
    return PRIVATE_VALUES.clone();
    }

复合优先于继承

复合(composition)

不扩展现有的类,而是在新的类中增加一个私有域,现有类变成新类的一个组件使用。

转发(forwarding)

新类中的每个实例方法调用被包含的现有类实例中对应的方法,并返回它的结果,新类中的方法被成为转发方法。

实例(新类和转发类):

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
//Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s){
super(s);
}
@Override public boolean add(E e){
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c){
addCount += c.size();
return super.addAll(c);
}
public int getAddCount(){
return addCount;
}
}

//Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s){this.s = s;}

public void clear() {s.clear();}
public boolean contains(Object o){return s.contains(o);}
public boolean isEmpty() {return s.isEmpty();}
public int size() {return s.size()}
public Iterator<E> iterator() {return s.iterator();}
public boolean add(E e) {return s.add(e);}
public boolean remove(Object o) {return s.remove(o);}
public boolean containsAll(Collection<?> c){ return s.containsAll(c);}
public boolean addAll(Collection<? extends E> c){ return s.addAll(c);}
public removeAll(Collection<?> c){return s.removeAll(c);}
public retainAll(Collection<?> c){return s.retainAll(c);}
public Object[] toArray() {return s.toArray();}
public <T> T[] toArray(T[] a) {return s.toArray(a);}
@Override public boolean equeals(Object o){return s.equals(o);}
@Override public int hashCode() {return s.hashCode();}
@Override public String toString() {return s.toString();}
}

设计继承类则必需提供详细文档说明,或不使用继承

为继承而设计的父类,唯一的方法是编写三个及以上的子类

父类需要遵循的一些约束(消除类中可覆盖方法的自用特性)

  • 构造器不可调用可被覆盖的方法
  • clone和readObject都不能调用可覆盖的方法

    把不需要子类化的类要禁止子类化

    禁止类子类化的两种方法

  • 方法一 把这个类声明为final
  • 方法二 把所有构造器都变成私有的,增加一些公有的静态工厂来代替构造器

    接口优于抽象类

    接口是定义mixin(混合类型)的理想选择,抽象类不能被用于定义mixin

    抽象骨架实现类

    接口负责定义类型,及一些缺省方法,骨架实现类负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。
    扩展骨架实现占了实现接口之外的大部分工作。这就是模版方法模式。
    如果基本方法和缺省方法覆盖了接口,就不需要骨架实现类了,否则就要编写一个类,声明实现接口,并实现所有剩下的接口方法。
    该类中可以包含任何非公有的域,以及适合该任务的方法。

实例Map.Entry接口:

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
//Skeletal implementation class
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V>{
// Entries in a modifiable map must override this method
@Override public V setValue(V value){
throw new UnsupportedOperationException();
}
// Implements the general contract of Map.Entry.equals
@Override public boolean equals(Object o){
if (o == this){
return true;
}
if(!(o instanceof Map.Entry)){
return false;
}
Map.Entry<?,?> e = (Map.Entry) o;
return Objects.equals(e.getKey(), getKey())
&& Objects.equals(e.getValue(), getValue());
}

//Implements the general contract of Map.Entry.hashCode
@Override public int hashCode() {
return Objects.hashCode(getKey())
^ Objects.hashCode(getValue());
}

@Override public String toString() {
return getKey() + "=" + getValue();
}
}
//该骨架实现不能在Map.Entry接口中实现,也不能作为子接口,**因为不允许缺省方法覆盖Object方法** (Object方法必须被显示覆盖),
如equals、hashCode和toString

泛型

尽量消除非受检警告

如果无法消除警告,同时可以证明引起警告的代码是类型安全的,才可以用一个@SuppressWarnings(“unchecked”)
注解来禁止这条警告。
应该在尽可能小的范围是用SuppressWarnings注解

列表优于数组

数组在运行时检查和强化元素类型;泛型则在编译时强化类型信息,在运行时通过擦除丢失类型信息。

创建具体类型化的泛型数组非法(错误提示或警告),创建无限制通配类型的数组合法

泛型会擦除具体类型,导致不同类型的数组互相可转换,违反类型转换规则。比如string类型泛型数组的元素插入Integer类型泛型数组中。
数组和泛型如果放在一起使用得到警告,可尝试用列表代替数组(list)

泛型通配符的PECS(producer-extends,consumer-super)原则

类型安全的异构容器(实例Favorites)

1
2
3
4
5
6
7
8
9
10
11
12
13
//Typesafe heterogeneous container pattern - implementation
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();

public <T> void putFavorite(Class<T> type, T instance){
favorites.put(Objects.requeireNonNull(type),instance);
}
public <T> T getFavorite(Class<T> type){
return type.cast(favorites.get(type));
}
}
//每个键都有一个不同的参数化类型,比如一个可以是Class<String>,下一个可以是Class<Integer>,
这就是异构。

枚举和注解

用enum代替int常量

  1. int枚举模式不具有类型安全性,enum模式改善了这种缺陷;
  2. enum枚举允许添加任意的方法和域,并实现任意的接口;

实例:

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
//Enum type with data and behavior
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23), 3.393e6);

private final double mass; // In kilograms
private final double radius; // In meters
private final double surfacegravity; // In m/s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius){
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() {return mass;}
public double radius() {return radius;}
public double surfaceGravity() {return surfaceGravity;}

public double surfaceWeight(double mass){
return mass * surfaceGravity; // F = ma
}
}
//每个枚举变量括号后面的数值就是传递给构造器的参数
public class WeightTable {
public static void main(String[] args){
doulbe earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for(Planet p: Planet.values()){
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
}

特定于枚举常量的方法实现

在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法
实例:

1
2
3
4
5
6
7
8
9
//Enum type with constant-specific method implementations
public enum Operation{
PLUS {public double apply(double x, double y) {return x + y;}},
MINUS {public double apply(double x, double y) {return x - y;}},
TIMES {public double apply(double x, double y){return x * y;}},
DIVIDE {public double apply(double x, double y){return x / y;}};

public abstract double apply(double x, double y);
}

枚举常量后可跟相应的符号,并通过覆盖toString方法输出常量对应的符号,如果这样,需要编写一个由常量符号转成
常量本身的方法,类似于valueOf

策略枚举

实现 每当添加一个枚举常量时,就强制选择一种加班报酬的策略,可通过策略枚举实现
实例:

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
//The strategy enum pattern
enum PayrollDay {
MONDAY, TUESDATY, WEDNESDAY, FRIDAY,
SATURDAY(PayType.WEEKDAY),SUNDAY(PayType.WEEKEND);

private final PayType payType;

PayrollDay(PayType payType){this.payType = payType;}
PayrollDay() {this(PayType.WEEKDAY);} //Default

int pay(int minutesWorked, int payRate){
return payType.pay(minutesWorked, payRate);
}

//The strategy enum type
private enum PayType{
WEEKDAY {
int overtimePay(int minsWorked, int payRate){
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate /2;
}
},
WEEKEND{
int overtimePay(int minsWorked, int payRate){
return minsWorked * payRate / 2;
}
};

abstract int overtimePay(int mins, int paRate);
private static final int MINS_PER_SHIFT = 8 * 60;

int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}

枚举索引数组使用EnumMap

标记接口和标记注解

Lambda和Stream

Lambda优先于匿名类

注意:

Lambda没有名称和文档,一行是理想的,三行是最大极限。
Lambda限于函数接口(包括构造函数)
不适合抽象类的实例,抽象类实例可通过匿名类完成
Lambda中,关键字this是指外围实例(匿名类中,关键字this是指匿名类实例)
Lambda是表示小函数对象的最佳方式

方法引用和Lambda哪个简洁用哪个

stream 谨慎使用

为了正确使用stream,必需了解收集器,重要的收集器工厂是toList、toSet、toMap、groupingBy和joining

方法

保护性拷贝

对于参数类型可以被不可信任方子类化的参数,不要使用clone方法进行保护性拷贝,应该使用new
实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//defensive copy class
public final class Period{
private final Date start;
private final Date end;
//defensive copies of parameters
public Period(Date start, Date end){
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());

if(this.start.compareTo(this.end)>0){
throw new IllegalArgumentException(this.start + " after " + this.end);
}
}

//defensive copies of internal fields
public Date start(){
return new Date(start.getTime());
}
public Date end(){
return new Date(end.getTime());
}
}

慎用重载方法

使用重载方法安全保守的策略是,永远不要导出两个具有相同参数数目的重载方法

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

Javadoc 生成注释文档

通用编程

for-each循环的优势与劣势

优势:避免混乱和出错
劣势:不能用于集合或数组的修改编辑,推荐仅在遍历时使用。

StringBuilder代替String来存储构造字符串,可提高性能

不要使用字符串连接操作符来合并多个字符串,除非对性能无要求,否则应该使用StringBuilder的append方法。

尽可能使用接口引用对象

接口优于反射机制

异常

最常见可重用异常

异常使用场合
IllegalArgumentException非null的参数值不正确
IllegalStateException不适合方法调用的对象对象状态
NullPointerException在禁止使用null的情况下参数值为null
IndexOutOfBoundsException下标参数值越界
ConcurrenModificationExcetion在禁止并发修改的情况,检测到对象的并发修改
UnsupportedOperationException对象不支持用户请求的方法

方法调用的失败原子性

失败的方法调用应该使对象保持在被调用之前的状态,具有该属性的方法被称为具有失败原子性

并发

Java语言规范保证读或者写一个变量是院子的(atomic),除非这个变量是long或者double类型

尽量将同步区域内的工作量限制到最少

executor task和stream 优先于线程

  • 如果编写的是小程序,或者是轻量负载的服务器,使用Executors.newCachedThreadPool是不错的选择
  • 如果服务器负载重,最好使用Executors.newFixedThreadPool,它提供了一个包含固定线程数目的线程池。
  • 最大限度的控制,可以使用ThreadPoolExecutor

    java.util.concurrent 中高级工具分为三类

  • Executor Framework
  • 并发集合(Concurrent Collection)
  • 同步器(Synchronizer)

序列化

尽可能使用第三方序列化方法代替Java序列化方法

  • Json
  • protobuf