web-dev-qa-db-ja.com

ラムダ内の暗黙の匿名型

この質問 で、ユーザー@Holgerが提供した 回答 これは、私が知らなかった匿名クラスの珍しい使用法を示しています。

その答えはストリームを使用していますが、この匿名型の構造は他のコンテキストで使用できるため、この質問はストリームに関するものではありません。

String s = "Digging into Java's intricacies";

Optional.of(new Object() { String field = s; })
    .map(anonymous -> anonymous.field) // anonymous implied type 
    .ifPresent(System.out::println);

驚いたことに、これは期待される出力をコンパイルして出力します。


注:古くから、匿名の内部クラスを作成し、そのメンバーを次のように使用できることをよく知っています。

int result = new Object() { int incr(int i) {return i + 1; } }.incr(3);
System.out.println(result); // 4

しかし、これは私がここで求めていることではありません。匿名型はOptionalメソッドチェーンを介して伝播されるため、私の場合は異なります。


これで、この機能の非常に便利な使用法を想像できます...多くの場合、元の要素を保持しながら、mapパイプライン上でStream操作を発行する必要がありました。私は人々のリストを持っています:

public class Person {
    Long id;
    String name, lastName;
    // getters, setters, hashCode, equals...
}

List<Person> people = ...;

また、PersonインスタンスのJSON表現をリポジトリに保存する必要があります。このため、すべてのPersonインスタンスと各PersonidのJSON文字列が必要です。 :

public static String toJson(Object obj) {
    String json = ...; // serialize obj with some JSON lib 
    return json;
}        

people.stream()
    .map(person -> toJson(person))
    .forEach(json -> repository.add(ID, json)); // where's the ID?

この例では、すべての人を対応するjson文字列に変換したため、Person.idフィールドが失われました。

これを回避するために、多くの人がHolderクラス、Pair、またはTuple、あるいは単にAbstractMap.SimpleEntryを使用しているのを見てきました。

people.stream()
    .map(p -> new Pair<Long, String>(p.getId(), toJson(p)))
    .forEach(pair -> repository.add(pair.getLeft(), pair.getRight()));

これはこの単純な例には十分ですが、それでも汎用のPairクラスが存在する必要があります。また、ストリームを介して3つの値を伝播する必要がある場合は、Tuple3クラスなどを使用できると思います。配列を使用することもオプションですが、すべての値が同じタイプでない限り、タイプセーフではありません。 。

したがって、暗黙の匿名型を使用すると、上記と同じコードを次のように書き直すことができます。

people.stream()
    .map(p -> new Object() { Long id = p.getId(); String json = toJson(p); })
    .forEach(it -> repository.add(it.id, it.json));

魔法です!これで、型の安全性を維持しながら、必要な数のフィールドを持つことができます。

これをテストしている間、コードの別々の行で暗黙の型を使用することができませんでした。元のコードを次のように変更した場合:

String s = "Digging into Java's intricacies";

Optional<Object> optional = Optional.of(new Object() { String field = s; });

optional.map(anonymous -> anonymous.field)
    .ifPresent(System.out::println);

コンパイルエラーが発生します:

Error: Java: cannot find symbol
  symbol:   variable field
  location: variable anonymous of type Java.lang.Object

fieldクラスにはObjectという名前のメンバーがいないため、これは予想されることです。

だから私は知りたいです:

  • これはどこかに文書化されていますか、それともJLSにこれについて何かありますか?
  • これにはどのような制限がありますか?
  • このようなコードを書くことは実際に安全ですか?
  • これの簡略構文はありますか、それともこれが私たちにできる最善の方法ですか?

この種の使用法はJLSで言及されていませんが、もちろん、プログラミング言語が提供するすべての可能性を列挙することによって仕様が機能するわけではありません。代わりに、型に関する正式なルールを適用する必要があり、匿名型についても例外はありません。つまり、仕様では、式の型を名前付きのスーパー型にフォールバックする必要があるとは規定されていません。匿名クラスの場合。

確かに、仕様の奥深くでそのようなステートメントを見落としていた可能性がありますが、匿名型に関する唯一の制限がそれらのanonymousの性質、つまり参照を必要とするすべての言語構造に由来することは常に自然に見えましたタイプに名前で、タイプを直接操作することはできないため、スーパータイプを選択する必要があります。

したがって、式new Object() { String field; }の型がフィールド「field」を含む無名型である場合、アクセスnew Object() { String field; }.fieldだけでなく、Collections.singletonList(new Object() { String field; }).get(0).field、明示的なルールがそれを禁止し、一貫していない限り、同じことがラムダ式にも当てはまります。

Java 10以降では、varを使用して、初期化子から型が推測されるローカル変数を宣言できます。このようにして、ラムダパラメーターだけでなく、匿名クラスの型を持つ任意のローカル変数を宣言できるようになりました。例:次の作品

_var obj = new Object() { int i = 42; String s = "blah"; };
obj.i += 10;
System.out.println(obj.s);
_

同様に、私たちはあなたの質問の例を機能させることができます:

_var optional = Optional.of(new Object() { String field = s; });
optional.map(anonymous -> anonymous.field).ifPresent(System.out::println);
_

この場合、 仕様 を参照して、これが見落としではなく意図された動作であることを示す同様の例を示します。

_var d = new Object() {};  // d has the type of the anonymous class
_

もう1つは、変数が指定できないタイプである可能性を示唆するものです。

_var e = (CharSequence & Comparable<String>) "x";
                          // e has type CharSequence & Comparable<String>
_

そうは言っても、私はこの機能の使いすぎについて警告しなければなりません。読みやすさの懸念(自分自身を「珍しい使用法」と呼んでいます)に加えて、それを使用する場所ごとに、別個の新しいクラスを作成しています(「二重中括弧の初期化」と比較してください)。同じメンバーのセットのすべてのオカレンスを同等に扱う、実際のタプル型や他のプログラミング言語の名前のない型とは異なります。

また、new Object() { String field = s; }のように作成されたインスタンスは、宣言されたフィールドだけでなく、フィールドの初期化に使用されるキャプチャされた値も含むため、必要な2倍のメモリを消費します。 new Object() { Long id = p.getId(); String json = toJson(p); }の例では、pがキャプチャされているため、2つではなく3つの参照のストレージに対して料金を支払います。非静的コンテキストでは、匿名内部クラスも常に周囲のthisをキャプチャします。

13
Holger

絶対に答えではありませんが、もっと0.02$

ラムダはコンパイラーによって推測される変数を提供するため、これが可能です。それは文脈から推測されます。そのため、推定の型でのみ可能であり、宣言の型では不可能です。

コンパイラは型を匿名としてdeduceすることができますが、それを表現できないため、使用できます名前で。したがって、情報ありますですが、言語の制限により、情報を取得できません。

それは言うようなものです:

 Stream<TypeICanUseButTypeICantName> // Stream<YouKnowWho>?

コンパイラにタイプを次のように指定したため、最後の例では機能しません:Optional<Object> optionalしたがって壊れているanonymous type推論。

これらの匿名タイプは現在(Java-10賢明)はるかに簡単な方法でも利用可能:

    var x = new Object() {
        int y;
        int z;
    };

    int test = x.y; 

var xはコンパイラによって推測されます。int test = x.y;も機能します

4
Eugene

これはどこかに文書化されていますか、それともJLSにこれについて何かありますか?

JLSに導入する必要があるのは匿名クラスの特殊なケースではないと思います。質問で述べたように、匿名のクラスメンバーに直接アクセスできます(例:incr(3))。

まず、代わりにローカルクラスの例を見てみましょう。これは、匿名クラスを持つチェーンがそのメンバーにアクセスできる理由を表しています。例えば:

@Test
void localClass() throws Throwable {
    class Foo {
        private String foo = "bar";
    }

    Foo it = new Foo();

    assertThat(it.foo, equalTo("bar"));
}

ご覧のとおり、ローカルクラスのメンバーは、そのメンバーがプライベートであっても、そのスコープ外からアクセスできます。

@Holgerが上記の回答で述べたように、コンパイラは匿名クラスごとにEnclosingClass${digit}のような内部クラスを作成します。したがって、Object{...}には、Objectから派生した独自のタイプがあります。チェーンメソッドが原因で、Objectから派生した型ではなく、独自の型EnclosingClass${digit}を返します。これが、匿名クラスインスタンスをチェーンする理由です。

@Test
void chainingAnonymousClassInstance() throws Throwable {
    String foo = chain(new Object() { String foo = "bar"; }).foo;

    assertThat(foo,equalTo("bar"));
}

private <T> T chain(T instance) {
    return instance;
}

匿名クラスを直接参照することはできないため、チェーンメソッドを2行に分割すると、実際にはから派生したタイプObjectを参照します。

[〜#〜] and [〜#〜] @ Holgerが回答した残りの質問。

編集

匿名型がジェネリック型変数で表されている限り、この構築は可能であると結論付けることができますか?

英語が苦手なので、JLSリファレンスが見つからなくてごめんなさい。しかし、私はあなたにそれがすることを言うことができます。 javapコマンドを使用して詳細を確認できます。例えば:

public class Main {

    void test() {
        int count = chain(new Object() { int count = 1; }).count;
    }

    <T> T chain(T it) {
        return it;
    }
}

checkcast命令が以下で呼び出されていることがわかります。

void test();
descriptor: ()V
     0: aload_0
     1: new           #2      // class Main$1
     4: dup
     5: aload_0
     6: invokespecial #3     // Method Main$1."<init>":(LMain;)V
     9: invokevirtual #4    // Method chain:(Ljava/lang/Object;)Ljava/lang/Object;
    12: checkcast     #2    // class Main$1
    15: getfield      #5    // Field Main$1.count:I
    18: istore_1
    19: return
1
holi-java