Warum müssen Interfaces in Java deklariert werden?

8

Manchmal haben wir mehrere Klassen, die einige Methoden mit der gleichen Signatur haben, die aber keiner deklarierten Java-Schnittstelle entsprechen. Zum Beispiel haben sowohl JTextField als auch JButton (unter mehreren anderen in javax.swing.* ) eine Methode

%Vor%

Nun nehme ich an, ich möchte etwas mit Objekten machen, die diese Methode haben; dann hätte ich gerne eine Schnittstelle (oder vielleicht um sie selbst zu definieren), z. B.

%Vor%

damit ich schreiben könnte:

%Vor%

Aber leider kann ich nicht:

%Vor%

Diese Besetzung wäre illegal. Der Compiler weiß dass JButton nicht ist CanAddActionListener , weil die Klasse nicht deklariert hat, diese Schnittstelle zu implementieren ... wie auch immer sie "tatsächlich" implementiert es .

Das ist manchmal eine Unannehmlichkeit - und Java selbst hat mehrere Kernklassen modifiziert, um eine neue Schnittstelle zu implementieren, die aus alten Methoden besteht (zum Beispiel String implements CharSequence ).

Meine Frage ist: Warum ist das so? Ich verstehe den Nutzen der Deklaration, dass eine Klasse eine Schnittstelle implementiert. Wie auch immer, wenn ich mein Beispiel anschaue, warum kann der Compiler nicht folgern, dass die Klasse JButton die Interface-Deklaration "erfüllt" (hineinschauen) und den Cast akzeptiert? Ist es ein Problem der Compiler-Effizienz oder gibt es grundsätzliche Probleme?

Meine Zusammenfassung der Antworten : Dies ist ein Fall, in dem Java eine gewisse "strukturelle Typisierung" berücksichtigt hätte (eine Art Duck-Typisierung - aber zur Kompilierzeit überprüft). Es tat es nicht. Abgesehen von einigen (für mich unklaren) Leistungs- und Umsetzungsschwierigkeiten gibt es hier ein wesentlich grundlegenderes Konzept: In Java soll die Deklaration einer Schnittstelle (und generell von allem) nicht nur strukturell (um Methoden mit diesen Signaturen zu haben), aber semantisch : Die Methoden sollen ein bestimmtes Verhalten / Absicht implementieren. Eine Klasse, die strukturell eine Schnittstelle erfüllt (dh sie hat die Methoden mit den erforderlichen Signaturen), erfüllt sie nicht notwendigerweise semantisch (ein extremes Beispiel: Rufen Sie den "marker" auf Schnittstellen ", die nicht einmal Methoden haben!). Daher kann Java bestätigen, dass eine Klasse eine Schnittstelle implementiert, weil (und nur weil) dies explizit deklariert wurde. Andere Sprachen (Go, Scala) haben andere Philosophien.

    
leonbloy 17.04.2011, 04:24
quelle

6 Antworten

8

Javas Design-Entscheidung, Implementierungsklassen zu deklarieren, deklariert ausdrücklich die von ihnen implementierte Schnittstelle - eine Design-Entscheidung. Natürlich ist die JVM für diese Wahl optimiert worden und die Implementierung einer anderen Wahl (zB Scalas Strukturtypisierung) kann jetzt zusätzliche Kosten mit sich bringen, es sei denn, bis einige neue JVM-Anweisungen hinzugefügt werden.

Also was genau ist die Designwahl? Es kommt auf die Semantik von Methoden an. Bedenken Sie: Sind die folgenden Methoden semantisch gleich?

  • zeichnen (String graphicalShapeName)
  • zeichne (String handgunName)
  • zeichnen (String playingCardName)

Alle drei Methoden haben die Signatur draw(String) . Ein Mensch könnte folgern, dass sie eine andere Semantik als die Parameternamen haben, oder indem sie eine Dokumentation lesen. Gibt es eine Möglichkeit für die Maschine zu sagen, dass sie anders sind?

Javas Designwahl besteht darin, vom Entwickler einer Klasse explizit zu verlangen, dass eine Methode der Semantik einer vordefinierten Schnittstelle entspricht:

%Vor%

Es besteht kein Zweifel, dass die Methode draw in JavascriptCanvas der Methode draw für eine grafische Anzeige entsprechen soll. Wenn jemand versuchte, ein Objekt zu passieren, das eine Handfeuerwaffe herausziehen würde, kann das Gerät den Fehler erkennen.

Go's Design-Wahl ist liberaler und ermöglicht die Definition von Schnittstellen im Nachhinein. Eine konkrete Klasse muss nicht angeben, welche Schnittstellen sie implementiert. Vielmehr kann der Entwickler einer neuen Kartenspielkomponente erklären, dass ein Objekt, das Spielkarten liefert, eine Methode haben muss, die der Signatur draw(String) entspricht. Dies hat den Vorteil, dass jede existierende Klasse mit dieser Methode verwendet werden kann, ohne dass der Quellcode geändert werden muss, aber der Nachteil, dass die Klasse anstelle einer Spielkarte eine Handfeuerwaffe herausziehen kann.

Die Entwurfsauswahl der Sprachen für die Entenschrift besteht darin, auf formale Schnittstellen gänzlich zu verzichten und einfach auf Methodensignaturen abzugleichen. Jedes Konzept von Schnittstelle (oder "Protokoll") ist rein idiomatisch, ohne direkte Sprachunterstützung.

Dies sind nur drei von vielen möglichen Design-Optionen. Die drei können wie folgt zusammengefasst werden:

Java: Der Programmierer muss explizit seine Absicht erklären, und die Maschine wird es überprüfen. Die Annahme ist, dass der Programmierer wahrscheinlich einen semantischen Fehler macht (Grafik / Pistole / Karte).

Go: Der Programmierer muss zumindest einen Teil seiner Absicht deklarieren, aber die Maschine hat weniger zu tun, wenn er sie überprüft. Die Annahme ist, dass der Programmierer wahrscheinlich einen Schreibfehler (Integer / String) macht, aber wahrscheinlich keinen semantischen Fehler macht (Grafik / Pistole / Karte).

Duck-Typisierung: Der Programmierer muss keine Absicht ausdrücken, und es gibt nichts, was die Maschine überprüfen kann. Die Annahme ist, dass Programmierer wahrscheinlich keinen klerikalen oder semantischen Fehler machen.

Es würde den Rahmen dieser Antwort sprengen, darauf einzugehen, ob Interfaces und das Tippen im Allgemeinen ausreichen, um auf Schreibfehler und semantische Fehler zu prüfen. Eine vollständige Diskussion müsste Build-Time-Compiler-Technologie, automatisierte Testmethodik, Laufzeit / Hot-Spot-Kompilierung und eine Vielzahl anderer Probleme berücksichtigen.

Es wird anerkannt, dass das draw(String) Beispiel absichtlich übertrieben ist, um einen Punkt zu machen. Echte Beispiele würden reichere Typen einschließen, die mehr Hinweise geben würden, um die Methoden zu disambiguieren.

    
WReach 17.04.2011, 05:28
quelle
5
  

Warum kann der Compiler nicht ableiten, dass die Klasse JButton die Interface-Deklaration "erfüllt" (hineinpasst) und den Cast akzeptiert? Ist es ein Problem der Compiler-Effizienz oder gibt es grundsätzliche Probleme?

Es ist ein grundsätzlicheres Problem.

Der Punkt einer Schnittstelle besteht darin, anzugeben, dass es eine gemeinsame API / Menge von Verhaltensweisen gibt, die von einer Anzahl von Klassen unterstützt wird. Wenn eine Klasse als implements SomeInterface deklariert wird, werden alle Methoden in der Klasse, deren Signaturen Methodensignaturen in der Schnittstelle entsprechen, als angenommen, um Methoden zu sein, die dieses Verhalten bereitstellen.

Wenn dagegen die Sprache nur auf Signaturen basiert ... unabhängig von den Schnittstellen ..., dann würden wir wahrscheinlich falsche Übereinstimmungen erhalten, wenn zwei Methoden mit der gleichen Signatur tatsächlich etwas bedeuten, das semantisch nicht verwandt ist .

(Der zweite Ansatz lautet "duck typing" ... und Java unterstützt das nicht.)

Die Wikipedia-Seite auf Typ-Systemen sagt, dass Duck-Typisierung weder "Nominativ-Typisierung" noch "Strukturelle Typisierung" ist . Im Gegensatz dazu erwähnt Pierce nicht einmal "Enten-Typisierung", aber er definiert Nominativ (oder "Nominal", wie er es nennt) Typisierung und strukturelle Typisierung wie folgt:

  

"Typsysteme wie Java's, in denen Namen [von Typen] signifikant sind und Untertypen explizit deklariert sind, heißen nominal . Geben Sie Systeme wie die meisten in diesem Buch ein, in denen Namen unwesentlich sind und Subtyping wird direkt auf der Struktur der Typen definiert und heißt strukturell . "

Nach der Definition von Pierce ist das Entschlüsseln von Eingängen eine Form der strukturellen Typisierung, wenngleich diese normalerweise durch Laufzeitprüfungen implementiert wird. (Die Definitionen von Pierce sind unabhängig von der Kompilierungszeit und der Laufzeitprüfung.)

Referenz:

  • "Typen und Programmiersprachen" - Benjamin C. Pierce, MIT Press, 2002, ISBN 0-26216209-1.
Stephen C 17.04.2011 04:59
quelle
2

Wahrscheinlich ist es ein Leistungsmerkmal.

Da Java statisch typisiert ist, kann der Compiler die Übereinstimmung einer Klasse mit einer identifizierten Schnittstelle bestätigen. Nach der Validierung kann diese Assertion in der kompilierten Klasse als einfache Referenz auf die konforme Schnittstellendefinition dargestellt werden.

Später, zur Laufzeit, wenn ein Objekt seine Klasse in den Schnittstellentyp umgewandelt hat, muss die Laufzeitumgebung nur die Metadaten der Klasse prüfen, um zu sehen, ob die Klasse, mit der sie auch gecastet wird, kompatibel ist Schnittstelle oder die Vererbungshierarchie).

Dies ist eine ziemlich billige Überprüfung, da der Compiler die meiste Arbeit erledigt hat.

Achtung, es ist nicht autoritativ. Eine Klasse kann SAGEN, dass sie mit einer Schnittstelle übereinstimmt, aber das bedeutet nicht, dass die tatsächliche Methode, die zur Ausführung gesendet wird, tatsächlich funktioniert. Die konforme Klasse ist möglicherweise veraltet und die Methode existiert möglicherweise nicht.

Aber eine Schlüsselkomponente für die Performance von Java ist, dass es zwar zur Laufzeit noch eine Form des dynamischen Methodenabrufs geben muss, dass aber die Methode nicht plötzlich hinter den Laufzeiten verschwindet. Sobald die Methode gefunden wurde, kann ihre Position in der Zukunft zwischengespeichert werden. Im Gegensatz zu einer dynamischen Sprache, in der Methoden kommen und gehen können, müssen sie weiterhin versuchen, die Methoden bei jedem Aufruf der Methode zu suchen. Offensichtlich haben dynamische Sprachen Mechanismen, um dies gut zu machen.

Wenn nun die Laufzeit feststellen sollte, dass ein Objekt einer Schnittstelle entspricht, indem die gesamte Arbeit selbst ausgeführt wird, können Sie sehen, wie viel teurer das sein kann, insbesondere bei einer großen Schnittstelle. Ein JDBC-ResultSet zum Beispiel hat mehr als 140 Methoden und solche darin.

Duck Typisierung ist effektiv dynamische Schnittstellenabgleich. Überprüfen Sie, welche Methoden für ein Objekt aufgerufen werden, und ordnen Sie sie zur Laufzeit zu.

All diese Informationen können im Cache gespeichert und zur Laufzeit erstellt werden. All dies kann (und ist in anderen Sprachen), aber das meiste zur Kompilierungszeit getan zu haben, ist tatsächlich ziemlich effizient sowohl auf der CPU der Laufzeiten und seine Erinnerung. Während wir Java mit mehreren GB-Heaps für Server mit langen Laufzeiten verwenden, ist es für kleinere Bereitstellungen und schlanke Laufzeiten geeignet. Auch außerhalb von J2ME. Daher besteht immer noch die Motivation, den Laufzeit-Footprint so schlank wie möglich zu halten.

    
Will Hartung 17.04.2011 05:03
quelle
2

Die Eingabe von Enten kann aus den Gründen, die Stephen C diskutiert hat, gefährlich sein, aber es ist nicht unbedingt das Böse, das die statische Eingabe unterbricht. Eine statische und sicherere Version der Duck-Typisierung liegt im Zentrum des Go-Typ-Systems, und eine Version ist in Scala verfügbar, wo sie als "Strukturtypisierung" bezeichnet wird. Diese Versionen führen immer noch eine Kompilierzeitüberprüfung durch, um sicherzustellen, dass das Objekt die Anforderungen erfüllt, haben jedoch potentielle Probleme, da sie das Entwurfsparadigma brechen, das die Implementierung einer Schnittstelle immer eine absichtliche Entscheidung darstellt.

Siehe Ссылка und Ссылка und Ссылка für eine Diskussion der Scala-Funktion.

    
Philip JF 17.04.2011 05:12
quelle
2

Ich kann nicht sagen, dass ich weiß, warum bestimmte Designentscheidungen vom Java-Entwicklungsteam getroffen wurden. Ich schärfe auch meine Antwort mit der Tatsache, dass diese Personen viel schlauer sind, als ich jemals in Bezug auf Software-Entwicklung und (insbesondere) Sprachdesign sein werde. Aber, hier ist ein Riss beim Versuch, Ihre Frage zu beantworten.

Um zu verstehen, warum sie sich nicht für eine Schnittstelle wie "CanAddActionListener" entschieden haben, müssen Sie die Vorteile von NOT unter Verwendung einer Schnittstelle betrachten und stattdessen abstrakte (und letztlich konkrete) Klassen bevorzugen.

Wie Sie vielleicht wissen, können Sie bei der Deklaration der abstrakten Funktionalität den Unterklassen Standardfunktionen zur Verfügung stellen. Okay ... na und? Große Sache, oder? Nun, besonders im Falle der Gestaltung einer Sprache ist es eine große Sache. Wenn Sie eine Sprache entwerfen, müssen Sie diese Basisklassen über das Leben der Sprache hinweg pflegen (und Sie können sicher sein, dass es bei der Entwicklung Ihrer Sprache Änderungen geben wird). Wenn Sie sich dafür entschieden haben, Schnittstellen zu verwenden, anstatt die Basisfunktionalität in einer abstrakten Klasse bereitzustellen, bricht jede Klasse, die die Schnittstelle implementiert, ab. Dies ist besonders wichtig nach der Veröffentlichung - sobald Kunden (Entwickler in diesem Fall) beginnen, Ihre Bibliotheken zu verwenden, können Sie die Schnittstellen nicht nach Belieben ändern oder Sie werden viel von Entwicklern angepisst sein!

Ich nehme an, dass das Java-Entwicklerteam erkannt hat, dass viele ihrer AbstractJ * -Klassen dieselben Methodennamen verwenden. Es wäre nicht vorteilhaft, wenn sie eine gemeinsame Schnittstelle hätten, da dies ihre API steif und unflexibel machen würde.

Um es zusammenzufassen ( vielen Dank für diese Seite hier ):

  • Abstrakte Klassen können leicht durch Hinzufügen neuer (nicht abstrakter) Methoden erweitert werden.
  • Eine Schnittstelle kann nicht geändert werden, ohne den Vertrag mit den implementierenden Klassen zu brechen. Sobald eine Schnittstelle versandt wurde, ist ihr Elementsatz permanent fixiert. Eine auf Schnittstellen basierende API kann nur durch Hinzufügen neuer Schnittstellen erweitert werden.

Das heißt natürlich nicht, dass Sie so etwas in Ihrem eigenen Code machen könnten (erweitern Sie AbstractJButton und implementieren Sie die CanAddActionListener-Schnittstelle), aber seien Sie sich der Gefahren bewusst.

    
JasCav 17.04.2011 05:00
quelle
0

Interfaces repräsentieren eine Form der Substitutionsklasse. Eine Referenz vom Typ, die von einer bestimmten Schnittstelle implementiert oder erbt, kann an eine Methode übergeben werden, die diesen Schnittstellentyp erwartet. Eine Schnittstelle spezifiziert im Allgemeinen nicht nur, dass alle implementierenden Klassen Methoden mit bestimmten Namen und Signaturen haben müssen, sondern wird im Allgemeinen auch einen zugehörigen Vertrag haben, der angibt, dass alle legitimen implementierenden Klassen Methoden mit bestimmten Namen und haben müssen Signaturen , die sich auf bestimmte festgelegte Weise verhalten . Es ist durchaus möglich, dass, selbst wenn zwei Schnittstellen Mitglieder mit denselben Namen und Unterschriften enthalten, eine Implementierung den Vertrag von einem, nicht aber den anderen erfüllen kann.

Als ein einfaches Beispiel könnte man, wenn man ein Framework von Grund auf neu erstellt, mit einer Enumerable<T> -Schnittstelle beginnen (die beliebig oft verwendet werden kann, um einen Enumerator zu erzeugen, der eine Sequenz von Ts ausgibt, aber unterschiedlich Anfragen können unterschiedliche Sequenzen ergeben), aber leiten Sie daraus eine Schnittstelle ImmutableEnumerable<T> ab, die sich wie oben verhalten würde, aber garantieren würde, dass jede Anfrage dieselbe Sequenz zurückgeben würde. Ein änderbarer Sammlertyp würde alle für ImmutableEnumerable<T> erforderlichen Mitglieder unterstützen, da jedoch die nach einer Mutation eingehenden Aufzählungsaufrufe eine andere Reihenfolge als die zuvor erstellten anfordern würden, würde der ImmutableEnumerable -Vertrag nicht eingehalten.

Die Fähigkeit einer Schnittstelle, einen Vertrag über die Signaturen ihrer Mitglieder hinaus zu kapseln, ist eine der Sachen, die Interface-basiertes Programmieren semantischer machtvoller macht als einfaches Duck-Tippen.

    
supercat 13.06.2014 19:10
quelle