Android (Java) 编码惯例及最佳实践

阿里云产品限时红包,最高 ¥1888 元,立即领取

1. 声明(Declaration)

1.1 每行声明变量的数量(Number Per Line)

推荐一行一个声明,因为这样以利于写注释。亦即,

1
2
int level; // indentation level      
int size; // size of table

要优于,

1
int level, size;

不要将不同类型变量的声明放在同一行,例如:

1
int foo, fooarry[]; // WRONG!

注意:上面的例子中,在类型和标识之间放了一个空格,另一种被允许的替代方法是多行变量注释的对齐:

1
2
3
int level;                // indentation level
int size; // size of table
Object currentEntry; // currently selected table entry

1.2 初始化(Initialization)

尽量在声明局部变量的同时进行初始化。唯一 不这么做理由是变量的初始值依赖于某些先前发生的计算。

1.3 布局(Placement)

建议只在代码块的开始处声明变量(一个块可以指任何被包含在大括号“{”和“}”中间的代码,也可以是逻辑上分块的代码)。通常不要在首次用于该变量时才声明之,这会把注意力不集中的程序员搞糊涂,同时会妨碍代码在该作用域内的可移植性。

1
2
3
4
5
6
7
8
void myMethod() {
int int1 = 0;

if (condition) {
int int2 = 0;

}
}

该规则的一个例外是for循环的索引变量

1
for (int i = 0; i < maxLoops; i++) { … }

避免声明的局部变量覆盖上一级声明的变量。例如,不要在内部代码块中声明相同的变量名:

1
2
3
4
5
6
7
8
9
int count;

myMethod() {
if (condition) {
int count = 0; // AVOID!

}

}

1.4 类和接口的声明(Class and Interface Declarations)

当编写类和接口时,应该遵守以下格式规则:

  1. 在方法名与其参数列表之前的左括号“(”间不要有空格。
  2. 左大括号“{”位于声明语句同行的末尾。
  3. 右大括号“}”另起一行,与相应的声明语句对齐,除非是一个空语句,“}”应紧跟在“{”之后。
1
2
3
4
5
6
7
8
9
10
11
12
13
class Sample extends Object {
int ivar1;
int ivar2;

Sample(int i, int j) {
ivar1 = i;
ivar2 = j;
}

int emptyMethod() {}


}

2. 注释(Comments)

Java 程序有两类注释:实现注释( implementation comments )和文档注释( document comments )。实现注释是那些在 C++ 中见过的,使用 /*…*/// 界定的注释。文档注释(被称为“doc comments”)是 Java 独有的,并由 /**…*/ 界定。文档注释可以通过 javadoc 工具转换成 HTML 文件。

实现注释用以注释代码或或者实现细节。文档注释从实现自由( implemtentation-free )的角度描述代码的规范。它可以被那些手头没有源码的开发人员读懂。

注释应被用来给出代码的总括,并提供代码自身没有提供的附加信息。注释应该仅包含与阅读和理解程序有关的信息。例如,相应的包如何被建立或位于哪个目录下之类的信息不应包括在注释中。

在注释里,对设计决策中重要的或者不是显而易见的地方进行说明是可以的,但应避免提供代码中已清晰表达出来的重复信息,多余的注释很容易过时。通常应避免那些代码更新就可能过时的注释。

注意:频繁的注释有时反映出代码的低质量。当你觉得被迫要加注释的时候,考虑一下重写代码使其更清晰。

注释不应写在用星号或字符画出来的大框里。注释不应包括诸如制表符和回退符之类的特殊字符。

2.1 实现注释的格式(Implementation Comment Formats)

程序可以有4种实现注释的风格:块(Block),单行(single-line),尾端(trailing)和行末(end-of-line)。

2.1.1 块注释

块注释通常用于提供对文件,方法,数据结构和算法的描述。块注释被置于每个文件的开始处以及每个方法之前。它们也可以被用于其他地方,比如方法的内部。在功能和方法内部的块注释应该和它们所描述的代码具有一样的缩进格式。

块注释之首应该有一个空行,用于把块注释和代码分割开来,比如:

1
2
3
/*
* Here is a block comment.
*/

2.1.2 单行注释(Single-Line Comments)

短注释可以显示一行内,并与其后的代码具有一样的缩进层级。如果一个注释不能在一行内写完,就该块注释(参见“块注释”)。单行注释之前应该有一个空行。以下是一个Java代码中单行注释的例子:

1
2
3
4
if (condition) {
/* Handle the condition. */
……
}

2.1.3 尾端注释(Trailing Comments)

极短的注释可以与它们所要描述的代码位于同一行,但是应该有足够的空白(至少一个空格)来分开代码和注释。若有多个短注释出现于大段代码中,它们应该具有相同的缩进。

以下是一个Java代码中尾端注释的例子:

1
2
3
4
5
if (a ==2) {
return TRUE; /* special case */
} else {
return isPrime(a); /* works only for odd a */
}

2.1.4 行末注释(End-Of-Line Comments)

注释界定符//,可以注释掉整行或者一行中的一部分。它一般不用于连续多行的注释文本;然而,它可以用来注释掉多行的代码段。以下是所有三种风格的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(foo > 1) {
// Do a double-filp.
……
} else {
return false; // Expalin why here.
}

//if (bar > 1) {
//
// // Do a triple-flip.
// ...
//}
//else {
// return false;
//}

2.2 文档注释(Documentation Comments)

注意:此处描述的注释格式之范例,参见“ Java 源文件范例”
若想了解更多,参见“How to Write Doc Comments for Javadoc”,其中包含了有关文档注释标记的信息(@return,@param,@see):

http://java.sun.com/javadoc/writingdoccomments/index.html

若想了解有关文档注释和 javadoc 的详细资料,参见 javadoc 的主页:

http://java.sun.com/javadoc/index.html

文档注释描述Java的类、接口、构造器、方法,以及字段(field)。每个文档注释都会被置于注释界定符 /**…*/ 之中,一个注释对应一个类、接口或成员。该注释应位于声明之前:

1
2
3
4
5
6
/**
* The Example class provides …
*
* @author FirstName Lastname (account@sohu-inc.com)
*/
public class Example { …

注意:顶层(top-level)的类和接口是不缩进的,而其成员是缩进的。描述类和接口的文档注释的第一行会被置于注释的第一行(/ **)不需要缩进;随后的文档注释每行都缩进1格(使星号纵向对齐)。成员,包括构造函数在内,其文档注释的第一行缩进4格,随后每行都缩进5格。

若你想给出有关类、接口、变量或方法的信息,而这些信息又不适合写在文档中,则可使用实现块注释(见2.1.1)或紧跟在声明后面的单行注释(见2.1.2)。例如,有关一个类实现的细节应放入紧跟在类声明后面的实现块注释中,而不是放在文档注释中。

文档注释不能放在一个方法或构造器的定义块中,因为 Java 会将位于文档注释之后的第一个声明与其相关联。

3. 编程实践

3.1 提供对实例以及类变量的访问控制(Providing Access to Instance and Class Variables)

若没有足够的理由,不要把实例或类类变量声明为 public。通常,实例变量无需显式的设置(set)和获取(gotten),通常这作为方法调用的边缘效应(side effect)而产生。

一个具有public实例变量的恰当例子,是类仅作为数据结构,没有行为。亦即,若你要使用一个结构(struct)而非一个类(如果Java支持结构的话),那么把类的实例变量声明为public是合适的。

Android 变量访问控制:Android 的开发通常允许公共的实例变量。

1
2
3
4
// Common practice in Android development.
public class MyClass {
public int pulicField; // public field for direct access.
}

3.2 引用类变量和类方法(Referring to Class Variables and Methods)

避免用一个对象访问一个类的静态变量和方法。应该用类名替代。例如:

1
2
3
classMethod();         // OK
AClass.classMethod(); // OK
anObject.classMethod(); // AVOID!

3.3 常量(Constants)

位于for循环中作为计数器值的数字常量,除了-1,0和1之外,不应被直接写入代码。

3.4 变量赋值(Variable Assignments)

避免在一个语句中给多个变量赋相同的值。它很难读懂。例如:

1
fooBar.fChar = barFoo.lchar = ‘c’;  // AVOID!

不要将赋值运算符用在容易与相等关系运算符混淆的地方。例如:

1
2
3
if (c++ = d++) {    // AVOID! (Java disallows)
….
}

应该写成

1
2
3
if ((c++ = d++) ! = 0) {

}

不要使用内嵌(embedded)赋值运算符试图提高运行时效率,这是编译器的工作。例如:

1
d = (a = b +c) + r; // AOVID!

应该写成

1
2
a = b + c;
d = a + r;

3.5 其它惯例(Miscellaneous Practices)

3.5.1 圆括号与运算符优先级(Parentheses and Operator Precedence)

一般而言,在含有多种运算符的表达式中使用括号来避免运算符优先级问题,是个好方法。即便运算符的优先级对你而言可能很清楚,但对其他人未必如此。你不能假设别的程序员和你一样清楚运算符的优先级。

1
2
if (a == b && c ==d)     // AVOID!
if ((a == b) && (c == d)) // RIGHT

3.5.2 返回值(Returning Values)

设法让你的程序结构符合目的。例如:

1
2
3
4
5
if (booleanException) {
return true;
} else {
return false;
}

应该代之以如下方法:

1
return booleanException;

类似地:

1
2
3
4
if (condition) {
return x;
}
return y;

应该写为:

1
return (condition ? x : y);

3.5.3 条件运算符“?”前的表达式 (Expressions before “?” in the Conditional Operator)

如果一个包含二元运算符表达式出现在三元运算符“? :”之前,那么应该给表达式添上一对圆括号。例如:

1
(x >= 0) ? x : -x;

3.5.4 特殊注释(Special Comments)

使用 TODO 来注释一个临时的或者未完成的解决方案。TODO 必须包含详细的信息,例如,需要做什么,打算怎么做,为什么以后才做,等。要发布到生产环境的代码尽量减少 TODO。

1
// TODO: Change this to use a flag instead of a constant.

如果有可能,尽量包含具体的开发者帐号,以及具体的修正日期。

1
// TODO(tom, Fix by Nov 2005): Change this to use a flag.

3.6 所有对象的共有方法(Methods Common to All Objects)

3.6.1 覆盖equals时请遵守通用约定

Item 8: Obey the general contract when overriding equals.

重写 equals 方法看起来很简单,实际上非常容易犯错误。在多数情况下我们不推荐重写equals方法。如果确实有必要,这里再强调一下 Object.equals 的约定:

  • 自反:对于任意 non-null 的引用值x,x.equals(x) 必须返回true。
  • 对称:对于任意的 non-null 的值x和y,x.equals(y) 和 y.equals(x) 必须返回相同的值。
  • 传递:对于任意 non-null 的值,x,y,z,如果 x.equals(y) 返回true以及 y.equals(z) 返回true,那么 x.equals(z) 也必须返回 true。
  • 一致:如果 equals 实现中的辅助信息没有改变,对于任意的x和y,对 x.equals(y) 必须返回相同的值。

对于所有的 non-null 的 x,x.equals(null) 必须返回false。

3.6.2 覆盖equals时总要覆盖hashCode

Item 9: Always override hashCode when you override equals.

对于每一个 equals 方法被重写的类,你必须重写 hashCode 方法。请仔细阅读 javadoc 中关于 Object.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
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;

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

// Broken - no hashCode method!
}

Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
// null is returned instead of "Jenny"
m.get(new PhoneNumber(707, 867, 5309));

3.7 泛型(Generics)

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

Item 23: Don’t use raw types in new code.

从1.5版本开始,java提供了泛型机制来保证类型的安全。除非为了向后兼容,不允许使用原生态类型,像List(而不是 List<String>)。

下面这段代码就很容易出问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Now a raw collection type - don't do this!
/**
* My stamp collection. Contains only Stamp instances.
*/
private final Collection stamps = ... ;

// Erroneous insertion of coin into stamp collection
stamps.add(new Coin( ... ));

// Now a raw iterator type - don't do this!
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp s = (Stamp) i.next(); // Throws ClassCastException
... // Do something with the stamp
}

使用泛型能够很好地解决这个问题:

1
2
3
4
5
6
7
// Parameterized collection type - typesafe
private final Collection<Stamp> stamps = ... ;

// for-each loop over a parameterized collection - typesafe
for (Stamp s : stamps) { // No cast
... // Do something with the stamp
}

3.7.2 消除非受检警告

Item 24: Elimate unchecked warnings.

在使用泛型编程的时候,我们经常会看到编译警告:非受检的类型转换警告,非受检的函数调用警告等等。很多非受检的警告是非常容易消除的,例如:

1
Set<Lark> exaltation = new HashSet();

编译器会抛出警告:

1
2
3
4
Venery.java:4: warning: [unchecked] unchecked conversion
found : HashSet, required: Set<Lark>
Set<Lark> exaltation = new HashSet();
^

我们很容易消除这个警告:

1
Set<Lark> exaltation = new HashSet<Lark>();

关于消除非受检的警告,有如下基本规则:

  • 尽可能地消除每个非受检的警告。
  • 如果无法消除,必须能够证明这个警告不会引起类型安全问题,同时通过 @SuppressWarnings("unchecked") 来消除警告。
  • SuppressWarning必须作用在尽可能小的范围。
  • 每一次使用 @SuppressWarnings("unchecked") 时,必须注释说明为什么这样做是安全的。

3.8 枚举和注解(Enums and Annotations)

3.8.1 用 enum 代替 int 常量

Item 30: Use enums instead of int constants.

下面的代码利用了一种叫 int enum pattern 的技术,它有很多问题。它不能保证类型安全;它不能很好地转换为可读的字符串;如果数值变了的话,使用它的代码必须重新编译(严重!)。

1
2
3
4
// The int enum pattern - severely deficient!
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

我们应该用enum来实现上面的代码:

1
public enum Orange { NAVEL, TEMPLE, BLOOD }

注意:出于效率考虑,Android 的开发允许使用 int 常量,但是使用时要非常小心。

8.8.2 用实例域代替序数

Item 31: Use instance fields instead of ordinals.

所有的枚举类型都有一个ordinal方法,它能够返回每个枚举常量在类型中的位置。有时候,你可能会想从这个方法中直接得到一个整数值:

1
2
3
4
5
6
// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {
SOLO, SEXTET, DUET, TRIO, QUARTET, QUINTET, SEPTET, OCTET, NONET, DECTET;

public int numberOfMusicians() { return ordinal() + 1; }
}

不要从枚举的序数中得到一个整数值,如果需要,请创建一个整数字段:

1
2
3
4
5
6
7
8
9
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);

private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}

3.8.3 坚持使用Override注解

Item 36: Consistently use the Override annotation.

你能发现下面代码的错误吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
public int hashCode() {
return 31 * first + second;
}
}

如果我们加入了 @Override 的标注,编译器就能告诉我们错误了。

1
2
3
4
5
6
7
8
@Override public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}

Bigram.java:10: method does not override or implement a method
from a supertype
@Override public boolean equals(Bigram b) {
^

3.9 方法 (Methods)

3.9.1 检查参数的有效性

Item 38: Check parameters for validaity.

大多数的方法对于传入参数的值有限制,例如,通常来说索引值必须是非负的,对象应用必须是非空的。一条总的规则是,在错误发生之前必须尽早地发现它、处理它。

如果我们能在方法执行主要逻辑之前检查到错误的参数,我们就能够及时退出,同时抛出一个合适的一场。如果错误的参数进入函数的主要执行逻辑,方法就可能抛出一个奇怪的异常。在更坏的情况下,方法可能成功返回,但是中间出现一些不可预知的结果。

对于公共方法,用 @throws 来注释异常。比如,这些异常可能是 IllegalArgumentException 或者是 NullPointerException。例如:

1
2
3
4
5
6
7
8
9
10
11
/**
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0) {
throw new ArithmeticException("Modulus <= 0: " + m);
}
... // Do the computation
}

3.10 异常(Exceptions)

3.10.1 只针对异常的情况才使用异常

Item 57: Use exceptions only for exceptional conditions.

异常,只应该被应用于异常的情况;不能被用于正常的控制流程。这一点在很多时候会被误用,下面是一个极端的例子:

1
2
3
4
5
6
7
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while(true)
range[i++].climb();
} catch(ArrayIndexOutOfBoundsException e) {
}

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

Item 58: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors.

受检异常在调用者能够合适处理并恢复程序执行的情况下使用。运行时异常用来表明程序自身的错误,大多数的运行时异常都是因为调用者违反了函数的预设条件。

应用程序所有非受检的异常应该派生自 RuntimeException ,作为惯例,Error 通常只被 JVM 使用。

3.10.3 抛出与抽象相对应的异常

Item 61: Throw exceptions appropriate to the abstraction.

抛出与当前方法明显无关的异常会让人十分迷惑,而且会暴露出实现的细节。这种情况通常是因为高一级的方法直接抛出底层调用的异常引起的。我们采用异常转换的方法来避免这一种情况。也就是说,高一级的方法应该捕获底层调用的异常,抛出一个更高抽象的异常。例如,下面是一段 AbstractSequentialList 实现代码,它需要实现 List<E>#get 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Returns the element at the specified position in this list.
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()}).
*/
public E get(int index) {
ListIterator<E> i = listIterator(index);
try {
return i.next();
} catch(NoSuchElementException e) {
throw new IndexOutOfBoundsException("Index: " + index);
}
}

需要注意的是,不要滥用了异常转换。

3.10.4 每个方法抛出的异常都要有文档

Item 62: Document all exceptions thrown by each method.

关于异常的文档有以下要求:

必须单独地声明每个受检异常,同时准确地用 @throws 来说明异常条件。

用 Javadoc 的 @throws 来表明非受检的异常;但是不要用throws关键字在方法的声明中包括非受检的异常。

如果一个异常会被同一个类的很多方法抛出,在类的注释中表明这个异常是可以接受的。但是通常不推荐这么做。

3.10.5 不要忽略异常

Item 65: Don’t ignore exceptions.

我们不允许像下面的代码一样忽略异常:

1
2
3
4
5
6
void setServerPort(String value) {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) {
}
}

至少,在 catch 语句里,应该有注释说明为什么忽略异常是合适的。

1
2
3
4
5
6
7
8
9
/** If value is not a valid number, original port number is used. */
void setServerPort(String value) {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) {
// Method is documented to just ignore invalid user input.
// serverPort will just be unchanged.
}
}

3.10.6 不要一次捕获所有异常

1
2
3
4
5
6
7
8
try {
someComplicatedIOFunction(); // may throw IOException
someComplicatedParsingFunction(); // may throw ParsingException
someComplicatedSecurityFunction(); // may throw SecurityException
// phew, made it all the way
} catch (Exception e) { // I'll just catch all exceptions
handleError(); // with one generic handler!
}

在绝大多数情况下,捕获 Exception 或者是 Throwable 都是不对的。这个非常危险,你可能会捕获一些没有想到的异常,或者是稍后加入的异常。

参考

Code Style Guidelines for Contributors

Google Java 编程风格指南