如何写出更好的Java代码

共计 10732 个字符,预计需要花费 27 分钟才能阅读完成。

from: http://it.deepinmind.com/java/2014/05/21/better-java.html

Java 是最流行的编程语言之一,但似乎并没有人喜欢使用它。好吧,实际上 Java 是一门还不错的编程语言,由于最近 Java 8 发布了,我决定来编辑一个如何能更好地使用 Java 的列表,这里面包括一些库,实践技巧以及工具。

这篇文章在 GitHub 上也有。你可以随时在上面贡献或者添加你自己的 Java 使用技巧或者最佳实践。

  • 编码风格

    • 结构体

      *   builder 模式
      • 依赖注入
    • 避免 null 值

    • 不可变

    • 避免过多的工具类

    • 格式

      *   文档
      • Stream
  • 部署

    • 框架

    • Maven

      *   依赖收敛
      • 持续集成
    • Maven 仓储

    • 配置管理

    • 遗失的特性

      *   Apache Commons
      • Guava

      • Gson

      • Java Tuples

      • Joda-Time

      • Lombok

      • Play framework

      • SLF4J

      • jOOQ

      • 测试

        • jUnit 4
      • jMock

      • AssertJ

  • 工具

    • IntelliJ IDEA

      *   Chronon
      • JRebel
    • 校验框架

    • Eclipse Memory Analyzer

  • 资源

    • 书籍
    • 播客

编码风格

传统的 Java 编码方式是非常啰嗦的企业级 JavaBean 的风格。新的风格更简洁准确,对眼睛也更好。

结构体

我们这些码农干的最简单的事情就是传递数据了。传统的方式就是定义一个 JavaBean:

public class DataHolder {private String data;
public DataHolder() {}public void setData(String data) {    this.data = data;}public String getData() {    return this.data;}

}

这不仅拖沓而且浪费。尽管你的 IDE 可以自动地生成这个,但这还是浪费。因此,[不要这么写](http://www.javapractices.com/topic/TopicAction.do?Id=84)。

相反的,我更喜欢 C 的结构体的风格,写出来的类只是包装数据:

public class DataHolder {public final String data;
public DataHolder(String data) {    this.data = data;}

}

这样写减少了一半的代码。不仅如此,除非你继承它,不然这个类是不可变的,由于它是不可变的,因此推断它的值就简单多了。

如果你存储的是 Map 或者 List 这些可以容易被修改的数据,你可以使用 ImmutableMap 或者 ImmutableList,这个在不可变性这节中会有讨论。

Builder 模式

如果你有一个相对复杂的对象,可以考虑下 Builder 模式。

你在对象里边创建一个子类,用来构造你的这个对象。它使用的是可修改的状态,但一旦你调用了 build 方法,它会生成一个不可变对象。

想象一下我们有一个非常复杂的对象 DataHolder。它的构造器看起来应该是这样的:

public class ComplicatedDataHolder {public final String data;    public final int num;    // lots more fields and a constructor
public 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();

关于 Builder 的使用 [这里](http://en.deepinmind.com/blog/2014/05/21/the-builder-pattern-in-practice.html) 还有些更好的例子,我这里举的例子只是想让你大概感受一下。当然这会产生许多我们希望避免的样板代码,不过好处就是你有了一个不可变对象以及一个连贯接口。

依赖注入

这更像是一个软件工程的章节而不是 Java 的,写出可测的软件的一个最佳方式就是使用依赖注入(Dependency injection,DI)。由于 Java 强烈鼓励使用面向对象设计,因此想写出可测性强的软件,你需要使用 DI。

在 Java 中,这个通常都是用 Spring 框架来完成的。它有一个基于 XML 配置的绑定方式,并且仍然相当流行。重要的一点是你不要因为它的基于 XML 的配置格式而过度使用它了。在 XML 中应该没有任何的逻辑和控制结构。它只应该是依赖注入。

还有一个不错的方式是使用 Dagger 库 以及 Google 的Guice。它们并没有使用 Spring 的 XML 配置文件的格式,而是将注入的逻辑放到了注解和代码里。

避免 null 值

如果有可能的话尽量避免使用 null 值。你可以返回一个空的集合,但不要返回 null 集合。如果你准备使用 null 的话,考虑一下 @Nullable 注解。IntelliJ IDEA 对于 @Nullable 注解有内建的支持。

如果你使用的是 Java 8 的话,可以考虑下新的 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&lt;Bar&gt; bar) {    this.data = data;    this.bar = bar;}public Optional&lt;Bar&gt; 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);

这比使用 if 来检查 null 好多了。唯一的缺点就是标准类库中对 Optional 的支持并不是很好,因此你还是需要对 null 进行检查的。

不可变

变量,类,集合,这些都应该是不可变的,除非你有更好的理由它们的确需要进行修改。

变量可以通过 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, ImmutableList, or ImmutableSet 类。这些类都有自己的构造器,你可以动态的创建它们,然后将它们设置成不可变的,。

要使一个类不可变,你可以将它的字段声明成不可变的(设置成 final)。你也可以把类自身也设置成 final 的这样它就不能被扩展并且修改了,当然这是可选的。

避免大量的工具类

如果你发现自己添加了许多方法到一个 Util 类里,你要注意了。

public class MiscUtil {public static String frobnicateString(String base, int times) {// ... etc}
public static void throwIfCondition(boolean condition, String msg) {    // ... etc}

}

这些类乍一看挺吸引人的,因为它们里面的这些方法不属于任何一个地方。因此你以代码重用之名将它们全都扔到这里了。

这么解决问题结果更糟。把它们放回它们原本属于的地方吧,如果你确实有一些类似的常用方法,考虑下 Java 8 里接口的默认方法。并且由于它们是接口,你可以实现多个方法。

public interface Thrower {public void throwIfCondition(boolean condition, String msg) {// ...}
public void throwAorB(Throwable a, Throwable b, boolean throwA) {    // ...}

}

这样需要使用它的类只需简单的实现下这个接口就可以了。

格式

格式远比许多程序员相像的要重要的多。一致的格式说明你关注自己的代码或者对别人有所帮助?是的。不过你先不要着急为了让代码整齐点而浪费一整天的时间在那给 if 块加空格了。

如果你确实需要一份代码格式规范,我强烈推荐 Google 的 Java 风格指南。这份指南最精彩的部分就是 编程实践 这节了。非常值得一读。

文档

面向用户的代码编写下文档还是很重要的。这意味着你需要提供一些 使用的示例,同时你的变量方法和类名都应该有适当的描述信息。

结论就是不要给不需要文档的地方添加文档。如果对于某个参数你没什么可说的,或者它已经非常明显了,别写文档了。模板化的文档比没有文档更糟糕,因为它欺骗了你的用户,让他觉得这里有文档。

Java 8 有一个漂亮的流和 lambda 表达式的语法。你的代码可以这么写:

final List<String> filtered = list.stream()    .filter(s -> s.startsWith("s"))    .map(s -> s.toUpperCase());

而不是这样:

final List<String> filtered = Lists.newArrayList();for (String str : list) {if (str.startsWith("s") {filtered.add(str.toUpperCase());    }}

这样你能写出更连贯的代码,可读性也更强。

部署

正确地部署 Java 程序还是需要点技巧的。现在部署 Java 代码的主流方式有两种:使用框架或者使用自家摸索出来的解决方案,当然那样更灵活。

框架

由于部署 Java 程序并不容易,因此才有了各种框架来用于部署。最好的两个是 Dropwizard 以及 Spring BootPlay Framework 也可以算是一个部署框架。

这些框架都试图降低部署程序的门槛。如果你是一个 Java 的新手或者你需要快速把事情搞定的话,那么框架就派上用场了。单个 jar 的部署当然会比复杂的 WAR 或者 EAR 部署要更容易一些。

然而,这些框架的灵活性不够,并且相当顽固,因此如果这些框架的开发人员给出的方式不太适合你的项目的话,你只能自己进行配置了。

Maven

备选方案:Gradle

Maven 仍然是编译,打包,运行测试的标准化工具。还有其它一些选择,比如 Gradle,不过它们的采用程度远不 Maven。如果你之前没用过 Maven,你可以看下这个Maven 的使用示例

我喜欢用一个根 POM 文件来包含所有的外部依赖。它看起来就像是 这样。这个根 POM 文件只有一个外部依赖,不过如果你的产品很大的话,你可能会有很多依赖。你的根 POM 文件自己就应该是一个项目:它有版本控制,并且和其它的 Java 项目一样进行发布。

如果你觉得为每个外部依赖的修改都给 POM 文件打个标签(tag)有点太浪费了,那是你还没有经历过花了一个星期的时间来跟踪项目依赖错误的问题。

你的所有 Maven 工程都应该包含你的主 POM 文件以及所有的版本信息。这样的话,你能知道公司项目的每个外部依赖所选择的版本,以及所有正确的 Maven 插件。如果你需要引入一个外部依赖的话,大概是这样的:

<dependencies>    <dependency>        <groupId>org.third.party</groupId>        <artifactId>some-artifact</artifactId>    </dependency></dependencies>

如果你需要进行内部依赖的话,应该在项目的段中单独进行维护。不然的话,主 POM 文件的版本号就要疯涨了。

依赖收敛

Java 的一个最好的地方就是有大量的第三方库,它们无所不能。几乎每个 API 或者工具都有相应的 Java SDK,并且可以很容易地引入到 Maven 中来。

所有的这些 Java 库自身可能又会依赖一些特定的版本的其它类库。如果你引入了大量的库,可能会出现版本冲突,比如说像这样:

Foo library depends on Bar library v1.0Widget library depends on Bar library v0.9

你的工程应该引入哪个版本?

有了 Maven 的依赖收敛的插件后,如果你的依赖版本不一致的话,编译的时候就会报错。那么你有两种解决冲突的方案:

  • 在 dependencyManagement 区中显式地选择某个版本的 bar。
  • Foo 或者 Widget 都不要依赖 Bar。
    到底选择哪种方案取决你的具体情况:如果你想要跟踪某个工程的版本,不依赖它是最好的。另一方面,如果你想要明确一点,你可以自己选择一个版本,不过这样的话,如果更新了其它的依赖,也得同步地修改它。

持续集成

很明显你需要某种持续集成的服务器来不断地编译你的 SNAPSHOT 版本,或者对 Git 分支进行构建。

JenkinsTravis-CI 是你的不二选择。

代码覆盖率也很重要,Cobertura有一个不错的 Maven 插件,并且对 CI 支持的也不错。当然还有其它的代码覆盖的工具,不过我用的是 Cobertura。

Maven 库

你需要一个地方来存储你编译好的 jar 包,war 包,以及 EAR 包,因此你需要一个代码仓库。

常见的选择是 Artifactory 或者Nexus。两个都能用,并且各有利弊。

你应该自己进行 Artifactory/Nexus 的安装并且将你的依赖做一份镜像。这样不会由于下载 Maven 库的时候出错了导到编译中断。

配置管理

那现在你的代码可以编译了,仓库也搭建起来了,你需要把你的代码带出开发环境,走向最终的发布了。别马虎了,因为自动化执行从长远来看,好处是大大的。

ChefPuppet,和 Ansible 都是常见的选择。我自己也写了一个可选方案,Squadron。这个嘛,当然了,我自然是希望你们能下载下它的,因为它比其它那些要好用多了。

不管你用的是哪个工具,别忘了自动化部署就好。

可能 Java 最好的特性就是它拥有的这些库了。下面列出了一些库,应该绝大多数人都会用得上。

Java 的标准库,曾经还是很不错的,但在现在看来它也遗漏掉了很多关键的特性。

Apache Commons

Apache Commons 项目 有许多有用的功能。

  • Commons Codec 有许多有用的 Base64 或者 16 进制字符串的编解码的方法。别浪费时间自己又写一遍了。
  • Commons Lang 是一个字符串操作,创建,字符集,以及许多工具方法的类库。
  • Commons IO,你想要的文件相关的方法都在这里了。它有 FileUtils.copyDirectory,FileUtils.writeStringToFile, IOUtils.readLines,等等。

Guava

Guava 是一个非常棒的库,它就是 Java 标准库”所缺失的那部分”。它有很多我喜欢的地方,很难一一赘述,不过我还是想试一下。

  • Cache,这是一个最简单的获取内存缓存的方式了,你可以用它来缓存网络访问,磁盘访问,或者几乎所有东西。你只需实现一个 CacheBuilder,告诉 Guava 如何创建缓存就好了。
  • 不可变集合。这里有许多类:ImmutableMap, ImmutableList,甚至还有 ImmutableSortedMultiSet,如果这就是你想要的话。
    我还喜欢用 Guava 的方式来新建可变集合:
    // Instead offinal Map<String, Widget> map = new HashMap<String, Widget>();

// You can use
final Map<String, Widget> map = Maps.newHashMap();

有许多像 Lists, Maps, Sets 的静态类,他们都更简洁易懂一些。

如果你还在坚持使用 Java 6 或者 7 的话,你可以用下Collections2,它有一些诸如 filter 和 transform 的方法。没有 Jvaa 8 的 stream 的支持,你也可以用它们来写出连贯的代码。

Guava 也有一些很简单的东西,比如 Joiner,你可以用它来拼接字符串,还有一个类可以用来 处理中断

Gson

Google 的 Gson 是一个简单高效的 JSON 解析库。它是这样工作的:

final Gson gson = new Gson();final String json = gson.toJson(fooWidget);

final FooWidget newFooWidget = gson.fromJson(json, FooWidget.class);

真的很简单,使用它会感觉非常愉快。Gson 的 [用户指南](https://sites.google.com/site/gson/gson-user-guide) 中有更多的示例。

Java Tuples

我对 Java 一个不爽的地方就是它的标准库中居然没有元组。幸运的是,Java tuples 工程 解决了这一问题。

它也很容易使用,并且真的很赞:

Pair<String, Integer> func(String input) {// something...    return Pair.with(stringResult, intResult);}

Joda-Time

Joda-Time 是我用过的最好的时间库了。简直,直接,容易测试。你还想要什么?

这个库里我最喜欢的一个类就是 Duration,因为我用它来告诉说我要等待多长时间,或者过多久我才进行重试。

Lombok

Lombok是一个非常有趣的库。它通过注释来减少了 Java 中的饱受诟病的样板代码(注:setter,getter 之类的)。

想给你类中的变量增加 setter,getter 方法?太简单了:

public class Foo {@Getter @Setter private int var;}

现在你可以这么写了:

final Foo foo = new Foo();foo.setVar(5);

[这里](http://jnb.ociweb.com/jnb/jnbJan2010.html)还有更多的示例。我还没在生产代码中用过 Lombok,不过我有点等不及了。

Play 框架

备选方案:Jersey或者Spark

在 Java 中实现 REST 风格的 WEB 服务有两大阵营:JAX-RS和其它。

JAX-RS 是传统的方式。你使用像 Jersey 这样的东西来将注解和接口,实现组合到一起来实现 WEB 服务。这样做的好处就是,你可以通过一个接口就能很容易创建出一个调用的客户端来。

Play 框架是在 JVM 上实现 WEB 服务的截然不同的一种方式:你有一个 routes 文件,然后你去实现 routes 中那些规则所引用到的类。它其实就是个完整的 MVC 框架,不过你可以只用它来实现 REST 服务。

它同时支持 Java 和 Scala。它优先使用 Scala 这点可能有点令人沮丧,但是用 Java 进行开发的话也非常不错。

如果你习惯了 Python 里的 Flask 这类的微框架,那么你应该会对 Spark 感到很熟悉。有了 Java 8 它简直如虎添翼。

SLF4J

Java 打印日志有许多不错的解决方案。我个人最喜欢的是 SLF4J,因为它是可挺插拔的,并且可以同时混合不同的日志框架中输出的日志。有个奇怪的工程同时使用了 java.util.logging, JCL, 和 log4j?没问题,SLF4J 就是为它而生的。

想入门的话,看下它的这个 两页的手册 就足够的了。

jOOQ

我不喜欢很重的 ORM 框架,因为我喜欢 SQL。因此我写了许多的 JDBC 模板,但它们很难维护。jOOQ 是个更不错的解决方案。

你可以在 Java 中以一种类型安全的方式来书写 SQL 语句:

// Typesafely execute the SQL statement directly with jOOQResult<Record3<String, String, String>> result =create.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)    .from(BOOK)    .join(AUTHOR)    .on(BOOK.AUTHOR_ID.equal(AUTHOR.ID))    .where(BOOK.PUBLISHED_IN.equal(1948))    .fetch();

将它以及 [DAO 模式](http://www.javapractices.com/topic/TopicAction.do?Id=66) 结合起来,你可以让数据库访问变得更简单。

测试

测试对软件来说至关重要。下面这些库能让测试变得更加容易。

jUnit 4

jUnit 就不用介绍了。它是 Java 中单元测试的标准工具。

不过可能你还没有完全发挥 jUnit 的威力。jUnit 还支持参数化测试,以及能让你少写很多样板代码的 测试规则,还有能随机测试代码的Theory, 以及Assumptions

jMock

如果你已经完成了依赖注入,那么它回报你的时候来了:你可以 mock 出带副作用的代码(就像和 REST 服务器通信那样),并且仍然能对调用它的代码执行断言操作。

jMock 是 Java 中标准的 mock 工具。它的使用方式是这样的:

public class FooWidgetTest {private Mockery context = new Mockery();
@Testpublic void basicTest() {    final FooWidgetDependency dep = context.mock(FooWidgetDependency.class);    context.checking(new Expectations() {        oneOf(dep).call(with(any(String.class)));        atLeast(0).of(dep).optionalCall();    });    final FooWidget foo = new FooWidget(dep);    Assert.assertTrue(foo.doThing());    context.assertIsSatisfied();}

}

这段代码通过 jMock 设置了一个 FooWidgetDependency,然后添加了一些期望的操作。我们希望 dep 的 call 方法被调用一次而 dep 的 optionalCall 方法会被调用 0 或更多次。

如果你反复的构造同样的 FooWidgetDependency,你应该把它放到一个 测试设备(Test Fixture)里,然后把 assertIsSatisfied 放到一个 @After 方法中。

AssertJ

你是不是用 jUnit 写过这些?

final List<String> result = some.testMethod();assertEquals(4, result.size());assertTrue(result.contains("some result"));assertTrue(result.contains("some other result"));assertFalse(result.contains("shouldn't be here"));

这些样板代码有点太聒噪了。AssertJ 解决了这个问题。同样的代码可以变成这样:

assertThat(some.testMethod()).hasSize(4)                             .contains("some result", "some other result")                             .doesNotContain("shouldn't be here");

连贯接口让你的测试代码可读性更强了。代码如此,夫复何求?

工具

IntelliJ IDEA

备选方案:Eclipse and Netbeans

最好的 Java IDE 当然是 IntelliJ IDEA。它有许多很棒的特性,我们之所以还能忍受 Java 这些冗长的代码,它起了很大的作用。自动补全很棒,< a href=”http://i.imgur.com/92ztcCd.png“target=”_blank”> 代码检查也超赞,重构工具也非常实用。

免费的社区版对我来说已经足够了,不过在旗舰版中有许多不错的特性比如数据库工具,Srping 框架的支持以及 Chronon 等。

Chronon

GDB 7 中我最喜欢的特性就是调试的时候可以按时间进行遍历了。有了 IntelliJ IDEA 的 Chronon 插件 后,这个也成为现实了。当然你得是旗舰版的。

你可以获取到变量的历史值,跳回前面执行的地方,获取方法的调用历史等等。第一次使用的话会感觉有点怪,但它能帮忙你调试一些很棘手的 BUG。

JRebel

持续集成通常都是 SaaS 产品的一个目标。你想想如果你甚至都不需要等到编译完成就可以看到代码的更新?

这就是 JRebel 在做的事情。只要你把你的服务器挂到某个 JRebel 客户端上,代码一旦有改动你马上就能看到效果。当你想快速体验一个功能的话,这个的确能节省不少时间。

验证框架

Java 的类型系统是相当弱的。它不能区分出普通字符串以及实际上是正则的字符串,也不能进行

正文完
 
zhaopeng
版权声明:本站原创文章,由 zhaopeng 2014-10-27发表,共计10732字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)