CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ENGINEERING / P-140 · 软件工程方法论

软件工程 020:Twitter Java Style Guide

引言

本指南的意图是提供一套鼓励写出好代码的约定(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)

插入换行通常有两个原因:

  1. 语句超出了列限制(column limit)
  2. 你想在逻辑上分隔一个”想法”

写代码就像讲故事。书面语言中的章节、段落和标点(分号、逗号、句号、连字符)传达了思想的层级与分隔;编程语言中也有类似的结构,你应该利用它们向代码的阅读者有效地讲故事。

不要不必要地拆分语句:

// 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 ExceptionErrorThrowable,这样就不用操心抛出的是什么类型。这通常是个坏主意,因为你最终捕获的会超出你真正想处理的范围。例如 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);

系列导航

← 设计模式 020:中介者(Mediator) 目录 JHipster 开发 20:可观测性——日志、指标、链路 →
← 返回文章列表