Style

Java 传统的代码风格是被用来编写非常复杂的企业级 JavaBean。新的代码风格看起来会更加整洁,更加正确,并且更加简单。

Structs

对我们程序员来说,包装数据是最简单的事情之一。下面是传统的通过定义一个 JavaBean 的实现方式:

  1. public class DataHolder {
  2. private String data;
  3. public DataHolder() {
  4. }
  5. public void setData(String data) {
  6. this.data = data;
  7. }
  8. public String getData() {
  9. return this.data;
  10. }
  11. }

这种方式既繁琐又浪费代码。即使你的 IDE 可以自动生成这些代码,也是浪费。因此,[别这么干][dontbean].

相反,我更喜欢 C 语言保存数据的风格来写一个类:

  1. public class DataHolder {
  2. public final String data;
  3. public DataHolder(String data) {
  4. this.data = data;
  5. }
  6. }

这样不仅减少了近一半的代码行数。并且,这个类里面保存的数据除了你去继承它,否则不会改变,由于它不可变性,我们可以认为这会更加简单。

如果你想保存很容易修改的对象数据,像 Map 或者 List,你应该使用 ImmutableMap 或者 ImmutableList,这些会在不变性那一部分讨论。

The Builder Pattern

如果你想用这种构造的方式构造更复杂的对象,请考虑构建器模式。

你可以建一个静态内部类来构建你的对象。构建器构建对象的时候,对象的状态是可变的,但是一旦你调用了 build 方法之后,构建的对象就变成了不可变的了。

想象一下我们有一个更复杂的 DataHolder。那么它的构建器看起来应该是这样的:

  1. public class ComplicatedDataHolder {
  2. public final String data;
  3. public final int num;
  4. // lots more fields and a constructor
  5. public static class Builder {
  6. private String data;
  7. private int num;
  8. public Builder data(String data) {
  9. this.data = data;
  10. return this;
  11. }
  12. public Builder num(int num) {
  13. this.num = num;
  14. return this;
  15. }
  16. public ComplicatedDataHolder build() {
  17. return new ComplicatedDataHolder(data, num); // etc
  18. }
  19. }
  20. }

然后调用它:

  1. final ComplicatedDataHolder cdh = new ComplicatedDataHolder.Builder()
  2. .data("set this")
  3. .num(523)
  4. .build();

这有[关于构建器更好的例子][builderex],他会让你感受到构建器到底是怎么回事。它没有使用许多我们尽力避免使用的样板,并且它会给你不可变的对象和非常好用的接口。

可以考虑下在众多的库中选择一个来帮你生成构建器,取代你亲手去写构建器的方式。

Immutable Object Generation

如果你要手动创建许多不可变对象,请考虑用注解处理器的方式从它们的接口自动生成。它使样板代码减少到最小化,减少产生 bug 的可能性,促进了对象的不可变性。看这 presentation 有常见的 Java 设计模式中一些问题的有趣的讨论。

一些非常棒的代码生成库如 [immutables]
(https://github.com/immutables/immutables), 谷歌的
auto-value
[Lombok][lombok]

Exceptions

使用[检查][checkedex]异常的时候一定要注意,或者干脆别用。它会强制你去用 try/catch 代码块包裹住可能抛出异常的部分。比较好的方式就是使你自定义的异常继承自运行时异常来取而代之。这样,可以让你的用户使用他们喜欢的方式去处理异常,而不是每次抛出异常的时候都强制它们去处理/声明,这样会污染代码。

一个比较漂亮的绝招是在你的方法异常声明中声明 RuntimeExceptions。这对编译器没有影响,但是可以通过文档告诉你的用户在这里可能会有异常抛出。

Dependency injection

在软件工程领域,而不仅是在 Java 领域,使用[依赖注入][di]是编写可测试软件最好的方法之一。
由于 Java 强烈鼓励使用面向对象的设计,所以在 Java 中为了开发可测试软件,你不得不使用依赖注入。

在 Java 中,通常使用[Spring 框架][spring]来完成依赖注入。Spring 有基于代码的和基于 XML 配置文件的两种连接方式。如果你使用基于 XML 配置文件的方式,注意不要[过度使用 Spring][springso],正是由于它使用的基于 XML 配置文件的格式。在 XML 配置文件中绝对不应该有逻辑或者控制结构。它应该仅仅被用来做依赖注入。

使用 Google 和 Square 的 [Dagger][dagger] 或者 Google 的 [Guice][guice] 库是 Spring 比较好的替代品。它们不使用像 Spring 那样的 XML 配置文件的格式,相反它们把注入逻辑以注解的方式写到代码中。

Avoid Nulls

尽量避免使用空值。不要返回 null 的集合,你应该返回一个 empty 的集合。如果你确实准备使用 null 请考虑使用 [@Nullable][nullable] 注解。[IntelliJ IDEA][intellij] 内置支持 @Nullable 注解。

阅读[计算机科学领域最糟糕的错误][the-worst-mistake-of-computer-science]了解更多为何不使用 null。

如果你使用的是 Java 8,你可以用新出的优秀的 [Optional][optional] 类型。如果有一个值你不确定是否存在,你可以像这样在类中用 Optional 包裹住它们:

  1. public class FooWidget {
  2. private final String data;
  3. private final Optional<Bar> bar;
  4. public FooWidget(String data) {
  5. this(data, Optional.empty());
  6. }
  7. public FooWidget(String data, Optional<Bar> bar) {
  8. this.data = data;
  9. this.bar = bar;
  10. }
  11. public Optional<Bar> getBar() {
  12. return bar;
  13. }
  14. }

这样,现在你可以清晰地知道 data 肯定不为 null,但是 bar 不清楚是不是存在。Optional 有如 isPresent 这样的方法,可以用来检查是否为 null,感觉和原来的方式并没有太大区别。但是它允许你可以这样写:

  1. final Optional<FooWidget> fooWidget = maybeGetFooWidget();
  2. final Baz baz = fooWidget.flatMap(FooWidget::getBar)
  3. .flatMap(BarWidget::getBaz)
  4. .orElse(defaultBaz);

这样比写一连串的判断是否为空的检查代码更好。使用 Optional 唯一不好的是标准库对 Optional 的支持并不是很好,所以对 null 的处理仍然是必要的。

Immutable-by-default

变量,类和集合应该设置为不可变的,除非你有很好的理由去修改他们。

变量可以用 final 关键字使起不可变:

  1. final FooWidget fooWidget;
  2. if (condition()) {
  3. fooWidget = getWidget();
  4. } else {
  5. try {
  6. fooWidget = cachedFooWidget.get();
  7. } catch (CachingException e) {
  8. log.error("Couldn't get cached value", e);
  9. throw e;
  10. }
  11. }
  12. // fooWidget is guaranteed to be set here

现在你可以确定 fooWidget 对象不会意外地被重新赋值了。final 关键词也可以在 if/else 和 try/catch 代码块中使用。当然,如果 fooWidget 对象本身不是不可变的,你可以很容易去修改它。

使用集合的时候,任何可能的情况下尽量使用 Guava 的 [ImmutableMap][immutablemap], [ImmutableList][immutablelist], 或者
[ImmutableSet][immutableset] 类。这些类都有构建器,你可以很容易地动态构建集合,一旦你执行了 build 方法,集合就变成了不可变的。

类应该声明不可变的字段(通过 final 实现)和不可变的集合使该类不可变。或者,可以对类本身使用 final 关键词,这样这个类就不会被继承也不会被修改了。

Avoid lots of Util classes

如果你发现在你正在往工具类中添加很多方法,就要注意了。

  1. public class MiscUtil {
  2. public static String frobnicateString(String base, int times) {
  3. // ... etc
  4. }
  5. public static void throwIfCondition(boolean condition, String msg) {
  6. // ... etc
  7. }
  8. }

乍一看这些工具类似乎很不错,因为里面的那些方法放在别处确实都不太合适。因此,你以可重用代码的名义全放这了。

这个想法比本身这么做还要糟糕。请把这些类放到它应该在的地方去并积极重构。不要命名一些像 “MiscUtils” 或者 “ExtrasLibrary” 这样的很普通的类,包或者库。这会鼓励产生无关代码。

Formatting

格式化代码对大多数程序员来说并没有它应有的那么重要。统一化你的代码格式对阅读你的代码的人有帮助吗?当然了。但是别在为了 if 代码块匹配添加空格上耗一天。

如果你确实需要一个代码格式风格的教程,我高度推荐 [Google’s Java Style][googlestyle] 这个教程。写的最好的部分是 [Programming Practices][googlepractices]。绝对值得一读。

Javadoc

文档对对你代码的阅读着来说也很重要。这意味着你要给出[使用示例][javadocex],并且给出你的变量,方法和类清晰地描述。

这样做的必然结果是不要对不需要写文档的地方填写文档。如果你对一个参数的含义没什么可说的,或者它本身已经很明显是什么意思了,就不要为其写文档了。统一样板的文档比没有文档更加糟糕,这样会让读你代码的人误以为那就是文档。

Streams

[Java 8][java8] 有很棒的 [stream][javastream] and lambda 语法。你可以像这样来写代码:

  1. final List<String> filtered = list.stream()
  2. .filter(s -> s.startsWith("s"))
  3. .map(s -> s.toUpperCase())
  4. .collect(Collectors.toList());

取代这样的写法:

  1. final List<String> filtered = new ArrayList<>();
  2. for (String str : list) {
  3. if (str.startsWith("s") {
  4. filtered.add(str.toUpperCase());
  5. }
  6. }

它让你可以写更多的流畅的代码,并且可读性更高。