Vererbung und Polymorphie

Status
Nicht offen für weitere Antworten.

hdi

Top Contributor
Dies ist ein kleiner Crash-Kurs in Sachen Vererbung und Polymorphie im Allgemeinen. Polymorphie ist ein
wesentlicher, wenn nicht der wesentlichste Bestandteil von OO-Programmierung. Los geht's:

Vererbung
Vererbung bedeutet: Wenn man mehrere Objekte hat, die in Teilen identisch sind, d.h. in ihrem Aussehen
oder ihren Eigenschaften (Attribute) oder ihrem Verhalten (Methoden) teils gleich sind, sollte man
Vererbung verwenden. Dazu findet man eine sog. "Oberklasse / Superklasse" für all diese Objekte. Sie besteht
dann genau aus diesen Dingen, in denen sich alle Objekte gleich sind.
Wenn man nun mehrere solch ähnliche Objekte hat, kann man einfach von der Oberklasse erben, und spart
sich dadurch erstens viel Schreibarbeit, und nutzt zum zweiten (und das ist das wichtige) die Polymorphie
von Java.

Bsp:

Code:
public class Auto{

   
   private int ps;
   private String marke;
   private String modell;
   private Color lackFarbe;
   private double marktWert;
   private String baujahr;
   
   public Auto( String marke, String modell, String baujahr){
         this.marke = marke;
         this.modell = modell;
         this.baujahr = baujahr;
   }

   // ...

}

Wenn ich nun einen BMW und einen Audi haben will, so sind das beides Autos, und sie haben beide all die Attribute
der Klasse Auto. Statt also zweimal die gleiche Klassenstruktur aufzuziehen, mache ich Auto zur Superklasse und
erbe dann davon:

Code:
public class BMW extends Auto{}
public class Audi extends Auto

Polymorphismus heisst nun:
Java nimmt überall dort, wo eine Klasse erwartet wird, auch jede Unterklasse dieser Klasse an.

So könnte ich zB die Methode

Code:
kaufeAuto (Auto a){...}

aufrufen mit:

Code:
kaufeAuto (new BMW( "BMW", "7er", "2007"));

oder mit:

Code:
kaufeAuto (new Audi( "Audi", "A8", "2005"));

Das ist also der Vorteil von Vererbung. Ziel beim Programmieren ist es, eine Klassen-Struktur zu finden,
sodass an jeder Stelle im Programm jeweils nur die Informationen gekapselt sind, die mich interessieren.
Damit meine ich: Wenn ich eine Methode habe

Code:
verkaufeAuto (Auto a){...}

Angenommen diese Methode steht in einer Klasse "Autohändler", dann ist es mir völlig Wurscht ob das
nun ein BMW oder ein Audi oder sonst was ist. Ich bin ja kein BMW-Händler oder Audi-Händler. Ich bin
Auto-Händler, und was ich will, das sind Autos. Und diese Abstrahierung von einem BMW zu einem
"Auto" ganz generell ist der Polymorphismus: Die Klasse Autohändler kennt keine spezifischen Autos,
sondern nur allgemein Autos. D.h. auch, dass ich jederzeit neue Auto-Unterklassen erstellen oder löschen
kann, ohne dass ich im Code von "Autohändler" etwas ändern muss.

Vererbung, Sichtbarkeit und Overriding
Im Zusammenhang mit Vererbung sollte man auch das Kapitel der Sichtbarkeit nicht weglassen.
Es gibt in Java 3 verschiedene Schlüsselwörter, mit denen man die Sichtbarkeit einer Variablen oder
einer Methode spezifizieren kann. "Variablen oder Methode" bezeichne ich im Folgenden als v/m :

1) private
2) protected
3) public

private heisst: v/m ist nur in der Klasse, in der die v/m definiert ist, sichtbar.
protected heisst: v/m ist in allen Klassen sichtbar, die sich im selben Package befinden wie die Klasse,
in der die v/m definiert ist.
public heisst: v/m ist überall sichtbar
Anmerkung: Man kann ein Schlüsselwort auch komplett weglassen, ich bin mir nicht sicher aber ich
glaube dann ist es immer automatisch protected, oder irgendein Zwischending zwischen protected
und was anderem ;) Ist nicht so wichtig, weil man sowas eh nie machen sollte.

Damit klar ist, was ich mit "sichtbar" meine: Sichtbar heisst einfach, ich kann auf die v/m zugreifen
an einer gewissen Stelle/Klasse im Programm.

Wie hängt das jetzt also mit Vererbung zusammen?
public und protected v/m werden vererbt und sind in der Unterklasse sichtbar.
private v/m werden implizit auch vererbt, sind aber in Unterklassen nicht sichtbar.

Hier besteht also der kleine aber feine Unterschied bei private v/m: Sie sind weder in Unterklassen
noch in komplett anderen Klassen sichtbar, aber wenn man sie vererbt, sind sie in der Unterklasse
"da", wenn auch versteckt. Ich denke ohne Beispiel wird das nicht ganz klar:

Code:
public class Auto{

     private int ps;

     public Auto (int ps){
          this.ps = ps;
     }
     public int getPs(){
           return this.ps;
     }
}

Code:
public class BMW extends Auto{

     public void pimp(){
           this.ps += 20;  // Compile-Error: Variable "ps" nicht bekannt
     }
}

Aber:

Code:
BMW myBmw = new BMW(200);
myBmw.getPs(); // <- liefert "200", also gibt es "ps" scheinbar doch ?!

Merke: Auch wenn private v/m in Unterklassen nicht direkt sichtbar sind, so sind sie doch da, weil es ja
eine Unterklasse ist, und sie demnach alle Attribute und Methoden der Superklasse hat. Zugreifen tut
man auf private Attribute immer mit - öffentlichen (public) - "Gettern" und "Settern".

Jetzt noch zu dem Wort "Override":
Das heisst ßberschreiben von Methoden aus der Superklasse in Unterklassen. Wenn wir uns das obige Bsp
mit Auto und BMW ankucken, wäre das hier ein Override:

Code:
public class BMW extends Auto{

     @Override
      public int getPs(){ 
               // ein Aufruf von bmw.getPs() liefert jetzt nicht mehr "ps" der Oberklasse, sondern das, 
               // was hier drin steht!! -> Methode wurde überschrieben!
               return 10;
      }
}

Kommentar sagt alles. Overriding ist eher nicht so toll, zumindest nicht von eigenen Methoden.
Dennoch ist die ursprüngliche Methode getPs() der Superklasse "Auto" nicht verloren. Man greift
auf Attribute und Methoden der Oberklasse per "super" zu:

Code:
@Override
      public int getPs(){ 
              return super.getPs(); // ßberschrieben aber wieder zur Oberklassen-Methode gelenkt.
                                                // Macht eindeutig eher nicht so viel Sinn...
      }


Mehrfach-Vererbung:
Es sei noch erwähnt, dass "echte" Mehrfach-Vererbung in Java nicht möglich ist:

Code:
public class BMW extends Auto extends Fahrzeug{} // Compile-Fehler!

Allerdings kann man über Verschachtelug von Super-/und Unterklassen eine Art Mehrfach-Vererbung
erreichen. so geht folgendes ohne Probleme:

Code:
public class Fahrzeug{}
public class Auto extends Fahrzeug{}
public class BMW extends Auto{}

Ein Objekt vom Typ BMW wäre nun sowohl ein BMW, als auch ein Auto, als auch ein Fahrzeug. Wenn
mein Autohändler also noch allgemeiner wäre, und nicht nur Autos annehmen würde sondern auch
Motorräder, könnte ich in einer Methode

Code:
verkaufe ( Fahrzeug f){}
nach wie vor einen BWM übergeben. Weil BMW = Auto = Fahrzeug! Das lässt sich beliebig weiter verschachteln.

das Schlüsselwort 'abstract'
Im Zusammenhang mit Vererbung wird auch oft das Keyword 'abstract' benutzt. Die Bedeutung ist im
Prinzip einfach der deutschen ßbersetzung zu entnehmen: "Abstrakt" ist ein Ding, dass es so in der
Realität gar nicht gibt, sondern nur eine Art Muster oder Rohfassung etc. ist...

Abstrakte Klassen:
... heisst jetzt also, ich habe etwas, das es so an sich gar nicht gibt, Bsp:

Code:
public abstract class Auto{}

Das trifft auch tatsächlich zu, denn gibt es sowas wie ein "Auto"? Nein! Du kannst nicht irgendwohin gehen
und sagen "ich möchte ein Auto". Das gibt es gar nicht. Es gibt nur BMW, Audi, VW, ... kein "Auto".
Deshalb ist die Klasse Auto ein gutes Beispiel für eine abstrakte Klasse. Was bringt das nun? Naja,
wenn eine Klasse abstrakt ist, heisst das einfach nur: Man kann keine Instanz davon erstellen:

Code:
new Auto(); // Compile-Error: Auto ist abstrakt, kann kein "Auto" erstellen

Das ist eigentlich alles, bringt also programmiertechnisch quasi nur die Sicherheit, dass man gar nicht erst
versuchen kann, ein Auto zu erstellen. Mehr nicht, man kann es genauso gut weglassen wenn man selbst
darauf achtet, nur spezifische Unterklassen zu erstellen.

Abstrakte Methoden:
Abstrakte Methoden sind jetzt aber schon vielmehr auch praktisch nützlich und ein ganz wesentlicher
Bestandteil von Polymorphie! ßber Vererbung habe ich gesagt, man nimmt sie, wenn Teile verschiedener Objekte
gleich sind. Vererbung zusammen mit abstrakten Methoden nimmt man dahingegen dann, wenn die
Funktionalität von den Objekten gleich sein soll (was sie können), aber die Implementierung unterschiedlich
ist auf Grund der Unterschiede zwischen den Klassen. (und nicht gleich wie bei Vererbung).

Bsp: Ein "Kreis" und ein "Quadrat" sind beides geometrische Figuren. Sie haben beide zB eine Position im
Koordinaten System (x,y), haben beide eine gewisse Grösse (Durchmesser bzw. Kantenlänge), und können zB
auch beide in einem JPanel gemalt werden.

Man kann das aber nicht mit "reiner" Vererbung lösen:

Code:
public abstract class GeometrischeFigur{

       public void male(Graphics g){
            // tja, wie malt sich eine "geometrische Figur". Was sind die Gemeinsamkeiten im Zeichenvorgang
            // von Kreisen und Quadraten?? Antwort: KEINE! 
       }
}

public class Kreis extends GeometrischeFigur
public class Quadrat extends GeometrischeFigur

Das Problem steht im Kommentar: Man die Methode male() nicht so schreiben, dass sie für alle geometrischen
Figuren funktioniert. Die einzige Lösung wäre über irgendwelche Abfragen:

Code:
public void male(Graphics g){
            if ( this instanceof Kreis ){
                   g.drawOval(...)
            }
            else if (this instanceof Quadrat){
                   g.drawRect(...)
            }
}

Sieht gar nicht so schlimm aus? Jetzt stell dir vor, es gibt nicht 2 verschiedene geometrische Objekte, sondern 200.
Viel Spass beim if-else-Hirntod! Und noch schlimmer: Wenn es nicht nur die Methode male() für jedes geometrische
Objekt geben soll, sondern auch noch die Methoden "verschiebe()", "drehe()", "färbe()", ...
Wie lange hockt man dann davor, um eine einzige neue Funtkionalität zu implementieren?

Die Lösung für das Problem sind abstrakte Methoden. Eine abstrakte Methode wird in der Superklasse nur definiert,
nicht deklariert,also nicht implementiert:

Code:
public abstract class GeometrischeFigur{

      public abstract void male(Graphics g); // kein Rumpf!
}

Jede Unterklasse, die jetzt von GF erbt, muss diese Methode für sich selbst implementieren, je nach dem wie
es halt passt. Die Klassen "Kreis" und "Quadrat" würden jetzt so aussehen:

Code:
public class Kreis extends GeometrischeFigur{
     @Override
      public void male(Graphics g){
             g.drawOval(...); // eigene Implementierung: zeichen Kreis
      }
}
Code:
public class Quadrat extends GeometrischeFigur{
     @Override
      public void male(Graphics g){
             g.drawRect(...); // eigene Implementierung: zeichen Rechteck
      }
}

So, jetzt haben wir noch immer diese schöne Kapselung wie auch oben bei der Vererbung ohne abstrakte
Methoden, d.h. ein "Geometrie-Scanner" würde alle Typen von Superklasse GemoetrischeFigur erkennen,
und das gute ist, er kannauch alle korrekt malen:

Code:
/* Dummer Klassen-Name: Verwaltet einfach geometrische Figuren und macht etwas damit */
public class Scanner{

        private List<GeometrischeFigur> meineFiguren;

        public void maleAlles(Graphics g){
                   for( GeometrischeFigur gf : meineFiguren ){
                        gf.male(g);
                   }
        }
}

Das tolle: Der Scanner hat keinen Plan, was "gf" jetzt im einzelnen ist, ob ein Kreis oder ein Quadrat oder sont
was. Ihm ist es aber egal: Er weiss, er hat geometrische Objekte, und diese Klasse bietet die Methode "male()",
also kann er sie aufrufen. Was genau da passiert ist ihm Wurscht. Und das übernehmen halt die einzelnen
Unterklassen davon jeweils für sich.

Interfaces
Es gibt kaum einen Unterschied zwischen Interfaces und abstrakten Methoden:

Code:
public abstract class Paintable{
       public abstract void paint();
       //Bem.: Sobald eine Klasse eine abstrakte Methode hat, 
       //MUSS die Klasse selbst auch abstrakt sein.
}
und
Code:
public interface Paintable{
       public void paint();
}

sind so ziemlich das gleiche. Ein Grund für Interfaces ist das o.g. Problem der Mehrfachvererbung.
Man könnte also die zweite Klasse, von der man erben will, als Interface erstellen und dieses dann
implementieren. Beachte, dass man bei Interfaces so viele implementieren kann, wie man will:

Code:
public class MegaClass extends MegaPower implements Interface1, Interface2, Interface3, .... // <-Okay!

Interfaces funktionieren also im Prinzip genauso wie das Erben von Superklassen mit abstrakten Methoden,
nur hat das Erben immer eine Einschränkung auf Sicht der Polymorphie. Deshalb gilt i.d.R:
Code:
Wenn man die Wahl hat zwischen der Implementierung eines Interfaces oder der Erbung von einer Klasse mit
abstrakten Methoden, sollte man immer lieber zum Interface greifen!
Es ist einfach flexiblerer Code.

Zusammenfassung / praktische Tipps
Manch einer denkt sich jetzt vllt: Das ist ja alles schön und gut, aber nur weil man jemandem die Regeln
von Schach erklärt hat, heisst das noch lange nicht, dass er ein Spiel auch nur annähernd gewinnen kann.
Ich hab diese Verbindung zu Schach mal irgendwo gelesen und fand sie extrem passend! Programmieren ist wie
Schach: Man lernt es, indem man es tut!
Trotzdem gibt es wie auch im Schach ein paar Tipps, ein paar Regeln, die einem einen Stubs in die
richtige Richtung geben können. Wenn man weiss, auf was man achten muss, erkennt man auch Gefahren.

Die folgenden Dinge kann man sich durchaus auf ein Blatt Papier schreiben und immer mal wieder durchlesen,
bis man sie verinnerlicht hat. Sie helfen beim Schreiben von gutem Design/Code:

1) Switch-Konstrukte oder nicht-elementare if-Statements (alles, was nicht mit <,>=,==,|| etc geprüft wird)
weisen auf einen Design-Fehler hin, den man durch Polymorphismus lösen kann. Bsp: Methode male() vor
und nach Benutzung von abstrakten Methoden
2) Vererbte Dinge sind immer in den Unterklassen da, aber nur public v/m sind echt "sichtbar"
3) Man verwendet immer private für Attribute einer Klasse und macht sich Getter/Setter um in Unterklassen
(und evtl auch aus anderen Klassen) darauf zugreifen zu können
4) Vererben tut man, wenn verschiedene Objekte in Teilen gleich sind und sich gleich Verhalten
5) Abstrakte Klassen nutzt man, wenn es die Oberklasse an sich gar nicht gibt
6) Abstrakte Methoden nutzt man dann, wenn man vererben möchte, die Funktionalitäten aber in jeder Unterklasse
anders implementiert werden müssen, und nicht gleich sind.
7) die Implementierung eines Interfaces ist sowas wie das Implementieren von abstrakten Methoden einer
Superklasse, nur besser!


Hoffe, ich konnte ein paar Leuten etwas klarer machen.

Wer Fehler findet oder Anregungen hat, was ich noch vergessen hab, bitte Bescheid geben!
 
Status
Nicht offen für weitere Antworten.

Oben