Java
Kotlin
IntelliJ
gradle

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

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

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

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

開発環境編

IDEにIntelliJを使用する

Eclipse固有のプラグイン等が必要でなければ、JetBrains社のIntelliJ IDEAをJava開発環境として使用しましょう。
Community版はビジネス用途でも問題なく使用可能です。(詳細は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開発者よりもしっかりした指摘をしてくれます。対面コードレビューはこういった静的解析ツールが扱わない観点(ex. 設計内容通りの実装か、DRY/SLAP等の原則に則っているか)で行いましょう。

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

さらにチェックを強化したい場合は、上記の2つに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-with-resources構文 (Java7以降)

従来のJavaではストリーム等のクローズ漏れを回避するためにfinally句にクローズ処理を記述する必要がありました。
しかしJava7以降では以下のtry-with-resources構文を使用することで、finally句にクローズ処理を書かなくてもクローズ処理が実行されることが保証されます。Pythonにおけるwith構文みたいなものです。

try (FileReader fr = new FileReader(path)) {
    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();

Java8で追加された機能の中でも「宣言的であるが故に処理の追加や変更が容易であり、実装の意図も伝えやすい」という特徴をもつStream APIとても重要な機能の一つです。既存のforループ処理で置き換えられるところは積極的に置き換えていくことをお勧めします。

ちなみに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のライブラリのように利用できます。以下に主な特徴を挙げています。

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

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

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

特にJava8以降ではJavaも色々と改善されていますが、それでもBetter JavaとしてはKotlinが何年も先を進んでいます。これまで紹介したJava7/8の新機能やLombokライブラリ等の機能は、Kotlin自身がサポートしているためほとんど不要となります。

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

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

一点注意して欲しいのは、以下に挙げる理由のためOpenJDK/OracleJDKのJVM環境では今後もKotlinがJavaを超える主流言語になる可能性は極めて低いという事実です。このため開発要員などの問題からプロダクトコードには導入しづらいと感じる人もいるかもしれません。その場合はテストコードやツール等をKotlinで実装するのがお勧めです。

  • Oracleの公式サポートがない。(補足: AndroidではKotlinはGoogle公式サポート言語です)
  • Kotlinのみで利用可能なキラーコンテンツ的なライブラリ等がない。
    • 基本的にJavaからでもKotlinのライブラリ等にアクセスできるため。
  • 「JavaからKotlinの乗り換え」の場合は「CからJavaの乗り換え」みたいな大きなメリットが必ずしもあるわけではない。
    • ex. マルチプラットフォーム対応、メモリ管理、IDE等の豊富な開発ツール
  • 既存のJava向け開発ツールや開発標準等が使えなくなる可能性。

NOTE: AndroidではKotlinはGoogle公式サポート言語となっています。このためAndroidについてはKotlinがJavaを超える主流言語になる可能性は十分にあります。

Kotlinを用いたテストについてはテスト編に記載しています。

テスト編

JVM言語にKotlinを使用する

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

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

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

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

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

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

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

NOTE: モックを大量に使用するような実装、テストにならないように心がけましょう。大量のモックに依存するようなテストは費用対効果が低くなる傾向にあり、機能の追加や変更の時に大きな技術負債として顕在化する可能性が高いです。(参考情報: リーンテストに関する要約記事)

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

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を掲載する