Better Java - 教你如何编写现代化的Java程式
原文地址: 这里
Java是目前世界上最流行的编程语言之一,但是并不是所有人都乐于使用它。不过Java其实是一个还不错的语言,也别是自从Java 8正式发布之后,所以我决定将我的一些实践、库和工具列举下来以供大家参考。
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; } }
不过这种方式是冗长并且浪费资源的,即便你的编辑器能够自动生成这种代码。作为替代方法,我宁可选择使用C风格里的结构体样式去编写这种容器数据:
public class DataHolder { public final String data; public DataHolder(String data) { this.data = data; } }
这种方式几乎可以使得代码的行数减半,除此之外,这种类是不可变的类,所以在某些情况下我们可以放心的使用它。如果你是希望能够用Map或者List这些结构体去存储对象而使得修改变得简单一点,可以使用ImmutableMap或者ImmutableList,这部分会在下面讨论。
Builder模式
如果你的数据元结构比较复杂,可以考虑使用如下的Builder模式。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();
Dependency injection(依赖注入)
依赖注入更多的从属于软件工程的范畴而不是Java的范畴,但是去撰写可测试的软件的最好的方式就是使用依赖注入。因为Java强烈推荐OO的设计方式,为了让软件具有较好的可测试性,可以去使用依赖注入。
在Java领域,最经典的DI框架当属Spring,它提供了基于代码的注入以及基于XML配置的注入方式。不过Spring确实有点繁琐,如果单纯的只是想使用依赖注入这个功能,可以选择Google 和 Square的 Dagger 库 或者 Google's Guice .
避免空指针
尽可能地避免使用空指针。特别是在可能返回空的集合的情况下务必返回一个内容为空的集合而不是一个null。如果使用的是Java 8 ,可以使用新的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; } }
根据上述代码可以知道,返回的数据肯定不会为null类型,不过bar不一定是present的。
final Optional<FooWidget> fooWidget = maybeGetFooWidget(); final Baz baz = fooWidget.flatMap(FooWidget::getBar) .flatMap(BarWidget::getBaz) .orElse(defaultBaz);
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代码块。对于集合类型,应该在任何允许的情况下使用Guava 的 ImmutableMap , ImmutableList , 或者 ImmutableSet 类。他们都含有构造器类型,可以使用Builder进行动态构造最终调用build方法设置为不可变。
而对于类,可以通过设置其成员变量为final类型来将其变为不可变类型。另外,也可以将类本身设置为final类型来保证其不可以被扩展或者设置为可变类型。
Avoid lots of Util Classes(避免使用大量工具类)
一不注意,就会发现自己写了N多的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 { default void throwIfCondition(boolean condition, String msg) { // ... } default void throwAorB(Throwable a, Throwable b, boolean throwA) { // ... } }
这样每个需要使用这些接口的类可以方便的进行自定义。
格式化
格式化本身的重要性不亚于编程本身,很多优秀的程序员会花一天的时间去为if代码块添加空格从而使代码看起来更加的整齐。如果需要一个完整的代码格式指南,可以参考Google的 Google's Java Style ,特别是其中的 Programming Practices 非常有意义。
JavaDoc
为你的代码添加JavaDoc同样非常重要,可以参考这个示例: using examples
Streams
Java 8提供了非常Nice的Stream API,可以用如下的写法:
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()); } }
这样可以帮助你写更多的高可读性的、流畅的代码。
Deploying(部署)
部分Java代码可能需要一定的技巧性,目前一般来说部署Java主要有两种方式:使用某个框架或者是有一个本地化的可伸缩框架。
Frameworks(框架)
框架是你部署Java代码的一个很好地方式,其中较好的选择有 Dropwizard 与 Spring Boot 。另外 Play framework 也是一个不错的选择。
Maven
Maven是一个非常优秀的Java编译与依赖管理工具,通过如下方式可以方便的添加Maven依赖项:
<dependencies> <dependency> <groupId>org.third.party</groupId> <artifactId>some-artifact</artifactId> </dependency> </dependencies>
关于Maven的具体使用可以参考笔者的其余文章
Dependence Convergence(依赖收敛)
Java中一个巨大的魅力即在于有大量的第三方类库可供参考,有必要将所有用到的API或者SDK置于Maven最后那个。不过各种类库之间往往也是相互依赖的,譬如:
Foo library depends on Bar library v1.0 Widget library depends on Bar library v0.9
利用 Maven dependency convergence plugin ,在编译的时候会告警有一个依赖项依赖不同的版本,一般来说,可以用如下方式处理:
1.在dependenceManagement块选择一个特定的版本。
2.在Foo或者Widget依赖项中使用Exclude移除Bar。
Continuous Integration(持续集成)
在大型项目开发中,往往需要一些持续集成工具来不断基于git构建测试版本,其中 Jenkins 和 Travis-CI 是较常见的选择。另外,在正式的构建之前往往需要使用代码测试工具, Cobertura 就是一个非常好用的测试覆盖率校验工具。
Maven Repository
在大型项目开发中,往往会需要一个Repo去存放私人的Jars、Wars以及EARs。 Artifactory 与 Nexus 都是不错的选择。
Configuration Management(配置管理)
Chef , Puppet , 以及 Ansible 都是不错的选择。
Libraries
可能Java最优秀的属性就是它的大量的扩展库,本部分列举了部分常用的扩展库。
Missing Features(遗失的特性)
Apache Commons
The Apache Commons project 包含了一些列常用的库.
-
Commons Codec包含了大量有用的编码与解码的方法。
-
Commons Lang包含了大量的字符串处理以及字符编码相关的方法。
-
Commons IO包含了大量与文件相关的操作。 It has FileUtils.copyDirectory , FileUtils.writeStringToFile , IOUtils.readLines and much more.
Guava
Guava is Google's excellent here's-what-Java-is-missing library.
Gson
Google's Gson library is a simple and fast JSON parsing library. Itworks like this:
final Gson gson = new Gson(); final String json = gson.toJson(fooWidget); final FooWidget newFooWidget = gson.fromJson(json, FooWidget.class);
It's really easy and a pleasure to work with. The Gson user guide has many more examples.
Java Tuples
Java的标准库未能提供Tuples相关的数据结构是一个很大的遗憾。幸亏 Java tuples 项目填补了这个空白:
Pair<String, Integer> func(String input) { // something... return Pair.with(stringResult, intResult); }
Lombok
Lombok 是一个非常有趣的类库,通过注解方式可以允许减少Java存在的冗余代码,譬如以下的常见的Getter/Setter代码的功能:
public class Foo { @Getter @Setter private int var; }
而现在可以这么写:
final Foo foo = new Foo(); foo.setVar(5);
Play framework
Good alternatives: Jersey or Spark
There are two main camps for doing RESTful web services in Java: JAX-RS and everything else.
JAX-RS is the traditional way. You combine annotations with interfaces andimplementations to form the web service using something like Jersey .What's nice about this is you can easily make clients out of just the interface class.
The Play framework is a radically different take on web services onthe JVM: you have a routes file and then you write the classes referenced inthose routes. It's actually an entire MVC framework , but you caneasily use it for just REST web services.
It's available for both Java and Scala. It suffers slightly from being Scala-first, but it's still good to use in Java.
If you're used to micro-frameworks like Flask in Python, Spark willbe very familiar. It works especially well with Java 8.
SLF4J
There are a lot of Java logging solutions out there. My favorite is SLF4J because it's extremely pluggable and can combine logs from manydifferent logging frameworks at the same time. Have a weird project that usesjava.util.logging, JCL, and log4j? SLF4J is for you.
The two-page manual is pretty much all you'll need to getstarted.
JOOQ
I dislike heavy ORM frameworks because I like SQL. So I wrote a lot of JDBC templates and it was sort of hard to maintain. jOOQ is amuch better solution.
It lets you write SQL in Java in a type safe way:
// 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();
Using this and the DAO pattern, you can make database access a breeze.
Testing
jUnit 4
jUnit needs no introduction. It's the standard tool for unit testingin Java.
But you're probably not using jUnit to its full potential. jUnit supports parametrized tests , rules to stop you from writingso much boilerplate, theories to randomly test certain code,and assumptions .
jMock
If you've done your dependency injection, this is where it pays off: mockingout code which has side effects (like talking to a REST server) and stillasserting behavior of code that calls it.
jMock is the standard mocking tool for Java. It looks like this:
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(); } }
This sets up a FooWidgetDependency via jMock and then adds expectations. Weexpect that dep 's call method will be called once with some String and that dep 's optionalCall method will be called zero or more times.
If you have to set up the same dependency over and over, you should probablyput that in a test fixture and put assertIsSatisfied in an @After fixture.
AssertJ
Do you ever do this with 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"));
This is just annoying boilerplate. AssertJ solves this. You cantransform the same code into this:
assertThat(some.testMethod()).hasSize(4) .contains("some result", "some other result") .doesNotContain("shouldn't be here");
This fluent interface makes your tests more readable. What more could you want?
Tools
IntelliJ IDEA
Good alternatives: Eclipse and Netbeans
The best Java IDE is IntelliJ IDEA . It has a ton of awesomefeatures, and is really the main thing that makes the verbosity of Javabareable. Autocomplete is great, the inspections are top notch , and the refactoringtools are really helpful.
The free community edition is good enough for me, but there are loads of greatfeatures in the Ultimate edition like database tools, Spring Framework supportand Chronon.
Chronon
One of my favorite features of GDB 7 was the ability to travel back in timewhen debugging. This is possible with the Chronon IntelliJ plugin when you get the Ultimate edition.
You get variable history, step backwards, method history and more. It's alittle strange to use the first time, but it can help debug some reallyintricate bugs, Heisenbugs and the like.
JRebel
Continuous integration is often a goal of software-as-a-service products. Whatif you didn't even need to wait for the build to finish to see code changeslive?
That's what JRebel does. Once you hook up your server to your JRebelclient, you can see changes on your server instantly. It's a huge time savingswhen you want to experiment quickly.
The Checker Framework
Java's type system is pretty weak. It doesn't differentiate between Stringsand Strings that are actually regular expressions, nor does it do any taint checking . However, the Checker Framework does this and more.
It uses annotations like @Nullable to check types. You can even define your own annotations to make the static analysis done evenmore powerful.
Eclipse Memory Analyzer
Memory leaks happen, even in Java. Luckily, there are tools for that. The besttool I've used to fix these is the Eclipse Memory Analyzer . It takes aheap dump and lets you find the problem.
There's a few ways to get a heap dump for a JVM process, but I use 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
Then you can open the heapdump.hprof file with the Memory Analyzer and seewhat's going on fast.
Resources(资源)
Books(书籍)
-
[Java Concurrency in Practice