Javaがスーパータイプを推測できないのはなぜですか?


19

Longが拡張することは誰もが知っていNumberます。では、なぜこれがコンパイルされないのでしょうか?

またwith、プログラムを手動キャストなしでコンパイルできるようにメソッドを定義する方法は?

import java.util.function.Function;

public class Builder<T> {
  static public interface MyInterface {
    Number getNumber();
    Long getLong();
  }

  public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
    return null;//TODO
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::getLong, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
    // works:
    new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
    // works:
    new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
    // compiles but also involves typecast (and Casting Number to Long is not even safe):
    new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
    // compiles but also involves manual conversion:
    new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
    // compiles (compiler you are kidding me?): 
    new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);

  }
  static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
    return f;
  }

}
  • の型引数を推測できません <F, R> with(F, R)
  • タイプBuilder.MyInterfaceからのgetNumber()のタイプは数値です。これは記述子の戻りタイプと互換性がありません:Long

使用例については、コンパイル時にラムダの戻り値型がチェックされない理由を参照してください。


投稿できますMyInterfaceか?
モーリスペリー

すでにクラスの内部
jukzi

うーん、試してみまし<F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)たがjava.lang.Number cannot be converted to java.lang.Long)、からの戻り値getterをに変換する必要があるというコンパイラの考えがどこにあるかわからないので、これは驚くべきことreturnValueです。
jingx

@jukzi OK。聞き取れませんでした。
モーリスペリー

に変更Number getNumber()すると<A extends Number> A getNumber()、機能します。これがあなたが望んでいたものかどうかはわかりません。他の人が言ったように、問題はそれMyInterface::getNumberDouble例えばではなく戻る関数である可能性があるということLongです。宣言により、コンパイラーは、存在する他の情報に基づいて戻り値の型を絞り込むことができません。汎用の戻り値の型を使用することで、コンパイラーがそうすることができるため、機能します。
ジャコモアルゼッタ

回答:


9

この表現:

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

次のように書き換えることができます:

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

メソッドの署名を考慮に入れる:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R に推測されます Long
  • F になります Function<MyInterface, Long>

そして、あなたはFunction<MyInterface, Number>これが鍵であると推論されるメソッド参照を渡します- そのLongようなシグネチャを持つ関数から実際に戻りたいことをコンパイラはどのように予測すべきですか?それはあなたのためにダウンキャスティングをしません。

以来NumberのスーパークラスであるLongNumbernecessairlyではありませんLong(それはコンパイルできない理由です) -あなたは自分で明示的にキャストする必要があります:

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

作るFことFunction<MyIinterface, Long>や、あなたが行ったように、メソッド呼び出し時に明示的に一般的な引数を渡します:

new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

そして、コードはコンパイルRされると見なされNumberます。


それは興味深い型キャストです。しかし、それでも、呼び出し元をキャストなしでコンパイルさせる「with」メソッドの定義を探しています。とにかくその考えをありがとう。
jukzi

@jukziできません。どのようにwith書いてもかまいません。あなたはそうMJyInterface::getNumber型を持っており、他の引数からも持っています(Javaリテラルは多態性ではないことにFunction<MyInterface, Number>注意R=NumberしてくださいR=Long!)。この時点で、a Numberをに変換できるとは限らないため、コンパイラは停止しLongます。この問題を解決する唯一の方法は変更することでMyInterface使用する<A extends Number> Number戻り値の型として、これは、コンパイラが持っているになりR=A、その後、R=Longそれ以来A extends Number、それは置き換えることができますA=Long
ジャコモAlzetta

4

あなたのエラーの鍵は、のタイプの一般的な宣言でありますFF extends Function<T, R>。機能しないステートメントはnew Builder<MyInterface>().with(MyInterface::getNumber, 4L);、次のとおりです。最初に、新しいがありBuilder<MyInterface>ます。したがって、クラスの宣言はを意味しT = MyInterfaceます。の宣言に従って、はwithFなければなりません。Function<T, R>これは、Function<MyInterface, R>この状況ではです。したがって、パラメーターは(メソッド参照andで満たされる)asパラメーターをgetter取り、を返す必要があります。これは、関数の2番目のパラメーターと同じ型である必要があります。さて、これがあなたのすべてのケースに当てはまるかどうか見てみましょう:MyInterfaceMyInterface::getNumberMyInterface::getLongRwith

// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time, 
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don't match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

この問題は、次のオプションで「修正」できます。

// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);

この点を超えて、それはほとんどの場合、特定のアプリケーションのコードの複雑さを減らすオプションの設計上の決定であるため、最適なものを選択してください。

キャストせずにこれを実行できない理由は、Java言語仕様の次のとおりです。

ボクシング変換は、プリミティブ型の式を対応する参照型の式として扱います。具体的には、次の9つの変換はボクシング変換と呼ばれます。

  • ブール型からブール型へ
  • バイト型からバイト型へ
  • タイプショートからタイプショートへ
  • char型からCharacter型へ
  • int型からInteger型へ
  • ロングタイプからロングタイプへ
  • float型からFloat型へ
  • タイプダブルからタイプダブルへ
  • null型からnull型へ

明らかなように、longからNumberへの暗黙のボックス化変換はありません。また、LongからNumberへの拡大変換は、コンパイラがLongではなくNumberを必要とすることが確実な場合にのみ発生します。数値を必要とするメソッド参照とLongを提供する4Lの間に矛盾があるため、コンパイラは(何らかの理由で???)Longが数値であるという論理的飛躍を実現できず、それFがであると推定できませんFunction<MyInterface, Number>

代わりに、関数シグネチャを少し編集することで問題を解決することができました。

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

この変更後、次のことが起こります。

// doesn't work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

編集:
少し時間を費やした後、ゲッターベースのタイプセーフティを適用するのは面倒です。以下は、setterメソッドを使用してビルダーのタイプセーフを適用する実用的な例です。

public class Builder<T> {

  static public interface MyInterface {
    //setters
    void number(Number number);
    void Long(Long Long);
    void string(String string);

    //getters
    Number number();
    Long Long();
    String string();
  }
  // whatever object we're building, let's say it's just a MyInterface for now...
  private T buildee = (T) new MyInterface() {
    private String string;
    private Long Long;
    private Number number;
    public void number(Number number)
    {
      this.number = number;
    }
    public void Long(Long Long)
    {
      this.Long = Long;
    }
    public void string(String string)
    {
      this.string = string;
    }
    public Number number()
    {
      return this.number;
    }
    public Long Long()
    {
      return this.Long;
    }
    public String string()
    {
      return this.string;
    }
  };

  public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
  {
    setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
    return this;
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
    // compile time error, as it shouldn't work
    new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
    // works, as it always did
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works, as it should
    new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
    // works, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, 4L);
    // compile time error, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, "blah");
  }
}

オブジェクトを作成するタイプセーフ機能を提供します。うまくいけば、将来のある時点で、ビルダーから不変のデータオブジェクトを返すことができます(おそらくtoRecord()インターフェイスにメソッドを追加し、ビルダーをとして指定することによりBuilder<IntermediaryInterfaceType, RecordType>)。したがって、結果のオブジェクトが変更されることを心配する必要さえありません。正直なところ、タイプセーフなフィールドフレキシブルビルダーを入手するのに多大な労力を必要とすることは絶対に残念ですが、いくつかの新機能、コード生成、または煩わしい量のリフレクションなしでは、おそらく不可能です。


すべての作業に感謝しますが、手動の型キャストを回避するための改善は見られません。私の素朴な理解は、コンパイラは人間ができるすべてのものを推論(つまり型キャスト)できるはずだということです。
jukzi

@jukzi私の素朴な理解は同じですが、何らかの理由でそのように機能しません。しかし、望ましい効果をほぼ達成する回避策を考え出しました
Avi

再度、感謝します。しかし、「。with(MyInterface :: getNumber、 "I AM NOT A NUMBER")」のコンパイルが許可されているため、新しい提案は広すぎます(stackoverflow.com/questions/58337639を参照)。
jukzi

「コンパイラはメソッド参照のタイプと4Lを同時に推論できない」という文章はクールです。しかし、私はそれを逆にしたいだけです。コンパイラーは、最初のパラメーターに基づいてNumberを試行し、2番目のパラメーターのNumberに拡張する必要があります。
jukzi

1
WHAAAAAAAAAAAT?BiConsumerが意図したとおりに機能するのに関数が機能しないのはなぜですか?わからない。これはまさに私が欲しかったタイプセーフであることを認めますが、ゲッターでは問題なく動作しません。なぜ、なぜ、なぜ。
jukzi

1

コンパイラが値4Lを使用してRがLongであると判断したようで、getNumber()は必ずしもLongではないNumberを返します。

しかし、値がメソッドよりも優先される理由はわかりません...


0

Javaコンパイラーは、一般に、複数の/ネストされた総称型またはワイルドカードを推論するのは得意ではありません。多くの場合、ヘルパー関数を使用して一部のタイプをキャプチャまたは推測しないと、コンパイルするものを取得できません。

しかし、あなたは本当にの正確な型キャプチャする必要がありますFunctionようにF?そうでない場合は、おそらく次のように機能し、ご覧のとおり、のサブタイプでも機能するようですFunction

import java.util.function.Function;
import java.util.function.UnaryOperator;

public class Builder<T> {
    public interface MyInterface {
        Number getNumber();
        Long getLong();
    }

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

    // example subclass of Function
    private static UnaryOperator<String> stringFunc = (s) -> (s + ".");

    public static void main(String[] args) {
        // works
        new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
        // works
        new Builder<String>().with(stringFunc, "s");

    }
}

「with(MyInterface :: getNumber、 "NOT A NUMBER")」はコンパイルしないでください
jukzi

0

最も興味深い部分は、これらの2行の違いにあると思います。

// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

最初のケースでは、T明示的であるNumber、そう4LもありませんNumber、何の問題。2番目のケースで4Lは、はでありLong、もTであるLongので、関数は互換性がなく、JavaはNumberまたはを意味しているかどうかを知ることができませんLong


0

次の署名があります:

public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)

3番目の例を除いて、すべての例がコンパイルされます。3番目の例では、メソッドに2つの型変数が必要です。

バージョンが機能しないのは、Javaのメソッド参照に特定の型がないためです。代わりに、指定されたコンテキストで必要なタイプがあります。あなたの場合、RはのLongせいであると推測されます4Lが、Function<MyInterface,Long>Javaではジェネリック型の引数は不変であるため、ゲッターは型を持つことができません。


コードはコンパイルさwith( getNumber,"NO NUMBER")れますが、これは望ましくありません。また、ジェネリックが常に不変であるとは限りません(setterのジェネリックがgetterのジェネリック以外に動作することの証明については、stackoverflow.com / a
58378661/9549750を参照

@jukziああ、私の解決策はすでにAviから提案されました。残念な... :-)。ちなみに、我々が割り当てることができるというのは本当であるThing<Cat>Thing<? extends Animal>変数が、実際の共分散のために私がいることを期待するThing<Cat>に割り当てることができますThing<Animal>。Kotlinなどの他の言語では、共変および反変の型変数を定義できます。
Hoopje
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.