Vererbung und dynamischer Zugriff auf Mitglieder / Attribute und Methoden in Java-ähnlichen Sprachen

7

Ich habe eine Frage zur Vererbung in Java-ähnlichen OO-Programmiersprachen. Es kam in meiner Compiler-Klasse vor, als ich erklärte, wie Methoden und deren Aufruf kompiliert werden. Ich habe Java als Beispielquellsprache zum Kompilieren verwendet.

Betrachten Sie nun dieses Java-Programm.

class A {
  public int x = 0;
  void f () { System.out.println ( "A:f" ); } }

class B extends A {
  public int x = 1;
  void f () { System.out.println ( "B:f" ); } }

public class Main {
  public static void main ( String [] args ) {
    A a = new A ();
    B b = new B ();
    A ab = new B ();

    a.f();
    b.f();
    ab.f();
    System.out.println ( a.x );
    System.out.println ( b.x );
    System.out.println ( ab.x ); } }

Wenn Sie es ausführen, erhalten Sie das folgende Ergebnis.

A:f
B:f
B:f
0
1 
0

Die interessanten Fälle sind solche, die mit dem Objekt abvom statischen Typ auftreten A, das Bdynamisch ist. Wie ab.f()druckt aus

B:f

Daraus folgt, dass Methodenaufrufe nicht vom Typ der Kompilierungszeit des Objekts beeinflusst werden, mit dem die Methode aufgerufen wird. Wird jedoch System.out.println ( ab.x )ausgedruckt 0, sodass der Mitgliederzugriff von den Typen zur Kompilierungszeit beeinflusst wird.

Ein Student fragte nach diesem Unterschied: Sollte der Zugang von Mitgliedern und Methoden nicht miteinander übereinstimmen? Ich könnte keine bessere Antwort finden als "das ist die Semantik von Java".

Würdest du einen klaren konzeptionellen Grund kennen, warum Mitglieder und Methoden in diesem Sinne unterschiedlich sind? Etwas, das ich meinen Schülern geben könnte?

Bearbeiten : Nach weiteren Untersuchungen scheint dies eine Java-Eigenart zu sein: C ++ und C # verhalten sich unterschiedlich, siehe z. B. Saeed Amiris Kommentar unten.

Edit 2 : Ich habe gerade das entsprechende Scala-Programm ausprobiert:

class A {
  val x = 0;
  def f () : Unit = { System.out.println ( "A:f" ); } }

class B extends A {
  override val x = 1;
  override def f () : Unit = { System.out.println ( "B:f" ); } }

object Main {
  def main ( args : Array [ String ] ) = {
    var a : A = new A ();
    var b : B = new B ();
    var ab : A = new B ();
    a.f();
    b.f();
    ab.f();
    System.out.println ( "a.x = " + a.x );
    System.out.println ( "b.x = " + b.x );
    System.out.println ( "ab.x = " + ab.x ); } }

Und zu meiner Überraschung ergibt sich daraus

A:f
B:f
B:f
a.x = 0
b.x = 1
ab.x = 1

Beachten Sie, dass die overriseModifikatoren erforderlich sind. Dies überrascht mich, weil Scala in die JVM kompiliert und sich beim Kompilieren und Ausführen des Java-Programms oben mit dem Scala-Compiler / der Laufzeit wie das Java-Programm verhält.

Martin Berger
quelle
3
Dies wird zu Ihrer Programmiersprache verwandt, wie beschäftigt sich mit Variableninitialisierung, wenn Sie so in C # tun: ideone.com/q6KpKk Sie ein anderes Ergebnis erhalten wird, Ihr Problem ist , dass Sie overrided fund x, ohne dies ausdrücklich zu erwähnen, IMO C # Version ist besser ( mehr in OO Sicht akzeptabel), anstatt dies zu tun, definieren x, fwie virtuelle, und Sie werden in durch zwingende sie Konsistenz Ergebnis erhalten B. Aber wenn Sie dies in Stackoverflow fragen, werden Sie mehr Aufmerksamkeit bekommen und ich bin sicher eine nette Antwort.
@SaeedAmiri danke für das C #, ich habe erwartet, dass es dort dasselbe ist. Wenn ich hier keine guten Antworten bekomme, werde ich zu Stackoverflow wechseln.
Martin Berger
Bitte über einen Link hinaus, wenn Sie Crosspost.
Nejc
1
@SaeedAmiri: Felder in Java können nicht überschrieben werden. Man kann nur Felder beschatten.
Dave Clarke
Ich könnte mich irren, aber ich habe kürzlich gelesen, dass Java nur "Methodenüberschreibung" erlaubt und "Feldüberschreibung" nicht unterstützt wird. Wenn Sie also eine Instanz der Klasse A erstellen, sucht sie für das Feld nach der nächstgelegenen Definition der Entität x, die sie in Klasse A findet, und damit nach dem Ergebnis. Die Leute können mich korrigieren, wenn ich hier falsch liege.
Rajat Saxena

Antworten:

6

Ich vermute, dass dies eng mit dem Abschatten von Feldern in Java zusammenhängt.

Wenn ich eine abgeleitete Klasse schreibe, kann ich wie in Ihrem Beispiel ein Feld schreiben, xdas die Definition xin der Basisklasse schattiert . Dies bedeutet, dass in der abgeleiteten Klasse die ursprüngliche Definition nicht mehr über den Namen zugänglich ist x. Der Grund, warum Java dies zulässt, besteht darin, dass abgeleitete Klassenimplementierungen weniger von den Implementierungsdetails der Basisklasse abhängig sind, wodurch (nur teilweise) das fragile Problem der Basisklasse vermieden wird. Wenn die Basisklasse beispielsweise ursprünglich kein Feld hatte x, aber anschließend eines einführte, verhindert die Java-Semantik, dass die Implementierung der abgeleiteten Klasse dadurch beschädigt wird.

Die Variable xin der abgeleiteten Klasse kann einen völlig anderen Typ haben als die Variable in der Basisklasse - ich habe dies getestet, es funktioniert. Dies steht in krassem Gegensatz zum Überschreiben von Methoden, bei denen der Typ ausreichend kompatibel sein muss (auch bekannt als derselbe). Wenn ich also Ain Ihrem Beispiel eine Variable vom Typ habe, kann ich nur den Typ ableiten, der in der Klasse A(oder höher) angegeben ist. Da dies von dem in class deklarierten Typ abweichen kann B, ist es nur typsicher, den Wert des Felds xvon class zurückzugeben A. (Und natürlich muss die Semantik unabhängig von der Art des Feldes in der Klasse dieselbe sein B.)

Dave Clarke
quelle
Hallo @ DaveClarke, deine Erklärung, warum ab.xdas AAttribut return zurückgeben soll, xist zwingend. Der gleiche Grund gilt jedoch auch für Mitglieder von type A, und tatsächlich rufen C # und C ++ die AMethode fin auf ab.f().
Martin Berger
2
In der Vergangenheit erlaubte C ++ sowohl den dynamischen (virtuellen) als auch den statischen Versand. Java-Designer hielten dies für zu komplex und unterstützten nach Sprachen wie Eiffel und Smalltalk nur die Semantik für den Methodenversand. Auch hier kann man argumentieren, dass dies sowohl einfacher als auch natürlicher ist.
Dave Clarke
Hallo @ DaveClarke, ich stimme zu, dass die Virtualisierung jeder Methode einfacher ist. Es wäre jedoch noch einheitlicher, wenn die Mitglieder auch virtualisiert würden (und dann keine beliebigen Typen haben könnten). Schließlich konnte man Methoden als Mitglieder höherer Ordnung sehen. Ich sage nicht, dass dies das Richtige ist, aber ich frage mich, ob es einen kritischen konzeptionellen Grund gibt, eine so drastische Unterscheidung zwischen Methoden und Mitgliedern zu treffen.
Martin Berger
Letztendlich würde die Virtualisierung von Mitgliedern / Feldern eigentlich nichts bedeuten. Es würde ein Feld xfür die gesamte Klassenhierarchie geben. Was bedeutet es zu überschreiben x? Einfach, um ihm einen neuen Wert zu geben. Das Problem ist also wirklich, warum Java alle virtuellen Methoden und das Abschatten von Feldern ausgewählt hat .
Dave Clarke
Aber, @DaveClarke, das obige Scala-Beispiel scheint mir zu zeigen, dass Scala tatsächlich seine Mitglieder virtualisiert (Sie müssen overridesie typisieren. Einige Sprachen treffen diese Wahl. Vielleicht gibt es keine eindeutigen, einfachen Argumente in diesem Raum?
Martin Berger
4

In Java gibt es keinen 'statischen Typ eines Objekts' - es gibt den Typ des Objekts und den Typ der Referenz. Alle Java-Methoden sind 'virtuell' (um die C ++ - Terminologie zu verwenden), dh sie werden basierend auf dem Objekttyp aufgelöst. Und übrigens. '@Override' hat keinerlei Auswirkungen auf den kompilierten Code - es ist nur ein Schutz, der einen Compilerfehler generiert, wenn die so kommentierte Methode eine Methode der Oberklasse nicht überschreibt.

Der Feldzugriff wird dagegen niemals dynamisch aufgelöst - oder, anders ausgedrückt, Java verwendet beim Zugriff auf Felder keinen Polymorphismus. In dem (pathologischen) Fall, dass eine Unterklasse ein Feld x mit demselben Namen wie ein Oberklassenfeld hat, hat die Unterklasse zwei Felder 'x'. Offensichtlich gibt es Namensschatten - der Name 'x' kann in einem bestimmten Kontext nur an eines dieser Felder gebunden werden.

Geben Sie die Syntax für die Namensauflösung ein. Wenn Sie bx schreiben, löst der Compiler dies in das in den Unterklassen deklarierte Feld auf. Wenn Sie jedoch ab.x schreiben, löst der Compiler dies in das in A deklarierte Feld auf - beide Felder sind Mitglieder der Klasse B. Sie kann mit super.x über Code in Klasse B auf beide Felder zugreifen:

Klasse B {... void g () {System.out.println ("beide:" + x + "und" + super.x);}}

Zur Laufzeit spielt es also keine Rolle, ob das Feld der Unterklasse 'x' oder 'y' heißt - in beiden Fällen hat die Unterklasse zwei Distince-Felder ohne Beziehung zwischen ihnen.

Arno
quelle
Sie haben Recht, @Arno, aber warum trifft Java diese Entscheidungen und warum treffen C # und Scala unterschiedliche Entscheidungen? Gibt es dafür einen guten eindeutigen Grund?
Martin Berger
1

Wenn Sie sich daran erinnern, dass Vererbung eine Form des Sprichworts B"ist a" Aist A, können geerbte Elementvariablen als Attribute des Objekts betrachtet werden , außer dass die Funktionalität von möglicherweise erweitert wird . Intuitiv sind Mitgliedsvariablen, wie wir sie über Vererbung lernen (zugegebenermaßen aus Java-Sicht), wie Attribute, und allgemeine Attribute sollen nicht erweitert werden, sondern nur die Funktionalität.

Angenommen, Sie haben eine PersonKlasse und die Person hat eine Namensvariable. Der Name wird intuitiv nicht geändert, auch wenn wir uns Personauf Studentund erstrecken Professor. Methoden, wie z. B. " Student.do_work()Erweitern", um "Hausaufgaben" zu machen, während die ProfessorKlasse Tests in bewerten kann Professor.do_work().

Natürlich können Sie diese Intuition brechen und im Wesentlichen Mitgliedsvariablen zu Eigenschaften machen oder Getter verwenden, die überschrieben werden können usw., aber ich denke, was ich oben gesagt habe, war die Intuition, dies auf diese Weise zu tun. Tatsächlich würde ich sagen, dass es in den meisten Fällen unkonventionell wäre, eine gleichnamige Mitgliedsvariable in einer abgeleiteten Klasse zu haben.

Realz Slaw
quelle