web-dev-qa-db-ja.com

Java 8の新しいJava.util.Arraysメソッドがすべてのプリミティブ型でオーバーロードされないのはなぜですか?

Java 8のAPIの変更を確認しましたが、Java.util.Arraysの新しいメソッドがすべてのプリミティブでオーバーロードされていないことに気付きました。気づいたメソッドは次のとおりです。

現在、これらの新しいメソッドは、intlong、およびdoubleプリミティブのみを処理します。

intlong、およびdoubleはおそらく最も広く使用されているプリミティブであるため、APIを制限する必要がある場合、これら3つを選択することは理にかなっていますが、なぜそうしたのかAPIを制限する必要がありますか?

54
Matthew M

この特定のシナリオだけでなく、質問全体に取り組むために、私たちは皆知りたいと思います...

Java 8にインターフェース汚染がある理由

たとえば、C#のような言語では、任意の数の引数を受け入れる定義済み関数型のセットがあり、オプションの戻り値型( Func および Action それぞれ) 1つは、異なるタイプの16個までのパラメーターT1T2T3、...、T16)ですが、JDK 8にはdifferent名前と- 別のメソッド名前、およびその抽象メソッドは、よく知られている function arities (つまり、nullary、unary、binary、ternaryなど)のサブセットを表します。そして、プリミティブ型を扱うケースが爆発的に増え、さらに機能的なインターフェースの爆発を引き起こす他のシナリオさえあります。

タイプ消去の問題

したがって、ある意味で、両方の言語は何らかの形のインターフェイス汚染(またはC#のデリゲート汚染)に悩まされています。唯一の違いは、C#ではすべて同じ名前であることです。残念ながら、Javaでは type erasure が原因で、Function<T1,T2>Function<T1,T2,T3>またはFunction<T1,T2,T3,...Tn>の間に違いはないため、当然のことながら、それらすべてに同じ名前を付けることはできず、考え出す必要がありました。可能なすべてのタイプの関数の組み合わせのクリエイティブ名。

専門家グループがこの問題に苦労しなかったとは思わないでください。 lambdaメーリングリスト のBrian Goetzの言葉:

[...]単一の例として、関数型を取り上げましょう。 devoxxで提供されているラムダストローマンには関数型がありました。私はそれらを削除すると主張し、これは私を不人気にしました。しかし、関数型への私の反対は、関数型が好きではないということではありません-関数型が大好きです-しかし、その関数型は、Java型システムの既存の側面、消去とひどく戦っていました。消去された関数タイプは、両方の世界で最悪です。したがって、これをデザインから削除しました。

しかし、「Javaは関数型を持たない」とは言いたくありません(ただし、Javaは関数型を持たない可能性があることは認識しています)。関数型に到達するためには、まず、消去。それは可能かもしれませんし、可能でないかもしれません。しかし、具体化された構造型の世界では、関数型はより理にかなっています[...]

このアプローチの利点は、必要な数の引数を受け入れるメソッドを使用して独自のインターフェイス型を定義でき、それらを使用して適切なラムダ式とメソッド参照を作成できることです。言い換えれば、我々には世界を汚染する力があり、さらに新しい機能的なインターフェースがあります。また、JDKの以前のバージョンのインターフェースや、これらのようなSAMタイプを定義した独自のAPIの以前のバージョンでも、ラムダ式を作成できます。これで、RunnableCallableを機能インターフェイスとして使用できるようになりました。

ただし、これらのインターフェイスはすべて名前とメソッドが異なるため、覚えるのが難しくなります。

それでも、なぜScalaのように問題を解決できなかったのか、Function0Function1Function2、...、FunctionNなどのインターフェースを定義しているのではないかと思う人もいます。おそらく、それに対して私が思いつくことができる唯一の議論は、前述のように、APIの以前のバージョンでインターフェースのラムダ式を定義する可能性を最大化したかったということです。

値タイプの不足の問題

したがって、ここでは明らかに型消去が1つの原動力です。しかし、同様の名前とメソッドシグネチャを持つこれらすべての追加の機能インターフェイスも必要であり、その唯一の違いがプリミティブ型の使用である理由について疑問に思っている方は、Javaでお知らせします。 also値のタイプの欠如 C#のような言語の値タイプのように。つまり、ジェネリッククラスで使用されるジェネリック型は参照型にのみなり、プリミティブ型にはなりません。

つまり、これは実行できません。

List<int> numbers = asList(1,2,3,4,5);

しかし、実際にこれを行うことができます。

List<Integer> numbers = asList(1,2,3,4,5);

ただし、2番目の例では、プリミティブ型との間でラップされたオブジェクトをボックス化およびボックス化解除するコストが発生します。これは、プリミティブ値のコレクションを処理する操作で非常に高価になる可能性があります。それで、専門家グループはこれを作成することを決定しましたインターフェースの爆発さまざまなシナリオに対処するため。物事を「悪化させない」ために、彼らは3つの基本的なタイプ(int、long、double)のみを扱うことにしました。

lambdaメーリングリスト でのブライアン・ゲッツの言葉を引用する:

[...]より一般的には、特殊なプリミティブストリーム(IntStreamなど)の背後にある哲学には、厄介なトレードオフが伴います。一方で、コードの重複、インターフェイスの汚染など、多くの醜いコードです。他方、ボックス化されたopsでのあらゆる種類の計算は失敗し、intを削減するためのストーリーがないのはひどいでしょう。だから私たちは厳しい状況にあり、それを悪化させないようにしています。

悪化させないための秘訣1は、8つのプリミティブ型すべてを実行していないことです。 int、long、doubleを実行しています。他のすべてはこれらによってシミュレートできます。おそらくintも取り除くことができますが、ほとんどのJava開発者がその準備ができていないと思います。はい、Characterの呼び出しがあり、答えは「intに貼り付ける」です。 (各専門分野は、JREフットプリントに対して〜100Kと予測されています。)

トリック#2は、プリミティブストリームを使用して、プリミティブドメイン(ソート、リダクション)で最適に行われることを公開していますが、ボックス化ドメインで実行できるすべてを複製しようとはしていません。たとえば、Alekseyが指摘するように、IntStream.into()はありません。 (ある場合、次の質問は「IntCollectionはどこですか?IntArrayList?IntConcurrentSkipListMap?」でしょう。)多くのストリームが参照ストリームとして開始され、プリミティブストリームとして終了する可能性がありますが、その逆ではありません。それは問題ありません。必要な変換の数を減らします(たとえば、int-> Tのマップのオーバーロードなし、int-> Tの関数の特殊化なしなど)[...]

これは専門家グループにとって難しい決定であったことがわかります。これがクールだと思う人はほとんどいないと思います。ほとんどの人は、これが必要だと同意するでしょう。

チェックされた例外の問題

事態をさらに悪化させる可能性があったである3番目の原動力があり、Javaがチェック済みとチェックなしの2種類の例外をサポートしているのは事実です。コンパイラーは、チェック済み例外を処理または明示的に宣言する必要がありますが、チェックなし例外には何も必要ありません。そのため、ほとんどの機能インターフェイスのメソッドシグネチャが例外のスローを宣言していないため、これは興味深い問題を引き起こします。したがって、たとえば、これは不可能です。

Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //oops! compiler error

writeオペレーションがチェック例外(つまり、IOException)をスローしますが、Consumerメソッドのシグネチャが例外をスローすると宣言していないため、これを行うことはできません。したがって、この問題の唯一の解決策は、さらに多くのインターフェイスを作成し、例外を宣言するものとしないものを作成することでした(または exception transparent の言語レベルでさらに別のメカニズムを考案します)。事態を「悪化」させるために、専門家グループはこの場合何もしないことを決定しました。

lambdaメーリングリスト のBrian Goetzの言葉:

[...]はい、独自の優れたSAMを提供する必要があります。しかし、ラムダ変換はそれらでうまく機能します。

EGはこの問題に対する追加の言語とライブラリのサポートについて話し合い、結局これはコストと利益のトレードオフとしては悪いものだと感じました。

ライブラリベースのソリューションは、SAMタイプで2倍の爆発を引き起こし(例外的なものとそうでないもの)、プリミティブな特殊化のために既存の組み合わせの爆発とうまく相互作用しません。

利用可能な言語ベースのソリューションは、複雑さと値のトレードオフから負けました。いくつかの代替ソリューションがありますが、今後も調査を続けていきますが、明らかに8向けではなく、おそらく9向けでもありません。

その間、あなたはあなたが望むことをするためのツールを持っています。私はあなたにラストマイルを提供することを好むと思います(そして、第二に、あなたの要求は本当に「すでにチェックされた例外をあきらめないのですか?」あなたはあなたの仕事を成し遂げる。 [...]

したがって、これらをケースバイケースで処理するかどうかは、開発者であるまださらに多くのインターフェース爆発を作成することです。

interface IOConsumer<T> {
   void accept(T t) throws IOException;
}

static<T> Consumer<T> exceptionWrappingBlock(IOConsumer<T> b) {
   return e -> {
    try { b.accept(e); }
    catch (Exception ex) { throw new RuntimeException(ex); }
   };
}

それをするために:

Writer out = new StringWriter();
Consumer<String> printer = exceptionWrappingBlock(s -> out.write(s));

おそらく、(おそらくJDK 9で) Javaでの値の型のサポート およびReificationを取得すると、それを取り除くことができます(または、少なくとももう使用する必要がなくなります)。これらの複数のインターフェースの一部。

要約すると、専門家グループがいくつかの設計上の問題に苦しんでいたことがわかります。下位互換性を維持するための必要性、要件、または制約のために物事が困難になり、値型の欠如、型の消去、チェックされた例外など、他の重要な条件があります。 Javaに最初の2つがあり、他の2つが欠けていた場合、JDK 8の設計はおそらく異なるでしょう。したがって、これらは多くのトレードオフを伴う難しい問題であり、EGはどこかに線を引いて決定を行わなければならなかったことをすべて理解する必要があります。

79
Edwin Dalorzo