9

同じシグニチャを持ついくつかのメソッドを持つが、宣言されたJavaインターフェイスに対応しないクラスがいくつかある場合があります。たとえば、JTextFieldJButton(のいくつかの他の中で javax.swing.*)の両方にメソッドがあります

public void addActionListener(ActionListener l)

ここで、そのメソッドを持つオブジェクトを使用して何かを実行したいとします。次に、インターフェイスが必要です(または、自分で定義することもできます)。

  public interface CanAddActionListener {
      public void addActionListener(ActionListener l);
  }

私が書くことができるように:

  public void myMethod(CanAddActionListener aaa, ActionListener li) {
         aaa.addActionListener(li);
         ....

しかし、悲しいことに、私はできません:

     JButton button;
     ActionListener li;
     ...
     this.myMethod((CanAddActionListener)button,li);

このキャストは違法です。クラスがそのインターフェースを実装することを宣言していないので、コンパイラーはそれがではないことを知っています...しかしそれ実際に」それを実装します。JButton CanAddActionListener

これは不便な場合があります。Java自体がいくつかのコアクラスを変更して、古いメソッドで作成された新しいインターフェイスを実装しています(String implements CharSequenceたとえば)。

私の質問は:なぜこれがそうなのか?クラスがインターフェイスを実装することを宣言することの有用性を理解しています。しかし、とにかく、私の例を見ると、なぜコンパイラーは、クラスJButtonがインターフェース宣言を「満たして」(その内部を見て)、キャストを受け入れることができないと推測できないのでしょうか?それはコンパイラの効率の問題ですか、それとももっと根本的な問題がありますか?

答えの要約:これは、Javaが何らかの「構造的型付け」(ダックタイピングのようなものですが、コンパイル時にチェックされます)を考慮に入れていた可能性がある場合です。そうではありませんでした。いくつかの(私にはわかりませんが)パフォーマンスと実装の難しさは別として、ここにははるかに基本的な概念があります。Javaでは、インターフェース(および一般的にはすべて)の宣言は、単に構造的(これらのシグニチャ)がセマンティック:メソッドは特定の動作/意図を実装することになっています。したがって、あるインターフェースを構造的に満たす(つまり、必要なシグニチャーを持つメソッドを持っている)クラスは、必ずしも意味的に満たすとは限りません。(極端な例:メソッドさえない「マーカーインターフェース」を思い出してください!)したがって、Javaは、これが明示的に宣言されているため(そしてそれだけのために)、クラスがインターフェースを実装していると断言できます。他の言語(Go、Scala)には他の哲学があります。

4

6 に答える 6

8

実装するクラスが実装するインターフェースを明示的に宣言するという Java の設計上の選択は、まさにそれ、つまり設計上の選択です。確かに、JVM はこの選択に合わせて最適化されており、別の選択 (たとえば、Scala の構造型付け) を実装するには、新しい JVM 命令が追加されない限り、追加コストがかかる可能性があります。

では、デザインの選択とは正確には何ですか?それはすべてメソッドのセマンティクスに帰着します。考慮してください: 次のメソッドは意味的に同じですか?

  • draw(文字列のグラフィカル形状名)
  • draw(String handgunName)
  • draw(String playingCardName)

3 つのメソッドにはすべて署名がありますdraw(String)。人間は、パラメーター名から、またはドキュメントを読むことによって、それらが異なるセマンティクスを持っていると推測するかもしれません。マシンがそれらが異なることを伝える方法はありますか?

Java の設計上の選択は、メソッドが事前定義されたインターフェースのセマンティクスに準拠していることをクラスの開発者が明示的に宣言することを要求することです。

interface GraphicalDisplay {
    ...
    void draw(String graphicalShapeName);
    ...
}

class JavascriptCanvas implements GraphicalDisplay {
    ...
    public void draw(String shape);
    ...
}

drawの方法がグラフィック表示JavascriptCanvasの方法と一致することを意図していることは間違いありません。draw拳銃を引き抜こうとしているオブジェクトを通過しようとすると、マシンはエラーを検出できます。

Go のデザインの選択はより自由であり、事後にインターフェースを定義することができます。具象クラスは、実装するインターフェースを宣言する必要はありません。むしろ、新しいカード ゲーム コンポーネントの設計者は、トランプを提供するオブジェクトには署名に一致するメソッドが必要であると宣言できますdraw(String)。これには、そのメソッドを持つ既存のクラスをソース コードを変更せずに使用できるという利点がありますが、クラスがトランプの代わりにハンドガンを引き出す可能性があるという欠点があります。

ダックタイピング言語の設計上の選択は、正式なインターフェイスを完全に省き、メソッド シグネチャを単純に一致させることです。インターフェイス (または「プロトコル」) の概念は、純粋に慣用的なものであり、直接的な言語サポートはありません。

これらは、多くの可能な設計選択肢のうちの 3 つにすぎません。この 3 つを簡単にまとめると、次のようになります。

Java: プログラマーは自分の意図を明示的に宣言する必要があり、マシンがそれをチェックします。プログラマーがセマンティック ミス (グラフィックス/ハンドガン/カード) を作成する可能性が高いと仮定します。

Go: プログラマーは意図の少なくとも一部を宣言する必要がありますが、マシンがそれをチェックする際に行うことは少なくなります。プログラマーは事務的な間違い (整数/文字列) を犯す可能性は高いが、意味上の間違い (グラフィックス/拳銃/カード) を犯す可能性は低いと想定されています。

ダックタイピング: プログラマーは意図を表明する必要がなく、マシンがチェックするものは何もありません。プログラマーが事務的な間違いや意味上の間違いを犯す可能性は低いと想定されています。

インターフェイス、および一般的なタイピングが、事務的および意味論的な間違いをテストするのに適切であるかどうかに対処することは、この回答の範囲を超えています。完全な議論では、ビルド時のコンパイラ テクノロジ、自動化されたテスト方法、実行時/ホットスポット コンパイル、およびその他の多くの問題を考慮する必要があります。

draw(String)この例は、強調するために意図的に誇張されていることが認められています。実際の例には、メソッドのあいまいさを解消するための手がかりを与えるより豊富な型が含まれます。

于 2011-04-17T05:28:44.267 に答える
5

クラス JButton がインターフェイス宣言を (内部を調べて) 「満たしている」とコンパイラが推測できず、キャストを受け入れることができないのはなぜですか? コンパイラの効率の問題ですか、それとももっと根本的な問題がありますか?

もっと根本的な問題です。

インターフェイスのポイントは、多くのクラスがサポートする共通の API / 一連の動作があることを指定することです。そのため、クラスが として宣言されている場合、implements SomeInterfaceシグネチャがインターフェイスのメソッド シグネチャと一致するクラスのメソッドは、その動作を提供するメソッドであると見なされます。

対照的に、言語が署名に基づいてメソッドを単純に一致させた場合...インターフェースに関係なく...同じ署名を持つ2つのメソッドが実際には意味的に無関係なことを意味/実行する場合、誤った一致が発生する可能性があります。

(後者のアプローチの名前は「ダックタイピング」です...そしてJavaはそれをサポートしていません。)


型システムに関するウィキペディアのページでは、ダック型付けは「主格型付け」でも「構造型付け」でもないと述べています。対照的に、Pierce は「ダック タイピング」についても触れていませんが、主格 (または彼が呼ぶ場合は「名義」) タイピングと構造タイピングを次のように定義しています。

「[型の] 名前が重要であり、サブタイピングが明示的に宣言されている Java のような型システムは、 Nominal と呼ばれます。名前が重要ではなく、サブタイピングが構造上で直接定義されている、この本のほとんどのものと同様の型システム」タイプは、構造的と呼ばれます。」

したがって、Pierce の定義によれば、ダック タイピングは、通常はランタイム チェックを使用して実装されますが、構造型タイピングの一種です。(Pierce の定義は、コンパイル時と実行時のチェックから独立しています。)

参照:

  • 「型とプログラミング言語」 - Benjamin C Pierce 著、MIT Press、2002 年、ISBN 0-26216209-1。
于 2011-04-17T04:59:08.020 に答える
2

ダックタイピングは、Stephen C が説明した理由から危険な場合がありますが、すべての静的タイピングを破壊するのは必ずしも悪ではありません。Go の型システムの中心には、静的でより安全なダック型付けのバージョンがあり、「構造型付け」と呼ばれるバージョンが Scala で利用できます。これらのバージョンは、オブジェクトが要件に従っていることを確認するためにコンパイル時のチェックを引き続き実行しますが、インターフェイスを常に意図的な決定で実装する設計パラダイムを破るため、潜在的な問題があります。

http://markthomas.info/blog/?p=66およびhttp://programming-scala.labs.oreilly.com/ch12.htmlおよびhttp://beust.com/weblog/2008/02/11/を参照してください。 Structure -typing-vs-duck-typing/で Scala の機能について説明します。

于 2011-04-17T05:12:54.310 に答える
2

Java 開発チームがなぜ特定の設計上の決定を下したのか、私にはわかりません。また、ソフトウェア開発と (特に) 言語設計に関して、これらの個人は私よりもはるかに賢いという事実に注意してください。しかし、これはあなたの質問に答えようとするときの亀裂です。

「CanAddActionListener」のようなインターフェースを使用することを選択しなかった理由を理解するには、インターフェースを使用せず、代わりに抽象 (そして最終的には具象) クラスを使用することの利点を検討する必要があります。

ご存知かもしれませんが、抽象機能を宣言するときに、サブクラスにデフォルトの機能を提供できます。よし…だから何?大したことですよね?特に言語を設計する場合、それは大変なことです。言語を設計するときは、言語の存続期間にわたってこれらの基本クラスを維持する必要があります (言語が進化するにつれて変更があることは確実です)。抽象クラスで基本機能を提供する代わりにインターフェイスを使用することを選択した場合、インターフェイスを実装するすべてのクラスが壊れます。これは公開後に特に重要です。顧客 (この場合は開発者) がライブラリを使い始めると、思いつきでインターフェイスを変更することはできません。

したがって、Java 開発チームは、AbstractJ* クラスの多くが同じメソッド名を共有していることを完全に認識していたと思います。共通のインターフェイスを共有することは、API を固定化して柔軟性を失わせるため、有利ではありません。

要約すると(このサイトに感謝します):

  • 抽象クラスは、新しい (非抽象) メソッドを追加することで簡単に拡張できます。
  • インターフェイスは、それを実装するクラスとの契約を破棄せずに変更することはできません。インターフェイスが出荷されると、そのメンバ セットは永続的に固定されます。インターフェイスに基づく API は、新しいインターフェイスを追加することによってのみ拡張できます。

もちろん、これは、独自のコードでこのようなことを行う (AbstractJButton を拡張し、CanAddActionListener インターフェイスを実装する) ことができると言っているわけではありませんが、そうする際の落とし穴に注意してください。

于 2011-04-17T05:00:40.873 に答える
2

おそらくそれはパフォーマンス機能です。

Java は静的に型付けされるため、コンパイラーは、識別されたインターフェースに対するクラスの適合性をアサートできます。検証が完了すると、そのアサーションは、準拠するインターフェイス定義への単なる参照として、コンパイルされたクラスで表すことができます。

その後、実行時に、オブジェクトの Class がインターフェース型にキャストされた場合、ランタイムが行う必要があるのは、クラスのメタデータをチェックして、キャストされているクラスにも互換性があるかどうかを確認することだけです (インターフェースまたは継承階層)。

コンパイラがほとんどの作業を行っているため、これは実行するのにかなり安価なチェックです。

気をつけてください、それは権威ではありません。クラスは、インターフェイスに準拠していると言うことができますが、実行しようとしている実際のメソッド送信が実際に機能するという意味ではありません。準拠するクラスが古くなっている可能性があり、メソッドが単に存在しない可能性があります。

しかし、Java のパフォーマンスにとって重要な要素は、実行時に動的メソッド ディスパッチの形式を実際に実行する必要がある一方で、メソッドがランタイムの背後で突然消えることはないという契約があることです。したがって、メソッドが見つかったら、その場所を後でキャッシュできます。メソッドが行き来する可能性があり、呼び出されるたびにメソッドを探し続けなければならない動的言語とは対照的です。明らかに、動的言語には、これを適切に実行するためのメカニズムがあります。

ここで、ランタイムがすべての作業を実行して、オブジェクトがインターフェイスに準拠していることを確認した場合、特に大規模なインターフェイスでは、それがどれほどコストがかかるかがわかります。たとえば、JDBC ResultSet には 140 を超えるメソッドなどが含まれています。

ダックタイピングは、事実上、動的なインターフェイス マッチングです。オブジェクトで呼び出されたメソッドを確認し、実行時にマップします。

その種の情報はすべてキャッシュでき、実行時にビルドすることもできます。これはすべて可能ですが (他の言語でも可能です)、コンパイル時にこの多くを行うと、実際にはランタイム CPU とそのメモリの両方で非常に効率的です。 . 長時間稼働するサーバーにはマルチ GB ヒープを備えた Java を使用していますが、実際には、小規模な展開や無駄のないランタイムにはかなり適しています。J2ME の外でも。そのため、実行時のフットプリントをできる限り少なくしようとする動機はまだあります。

于 2011-04-17T05:03:20.717 に答える
0

インターフェイスは、置換クラスの形式を表します。特定のインターフェイスを実装または継承する型の参照は、そのインターフェイス型を期待するメソッドに渡すことができます。インターフェイスは一般に、すべての実装クラスが特定の名前とシグネチャを持つメソッドを持たなければならないことを指定するだけでなく、一般に、すべての正当な実装クラスが特定の名前とシグネチャを持つメソッドを持たなければならないことを指定する関連するコントラクトも持ちます。方法。2 つのインターフェイスに同じ名前と署名を持つメンバーが含まれている場合でも、実装が一方のコントラクトを満たし、他方のコントラクトを満たさない可能性は十分にあります。

簡単な例として、フレームワークをゼロから設計する場合、インターフェイスから始めるEnumerable<T>ことができます (これは、T のシーケンスを出力する列挙子を作成するために必要な回数だけ使用できますが、異なる要求は異なるシーケンスを生成する可能性があります)。 、しかしそれからImmutableEnumerable<T>、上記のように動作するが、すべてのリクエストが同じシーケンスを返すことを保証するインターフェースを派生させます。可変コレクション タイプは、 に必要なすべてのメンバーをサポートしますがImmutableEnumerable<T>、変異後に受け取った列挙の要求は、以前に行われた要求とは異なるシーケンスを報告するため、ImmutableEnumerable契約には従いません。

インターフェイスがメンバーの署名を超えてコントラクトをカプセル化していると見なされる能力は、インターフェイス ベースのプログラミングを単純なダックタイピングよりも意味的に強力にするものの 1 つです。

于 2014-06-13T19:10:32.430 に答える