更好的Java

Java是最流行的语言之一,但是似乎没人喜欢使用它。好吧,Java仅仅是一种“还好”的编程语言。自从Java 8的面世,我决定编辑一个关于Java的列表,包括库、最佳实践以及工具让我们能更好的使用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;
    }
}

这段代码的行数减少了一半。更重要的是,该类是不可以变的,除非你继承该类。所以我们更容易使用它,因为我们知道它不能改变。

你应该使用 ImmutableMap 和 ImmutableList 来替代将对象存储在一个能轻易改变 Map 或 List 中,这些内容在关于不可变性的部分中讨论。

建造者模式

如果你有一个相当复杂的对象,你想使用某种结构来创建这种对象的时候,可以考虑使用建造者模式。

你可以在构造你的对象的类中创建一个子类,它的状态是可变的,但是一旦你调用 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();

可能会有其它更好的建造者模式例子,这个例子只是让你了解一下建造者模式是什么东西?最后,这里我们试着避开很多公式化的例子,创建了一个不可变的对象和一个很流畅的接口。

依赖注入

这个更多的是软件工程的内容,而不是Java的内容。但是,编写可测试软件的最好方式就是使用依赖注入(DI)。因为Java强力推荐面向对象设计,为了创建可测试软件,你需要使用DI。

在Java里面,最典型的例子是Spring框架。它可以使用基于注解的注入或基于XML配置的注入。如果你想使用XML配置,很重要的事情是不要过度使用Spring的这种基于XML的配置形式。在这个XML中绝不应该有逻辑和控制结构,它仅仅是依赖注入。

使用的比较好的Spring例子是Google和Square的Dagger库以及Google的Guice。它们都没有使用Spring的XML配置文件,而是把注入的逻辑写在了注解和代码里面。

避免空值

尽可能的避免使用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<Bar> bar) {
        this.data = data;
        this.bar = bar;
    }

    public Optional<Bar> getBar() {
        return bar;
    }
}

所以,现在非常清晰,data 从来不会为 null,但是 bar 可能存在,也可能不存在。Optional 有一些方法,如 isPresent,该方法感觉跟判空没什么不同,但是它允许你像下面这样写代码:

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

这样比连续的进行 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的 ImmutableMapImmutableList ImmutableSet 类。这些已存在的建造者可以让你通过调用它们的 build 方法动态的建造不可变的集合。

你应该通过声明不可变的成员变量(通过 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 {
    default void throwIfCondition(boolean condition, String msg) {
        // ...
    }

    default void throwAorB(Throwable a, Throwable b, boolean throwA) {
        // ...
    }
}

任何需要它的类都可以很简单地实现这个接口。

格式化

格式对编码的人来说没那么重要。但是它是不是能帮助你持续的关注你的原稿?是不是能帮助别人阅读你的代码?很明显是的。但是请不要浪费一天去对 if 块增加空格来保证它是“匹配”的。

如果你特别想要一份代码格式化指南,我强烈推荐Google的Java编程风格指南。该指南最好的部分是编程实践,绝对值得一读。

Javadoc

文档对使用你代码的人来说是很重要的。这包括使用的实例及对变量、方法和类的有意义的描述等。

这个推论是如果不需要文档说明的时候,不要写文档。如果对参数没有什么需要说明,或者说明是模糊的,那就不要写说明文档。无意义的文档比完全没有文档还要糟糕,因为它会误导用户以为这就是说明文档。

Stream

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比较困难,框架可以帮助解决这个问题。两个最好框架是DropwizardSpring BootPlay框架也是部署框架中的一种。

所有的这些框架都是为了降低代码部署的门槛。它们尤其对Java新手或者需要快速完成某件事情有很好的帮助。单个jar包部署比复杂的WAR和EAR部署要更简单。

但是,框架也从某种意义上降低了灵活性,不能自由发挥。所以,如果你的项目不适合于你选择的框架,你将不得不迁移到一个更多手工操作的配置上去。

Maven

其它好的选择:Gradle

Maven仍然是标准的构建、打包和运行测试工具。有其它可选的工具,例如Gradle,但是他们与maven采用的方式不同。如果你刚开始使用Maven,你应该从Maven实例开始。

我喜欢有一个根POM来包含所有的使用到的外部依赖。它看起来像这样,只有这个根POM有外部依赖,但是你的产品非常大,可以有很多个模块。你的根POM文件应该归属于这样一个项目:有版本控制和发布,类似于其他的Java项目。

所有的Maven项目应该包含你的根POM及所有的版本信息。用这种方式,你能使你的公司使用的每个外部依赖和maven插件都是一致的。如果你需要加一个外部依赖,只要这样就可以了:

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

如果你想使用内部依赖,你需要独立的管理项目的各个部分。否则,这将比通过POM来管理稳定的版本更加困难。

重复依赖检测

关于Java最好的一部分是存在大量的第三方库可以做任何事情。本质上讲每个API或者工具都是建立在Java SDK上的,所有很容易通过Maven获取下来。

这些库本身又会依赖指定版本的其它类库。如果你下载下来够多的库,你就会遇到版本冲突,类似这样:

Foo库依赖Bar库的1.0版
Widget库依赖Bar库的0.9版本

那到底哪个版本会拉到你的项目里面呢?

使用Maven依赖收敛插件,如果你的依赖使用到不同的版本,将会出现错误。你可以有两种选择来解决冲突:

  1. 显示的在依赖管理部分选择一个Bar版本。
  2. 从Foo或Widget中排除Bar。

至于选择哪种方式就要根据你的具体情况了:如果你想监测一个项目的版本,排除是有意义的。另一方面,如果你想显示的知道是哪个版本,就应该自己选择一个版本,虽然这样当其他依赖更新的时候同时需要更新该依赖。

持续集成

很明显,你需要一种持续集成的服务器,这样能持续的为你构建SNAPSHOT版本和基于git标签的标签构建。

JenkinsTravis-CI是很自然的选择。

代码覆盖率是很有用的,Cobertura有一个很好的Maven的插件,以及对CI的支持。还有一些其它Java检查代码覆盖率的工具,但是我只用过Cobertura。

Maven仓库

你需要一个地方来存放你打包的JAR、WAR及EAR,所以你需要一个仓库:

通常的选择是ArtifactoryNexus。两个都还可以,各自有它们的优缺点

你应该自己安装的 Artifactory/Nexus 服务器,并在上面建立依赖包的镜像。这样避免在构建你的项目时由于要从外部仓库下载jar而被中断。

配置管理

现在你的代码已经编译了,你的仓库也已经建立了,这个时候你需要将你的代码从开发环境推到生产环境。不要节省这个过程,因为这个过程的自动化将会获得长久的好处。

ChefPuppetAnsible都是典型的选择。也可以选择我曾写过的Squadron。当然,我想你应该测试一下,因为正确了解这些东西比选择更容易。

尽管有很多工具可以选择,但是不要忘记使你的部署自动化。

Java可能最好的特性是存在许多可扩展的库。对大部分人的应用来说,用到的库只是整个库中的很小一部分。

缺失的特性

Java的标准库,每向前一步都非常令人惊讶,不过就现在来看,还是缺乏几个关键的特性。

Apache Commons

Apache Commons项目提供了许多有用的库。

  • Commons Codec提供了许多关于Base64和16进制字符串的编码和解码方法。不要浪费你的时间去重写这些方法。
  • Commons Lang提供了String类的创建、操作及字符集等一系列五花八门的工具方法。
  • Commons IO拥有所有的你能想到的文件相关的方法。它有 FileUtils.copyDirectoryFileUtils.writeStringToFileIOUtils.readLines等方法。

Guava

Guava是Google旗下一个优秀库,提供了Java现在缺乏的特性。很难提炼关于这个库的所有东西,但是我尽量试试。

Cache提供一个简单的方式来建立内存级别的缓存,这可以用来缓存网络访问、磁盘访问以及内存函数或其它的东西。只需要实现CacheBuilder来告诉Guava怎样创建你缓存,这就是你所有的设置。

Immutable集合,这里面有许多这种集合:ImmutableMapImmutableList甚至ImmutableSortedMultiSet

我也喜欢使用Guava的方式来编写不可变集合:

// Instead of
final Map<String, Widget> map = new HashMap<String, Widget>();

// You can use
final Map<String, Widget> map = Maps.newHashMap();
There are static classes for Lists, Maps, Sets and more. They're cleaner and easier to read.

这个库为ListsMapsSets等创建了静态的类。它们读起来更清晰、简单。

如果你使用的是Java 6或7,你可以使用Collections2类,该类提供类似 filter 和 transform 的方法。这些方法允许你在没有Java 8的stream的支持下写出流式的代码。

Guava也做一些简单的事情,如Joiner,就是通过分隔符连接字符串以及中断处理的类

Gson

Google的Gson库是一个简单快速处理JSON的库,它类似如下工作:

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

它使用起来确实很简单令人愉悦。Gson用户指南上有更多的例子。

Java Tuples

一件让我很烦恼的事情是Java没有内嵌建立元组的标准库。幸运的是,Java tuples项目解决了这个问题。它使用简单,而且效果很好:

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

Joda-Time

Joda-Time是我使用过的最简单的时间库。简单、直接且易于测试。你还能有什么要求?

你唯一的要求如果没使用Java 8,这样的话你就不能使用最新的日期时间库。

Lombok

Lombok是一个非常有趣的库。通过注解,可以让你减少模式化的代码,这种模式化代码让Java看起很不友好。

想要为你变量生成 setters 和 getters 方法吗?非常简单:

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

现在,你可以这样做了:

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

这个库还有更多的功能,虽然我还没有在生产环境中使用Lombok,但是我已经等不及要使用了。

Play框架

其它好的选择:JerseySpark

Java中关于RESTful的Web Service存在两个主要的阵营:JAX-RS和其它。

JAX-RS是传统的方式。通过联合注解和接口的实现的形式来实现Web Service,Jersey就是这样实现的。这样做的好处是可以很容易的建立客户端,只需要一个实现接口的类就行了。

Play框架使用非常不同的方式在JVM上建立Web Service:你有一个远程文件,然后你将一个类的引用写入到这个远程文件中。它的本质是一个完整的MVC框架,但是可以很简单使用它来做Rest Web Service。它对Java和Scala都是有效的。它刚开始是用于Scala,但是在Java中同样很好用。

SLF4J

存在有很多Java日志解决方案。我最喜欢的是SLF4J,因为它是一个可插入的且能同时联合许多不同的日志框架。你是否有一个奇怪的项目使用了java.util.logging、JCL以及log4j?如果是的话,那SLF4J非常适合你。

两页的手册完全能够满足入门要求。

jOOQ

我不喜欢重量级的ORM框架,因为我喜欢SQL。所以我写了许多JDBC模板,但是很难维护。jOOQ是一个更好的解决方案。

它使得你在Java里面编写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的全部功能。jUnit支持参数化的测试,支持一些规则可以让你不需要写那么多的模式化代码,支持随机测试指定代码的思想,支持假设

jMock

如果你已经使用了依赖注入,这是你付出代价的地方:模拟出能够产生副作用(类似于一个REST的服务器)的代码,并声明该代码调用时的行为。

在Java里,jMock是一个标准的模拟工具,它类似于这样:

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 方法一旦被同一个String调用,则dep的optionalCall 将被调用0次或多次。

如果你重复设置相同的依赖,可能需要将它们放到测试夹具(test fixture)中,并在@After夹具后添加assertIsSatisfied

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(&quot;some result&quot;, &quot;some other result&quot;)
                             .doesNotContain(&quot;shouldn&#039;t be here&quot;);

这个流式的接口使得你的测试可读性更强。还有什么是你需要的?

工具

IntelliJ IDEA

其它好的选择:EclipseNetbeans

最好的Java IDE是IntelliJ IDEA。它有很多的非常好的特性,确确实实的使得冗长的Java变得干脆利落。自动补全相当好用,检测功能也极为强大以及重构工具也非常的有帮助。

免费的版本对我来说已经足够,但是完整的版本存在更多优秀的特性,例如数据库工具、Spring框架的支持以及Chronon。

Chronon

我最喜欢的GDB7功能是在调试时可以即时返回(travel back)。如果你使用的是“旗舰版”,可以通过Chronon IntelliJ插件使用这个功能。

你可以获取历史变量、向后的步骤、历史方法等等。第一次使用的时候可能会觉得有点奇怪,但是它能帮助你调试出很多错综复杂的bug,Heisenbugs等工具与之类似的。

JRebel

持续集成通常的目标是软件即服务的产品。但是,如果你想不需要等到产品构建完成的时候就能看到代码的变化是什么?

这就是JRebel做的事情。一旦你将你的服务器挂上一个JRebel客户端,你可以实时的观察到服务器的变化。当你想快速试验的时候这会给你节省大量的时间。

Checker框架

Java的类型系统是非常脆弱的。它区分不是字符串,而是正则表达式,也不做污点检测(taint checking)。但是,Checker框架会这样做,并且还有其他。它使用像 @Nullable 这样的注解来做类型检查。你甚至能自定义注解,使得静态分析更加强大。

Eclipse Memory Analyzer

即使在Java中,内存泄露也是会发生的。幸运的是,有工具来检测内存泄露。我用过的最好的工具是Eclipse Memory Analyzer,它能从heap dump文件帮助你发现问题。

有多种方式来获取JVN进程的heap dump文件,我使用的是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大师。

书籍

播客

原文链接: Better Java 翻译: ImportNew.com - paddx
译文链接: http://www.importnew.com/16160.html
[ 转载请保留原文出处、译者和译文链接。]



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部