Style
Java 传统的代码风格是被用来编写非常复杂的企业级 JavaBean。新的代码风格看起来会更加整洁,更加正确,并且更加简单。
Structs
对我们程序员来说,包装数据是最简单的事情之一。下面是传统的通过定义一个 JavaBean 的实现方式:
public class DataHolder {
private String data;
public DataHolder() {
}
public void setData(String data) {
this.data = data;
}
public String getData() {
return this.data;
}
}
这种方式既繁琐又浪费代码。即使你的 IDE 可以自动生成这些代码,也是浪费。因此,[别这么干][dontbean].
相反,我更喜欢 C 语言保存数据的风格来写一个类:
public class DataHolder {
public final String data;
public DataHolder(String data) {
this.data = data;
}
}
这样不仅减少了近一半的代码行数。并且,这个类里面保存的数据除了你去继承它,否则不会改变,由于它不可变性,我们可以认为这会更加简单。
如果你想保存很容易修改的对象数据,像 Map 或者 List,你应该使用 ImmutableMap 或者 ImmutableList,这些会在不变性那一部分讨论。
The Builder Pattern
如果你想用这种构造的方式构造更复杂的对象,请考虑构建器模式。
你可以建一个静态内部类来构建你的对象。构建器构建对象的时候,对象的状态是可变的,但是一旦你调用了 build 方法之后,构建的对象就变成了不可变的了。
想象一下我们有一个更复杂的 DataHolder。那么它的构建器看起来应该是这样的:
public class ComplicatedDataHolder {
public final String data;
public final int num;
// lots more fields and a constructor
public static class Builder {
private String data;
private int num;
public Builder data(String data) {
this.data = data;
return this;
}
public Builder num(int num) {
this.num = num;
return this;
}
public ComplicatedDataHolder build() {
return new ComplicatedDataHolder(data, num); // etc
}
}
}
然后调用它:
final ComplicatedDataHolder cdh = new ComplicatedDataHolder.Builder()
.data("set this")
.num(523)
.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 包裹住它们:
public class FooWidget {
private final String data;
private final Optional<Bar> bar;
public FooWidget(String data) {
this(data, Optional.empty());
}
public FooWidget(String data, Optional<Bar> bar) {
this.data = data;
this.bar = bar;
}
public Optional<Bar> getBar() {
return bar;
}
}
这样,现在你可以清晰地知道 data 肯定不为 null,但是 bar 不清楚是不是存在。Optional 有如 isPresent 这样的方法,可以用来检查是否为 null,感觉和原来的方式并没有太大区别。但是它允许你可以这样写:
final Optional<FooWidget> fooWidget = maybeGetFooWidget();
final Baz baz = fooWidget.flatMap(FooWidget::getBar)
.flatMap(BarWidget::getBaz)
.orElse(defaultBaz);
这样比写一连串的判断是否为空的检查代码更好。使用 Optional 唯一不好的是标准库对 Optional 的支持并不是很好,所以对 null 的处理仍然是必要的。
Immutable-by-default
变量,类和集合应该设置为不可变的,除非你有很好的理由去修改他们。
变量可以用 final 关键字使起不可变:
final FooWidget fooWidget;
if (condition()) {
fooWidget = getWidget();
} else {
try {
fooWidget = cachedFooWidget.get();
} catch (CachingException e) {
log.error("Couldn't get cached value", e);
throw e;
}
}
// 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
如果你发现在你正在往工具类中添加很多方法,就要注意了。
public class MiscUtil {
public static String frobnicateString(String base, int times) {
// ... etc
}
public static void throwIfCondition(boolean condition, String msg) {
// ... etc
}
}
乍一看这些工具类似乎很不错,因为里面的那些方法放在别处确实都不太合适。因此,你以可重用代码的名义全放这了。
这个想法比本身这么做还要糟糕。请把这些类放到它应该在的地方去并积极重构。不要命名一些像 “MiscUtils” 或者 “ExtrasLibrary” 这样的很普通的类,包或者库。这会鼓励产生无关代码。
Formatting
格式化代码对大多数程序员来说并没有它应有的那么重要。统一化你的代码格式对阅读你的代码的人有帮助吗?当然了。但是别在为了 if 代码块匹配添加空格上耗一天。
如果你确实需要一个代码格式风格的教程,我高度推荐 [Google’s Java Style][googlestyle] 这个教程。写的最好的部分是 [Programming Practices][googlepractices]。绝对值得一读。
Javadoc
文档对对你代码的阅读着来说也很重要。这意味着你要给出[使用示例][javadocex],并且给出你的变量,方法和类清晰地描述。
这样做的必然结果是不要对不需要写文档的地方填写文档。如果你对一个参数的含义没什么可说的,或者它本身已经很明显是什么意思了,就不要为其写文档了。统一样板的文档比没有文档更加糟糕,这样会让读你代码的人误以为那就是文档。
Streams
[Java 8][java8] 有很棒的 [stream][javastream] and lambda 语法。你可以像这样来写代码:
final List<String> filtered = list.stream()
.filter(s -> s.startsWith("s"))
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
取代这样的写法:
final List<String> filtered = new ArrayList<>();
for (String str : list) {
if (str.startsWith("s") {
filtered.add(str.toUpperCase());
}
}
它让你可以写更多的流畅的代码,并且可读性更高。