引言
本指南的意图是提供一套鼓励写出好代码的约定(conventions),它是许多年软件工程与 Java 开发 combined 经验的结晶。虽然有的建议比其它的更严格,但你始终应该运用良好的判断力(good judgement)。
如果遵循指南会导致不必要的” hoops 跳跃”或者让代码更难读,那么可读性(readability)优先于指南;但如果”更可读”的写法伴随着风险或陷阱,则可以牺牲可读性。
总体而言,本指南的大部分风格和约定与 Code Conventions for the Java Programming Language 以及 Google 的 Java Style Guide 一致(可对照本系列前一篇 Google Java 编程风格指南)。
推荐阅读:
- Effective Java
- Java Concurrency in Practice
- Code Complete 2(不是 Java 专属,但是一本很好的编程最佳实践手册)
编码风格(Coding style)
格式化(Formatting)
明智地使用换行(Use line breaks wisely)
插入换行通常有两个原因:
- 语句超出了列限制(column limit)
- 你想在逻辑上分隔一个”想法”
写代码就像讲故事。书面语言中的章节、段落和标点(分号、逗号、句号、连字符)传达了思想的层级与分隔;编程语言中也有类似的结构,你应该利用它们向代码的阅读者有效地讲故事。
不要不必要地拆分语句:
// Like this.
if (x < 0) {
negative(x);
} else {
nonnegative(x);
}
// Not like this.
if (x < 0)
negative(x);
// Also not like this.
if (x < 0) negative(x);
// Bad.
// - Line breaks are arbitrary.
// - Scanning the code makes it difficult to piece the message together.
throw new IllegalStateException("Failed to process request" + request.getId()
+ " for user " + user.getId() + " query: '" + query.getText()
+ "'");
// Good.
// - Each component of the message is separate and self-contained.
// - Adding or removing a component of the message requires minimal reformatting.
throw new IllegalStateException("Failed to process"
+ " request " + request.getId()
+ " for user " + user.getId()
+ " query: '" + query.getText() + "'");
// Bad.
final String value =
otherValue;
// Good.
final String value = otherValue;
方法声明的续行(Method declaration continuations):
// Sub-optimal since line breaks are arbitrary and only filling lines.
String downloadAnInternet(Internet internet, Tubes tubes,
Blogosphere blogs, Amount<Long, Data> bandwidth) {
tubes.download(internet);
...
}
// Acceptable.
String downloadAnInternet(Internet internet, Tubes tubes, Blogosphere blogs,
Amount<Long, Data> bandwidth) {
tubes.download(internet);
...
}
// Nicer, as the extra newline gives visual separation to the method body.
String downloadAnInternet(Internet internet, Tubes tubes, Blogosphere blogs,
Amount<Long, Data> bandwidth) {
tubes.download(internet);
...
}
// Also acceptable, but may be awkward depending on the column depth of the opening paren.
public String downloadAnInternet(Internet internet,
Tubes tubes,
Blogosphere blogs,
Amount<Long, Data> bandwidth) {
tubes.download(internet);
...
}
// Preferred for easy scanning and extra column space.
public String downloadAnInternet(
Internet internet,
Tubes tubes,
Blogosphere blogs,
Amount<Long, Data> bandwidth) {
tubes.download(internet);
...
}
链式方法调用(Chained method calls):
// Bad.
// - Line breaks are based on line length, not logic.
Iterable<Module> modules = ImmutableList.<Module>builder().add(new LifecycleModule())
.add(new AppLauncherModule()).addAll(application.getModules()).build();
// Better.
// - Calls are logically separated.
// - However, the trailing period logically splits a statement across two lines.
Iterable<Module> modules = ImmutableList.<Module>builder().
add(new LifecycleModule()).
add(new AppLauncherModule()).
addAll(application.getModules()).
build();
// Good.
// - Method calls are isolated to a line.
// - The proper location for a new method call is unambiguous.
Iterable<Module> modules = ImmutableList.<Module>builder()
.add(new LifecycleModule())
.add(new AppLauncherModule())
.addAll(application.getModules())
.build();
不使用 tabs(No tabs)
老生常谈但很有效:我们发现 tab 字符弊大于利。
100 列限制(100 column limit)
你应该遵循所工作的代码库已有的约定。我们倾向于使用 100 列,在”更少的续行”和”在高分辨率显示器上并排打开两个编辑器 tab 仍然放得下”之间取得平衡。
类型用 CamelCase,变量用 camelCase,常量用 UPPER_SNAKE_CASE
无行尾空白(No trailing whitespace)
行尾空白字符虽然在逻辑上无害,但对程序没有任何贡献,却会让使用键盘快捷键导航代码的开发者感到沮丧。
字段、类和方法声明
修饰符顺序(Modifier order)
我们遵循 Java 语言规范中的修饰符排序(8.1.1、8.3.1 和 8.4.3 节):
// Bad.
final volatile private String value;
// Good.
private final volatile String value;
变量命名(Variable naming)
极短的变量名应该只留给循环下标(loop indices)这类场景:
// Bad.
// - Field names give little insight into what fields are used for.
class User {
private final int a;
private final String m;
...
}
// Good.
class User {
private final int ageInYears;
private final String maidenName;
...
}
在变量名中包含单位(Include units in variable names):
// Bad.
long pollInterval;
int fileSize;
// Good.
long pollIntervalMs;
int fileSizeGb;
// Better.
// - Unit is built in to the type.
// - The field is easily adaptable between units, readability is high.
Amount<Long, Time> pollInterval;
Amount<Integer, Data> fileSize;
不要在变量名中嵌入元数据(Don’t embed metadata in variable names)
变量名应该描述变量的用途。附加诸如作用域(scope)和类型(type)之类的额外信息通常是一个坏变量名的标志。避免把字段类型嵌入字段名,也避免把作用域信息嵌入变量。基于层级结构的命名(hierarchy-based naming)暗示这个类太复杂、应该被拆分:
// Bad.
Map<Integer, User> idToUserMap;
String valueString;
// Good.
Map<Integer, User> usersById;
String value;
// Bad.
String _value;
String mValue;
// Good.
String value;
运算符和等号两侧加空格(Space pad operators and equals):
// Bad.
// - This offers poor visual separation of operations.
int foo=a+b+1;
// Good.
int foo = a + b + 1;
明确表达运算符优先级(Be explicit about operator precedence)
如果你期望特定的运算顺序,不要让你的读者去翻规范,用括号让它显而易见。即使”非常明显”也是好事:
// Bad.
return a << 8 * n + 1 | 0xFF;
// Good.
return (a << (8 * n) + 1) | 0xFF;
if ((values != null) && (10 > values.size())) {
...
}
文档(Documentation)
代码的可见性越高(相应地,消费者可能离得越远),就越需要文档。
不要写”I’m writing a report about…”式的文档
小学老师是对的——你不应该以这种方式开始一个陈述,同样也不应该这样写文档:
// Bad.
/**
* This is a class that implements a cache. It does caching for you.
*/
class Cache {
...
}
// Good.
/**
* A volatile storage for objects based on a key, which may be invalidated and discarded.
*/
class Cache {
...
}
为类写文档(Documenting a class)
类的文档可以从一个句子到带代码示例的多个段落。文档应当消除 API 中概念上的空白,让人更容易快速而正确地使用你的 API。一个完整的类文档通常有一句话概述,必要时再给出更详细的解释:
/**
* An RPC equivalent of a unix pipe tee. Any RPC sent to the tee input is guaranteed
* to have been sent to both tee outputs before the call returns.
*
* @param <T> The type of the tee'd service.
*/
public class RpcTee<T> {
...
}
为方法写文档(Documenting a method)
方法文档应该说明方法做什么。根据参数类型,记录输入格式也可能很重要。示例从 Bad 到 Great 逐级改进——Bad 是”填充式文档”,虽然能通过 style check 但对任何人都没有帮助:
// Bad.
// - The doc tells nothing that the method declaration didn't.
// - This is the 'filler doc'. It would pass style checks, but doesn't help anybody.
/**
* Splits a string.
*
* @param s A string.
* @return A list of strings.
*/
List<String> split(String s);
// Better.
// - We know what the method splits on.
// - Still some undefined behavior.
/**
* Splits a string on whitespace.
*
* @param s The string to split. An {@code null} string is treated as an empty string.
* @return A list of the whitespace-delimited parts of the input.
*/
List<String> split(String s);
// Great.
// - Covers yet another edge case.
/**
* Splits a string on whitespace. Repeated whitespace characters are collapsed.
*
* @param s The string to split. An {@code null} string is treated as an empty string.
* @return A list of the whitespace-delimited parts of the input.
*/
List<String> split(String s);
保持专业(Be professional)
我们都曾在使用其它库时遇到挫折,但抱怨对你没有任何好处。收起脏话,直击要点:
// Bad.
// I hate xml/soap so much, why can't it do this for me!?
try {
userId = Integer.parseInt(xml.getField("id"));
} catch (NumberFormatException e) {
...
}
// Good.
// TODO(Jim): Tuck field validation away in a library.
try {
userId = Integer.parseInt(xml.getField("id"));
} catch (NumberFormatException e) {
...
}
重写方法通常不写文档(Don’t document overriding methods (usually))
重写方法的文档若没有增加任何信息就不必写;但如果它解释了与接口文档的差异或补充,则值得写:
interface Database {
/**
* Gets the installed version of the database.
*
* @return The database version identifier.
*/
String getVersion();
}
// Bad.
// - Overriding method doc doesn't add anything.
class PostgresDatabase implements Database {
/**
* Gets the installed version of the database.
*
* @return The database version identifier.
*/
@Override
public String getVersion() {
...
}
}
// Good.
class PostgresDatabase implements Database {
@Override
public int getVersion();
}
// Great.
// - The doc explains how it differs from or adds to the interface doc.
class TwitterDatabase implements Database {
/**
* Semantic version number.
*
* @return The database version in semver format.
*/
@Override
public String getVersion() {
...
}
}
使用 javadoc 特性(Use javadoc features)
不使用 author 标签(No author tags)
代码在其生命周期中可能多次易手,源文件最初的作者在若干次迭代后往往已经无关紧要。我们认为更应该信任 commit 历史和 OWNERS 文件来确定一段代码的所有权。
导入(Imports)
Import 排序(Import ordering)
Import 按顶层包(top-level package)分组,组之间用空行分隔。Static import 以同样的方式分组,放在传统 import 之下的一个区段:
import java.*
import javax.*
import scala.*
import com.*
import net.*
import org.*
import com.twitter.*
import static *
不使用通配符导入(No wildcard imports)
通配符导入让一个被导入类的来源不那么清晰,也倾向于隐藏较高的 class fan-out。参见下文的 texas imports:
// Bad.
// - Where did Foo come from?
import com.twitter.baz.foo.*;
import com.twitter.*;
interface Bar extends Foo {
...
}
// Good.
import com.twitter.baz.foo.BazFoo;
import com.twitter.Foo;
interface Bar extends Foo {
...
}
明智地使用注解(Use annotations wisely)
@Nullable
默认——禁止 null。当一个变量、参数或方法返回值可能为 null 时,用 @Nullable 显式标明。即使对 private 可见性的字段/方法也建议这样做:
class Database {
@Nullable private Connection connection;
@Nullable
Connection getConnection() {
return connection;
}
void setConnection(@Nullable Connection connection) {
this.connection = connection;
}
}
@VisibleForTesting
有时把成员和函数隐藏起来是合理的,但良好的测试覆盖率又需要它们。通常更可取的做法是将其设为 package-private 并加上 @VisibleForTesting 来表明可见性的用途。常量就是经常以这种方式暴露的好例子:
// Bad.
// - Any adjustments to field names need to be duplicated in the test.
class ConfigReader {
private static final String USER_FIELD = "user";
Config parseConfig(String configData) {
...
}
}
public class ConfigReaderTest {
@Test
public void testParseConfig() {
...
assertEquals(expectedConfig, reader.parseConfig("{user: bob}"));
}
}
// Good.
// - The test borrows directly from the same constant.
class ConfigReader {
@VisibleForTesting static final String USER_FIELD = "user";
Config parseConfig(String configData) {
...
}
}
public class ConfigReaderTest {
@Test
public void testParseConfig() {
...
assertEquals(expectedConfig,
reader.parseConfig(String.format("{%s: bob}", ConfigReader.USER_FIELD)));
}
}
使用接口(Use interfaces)
接口将功能与实现解耦,让你无需修改消费者就能使用多个实现。接口是隔离包的好方法——提供一组接口,而把实现保持为 package private。
大量小接口可能显得很重,因为会产生大量的源文件。可以考虑下面的替代模式——在一个接口内放静态实现类,在只预期有一个实现时尤其有用:
interface FileFetcher {
File getFile(String name);
// All the benefits of an interface, with little source management overhead.
// This is particularly useful when you only expect one implementation of an interface.
static class HdfsFileFetcher implements FileFetcher {
@Override File getFile(String name) {
...
}
}
}
利用或扩展现有接口(Leverage or extend existing interfaces)
有时一个现有接口能让你的类轻松地”插入”其它相关类,带来高内聚的代码。例如让 Blobs 实现 Iterable<byte[]>,调用者就可以轻松地适配到标准集合,或做过滤之类的复杂事情,而不是为它写特定的胶水代码:
// An unfortunate lack of consideration. Anyone who wants to interact with Blobs will
// write specific glue code.
class Blobs {
byte[] nextBlob() {
...
}
}
// Much better. Now the caller can easily adapt this to standard collections, or do more
// complex things like filtering.
class Blobs implements Iterable<byte[]> {
@Override
Iterator<byte[]> iterator() {
...
}
}
警告——不要为了套用而曲解现有接口的定义。如果该接口在概念上并不能干净地适用,最好避免这么做。
编写可测试代码(Writing testable code)
编写单元测试不必很难,只要在设计类和接口时把可测试性(testability)放在心上,就能让这件事变容易。
Fakes 与 mocks(Fakes and mocks)
测试一个类时,你经常需要提供某种”罐装”功能来替代真实世界的行为。例如,不是从真实数据库取一行数据,而是返回一条测试用的行。这通常通过 fake object 或 mock object 完成。虽然差别听起来微妙,但 mocks 相比 fakes 有显著优势——下面 Bad 的例子中,测试几乎无法控制方法调用的顺序和频率,要小心 HttpTransport 的改动不会破坏 FakeHttpTransport,而且可能需要大量代码;Good 的例子通过 mock 接口获得非常精细的期望控制:
class RpcClient {
RpcClient(HttpTransport transport) {
...
}
}
// Bad.
// - Our test has little control over method call order and frequency.
// - We need to be careful that changes to HttpTransport don't disable FakeHttpTransport.
// - May require a significant amount of code.
class FakeHttpTransport extends HttpTransport {
@Override
void writeBytes(byte[] bytes) {
...
}
@Override
byte[] readBytes() {
...
}
}
public class RpcClientTest {
private RpcClient client;
private FakeHttpTransport transport;
@Before
public void setUp() {
transport = new FakeHttpTransport();
client = new RpcClient(transport);
}
...
}
interface Transport {
void writeBytes(byte[] bytes);
byte[] readBytes();
}
class RpcClient {
RpcClient(Transport transport) {
...
}
}
// Good.
// - We can mock the interface and have very fine control over how it is expected to be used.
public class RpcClientTest {
private RpcClient client;
private Transport transport;
@Before
public void setUp() {
transport = EasyMock.createMock(Transport.class);
client = new RpcClient(transport);
}
...
}
让调用者构造支撑对象(Let your callers construct support objects)
与其在构造方法里自己去 new 依赖(例如从文件名打开 FileInputStream,导致单测必须在磁盘上管理临时文件),不如接受已经构造好的对象(例如 InputStream),测试时用 ByteArrayInputStream 包装一个 String 即可:
// Bad.
// - A unit test needs to manage a temporary file on disk to test this class.
class ConfigReader {
private final InputStream configStream;
ConfigReader(String fileName) throws IOException {
this.configStream = new FileInputStream(fileName);
}
}
// Good.
// - Testing this class is as easy as using ByteArrayInputStream with a String.
class ConfigReader {
private final InputStream configStream;
ConfigReader(InputStream configStream) {
this.configStream = checkNotNull(configStream);
}
}
测试多线程代码(Testing multithreaded code)
测试使用多线程的代码出了名地难。但只要谨慎对待,不需要死锁或不必要的 time-wait 语句也可以完成:
- 如果被测代码需要执行周期性后台任务(例如使用 ScheduledExecutorService),考虑 mock 该服务,或者从测试中手动触发任务,避免真实的调度
- 如果被测代码向 ExecutorService 提交任务,可以考虑允许注入 executor,并在测试中提供一个单线程 executor
- 在多线程不可避免的情况下,
java.util.concurrent提供了一些有用的库来帮助管理同步执行。例如,异步操作执行时,LinkedBlockingDeque 可以在 producer 与 consumer 之间提供同步;当队列不适用时,CountDownLatch 对状态/操作同步很有用
测试反模式(Testing antipatterns)
时间依赖(Time-dependence)
捕获真实墙上时间的代码很难可重复地测试,尤其当时间差有意义时。因此尽量避免 new Date()、System.currentTimeMillis() 和 System.nanoTime()。合适的替代是 Clock:正常运行时使用 Clock.SYSTEM_CLOCK,测试中使用 FakeClock。
隐藏的压力测试(The hidden stress test)
避免编写试图验证某种性能指标的单元测试。这类测试应该被单独处理,并在比单元测试更受控的环境中运行。
Thread.sleep()
Sleep 很少是正当的,尤其在测试代码中。Sleep 表达了”当执行线程挂起时,别处正在发生某些事”的期望,这很快导致脆弱(brittleness),例如后台线程在你 sleep 期间没有被调度。测试中 sleep 还有一个坏处:它给测试的执行速度设定了坚硬的下限——无论机器多快,一个 sleep 一秒的测试永远不可能在一秒内执行完,长期下来会导致非常长的测试执行周期。
避免测试中的随机性(Avoid randomness in tests)
在测试中使用随机值看似是个好主意,可以用更少的代码覆盖更多测试用例;问题是你失去了对”正在覆盖哪些用例”的控制,一旦遇到测试失败,可能很难重现。带固定种子的伪随机输入略好,但实践中很少真正提高测试覆盖率。一般来说,更好地做法是使用能命中已知边界情况(edge cases)的固定输入数据。
最佳实践(Best practices)
防御式编程(Defensive programming)
避免 assert(Avoid assert)
我们避免 assert 语句,因为它可以在执行时被禁用;我们更倾向于在任何时候都强制这类不变量(invariants)。参见下文的 preconditions。
前置条件(Preconditions)
Preconditions 检查是一个好实践,它为来自调用者的坏输入提供了一道定义良好的屏障。按约定,public 构造方法和方法的对象参数应该总是检查 null,除非显式允许 null:
// Bad.
// - If the file or callback are null, the problem isn't noticed until much later.
class AsyncFileReader {
void readLater(File file, Closure<String> callback) {
scheduledExecutor.schedule(new Runnable() {
@Override public void run() {
callback.execute(readSync(file));
}
}, 1L, TimeUnit.HOURS);
}
}
// Good.
class AsyncFileReader {
void readLater(File file, Closure<String> callback) {
checkNotNull(file);
checkArgument(file.exists() && file.canRead(), "File must exist and be readable.");
checkNotNull(callback);
scheduledExecutor.schedule(new Runnable() {
@Override public void run() {
callback.execute(readSync(file));
}
}, 1L, TimeUnit.HOURS);
}
}
最小化可见性(Minimize visibility)
在类的 API 中,你应当对你开放访问的所有方法和字段负责。因此只暴露你打算让调用者使用的东西,这在编写线程安全代码时尤其重要:
public class Parser {
// Bad.
// - Callers can directly access and mutate, possibly breaking internal assumptions.
public Map<String, String> rawFields;
// Bad.
// - This is probably intended to be an internal utility function.
public String readConfigLine() {
..
}
}
// Good.
// - rawFields and the utility function are hidden.
// - The class is package-private, indicating that it should only be accessed indirectly.
class Parser {
private final Map<String, String> rawFields;
private String readConfigLine() {
..
}
}
倾向不可变性(Favor immutability)
可变对象带着负担——你需要确保那些能够修改它的人不会违反该对象其它使用者的期望,并且他们修改它甚至是安全的:
// Bad.
// - Anyone with a reference to User can modify the user's birthday.
// - Calling getAttributes() gives mutable access to the underlying map.
public class User {
public Date birthday;
private final Map<String, String> attributes = Maps.newHashMap();
...
public Map<String, String> getAttributes() {
return attributes;
}
}
// Good.
public class User {
private final Date birthday;
private final Map<String, String> attributes = Maps.newHashMap();
...
public Map<String, String> getAttributes() {
return ImmutableMap.copyOf(attributes);
}
// If you realize the users don't need the full map, you can avoid the map copy
// by providing access to individual members.
@Nullable
public String getAttribute(String attributeName) {
return attributes.get(attributeName);
}
}
小心 null(Be wary of null)
在合理的地方使用 @Nullable,但更倾向 Optional 而非 @Nullable——Optional 为”值的缺失”提供了更好的语义。
用 finally 清理(Clean up with finally)
即使没有 checked exception,也存在应当使用 try/finally 来保证资源对称(resource symmetry)的情况:
FileInputStream in = null;
try {
...
} catch (IOException e) {
...
} finally {
Closeables.closeQuietly(in);
}
// Bad.
// - Mutex is never unlocked.
mutex.lock();
throw new NullPointerException();
mutex.unlock();
// Good.
mutex.lock();
try {
throw new NullPointerException();
} finally {
mutex.unlock();
}
// Bad.
// - Connection is not closed if sendMessage throws.
if (receivedBadMessage) {
conn.sendMessage("Bad request.");
conn.close();
}
// Good.
if (receivedBadMessage) {
try {
conn.sendMessage("Bad request.");
} finally {
conn.close();
}
}
代码整洁(Clean code)
消除歧义(Disambiguate)
偏好可读性——如果存在歧义和无歧义两种写法,永远选择无歧义的。
删除死代码(Remove dead code)
删除不用的代码(imports、fields、parameters、methods、classes),它们只会腐烂。
使用一般类型(Use general types)
声明字段和方法时,尽可能使用更一般的类型。这可以避免实现细节通过 API 泄漏,也允许你在不影响用户和外围代码的情况下更改内部使用的类型:
// Bad.
// - Implementations of Database must match the ArrayList return type.
// - Changing return type to Set<User> or List<User> could break implementations and users.
interface Database {
ArrayList<User> fetchUsers(String query);
}
// Good.
// - Iterable defines the minimal functionality required of the return.
interface Database {
Iterable<User> fetchUsers(String query);
}
总是使用类型参数(Always use type parameters)
Java 5 引入了 generics,为集合类型添加了类型参数,也允许用户实现自己的类型参数化类。向后兼容和类型擦除(type erasure)使类型参数是可选的,但根据用法它们确实会产生编译器警告。按惯例,我们在每个类型被参数化的声明上都包含类型参数;即使类型未知,也最好包含通配符或宽类型:
// Bad.
// - Depending on the font, it may be difficult to discern 1001 from 100l.
long count = 100l + n;
// Good.
long count = 100L + n;
远离 Texas(Stay out of Texas)
尽量让你的类保持”一口大小”(bite-sized)并具有清晰定义的职责。随着程序演进这会非常难,通常只是一种直觉判断,但这些是类过大或过复杂、应该被拆分的特征:
- texas imports
- texas constructors:这个类能否被干净地拆开?如果不能,考虑 builder pattern
- texas methods
我们可以对每个阈值做科学统计,但那可能没什么用。
避免类型转换(Avoid typecasting)
类型转换是类设计不佳的标志,通常可以避免。一个明显的例外是重写 equals。
使用 final 字段(Use final fields)
Final 字段很有用,因为它们声明了一个字段不可被重新赋值。在检查线程安全时,一个 final 字段就少了一件需要检查的事情。参见 favor immutability。
避免可变静态状态(Avoid mutable static state)
可变静态状态很少是必要的,一旦出现就会带来大量问题。一个很简单的例子是单元测试:由于单元测试通常在单个 VM 中运行,static 状态会在所有测试用例间持续存在。总的来说,可变静态状态是类设计不佳的标志。
异常(Exceptions)
捕获窄异常(Catch narrow exceptions)
有时使用 try/catch 时,会很想直接 catch Exception、Error 或 Throwable,这样就不用操心抛出的是什么类型。这通常是个坏主意,因为你最终捕获的会超出你真正想处理的范围。例如 catch Exception 会捕获 NullPointerException,catch Throwable 会捕获 OutOfMemoryError:
// Bad.
// - If a RuntimeException happens, the program continues rather than aborting.
try {
storage.insertUser(user);
} catch (Exception e) {
LOG.error("Failed to insert user.");
}
try {
storage.insertUser(user);
} catch (StorageException e) {
LOG.error("Failed to insert user.");
}
不要吞异常(Don’t swallow exceptions)
空的 catch 块通常是坏主意,因为你完全没有问题发生的信号。再叠加”捕获窄异常”的违反,就是灾难的配方。
中断时恢复线程中断状态(When interrupted, reset thread interrupted state)
许多阻塞操作抛出 InterruptedException,以便在 JVM shutdown 之类的事件发生时唤醒你。捕获 InterruptedException 时,确保线程的 interrupted 状态被保留是良好实践(IBM 有一篇关于这个主题的好文章):
// Bad.
// - Surrounding code (or higher-level code) has no idea that the thread was interrupted.
try {
lock.tryLock(1L, TimeUnit.SECONDS)
} catch (InterruptedException e) {
LOG.info("Interrupted while doing x");
}
// Good.
// - Interrupted state is preserved.
try {
lock.tryLock(1L, TimeUnit.SECONDS)
} catch (InterruptedException e) {
LOG.info("Interrupted while doing x");
Thread.currentThread().interrupt();
}
抛出合适的异常类型(Throw appropriate exception types)
让你的 API 用户能够”捕获窄异常”,不要抛出 Exception。即使你在调用另一个抛出 Exception 的”淘气”API,至少也要把它隐藏起来,不让它继续向上冒泡。在异常方面,你也应该努力向调用者隐藏实现细节:
// Bad.
// - Caller is forced to catch Exception, trapping many unnecessary types of issues.
interface DataStore {
String fetchValue(String key) throws Exception;
}
// Better.
// - The interface leaks details about one specific implementation.
interface DataStore {
String fetchValue(String key) throws SQLException, UnknownHostException;
}
// Good.
// - A custom exception type insulates the user from the implementation.
// - Different implementations aren't forced to abuse irrelevant exception types.
interface DataStore {
String fetchValue(String key) throws StorageException;
static class StorageException extends Exception {
...
}
}
使用更新更好的库(Use newer/better libraries)
StringBuilder over StringBuffer
StringBuffer 是线程安全的,而很少需要。
ScheduledExecutorService over Timer
取自 Java Concurrency in Practice(直接借自一个 stackoverflow 问题):
- Timer 对系统时钟的变化敏感,ScheduledThreadPoolExecutor 则不会
- Timer 只有一个执行线程,长时间运行的任务会延迟其它任务;ScheduledThreadPoolExecutor 可以配置多个线程和一个 ThreadFactory
- TimerTask 中抛出的异常会杀死该线程,使 Timer 失效;ThreadPoolExecutor 提供 afterExecute 让你显式处理执行结果
参见 manage threads properly。
List over Vector
Vector 是同步的,而往往不需要同步。当确实需要同步时,synchronized list 通常可以作为 Vector 的直接替代。
equals() 和 hashCode()
如果你重写了其中一个,就必须实现两者。参见 equals/hashCode 约定。Guava 的 Objects.equal() 和 Objects.hashCode() 让遵守这些约定变得非常容易。
过早优化是万恶之源
Donald Knuth 很聪明,他对这个话题有不少论述。除非你有充分的证据表明优化是必要的,否则通常最好先实现未优化的版本(可以留下可优化之处的注释)。所以,在你花一周写内存映射、压缩、huffman 编码的 hashmap 之前,先用现成的东西并测量。
TODOs
尽早并经常留 TODO(Leave TODOs early and often)
TODO 不是坏事——它向未来的开发者(可能是你自己)信号:一个考虑被做出了,但因为各种原因被省略。它在调试时也能作为有用的信号。
不留无主的 TODO(Leave no TODO unassigned)
TODO 应该有 owner,否则它们不太可能被解决:
// Bad.
// - TODO is unassigned.
// TODO: Implement request backoff.
// Good.
// TODO(George Washington): Implement request backoff.
认领 TODO(Adopt TODOs)
如果 owner 已经离开了公司/项目,或者你直接修改了与该 TODO 主题直接相关的代码,你应该认领这个”孤儿”。
遵守迪米特法则(Obey the Law of Demeter, LoD)
迪米特法则最明显的违反是打破”one dot rule”,但也有其它违反该法则精神的代码结构。
在类中(In classes)
只拿你需要的,不多拿。这常与 texas constructors 相关,但也会藏在只接受少量参数的构造方法或方法中。关键思想是把组装(assembly)推迟到”知道如何组装”的代码层,而只接受完成工作所需的最小接口。如果 Weigher 的基本构造方法只接受它实际使用的东西,它就更容易进行单元测试,也更容易随系统演进适配——你仍然可以提供便利构造方法、工厂方法或 builder 形式的外部工厂:
// Bad.
// - Weigher uses hosts and port only to immediately construct another object.
class Weigher {
private final double defaultInitialRate;
Weigher(Iterable<String> hosts, int port, double defaultInitialRate) {
this.defaultInitialRate = validateRate(defaultInitialRate);
this.weightingService = createWeightingServiceClient(hosts, port);
}
}
// Good.
class Weigher {
private final double defaultInitialRate;
Weigher(WeightingService weightingService, double defaultInitialRate) {
this.defaultInitialRate = validateRate(defaultInitialRate);
this.weightingService = checkNotNull(weightingService);
}
}
在方法中(In methods)
如果一个方法有多个相互隔离的块,考虑通过提取”只做一件事”的 helper 方法来为这些块命名。除了让调用点读起来更像英语之外,提取出的代码对肉眼也更容易做流程分析。经典的场景是分支变量赋值——极端情况下永远不要这样写:
void calculate(Subject subject) {
double weight;
if (useWeightingService(subject)) {
try {
weight = weightingService.weight(subject.id);
} catch (RemoteException e) {
throw new LayerSpecificException("Failed to look up weight for " + subject, e);
}
} else {
weight = defaultInitialRate * (1 + onlineLearnedBoost);
}
// Use weight here for further calculations
}
而应该这样写:
void calculate(Subject subject) {
double weight = calculateWeight(subject);
// Use weight here for further calculations
}
private double calculateWeight(Subject subject) throws LayerSpecificException {
if (useWeightingService(subject)) {
return fetchSubjectWeight(subject.id);
} else {
return currentDefaultRate();
}
}
private double fetchSubjectWeight(long subjectId) {
try {
return weightingService.weight(subjectId);
} catch (RemoteException e) {
throw new LayerSpecificException("Failed to look up weight for " + subject, e);
}
}
private double currentDefaultRate() {
return defaultInitialRate * (1 + onlineLearnedBoost);
}
一个总体上信任”方法名即所作所为”的代码阅读者,现在可以快速扫过 calculate,只在想深入了解的地方下钻。
不要重复自己(Don’t Repeat Yourself, DRY)
- 在有意义的地方提取常量(Extract constants whenever it makes sense)
- 将重复逻辑集中到工具函数(Centralize duplicate logic in utility functions)
恰当地管理线程(Manage threads properly)
无论是直接还是通过线程池创建线程,你都需要特别注意正确管理生命周期。请阅读 Thread 的文档,熟悉 daemon 与 non-daemon 线程的概念(以及它们对 JVM 生命周期的影响),不理解这些概念可能导致应用在 shutdown 时挂起。
正确地关闭一个 ExecutorService 是个略显棘手的过程(见 javadoc)。如果你的代码管理着一个带 non-daemon 线程的 executor service,你需要遵循相应流程;ExecutorServiceShutdown 很好地封装了这一行为。如果你想在 VM 关闭时自动执行这类清理,可以考虑注册 ShutdownRegistry。
避免不必要的代码(Avoid unnecessary code)
多余的临时变量(Superfluous temporary variables):
// Bad.
// - The variable is immediately returned, and just serves to clutter the code.
List<String> strings = fetchStrings();
return strings;
// Good.
return fetchStrings();
不需要的赋值(Unneeded assignment):
// Bad.
// - The null value is never realized.
String value = null;
try {
value = "The value is " + parse(foo);
} catch (BadException e) {
throw new IllegalStateException(e);
}
// Good
String value;
try {
value = "The value is " + parse(foo);
} catch (BadException e) {
throw new IllegalStateException(e);
}
“快”实现(The ‘fast’ implementation)
不要用一个方法的”fast”或”optimized”实现来迷惑你的 API 用户——既然有”快”的 add,调用者为什么还要用普通的那个?
int fastAdd(Iterable<Integer> ints);
// Why would the caller ever use this when there's a 'fast' add?
int add(Iterable<Integer> ints);
系列导航
- 上一篇:软件工程 019:Google Java 编程风格指南
- 回到系列起点:软件工程 001:常见开发理论