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


99

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

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

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

要するに:

Java 8より前:

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

Java 8以降:

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

このような大きな変更の主な理由は何ですか?また、追加された新しいメリット(ある場合)は何ですか?


30
ボーナス質問:代わりにクラスの多重継承を導入しなかったのはなぜですか?

2
静的メソッドはインターフェイスに属しません。これらはユーティリティクラスに属します。いいえ、彼らは@Deprecatedカテゴリに属します!静的メソッドは、無知と怠のために、Javaで最も悪用される構成要素の1つです。静的メソッドの多くは通常、無能なプログラマーを意味し、結合を数桁増加させます。そして、なぜそれらが悪い考えであるかを理解すると、単体テストとリファクタリングに悪夢です!

11
@JarrodRoberson「あなたはなぜ彼らが悪い考えであることに気付いたとき、ユニットテストとリファクタリングに悪夢です!」に関するいくつかのヒント(リンクは素晴らしいでしょう)を提供できますか?私はそれを考えたこともなかったし、それについてもっと知りたい。
水っぽい14

11
@Chris状態の多重継承は、特にメモリの割り当て(古典的なダイアモンドの問題)で多くの問題を引き起こします。ただし、動作の多重継承は、インターフェイスによって既に設定されているコントラクトに適合する実装にのみ依存します(インターフェイスは、宣言および要求する他のメソッドを呼び出すことができます)。微妙ですが、非常に興味深い違いです。
ssube 14年

1
既存のインターフェースにメソッドを追加したい場合、java8がデフォルトのメソッドとして追加できるようになるまで、面倒だったりほとんど不可能でした。
VdeX

回答:


59

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

list.sort(ordering);

の代わりに

Collections.sort(list, ordering);

そうでなければ、複数の同一の実装がなければ、それを行うことができなかったと思いますList.sort


19
C#は、拡張メソッドを使用してこの問題を解決します。
ロバート・ハーヴェイ

5
それがリンクされたリストは、Java 7には、所定の位置にマージすることができるので、リンクされたリストは、それが外部の配列にダンプしてからソートこと、(1)余分なスペースとO時間のマージ(N Nログ)Oを使用することができます
ラチェットフリーク

5
Goetzが問題を説明しているこの論文を見つけました。したがって、私はこの答えを今のところ解決策としてマークします。
ミスタースミス14年

1
@RobertHarvey:2億項目のList <Byte>を作成IEnumerable<Byte>.Appendし、それらを使用して結合し、を呼び出してCount、拡張メソッドが問題を解決する方法を教えてください。場合CountIsKnownCountのメンバーだったIEnumerable<T>、からの復帰はAppend宣伝ができCountIsKnown、構成コレクションがなかった場合、しかし、ことはできません、このような方法なし。
supercat

6
@supercat:あなたが何について話しているのか、私には少しでも分かりません。
ロバートハーヴェイ14年

48

正しい答えは、実際にはJava Documentationに記載されています。

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

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

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

同様の注意として、これらの新機能がどのように役立つかという例は、最近私を悩ませる1つのクラスを考慮することですjava.util.UUIDUUIDタイプ 1、2、または5のサポートは実際には提供されず、そのために容易に変更することはできません。また、オーバーライドできない事前定義済みのランダムジェネレーターもあります。サポートされていないUUIDタイプのコードを実装するには、インターフェイスではなくサードパーティAPIに直接依存するか、変換コードのメンテナンスとそれに伴う追加のガベージコレクションのコストが必要です。静的メソッドを使用すると、UUID代わりにインターフェイスとして定義でき、欠落している部分の実際のサードパーティ実装が可能になります。(UUID元々インターフェースとして定義されていた場合、おそらくある種の不格好なものがあるでしょうUuidUtil 多くのJavaのコアAPIは、インターフェイスに基づいていないために低下しますが、Java 8では、この悪い動作の言い訳の数はありがたいことに減少しました。

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

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


1
私の最後の段落でさらにフォローアップする(つまり、インターフェイスで契約を強制しない)ために、defaultメソッドがequalshashCodeおよびをオーバーライドできないことを指摘する価値がありますtoString。これは許可されない理由の非常に有益な費用/便益分析は、ここで見つけることができます:mail.openjdk.java.net/pipermail/lambda-dev/2013-March/...を
ngreen

Javaが単一の仮想メソッドequalsと単一のhashCodeメソッドしか持たないのは、コレクションがテストする必要のある2種類の同等性があり、複数のインターフェイスを実装するアイテムが競合する契約要件に固執する可能性があるため、残念です。hashMapキーとして変更されないリストを使用できると便利ですがhashMap、現在の状態ではなく等価性に基づいて一致するものにコレクションを保存すると役立つ場合もあります[等価性は状態と不変性の一致を意味します] 。
supercat 14年

Javaには、ComparatorおよびComparableインターフェースを使用した回避策があります。しかし、それらはちょっといです。
ngreen

これらのインターフェイスは特定のコレクションタイプでのみサポートされており、独自の問題を引き起こします:コンパレーター自体が状態をカプセル化する可​​能性があります(たとえば、特殊なストリングコンパレーターは各ストリングの開始時に構成可能な文字数を無視する場合があり、その場合、無視する文字はコンパレーターの状態の一部になります)、ソートされたコレクションの状態の一部になりますが、2つのコンパレーターが同等かどうかを確認するメカニズムは定義されていません。
supercat

そうそう、コンパレーターの苦痛を感じます。私はツリー構造に取り組んでいますが、それは単純であるべきですが、コンパレーターを正しくするのが難しいからではありません。問題を解決するためだけに、カスタムツリークラスを作成するつもりです。
ngreen

44

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

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


インターフェースで実装を提供できることがなぜ役に立つのかという動機付けの例として、次のStackインターフェースを検討してください。

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

    T pop() throws EmptyException;
 }

誰かがインターフェースを実装したときにpop、スタックが空の場合に例外をスローすることを保証する方法はありません。このルールは、コントラクトを実行するメソッドと実際のポップを実行するメソッドのpop2つのメソッドに分けることで実行できます。public finalprotected 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のアプローチが最終メソッドまたは保護された抽象メソッドの追加を許可するかどうかはわかりませんが、D言語がそれを許可し、Design by Contractのネイティブサポートを提供することを知っています。popfinalであるため、この手法には危険はありません。したがって、実装クラスはそれをオーバーライドできません。

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

また、

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

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


インターフェイスで静的メソッドを優先する例として、Java APIのコレクションのようなユーティリティクラスを検討してください。これらの静的メソッドは、それぞれのインターフェイスで宣言できないため、そのクラスのみが存在します。インターフェースでCollections.unmodifiableList宣言することもできたListでしょうし、見つけやすかったでしょう。


4
反論:静的メソッドは、適切に記述されていれば自己完結型であるため、クラス名ごとに収集および分類できる別の静的クラスでより意味があり、本質的に便利なインターフェースではあまり意味がありませんオブジェクトに静的状態を保持したり、副作用を引き起こしたり、静的メソッドをテストできないなどの悪用を招きます。
ロバートハーヴェイ14年

3
@RobertHarvey静的メソッドがクラスにある場合でも、同様に愚かなことをするのを止めるものは何ですか?また、インターフェイス内のメソッドは状態をまったく必要としない場合があります。単に契約を実施しようとしているだけかもしれません。あなたが持っていると仮定Stackインターフェースを、あなたはときようにしたいpop空のスタックと呼ばれ、例外がスローされます。抽象メソッドboolean isEmpty()とを指定するとprotected T pop_impl()、実装できます。final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }これにより、すべての実装者に契約が強制されます。
ドーバル14年

待って、何?スタック上のプッシュメソッドとポップメソッドはそうなりませんstatic
ロバートハーヴェイ14年

@RobertHarveyコメントの文字数制限がなければ明確になっていたでしょうが、静的メソッドではなく、インターフェイスのデフォルト実装について主張していました。
ドーバル14年

8
デフォルトのインターフェースメソッドは、標準ライブラリを拡張するために導入されたハックであり、それに基づいて既存のコードを適応させる必要はないと思います。
ジョルジオ14年

2

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

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


1
拡張メソッドは、インターフェイスに機能を追加しません。拡張メソッドは、便利なlist.sort(ordering);形式を使用してクラスの静的メソッドを呼び出すための単なる構文上の砂糖です。
ロバートハーヴェイ14年

IEnumerableC#のインターフェイスを見ると、そのインターフェイスに拡張メソッドを実装すると(実装するのと同じようにLINQ to Objects)を実装するすべてのクラスに機能が追加されることがわかりますIEnumerable。機能を追加するということです。
rae1

2
それが拡張メソッドの素晴らしいところです。これらは、機能をクラスまたはインターフェースに追加しているような錯覚を与えます。それをクラスに実際のメソッドを追加することと混同しないでください。クラスメソッドはオブジェクトのプライベートメンバーにアクセスできますが、拡張メソッドはアクセスできません(静的メソッドを呼び出す別の方法であるため)。
ロバートハーヴェイ14年

2
まさに、だからこそ、Javaのインターフェイスに静的メソッドまたはデフォルトメソッドを持つことに何らかの関係があるのです。実装は、クラス自体ではなく、インターフェイスで利用可能なものに基づいています。
rae1 14年

1

defaultメソッドに見られる2つの主な目的(いくつかのユースケースは両方の目的に役立ちます):

  1. 構文シュガー。ユーティリティクラスはその目的に役立ちますが、インスタンスメソッドの方が優れています。
  2. 既存のインターフェースの拡張。実装は一般的ですが、時には非効率的です。

それが第2の目的にすぎない場合、のようなまったく新しいインターフェースには表示されませんPredicate@FunctionalInterfaceラムダが実装できるように、すべての注釈付きインターフェイスには抽象メソッドが1つだけ必要です。追加しましたdefaultような方法にはandornegateちょうどユーティリティであり、あなたはそれらを上書きすることになっていません。ただし、静的メソッドのほうが優れている場合があります

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


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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.