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 ab
vom statischen Typ auftreten A
, das B
dynamisch 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 overrise
Modifikatoren 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.
quelle
f
undx
, ohne dies ausdrücklich zu erwähnen, IMO C # Version ist besser ( mehr in OO Sicht akzeptabel), anstatt dies zu tun, definierenx
,f
wie virtuelle, und Sie werden in durch zwingende sie Konsistenz Ergebnis erhaltenB
. Aber wenn Sie dies in Stackoverflow fragen, werden Sie mehr Aufmerksamkeit bekommen und ich bin sicher eine nette Antwort.Antworten:
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,
x
das die Definitionx
in der Basisklasse schattiert . Dies bedeutet, dass in der abgeleiteten Klasse die ursprüngliche Definition nicht mehr über den Namen zugänglich istx
. 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 hattex
, aber anschließend eines einführte, verhindert die Java-Semantik, dass die Implementierung der abgeleiteten Klasse dadurch beschädigt wird.Die Variable
x
in 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 alsoA
in Ihrem Beispiel eine Variable vom Typ habe, kann ich nur den Typ ableiten, der in der KlasseA
(oder höher) angegeben ist. Da dies von dem in class deklarierten Typ abweichen kannB
, ist es nur typsicher, den Wert des Feldsx
von class zurückzugebenA
. (Und natürlich muss die Semantik unabhängig von der Art des Feldes in der Klasse dieselbe seinB
.)quelle
ab.x
dasA
Attribut return zurückgeben soll,x
ist zwingend. Der gleiche Grund gilt jedoch auch für Mitglieder von typeA
, und tatsächlich rufen C # und C ++ dieA
Methodef
in aufab.f()
.x
für die gesamte Klassenhierarchie geben. Was bedeutet es zu überschreibenx
? 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 .override
sie typisieren. Einige Sprachen treffen diese Wahl. Vielleicht gibt es keine eindeutigen, einfachen Argumente in diesem Raum?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.
quelle
Wenn Sie sich daran erinnern, dass Vererbung eine Form des Sprichworts
B
"ist a"A
istA
, 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
Person
Klasse und die Person hat eine Namensvariable. Der Name wird intuitiv nicht geändert, auch wenn wir unsPerson
aufStudent
und erstreckenProfessor
. Methoden, wie z. B. "Student.do_work()
Erweitern", um "Hausaufgaben" zu machen, während dieProfessor
Klasse Tests in bewerten kannProfessor.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.
quelle