《Effective Java》摘记

第一遍看完感觉没有学到什么,又看了一遍,然后自己找了些实例对比着理解了书上的要点,算是对Java编程有了更多的认识。以下是 《Effective Java 2nd edition》的摘记。

Consider static factory methods instead of constructors

相比构造函数,静态工厂方法有哪些优点了?首先,静态工厂方法有自己的名字,这可以增强程序的可读性;其次,静态工厂方法每次调用时,不用创建新的对象实例;第三,静态工厂方法可以返回其返回类型的任何子类型对象。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public final class Boolean implements java.io.Serializable,
Comparable<Boolean>
{

public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

....
//根据boolean值返回相应的Boolean对象
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

}

Consider a builder when faced with many constructor parameters

当存在多个参数时,静态工厂方法和构造函数都会遇到问题。以往的做法是,使用telescoping constructor,对外提供多个构造函数,每个构造函数有不同个数的参数。例如,Android中的View,共有四个构造函数,第一个构造函数有一个参数,一次类推,第四个构造函数则由四个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public View(Context context) {
mContext = context;
....
}

public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}


public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
.....
}

但如果参数太多,telescoping constructor方法也会变得难以卒读。还有一种方式就是采用JavaBeans,但JavaBeans也有自身的不足:由于构造实例是通过多个接口进行的,一个JavaBean可能使实例处于不一致的状态,另一方面也会一个类处于可变状态(mutable)。

关于什么是JavaBeans: what-is-a-javabean-exactly
wikipedia:JavaBeans

结合上述两种方法的优点,builder模式很好的解决了上述问题:用户获取到一个builder对象,然后将设定的参数传递给该对象,最后通过buidler对象来产生一个不可变的目标对象。通常,builder对象是被构造类的静态成员类。在Android中,构造AlertDialog时,就用到了这种方法:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

public class AlertDialog extends Dialog implements DialogInterface {

public static class Builder {
private final AlertController.AlertParams P;

private boolean mIsDeviceDefaultLight;
private int mTheme;

public Builder(Context context) {
this(context, resolveDialogTheme(context, 0));
}

public Builder(Context context, int themeResId) {
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, themeResId)));

mTheme = themeResId;
....
}

.....

public Builder setTitle(CharSequence title) {
P.mTitle = title;
return this;
}

public Builder setCustomTitle(View customTitleView) {
P.mCustomTitleView = customTitleView;
return this;
}

public Builder setMessage(CharSequence message) {
P.mMessage = message;
return this;
}

public Builder setIcon(@DrawableRes int iconId) {
P.mIconId = iconId;
return this;
}
....

public Builder setPositiveButton(CharSequence text, final OnClickListener listener) {
P.mPositiveButtonText = text;
P.mPositiveButtonListener = listener;
return this;
}

public Builder setNegativeButton(@StringRes int textId, final OnClickListener listener) {
P.mNegativeButtonText = P.mContext.getText(textId);
P.mNegativeButtonListener = listener;
return this;
}

public Builder setNeutralButton(@StringRes int textId, final OnClickListener listener) {
P.mNeutralButtonText = P.mContext.getText(textId);
P.mNeutralButtonListener = listener;
return this;
}

public Builder setCancelable(boolean cancelable) {
P.mCancelable = cancelable;
return this;
}

public Builder setOnCancelListener(OnCancelListener onCancelListener) {
P.mOnCancelListener = onCancelListener;
return this;
}

public Builder setItems(@ArrayRes int itemsId, final OnClickListener listener) {
P.mItems = P.mContext.getResources().getTextArray(itemsId);
P.mOnClickListener = listener;
return this;
}

public Builder setAdapter(final ListAdapter adapter, final OnClickListener listener) {
P.mAdapter = adapter;
P.mOnClickListener = listener;
return this;
}

public Builder setCursor(final Cursor cursor, final OnClickListener listener,
String labelColumn) {
P.mCursor = cursor;
P.mLabelColumn = labelColumn;
P.mOnClickListener = listener;
return this;
}
.....

public Builder setView(View view) {
P.mView = view;
P.mViewLayoutResId = 0;
P.mViewSpacingSpecified = false;
return this;
}

public AlertDialog create() {
AlertDialog dialog;
if(mIsDeviceDefaultLight) {
dialog = new AlertDialog(P.mContext, mTheme , false);
} else {
dialog = new AlertDialog(P.mContext, 0, false);
}

P.apply(dialog.mAlert);
....
return dialog;
}

public AlertDialog show() {
final AlertDialog dialog = create();
dialog.show();
return dialog;
}
}

}

总的说来,在所构造的对象有很多参数时,特别地,有些参数时可选的,builder模式是一个非常不错的选择。相比而言,使用builder模式,客户端代码更简洁易读(telescoping constructor),也更安全(可构造一个不可变的对象,JavaBeans)。

Eliminate obsolete object references

An obsolete reference is simply a reference that will never be dereferenced again. Memory leaks in garbage-collected languages(known as unintentional object retentions) are insidious. If an object reference is unintentionally retained, not only is that object excluded from garbage collection, but so too are any objects referenced by that object, and so on.

The fix for this sort of problem is simple: null out references once they become obsolete. So, when object references should be nulled?

  1. Nulling out object references should be the exception rather than the norm. The best way to eliminate an obsolete reference is to let the variable that contained the reference fall out of scope. This occurs naturally if you define each variable in the narrowest possible scope.
  2. Generally speaking, whenever a class manages its own memory, the programmer should be alert for memory leaks. Whenever an element is freed, any object references contained in the element should be nulled out.
  3. Another commmon source of memory leaks is caches: once you put an object reference into a cache, it’s easy to forget that it’s there and leave it in the cache long after it becomes irrelevant.
  4. A third common source of memory leaks is listeners and callbacks: if you implement an API where clients register callbacks but don’t deregister them explicitly, they will accumulate unless you take some action.

Obey the general contract when overriding equals

什么时候不需要重载object中的equals方法了?

  1. 类的每个实例都是唯一的:例如像Thread这种活动实体类;
  2. 不用关心类是否提供了“逻辑相等”测试:比如java.util.Random类并不需要重载equals方法来检测两个实例是否相同;
  3. 父类已经重载了equals,而且该方法同样适用于这个类时,不用再重载;
  4. 类时私有的或者包私有的,并且可以确定equals方法不会被调用,则不用重载。

那么,在重载equals方法时,需要遵循那些原则了?

  1. 反射性(reflexive): 对任何非NULL引用 x, x.equals(x)必须为真;
  2. 对称性(symmetric): 对任何非NULL引用 x,y: 当且仅当x.equals(y)返回真时,y.equals(x)才为真;
  3. 传递性(transitive): 对任何非NULL引用x,y,z,如果x.equals(y)y.equals(z)均为真,则x.equals(z)也为真;
  4. 一致性(consisitency): 如果两个非NULL引用x,y相等,那么应该一直相等除非有一个被改变了;
  5. 空值行(Non-nullity): 对于任何非NULL引用 x,x.equals(NULL)为假;

示例:

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

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];

....

public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
}

Always override hashCode when you override equals

如果重载equals时,不重载hashCode的话,在使用基于hash值的集合类,如HashMap,HashSet,Hashtable时,可能出现异常情况。

那么,如何写好一个hashCode函数了?

  1. 将一个int型变量 result 初始化为一个非零值,比如17;
  2. 对于类中不同类型的成员变量,其相应的hash值 c 如下:

    • boolean: c = (f ? 0 : 1)
    • byte, char,short, or int: c = (int) f
    • long: c = (int) (f ^( f >>> 32))
    • float: c = Float.floatToIntBits(f)
    • double: long l = Double.doubleToLongBits(f);c = (int)(1^(l >>> 32))
    • Object: c = f.hashCode()
    • Array: 应用上述规则
    • 将所得hash值计算存入result: result = 31 * result + c;
  3. 返回结果;并确保两个相等(equals)的对象是否有相同的hash值;

示例:

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

public class FileKey {

private long mDev; // ID of device
private long mIno; // Inode number

private FileKey() { }

public int hashCode() {
return (int)(mDev ^ (mDev >>> 32)) +
(int)(mIno ^ (mIno >>> 32));
}

public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof FileKey))
return false;
FileKey other = (FileKey)obj;
if ((this.mDev != other.mDev) ||
(this.mIno != other.mIno)) {
return false;
}
return true;
}

}

Always override toString

重载toString()函数可以让类使用起来更方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public static final class Mode implements Parcelable {
private final int mModeId;
private final int mWidth;
private final int mHeight;
private final float mRefreshRate;

@Override
public String toString() {
return new StringBuilder("{")
.append("id=").append(mModeId)
.append(", width=").append(mWidth)
.append(", height=").append(mHeight)
.append(", fps=").append(mRefreshRate)
.append("}")
.toString();
}
}

Mininmize mutability

一个不可变的类是指其对象不可修改。那么,怎么让一个类不可变了?有以下五个原则:

  1. 不要提供修改对象状态的任何接口(mutators);
  2. 确保类不可扩展(extend):防止类被子类化的简单办法是将其声明为final;或者将其构造函数都声明为private,紧急只提供一个静态的工厂方法来产生类的对象;
  3. 将所有成员变量声明为final
  4. 将所有成员声明为private
  5. 有可变的成员变量时,确保其不可被外部修改;

Favor composition over inheritance

跟方法调用不同的是,继承(不是interface implementation)与封装原则是冲突的。在子类确实一个父类的一个子类型时,继承比较合适。换句话说,类B需要扩展类A,仅当两个类之间存在是一个的关系时。因此,在选择使用继承与组合时,应该问一问:类B是否是一个类A?如果不是,则考虑使用组合。

另外,在考虑使用继承时,还应该考虑下:被继承的类的API是否存在缺陷?如果有的话,使用继承方法则会将API的缺陷带入子类,而使用组合,则可以通过包装(wrapper)的方式隐藏这些缺陷。

Prefer interfaces to abstract classes

It is far easier to evolve an abstract class than an interface(once an interface is released and widely implemented, it is almost impossible to change);

In summary, an interface is generally the best way to define a type that permits multiple implementations. An exception to this rule is the case where ease of evolution is deemed more important than flexibility and power.

Favor static member classes over nonstatic

If you declare a member class that doesnot require access to an enclosing instance, always put the static modifier in its declaration.

Do not use raw types in new code

Using raw types can lead to exceptions at runtime, so don’t use them in new code.

You must use raw types in class literals: List.class,String[].class,and int.class are all legal,but List<String>.classList<?>.class are not

Eliminate unchecked warnings

每一个unchecked warnings都很重要,它表示了一个运行时可能出现的ClassCastException,尽可能的移除这些警告。如果无法消除这些警告,并且可以十分确定代码是typesafe的话,在尽可能小的范围内使用@SuppressWarnings("unchecked")注解将这些警告移除,并作出相应的comment。

User bouned wildcards to increase API flexibility

参数化的类型(parameterized types)是不可变的,换句话说,对于任何两个不同的类型Type1,Type2,List<Type1>List<Type2>是不同的:List<Type1>既非List<Type2>的子类型,也非其父类型。

为了提升API的弹性,使用通用匹配符类型(wildcard types)作为表示生产者(producers)或者消费者(consumers)的输入参数。如果一个输入参数既是生产者又是消费者,通用匹配符类型并无多大帮助.

记住: Producer-extends, Consumer-super(PECS); all comparables and comparators are consumers

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// Producer
public interface List<E> extends Collection<E> {
....
boolean addAll(Collection<? extends E> c);
....
}

// Consumer
public void popAll(Collection<? super E> dst){
while(!isEmpty())
dst.add(pop());
}

Use Enums instead of int constants

什么时候需要使用enum? 在需要固定数目的常量集合时都可以使用。

如果写一个可扩展的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
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

public interface Operation {
double apply(double x, double y);
}

//BasicOperations
public enum BasicOperations implements Operation{

ADD("+"){
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-"){
public double apply(double x, double y){
return x - y;
}
},
MULTIPLY("*"){
public double apply(double x, double y){
return x * y;
}
},
DIVIDE("/"){
public double apply(double x, double y){
if(Double.compare(y, 0.0) == 0){
return Double.NaN;
}

return x/y;
}
};

private final String mOpCode;

BasicOperations(String op){
mOpCode = op;
}

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

//ExtendOperations
public enum ExtendOperations implements Operation{

POWER("^"){
public double apply(double x, double y){
return Math.pow(x, y);
}
},
MOD("%"){
public double apply(double x, double y){
return x % y;
}
};

private final String mOpCode;

ExtendOperations(String op){
mOpCode = op;
}

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


Prefer annotations to naming patterns

在Java 1.5发布之前,一些工具或者框架采用命名模式对某些程序做特殊的处理,例如JUnit测试框架最开始要求使用者将每个测试方法以test开头。这种方法存在的不足是,万一使用者拼写有误,测试就无法通过;另外一个不足是,对于方法以外的程序部分如类,同样无法进行测试。同时,命名模式无法将参数值与程序元素无法关联,例如假如你想要设计一个测试集合,检查一个方法是否抛出特定的异常,这时本质上来说,异常类型是测试的一个参数。

上述问题都可以使用annotations(注解)来得到解决。这里,我们来定义一个注解类型用于设计用于自动运行并且在抛出异常时失败的测试(使用时只需在方法前面加上:@Test即可):

1
2
3
4
5
6
7
8
9
10
11

import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import static java.lang.annotation.ElementType.METHOD;

@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD})
public @interface Test {
}

在Android的源码中,也可以看到很多注解用于对方法或者类做特殊的处理,例如@TargetApi()用于指定某个方法或者构造函数的API级别:

1
2
3
4
5
6
7
8
9
10
11
12

/** Indicates that Lint should treat this type as targeting a given API level, no matter what the
project target is. */
@Target({TYPE, METHOD, CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface TargetApi {
/**
* This sets the target api level for the type..
*/
int value();
}

参考源码: /android/frameworks/base/core/java/android/annotation/

Design Method signatures carefully

怎么使得设计的API更容易被理解,更方便的被使用而不易产生错误了?

  1. 谨慎的命名方法:命名始终遵循标准的命名约定,首要的目标应该是确保命名是可被理解,并且与包内其他命名保持一致;
  2. 不要提供太多的方法: 方法太多容易使类变得难以学习,使用,编写文档,测试跟维护,这个对于interface来说尤其如此。因此,当存疑时,应该直接忽略;
  3. 避免过长的参数列表:保持四个或者更少的参数。怎么减少参数?有三个办法:一个是将该方法分解为几个不同的方法,每个方法只包含一部分参数;第三个方法是使用Builder模式
  4. 对于参数类型来说,使用interface而不是classes;
  5. 尽量使用两个元素的enum类型而非boolean参数,让程序变得更具可读性;

Use overloading judiciously

overloaded(重载)方法是静态的,其在编译时就确定了如何调用,而overriden(覆盖)方法是动态的,具体如何调用是在运行时确定的。

Return emtpy arrays or collections, not nulls

需要返回一个数组或者集合类值时,如果是空,不要返回null,而是返回一个空数组或者空集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// emtpy array
private List<Cheese> allCheeses = new ArrayList<>();

private static final Cheese[] EMPTY_ARRAY_CHEESE = new Cheese[0];

public Cheese[] getCheeses(){
return allCheeses.toArray(EMPTY_ARRAY_CHEESE);
}


// collections
private List<Cheese> allCheeses = new ArrayList<>();

public List<Cheese> getCheeseList(){
if(allCheeses.isEmpty()){
return Collections.emptyList();
}

return allCheeses;
}

Write doc comments for all exposed API elements

合适的描述API,需要将每个输出的类、接口、构造函数、方法以及成员变量写评述。同时,为了代码的可维护性,对于不输出的类、接口、构造函数、方法以及成员变量也应做出适当的说明。

Minimize the scope of local variables

一个减少局部变量作用域的方法是在它第一次使用时才声明。另外一个办法是尽量让方法的变小,并且每个方法只做一件事。

Refer to objects by their interfaces

如果存在合适的接口类型,那么参数,返回值,变量以及成员变量都应该声明为接口类型。

Optimize judiciously

如果过早的进行优化,很可能带来更多的危害而不是好处。不要牺牲合理的架构原则来换取性能:努力写出好的代码而不是快的代码

M.A.Jackson: we follow two rules in the matter of optimization:

  • Rule 1. Don’t do it;
  • Rule 2. (for experts only). Don’t do it yet - that is,not until you have a perfectly clear and unoptimized solution.

Use checked exceptions fo recoverable conditions and runtime exceptions for programming errors

Java语言提供了三类可抛出对象(throwables):可检查的异常,运行时异常,以及错误。对于客户端需要从异常中恢复的情况,应该使用可检查的异常;而对于编程错误(如客户端调用接口有误)则使用运行时异常。

Synchronize access to shared mutable data

适当的实用synchronize,可以确保对象在各个线程中的状态保持一致。在另一方面,同步不仅仅用于互斥访问(任何时候都只有一个线程可以读写),也可用于线程之间的通信。

记住,当有多个线程共享可变数据(mutable data)时,确保每个线程执行读或者写操作时都保持了同步。