Java
Kotlin
IntelliJ
gradle

モダンなJava開発ガイド (2018年版)

2018年現在でもJava開発をしていると、Antすら使っていないEclipseプロジェクトにそこそこの頻度で出くわします。Eclipseの自動コンパイルが通ればOKであり、ビルドはExcel手順書をもとに手動で行われ、依存関係ライブラリはもちろんlibフォルダに各種jarファイルが放り込んであります。Eclipse上以外ではどう動かせば分かる人がいないため、コマンドラインからビルドなどを行うことは叶わず、CI化なんて夢のまた夢です。

そんなJava開発から脱却したい人向けのJava開発のモダン化ガイドです。

  • 基本的にJava 8以降での開発を想定しています。
  • 英語のIDE、ツール等は積極的に使用します。(英語嫌いだとモダン化は難しい)
  • Java開発全般を前提としているため、Web固有のもの等は除外しています。
  • Java向けに記載していますが、他言語でも大体の場合は似たものが用意されているため応用できます。

開発環境編

IDEにIntelliJを使用する

Eclipse固有のプラグイン等が必要でなければ、JetBrains社のIntelliJ IDEAをJava開発環境として使用しましょう。Community版はビジネス用途でも問題なく使用可能です。

https://www.jetbrains.com/idea/

  • Windows/Mac/Linuxのインストールが簡単
  • IDEとしての各種機能およびプラグインが充実している (プラグインはさすがにEclipseには劣る)
  • Maven、Gradle、GitなどがIDEに標準で組み込まれている
  • Eclipseで経験するような設定周り、プラグイン周りの不具合等が少ない
  • Android Studio、PyCharm、WebStorm等でも主要なショートカットや機能を活用できる

Eclipseはプラグイン周りやネットワークプロキシ等の設定周りでトラブルが多いので、必要な機能なプラグインが無い限りは避けた方がよいです。

NOTE: プラグイン周りの状況が劇的に改善しているのであれば、コメントに書いていただけると幸いです。

プロジェクト管理にMaven/Gradleを使用する

プロジェクトはMavenまたはGradleで管理しましょう。機能面やプラグインの豊富さなどからGradleから選ぶことをお勧めします。

NOTE: 大規模プロジェクトではMavenの方が管理しやすいという人も一定数いるため、そういった理由が明確ならMavenを選びましょう。GradleはGroovyベースのDSL言語のため、プロジェクトファイルであるbuild.gradleがカオスな状態になりやすいです。

  • Gradle WrapperによるGradle本体の自動ダウンロード&インストール
    • 事前にGradle本体のインストールする必要がない
  • プロジェクト単位にGradleのbuild.gradleを配置して管理
    • ビルド、テスト、デプロイ等を全て記述できる (デフォルトで間に合うことも多い)
    • Java/Kotlin共存のプロジェクトの設定、依存関係ダウンロード等も全て自動的に行ってくれる
  • 依存ライブラリは自動ダウンロードされる
  • プラグインが豊富
    • コード品質チェック、ビルド、リリース等の各種作業を自動化できる
  • IntelliJ IDEAと相性が良い
    • フォルダ構成、依存ライブラリ等のプロジェクトを自動構築してくれる

IDE固有の設定(保存時の動作、コーディングフォーマット等)はEclipseやIntelliJ IDEAのプロジェクト関連ファイルを利用すべきですが、それ以外のビルドやCI周りは基本的にGradleベースにしましょう。

Code Formatterを使用する

コーディング規約の自動適用のためにCode Formatterを使用しましょう。コーディング規約はJava8のラムダ式等の新しめの言語仕様にも対応しているGoogle Java Styleがお勧めです。

https://google.github.io/styleguide/javaguide.html

IntelliJでソースコードを保存した時にコーディング規約に基づいたコードフォーマットを実施したい場合は、以下のプラグインを組み合わせるのがお勧めです。

なおコーディング規約に従っているかのチェックは、後述するCheck Styleを使用することで可能です。

Linter/Static Code Checkerを使用する

Javaには色々なLint/静的解析ツールがありますが、費用対効果の面で以下の2つを入れることをお勧めします。中級レベルくらいのJava開発者よりもしっかりした指摘をしてくれます。対面コードレビューはこういった静的解析ツールが扱わない観点で行いましょう。

  • Error Prone
    • Google製のJavaコンパイル時のチェックを強化するツール。
  • SpotBugs + fb-contrib
    • バイトコードレベルでエラーの可能性が高い実装を検出する。有益な指摘が多い。
    • SpotBugsはFindBugsの後継。(FindBugsはメンテされなくなっているため、今後はSpotBugsを使用するのがお勧め)
    • fb-contribはFindBugsのチェックルールを追加するプラグイン。

さらにチェックを強化したい場合は、上記にPMDCheck Styleを加えることをお勧めします。ただし、以下に理由により優先度はやや低くてもいいかもしれません。

  • Error-ProneSpotBugs + fb-contribと比較して、PMDはかなり細かいところまでチェック&指摘を行う。このためやや改修に対する費用対効果が薄いかもしれない。ただし循環的複雑度(Cyclomatic Complexity)チェックのような有益なものもあるため、チェックルールを絞って運用すると良い。
  • 前述したCode Formatterを既に適用している場合、Check Styleの指摘事項は少ない。ただしCIでの自動チェックやJavaDoc構文チェック等もしたい場合は入れることをお勧めする。

上記以外のも以下のようなツールがあるので、必要に応じて積極的に使用していきましょう。

  • Infer - Facebook製の静的解析ツールである。並行性バグ解析チェックを行うRacerDも含まれています。
  • OWASP Dependency-Check - Java依存ライブラリの脆弱性チェックツール。サードパーティライブラリ周りのセキュリティも強化したい場合はお勧めです。

コーディング編

Java 7/8の新機能を使用する

Javaもバージョンアップに伴い便利な言語機能が少しずつ増えています。これらの言語機能を活用することにより、色々な処理がより簡潔かつ堅牢に記述できるようになるので積極的に使いましょう。

ちなみに上述したError-ProneSpotBugs + fb-contribでは、ここで紹介するJava7/8の新機能の一部を使用することを推奨するような指摘もしてくれます。

Stringのswitch構文 (Java7以降)

Stringの条件分岐をif/elseではなくswitch構文で記述可能です。

switch (s) {
case "Taro":
    System.out.println("Taro");
    break;
case "Hanako":
    System.out.println("Hanako");
    break;
}

ちなみにStringのswitch構文の計算量はO(1)のため、if/elseによるO(N)よりもパフォーマンスも良いです。詳細については下記URLが参考になります。

How is String in switch statement more efficient than corresponding if-else statement?

try-catch-resource構文 (Java7以降)

ストリーム等のクローズ漏れを回避するために、従来のJavaではfinally句にクローズ処理を記述していました。

しかしJava7以降では以下のtry-catch-resource構文を使用することで、finally句にクローズ処理を書かなくてもクローズ処理が実行されることが保証されます。Pythonにおけるwity構文です。

try (FileReader fr = new FileReader(path)) {
    return fr.readLine();
}

ちなみにtry(...)内はセミコロンで区切ることにより、複数のReader/Writer等を記述することも可能です。

try (FileReader fr = new FileReader(inPath);
     FileWriter fw = new FileWriter(outPath)) {
    // 処理を記述する
}

Stream API (Java8以降)

NOTE: KotlinではSequenceとして同じようなものが用意されており、さらに便利になってます

コレクションをfor文で回す時、単純にある条件に合致した要素を抽出したり、別の要素に変換したりすることが多いと思います。このようなケースではfor文によるImperativeな実装ではなくStream APIによるDeclarativeな実装にしましょう

以下は「aを含む単語の最も長い文字数を求める」例をforループとStream APIの両方で実装した例です。慣れてないと違和感があるかもしれませんが、Stream APIの方がコレクションをどのように処理しているかが分かりやすいと思います。

List<String> words = Arrays.asList("apple", "taro", "longest");

// forループの場合 (Imperative)
int maxLength = -1;
for(String word : words) {
    if (word.contains("a")) {
        if (word.length() > maxLength ) {
            maxLength = word.length();
        }
    }
}

// Stream APIの場合 (Declarative)
OptionalInt maxLength = words.stream()
        .filter(s -> s.contains("a"))
        .mapToInt(String::length)
        .max();

Stream APIの強みを少し実感するために、上記の例の文字数の条件に「単語の文字数が奇数のもののみを対象とする」を加えてみましょう。以下のようコードになります。この時点でforループの方はやや手に余る記述になりつつありますが、Stream APIの方は十分にコントロールできる記述を維持できています。これ以上は書きませんが、条件を3,4個追加すればその差はさらに歴然としたものになります。またStream APIの場合は簡単に「最小値を求めたいからmax()をmin()に変更」したり「平均を求めたいからmax()をaverage()に変更」したりできます。

List<String> words = Arrays.asList("apple", "taro", "longest");

// forループの場合 (Imperative)
int maxLength = -1;
for(String word : words) {
    if (word.contains("a")) {
        // *** 偶数checkのAND条件を追加 ***
        if (word.length() > maxLength && word.length() % 2 == 0) {
            maxLength = word.length();
        }
    }
}

// Stream APIの場合 (Declarative)
OptionalInt maxLength = words.stream()
        .filter(s -> s.contains("a"))
        .mapToInt(String::length)
        .filter(n -> n % 2 == 0)  // *** 偶数checkのフィルタを追加 ***
        .max();

ちなみにIntelliJではStream APIのmapfiltercollectの引数等まで自動補完などが働くためとても便利です。

ラムダ式 (Java8以降)

NOTE: Kotlinはさらに便利になっており、Rubyっぽいことが色々できます

Java8より前はRunnable等の処理を定義する時には無名クラスを必ず用意する必要がありましたが、Java8以降ではラムダ式によってこういったアドホック的な無名クラスを排除できます。

// 無名クラスの場合
Runnable runner = new Runnable() {
  public void run() {
    System.out.println("Do something...");
  }
};

// ラムダ式の場合
Runnable runner = () -> {System.out.println("Do something...");};

これだけだと大きなメリットには感じらないかもしれません。しかし無名クラスを大量に作る傾向にある非同期処理プログラムやAndroid開発などでは、記述が簡潔になり可読性が大幅に向上するため後々の保守コストの削減に地味に効いてきます。

また前項目で説明したStream APIでもfilterメソッド等にラムダ式を使用ており、Javaでちょっとした関数型プログラミングを行う場合にも重宝します。

IntelliJではラムダ式が入力可能な箇所で補完を試みると、適切なラムダ式の補完候補(適切な数の引数、関数ブロックなど)が表示されます。Stream APIと同様でとても便利です。

Lombokを使用する

NOTE: Kotlinを使う場合はLombokはほぼ不要ですが、KotlinのデータクラスみたいなものをJavaで欲しい場合はLombokを使いましょう。

Lombokはアノテーションを付けるだけで、Javaのコンストラクタ、Getter、Setter等を自動生成してくれるJavaライブラリです。

Lombok

Javaでデータ用クラスを作る時にSetter/Getter、デフォルトコンストラクタ、全コンストラクタを何度も何度も作る場合は、Lombokを使用することをお勧めします。特にDB関連のフレームワーク等を使う時にデータアクセス関連クラス等を定義する場合は、Lombokを使用することにより宣言的にクラスを定義することができます。

IntelliJやEclipseではLombokアノテーションにより自動生成される各メソッド(Setter, Getter, コンストラクタ等)の補完、コンパイルチェックまで適切に行ってくれるようになるプラグインも提供されています。このためLombokを使用していても、IDE上で静的チェックが適切に行われます。

IntelliJをLombokに対応させる

なお、プロダクトコードにKotlinを併用することに抵抗がない場合はKotlinでデータクラス等を作成するのもお勧めです。Kotlinについては後述します。

Guavaを使用する

GuavaはGoogle製のJavaコアライブラリです。

各種コレクション、不変(Immuitable)コレクション、関数型タイプ、I/O、文字列処理、並列処理ユーティリティ等の便利なライブラリが一通り揃っています。

NOTE: Apache Commonsと比較すると、Guavaの方がよりもモダンで、より高いレベルで汎用化されたライブラリが多いです。Apache Commonsの方がEメール、CLI、Net、Logging、DB等のより実用性に直結するライブラリが揃っているため、利用用途が完全に重なるわけではありません。

他項目で紹介したError-Proneは不変性などに関する指摘も行っており、この時にはGuavaによるImmutable関連コレクションの使用を推奨していたりします。このため自分達のプロダクトコードでGuavaをどう使用していいかイメージがつかない場合でも、より堅牢なコードを実現するためにGuavaは導入しておくことをお勧めします。

JVM言語にKotlinを使用する (またはJavaと併用する)

KotlinはJetBrains社が開発したJVM言語です。Javaと同等のパフォーマンスをもち、Javaとの完全な相互運用が可能です。JavaからKotlinを使用する場合は、Kotlinに@JvmStatic等のアノテーションを付ける必要がありますが、KotlinからJavaを使用する場合はあたかもKotlinのライブラリのように利用できます。

Kotlinを使用する大きな利点の一つは、Javaの静的型付けを保ったままPythonやRuby等にあるような便利な機能や構文を利用できるため、コードを短く簡潔に記述しやすい点だと思います。Javaでは各種ライブラリやフレームワークを使わないと実装やメンテナンスが困難な処理で「PythonやRubyなら簡潔に書けるのに」と思うような処理もKotlinなら簡単に書けることが多いです。また一部デザインパターン等はそもそもKotlinでは不要となるため、Javaよりも設計がシンプルになる場合もあります。

NOTE: それ以外にもKotlinのクラスのインタフェース、継承などの関係をより明確に記述できるキーワード(ex. data class, open, sealed, lateinit , override, internal, object, companion object)が追加されているため、より明確に設計と実装を行えるという利点があります。大規模開発に携わる人の場合は、こちらの利点の方がより魅力的に感じる人が多いかもしれません。

Java8以降の登場によりJavaも色々と改善されましたが、Better JavaとしてはKotlinが何年も先を進んでいます。Kotlinを使用する場合は、これまで紹介したJava8の新機能、Lombokライブラリ等はほとんど必要なくなります。

NOTE: KotlinでString、Integer、File、InputStream、OutputStreamなどのインスタンスのメソッド補完候補を眺めてみましょう。「こんなものがあるのか!」とちょっとだけ感動します。

IntelliJとGradleを使用することにより、JavaとKotlinは同じプロジェクトの中で併用することができます。このためJavaでは記述がとても冗長になったり複雑になってしまうような実装だけをKotlinで実装して段階的にKotlinを適用するような開発も可能です。

  • Nullを排除できる
  • Javaとほぼ同等のパフォーマンス (Groovy, Scalaなどは基本的に性能が下がる)
  • テンプレート文字列による文字列への変数埋め込み
  • 文字列操作、ファイル/ストリーム操作、コレクション操作が充実している
  • データクラス、プロパティによりset/getの冗長な記述を排除できる
  • メソッドのデフォルト引数を設定できる
  • メソッド呼び出し時の引数名を明示できる
  • コルーチンによる並列処理のasync/await対応

一点注意して欲しいのは、今後もKotlinがJavaを超える主流言語になることはまずないという事実です。このため開発要員などの問題からプロダクトコードには導入しづらいと感じる人もいるかもしれません。その場合はテストコードはツール等のみをKotlinで実装するのがお勧めです。(テスト編で後述しています)

テスト編

JVM言語にKotlinを使用する

Gradleプロジェクトではプロダクトコード(src/main配下)とテストコード(src/test配下)でJavaとKotlinを使い分けることができます。このため「プロダクトコードはJavaで実装し、テストコードにはKotlinで実装する」という形で開発を進めることができます

テストコードをKotlinで記述することにより、以下の恩恵を受けることができます。

  • データクラスにより、テスト条件などのデータ化が簡単になる
  • テンプレート文字列でテストケース向け文字列が簡単になる
    • いくつかの変数を置き換えるだけのJSONとかなら、外部ファイルやテンプレートエンジンが不要になる
  • デフォルト引数によりテストケース向けクラスやメソッド等の拡張や管理が簡単になる
    • Javaのようにオーバロードで継ぎ足す必要がなく、Builderパターンもあまり必要なくなる
  • コレクション操作、ファイル操作などが簡単になる
    • GuavaやCommonsを利用すればJavaでもある程度は実現可能

テストツールにJUnit5を使用する

2017年にJUnit5が正式リリースされました。基本機能の拡充、Java8機能への対応、プラグインによる拡張が可能な設計などが加わったことにより便利になっています。

  • @EachBefore, @Test, @EachAfter等の前後の処理を記述可能となった
    • JUnit4ではRuleによりある程度は記述できたが、ここまで細かい粒度では記述ができなかった
  • パラメータテスト等に対応 (ただしプラグインのJavaライブラリを引き込む必要あり)
  • Gradleはv4.6以降でJUnit5に正式対応
  • Jupyter Vintageを使用することによりJUnit4との共存が可能たため、テストケースを段階的に移行することもできる

モックツールにMockito v2を使用する

NOTE: あまりモックを大量に使用するような実装、テストにならないようにしましょう。

Javaのモックツールは基本的にMockito(Version 2)を使用しましょう。

NOTE: JMockitというモック周りの機能がより強力なモックライブラリもありますが、APIの仕様変更が多くdeprecatedになったAPIは油断するとすぐに削除されます。後方互換性を重視するライブラリが多いJavaの世界ではなかなかの風雲児的な存在です。このため定期的にバージョンアップして最新のバグFixや機能追加の恩恵を受けたい人には向かないかもしれません。「モック化するためにJMockitのこの機能が必須」といった強い理由がない限りは避けましょう。採用した場合でもバージョンアップは慎重に行いましょう。

以下がMockitoの簡単な例です。最近のJavaによくあるメソッドチェイニングでモックを定義する形になっていますね。これ以外にも@Mockのようなアノテーション指定による記述も可能なので、適切なものを選択しましょう。

Mockito
import static org.mockito.Mockito.*;

// モックを準備
LinkedList mockedList = mock(LinkedList.class);
when(mockedList.get(0))
  .thenReturn("first");

// モックを使用
System.out.println(mockedList.get(0));

おまけ:Javaが時代遅れという誤解

日本のSIer業界を中心に長らくトップに君臨してきたためかJavaは「時代遅れ」とか「遅い」とか悪いイメージが多いようです。しかしJavaは今でも最も強力なプログラミング言語のひとつです。

JavaはJVM仮想マシンと強力なエコシステムを軸とした最も強力なプラットフォームの一つです。JVM(Java仮想マシン)は高速かつ堅牢かつ高機能な実行環境ですし、Groovy/Scala/Kotlin/Clojure等の各種JVM言語を使用することができます。各種ライブラリ・フレームワークは豊富で完成度の高いものが多いですし、IDE等の開発ツールの充実度も群を抜いています。何よりBattle Testedと形容されるその実績と信頼性を一般の開発者が利用できるメリットは計り知れません。

私は小規模なプログラムやツール開発には主にPythonを使用していますが、性能も安定性も重要なシステムでは基本的にJavaを選択することが多いです。極めて高い効率と性能が要求される場合はC/C++やGolang等も選択肢に入ることがありますが、多くの場合はJavaプラットフォームで目標を達成することができます。

そんな素敵なJavaですが、インターネット上ではすこぶる評判が悪いです。おそらく以下のようなJava暗黒時代の副産物によるものでしょう。

  • Javaの醜いエンタープライズ要素に触れ続けてきた
    • J2EEフレームワーク等のXML地獄
    • 無意味または過剰なデザインパターン
    • 上記をさらに煮詰めて凝縮したSIer独自フレームワーク
  • レベルの低い開発チームでしか開発したことがない
    • Java1.4時代の開発で止まっている (Genericsがない時代)
    • Gradle/MavenどころかAntすら使わない
    • 低品質な車輪の再発明
    • コマンドラインを知らず、IDEが提供する機能に頼り切りの開発

ちなみに日本の大手企業の開発現場だと、未だにこれらに該当するようなプロジェクトは珍しくありません。これではJavaの評判が良くなるわけがありませんね。

しかし実際にはJavaプラットフォームと開発環境周りは着実に進化しています。

  • JVMの進化 (Parallel Full GC for G1、Docker対応等)
  • Javaと相互運用が可能なJVM言語の登場 (Groovy, Scala, Clojure, Kotlin等)
  • Java言語の進化 (Stream API, ラムダ式、型推論等)
  • Android開発の台頭による新たな開発モデルの登場 (RxJava, Kotlin等)
  • 開発、ビルドツールの進化 (IntelliJ, Maven, Gradle等)
  • Web開発の進化 (Spring Boot, Play, Grails, Dropwizard等)
  • 開発向けライブラリの進化 (Guava, Guice, Lombok等)
  • 非同期型プログラム開発の進化 (Akka, Netty, Vert.x等)
  • テストライブラリの進化 (JUnit5, Mockito2, Spock, Cucumber等)

新たに台頭しているこれらの勢力は、これまでのJavaやフレームワーク等の問題点(過剰なデザインパターン、XML地獄)を解消してよりモダンな開発が可能となっていることが多いです。おそらく初級~中級レベルくらいのJava開発者であれば、不満の多くはIntelliJ + Gradle + Kotlinで解消するのではないかと思ってます。

TODO

全体を広く浅く記載しているため、時間があるときに下記トピックを追加、補強する予定。

  • 各種ツール等を利用する時のbuild.gradleの記述例、統合の仕方
  • テスト系はほぼ結論のみとなっているため、テストツール周りの機能、メリット等を追記
  • 各トピックの参考サイトURLを掲載する