Fortsetzung von letzter Seite
Beispielprojekt:
Statische Methoden und Variablen
Theoretisch mag man den Sachverhalt nun verstanden haben, jedoch möchte
ich das aus noch praktisch verdeutlichen. Ich möchte einen Umrechner
für kW in PS-Beträge entwerfen. Der Benutzer soll eine Zahl
eingeben und dann wählen dürfen, ob er von kW in PS oder anders
herum konvertieren möchte. Eine statische Methode soll dann prüfen,
ob die korrekte Zahl eingegeben wurde. Eine andere Methode soll dann die
Berechnung vornehmen, das ganze wird dann schön formatiert ausgegeben.
Der Quelltext dazu soll so aussehen:
import java.lang.Math;
public class Rechner { private int zahl; public static int auswahl;
public static void setAuswahl(int w) { if (w==1 || w ==2) auswahl =w; else auswahl=1; }
public void setZahl(int zahl) { if(zahl >=0) this.zahl=zahl; else zahl=1; }
public void showErgebnis() { int erg; if(auswahl==1) { erg = (int)Math.floor((zahl * 1.361) + 0.5d); System.out.println(zahl+"kW entsprechen "+ erg+"PS!");
} else { erg = (int)Math.floor((zahl * 0.735) + 0.5d); System.out.println(zahl+"PS entsprechen "+ erg+"kW!"); } } }
So sieht die Hauptklasse aus:
import java.io.*;
public class Haupt { public static void main(String argv[]) throws IOException { BufferedReader in = new BufferedReader (new InputStreamReader(System.in)); Rechner r1; r1 = new Rechner(); System.out.println(" - kW-PS Umrechner -"); System.out.println(" ----------------------------"); System.out.print("Für Umrechnung kW->PS (1), für PS->kW (2) eingeben:"); Rechner.setAuswahl(Integer.parseInt(in.readLine())); System.out.print("Bitte Betrag eingeben:"); r1.setZahl(Integer.parseInt(in.readLine())); r1.showErgebnis(); } }
Konstruktor
Ich möchte noch etwas zum Konstruktor loswerden. Zur Erinnerung:
Der Konstruktor ist bei der Erzeugung von Instanzen einer Klasse für
die Reservierung von Speicherplatz für die Instanz zuständig.
Bisher haben wir den Konstruktor (z.B. r1 = new Rechner();) immer nur
aufgerufen. Mittlerweile sollte jedem klar sein, dass in Java immer alles
definiert sein muss, bevor man es ausrufen kann. So ist es - wie sollte
es auch anders sein - auch bei den Konstruktoren. Eigentlich braucht man
sich in Java nicht um den Konstruktor zu kümmern, da der Compiler
(der Übersetzer des Quellcodes in Bytecode) so freundlich ist und
- sollte keiner definiert sein - diesen in der Klasse durch den Standardkonstruktor
ergänzt.
Es hat jedoch einen Grund, warum ich trotzdem auf den Konstruktor eingehe:
Der Standardkonstruktor hat keine Parameter! Manchmal ist es jedoch sinnvoll
mit Parametern zu arbeiten. Schließlich kann man diese überladen.
Da haben wir es schon! Durch Parameter kann man Überladen und durch
Überladen wird das Ganze flexibler und benutzerfreundlicher. Das
ist ein sehr guter Grund die wenigen Zeilen der Definition des Konstruktors
selber zu übernehmen.
Der Konstruktor ist eine Art "Methode ohne Rückgabewert"
und trägt immer denselben Namen wir die Klasse in der er steht. Der
Konstruktor hat KEINEN Rückgabewert (aus diesem Grund ist NICHT mit
void zu arbeiten!). Bei der Klasse "Rechner" sieht der Konstruktor
also so aus:
public Rechner() { }
Die Konstruktoren können in
der Klassendefinition zwar an einer beliebigen Stelle stehen, jedoch ist
es guter Stil folgende Reihenfolge einzuhalten: Attribute, Konstruktoren,
Methoden.
Vererbung
Die Vererbung ist eines der wichtigsten objektorientierten Prinzipien der
Klassen. Die Vererbung in Java ist im Grunde genommen der des Menschen
ganz ähnlich. Der Vorfahr - in Java die Ober- oder Superklasse -
vererbt an den Nachfahr - in Java die Unter- oder Subklasse - alle Eigenschaften,
jedoch kann der Nachfahr noch einige Eigenschaften zusätzlich haben.
In der Praxis sieht das so aus: Es wird eine allgemeine Klasse erstellt,
z.B. Mensch, danach wird eine weitere spezifischere Klasse, z.B. Arbeiter,
erstellt, bei der wird Vererbung eingesetzt, d.h. alle Eigenschaften werden
automatisch übernommen und in der zweiten Klasse werden zu den Eigenschaften
die die Klasse schon von der ersten geerbt hat noch weitere spezifische
Eigenschaften hinzugefügt. So braucht man die allgemeinen Eigenschaften
nur einmal definieren und kann diese in den speziellen Klassen um spezielle
Eigenschaften erweitern.
In Java wird die Vererbung mit folgender Syntax vollzogen:
public class Klasse1 { ... }
public class Klasse2 extends Klasse1 { ... }
Klasse1 ist die Oberklasse und Klasse2 die Unterklasse.
Man sieht, dass die eigentliche Vererbung mit extends Oberklasse von Statten geht. Nun kann man auch auf die geerbten Eigenschaften von Klasse2 zugreifen als wären es die eigenen Eigenschaften. Ist z.B.
in Klasse1 das Attribut name vereinbart, so ist es möglich
auf dieses Attribut der Klasse2 mittels Klasse2.name zuzugreifen,
obwohl doch eigentlich in Klasse2 kein solches Attribut vereinbart wurde;
die Vererbung macht's möglich!
Wir haben uns einige Abschnitte zuvor mit Modifiern befasst.
Diese haben auch Einfluss auf die Vererbung! Ist ein Attribut oder eine
Methode mit private gekennzeichnet, so wird diese NICHT mitvererbt,
sondern bleibt gemäß der Definition in der Oberklasse. Zusätzlich
zu dem Modifier private kommt bei der Vererbung noch der Modifier protected ins Spiel.
Dieser Modifier bewirkt in der Oberklasse das gleiche wie der Modifier private (der Zugriff von außen wird verhindert), jedoch
können Attribute oder Methoden, die mit protected gekennzeichnet
sind mitvererbt werden. So haben diese Attribute oder Methoden auch in
der Unterklasse die zugriffsbeschränkenden Eigenschaften.
Methoden überschreiben
Bei der Vererbung werden Eigenschaften aus der Oberklasse an die Unterklasse
vererbt und um Methoden und Attribute ergänzt. Wenn aber in der Unterklasse
eine Methode mit dem gleichen Namen, der gleichen Parameterliste und dem
gleichen Rückgabewert definiert wird, so nennt man das Überschreiben
einer Methode.
Beim Überschreiben der Methode gibt es zwei Möglichkeiten, entweder
man will eine ganz neue Definition der Methode durchführen, oder
man will die Methode nur erweitern. Wenn man die Methode nur erweitern
will, so kann man mit der Referenz super auf die Eigenschaft
der Oberklasse verweisen. Folgendes Beispiel soll das verdeutlichen:
public class Klasse1 { public void meth() { System.out.println("Sehr geehrte Damen,"); } }
public class Klasse2 extends Klasse1 { public void meth() { super.meth(); System.out.println("liebe Herren"); } }
public class Haupt { public static void main (String argv[]) { Klasse2 k = new Klasse2(); k.meth(); } }
Die abgeleitete Klasse sorgt dafür, dass nicht nur
'Sehr geehrte Damen,' sondern auch noch eine Zeile tiefer 'liebe
Herren' ausgegeben wird.
Hinter super muss immer eine Methode oder ein Attribut stehen!
Konstruktoren
und Vererbung
Konstruktoren werden nicht vererbt. Deswegen muss man bei Bedarf in den
Unterklassen ganz neue Konstruktoren definieren. Wenn ein Objekt einer
Unterklasse erzeugt wird, ruft der Konstruktor der Unterklasse automatisch
den Standard-Konstruktor der Oberklasse auf.
Wie schon soeben beim Überschreiben der Methoden, kommt auch jetzt
wieder das Schlüsselwort super zum Einsatz. Diesmal jedoch
gelten andere Regel für die Benutzung! Denn nun darf keine Anweisung
vor dem Aufruf des Konstruktors stehen.
Der Konstruktor wird jedoch erst dann interessant, wenn man Parameter
einsetzt; und das sieht so aus:
public class KlasseA { private int x;
public KlasseA(int wert) { x = wert; } }
public class KlasseB extends KlasseA {
public KlasseB(int zahl) { super(zahl);
} }
In KlasseB wird der Konstruktor mit der Parameterliste
(also mit int-Werten) der Oberklasse ausgerufen.
Polymorphie
Polymorphie (= Vielgestaltigkeit) ist eine weitere objektorientierte Methode
von Java. Sie ermöglicht dynamisches Binden, d.h. der Compiler entscheidet
zur Laufzeit dynamisch welche Methode er aufruft.
Das klingt jetzt komisch und bevor ich mit der eigentliche Polymorphie
beginne muss ich noch eine weitere Technik einführen: die Typanpassung.
Instanzen werden ja durch folgende Anweisung erzeugt: Klasse instanz
= new Klasse();
Es ist jedoch auch möglich ein Objekt einer Unterklasse einem Objekt
der Oberklasse zuzuweisen. Das sieht dann so aus:
Erbe instanz1 = new Erbe();
Ahn instanz2 = instanz1;
Man erzeugt also zuerst das Objekt instanz1 - das auf herkömmlichen Wege erstellt wurde - danach erzeugt man
eine Referenz instanz2 der Klasse Ahn und lassen diese
auf das Objekt instanz1 zeigen. Da das Erbe-Objekt ein
spezialisiertes Ahn-Objekt ist, funktioniert diese Zuweisung. Das erscheint
auf den ersten Blick blödsinnig! Jedoch übernimmt instanz2 alle Attribute und Methoden die Ober- und Unterklasse gemeinsam haben.
Alle Attribute und Methoden die nach der Vererbung in die Unterklasse Erbe eingefügt wurden, werden also nicht in instanz2 übernommen.
Achtung! Es lässt sich immer nur eine Referenz von
Oberklasse auf die Unterklasse legen. Es ist also nicht möglich eine
Referenz vom Typ Erbe auf Ahn zu legen (folgende Anweisung ist also nicht
möglich: Erbe instant1 = instanz2;). Hier gilt, dass die Typen unvereinbar
sind.
Das war die Vorarbeit. Nun geht es an die eigentliche Polymorphie. Wir
haben soeben definiert, dass bei der Typanpassung in das Oberklasseobjekt
alle Attribute und Methoden übernommen werden, die Ober- und Unterklasse
gemeinsam haben. Was aber ist, wenn alle Klassen die gleichen Attribute
und Methoden besitzen und in den Unterklassen nur die Methoden überschrieben
werden?
Was also macht man bei folgender Codierung:
Ahn a1 = new Erbe1(); Ahn a2 = new Erbe2(); System.out.println(a1.text()); System.out.println(a2.text());
Wird nun in beiden Fällen die Methode text() aus der Ahn-Klasse
aufgerufen? NEIN! Der Compiler registriert zwar dass es sich um die Ahn-Klasse
handelt, er erkennt jedoch auch dass ein Erbe-Objekt erzeugt
wird.
Wie eingangs erwähnt entscheidet sich der Compiler dynamisch welche
Methode aufzurufen ist. Die Polymorphie wurde also eingesetzt.
|