web-dev-qa-db-ja.com

すでに抽象クラスがあるのに、デフォルトメソッドと静的メソッドがJava 8のインターフェイスに追加されたのはなぜですか?

Java 8では、インターフェースに実装済みメソッド、静的メソッド、およびいわゆる「デフォルト」メソッド(実装クラスがオーバーライドする必要がない)を含めることができます。

私の(おそらくナイーブな)見方では、このようなインターフェースに違反する必要はありませんでした。インターフェイスは常にあなたが満たさなければならない契約であり、これは非常にシンプルで純粋なコンセプトです。今ではいくつかのものが混在しています。私の考えでは:

  1. 静的メソッドはインターフェースに属していません。それらはユーティリティクラスに属します。
  2. 「デフォルト」のメソッドは、インターフェースで許可されるべきではありませんでした。この目的には、常に抽象クラスを使用できます。

要するに:

前Java 8:

  • 抽象クラスと通常クラスを使用して、静的メソッドとデフォルトメソッドを提供できます。インターフェイスの役割は明らかです。
  • インターフェースのすべてのメソッドは、クラスを実装することによってオーバーライドする必要があります。
  • すべての実装を変更せずに新しいメソッドをインターフェイスに追加することはできませんが、これは実際には良いことです。

Java 8の後:

  • インターフェースと抽象クラスの間に実質的な違いはありません(多重継承を除く)。実際、通常のクラスをインターフェースでエミュレートできます。
  • 実装をプログラミングするとき、プログラマーはデフォルトのメソッドをオーバーライドすることを忘れることがあります。
  • クラスが同じシグネチャを持つデフォルトのメソッドを持つ2つ以上のインターフェースを実装しようとすると、コンパイルエラーが発生します。
  • デフォルトのメソッドをインターフェースに追加することにより、すべての実装クラスがこの動作を自動的に継承します。これらのクラスの一部は、その新しい機能を考慮して設計されていない場合があり、これが問題を引き起こす可能性があります。たとえば、誰かが新しいデフォルトのメソッドdefault void foo()をインターフェースIxに追加した場合、クラスCxIxを実装し、プライベートfooメソッドはコンパイルされません。

このような大きな変更の主な理由は何ですか?また、それらが追加する新しい利点(ある場合)は何ですか?

103
Mister Smith

デフォルトのメソッドの動機付けの良い例は、Java標準ライブラリにあります。

list.sort(ordering);

の代わりに

Collections.sort(list, ordering);

そうでなければ、List.sortの同一の実装が2つ以上なければ、そうすることはできなかったでしょう。

59
soru

正しい答えは、実際には Java Documentation にあります。

[d] efaultメソッドを使用すると、ライブラリのインターフェイスに新しい機能を追加し、それらのインターフェイスの古いバージョン用に作成されたコードとのバイナリ互換性を確保できます。

インターフェースが公開されるとインターフェースを進化させることが不可能になる傾向があるため、これはJavaにとって長年の痛みの原因でした。 (ドキュメントの内容は、コメントでリンクしたペーパーに関連しています: 仮想拡張メソッドによるインターフェースの進化 。)さらに、新機能(ラムダや新しいストリームAPIなど)の迅速な採用既存のコレクションインターフェイスを拡張し、デフォルトの実装を提供することによってのみ実行できます。バイナリ互換性を壊したり、新しいAPIを導入したりすると、Java 8の最も重要な機能が一般的に使用されるようになるまでには数年かかります。

インターフェイスで静的メソッドを許可する理由は、ドキュメントによって再び明らかになります:[t] hisを使用すると、ライブラリ内のヘルパーメソッドを整理しやすくなります。別のクラスではなく、同じインターフェイスのインターフェイスに固有の静的メソッドを保持できます。つまり、 Java.util.Collections が(最終的に)一般的なアンチパターンと見なされるようになりました(もちろんalwaysではありません)。私の推測では、仮想拡張メソッドが実装された後は、この動作のサポートを追加することは簡単でした。そうでなければ、おそらく実行されなかったでしょう。

同様に、これらの新機能がどのように役立つかを示す例として、最近私を困らせたクラス Java.util.UUIDIDタイプ 1、2、または5のサポートは実際には提供されていません。そのため、簡単に変更することはできません。また、オーバーライドできない事前定義のランダムジェネレーターが残っています。サポートされていないUUIDタイプのコードを実装するには、インターフェースではなくサードパーティのAPIに直接依存するか、変換コードのメンテナンスとそれに伴うガベージコレクションの追加コストが必要です。静的メソッドを使用すると、代わりにUUIDをインターフェイスとして定義して、不足している部分の実際のサードパーティによる実装を可能にすることができます。 (もしUUIDが元々インターフェースとして定義されていたなら、恐らく静的メソッドを持つある種の不格好なUuidUtilクラスがあり、それもひどいでしょう。)多くのJavaのコアAPIはインターフェースに基づくことに失敗しましたが、Java 8現在、この悪い動作の言い訳の数は、ありがたいことに減少しています。

[t]インターフェイスと抽象クラスの違いはほとんどありません。抽象クラスは状態を持つことができます(つまり、宣言します)フィールド)ながら、インターフェイスはできません。したがって、多重継承や、ミックスインスタイルの継承と同等ではありません。適切なミックスイン(Groovy 2.3の traits など)は状態にアクセスできます。 (Groovyは静的拡張メソッドもサポートしています。)

私の意見では、 Doval の例に従うことも良い考えではありません。インターフェイスはコントラクトを定義することになっていますが、コントラクトを強制することは想定されていません。 (いずれにせよJavaとにかく。)実装の適切な検証は、テストスイートまたは他のツールの責任です。契約の定義は、アノテーションを使用して行うことができます OVal は良い例ですが、インターフェイスで定義された制約をサポートするかどうかはわかりません。このようなシステムは、現在存在しない場合でも実現可能です(戦略には、javacのコンパイル時のカスタマイズが含まれます- 注釈プロセッサ APIと実行時のバイトコード生成。)理想的には、コントラクトはコンパイル時に強制され、最悪の場合はテストスイートを使用しますが、私の理解では、実行時の強制は不快です。別の興味深いツールJavaは Checker Framework です)で契約プログラミングを支援する可能性があります。

50
ngreen

継承できるクラスは1つだけだからです。実装が複雑で、抽象基本クラスが必要な2つのインターフェースがある場合、これら2つのインターフェースは実際には相互に排他的です。

代わりの方法は、これらの抽象基本クラスを静的メソッドのコレクションに変換し、すべてのフィールドを引数に変換することです。これにより、インターフェイスのすべての実装者が静的メソッドを呼び出して機能を取得できますが、これは、言語が多すぎて非常に冗長な定型文です。


インターフェースで実装を提供できると便利な理由の動機付けの例として、このStackインターフェースを検討してください。

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

誰かがインターフェイスを実装したときに、スタックが空の場合、popが例外をスローすることを保証する方法はありません。 popを2つのメソッドに分割することにより、このルールを適用できます。契約を適用するpublic finalメソッドと、実際のポップを実行するprotected abstractメソッドです。

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

すべての実装がコントラクトを尊重することを保証するだけでなく、スタックが空であるかどうかをチェックして例外をスローする必要がないようにしました。それは大きな勝利です!...インターフェイスを抽象クラスに変更する必要があったという事実を除いて。単一継承の言語では、これは柔軟性の大きな損失です。それはあなたのインターフェースを相互に排他的にします。インターフェースメソッド自体にのみ依存する実装を提供できれば、問題は解決します。

Java 8のメソッドをインターフェイスに追加するための8のアプローチでfinalメソッドまたは保護された抽象メソッドを追加できるかどうかはわかりませんが、 D言語 はそれを許可し、ネイティブを提供します Design by Contract のサポートpopが最終的なため、この手法に危険はなく、実装するクラスはそれをオーバーライドできません。

オーバーライド可能なメソッドのデフォルト実装については、Java APIに追加されたデフォルト実装は、それらが追加されたインターフェースの規約にのみ依存するため、インターフェースを正しく実装するクラスもすべてデフォルトの実装で正しく動作します。

また、

インターフェースと抽象クラスの間に実質的な違いはありません(多重継承を除く)。実際、通常のクラスをインターフェースでエミュレートできます。

インターフェースでフィールドを宣言できないため、これはまったく当てはまりません。インターフェースで記述するメソッドは、実装の詳細に依存できません。


インターフェースの静的メソッドを支持する例として、Java APIの Collections のようなユーティリティクラスを検討してください。このクラスは、静的メソッドを宣言できないためにのみ存在します。 Collections.unmodifiableListListインターフェースで宣言することもでき、簡単に見つけることができます。

44
Doval

おそらく、その目的は、依存関係を介して静的な情報や機能を注入する必要性を置き換えることによって、 mixin クラスを作成する機能を提供することでした。

このアイデアは、C#の拡張メソッドを使用して、実装された機能をインターフェイスに追加する方法に関連しているようです。

2
rae1

defaultメソッドで見られる2つの主な目的(いくつかのユースケースは両方の目的を果たします):

  1. 構文糖。ユーティリティクラスはその目的を果たすことができますが、インスタンスメソッドの方が優れています。
  2. 既存のインターフェースの拡張。実装は一般的ですが、非効率的な場合があります。

それが2番目の目的にすぎない場合、Predicateのような新しいインターフェースではそれがわかりません。すべての_@FunctionalInterface_注釈付きインターフェースは、ラムダが実装できるように、1つの抽象メソッドを持つ必要があります。 defaultandorのような追加されたnegateメソッドはユーティリティであり、オーバーライドする必要はありません。ただし、 静的メソッドの方が優れている場合があります

既存のインターフェースの拡張に関しては-そこにさえ、いくつかの新しいメソッドは単なるシンタックスシュガーです。 CollectionのようなstreamforEachremoveIfのメソッド-基本的に、これはオーバーライドする必要のないユーティリティです。そして、spliteratorのようなメソッドがあります。デフォルトの実装は最適ではありませんが、少なくともコードはコンパイルされます。インターフェースがすでに公開され、広く使用されている場合にのみ、これに頼ってください。


staticメソッドについては、他のメソッドが非常にうまくカバーしていると思います。これにより、インターフェイスを独自のユーティリティクラスにすることができます。たぶん、Javaの将来にCollectionsを取り除くことができるでしょうか? Set.empty()はすばらしいでしょう。

1
Vlasec