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

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

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

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

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

ベストアンサー1

この特定のシナリオだけでなく、全体としての質問に対処するために、私たち全員が知りたいと思うのは...

Java 8 でインターフェース汚染が発生する理由

たとえば、C#のような言語では、任意の数の引数とオプションの戻り値の型を受け入れる定義済みの関数型のセットがあります(機能そしてアクションそれぞれ最大16個の異なる型のパラメータT1T2、、、T3...、T16)を持ちますが、JDK 8では、異なる機能インタフェースのセットがあり、違う名前と別の方法名前と抽象メソッドはよく知られているサブセットを表す関数の引数(つまり、ゼロ項、単項、二項、三項など)。そして、プリミティブ型を扱うケースが爆発的に増加し、さらに機能的なインターフェースが爆発的に増加するシナリオもあります。

型消去の問題

つまり、ある意味では、どちらの言語も何らかの形のインターフェース汚染(C#ではデリゲート汚染)に悩まされている。唯一の違いは、C#ではすべてが同じ名前であることだ。残念ながら、Javaでは型消去Function<T1,T2>Function<T1,T2,T3>またはの間には違いがないのでFunction<T1,T2,T3,...Tn>、明らかに、すべてに同じ名前を付けることはできず、すべての可能な関数の組み合わせに独創的な名前を付ける必要がありました。これに関する詳細については、を参照してください。ジェネリック医薬品の入手方法ブライアン・ゲッツ著。

専門家グループがこの問題に苦労しなかったとは思わないでください。ブライアン・ゲッツの言葉を借りれば、ラムダメーリングリスト:

[...] 1 つの例として、関数型を取り上げてみましょう。devoxx で提案されたラムダのストローマンには関数型がありました。私は関数型を削除するよう主張し、それが不評を買いました。しかし、関数型に対する私の反対は、関数型が嫌いだからではなく (私は関数型が大好きです)、関数型が Java 型システムの既存の側面である消去とうまく競合しないからでした。消去された関数型は、両方の世界の最悪のものです。そのため、これを設計から削除しました。

しかし、私は「Java には関数型が絶対にない」とは言いたくありません (Java には関数型が絶対にないかもしれないことは認識していますが)。関数型に到達するには、まず消去に対処する必要があると私は考えています。それが可能かどうかはわかりません。しかし、具体化された構造型の世界では、関数型はより意味を持ち始めます [...]

このアプローチの利点は、任意の数の引数を受け入れるメソッドを持つ独自のインターフェース型を定義でき、必要に応じてラムダ式やメソッド参照を作成できることです。言い換えると、世界を汚染する力さらに新しい関数型インターフェースが追加されました。また、JDK の以前のバージョンのインターフェースや、このような SAM 型を定義した独自の API の以前のバージョンに対しても、ラムダ式を作成できます。そのため、関数型インターフェースとしてRunnableと を使用できるようになりましたCallable

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

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

値タイプの不足の問題

明らかに、型消去はここでの原動力の1つです。しかし、似たような名前とメソッドシグネチャを持ち、違いはプリミティブ型の使用だけであるこれらの追加の関数型インターフェースがなぜ必要なのか疑問に思う人がいるなら、Javaではまた 値の型の不足C# のような言語の場合と同様です。つまり、ジェネリック クラスで使用されるジェネリック型は参照型のみであり、プリミティブ型は使用できません。

つまり、これはできません:

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

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

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

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

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

[...] より一般的には、特殊なプリミティブ ストリーム (IntStream など) を持つという考え方には、厄介なトレードオフが伴います。一方では、多くの醜いコードの重複、インターフェイスの汚染などが発生します。他方では、ボックス化された演算のあらゆる種類の演算はひどいものであり、int の削減に関するストーリーがないのはひどいことです。そのため、私たちは厳しい状況にあり、状況を悪化させないように努めています。

状況を悪化させないための秘訣その 1 は、8 つのプリミティブ型すべては実行しないことです。実行しているのは int、long、double です。他のすべてはこれらでシミュレートできます。int も削除できるかもしれませんが、ほとんどの Java 開発者がそれに対応する準備ができているとは思えません。確かに、Character を求める声はありますが、その答えは「int に詰め込む」ことです (各特殊化は、JRE フットプリントに対して約 100K になると予測されています)。

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

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

この件に関するさらなる参考として、以下をお読みください。値型の現状著者:ジョン・ローズ、ブライアン・ゲッツ、ガイ・スティール。

チェック例外の問題

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

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

writeこれは、操作がチェック例外(つまりIOException)をスローしますが、メソッドのシグネチャでは例外をスローすると宣言されていないため、実行できませんConsumer。したがって、この問題の唯一の解決策は、さらに多くのインターフェイスを作成し、いくつかは例外を宣言し、いくつかは宣言しないことです(または、言語レベルでさらに別のメカニズムを考え出す必要があります)。例外の透明性再び、事態を「少しでも悪くしない」ために、専門家グループはこの件では何もしないことに決めました。

ブライアン・ゲッツの言葉を借りれば、ラムダメーリングリスト:

[...] はい、独自の例外的な SAM を提供する必要があります。ただし、その場合、ラムダ変換は問題なく機能します。

EG はこの問題に対する追加の言語およびライブラリ サポートについて議論しましたが、最終的には、これはコストと利益のバランスが悪いと感じました。

ライブラリベースのソリューションは、SAM タイプ (例外的 vs. 例外的でない) の 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));

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

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

おすすめ記事