Warum weisen wir dem untergeordneten Objekt in Java einen übergeordneten Verweis zu?

79

Ich stelle eine recht einfache Frage, bin aber etwas verwirrt.

Angenommen, ich habe eine Klasse Parent:

public class Parent {

    int name;
}

Und noch eine Klasse Child:

public class Child extends Parent{

    int salary;
}

Und schließlich meine Main.java-Klasse

public class Main {

    public static void main(String[] args)
    {
        Parent parent = new Child();
        parent.name= "abcd";
    }
}

Wenn ich ein untergeordnetes Objekt wie mache

Child child = new Child():

Dann childkann das Objekt auf beide name and salaryVariablen zugreifen .

Meine Frage ist:

Parent parent = new Child();

Ermöglicht den Zugriff nur auf nameVariablen der übergeordneten Klasse. Also, was ist die genaue Verwendung dieser Linie?

 Parent parent = new Child();

Und auch wenn dynamischer Polymorphismus verwendet wird, warum ist die Variable der untergeordneten Klasse danach nicht mehr zugänglich?

Parent parent = new Child();
Narendra Pal
quelle
1
Lesen Sie das
Tierbeispiel
In Ihrem Beispiel hilft es nicht wirklich. Denken Sie jedoch, dass Sie eine Methode haben, die etwas mit einer übergeordneten Referenz tut. Zum Beispiel irgendwo in der Hauptklasse. Es nimmt eine übergeordnete Objektreferenz und macht Sachen damit. Es wäre sinnvoll, auch Unterklassen von Parent zu akzeptieren, wie z. B. ein Child-Objekt, obwohl es nur Parent-Sachen damit macht. Schauen Sie sich außerdem die dynamische Bindung an, um weitere interessante Informationen zu erhalten.
Andrei Bârsan
Ist das der namerichtige Typ?
Roman C

Antworten:

52

Zunächst eine Klarstellung der Terminologie: Wir weisen Childeiner Variablen vom Typ ein Objekt zu Parent. Parentist eine Referenz auf ein Objekt, das zufällig ein Subtyp von Parenta ist Child.

Es ist nur in einem komplizierteren Beispiel nützlich. Stellen Sie sich vor, Sie fügen getEmployeeDetailsder Klasse Parent Folgendes hinzu:

public String getEmployeeDetails() {
    return "Name: " + name;
}

Wir könnten diese Methode überschreiben, Childum weitere Details bereitzustellen:

@Override
public String getEmployeeDetails() {
    return "Name: " + name + " Salary: " + salary;
}

Jetzt können Sie eine Codezeile schreiben, die alle verfügbaren Details abruft, unabhängig davon, ob es sich bei dem Objekt um ein Parentoder um Folgendes handelt Child:

parent.getEmployeeDetails();

Der folgende Code:

Parent parent = new Parent();
parent.name = 1;
Child child = new Child();
child.name = 2;
child.salary = 2000;
Parent[] employees = new Parent[] { parent, child };
for (Parent employee : employees) {
    employee.getEmployeeDetails();
}

Wird zur Ausgabe führen:

Name: 1
Name: 2 Salary: 2000

Wir haben a Childals Parent. Es hatte ein spezielles Verhalten, das für die ChildKlasse einzigartig war , aber als wir getEmployeeDetails()anriefen, konnten wir den Unterschied ignorieren und uns darauf konzentrieren, wie Parentund Childähnlich sind. Dies wird als Subtyp-Polymorphismus bezeichnet .

In Ihrer aktualisierten Frage wird gefragt, warum Child.salarynicht zugänglich ist, wenn das ChildObjekt in einer ParentReferenz gespeichert ist . Die Antwort ist der Schnittpunkt von "Polymorphismus" und "statischer Typisierung". Da Java zur Kompilierungszeit statisch typisiert ist, erhalten Sie bestimmte Garantien vom Compiler, müssen jedoch im Austausch Regeln befolgen, da der Code sonst nicht kompiliert wird. Hier besteht die relevante Garantie darin, dass jede Instanz eines Subtyps (z. B. Child) als Instanz seines Supertyps (z Parent. B. ) verwendet werden kann. Beispielsweise wird Ihnen garantiert, dass beim Zugriff employee.getEmployeeDetailsoder employee.namebeim Definieren der Methode oder des Felds für jedes Nicht-Null-Objekt, das einer Variablen employeevom Typ zugewiesen werden könnteParent . Um diese Garantie zu gewährleisten, berücksichtigt der Compiler nur diesen statischen Typ (im Grunde den Typ der Variablenreferenz).Parent ), wenn er entscheidet, auf was Sie zugreifen können. Sie können also nicht auf Mitglieder zugreifen, die für den Laufzeittyp des Objekts definiert sind Child.

Wenn Sie wirklich wollen , eine verwenden , Childals Parentdies ist eine einfache Einschränkung zu leben und Ihr Code für verwendbar sein Parentund alle Subtypen. Wenn dies nicht akzeptabel ist, geben Sie den Typ der Referenz an Child.

John Watts
quelle
22

Wenn Sie Ihr Programm kompilieren, erhält die Referenzvariable der Basisklasse Speicher und der Compiler überprüft alle Methoden in dieser Klasse. Daher werden alle Methoden der Basisklasse überprüft, nicht jedoch die Methoden der untergeordneten Klasse. Jetzt können zur Laufzeit, wenn das Objekt erstellt wird, nur überprüfte Methoden ausgeführt werden. Falls eine Methode in der untergeordneten Klasse überschrieben wird, wird diese Funktion ausgeführt. Andere Funktionen der untergeordneten Klasse werden nicht ausgeführt, da der Compiler sie zum Zeitpunkt der Kompilierung nicht erkannt hat.

Akshat Chaturvedi
quelle
1
Diese Antwort macht Sinn! Danke
user7098526
12

Sie können über eine gemeinsame übergeordnete Schnittstelle auf alle Unterklassen zugreifen. Dies ist vorteilhaft für die Ausführung gemeinsamer Operationen, die für alle Unterklassen verfügbar sind. Ein besseres Beispiel wird benötigt:

public class Shape
{
  private int x, y;
  public void draw();
}

public class Rectangle extends Shape
{ 
  public void draw();
  public void doRectangleAction();
}

Nun, wenn Sie haben:

List<Shape> myShapes = new ArrayList<Shape>();

Sie können jedes Element in der Liste als Form referenzieren. Sie müssen sich keine Sorgen machen, ob es sich um ein Rechteck oder einen anderen Typ handelt, wie z. B. Kreis. Sie können sie alle gleich behandeln; Sie können alle zeichnen. Sie können doRectangleAction nicht aufrufen, da Sie nicht wissen, ob die Form wirklich ein Rechteck ist.

Dies ist ein Handel zwischen der generischen Behandlung von Objekten und der spezifischen Behandlung von Objekten.

Wirklich, ich denke, Sie müssen mehr über OOP lesen. Ein gutes Buch sollte helfen: http://www.amazon.com/Design-Patterns-Explained-Perspective-Object-Oriented/dp/0201715945

anio
quelle
2
Verwenden Sie in Ihrem zitierten Beispiel "abstrakte Klasse" oder "Schnittstelle"? Weil eine einfache Klasse keine nicht implementierten Methoden haben kann.
HQuser
1
@HQuser - gut entdeckt, scheint mehr über Polymorphismus im Kontext der Frage erklären zu wollen, aber dies muss sicherlich geändert werden.
Vivek
11

Wenn Sie einer Unterklasse einen übergeordneten Typ zuweisen, bedeutet dies, dass Sie damit einverstanden sind, die allgemeinen Funktionen der übergeordneten Klasse zu verwenden.

Es gibt Ihnen die Freiheit, von verschiedenen Unterklassenimplementierungen zu abstrahieren. Infolgedessen werden Sie mit den übergeordneten Funktionen eingeschränkt.

Diese Art der Zuweisung wird jedoch als Upcasting bezeichnet.

Parent parent = new Child();  

Das Gegenteil ist Niedergeschlagenheit.

Child child = (Child)parent;

Wenn Sie also eine Instanz von erstellen Childund diese an ein Downcast senden, können ParentSie dieses Typattribut verwenden name. Wenn Sie eine Instanz von erstellen Parent, können Sie dasselbe wie im vorherigen Fall tun, aber nicht verwenden, salaryda das Attribut in der Instanz nicht vorhanden ist Parent. Kehren Sie zum vorherigen Fall zurück, der salarynur bei Downcasting verwendet werden kann Child.

Es gibt detailliertere Erklärungen

Roman C.
quelle
Dafür kann ich Parent parent = new Parent () machen. Warum das tun? Bitte helfen Sie mir.
Narendra Pal
Stimmt, erklärt aber immer noch nicht, warum Sie wollen würden.
John Watts
Warum dies zu tun ist, hängt davon ab, was Sie tun möchten.
Roman C
Wenn Sie zuweisen Parent, können Sie verwenden, Parentwenn Sie zuweisen (wenn Sie können) Child, können Sie verwenden Child.
Roman C
1
Kind Kind = (Kind) Elternteil; Dies führt zu ClassCastException. Sie können ein übergeordnetes Objekt nicht auf eine untergeordnete Referenzvariable herunterstufen.
Muhammad Salman Farooq
7

Es ist einfach.

Parent parent = new Child();

In diesem Fall ist der Typ des Objekts Parent. Ameise Parenthat nur eine Eigenschaft. Es ist name.

Child child = new Child();

Und in diesem Fall ist der Typ des Objekts Child. Ameise Childhat zwei Eigenschaften. Sie sind nameund salary.

Tatsache ist, dass das nicht endgültige Feld nicht sofort bei der Deklaration initialisiert werden muss. Normalerweise geschieht dies zur Laufzeit, da Sie häufig nicht genau wissen, welche Implementierung Sie genau benötigen. Stellen Sie sich zum Beispiel vor, Sie haben eine Klassenhierarchie mit der Klasse Transportan der Spitze. Und drei Unterklassen: Car, Helicopterund Boat. Und es gibt noch eine andere Klasse, Tourdie ein Feld hat Transport. Das ist:

class Tour {
   Transport transport;
}  

Solange ein Benutzer keine Reise gebucht und keine bestimmte Transportart ausgewählt hat, können Sie dieses Feld nicht initialisieren. Es ist zuerst.

Zweitens wird angenommen, dass alle diese Klassen eine Methode haben müssen, go()jedoch mit einer anderen Implementierung. Sie können standardmäßig eine Basisimplementierung in der Oberklasse definieren Transportund eindeutige Implementierungen in jeder Unterklasse besitzen. Mit dieser Initialisierung können Transport tran; tran = new Car();Sie die Methode aufrufen tran.go()und Ergebnisse abrufen , ohne sich um eine bestimmte Implementierung kümmern zu müssen. Es wird eine überschriebene Methode aus einer bestimmten Unterklasse aufgerufen.

Darüber hinaus können Sie die Instanz der Unterklasse überall dort verwenden, wo die Instanz der Oberklasse verwendet wird. Zum Beispiel möchten Sie die Möglichkeit bieten, Ihren Transport zu mieten. Wenn Sie nicht Polymorphismus verwenden, haben Sie eine Menge von Methoden für jeden Fall schreiben: rentCar(Car car), rentBoat(Boat boat)und so weiter. Gleichzeitig können Sie durch Polymorphismus eine universelle Methode erstellen rent(Transport transport). Sie können darin ein Objekt jeder Unterklasse von übergeben Transport. Wenn sich Ihre Logik im Laufe der Zeit erhöht und Sie eine weitere Klasse in der Hierarchie erstellen müssen? Wenn Sie Polymorphismus verwenden, müssen Sie nichts ändern. Erweitern Sie einfach die Klasse Transportund übergeben Sie Ihre neue Klasse an die Methode:

public class Airplane extends Transport {
    //implementation
}

und rent(new Airplane()). Und new Airplane().go()im zweiten Fall.

Kapelchik
quelle
das weiß ich, aber meine frage ist warum das zu tun? Wenn wir Parent machen können, ist p = new Parent (), um die übergeordnete Referenz zu erhalten.
Narendra Pal
Das ist Polymorphismus . Es ist sehr nützlich. Polymorphismus macht Ihr Programm flexibler und erweiterbarer. Wie genau kann das nützlich sein? Siehe meine Beispiele in der aktualisierten Antwort .
Kapelchik
1
Tolles Beispiel mit Transport ... leicht zu verstehen. Vielen Dank!
ChanwOo Park
2

Diese Situation tritt auf, wenn Sie mehrere Implementierungen haben. Lassen Sie mich erklären. Angenommen, Sie haben mehrere Sortieralgorithmen und möchten zur Laufzeit den zu implementierenden auswählen oder jemand anderem die Möglichkeit geben, seine Implementierung hinzuzufügen. Um dieses Problem zu lösen, erstellen Sie normalerweise eine abstrakte Klasse (Parent) und haben eine andere Implementierung (Child). Wenn Sie schreiben:

Child c = new Child();

Sie binden Ihre Implementierung an die Child-Klasse und können sie nicht mehr ändern. Andernfalls, wenn Sie verwenden:

Parent p = new Child();

Solange Child Parent erweitert, können Sie es in Zukunft ändern, ohne den Code zu ändern.

Dasselbe kann mit Schnittstellen gemacht werden: Parent ist keine Klasse mehr, sondern eine Java-Schnittstelle.

Im Allgemeinen können Sie diesen Ansatz in DAO-Mustern verwenden, in denen Sie mehrere DB-abhängige Implementierungen wünschen. Sie können einen Blick auf FactoryPatter oder AbstractFactory Pattern werfen. Hoffe das kann dir helfen.

FrancescoAzzola
quelle
1

Angenommen, Sie möchten ein Array von Instanzen der Parent-Klasse und eine Reihe von Child-Klassen Child1, Child2, Child3, die Parent erweitern. Es gibt Situationen, in denen Sie nur an der allgemeineren Implementierung der übergeordneten Klasse interessiert sind und sich nicht für spezifischere Dinge interessieren, die von untergeordneten Klassen eingeführt werden.

Monolog
quelle
1

Ich denke, alle obigen Erklärungen sind etwas zu technisch für Leute, die mit objektorientierter Programmierung (OOP) noch nicht vertraut sind. Vor Jahren habe ich eine Weile gebraucht, um mich damit zu beschäftigen (als Jr Java Developer), und ich habe wirklich nicht verstanden, warum wir eine übergeordnete Klasse oder eine Schnittstelle verwenden, um die tatsächliche Klasse, die wir tatsächlich aufrufen, unter der Decke zu verbergen.

  1. Der unmittelbare Grund dafür ist, die Komplexität zu verbergen, damit sich der Anrufer nicht oft ändern muss (in Laienbegriffen gehackt und aufgebockt werden). Dies ist sehr sinnvoll, insbesondere wenn Sie vermeiden möchten, dass Fehler entstehen. Und je mehr Sie Code ändern, desto wahrscheinlicher ist es, dass sich einige davon auf Sie einschleichen. Wenn Sie jedoch nur Code erweitern, ist es weitaus weniger wahrscheinlich, dass Sie Fehler haben, da Sie sich jeweils auf eine Sache konzentrieren und sich Ihr alter Code nicht oder nur geringfügig ändert. Stellen Sie sich vor, Sie haben eine einfache Anwendung, mit der die Mitarbeiter der Ärzteschaft Profile erstellen können. Nehmen wir zur Vereinfachung an, wir haben nur Allgemeinmediziner, Chirurgen und Krankenschwestern (in Wirklichkeit gibt es natürlich viel spezifischere Berufe). Für jeden Beruf, Sie möchten einige allgemeine und einige spezifische Informationen nur für diesen Fachmann speichern. Beispielsweise kann ein Chirurg allgemeine Felder wie Vorname, Nachname, Jahre Erfahrung als allgemeine Felder haben, aber auch bestimmte Felder, z. B. Spezialisierungen, die in einer Listeninstanzvariablen gespeichert sind, wie Liste mit Inhalten, die "Knochenchirurgie", "Augenchirurgie" usw. ähnlich sind. Eine Krankenschwester hätte nichts davon, könnte aber Listenverfahren haben, mit denen sie vertraut ist. GeneralPractioners hätte ihre eigenen Besonderheiten. Als Ergebnis, wie Sie ein Profil von bestimmten speichern. Sie möchten jedoch nicht, dass Ihre ProfileManager-Klasse über diese Unterschiede informiert wird, da sie sich im Laufe der Zeit zwangsläufig ändern und erhöhen, wenn Ihre Anwendung ihre Funktionalität auf weitere medizinische Berufe erweitert, z. B. Physiotherapeut, Kardiologe, Onkologe usw. Sie möchten lediglich, dass Ihr ProfileManger save () sagt, unabhängig davon, wessen Profil er speichert. Daher ist es üblich, diese hinter und Schnittstelle und abstrakte Klasse oder eine Elternklasse zu verbergen (wenn Sie die Erstellung eines allgemeinen medizinischen Mitarbeiters zulassen möchten). In diesem Fall wählen wir eine übergeordnete Klasse aus und nennen sie MedicalEmployee. Unter dem Deckmantel kann es auf jede der oben genannten spezifischen Klassen verweisen, die es erweitern. Wenn der ProfileManager myMedicalEmployee.save () aufruft, wird die save () -Methode polymorph (viele-strukturell) in den richtigen Klassentyp aufgelöst, der ursprünglich zum Erstellen des Profils verwendet wurde, z. B. Nurse, und ruft darin die save () -Methode auf Klasse. oder eine Elternklasse (wenn Sie die Erstellung eines allgemeinen medizinischen Mitarbeiters zulassen möchten). In diesem Fall wählen wir eine übergeordnete Klasse aus und nennen sie MedicalEmployee. Unter dem Deckmantel kann es auf jede der oben genannten spezifischen Klassen verweisen, die es erweitern. Wenn der ProfileManager myMedicalEmployee.save () aufruft, wird die save () -Methode polymorph (viele-strukturell) in den richtigen Klassentyp aufgelöst, der ursprünglich zum Erstellen des Profils verwendet wurde, z. B. Nurse, und ruft die save () -Methode auf Klasse. oder eine Elternklasse (wenn Sie die Erstellung eines allgemeinen medizinischen Mitarbeiters zulassen möchten). In diesem Fall wählen wir eine übergeordnete Klasse aus und nennen sie MedicalEmployee. Unter dem Deckmantel kann es auf jede der oben genannten spezifischen Klassen verweisen, die es erweitern. Wenn der ProfileManager myMedicalEmployee.save () aufruft, wird die save () -Methode polymorph (viele-strukturell) in den richtigen Klassentyp aufgelöst, der ursprünglich zum Erstellen des Profils verwendet wurde, z. B. Nurse, und ruft darin die save () -Methode auf Klasse.

  2. In vielen Fällen wissen Sie nicht genau, welche Implementierung Sie zur Laufzeit benötigen. Aus dem obigen Beispiel wissen Sie nicht, ob ein Generalpraktiker, ein Chirurg oder eine Krankenschwester ein Profil erstellen würde. Sie wissen jedoch, dass Sie dieses Profil nach Abschluss speichern müssen, egal was passiert. MedicalEmployee.profile () macht genau das. Es wird von jedem bestimmten Typ von MedicalEmployee repliziert (überschrieben) - GeneralPractitioner, Surgeon, Nurse,

  3. Das Ergebnis von (1) und (2) oben ist, dass Sie jetzt neue medizinische Berufe hinzufügen, save () in jeder neuen Klasse implementieren und dadurch die save () -Methode in MedicalEmployee überschreiben können und ProfileManager bei nicht ändern müssen alles.

George Smith
quelle
1

Ich weiß, dass dies ein sehr alter Thread ist, aber ich bin einmal auf denselben Zweifel gestoßen.

Das Konzept von Parent parent = new Child();hat also etwas mit frühem und spätem Binden in Java zu tun.

Die Bindung von privaten, statischen und endgültigen Methoden erfolgt beim Kompilieren, da sie nicht überschrieben werden können und die normalen Methodenaufrufe und überladenen Methoden Beispiele für eine frühe Bindung sind.

Betrachten Sie das Beispiel:

class Vehicle
{
    int value = 100;
    void start() {
        System.out.println("Vehicle Started");
    }

    static void stop() {
        System.out.println("Vehicle Stopped");
    }
}

class Car extends Vehicle {

    int value = 1000;

    @Override
    void start() {
        System.out.println("Car Started");
    }

    static void stop() {
        System.out.println("Car Stopped");
    }

    public static void main(String args[]) {

        // Car extends Vehicle
        Vehicle vehicle = new Car();
        System.out.println(vehicle.value);
        vehicle.start();
        vehicle.stop();
    }
}

Ausgabe: 100

Auto gestartet

Fahrzeug angehalten

Dies geschieht, weil stop()es sich um eine statische Methode handelt, die nicht überschrieben werden kann. Die Bindung von stop()erfolgt also zur Kompilierungszeit und start()ist nicht statisch. Sie wird in der untergeordneten Klasse überschrieben. Die Informationen über den Objekttyp sind also nur zur Laufzeit verfügbar (späte Bindung), und daher wird die start()Methode der Fahrzeugklasse aufgerufen.

Auch in diesem Code vehicle.valuegibt uns das 100als Ausgabe an, da die Initialisierung von Variablen nicht zu spät gebunden wird. Das Überschreiben von Methoden ist eine der Möglichkeiten, mit denen Java den Laufzeitpolymorphismus unterstützt .

  • Wenn eine überschriebene Methode über eine Oberklassenreferenz aufgerufen wird, bestimmt Java anhand des Typs des Objekts, auf das zum Zeitpunkt des Aufrufs verwiesen wird, welche Version (Oberklasse / Unterklassen) dieser Methode ausgeführt werden soll. Somit erfolgt diese Bestimmung zur Laufzeit.
  • Zur Laufzeit hängt es vom Typ des Objekts ab, auf das verwiesen wird (nicht vom Typ der Referenzvariablen), das bestimmt, welche Version einer überschriebenen Methode ausgeführt wird

Ich hoffe, dies beantwortet, wo Parent parent = new Child();es wichtig ist und warum Sie mit der obigen Referenz nicht auf die untergeordnete Klassenvariable zugreifen konnten.

Swapneil
quelle
-3

Sie deklarieren parent als Parent, sodass Java nur Methoden und Attribute der Parent-Klasse bereitstellt.

Child child = new Child();

sollte arbeiten. Oder

Parent child = new Child();
((Child)child).salary = 1;
Thargor
quelle
1
Das funktioniert natürlich. OP fragte nach einem Kontext, in dem Parent parent = new Child();es nützlich / sinnvoll ist.
Baz
2
Es ist die gleiche Erklärung wie in den anderen Antworten. Warum also abstimmen? Ich sage nicht, dass ich nicht arbeite, ich sage, wenn Sie Child c = new Parant () sagen, dann haben Sie eine Instanz von Parent und nicht von Child, und so haben Sie kein Attributgehalt. Wenn Sie das Attributgehalt möchten, müssen Sie eine Instanz von Child erstellen oder Parent to Child
umwandeln