キーポイント
- Java 16, and the imminent Java 17 release, come with a plethora of features and language enhancements that will help boost developer productivity and application performance
- Java 16 Stream API provides new methods for commonly used terminal operations and help reduce boilerplate code clutter
- Record is a new Java 16 language feature to concisely define data-only classes. The compiler provides implementations of constructors, accessors, and some of the common Object methods
- Pattern matching is another new feature in Java 16, which, among other benefits, simplifies the otherwise explicit and verbose casting done with instanceof code blocks
Java 16がプロダクションで使用することを目的としたGAビルドとして2021年3月にリリースされました。ビデオプレゼンテーションで詳細に新機能について説明しています。そして、次のLTSビルドであるJava 17は今年の9月にリリースされる予定です。Java 17には、多くの改善と言語の強化が詰め込まれています。そのほとんどはJava 11以降に提供されたすべての新機能と変更の集大成です。
Java 16の新機能に関しては、Stream APIのすばらしいアップデートを共有してから、主に言語の変更に焦点を当てます。
StreamをListにする
List<String> features =
Stream.of("Records", "Pattern Matching", "Sealed Classes")
.map(String::toLowerCase)
.filter(s -> s.contains(" "))
.collect(Collectors.toList());
上のコードスニペットは、Java Stream APIの操作を使っている場合はかなりなじみがあるはずです。
コードに含まれているのは、いくつかの文字列の stream です。関数をその上に map してから、stream をフィルタリングします。
最後に stream を list に具体化します。
このように通常ターミナル操作で collect
を呼び出し collector を渡します。
collect
を使用し Collectors.toList()
をそれに渡すという非常によくある方法は、ボイラープレートコードのように感じます。
幸いなことに、Java 16ではStream APIに新しいメソッドが追加されました。これにより、stream のターミナル操作として toList()
を即座に呼び出すことができます。
List<String> features =
Stream.of("Records", "Pattern Matching", "Sealed Classes")
.map(String::toLowerCase)
.filter(s -> s.contains(" "))
.toList();
上のコードでこの新しいメソッドを使用すると、スペースを含むストリームからの文字列のリストが作成されます。返されるこのリストは変更不可のリストであることに注意してください。つまり、このターミナル操作から返されたリストに要素を追加または削除することはできなくなります。stream を可変リストに収集する場合は collect()
関数で collector を引き続き使用する必要があります。したがってJava 16で利用可能になったこの新しい toList()
メソッドは、実際はちょっとした嬉しさがあるだけです。そしてこの新しいアップデートによりストリームパイプラインコードブロックが少し読みやすくなることを願っています。
Stream APIのもう1つの更新は mapMulti()
メソッドです。その目的は flatMap()
メソッドに少し似ています。通常 flatMap()
を使用しそれに渡すラムダの内部ストリームに map する場合に mapMulti()
で要素をコンシューマにプッシュすることを行う別の方法を提供します。この記事では、Java 16の新しい言語機能について説明したいので、このメソッドについて詳しくは説明しません。mapMulti()
について詳しく知りたい場合は、このメソッドのJavaドキュメントを参照することをお勧めします。
レコード (Records)
Java 16で提供された最初の大きな言語機能はレコードと呼ばれるものです。レコードは、データを任意のクラスではなくJavaコードのデータとして表すことです。Java 16より前は、単にデータを表現する必要があったときに、以下のコードスニペットに示すような任意のクラスにしていました。
public class Product {
private String name;
private String vendor;
Private int price;
private boolean inStock;
}
ここに、4つのメンバーを持つ Product
クラスがあります。これは、このクラスを定義するために必要なすべての情報がなければなりません。もちろん、これを機能させるには、さらに多くのコードが必要です。たとえば、コンストラクタが必要です。メンバーの値を取得するには、対応するgetterメソッドが必要です。完全にするために、定義したメンバーと一致する equals()
、hashCode()
および toString()
の実装も必要です。このボイラープレートコードの一部はIDEで生成できますが、これにはいくつかの欠点があります。Lombokのようなフレームワークを使用することもできますが、これにも同様にいくつかの欠点もあります。
私たちが本当に必要としているのは、データのみのクラスを持つというこのコンセプトをより厳密に記述するためのJava言語内のメカニズムです。そのため、Java 16には、レコードの概念があります。次のコードスニペットでは Product
クラスをレコードとして再定義しました。
public record Product(
String name,
String vendor,
int price,
boolean inStock) {
}
新しいキーワード record
の導入に注意してください。キーワード record
の直後にレコード型の名前を指定する必要があります。この例では、名前は Product
です。そして、これらのレコードを構成するコンポーネントを提供するだけで済みます。ここでは、型と名前を付けて4つのコンポーネントを提供しました。そしてこれで完了です。Javaのレコードは、このデータのみを含むクラスの特殊な形式です。
レコードは私たちに何を提供するのでしょうか? レコード宣言を取得するとレコードのコンポーネントのすべての値を受け入れる暗黙のコンストラクタを持つクラスを取得します。すべてのレコードコンポーネントに基づいて equals()
、hashCode()
および toString()
メソッドの実装を自動的に取得します。さらに、レコードにあるすべてのコンポーネントのアクセサメソッドも取得します。上記の例では、レコードのコンポーネントの実際の値をそれぞれ返す name
メソッド、vendor
メソッド、price
および inStock
メソッドを取得します。
レコードは常に不変です。セッタメソッドはありません。つまり、レコードが特定の値でインスタンス化されると、変更することはできなくなります。また、レコードクラスは final でもあります。レコードとのインターフェースを実装することはできますが、レコードを定義するときに他のクラスを拡張することはできません。全体として、ここではいくつかの制限があります。しかし、レコードは、アプリケーションでデータのみのクラスを簡潔に定義するための非常に強力な方法を提供します。
レコードの考え方
これらの新しい言語要素についてどのように考え、アプローチすべきでしょうか? レコードは、データをデータとしてモデル化するために使用されるクラスの新しい制限された形式です。レコードに状態を追加することはできません。レコードのコンポーネントに加えて (非静的) フィールドを定義することはできません。レコードは、実際には不変データのモデリングに関するものです。レコードをタプルと考えることもできますが、インデックスで参照できる任意のコンポーネントがある他の言語の一般的な意味でのタプルではありません。Javaではタプル要素には実際の名前があり、名前がJavaでは重要であるため、タプル型自体であるレコードにも名前があります。
レコードをどのように考えてはいけないのか
完全に適切ではないが、レコードについて考えたくなるかもしれないいくつかのことがあります。何よりもまず、これらは既存のコードのボイラープレートの削減メカニズムとして意図されたものではありません。レコードを定義する方法は非常に簡潔になりましたが、主にレコードによって課せられる制限のために、アプリケーションのクラスなどのデータをレコードに簡単に置き換えることができるという意味ではありません。このことは、実際に設計目標ではありません。
レコードの設計目標は、データをデータとしてモデル化するための優れた方法を持つことです。また、JavaBeansのドロップイン置換ではありません。前述したように、たとえば、アクセサメソッドはJavaBeansのget標準に準拠していないためです。また、JavaBeansは一般的に変更可能ですが、レコードは不変です。それらは多少似た目的を果たしますが、レコードは意味のある方法でJavaBeansを置き換えるものではありません。レコードを値型と考えるべきでもありません。
値型は、将来のJavaリリースで言語拡張として提供される可能性があり、値型はメモリレイアウトとクラス内のデータの効率的な表現に非常に関係しています。もちろん、これら2つの世界はある時点で一緒になる可能性がありますが、今のところ、レコードはデータのみのクラスを表現するためのより簡潔な方法にすぎません。
レコードについての詳細
まったく同じ値の型 Product
のレコード p1
と p2
を作成する次のコードについて考えてみます。
Product p1 = new Product("peanut butter", "my-vendor", 20, true);
Product p2 = new Product("peanut butter", "my-vendor", 20, true);
これらのレコードは、参照の同等性によって比較できます。また、レコードの実装によって自動的に提供される equals()
メソッドを使用して比較することもできます。
System.out.println(p1 == p2); // Prints false
System.out.println(p1.equals(p2)); // Prints true
ここで表示されるのは、これら2つのレコードが2つの異なるインスタンスであるため、参照比較は false と評価されることです。ただし equals()
を使用すると、これら2つのレコードの値のみが調べられ、trueと評価されます。レコード内にあるデータについてのみであるためです。繰り返しになりますが、等価性とハッシュコード (hashcode) の実装はレコードのコンストラクタに提供する値に完全に基づいています。
注意すべき点の1つは、レコード定義内で、アクセサメソッド、または等価性とハッシュコードの実装をオーバーライドできることです。ただし、レコードのコンテキストでこれらのメソッドのセマンティクスを保持するのはユーザの責任です。また、レコード定義にメソッドを追加できます。これらの新しいメソッドでレコードの値にアクセスすることもできます。
レコードで実行したい場合があるもう1つの重要な機能はバリデーションです。たとえば、レコードのコンストラクタに提供された入力が有効な場合にのみレコードを作成したい場合があります。バリデーションを行う従来の方法は、引数をメンバ変数に割り当てる前に検証される入力引数を使用するコンストラクタを定義することです。しかし、レコードの場合は、新しい形式、いわゆるコンパクトコンストラクタ (compact constructor) を使用できます。この形式では、正式なコンストラクタ引数を省略できます。コンストラクタは、暗黙的にコンポーネント値にアクセスできます。Product
の例では、価格 (price) がゼロ未満の場合、IllegalArgumentException
をスローすることができます。
public record Product(
String name,
String vendor,
int price,
boolean inStock) {
public Product {
if (price < 0) {
throw new IllegalArgumentException();
}
}
}
上のコードスニペットからわかるように、価格 (price) がゼロを超えている場合は、明示的に割り当てを行う必要はありません。(暗黙の) コンストラクタパラメータからレコードのフィールドへの割り当ては、このレコードをコンパイルするときにコンパイラによって自動的に追加されます。
必要に応じて、正規化を行うこともできます。たとえば、価格 (price) がゼロ未満の場合に例外をスローする代わりに、暗黙的に使用可能な価格 (price) パラメータをデフォルト値で設定できます。
public Product {
if (price < 0) {
price = 100;
}
}
この場合も、レコードの実際のメンバへの割り当て、つまりレコード定義の一部である最後のフィールドは、このコンパクトなコンストラクタの最後にコンパイラによって自動的に挿入されます。全体からみて、Javaでデータのみのクラスを定義するための非常に用途が広い、非常に優れた方法です。
メソッド内でローカルにレコードを宣言および定義することもできます。これは、メソッド内で使用したい中間状態がある場合に非常に便利です。たとえば、割引商品 (discounted product) を定義するとします。Product
を取得するレコードと、プロダクトが割引されているかどうかを示す boolean
を定義できます。
public static void main(String... args) {
Product p1 = new Product("peanut butter", "my-vendor", 100, true);
record DiscountedProduct(Product product, boolean discounted) {}
System.out.println(new DiscountedProduct(p1, true));
}
上のコードスニペットからわかるように、新しいレコード定義の本文を提供する必要はありません。そして、引数として p1
と true
を使用して DiscountedProduct
をインスタンス化できます。コードを実行すると、これがソースファイルの最上位レコードとまったく同じように動作することがわかります。ローカル構造としてのレコードは、ストリームパイプラインなどの中間段階でデータをグループ化する場合に非常に役立ちます。
レコードをどこで使えばよいのか
レコードを使用できる明らかな場所がいくつかあります。そのような場所の1つは、データ転送オブジェクト (DTO) を使用する場合です。DTOは、定義上IDや動作を必要としないオブジェクトです。それらはすべてデータの転送に関するものです。たとえば、Jacksonライブラリはバージョン 2.12以降レコードのJSONおよびその他のサポートされている形式へのシリアル化と逆シリアル化をサポートしています。
レコードは、マップ内のキーを複合キーとして機能する複数の値で構成する場合にも非常に役立ちます。このシナリオでレコードを使用すると、equals および hashcode の実装に対して正しい動作が自動的に得られるため、非常に役立ちます。また、レコードは名目タプル、つまり各コンポーネントに名前が付いているタプルと考えることもできるため、レコードを使用してメソッドから呼び出し元に複数の値を返すことが非常に便利であることが容易にわかります。
一方、Java Persistence APIに関しては、レコードはあまり使用されないと思います。レコードを使用してエンティティを表す場合、エンティティはJavaBeans規則に深く基づいているため、現実は不可能です。また、エンティティは通常不変ではなく可変である傾向があります。もちろん、通常のクラスの代わりに、クエリで読み取り専用のビューオブジェクトのインスタンス化にレコードを使用できる場合もあります。
概観すると、Javaでレコードを作成できるようになったのは非常にエキサイティングな開発だと思います。私は広く使われるようになると思います。
instanceof のパターンマッチング
ここでJava 16の2つ目の言語の変更に案内します。これは instanceof
のパターンマッチングです。これは、Javaにパターンマッチングを導入するという長いジャーニーの最初のステップです。今Java 16で最初のサポートが提供されていることは本当に素晴らしいことだと思います。次のコードスニペットを見てください。
if (o instanceOf String) {
String s = (String) o;
return s.length();
}
コードの一部で、オブジェクトのインスタンスの型 (この場合は String
クラス) であるかどうかをチェックするこのパターンにおそらく気付くでしょう。チェックに合格した場合は、新しいスコープ変数を宣言し、値をキャストして割り当てる必要があります。そうして初めて、型付き変数の使用を開始できます。この例では、変数 s
を宣言し、cast o
で String
にキャストしてから length()
メソッドを呼び出す必要があります。これは機能しますが、冗長であり、コードを明らかにする意図はありません。もっとうまくすることができます。
Java 16以降、新しいパターンマッチング機能を使用できます。パターンマッチングを使用すると o
が特定の型のインスタンスであると言う代わりに o
を型パターンと照合できます。型パターンは、型とバインディング変数で構成されます。例を見てみましょう。
if (o instanceOf String s) {
return s.length();
}
上のコードスニペットで何が起こるかというと、o
が実際に String
のインスタンスである場合、String s
はすぐに o
の値にバインドされます。これは、if
のボディで明示的にキャストしなくても、すぐに s
を文字列として使用できることを意味します。ここでのもう1つの優れた点は、s
のスコープが if
のボディのみに制限されていることです。ここで注意すべきことの1つは、ソースコードの o
の型が String
のサブタイプであってはならないということです。この場合、条件が常にtrueになってしまうためです。そのため、通常テスト対象のオブジェクトの型がパターン型のサブタイプであることをコンパイラが検出すると、コンパイル時エラーがスローされます。
指摘すべきもう1つの興味深い点は、次のコードスニペットに示すように、条件が true または false のどちらに評価されるかに基づいて s
のスコープを推測できるほどコンパイラが賢いことです。
if (!(o instanceOf String s)) {
return 0;
} else {
return s.length();
}
コンパイラは、パターンマッチが成功しなかった場合の else
ブランチで、スコープ内に String
型の s
があることを確認します。また if
ブランチに s
がスコープ内になく、スコープ内には o
しかないことを確認します。このメカニズムはフロースコープ (flow scoping) と呼ばれ、パターンが実際に一致する場合にのみ型パターン変数がスコープ内に存在します。これは本当に便利です。これは本当にコードを引き締めるのに役立ちます。これはあなたが知っておく必要があることであり、慣れるのに少し時間がかかるかもしれません。
このフロータイピングの動作を非常によく見ることができるもう1つの例は、equals()
メソッドの次のコード実装を書き直す場合です。通常の実装では、最初に o
が MyClass
のインスタンスであるかどうかを確認します。そうである場合は、o
を MyClass
にキャストしてから、o
の名前 (name) フィールドを MyClass
の現在のインスタンスと照合します。
@Override
public boolean equals(Object o) {
return (o instanceOf MyClass) &&
((MyClass) o).name.equals(name);
}
次のコードスニペットに示すように、新しいパターンマッチングメカニズムを使用して実装を簡素化できます。
@Override
public boolean equals(Object o) {
return (o instanceOf MyClass m) &&
m.name.equals(name);
}
繰り返しになりますが、コードの明示的で冗長なキャストの素晴らしい簡素化です。パターンマッチングは、適切なユースケースで使用されると、多くのボイラープレートコードを抽象化します。
パターンマッチングの未来
Javaチームは、パターンマッチングの将来の方向性のいくつかをスケッチしました。もちろん、これらの将来の方向性が実際の公式の言葉でいつ、どのように終わるかについての約束ということではありません。次のコードスニペットでは、新しい switch 式で、前に説明したように instanceOf
で型パターンを使用できることがわかります。
static String format(Object o) {
return switch(o) {
case Integer i -> String.format("int %d", i);
case Double d -> String.format("int %f", d);
default -> o.toString();
};
}
o
が整数 (integer) の場合、フロースコープが開始され、変数 i
をすぐに整数 (integer) として使用できるようになります。同じことが他のケースとデフォルトのブランチにも当てはまります。
もう1つの新しくエキサイティングな方向性は、レコードパターンをパターンマッチングして、コンポーネント値を新しい変数にすぐにバインドできる可能性があるレコードパターンです。次のコードスニペットを見てください。
if (o instanceOf Point(int x, int y)) {
System.out.println(x + y);
}
Point
レコードに x
と y
があります。オブジェクト o
が実際に Point である場合、x
コンポーネントと y
コンポーネントを x
変数と y
変数にすぐにバインドし、それらがすぐに使えるようになります。
Array (配列) パターンは、Javaの将来のバージョンで取得される可能性のある別の種類のパターンマッチングです。次のコードスニペットを見てください。
if (o instanceOf String[] {String s1, String s2, ...}) {
System.out.println(s1 + s2);
}
o
が文字列 (String) の配列である場合、文字列配列の最初と2番目の部分をすぐに s1
と s2
に抽出できます。もちろん、これは文字列配列に実際に2つ以上の要素がある場合にのみ機能します。そして、3つのドット表記を使用して、配列要素の残りを無視することができます。
要約すると、instanceOf
のパターンマッチングは素晴らしい小さな機能ですが、クリーンでシンプルで読みやすいコードを書くために使用できる追加の種類のパターンの可能性のある、新しい未来への小さな一歩です。
プレビュー機能: Sealed Class
Sealed Class 機能について話しましょう。これはJava 16のプレビュー機能ですが、Java 17では最終的なものになることに注意してください。この機能をJava 16で使用するには、コンパイラ呼び出しとJVM呼び出しに --enable-preview
フラグを渡す必要があります。この機能を使用すると継承階層を制御できます。
スーパータイプ Option
のサブタイプとして Some
と Empty
のみを使用するモデル化をしたいとします。そして、Option
型の恣意的な拡張を防ぎたいとします。たとえば、階層に Maybe
型を許可したくない場合です。
したがって、基本的に Option
型のすべてのサブタイプの包括的な概要があります。ご存知のように、現時点でJavaの継承を制御する唯一のツールは、final
キーワードを使用することです。これは、サブクラスがまったく存在できないことを意味します。しかし、それは私たちが望んでいることではありません。Sealed Class なしでこの機能をモデル化できるようにするためのいくつかの回避策がありますが、Sealed Class を使用するとこれがはるかに簡単になります。
Sealed Class 機能には、sealed
と permits
の新しいキーワードが付属しています。次のコードスニペットを見てください。
public sealed class Option<T>
permits Some, Empty {
...
}
public final class Some
extends Option<String> {
...
}
public final class Empty
extends Option<Void> {
...
}
sealed
の Option
クラスを定義できます。次に、クラス宣言の後、permits
キーワードを使用して、Some
クラスと Empty
クラスのみが Option
クラスの拡張が許可されることを示します。次に、Some
と Empty
を通常どおりクラスとして定義できます。それ以上の継承を防ぎたいので、これらのサブクラスを final
にします。Option
クラスを拡張した他のクラスをコンパイルすることはできなくなります。これは、Sealed Class メカニズムを介してコンパイラによって強制されます。
この機能については、この記事ではカバーできないさらに多くのことが言えます。詳細については、JEP 360 の sealed classes Java Enhancement Proposal ページにアクセスして、詳細を読むことをお勧めします。
追記
Java 16には、この記事では取り上げられなかったことが他にもたくさんあります。たとえば、Vector API、Foreign Linker API、Foreign-Memory Access APIなどのインキュベーターAPIはすべて非常に有望です。また、JVMレベルで多くの改善が行われました。たとえば ZGC ではパフォーマンスがいくつか改善されています。JVMでいくつかの Elastic Metaspace の改善が行われました。そして、Windows、Mac、およびLinux用のネイティブインストーラーを作成できるJavaアプリケーション用の新しいパッケージツールがあります。最後に、これは非常に影響が大きいと思いますが、classpath
からアプリケーションを実行すると、JDKのカプセル化された型が強力に保護されます。
これらの新機能と言語拡張機能のいくつかはアプリケーションに大きな影響を与える可能性があるため、すべてを調べることを強くお勧めします。
著者について
Sander Mak氏 は、Javaコミュニティで10年以上活動しているJavaチャンピオンです。現在、彼はPicnicの技術ディレクトリです。同時に、Mak氏はカンファレンスを通じてだけでなく、オンラインのeラーニングプラットフォームでも、知識の共有に関して非常に積極的です。