1. プロジェクトアンバーとは

Project Amberは、JavaおよびOpenJDKの開発者による現在のイニシアチブであり、開発プロセスをより良いものにするために、JDKにいくつかの小さいながらも重要な変更を提供することを目的としています。 これは2017年から継続されており、すでにJava 10および11にいくつかの変更が加えられており、他の変更はJava 12に含まれる予定であり、さらに将来のリリースで追加される予定です。

これらのアップデートはすべて、 JEPs –JDK拡張提案スキームの形式でパッケージ化されています。

2. 配信されたアップデート

現在までに、Project Amberは、現在リリースされているバージョンのJDK JEP-286およびJEP-323にいくつかの変更を加えることに成功しています。

2.1. ローカル変数型推論

Java 7では、ジェネリックスの操作を容易にする方法として、DiamondOperatorが導入されました。 この機能は、変数を定義するときに、同じステートメントに一般的な情報を複数回書き込む必要がなくなったことを意味します。

List<String> strings = new ArrayList<String>(); // Java 6
List<String> strings = new ArrayList<>(); // Java 7

Java 10には、JEP-286で完了した作業が含まれており、コンパイラーが既に使用可能な場所で型情報を複製する必要なしに、Javaコードでローカル変数を定義できます。 これは、より広いコミュニティでは var キーワードと呼ばれ、他の多くの言語で利用できるのと同様の機能をJavaにもたらします。

この作業により、ローカル変数を定義するときはいつでも、完全な型定義の代わりにvarキーワードを使用でき、コンパイラーは使用する正しい型情報を自動的に計算します。

var strings = new ArrayList<String>();

上記では、変数文字列はArrayList型であると判断されています () 、ただし、同じ行に情報を複製する必要はありません。

値の決定方法に関係なく、ローカル変数を使用する場所ならどこでもこれを使用できます。 これには、リターンタイプと式、および上記のような単純な割り当てが含まれます。

var という単語は、予約語ではないという点で特殊なケースです。 代わりに、それは特別なタイプ名です。 これは、変数名など、コードの他の部分にこの単語を使用できることを意味します。 混乱を避けるために、これを行わないことを強くお勧めします。

ローカル型推論を使用できるのは、宣言の一部として実際の型を提供する場合のみです。 値が明示的にnullの場合、値がまったく提供されない場合、または提供された値が正確なタイプを判別できない場合(たとえば、Lambda定義)は、意図的に機能しないように設計されています。

var unknownType; // No value provided to infer type from
var nullType = null; // Explicit value provided but it's null
var lambdaType = () -> System.out.println("Lambda"); // Lambda without defining the interface

ただし、呼び出し自体が型情報を提供するため、他の呼び出しからの戻り値の場合、値はnullになる可能性があります。

Optional<String> name = Optional.empty();
var nullName = name.orElse(null);

この場合、 nullNameはタイプStringを推測します。これは、 name.orElse()の戻りタイプであるためです。

この方法で定義された変数は、他の変数と同じ方法で他の修飾子を持つことができます。たとえば、推移的、同期、最終などです。

2.2. ラムダのローカル変数型推論

上記の作業により、型情報を複製することなくローカル変数を宣言できます。 ただし、これはパラメーターリストでは機能しません。特に、ラムダ関数のパラメーターでは機能しません。これは意外に思われるかもしれません。

Java 10では、Lambda関数を次の2つの方法のいずれかで定義できます。型を明示的に宣言するか、完全に省略します。

names.stream()
  .filter(String name -> name.length() > 5)
  .map(name -> name.toUpperCase());

ここで、2行目には明示的な型宣言— String —がありますが、3行目では完全に省略されており、コンパイラーは正しい型を処理します。 ここでvarタイプを使用することはできません

Java 11ではこれを実行できるので、代わりに次のように記述できます。

names.stream()
  .filter(var name -> name.length() > 5)
  .map(var name -> name.toUpperCase());

これは、コードの他の場所でのvarタイプの使用と一致しています

Lambdasは常に、すべてのパラメーターにフルタイプ名を使用するか、いずれにもフルタイプ名を使用しないように制限してきました。 これは変更されておらず、 varの使用は、すべてのパラメーターに対して行うか、パラメーターのいずれに対しても行わない必要があります

numbers.stream()
    .reduce(0, (var a, var b) -> a + b); // Valid

numbers.stream()
    .reduce(0, (var a, b) -> a + b); // Invalid

numbers.stream()
    .reduce(0, (var a, int b) -> a + b); // Invalid

ここで、最初の例は完全に有効です–2つのラムダパラメーターが両方ともvarを使用しているためです。 2番目と3番目のパラメーターは不正です。ただし、3番目のケースでは明示的な型名もありますが、 varを使用するパラメーターは1つだけです。

3. 差し迫った更新

リリースされたJDKですでに利用可能なアップデートに加えて、次のJDK 12リリースには、JEP-325。という1つのアップデートが含まれています。

3.1. 式の切り替え

JEP-325は、switchステートメントの動作方法を簡素化し、それらを式として使用できるようにするためのサポートを提供します。これにより、それらを使用するコードがさらに簡素化されます。

現在、 switch ステートメントは、CやC++などの言語のステートメントと非常によく似た方法で機能します。 これらの変更により、KotlinのwhenステートメントまたはScalaのmatchステートメントに非常によく似たものになります。

これらの変更により、 switchステートメントを定義するための構文は、-> シンボルを使用して、lambdasの構文と同様になります。 これは、大文字と小文字の一致と実行されるコードの間にあります。

switch (month) {
    case FEBRUARY -> System.out.println(28);
    case APRIL -> System.out.println(30);
    case JUNE -> System.out.println(30);
    case SEPTEMBER -> System.out.println(30);
    case NOVEMBER -> System.out.println(30);
    default -> System.out.println(31);
}

breakキーワードは不要であり、さらに、ここでは使用できないことに注意してください。 すべての一致が明確であり、フォールスルーはオプションではないことが自動的に暗示されます。 代わりに、必要なときに古いスタイルを引き続き使用できます。

矢印の右側は、式、ブロック、またはthrowsステートメントのいずれかである必要があります。 それ以外はエラーです。 これにより、switchステートメント内で変数を定義する問題も解決されます。これはブロック内でのみ発生する可能性があります。つまり、変数は自動的にそのブロックにスコープされます。

switch (month) {
    case FEBRUARY -> {
        int days = 28;
    }
    case APRIL -> {
        int days = 30;
    }
    ....
}

古いスタイルのswitchステートメントでは、変数が重複しているため、これはエラーになります日々。 ブロックを使用する要件はこれを回避します。

矢印の左側には、コンマ区切りの値をいくつでも指定できます これは、フォールスルーと同じ機能の一部を許可するためですが、一致全体に対してのみです。偶然ではありません:

switch (month) {
    case FEBRUARY -> System.out.println(28);
    case APRIL, JUNE, SEPTEMBER, NOVEMBER -> System.out.println(30);
    default -> System.out.println(31);
}

これまでのところ、これはすべて、 switch ステートメントが機能し、より整理された現在の方法で可能です。 ただし、この更新により、switchステートメントを式として使用できるようになります。 これはJavaにとって重要な変更ですが、他のJVM言語を含む他の言語がいくつ機能し始めているかと一致しています。

これにより、スイッチ式を値に解決し、その値を他のステートメントで使用できるようになります-たとえば、割り当て:

final var days = switch (month) {
    case FEBRUARY -> 28;
    case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
    default -> 31;
}

ここでは、 switch 式を使用して数値を生成し、その数値を変数に直接割り当てています。

以前は、これは変数daysをnullとして定義し、スイッチケース内で値を割り当てることによってのみ可能でした。 つまり、は最終的なものではなく、ケースを見逃した場合は割り当てが解除される可能性があります。

4. 今後の変更

これまでのところ、これらの変更はすべてすでに利用可能であるか、次のリリースで行われる予定です。 Project Amberの一部として、まだリリースが予定されていない変更案がいくつかあります。

4.1. 生の文字列リテラル

現在、Javaには文字列リテラルを定義する方法が1つだけあります。それは、コンテンツを二重引用符で囲むことです。 これは使いやすいですが、より複雑なケースでは問題が発生します。

具体的には、特定の文字を含む文字列を作成することは困難です。これには、新しい行、二重引用符、円記号が含まれますが、これらに限定されません。 これは、これらの文字が通常よりも一般的である可能性があるファイルパスや正規表現で特に問題になる可能性があります。

JEP-326では、Raw StringLiteralsと呼ばれる新しい文字列リテラルタイプが導入されています。 これらは二重引用符ではなくバッククォートマークで囲まれ、その中に任意の文字を含めることができます。

これは、複数行にまたがる文字列や、引用符や円記号を含む文字列をエスケープせずに書き込むことができるようになることを意味します。したがって、読みやすくなります。

例えば:

// File system path
"C:\\Dev\\file.txt"
`C:\Dev\file.txt`

// Regex
"\\d+\\.\\d\\d"
`\d+\.\d\d`

// Multi-Line
"Hello\nWorld"
`Hello
World`

3つのケースすべてで、バッククォートのあるバージョンで何が起こっているかを確認するのが簡単です。これにより、エラーが発生しにくくなります

新しいRawStringリテラルを使用すると、複雑にすることなくバックティック自体を含めることもできます。 文字列の開始と終了に使用されるバッククォートの数は、必要なだけ長くすることができます。バッククォートは1つだけである必要はありません。 文字列は、同じ長さのバックティックに達したときにのみ終了します。 したがって、たとえば:

``This string allows a single "`" because it's wrapped in two backticks``

これらにより、特定の文字を機能させるために特別なシーケンスを必要とせずに、文字列をそのまま入力できます。

4.2. ラムダの残り物

JEP-302 では、ラムダの動作方法にいくつかの小さな改善が導入されています。

主な変更点は、パラメーターの処理方法です。 まず、この変更により、未使用のパラメーターにアンダースコアを使用できるようになり、不要な名前が生成されなくなりました。 これは以前は可能でしたが、アンダースコアが有効な名前であったため、単一のパラメーターに対してのみ可能でした。

Java 8では、名前としてアンダースコアを使用することが警告になるように変更が導入されました。 その後、Java 9はこれを進行させてエラーになり、使用できなくなりました。 この今後の変更により、競合を引き起こすことなくラムダパラメーターを使用できるようになります。 これにより、たとえば、次のコードが可能になります。

jdbcTemplate.queryForObject("SELECT * FROM users WHERE user_id = 1", (rs, _) -> parseUser(rs))

この拡張機能では、 2つのパラメーターを使用してラムダを定義しましたが、名前にバインドされるのは最初のパラメーターのみです。 2つ目はアクセスできませんが、同様に、使用する必要がないため、このように記述しています。

この拡張機能のその他の主な変更点は、ラムダパラメーターが現在のコンテキストから名前をシャドウできるようにすることです。 これは現在許可されていないため、理想的とは言えないコードを記述してしまう可能性があります。 例えば:

String key = computeSomeKey();
map.computeIfAbsent(key, key2 -> key2.length());

コンパイラ以外に、keyとkey2が名前を共有できない理由は実際には必要ありません。 ラムダは変数keyを参照する必要はなく、これを強制するとコードが醜くなります。

代わりに、この拡張機能により、より明白で単純な方法でそれを書くことができます。

String key = computeSomeKey();
map.computeIfAbsent(key, key -> key.length());

さらに、この拡張機能には、オーバーロードされたメソッドにラムダ引数がある場合に、オーバーロードの解決に影響を与える可能性のある変更が提案されています。 現在、過負荷解決が機能するルールのために、これがあいまいさをもたらす可能性がある場合があり、このJEPは、このあいまいさの一部を回避するためにこれらのルールをわずかに調整する場合があります。

たとえば、現在、コンパイラは次のメソッドをあいまいであると見なします

m(Predicate<String> ps) { ... }
m(Function<String, String> fss) { ... }

これらのメソッドは両方とも、単一の String パラメーターを持ち、非voidリターン型を持つラムダを取ります。 これらが異なることは開発者には明らかです。1つは文字列を返し、もう1つはブール値を返しますが、コンパイラはこれらをあいまいなものとして扱います

このJEPは、この欠点に対処し、この過負荷を明示的に処理できるようにする場合があります。

4.3. パターンマッチング

JEP-305 は、instanceof演算子と自動型強制を操作する方法の改善を導入しています。

現在、Javaで型を比較する場合、 instanceof 演算子を使用して値が正しい型であるかどうかを確認し、その後、値を正しい型にキャストする必要があります。

if (obj instanceof String) {
    String s = (String) obj;
    // use s
}

これは機能し、すぐに理解されますが、必要以上に複雑です。 コードには非常に明白な繰り返しがあるため、エラーが入り込むリスクがあります。

この拡張機能は、Java7のtry-with-resourcesで以前に行われたのと同様の調整をinstanceof演算子に加えます。 この変更により、比較、キャスト、および変数の宣言は、代わりに1つのステートメントになります。

if (obj instanceof String s) {
    // use s
}

これにより、重複やエラーがに忍び寄るリスクのない、単一のステートメントが得られますが、上記と同じように機能します。

これはブランチ間でも正しく機能し、以下が機能するようになります。

if (obj instanceof String s) {
    // can use s here
} else {
    // can't use s here
}

拡張機能は、必要に応じてさまざまなスコープ境界を越えて正しく機能します instanceof 句で宣言された変数は、期待どおり、その外部で定義された変数を正しくシャドウイングします。 ただし、これは適切なブロックでのみ発生します。

String s = "Hello";
if (obj instanceof String s) {
    // s refers to obj
} else {
    // s refers to the variable defined before the if statement
}

これは、 null チェックに依存するのと同じ方法で、同じif句内でも機能します。

if (obj instanceof String s && s.length() > 5) {
    // s is a String of greater than 5 characters
}

現在、これはifステートメントに対してのみ計画されていますが、将来の作業では、スイッチ式でも機能するように拡張される可能性があります。

4.4. 簡潔な方法の本体

JEPドラフト8209434は、ラムダ定義の動作と同様の方法で、簡略化されたメソッド定義をサポートするための提案です。

現在、ラムダを3つの異なる方法で定義できます:ボディを使用する、単一の式として、またはメソッド参照として:

ToIntFunction<String> lenFn = (String s) -> { return s.length(); };
ToIntFunction<String> lenFn = (String s) -> s.length();
ToIntFunction<String> lenFn = String::length;

ただし、実際のクラスメソッド本体の記述に関しては、現在、完全にで書き出す必要があります。

この提案は、これらのメソッドが適用可能な場合に、これらのメソッドの式およびメソッド参照フォームもサポートすることです。 これは、特定のメソッドを現在よりもはるかに単純に保つのに役立ちます。

たとえば、getterメソッドは完全なメソッド本体を必要としませんが、単一の式に置き換えることができます。

String getName() -> name;

同様に、他のメソッドの単なるラッパーであるメソッドを、以下を介してパラメーターを渡すことを含め、メソッド参照呼び出しに置き換えることができます。

int length(String s) = String::length

これらは、意味がある場合に、より単純なメソッドを可能にします。これは、クラスの他の部分の実際のビジネスロジックを覆い隠す可能性が低いことを意味します。

これはまだドラフトステータスであり、そのため、配信前に大幅に変更される可能性があることに注意してください。

5. 強化された列挙

JEP-301 は、以前はProjectAmberの一部になる予定でした。 これにより、列挙型にいくつかの改善がもたらされ、個々の列挙型要素が個別のジェネリック型情報を持つことが明示的に可能になります

たとえば、次のことが可能になります。

enum Primitive<X> {
    INT<Integer>(Integer.class, 0) {
       int mod(int x, int y) { return x % y; }
       int add(int x, int y) { return x + y; }
    },
    FLOAT<Float>(Float.class, 0f)  {
       long add(long x, long y) { return x + y; }
    }, ... ;

    final Class<X> boxClass;
    final X defaultValue;

    Primitive(Class<X> boxClass, X defaultValue) {
       this.boxClass = boxClass;
       this.defaultValue = defaultValue;
    }
}

残念ながら、Javaコンパイラアプリケーション内でのこの拡張機能の実験では、以前に考えられていたよりも実行可能性が低いことが証明されています。 列挙型要素に汎用型情報を追加すると、それらの列挙型を他のクラス( EnumSet など)の汎用型として使用できなくなりました。 これにより、拡張機能の有用性が大幅に低下します。

そのため、この拡張機能は、これらの詳細が解決されるまで現在保留中です。

6. 概要

ここでは、さまざまな機能について説明しました。 それらのいくつかはすでに利用可能であり、他はまもなく利用可能になり、さらに多くは将来のリリースで計画されています。 これらは現在および将来のプロジェクトをどのように改善できますか?