Java のリリースサイクル が変更され、2019/01 現在の Java の最新バージョンは早 11。文法面での改善も進んでおり、モダンな Java のスタイルにも変化が見られる。この記事では Java 11 時代におけるモダンな Java プログラミングのスタイルをまとめてみたい。
筆者の主観を多分に含むため、その点ご注意を。
var(ローカル変数の型推論)
-
var
を積極的に利用する。
Java 10 で導入された var
によるローカル変数の型宣言だが、基本的には使用できる場面では積極的に使用するというスタンスでよいだろう。モダンな言語の多くは同様の型推論を採用しているが、それで問題になったという話は聞かない。
高度に訓練された Java プログラマーにとっては左辺の型定義など IDE が自動補完してくれるので便利さを感じないという意見には一理あるのだが、コードを読むときに限っては Java の左辺の文字数の多さには可読性を著しく損なう冗長さがある。
// before
Owner owner = new Owner();
List<Pet> pets = List.of(new Pet("pochi"), new Pet("tama"));
// after
var owner = new Owner();
var pets = List.of(new Pet("pochi"), new Pet("tama"));
上記の例なら大したことはないが、ジェネリクスも含めると Java のクラス名は長くなりがちなので、var
を使って情報量を減らしてあげた方が読みやすい場面が多いだろう。
コレクション操作
unmodifiable
- コレクションを変更不可能にできる箇所ではできるだけ変更不可能にする。
Java の標準コレクションライブラリには ImmutableList
といった変更不可能性を表すインターフェースは存在しないが、要素の変更を行おうとした時に例外を発生させるようにすることはできる。予期せぬ要素の変更を防止するために、変更不可にできる箇所はできるだけ変更不可にしておいた方がいいだろう。
// Java 9 で追加されたファクトリーメソッドは変更不可能なコレクションを生成する
List.of(1, 2, 3); // unmodifiable
Set.of(1, 2, 3); // unmodifiable
Map.of("keyA", "valueA", "keyB", "ValueB"); // unmodifiable
// Collections クラスにはコレクションの変更不可能なビューを返すメソッドが用意されている。
Collections.unmodifiableList(modifiableList); // unmodifiable
// Java 10 で追加されたファクトリーメソッドによっても既存のコレクションの変更不可能なビューを作成可能
List.copyOf(modifiableList); // unmodifiable
// Java 10 では Stream の Collector として変更不可能なコレクションに変換するメソッドが追加されている
modifiableList.stream().collect(Collectors.toUnmodifiableList());
Stream API
- 極力 for 文を使用せず、Stream API によって処理する。
Java 8 での Stream API の出現によって for 文が活躍する機会はほとんどなくなった。Stream API の宣言的な記述はうまく使用すれば可読性が高く、パフォーマンスの観点からも優れるため、例外的な場面を除いてコレクション操作は Stream API によって行うのがよいだろう。
var beforeList = List.of("a", "b", "c");
// before
var afterList1 = new ArrayList<String>();
for (var str : beforeList) {
afterList.add(str.toUpperCase());
}
// after
var afterList2 = beforeList
.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
Null
Optional
- メソッドの戻り値として
null
が返り得る場合は、Optional
型として返す。
Java 8 ではその値が null
かもしれないことを表す Optional
型が追加された。これを使わない理由はないので、null
が返り得る場合には null
を直接返すのではなく、常に Optional
型として返すようにする。
Optional<String> getName() {
return Optional.ofNullable(this.name);
}
Optonal
はメソッドの戻り値として使用されることメインに想定されているようなので、メソッドの引数やフィールドでは基本的には使わないようにするのがよいだろう。
Nullability
-
null
可否をアノテーションによって表現する。
Kotlin 導入のメリットとしてよく語られるのが null 安全であるというものだが、Java でもそれを部分的に実現する機構が用意されていて、それがアノテーションによる null 可能性の宣言である。これは Java 標準機能として採用されることはなかったため、何らかのサードパーティーライブラリの導入を必要とするが、広く用いられている機構である。
例えば Spring Framework がバージョン 5 から提供しているアノテーションを使うと以下のような表現が可能となる。
public @Nullable Pet getPetByName(@NonNull String name) {
// 引数 name には null を渡してはならない
// 返り値 Pet は null になりうる(通常は Optional<Pet> とするほうがよい)
}
これによって IDE の静的検査による事前の null 可能性のチェックが可能となる。
ちなみに、Kotlin から Java のコードを利用する際にもこれらのアノテーションが付与されていることで null 非許容型だと判別してくれるといったメリットもある。
※ 具体的にどのライブラリのアノテーションを使用すべきかというのはちょっと断言できない。おそらく現状一番使用されているのは JSR 305 だと思うのだが、この JSR は休止状態にあって今後も採用の見込みがなく、ライセンス上も使用することは推奨できない状況。Checker Framework が有力な選択肢となりうると思うが、他にも選択肢があるので複数比較して使用するものを決定する必要があるだろう。
参考)
- nullpointerexception - Which @NotNull Java annotation should I use? - Stack Overflow
- https://qiita.com/ptiringo/items/71c2792e7c553399c2e2#comment-b7026b640ba564ba7b2d
Objects.requireNonNull
- メソッドの null 非許容な引数は
Objects.requireNonNull
メソッドを使って早いタイミングで null チェックを行う。
Nullability を表すアノテーションである程度 null の混入を防止することができるが、あくまで静的解析でしか無いため実行時に null が混入した場合には無力である。実行時の null 混入を早めに検知するためにはメソッド内部での null チェックが結局必要となる。
Java 7 で導入された Objects.requireNonNull
を使えば、簡潔に null 検査を行うことができるので積極的にこれを使っていくとよいだろう。
Pet(@NonNull String name, @NonNull LocalDate birthdate) {
this.name = Objects.requireNonNull(name);
this.birthdate = Objects.requireNonNull(birthdate);
}
ちなみに IntelliJ であれば、@NonNull
などのアノテーションが付与された変数の上で Option + Enter することで 自動的に Objects.requireNonNull
で囲ってくれるオプションを表示してくれるので、これを利用するのが効率的だ。
try-with-resources
- リソースのクローズ処理が必要な箇所では常に try-with-resources を使う。
Java 7 で try-with-resource の機構が導入され、リソースのクローズ処理が容易に行えるようになりました。データベースコネクションなど、処理の成功・失敗にかかわらず必ずクローズ処理が必要になるリソースについては常に try-with-resource の機構を使ってクローズ処理を行うのがよいでしょう。
// before
FileInputStream inputStream1 = null;
try {
inputStream1 = new FileInputStream("foo.txt");
inputStream1.read();
} finally {
if (inputStream1 != null) {
try {
inputStream1.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// after
try (var inputStream2 = Files.newInputStream(Path.of("foo.txt"))) {
inputStream2.read();
}
日時
- Date and Time API を使う。
- Date クラスや Calendar クラス、SimpleDateFormat クラスなどの旧クラス群は特別な理由がない限り使用しない。
Java 8 で導入された Date and Time API によって旧来の Date クラスや Calendar クラス、SimpleDateFormatter クラスなどをほとんど全て置き換えられるようになった。特別な理由がない限り旧来のクラスは使用しないのがよいだろう。
// before
var date = new Date();
var simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd");
System.out.println(simpleDateFormat.format(date));
// after
var localDate = LocalDate.now();
var dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
System.out.println(dateTimeFormatter.format(localDate));
旧来のクラス群の使いづらさを解消するために使用された Joda-Time などのライブラリも役割を終えたと言えるだろう。
ファイル操作
-
java.io
パッケージのクラス (Java I/O) よりもjava.nio.file
パッケージのクラス (NIO2) を優先的に使用する。
Java 7 で導入された NIO2 によって Java のファイル操作 API は新しくなっている。基本的には旧来の API は極力使わず、新しい API を使用するという方針で行くのがいいだろう。
// before
var dir = new File("/tmp/dir");
var succeeded = dir.mkdir();
if (succeeded) {
try {
new File(dir, "foo.txt").createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
// after
try {
var directory = Files.createDirectory(Path.of("/tmp/dir2"));
Files.createFile(directory.resolve("foo.txt"));
} catch (IOException e) {
e.printStackTrace();
}
new File(..)
ではなく Path.of(..)
および Files.xxx
を使うことを心がける。
モジュール
理解次第追記……。