型パラメーターがメソッドパラメーターよりも強いのはなぜですか


12

なぜですか

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

より厳しい

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

これは、ラムダの戻り値の型がコンパイル時にチェックされない理由のフォローアップです。私は次のwithX()ような方法を使用して見つけました

.withX(MyInterface::getLength, "I am not a Long")

必要なコンパイル時エラーを生成します。

タイプBuilderExample.MyInterfaceのgetLength()のタイプは長く、これは記述子の戻りタイプと互換性がありません:文字列

メソッドを使用している間with()はしません。

完全な例:

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
      return this;
    }

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors

拡張された例

次の例は、サプライヤーに煮詰められたメソッドと型パラメーターの異なる動作を示しています。さらに、型パラメーターのコンシューマーの動作との違いも示しています。また、メソッドパラメータのコンシューマまたはサプライヤであるかどうかは関係ありません。

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> {
    TypeInference be(R r);
  }

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) {
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  }
}

1
後者との推論のため。どちらもユースケースに基づいていますが、実装する必要があります。あなたにとっては、前者は厳密で良いかもしれません。柔軟性のために、他の誰かが後者を好むことができます。
Naman、

これをEclipseでコンパイルしようとしていますか?貼り付けた形式のエラー文字列を検索すると、これはEclipse(ecj)固有のエラーであることが示唆されます。Raw javacや、GradleやMavenのようなビルドツールでコンパイルするときに同じ問題が発生しますか?
user31601

@ user31601私はjavac出力の完全な例を追加しました。エラーメッセージの
形式

回答:


12

これは本当に興味深い質問です。答えは、私は恐れていますが、複雑です。

tl; dr

違いを理解するには、Javaの型推論仕様をかなり詳しく読む必要がありますが、基本的には次のようになります。

  • 他のすべてのものは同等であり、コンパイラーは、最も具体的な型を推測します。
  • それが見つけることができる場合は、すべての要件を満たしが、その後、コンパイルが成功することを型パラメータの置換を、しかし漠然とした置換があることが判明します。
  • withすべての要件を満たす(確かにあいまいな)置換があるためですRSerializable
  • 以下のためにwithX、追加の型パラメータの導入は、F解決するために、コンパイラを強制的R制約を考慮せずに、最初にF extends Function<T,R>R(より具体的)に解決します。Stringこれは、推論がF失敗することを意味します。

この最後の箇条書きのポイントは最も重要ですが、最も手が波打っています。簡潔な言い回しは考えられないので、詳細を知りたい場合は、以下の詳細な説明を読むことをお勧めします。

これは意図された動作ですか?

ここで手足を出し、ノーと言うつもりです。

仕様にバグがあることを示唆しているわけではありません。それ以上に(の場合withX)言語デザイナーが手を挙げて、「型の推論が難しくなりすぎて、失敗するだけの状況もある」と述べました。に関するコンパイラの動作はwithXあなたが望んでいるように見えますが、私はそれを積極的に意図された設計決定ではなく、現在の仕様の偶発的な副作用であると考えます。

これは重要です。それは、アプリケーション設計でこの動作に依存する必要があるかという質問を通知するためです。言語の将来のバージョンがこのように動作し続けることを保証できないので、私はそうすべきではないと主張します。

言語設計者がスペック/デザイン/コンパイラを更新するときに既存のアプリケーションを壊さないように努力していることは事実ですが、問題となるのは、依存したい動作がコンパイラが現在失敗している動作(つまり、既存のアプリケーションではない)であるということです。Langaugeの更新により、非コンパイルコードが常にコンパイルコードに変わります。たとえば、次のコードは可能保証のJava 7でコンパイルしていないが、考えたJava 8でコンパイルします。

static Runnable x = () -> System.out.println();

ユースケースも同じです。

私があなたのwithX方法を使用することに注意するもう一つの理由は、Fパラメータ自体です。一般に、メソッドのジェネリック型パラメーター(戻り値の型には表示されません)は、署名の複数の部分の型をバインドするために存在します。それは言っています:

Tでもかまいませんが、どこで使用しTても同じタイプになるようにしたいと思います。

したがって、論理的には、各型パラメーターはメソッドシグネチャに少なくとも2回出現することを期待します。それ以外の場合は、「何もしません」。FあなたのwithX唯一のシグネチャに一度だけ表示されます。これは、言語のこの機能の意図に沿っていない型パラメータの使用を示唆しています。

代替実装

これをもう少し「意図された動作」の方法で実装する1つの方法は、withメソッドを2つのチェーンに分割することです。

public class Builder<T> {

    public final class With<R> {
        private final Function<T,R> method;

        private With(Function<T,R> method) {
            this.method = method;
        }

        public Builder<T> of(R value) {
            // TODO: Body of your old 'with' method goes here
            return Builder.this;
        }
    }

    public <R> With<R> with(Function<T,R> method) {
        return new With<>(method);
    }

}

これは次のように使用できます。

b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error

これは、あなたのように無関係な型パラメーターを含みませんwithX。メソッドを2つのシグネチャに分解することで、タイプセーフの観点から、実行しようとしていることの意図をより適切に表現します。

  • 最初のメソッドは、メソッド参照に基づいてタイプを定義するクラス(With)をセットアップします。
  • scondメソッド(of)は、以前に設定したものと互換性があるようにのタイプを制限valueします。

言語の将来のバージョンがこれをコンパイルできる唯一の方法は、実装された完全なダックタイピングである場合です。

:最後の注意点は、この全体のことは無関係作るために 私は思うMockitoを(特にそのスタブ機能)は、基本的には、すでにあなたの「タイプの安全なジェネリックビルダー」を達成しようとしているものを行う可能性があります。代わりに、それを代わりに使用できますか?

完全な(説明)説明

との両方の型推論手順について説明withwithXます。これはかなり長いので、ゆっくりと見てください。長いにもかかわらず、私はまだかなりの詳細を省きました。詳細については、仕様を参照して(リンクをたどって)、自分が正しいと確信してください(間違いを犯した可能性があります)。

また、少し簡単にするために、より最小限のコードサンプルを使用します。主な違いは、と交換されるFunctionためSupplier、関係する型とパラメーターが少なくなることです。これは、あなたが説明した動作を再現する完全なスニペットです。

public class TypeInference {

    static long getLong() { return 1L; }

    static <R> void with(Supplier<R> supplier, R value) {}
    static <R, F extends Supplier<R>> void withX(F supplier, R value) {}

    public static void main(String[] args) {
        with(TypeInference::getLong, "Not a long");       // Compiles
        withX(TypeInference::getLong, "Also not a long"); // Does not compile
    }

}

次に、各メソッド呼び出しの型適用性推論型推論手順を順に見ていきましょう。

with

我々は持っています:

with(TypeInference::getLong, "Not a long");

初期の境界セットB 0は次のとおりです。

  • R <: Object

すべてのパラメータ式は、適用性に関連しています。

したがって、初期のための制約セットの適用の推論はCは、次のとおりです。

  • TypeInference::getLong と互換性があります Supplier<R>
  • "Not a long" と互換性があります R

これ、次の境界セットB 2削減されます。

  • R <: ObjectB 0から)
  • Long <: R (最初の制約から)
  • String <: R (2番目の制約から)

これはバウンド「が含まれていないので、偽の」、そして(私は仮定)解像度R成功(与えるをSerializable)、その後、呼び出しは適用されます。

そこで、呼び出しタイプの推論に移ります

関連する入力変数と出力変数を含む新しい制約セットCは、次のとおりです。

  • TypeInference::getLong と互換性があります Supplier<R>
    • 入力変数:なし
    • 出力変数: R

これには入力変数と出力変数間の相互依存性が含まれていないため、1つのステップで削減でき、最終的なバインドセットB 4B 2と同じです。したがって、解決は以前と同じように成功し、コンパイラーは安堵のため息をつきます!

withX

我々は持っています:

withX(TypeInference::getLong, "Also not a long");

初期の境界セットB 0は次のとおりです。

  • R <: Object
  • F <: Supplier<R>

2番目のパラメーター式のみが適用可能性に関連しています。最初の(TypeInference::getLong)は次の条件を満たすため、そうではありません。

場合mジェネリックメソッドおよびメソッドの呼び出しでは、明示的な型の引数を提供しない、明示的に型付けされたラムダ式または対応するターゲット・タイプのための正確な方法基準式(の署名に由来するとしてm)の型パラメータですm

したがって、初期のための制約セットの適用の推論はCは、次のとおりです。

  • "Also not a long" と互換性があります R

これ、次の境界セットB 2削減されます。

  • R <: ObjectB 0から)
  • F <: Supplier<R>B 0から)
  • String <: R (制約から)

繰り返しますが、これにはバインドされた「false」が含まれておらず、解決R成功(String)しているため、呼び出しが適用されます。

呼び出しタイプの推論をもう一度...

今回は、関連する入力変数と出力変数を含む新しい制約セットCは次のとおりです。

  • TypeInference::getLong と互換性があります F
    • 入力変数: F
    • 出力変数:なし

この場合も、入力変数と出力変数の間に相互依存関係はありません。しかし今回は、そこにある入力変数は、F私たちがしなければならないので)、解決しようとする前にこれを削減します。したがって、バインドされたセットB 2から始めます。

  1. V以下のようにサブセットを決定します。

    解決する推論変数のセットが与えられた場合、Vこのセットと、このセットの少なくとも1つの変数の解決が依存するすべての変数の和集合とします。

    結合した第によってB 2の解像度はF依存Rので、V := {F, R}

  2. Vルールに従ってのサブセットを選択します。

    { α1, ..., αn }でインスタンス生成変数の空でない部分集合であるV全てのこのような私は)i (1 ≤ i ≤ n)場合、αi可変の解像度に依存βし、いずれかのβインスタンスを有するか、またはいくつかがあるjようにβ = αj。ii){ α1, ..., αn }このプロパティを持つ空でない適切なサブセットが存在しない。

    Vこのプロパティを満たす唯一のサブセットは{R}です。

  3. 3番目のバウンド(String <: R)を使用して、R = Stringこれをインスタンス化し、バウンドセットに組み込みます。Rが解決され、2番目の境界は事実上になりF <: Supplier<String>ます。

  4. (改訂された)2番目の境界を使用して、をインスタンス化しますF = Supplier<String>F解決されました。

これでF解決されたので、新しい制約を使用してリダクションを続行できます。

  1. TypeInference::getLong と互換性があります Supplier<String>
  2. ... Long と互換性がある String
  3. ...これはfalseに減少します

...そして、コンパイラエラーが発生します!


「拡張された例」に関する追加メモ

質問の拡張例では、上記の仕組みで直接カバーされていないいくつかの興味深いケースを取り上げています。

  • 値の型がメソッドの戻り値の型のサブタイプである場合(Integer <: Number
  • 推論された型で機能インターフェースが反変であるConsumer場合(つまり、ではなくSupplier

特に、指定された呼び出しのうち3つは、説明で説明されているものとは「コンパイラーの異なる」振る舞いを示唆している可能性があるため、際立っています。

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

これらの3の第二は、まったく同じ推論プロセスを通過しますwithX(単に置き換える以上LongNumberし、StringInteger)。これは、ここでのコンパイルの失敗は望ましい動作ではない可能性が高いため、クラス設計でこの失敗した型推論の動作に依存してはならないもう1つの理由を示しています。

他の2つ(および実際に処理しConsumerたいa を含む他の呼び出し)では、上記のメソッドの1つ(つまりwith、最初のメソッドwithX、第三)。注意する必要がある小さな変更が1つだけあります。

  • 最初のパラメータ(上の制約t::setNumber に対応している Consumer<R>)であろう低減するR <: Number代わりにNumber <: R、それはの場合と同様Supplier<R>。これは、削減に関するリンクされたドキュメントで説明されています。

私は、読者がこの追加の知識を備えた上記の手順のいずれかを慎重に実行して、特定の呼び出しがコンパイルされるまたはコンパイルされない理由を正確に示すための練習問題として残します。


非常に詳細で、よく研究され、定式化されています。ありがとう!
Zabuzard

@ user31601サプライヤーと消費者の違いがどこに関係するのかを指摘していただけますか。そのための元の質問に拡張例を追加しました。サプライヤ/コンシューマに応じて、letBe()、letBeX()、let()。be()の異なるバージョンの共変、反変、および不変の動作を示しています。
jukzi

@jukziいくつかのメモを追加しましたが、これらの新しい例を自分で処理するのに十分な情報が必要です。
user31601

それは興味深い:18.2.1の非常に多くの特殊なケース。私の素朴な理解からそれらのための特別なケースをまったく期待しなかったであろうラムダとメソッド参照のために。そしておそらく普通の開発者は期待しないでしょう。
jukzi

ええと、その理由は、ラムダとメソッド参照の場合、コンパイラはラムダが実装する適切な型を決定する必要があるためだと思います-選択する必要があります!例えば、TypeInference::getLongimlementができSupplier<Long>たりSupplier<Serializable>またはSupplier<Number>など、しかし決定的にそれが唯一(ただ他のクラスと同様に)それらのいずれかを実装することができます!これは、他のすべての式とは異なり、実装された型はすべて事前に認識されており、コンパイラーは、それらの1つが制約要件を満たすかどうかを計算する必要があります。
user31601
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.