原文地址:Better Java
Better Java
Java是目前最流行的编程语言之一,但并不是所有的人都能享受地使用它。其实,Java是一种非常棒的编程语言,由于最近Java 8的发布,我决定整理一个包括库、实践、和工具的列表来帮助大家更好的使用Java。
这篇文章也放在Github上,你可以随时贡献并添加你自己的Java秘籍与最佳实践。
风格
在传统风格中,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能够自动生成这些代码,看起来也是挺糟糕的。因此,尽量不要这么做。
作为替代方式,我更喜欢用C语言中结构体的风格来封装数据:
public class DataHolder { public final String data; public DataHolder(String data) { this.data = data; } }
这种方式少了近一半的代码量。而且这个类是不可变的,除非你继承它进行扩展。由于具有不变性,因此在某些情况下可以放心的使用它。
如果你想保存像Map
和List
一样容易修改的对象,你应该使用ImmutableMap
或者ImmutableList
,这些将在不变性的那一部分讨论。
Builder模式
如果你想构造一个比较复杂的对象,可以考虑使用Builder模式。
使用一个静态内部类来创建你的对象,内部类对象的状态是可变的,一旦你调用它的build
方法,它将构造出一个你所需要的不可变对象。
想象一下我们有一个很复杂的DataHolder
,里面定义了很多数据字段,那么Builder模式大概是这样的:
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();
更多的例子,通过这个小例子你应该知道Builder模式大概是什么样子了。它没有使用许多模板代码,而且它提供了不可变对象和非常良好的接口。
依赖注入
在软件工程领域,而不仅是在Java领域,使用依赖注入是编写可测试软件最好的方式之一。因为Java强烈鼓励使用面向对象的设计,为了开发可测试软件,你不得不使用依赖注入。
在Java中,通常的方式是使用Spring框架来完成依赖注入。它能够通过基于代码方式和基于XML配置文件方式来完成依赖注入。如果你使用基于XML配置文件的方式,记住不要过度使用Spring,正式因为它使用的基于XML配置文件的格式。在XML配置文件中绝对不应该有逻辑或者控制结构,它应该仅仅用来做依赖注入。
比较好的能够替代Spring的有Google和Square的Dagger库,或者Google的Guice库。它们不使用像Spring那样的XML配置文件,相反的它们把注入逻辑放在注解和代码中。
避免null值
尽量避免使用null值,不要返回一个null集合,而应该返回一个empty集合。如果你确实要使用null值,可以考虑加上@Nullable注解,IntelliJ IDEA已经内建了对@Nullable
注解的支持。
如果你正在使用Java 8,你可以使用新提供的类Optional。如果一个值可能是null,可以将它包裹在一个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或许是null。Optional
有像isPresent
这样的方法,可以用来检查是否为为null,或许感觉和原来的方式没什么不同。但是你可以这样写代码:
final Optional<FooWidget> fooWidget = maybeGetFooWidget(); final Baz baz = fooWidget.flatMap(FooWidget::getBar) .flatMap(BarWidget::getBaz) .orElse(defaultBaz);
这样比写一连串的判空处理代码更好,唯一的缺点就是标准库对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或者ImmutableSet类。它们都有构建器,你能够很容易的动态构建它们,然后调用build
方法获取一个不可变集合。
类应该声明不可变字段(通过final
实现)和不可变的集合使该类不可变。或者,你也可以对类本身使用final
,这样这个类就不会被继承也不会被修改了。
避免过多的工具类
如果你发现你正在往一个工具类中添加很多方法,你就要注意了。
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 { default void throwIfCondition(boolean condition, String msg) { // ... } default void throwAorB(Throwable a, Throwable b, boolean throwA) { // ... } }
然后每一个需要它们的类只需要简单的实现这个接口就行了。
格式
大部分程序员认为格式没那么重要,代码的一致性能够帮助其他人更好的阅读吗?当然啦,但是别为匹配你的代码块而浪费一天时间去加空格。
如果你需要一份代码规范教程,我强烈推荐Google's Java Style Guide,这份指导中写的最好的部分是Programming Practices,绝对值得一读。
Javadoc
文档对你代码的阅读者来说是很重要的,这意味着你要给出使用示例,并且给出变量、方法以及类的清晰描述。
这样做的必然结果是不要给不需要的部分写文档。如果你对一个参数的含义没什么可说的,或者它本身是什么意思已经是显而易见的,那就不要写文档了。样版文档比没有文档更糟糕,这会让读你代码的人误以为那就是文档。
Streams
Java 8提供了很棒的stream和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程序并不太容易,所以使用框架来完成能帮不少忙。最好的2个框架是Dropwizard和Spring Boot。Play framework也是众多框架里可以考虑的。
所有的这些框架降低了你部署代码的难度。如果你刚接触Java或者需要快速完成这些工作,那么它们是格外有帮助的。单个JAR包的部署比复杂的WAR包或者EAR包部署更加容易。
然而,这些框架有些地方还是不太灵活,如果你的项目不能符合框架开发者的选择,你必须手动做一些配置。
Maven
不错的替代品:Gradle
Maven是一个构建、打包、测试的标准工具。有一些不错的替代品,比如说Gradle,但是它们没有Maven那样的适应性。如果你是Maven新手,你应该从Maven示例开始。
我喜欢使用一个根POM来管理所有用到的外部依赖,就像这样。这个根POM仅仅有一个外部依赖,如果你的项目足够大,你将会需要更多,依据你的项目而定。你的根POM应该像其他的Java项目一样使用版本控制和发布的方式,有一个自己的项目。
如果你认为为每一个外部依赖的改变标记你的根POM过于繁琐,你可能会花费更多的时间来跟踪错误。
你的所有Maven项目都包含你的根POM和一些版本信息,按照这种方式,你能轻易获得你需要的相应版本的外部依赖和所有Maven插件。如果你需要外部依赖,你可以这样配置:
<dependencies> <dependency> <groupId>org.third.party</groupId> <artifactId>some-artifact</artifactId> </dependency> </dependencies>
如果你需要内部依赖,应该由每个项目自己来管理,否则难以保持根POM版本号是正常的。
Dependency Convergence
Java最好的一面就是拥有大量的第三方库能够使用。基本上每一个API或者工具包都有一个Java SDK,可以很容易使用Maven引入。
有些Java包依赖于另一个包的特定版本,如果你加入太多jar包,你也许会发现一些版本冲突的问题,就像这样:
Foo library depends on Bar library v1.0 Widget library depends on Bar library v0.9
那么你应该获取哪一个版本呢?
如果你的项目依赖于不同版本的同一个库,使用Maven dependency convergence插件构建时会发生错误,这时你有两个方案来解决:
- 在你的dependencyManagement部分明确地支出你所使用的Bar的版本号
- 在FOO或者Widget中排除对Bar的依赖
至于选择那一种方式取决于具体情况:如果你想跟踪一个项目的版本,那么选择排除的方案是不错的。另一方面,如果你想明确一些,你可以选择一个版本,尽管当你更新其它依赖的时候你应该跟新它。
持续集成
很明显,你需要一些持续集成服务器来创建你的快照版本和基于git标签构建,Jenkins和Travis-CI会是很自然的选择。
代码覆盖率是很有用的,Cobertura有一个很好的Maven插件和CI支持。也有一些其他的代码覆盖工具,但是我使用Cobertura。
Maven仓库
你需要一个地方来放置你的JAR包, WAR包, 和EAR包,因此你需要一个仓库。
通用的选择会是Artifactory和Nexus,他们都有各自的优缺点。
你应该安装你自己的Artifactory/Nexus设施,使你的依赖基于此。这能够避免一些上游的Maven仓库所带来的问题。
配置管理
现在你的代码已经能够编译,你的仓库也已经安装,你需要把开发环境的代码部署到生产环境上。不要嫌麻烦而节省操作,因为将来很长一段时间,你会从这些自动化方式中尝到很多的甜头。
Chef,Puppet,和Ansible是典型的选择,我写过一个替代品叫做Squadron,当然,你觉得哪个容易就使用哪个。
无论你选择什么工具,不要忘记使你的部署实现自动化。
库
可能Java最大的特点就是它有大量的库可供使用,下面这些库对大多数人来说都很适用。
缺少的特性
Java标准库一直在不断扩展,但是似乎仍然缺少一些特性。
Apache Commons
Apache Commons项目有许多有用的类库。
Commons Codec 有许多有用的Base64和16进制编解码方法,不要浪费时间重复造轮子了。
Commons Lang 对String类的管理和创建,字符集,还有以大量各种各样的工具方法。
Commons IO 有你能想到的所有文件有关的方法,例如FileUtils.copyDirectory,FileUtils.writeStringToFile,IOUtils.readLines等等。
Guava
Guava是Google的优秀杰作。从这个库中提取所有精华不是一件容易的事,但是我正在尝试。
Cache 让你可以用一种简单的方法,实现把网络访问,磁盘访问,缓存函数或者任何你想要缓存的内容,缓存到内存中。你所要做的仅仅是实现CacheBuider并告诉Guava怎样创建缓存就行了。
Immutable 集合包含许多组件:ImmutableMap,ImmutableList,ImmutableSortedMultiSet。
我也喜欢使用Guava来写可变集合:
// Instead of final Map<String, Widget> map = new HashMap<String, Widget>(); // You can use final Map<String, Widget> map = Maps.newHashMap();
还有一些静态类Lists、Maps、Sets等等,他们简洁易读。
如果你使用Java6或者7,你能使用Collection2类,它有一些方法例如filter
和transform
,它能够让你在没有Java 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用户指南有许多例子参考。
Java Tuples
Java标准库中并没有内建元组类型,这让我十分烦恼,幸运的是Java tuples项目弥补了它。
使用起来很方便:
Pair<String, Integer> func(String input) { // something... return Pair.with(stringResult, intResult); }
Joda-Time
Joda-Time是我使用过的最好的时间库,简单明了,容易测试,你还要求什么呢?
你仅仅需要这个如果你还没有使用Java8,它已经有了新的日期时间库。
Lombok
Lombok是一个有趣的库,通过注解,可以减少大量臃肿的代码。
想要为你的类声明添加setters和getters方法?
简单:
public class Foo { @Getter @Setter private int var; }
现在你可以这么使用:
final Foo foo = new Foo(); foo.setVar(5);
还有更多示例,虽然我没有在项目中使用过Lombok,但是我已经迫不及待了。
Play framework
关于RESTful在Java中有两大主要阵营:JAX-RS和其它。
JAX-RS是传统的方式。你可以用像Jersey这样的框架,以注解的方式来实现接口及其实现的结合。这样你就可以很容易的根据接口类来开发客户端。
Play框架基于JVM的web services实现和其它框架不同:他又一个路由文件,你写的类要和路由文件中的信息关联起来。它事实上是个完整的MVC框架,但是你可以很容易仅仅使用它的RESTweb功能。
它同时支持Java和Scala。虽然对重点支持的Scala稍有不足,但是对Java的支持还是很好用的。
如果你在Python中用过像Flask这样的微框架,你对Spark肯定会很熟悉。它对Java 8的支持尤其的好。
SLF4J
有许多Java日志解决方案,我最喜欢的是SLF4J,因为它拥有非常棒的可插拔性,能够同时和许多日志框架结合,你是否有一个怪异的项目同时使用java.util.logging、JCL和log4j?那么SLF4J 很适合你。
SLF4J手册是相当全面的你可以从这里开始。
jOOQ
我不喜欢重量级的ORM框架,因为我喜欢SQL。因此我写了大量JDBC模板,它是极难维护的,JOOQ是一个很好的解决方案。
使用它你能用一种类型安全的方式写SQL。
// Typesafely execute the SQL statement directly with jOOQ Result<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模式,你能轻松访问数据库。
测试
测试对你的软件来说是非常关键的,这些包能让测试更加容易。
jUnit 4
jUnit不需要介绍了,它是Java的标准单元测试工具。
但是你可能并没有使用到jUnit的全部特性,jUnit支持参数化测试,规则化测试能够让你避免许多样板代码,还包括一些随机测试部分代码的理论和假设。
jMock
如果你完成了依赖注入,这是它的回报:可以mock出有副作用(比如和REST服务器交互)的代码,并且可以断言调用这段代码的行为。
jMock是标准的Java mock工具,使用起来就像这样:
public class FooWidgetTest { private Mockery context = new Mockery(); @Test public 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
方法被以一个字符串为参数的形式调用,并且会被调用0次或者多次。
如果你想一遍又一遍地设置相同的依赖,你应该把它放到test fixture中,并且把assertIsSatisfied
放在以@After
注解的fixture中。
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
IntelliJ IDEA是最好的Java开发工具了,自动完成功能超棒,代码检查功能也是顶尖的,重构工具那是相当有帮助。
免费社区版对我来说已经足够了,但是旗舰版有许多非常棒的功能特性,比如说数据库工具,Spiring框架和Chronon的支持。
Chronon
我最喜欢GDB 7的特性之一就是调试的时候能够按照时间跟踪回来。只需要在旗舰版上安装Chronon IntelliJ插件就行了。
你可以获取到变量的变化历史,后退,方法的历史以及更多的信息。如果你是第一次用会觉得有点怪,但是它真的能够帮你解决很复杂的bug,诸如海森堡类的bug。
JRebel
持续集成往往以软件即服务为产品目标。想象一下如果你不用等待代码构建完成而能实时看到代码的变化会是怎样?
这就是JRebel所做的。一旦你将你的服务器和你的JReble以hook方式连接,你就可以从服务器看到实时变化。当你想快速试验的时候它能为你节省大量的时间。
Checker框架
Java的类型系统很差劲。它不能够区分正常的字符串和正则表达式字符串,更不用说坏点检查了。但是The Checker Framework框架能做得很好。
它使用像@Nullable
这样的注解来检查类型,你甚至可以使用自定义注解来做更强大的静态分析。
Eclipse Memory Analyzer
即使是在Java中内存泄漏也时有发生,幸运的是,有许多工具可以使用。我用过最好的工具就是Eclipse Memory Analyzer。它能够帮你分析堆栈空间让你发现问题。
有一些方式能够得到JVM进程的堆栈信息,我通常使用jmap:
$ jmap -dump:live,format=b,file=heapdump.hprof -F 8152 Attaching to process ID 8152, please wait... Debugger attached successfully. Server compiler detected. JVM version is 23.25-b01 Dumping heap to heapdump.hprof ... ... snip ... Heap dump file created
然后你可以用Memory Analyzer打开heapdump.hprof
文件,看看到底肿么了。
资源
这些资源能帮助你成为Java大牛。