0%

Java编程思想笔记

考虑使用静态工厂方法替代构造方法

1
2
3
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}

使用Builder模式替代参数过多的构造器

当类构造器的参数过多时,容易导致程序由于参数传递顺序等问题产生一些难以调试的Bug,因此有一种解决方法是JavaBeans模式,这种模式通过提供一个无参的构造器,然后通过一系列的setter方法去设置对象的属性,这种模式有两个缺点:

  • 代码冗长
  • 由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean 可能处于不一致的状态。一个相关的缺点是,JavaBeans 模式排除了让类不可变的可能性,并且需要增加工作以确保线程安全。

另一种解决方式是Builder模式,通过在类中提供一个通常是static类型的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
// 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) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();

总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是如果许多参数是可选的或相同类型的。客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且 builder 比 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
    // Can you spot the "memory leak"?
    public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
    elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
    ensureCapacity();
    elements[size++] = e;
    }

    // 内存泄漏
    public Object pop() {
    if (size == 0)
    throw new EmptyStackException();
    return elements[--size];
    }

    // 修改版本
    public Object pop() {
    if (size == 0)
    throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
    }

    /**
    * Ensure space for at least one more element, roughly
    * doubling the capacity each time the array needs to grow.
    */
    private void ensureCapacity() {
    if (elements.length == size)
    elements = Arrays.copyOf(elements, 2 * size + 1);
    }
    }
  • 内存泄漏原因之二:缓存。可以使用WeakHashMap来避免。

  • 内存泄漏原因之三:监听器等回调方法。

避免使用Finalizer和Cleaner(Java 9)机制

使用 try-with-resources 语句替代 try-finally

1
2
3
4
5
try {
throw new Exception();
} finally {
throw new NullPointerException();
}

如上语句中,只会捕获finally中的异常。为了防止该问题的出现可以使用try-with-resources,这是Java 7的一个特性,Java 类库和第三方类库中的许多类和接口现在都实现或继承了AutoCloseable接口。如果你编写的类表示必须关闭的资源,那么这个类也应该实现 AutoCloseable 接口。

1
2
3
public interface AutoCloseable {
void close() throws Exception;
}

因此建议使用如下方式关闭资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}

// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}

如果调用 readLine 和(不可见)close 方法都抛出异常,则后一个异常将被抑制(suppressed),而不是前者。 事实上,为了保留你真正想看到的异常,可能会抑制多个异常。 这些抑制的异常没有被抛弃, 而是打印在堆栈跟踪中,并标注为被抑制了。 你也可以使用 getSuppressed 方法以编程方式访问它们,该方法在 Java 7 中已添加到的 Throwable 中。

可以在 try-with-resources 语句中添加 catch 子句,就像在常规的 try-finally 语句中一样。这允许你处理异常,而不会在另一层嵌套中污染代码:

1
2
3
4
5
6
7
8
9
// try-with-resources with a catch clause
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}

重写equals方法

重写equals方法必须遵守相应的规则:

  • 自反性: 对于任何非空引用 x,x.equals(x) 必须返回 true。
  • 对称性: 对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true。
  • 传递性: 对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,则 x.equals(z) 必须返回 true。
  • 一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用必须始终返回 true 或始终返回 false。
  • 对于任何非空引用 x,x.equals(null) 必须返回 false。
  • 重写equals方法一定要重写hashCode方法。

equals方法与hashCode方法有几个约定:

  • 当在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。
  • 如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
  • 如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。

当不重写 hashCode 时,所违反第二个关键条款是:相等的对象必须具有相等的哈希码( hash codes)。

始终重写toString方法

谨慎地重写clone方法

考虑实现Comparable接口

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

  • private —— 该成员只能在声明它的顶级类内访问。
  • package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符(接口成员除外,它默认是公共的),这是默认访问级别。
  • protected —— 成员可以从被声明的类的子类中访问(会受一些限制 [JLS, 6.6.2]),以及它声明的包中的任何类。
  • public —— 该成员可以从任何地方被访问。

最小化可变性

不可变类简单来说是它的实例不能被修改的类。包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。Java平台类库包含许多不可变的类,包括 String 类,基本类型包装类以及 BigInteger 类和 BigDecimal 类。

不可变类比可变类更容易设计,实现和使用。他们不太容易出错,更安全。除非有充分的理由使类成为可变类,否则类应该是不可变的。要使一个类不可变,需要遵循以下五条规则:

  • 不要提供修改对象状态的方法。
  • 确保这个类不能被继承。
  • 把所有属性设置为 final。
  • 把所有的属性设置为 private。
  • 确保对任何可变组件的互斥访问。如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。在构造方法,访问方法和 readObject 方法中进行防御性拷贝。

不可变的特点:

  • 不可变对象本质上是线程安全的; 它们不需要同步
  • 不仅可以共享不可变的对象,而且可以共享内部信息
  • 不可变对象为其他对象提供了很好的构件
  • 不可变对象提供了免费的原子失败机制
  • 不可变类的主要缺点是对于每个不同的值都需要一个单独的对象

为了保证不变性,一个类不得允许子类化,这可以通过使类用 final 修饰,但是还有另外一个更灵活的选择:可以使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Immutable class with static factories instead of constructors
public class Complex {

private final double re;
private final double im;

private Complex(double re, double im) {
this.re = re;
this.im = im;
}

public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}

... // Remainder unchanged
}

总而言之,坚决不要为每个属性编写一个 get 方法后再编写一个对应的 set 方法。 除非有充分的理由使类成为可变类,否则类应该是不可变的。 不可变类提供了许多优点,唯一的缺点是在某些情况下可能会出现性能问题。

组合优于继承(装饰器模式)

继承打破了封装,换句话说,一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。

为了具体说明,假设有一个使用 HashSet 的程序,为了调整程序的性能,需要查询 HashSet ,从创建它之后已经添加了多少个元素,为了提供这个功能,编写了一个 HashSet 变体,它保留了尝试元素插入的数量,并导出了这个插入数量的一个访问方法,HashSet 类包含两个添加元素的方法,分别是 add 和 addAll,所以我们重写这两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;

public InstrumentedHashSet() {
}

public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@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;
}
}

此时的addAll方法会出现一个问题,因为在其内又调用了add方法,因此计数器叠加了两次。

为了解决这个问题,可以使用组合替代继承。给新类增加一个私有属性,该属性是现有类的实例引用,这种设计被称为组合。因此可以这样修改上述功能代码:

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
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 boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}

public boolean 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 equals(Object o) {
return s.equals(o);
}

@Override
public int hashCode() {
return s.hashCode();
}

@Override
public String toString() {
return s.toString();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}
}

InstrumentedSet 类被称为包装类,因为每个 InstrumentedSet 实例都包含(“包装”)另一个 Set 实例。 这也被称为装饰器模式,因为 InstrumentedSet 类通过添加计数功能来“装饰”一个集合。

总之,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。

接口优于抽象类

接口通过包装类模式确保安全的,强大的功能增强成为可能。如果使用抽象类来定义类型,那么只能继承,生成的类比包装类更脆弱。

接口仅用来定义类型

使用静态成员类而不是非静态类

如果声明了一个不需要访问宿主实例的成员类,应该使它成为一个静态成员类,而不是非静态的成员类,因为非静态内部类的每个实例都会有一个隐藏的外部引用给它的宿主实例,如前所述,存储这个引用需要占用时间和空间;更严重的是可能会导致即使宿主类在满足垃圾回收的条件时却仍然驻留在内存中,由此产生的内存泄漏可能是灾难性的,由于引用是不可见的,所以通常难以检测到。

将源文件限制为单个顶级类

虽然 Java 编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能,使用哪个定义会受到源文件传递给编译器的顺序的影响。

不要使用原始类型

术语 中文含义 举例
Parameterized type 参数化类型 List<String>
Actual type parameter 实际类型参数 String
Generic type 泛型类型 List<E>
Formal type parameter 形式类型参数 E
Unbounded wildcard type 无限制通配符类型 List<?>
Raw type 原始类型 List
Bounded type parameter 限制类型参数 <E extends Number>
Recursive type bound 递归类型限制 <T extends Comparable<T>>
Bounded wildcard type 限制通配符类型 List<? extends Number>
Generic method 泛型方法 static <E> List<E> asList(E[] a)
Type token 类型令牌 String.class

列表优于数组

数组是协变的(covariant),这意味着如果 Sub 是 Super 的子类型,则数组类型 Sub[] 是数组类型 Super[] 的子类型;相比之下,泛型是不变的(invariant),对于任何两种不同的类型 Type1 和 Type2,List 既不是 List 的子类型也不是父类型。

1
2
3
4
5
6
7
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException

// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");

优先考虑泛型

使用限定通配符来增加API的灵活性

使用枚举类型替代整型常量

注解优于命名模式

如下实例:

1
2
3
4
5
6
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});

可以修改为:

1
2
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

另一个实例,在枚举中:

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
// Enum type with constant-specific class bodies & data 
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; }
};

private final String symbol;

Operation(String symbol) { this.symbol = symbol; }

@Override
public String toString() { return symbol; }

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

DoubleBinaryOperator 接口是 java.util.function 中许多预定义的函数接口之一,它表示一个函数,它接受两个 double 类型参数并返回 double 类型的结果。因此可以修改上述枚举如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum Operation {
PLUS ("+", (x, y) -> x + y),
MINUS ("-", (x, y) -> x - y),
TIMES ("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);

private final String symbol;
private final DoubleBinaryOperator op;

Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}

@Override
public String toString() { return symbol; }

public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}

lambda 没有名称和文档; 如果计算不是自解释的,或者超过几行,则不要将其放入 lambda 表达式中。 一行代码对于 lambda 说是理想的,三行代码是合理的最大值。

Lambda 仅限于函数式接口,它不能获取对自身的引用。

通过接口引用对象

明智地进行优化

1
2
不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。
—Donald E. Knuth [Knuth74]

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

对于可恢复的情况,要抛出受检异常;对于程序错误,就要抛出运行时异常。不确定是否可恢复,就抛出为受检异常。不要定义任何既不是受检异常也不是运行异常的抛出类型。要在受检异常上提供方法,以便协助程序恢复。

但是要避免不必要的使用受检异常。

抛出与抽象对应的异常

为了避免方法抛出的异常和它所执行的任务没有任何关系,我们可以在程序中进行异常转译:更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,如下所示:

1
2
3
4
5
6
7
8
public E get(int index) {
ListIterator<E> i = listIterator(index);
try {
return(i.next() );
} catch (NoSuchElementException e) {
throw new IndexOutOfBoundsException("Index: " + index);
}
}

如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。

保持失败原子性

当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中,即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者期望能从这种异常中进行恢复。一般而言,失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性 (failure atomic)。

有几种途径可以实现这种效果:

  1. 设计不可变的对象。如果一个操作失败了,它可能会阻止创建新的对象,但是永远也不会使已有的对象保持在不一致的状态之中,因为当每个对象被创建之后它就处于一致的状态之中,以后也不会再发生变化。

  2. 在可变对象上执行操作时,获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性,这可以使得在对象的状态被修改之前,先抛出适当的异常。如下代码所示:

    1
    2
    3
    4
    5
    6
    7
    public Object pop() {
    if ( size == 0 )
    throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; /* Eliminate obsolete reference */
    return(result);
    }

    另一种类似的方式是:调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。

  3. 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。

  4. 编写一段恢复代码 (recovery code),由它来拦截操作过程中发生的失败,以及便对象回滚到操作开始之前的状态上,这种办法主要用于永久性的(基于磁盘的)数据结构。

总而言之,作为方法规范的一部分,它产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API 文档就应该清楚地指明对象将会处于什么样的状态。遗憾的是,大量现有的 API 文档都未能做到这一点。

不要忽略异常

如果选择忽略异常,catch 块中应该包含一条注释,说明为什么可以这么做。

谨慎地使用延迟初始化

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

移位运算比乘除法快

  • 从效率上看,使用移位指令有更高的效率,因为移位指令占2个机器周期,而乘除法指令占4个机器周期。
  • 从硬件上看,移位对硬件而言更容易实现。