注意:我正在编写本指南的第二版,需要您的帮助!请使用此表格向我提供您认为下一版本应包含哪些内容的反馈。谢谢!
Java 是最流行的编程语言之一,但似乎没有人喜欢使用它。嗯,Java 实际上是一种不错的编程语言,自从 Java 8 最近发布以来,我决定编译一个库、实践和工具列表,以便更好地使用 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,这将在有关不变性的部分中讨论。
如果您想要为其构建结构的相当复杂的对象,请考虑构建器模式。
您创建一个静态内部类来构造您的对象。它使用可变状态,但是一旦您调用构建,它就会发出一个不可变对象。
想象一下我们有一个更复杂的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 编码模式的一些问题的有趣讨论。
一些出色的代码生成库是不可变的、Google 的自动值和 Lombok。
如果需要的话,应谨慎使用受检查的异常。它们迫使您的用户添加许多 try/catch 块并将您的异常包装在自己的异常中。更好的方法是让您的异常扩展 RuntimeException。这允许您的用户以他们想要的方式处理您的异常,而不是强迫他们每次处理/声明它都会抛出,这会污染代码。
一个巧妙的技巧是将 RuntimeExceptions 放入方法的 throws 声明中。这对编译器没有影响,但会通过文档通知用户可以抛出这些异常。
这更像是软件工程部分,而不是 Java 部分,但编写可测试软件的最佳方法之一是使用依赖项注入 (DI)。因为Java强烈鼓励OO设计,所以要制作可测试的软件,就需要使用DI。
在 Java 中,这通常是通过 Spring 框架完成的。它具有基于代码的连接或基于 XML 配置的连接。如果您使用 XML 配置,请务必不要过度使用 Spring,因为它是基于 XML 的配置格式。 XML 中绝对不应该有任何逻辑或控制结构。它应该只注入依赖项。
使用 Spring 的良好替代方案是 Google 和 Square 的 Dagger 库或 Google 的 Guice。他们不使用Spring的XML配置文件格式,而是将注入逻辑放在注释和代码中。
尽可能避免使用空值。当您应该返回空集合时,不要返回空集合。如果要使用 null,请考虑 @Nullable 注释。 IntelliJ IDEA 内置了对 @Nullable 注释的支持。
在计算机科学中最严重的错误中阅读有关为什么不使用空值的更多信息。
如果您使用 Java 8,则可以使用出色的新可选类型。如果某个值可能存在也可能不存在,请将其包装在可选类中,如下所示:
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 );
这比链式 if null 检查要好得多。使用Optional的唯一缺点是标准库没有良好的Optional支持,因此仍然需要处理空值。
除非你有充分的理由这样做,否则变量、类和集合应该是不可变的。
可以使用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 类。它们具有构建器,以便您可以动态构建它们,然后通过调用构建方法将它们标记为不可变。
应该通过声明字段不可变(通过final )和使用不可变集合来使类不可变。或者,您可以将类本身设置为最终类,以便它无法扩展或可变。
如果您发现自己向 Util 类添加了很多方法,请务必小心。
public class MiscUtil {
public static String frobnicateString ( String base , int times ) {
// ... etc
}
public static void throwIfCondition ( boolean condition , String msg ) {
// ... etc
}
}
这些类乍一看很有吸引力,因为其中的方法并不真正属于任何一个地方。所以你以代码重用的名义将它们全部扔在这里。
治疗方法比疾病本身更糟糕。将这些类放在它们所属的位置并积极重构。不要将类、包或库命名得太通用,例如“MiscUtils”或“ExtrasLibrary”。这鼓励在那里转储不相关的代码。
格式化并不像大多数程序员想象的那么重要。一致性是否表明你关心你的手艺并且它是否有助于其他人阅读?绝对地。但是,我们不要浪费一天的时间向 if 块添加空格以使其“匹配”。
如果您确实需要代码格式化指南,我强烈推荐 Google 的 Java 样式指南。该指南最好的部分是编程实践部分。绝对值得一读。
记录面向用户的代码很重要。这意味着使用示例并使用变量、方法和类的合理描述。
这样做的必然结果是不记录不需要记录的内容。如果您对参数是什么没有什么可说的,或者如果它很明显,请不要记录它。样板文档比没有文档更糟糕,因为它会欺骗用户认为有文档。
Java 8 有一个很好的流和 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 ());
}
}
这使您可以编写更流畅的代码,更具可读性。
正确部署 Java 可能有点棘手。如今部署 Java 有两种主要方法:使用框架或使用更灵活的自行开发的解决方案。
由于部署 Java 并不容易,因此已经开发出了可以提供帮助的框架。其中最好的两个是 Dropwizard 和 Spring Boot。 Play 框架也可以被视为这些部署框架之一。
他们所有人都试图降低代码发布的障碍。如果您是 Java 新手或者需要快速完成工作,它们会特别有用。单个 JAR 部署比复杂的 WAR 或 EAR 部署更容易。
然而,它们可能有些不灵活并且相当固执己见,因此如果您的项目不适合框架开发人员所做的选择,您将不得不迁移到更手动的配置。
不错的选择:Gradle。
Maven 仍然是构建、打包和运行测试的标准工具。还有其他替代方案,例如 Gradle,但它们的采用程度不如 Maven。如果您是 Maven 新手,您应该从 Maven by Examples 开始。
我喜欢拥有一个根 POM,其中包含您想要使用的所有外部依赖项。它看起来像这样。这个根 POM 只有一个外部依赖项,但如果您的产品足够大,您将有数十个。您的根 POM 应该是一个独立的项目:在版本控制中并像任何其他 Java 项目一样发布。
如果您认为为每个外部依赖项更改标记根 POM 太多,那么您就没有浪费一周时间来跟踪跨项目依赖项错误。
您的所有 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.0
Widget library depends on Bar library v0.9
哪个版本将被拉入您的项目?
使用 Maven 依赖聚合插件,如果您的依赖项不使用相同的版本,构建将会出错。然后,您有两种解决冲突的选择:
选择哪一个取决于您的情况:如果您想跟踪一个项目的版本,那么排除是有意义的。另一方面,如果您想明确说明它,您可以选择一个版本,尽管您在更新其他依赖项时需要更新它。
显然,您需要某种持续集成服务器,它将持续构建您的快照版本和基于 git 标签的标签构建。
Jenkins 和 Travis-CI 是自然的选择。
代码覆盖率很有用,Cobertura 有良好的 Maven 插件和 CI 支持。还有其他适用于 Java 的代码覆盖率工具,但我使用了 Cobertura。
您需要一个地方来放置您制作的 JAR、WAR 和 EAR,因此您需要一个存储库。
常见的选择是 Artifactory 和 Nexus。两者都有效,并且各有优缺点。
您应该有自己的 Artifactory/Nexus 安装并将您的依赖项镜像到它上面。这将阻止您的构建因某些上游 Maven 存储库崩溃而中断。
现在您已经编译了代码,设置了存储库,并且需要将代码放入开发环境中并最终将其投入生产。不要在这里吝啬,因为自动化将在很长一段时间内带来红利。
Chef、Puppet 和 Ansible 是典型的选择。我写了一个名为 Squadron 的替代方案,当然,我认为您应该检查一下,因为它比其他替代方案更容易正确。
无论您选择什么工具,都不要忘记自动化部署。
Java 的最佳特性可能是它拥有大量的库。这是一小部分库,可能适用于最大的人群。
Java 的标准库曾经向前迈出了惊人的一步,但现在看起来缺少几个关键功能。
Apache Commons 项目有很多有用的库。
Commons Codec对于 Base64 和十六进制字符串有许多有用的编码/解码方法。不要浪费时间重写这些。
Commons Lang是字符串操作和创建、字符集和一堆杂项实用方法的首选库。
Commons IO拥有您可能想要的所有与文件相关的方法。它有 FileUtils.copyDirectory、FileUtils.writeStringToFile、IOUtils.readLines 等等。
Guava 是 Google 的一个优秀的 Java 缺失库。几乎很难提炼出这个库中我喜欢的所有内容,但我会尝试。
缓存是一种获取内存缓存的简单方法,可用于缓存网络访问、磁盘访问、记忆功能或任何其他内容。只需实现一个 CacheBuilder 来告诉 Guava 如何构建缓存,就可以了!
不可变的集合。其中有很多:ImmutableMap、ImmutableList,甚至 ImmutableSortedMultiSet(如果这是您的风格)。
我也喜欢用 Guava 方式编写可变集合:
// Instead of
final Map < String , Widget > map = new HashMap <>();
// You can use
final Map < String , Widget > map = Maps . newHashMap ();
列表、映射、集合等都有静态类。它们更干净、更容易阅读。
如果您无法使用 Java 6 或 7,则可以使用 Collections2 类,它具有过滤器和转换等方法。它们允许您在没有 Java 8 流支持的情况下编写流畅的代码。
Guava 也有简单的东西,比如一个连接分隔符上的字符串的Joiner和一个通过忽略中断来处理中断的类。
Google的Gson库是一个简单快速的JSON解析库。它的工作原理如下:
final Gson gson = new Gson ();
final String json = gson . toJson ( fooWidget );
final FooWidget newFooWidget = gson . fromJson ( json , FooWidget . class );
与它一起工作真的很容易并且很愉快。 Gson 用户指南还有更多示例。
我对 Java 的持续烦恼之一是它没有内置到标准库中的元组。幸运的是,Java 元组项目解决了这个问题。
它使用简单并且效果很好:
Pair < String , Integer > func ( String input ) {
// something...
return Pair . with ( stringResult , intResult );
}
Javaslang 是一个函数库,旨在添加本应属于 Java 8 一部分的缺失功能。其中一些功能是
有几个 Java 库依赖于原始 Java 集合。这些受到限制,以保持与以面向对象为重点创建并设计为可变的类兼容。 Java 的 Javaslang 集合是一个全新的版本,其灵感来自 Haskell、Clojure 和 Scala。它们的创建以功能为重点,并遵循不可变的设计。
像这样的代码自动是线程安全的并且无需 try-catch:
// Success/Failure containing the result/exception
public static Try < User > getUser ( int userId ) {
return Try . of (() -> DB . findUser ( userId ))
. recover ( x -> Match . of ( x )
. whenType ( RemoteException . class ). then ( e -> ...)
. whenType ( SQLException . class ). then ( e -> ...));
}
// Thread-safe, reusable collections
public static List < String > sayByeBye () {
return List . of ( "bye, " bye ", "collect" , "mania" )
. map ( String :: toUpperCase )
. intersperse ( " " );
}
Joda-Time 无疑是我用过的最好的时间库。简单、直接、易于测试。你还能要求什么?
仅当您尚未使用 Java 8 时才需要它,因为 Java 8 有自己的新时间库,而且还不错。
龙目岛是一个有趣的图书馆。通过注释,它可以让您减少 Java 所遭受的严重的样板文件。
想要类变量的 setter 和 getter 吗?简单的:
public class Foo {
@ Getter @ Setter private int var ;
}
现在你可以这样做:
final Foo foo = new Foo ();
foo . setVar ( 5 );
还有更多。我还没有在生产中使用 Lombok,但我已经等不及了。
不错的选择:Jersey 或 Spark
使用 Java 实现 RESTful Web 服务有两个主要阵营:JAX-RS 和其他。
JAX-RS是传统方式。您可以使用 Jersey 之类的工具将注释与接口和实现结合起来形成 Web 服务。这样做的好处是您可以轻松地仅使用接口类来创建客户端。
Play 框架与 JVM 上的 Web 服务完全不同:您有一个路由文件,然后编写这些路由中引用的类。它实际上是一个完整的 MVC 框架,但您可以轻松地将它用于 REST Web 服务。
它适用于 Java 和 Scala。它因 Scala 优先而受到一些影响,但在 Java 中使用它仍然很好。
如果您习惯使用 Python 中的 Flask 等微框架,那么 Spark 将会非常熟悉。它与 Java 8 配合得特别好。
有很多 Java 日志记录解决方案。我最喜欢的是 SLF4J,因为它具有极高的可插入性,并且可以同时组合来自许多不同日志框架的日志。有一个使用 java.util.logging、JCL 和 log4j 的奇怪项目吗? SLF4J 适合您。
两页的手册几乎是您入门所需的全部内容。
我不喜欢沉重的 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 模式,您可以使数据库访问变得轻而易举。
测试对于您的软件至关重要。这些软件包有助于使事情变得更容易。
不错的选择:TestNG。
jUnit 无需介绍。它是 Java 单元测试的标准工具。
但您可能没有充分利用 jUnit 的潜力。 jUnit 支持参数化测试、阻止您编写大量样板文件的规则、随机测试某些代码的理论和假设。
如果您已经完成了依赖注入,那么这就是它的回报:模拟具有副作用的代码(例如与 REST 服务器通信),并仍然断言调用它的代码的行为。
jMock 是 Java 的标准模拟工具。它看起来像这样:
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方法将被调用零次或多次。
如果您必须一遍又一遍地设置相同的依赖项,您可能应该将其放入测试装置中,并将assertIsSatisfied放入@After装置中。
你用过 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" );
这种流畅的界面使您的测试更具可读性。你还想要什么?
不错的选择:Eclipse 和 Netbeans
最好的 Java IDE 是 IntelliJ IDEA。它具有大量令人敬畏的功能,并且确实是使 Java 的冗长变得可以忍受的主要因素。自动完成很棒,检查是一流的,并且重构工具非常有帮助。
免费社区版对我来说已经足够好了,但终极版中有很多很棒的功能,如数据库工具、Spring 框架支持和 Chronon。
我最喜欢的 GDB 7 功能之一是调试时能够回到过去。当您获得终极版时,可以使用 Chronon IntelliJ 插件来实现这一点。
您可以获得变量历史记录、后退、方法历史记录等等。第一次使用有点奇怪,但是它可以帮助调试一些非常复杂的错误,Heisenbugs之类的。
不错的选择:DCEVM
持续集成通常是软件即服务产品的目标。如果您甚至不需要等待构建完成即可实时查看代码更改怎么办?
这就是 JRebel 所做的。将服务器连接到 JRebel 客户端后,您可以立即看到服务器上的更改。当您想快速进行实验时,这可以节省大量时间。
Java 的类型系统相当弱。它不区分字符串和实际上是正则表达式的字符串,也不进行任何污点检查。然而,Checker 框架可以做到这一点以及更多。
它使用@Nullable等注释来检查类型。您甚至可以定义自己的注释,使静态分析变得更加强大。
即使遵循最佳实践,即使是最好的开发人员也会犯错误。您可以使用许多工具来验证 Java 代码以检测代码中的问题。以下是一些最流行工具的一小部分选择。其中许多与流行的 IDE(例如 Eclipse 或 IntelliJ)集成,使您能够更快地发现代码中的错误。
除了在开发过程中使用这些工具之外,在构建阶段运行它们通常也是一个好主意。它们可以绑定到 Maven 或 Gradle 等构建工具,也可以绑定到持续集成工具。
即使在 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
然后您可以使用内存分析器打开heapdump.hprof文件并快速查看发生了什么。
帮助您成为 Java 高手的资源。