Inhalte basieren auf meinem Buch Java (Alles in einem Band) für Dummies.
- Die Klasse als zentraler Baustein der OOP
- Vererbung, Ableitung, Erweiterung
- Namensräume (package/namespace)
- Sichtbarkeit: Wir haben etwas zu verbergen
- Polymorphie
- Abstrakte Methoden und Klassen
- Interfaces
- Klassenattribute und -methoden
- Singleton
- Die Klasse Object
- Generics
Die Klasse als zentraler Baustein der OOP
- Eine Klasse schafft eigene Datentypen durch Kombination von Typen.
- Eine Klasse fasst Daten (Attribute) und die zugehörigen Funktionen (Methoden) zusammen.
Die Klasse und ihre Attribute
Die Datenanteile einer Klasse nennt man Attribute. Diese Attribute bilden ein Objekt der realen Welt nach.- Ein Datum besteht aus drei ganzzahligen Werten (int), nämlich Tag, Monat und Jahr.
- Ein Auto besteht aus einem Modellnamen (String), Hersteller (vielleicht auch String), Leistung (kW, int), Preis (double) und weiteren Eigenschaften.
Ein Händlerprogramm wird vor allem Einkaufs- und Verkauspreis speichern. Das Programm des Finanzamts errechnet die Steuer aus Hubraum und CO2-Ausstoß. Der Veranstalter von Autorennen interessiert sich für die Höchstgeschwindigkeit. Der Familienvater für die Anzahl der Sitzplätze.
Dasselbe reale Objekt kann in verschiedenen Programmen zu unterschiedlichen Attributen führen.
Die Attribute speichern die Eigenschaften einer Klasse. Man kann auch vom Status sprechen.
Die Kasse und ihre Methoden
Die Klasse wird nicht nur durch ihre Attribute definiert, sondern in besonderem Maß durch ihre Methoden. So bestehen die Koordinaten genauso wie ein Bruch aus zwei Zahlen. Aber für einen Bruch werden alle mathematischen Grundrechenarten sinnvol definiert, während Koordinaten bestenfalls addiert werden können.Beispiele
- Datum
-
Attribute: Ein Datum besteht aus den drei int-Werten für Tag, Monat und Jahr.
Methoden: Eine typische Methode für ein Datum ist die Berechnung des Wochentags.
- Bruchrechnung
-
Attribute: Ein Bruch besteht aus zwei int-Werten je für Zähler und Nenner.
Methoden: Die Klasse Bruch muss Methoden für die Grundrechenarten zur Verfügung stehen, weil Java nicht weiß, wie diese auf Brüche wirken.
- Koordinate
-
Attribute: Eine Koordinate besteht - wie ein Bruch - aus zwei Zahlenwerten.
Methoden: Der entscheidende Unterschied zu Bruch liegt weniger in den gespeicherten Daten als in den Methoden. Während ein Bruch dividiert werden kann, wird bei Koordinaten vielleicht der Winkel und die Entfernung zueinander bestimmt.
Klassendefinition
Die Klasse Bruch enthält einen Zähler und einen Nenner:class Bruch { int zaehler, nenner; }In Java muss jede Klasse in einer eigenen Datei stehen, die den Namen der Klasse trägt. Die Klasse Bruch steht also in einer Datei namens Bruch.java. In anderen Sprachen ist das zwar empfehlenswert, aber nicht zwingend.
Anlegen eines Objekts
public static void main(String[] args) { Bruch meinAnteil; meinAnteil = new Bruch(); meinAnteil.zaehler = 3; meinAnteil.nenner = 4; }
- meinAnteil ist eine nicht initialisierte Referenz!
- Erst new erzeugt ein Objekt!
- Bruch() ruft eine Methode auf, den Konstruktor!
- Der Zugriff auf Objektelemente erfolgt über den Punkt.
Die Methode zur Multiplikation
Da Java keinen Bruch mit * multiplizieren kann, muss die Klasse Bruch eine Multiplikation liefern.Das Bruch-Objekt, über das multipliziert wird, soll sich durch die Multiplikation nicht ändern. (Der Anteil soll immer gleich bleiben, egal welche Beute darüber multipliziert wird.) Also muss das Ergebnis der Multiplikationsmethode zurückgegeben werden.
public class Bruch { int zaehler, nenner; public Bruch mul(int wert) { Bruch ergebnis = new Bruch(); ergebnis.zaehler = this.zaehler * wert; ergebnis.nenner = this.nenner; return ergebnis; } }Wenn aber die Beute ein halbes Schwein ist, muss die Klasse Bruch auch Brüche multiplizieren können.
public class Bruch { int zaehler, nenner; public Bruch mul(int wert) { Bruch ergebnis = new Bruch(); ergebnis.zaehler = this.zaehler * wert; ergebnis.nenner = this.nenner; return ergebnis; } public Bruch mul(Bruch wert) { Bruch ergebnis = new Bruch(); ergebnis.zaehler = this.zaehler * wert.zaehler; ergebnis.nenner = this.nenner * wert.nenner; return ergebnis; } }
Überladen
Es dürfen mehrere Methoden gleichen Namens in einer Klasse existieren, sofern sie sich durch ihre Parameter unterscheiden. Der Rückgabewert ist diesbezüglich kein Unterscheidungsmerkmal!Java-Klassen dürfen keine Operatoren überladen, also nicht den * der Multiplikation oder das + der Addition, weil man den Programmierer vor sich selbst schützen möchte. Andere Sprachen wie C++ trauen ihren Programmierern mehr Verantwortungsgefühl zu.
Konstruktoren
Hinter dem Befehl new steht der Aufruf des Konstruktors.- Der Konstruktor hat den gleichen Namen wie die Klasse.
- Der Konstruktor wird immer beim Erzeugen eines Objekts aufgerufen.
- Ein Konstruktor hat KEINEN Rückgabewert, auch nicht void.
- Ein Konstruktor kann Parameter haben.
- Konstruktoren können überladen werden. Es kann also mehrere Konstruktoren geben, die sich durch ihre Parameter unterscheiden.
Beispiel: Konstruktor für Bruch
Für die Klasse Bruch bietet sich ein Konstruktor mit zwei Parametern an. So wird bei der Erstellung bereits der Wert vorgeben.
public class Bruch { int zaehler, nenner; public Bruch(int zaehler, int nenner) { this.zaehler = zaehler; this.nenner = nenner; } }Beim Erstellen eines Bruches kann der Wert übergeben werden:
Bruch einHalb = new Bruch(1, 2);Danach funktioniert die folgende Zeile nicht mehr. Warum?
Bruch einHalb = new Bruch(); // führt zu einem Fehler
Standard-Konstruktor
Der Standard-Konstruktor ist ein Konstruktor ohne Parameter- Stellt die Klasse keinen Konstruktor zur Verfügung, erstellt Java automatisch einen Standardkonstruktor.
- Gibt es also einen Konstruktor mit Parametern, stellt Java keinen Standard-Konstruktor mehr zur Verfügung.
- Soll es dennoch einen Standard-Konstruktor geben, muss er explizit geschrieben werden.
- Im Beispiel der Klasse Bruch kann es durchaus sinnvoll sein, keinen Standard-Konstruktor zur Verfügung zu stellen, weil es so keinen nicht initialisierten Bruch gibt.
Noch einmal das Anlegen eines Objekts
Kehren wir noch einmal zur Erzeugung eines Objekts zurück und schauen uns die Zeile genauer an:Bruch meinAnteil = new Bruch();
- Der Klassenname Bruch ganz links bestimmt den Typ der Variablen
- Die Referenz meinAnteil verweist auf das Objekt, das durch new erzeugt wird. Im Gegensatz zu den primitiven Typen (int, boolean, char) enthält die Variable nicht den Inhalt, sondern verweist nur darauf. Das wird später noch einmal wichtig.
- Auf das new folgt der Aufruf des Konstruktors, erkennbar an dem Klammernpaar.
Zur Einübung: Aufgaben zur Bruchrechnung
Vererbung, Ableitung, Erweiterung
Alle drei Begriffe stehen für den gleichen Inhalt. Es geht darum, dass Klassen oft gleiche Attribute und Methoden haben, weil sie einander ähnlich sind.In solchen Fällen greift die Regel, dass ein Programm möglichst keinen wiederholenden Code haben sollte, weil ansonsten Fehler mitkopiert werden und sich gleicher Code im Zuge von Korrekturen auseinander entwickelt. In jedem Fall werden Programme mit überflüssigen Codezeilen unübersichtlicher.
Eine abgeleitete bzw. erweiternde Klasse wird in Java mit dem Schlüsselwort extends gekennzeichnet. C++ setzt hinter einem Doppelpunkt den Namen der Basisklasse. Python nennt die Basisklasse in Klammern.
Beispiel: Student und Professor
Beispielsweise hat ein Student und ein Professor eine Telefonnummer, eine Adresse, einen Namen und einen Geburtstag gemeinsam. Darüber hinaus gehören beide einer Hochschule an.Sie unterscheiden sich darn, dass der Student eine Matrikelnummer besitzt, während der Professor einen Lehrstuhl innehat.
Es ist hochgradig ärgerlich, wenn man für die Klasse Student und die Klasse Professor die Gemeinsamkeiten jedes Mal neu implementiert. Die Idee bei der Vererbung ist es, eine Klasse zu schaffen, die die gemeinsamen Eigenschaften realisiert. Die Klassen Student und Professor beziehen sich auf diese Basisklasse und definieren nur noch, was sie von der Basisklasse unterscheidet.
class Hochschulangehoeriger { String name; String telefon; Date geburtsdatum; String hochschule; // ... } class Student extends Hochschulangehoeriger { int martikelnr; } class Professor extends Hochschulangehoeriger { String lehrstuhl; }
Wenn Sie nun ein Objekt der Klasse Student erzeugen, hat dieses neben der Matrikelnummer auch eine Adresse und einen Namen, obwohl das in der Klasse Student nicht explizit aufgeführt ist. Das Objekt enthält die Eigenschaften von Student und alle Eigenschaften der Klasse Hochschulangehoeriger.
Beispiel: Personen
Die Welt besteht aber nicht nur aus Hochschulangehörigen. Es gibt Zahnärzte, Kaufleute, Rentner und Kinder. Diese haben alle einen Namen und Adresse, gehören aber keiner Hochschule an. Also bauen Sie eine neue Klasse Person, die alle Gemeinsamkeiten enthält und leiten davon die Klasse Hochschulangehoeriger ab.class Person { String name; String telefon; Date geburtsdatum; // ... } class Hochschulangehoeriger extends { String hochschule; }Spinnen wir weiter:
- Eine Person hat Name, Adresse, Telefon und evtl. E-Mail-Adresse
- Ein Geschaeftspartner ist eine Person und hat zusätzlich eine Kontoverbindung.
- Eine Kunde ist ein Geschaeftspartner und hat zusätzlich eine Lieferanschrift.
- Ein Lieferant ist ebenfalls ein Geschaeftspartner, der vermutlich ein paar Rechnungen offen hat.
- Auch ein Mitarbeiter ist ein Geschaeftspartner. Er hat zusätzlich eine Krankenkasse und ein Gehalt.
- Ein Aussendienstler ist ein Mitarbeiter der einen eigenen Bezirk hat.
Die Ist-Ein-Beziehung
- A erweitert B
- A IST EIN B (Ein Professor IST EINE Person)
- C ist Attribut von D
- D HAT EIN C (Ein Professor HAT EINEN Lehrstuhl)
In vielen Fällen hat man Wahlmöglichkeiten. Hier ist das gestalterische Geschick und die Übung beim Modellieren gefragt. Beispielsweise:
- Welche Beziehung hat ein Auto zu einem Motor?
- Die Sache scheint klar: Ein Auto HAT EINEN Motor, also besitzt die Klasse Auto ein Attribut Motor.
- Aber: Wenn eine Motorenfabrik neben Motoren auch einen Motor mit Fahrgestell und Karosserie ausliefert, dann ist aus dessen Sicht ein Auto ein Motor mit vier Rädern. Das Auto IST also ein Motor und ist demnach eine Erweiterung von Motor.
Namensräume
Namensräume werden geschaffen, damit in einem Programm zwei Klassen gleichen Namens nicht in Konflikt geraten. Jedes Team bzw. jeder Arbeitsbereich bekommt einen eigenen Namensraum und wird so frei in der Namenswahl.Java Packages
Ein Package ist eine Gliederungsstruktur für Klassen, die oft verwendet wird, um Bibliotheken zusammenzufassen.- Packages fassen Klassen zusammen.
- Packages bieten einen eigenen Namensraum.
- Packages werden in Verzeichnissen realisiert.
package verkauf; // Muss immer der erste Befehl sein! public class Mitarbeiter {Beim Aufruf wird der Package-Name vor die Klasse gestellt, um die Klasse zu identifizieren, die zu einem anderen Namensraum gehört.
verkauf.Mitarbeiter erwin = new verkauf.Mitarbeiter();
import
Überschneidet sich der Klassenname nicht mit anderen verwendeten Klassen, kann ein import die Wiederholung des Package-Namen sparen.import verkauf.Mitarbeiter; ... Mitarbeiter erwin = new Mitarbeiter();
- Packages können wiederum in Packages liegen.
- Im Listing werden die Packages durch Punkte getrennt.
- Im Dateisystem durch verschachtelte Verzeichnisse realisiert
- Weltweite Eindeutigkeit kann durch Voranstellen des umgedrehten Domain-Namen erreicht werden.
Namensräume unter C++
C++ verwendet das Schlüsselwort namespace, um Namensräume zu deklarieren. Mit dem Befehl using namespace kann man die Namen eines fremden Namensraums in den eigenen integrieren, was kein Problem bereitet, so lange sich keine gleichen Namen überdecken.Sichtbarkeit: Wir haben etwas zu verbergen
Für die Sichtbarkeit und den Zugriff werden Attributen, Methoden und Klassen Modifizierer vorangestellt:
- (+) public: zugreifbar für alle Objekte (auch die anderer Klassen),
- (-) private: nur für Objekte der eigenen Klasse (this) zugreifbar,
- (#) protected:
- (~) Ohne Angabe kann bei Java nur ein Objekte des eigenen Packages zugreifen. C++ setzt die Sichtbarkeit auf private, wenn keine andere Sichtbarkeit angegeben wird.
public class Datum { private int tag, monat, jahr, wochentag=-1; public int getWochentag() { if (wochentag<0) { wochentag = berechneWochentag(); } return wochentag; } ... public setTag(int tag) { this.tag = tag; wochentag = -1; } }Der bisher errechnete Wochentag kann so lange geliefert werden, solange weder Tag, noch Monat, noch Jahr verändert wurden. Kontrolliert wird dies dadurch, dass diese Attribute private sind. Nur die set-Methoden (sogenannte Setter) können die Attribute ändern. Geschieht dies, wird wochentag auf -1 gesetzt, wodurch getWochentag bei seinem nächsten Aufruf eine Neuberechnung veranlasst.
Oftmals werden Klassen zur Modellierung und zum Transport von Daten verwendet. Dann werden alle Attribute private definiert und für jedes Attribut eine set- und eine get-Methode erstellt. So können Attribute, die nicht von außen verändert werden sollen, leicht schreibgeschützt werden, indem man keine set-Methode zur Verfügung stellt. Eine solche Klasse wird bei Java Java Beans genannt.
Polymorphie
Die Polymorphie setzt Vererbung voraus. Folgende Effekte werden benutzt:- Ein Zeiger bzw. eine Referenz auf eine Basisklasse wird in einem Array oder in einem Parameter einer Funktion oder Methode verwendet, um ein beliebiges Objekt dieser Klasse oder einer ihrer Erweiterungen zu referenzieren.
- Die erweiternde Klasse überschreibt eine Methode der Basisklasse. Beispielsweise in C++ muss die Fähigkeit zur Polymorphie syntaktisch angekündigt werden, in C++ durch das Schlüsselwort virtual.
- Über den Zeiger der Basisklasse wird nun die besagte Methode aufgerufen.
- Der Effekt der Polymorphie führt dazu, dass Methode der Klasse aufgerufen wird, von der das Objekt tatsächlich angelegt wurde, also nicht zwingend die Methode der Basisklasse.
Die Figur
Betrachten wir eine Figur. Die Basisfigur hat beispielsweise nur die Attribute x und y, die besagen, wo die Figur ihren Ursprung hat.Die Figur hat auch eine Methode zeichne.
Es gibt als Erweiterungen Rechtecke, Dreiecke und Kreise. Als Sonderform der Kreise vielleicht noch Ellipsen und Kreisausschnitte. Alle diese erweiternden Klassen implementieren die Methode zeichne und stellen die Ellipsen, Quadrate und Kreisausschnitte dar.
Wir können eine Methode schreiben, die eine Referenz auf eine Figur als Parameter hat. Dieser Methode können wir einen Kreis übergeben, weil ein Kreis durch die Vererbung eben eine Figur ist. Wenn die Methode nun auf das übergebene Objekt die Methode zeichne anwendet, werden wir erwarten, dass ein Kreis gezeichnet wird. Genau dies erfolgt bei der Polymorphie.
Späte Bindung
Die Entscheidung, die Methode welcher Klasse gewählt wird, fällt zur Laufzeit, weil erst dann entschieden werden kann, welcher Klasse das Objekt, über das die Methode aufgerufen wird, überhaupt ist. Darum spricht man hier auch von später Bindung.Da der Compiler nicht festlegen kann, welche Methode aufgerufen werden soll, muss das Laufzeitsystem diese Information irgendwo finden.
- Java
- Eine Referenz enthält in Java immer zwei Informationen. Einmal die Position des Objekts im Speicher. Dazu kommt der Name der Klasse. Beides kann man sehen, wenn man eine Referenz direkt per System.out.print auf dem Bildschirm ausgibt.
- C++
- In C++ wird diese Information in einer V-Table im Objekt gespeichert. Da das dazu führt, dass das Objekt größer wird als die reine Zusammensetzung der Attribute, muss dies in C++ explizit in der Basisklasse durch das Schlüsselwort virtual angekündigt werden.
Beispiel: Die Mensa der Universität Gintoft
Was heißt das? Ein Beispiel finden Sie in der Mensa der Universität Gintoft. Diese ist sehr klein und hat darum auch einen Aushilfskoch ohne Ambitionen. Er kann eigentlich nur zwei Gerichte: Kartoffelbrei, den er aus einer hellen Pampe anrührt und Suppe, die er aus einem roten Pulver anrührt. Wir sollen eine Speisekarte für die Mensa erstellen. Dazu müssen wir die Mahlzeiten modellieren.
Kartoffelpampe
Fangen wir an mit dem Kartoffelbrei, der dienstags als Kartoffelschnee und donnerstags als Purree de Pomes angeboten werden soll. Zunächst wird die Basisklasse erstellt:class Kitt { private double preis = 0.50; public double kostet() { return preis; } }Wir erweitern zu Kartoffelschnee.
class Schnee extends Kitt { private String name = "Kartoffelschnee"; public String getName() { return name; } }Und dann noch zu Purree de Pommes:
class Purree extends Kitt { private String name = "Purree de Pommes"; public String getName() { return name; } }
Suppe
Als Basisklasse gibt es Suppe:class Suppe { private double preis = 0.70; public double kostet() { return preis; } }Das Pulver ist rot, darum wird es Montags als Tomatensuppe verkauft:
class Tomatensuppe extends Suppe { private String name = "Tomatensuppe"; public String getName() { return name; } }Mittwochs sind noch Reste da, die dann eher braun sind und darum als Gulaschsuppe verkauft werden kann. Und schließlich wird die Brühe am Freitag grün und kann als Erbsensuppe angeboten werden. Sie ist dann auch würziger.
Allerding protestiert der AStA, dass nach EU-Vorschrift eine Erbsensuppe auch vereinzelt Erbsen enthalten muss. Der Koch kippt noch eine Dose Erbsen in den Topf. Das verteuert die Suppe um einen Cent auf 71 Cent.
class Erbsensuppe extends Suppe { private String name = "Erbsensuppe"; public String getName() { return name; } @Override public double kostet() { return 0.71; } }@Override sorgt dafür, dass der Compiler prüft, ob die Basisklasse wirklich eine Methode mit gleichem Namen und Parametern hat.
Der Haken an der Lösung oben ist, dass die Erbsensuppe plötzlich billiger wird als die Tomatensuppe, wenn der Preis der Suppe auf 75 Cent steigt.
class Erbsensuppe extends Suppe { public String getName() { return "Erbsensuppe"; } @Override public double kostet() { return super.kostet() + 0.01; } }
Speiseplanerstellung: Suppe
Wir erstellen einen Speiseplan zunächst nur für Suppen und deren Preis. Als Basis dient ein Array der Basisklasse Suppe Dieses ist zuweisungskompatibel, da Tomaten-, Gulasch- und Erbsensuppe die Basisklasse Suppe erweitern. So kann jede Referenz einem Array-Element vom Typ Suppe zugewiesen werden.class Speiseplan { public static void main(String[] args) { Suppe karte[] = new Suppe[3]; karte[0] = new Tomatensuppe(); karte[1] = new Gulaschsuppe(); karte[2] = new Erbsensuppe(); for (int i=0; i<karte.length; i++) { System.out.println("Kosten: " + karte[i].kostet()); } } }Der Start des Programms ergibt:
- Die ersten beiden Suppen liefern wie erwartet als Preis 70 Cent.
- Die dritte Zahl ist 0.71, also der Preis der Erbsensuppe!
Das referenzierte Objekt behält die Information über seine Klasse auch dann, wenn es über eine Referenz einer Basisklasse zugegriffen wird.
Aber: Der Compiler weiß nicht, dass sich hinter der Suppe eine Erbsensuppe verbirgt. Erst zur Laufzeit wird der Unterschied erkannt. Darum spricht man auch von einer späten Bindung.
Problem mit getName
System.out.print("Name: " + karte[i].getName()); // Compilerfehler!!! System.out.print("Kosten: " + karte[i].kostet());Die Methode getName wird nur in den Erweiterungen von Suppe implementiert. Darum kennt Suppe diese nicht und kann sie auch nicht aufrufen, obwohl jede Erweiterung von Suppe die Methode getName() implementiert.
Lösung: Die Basisklasse implementiert die Methode getName() und alle abgeleiteten Klassen überschreiben sie. Die Klasse Suppe erhält also eine Methode getName, die aber nichts weiter tut.
class Suppe { private preis = 0.70; public double kostet() { return preis; } public String getName() { return null; } }Da die Methode getName in Suppe existiert, kann ein Suppenspeiseplan mit Namen und Preis erstellt werden.
Vollständiger Speiseplan
Der vollständige Speiseplan muss sowohl Suppe als auch Kitt aufnehmen können. Das ist zu lösen, indem eine gemeinsame Basisklasse geschaffen wird, die wir einfach Mahlzeit nennen.
class Mahlzeit { public double kostet() { return 0; } public String getName() { return null; } }Damit Suppe zur Mahlzeit wird, muss sie sie erweitern.
class Suppe extends Mahlzeit { // ... }Nun endlich kann die Mensa der Universität Gintoft ihren wöschentlichen Speiseplan ausgeben und wünscht guten Appetit.
Mahlzeit[] speise = new Mahlzeit[5]; speise[0] = new Tomatensuppe(); speise[1] = new Schnee(); speise[2] = new Gulaschsuppe(); speise[3] = new Purree(); speise[4] = new Erbsensuppe(); for (int i=0; i<speise.length; i++) { System.out.print(speise[i].getName()); System.out.print(": "); System.out.println(speise[i].kostet()); }
Abstrakte Methoden und Klassen
Wenn Sie sich die Klasse Suppe noch einmal ansehen, hat sie nur darum eine Methode getName, damit man die Methode getName der Erweiterungen erreicht. Wie die Objekte der Klasse Suppe heißen, wollen wir gar nicht wissen.Mann könnte null zurückgeben und den Fehler bemerken, wenn irgendjemand den Namen der Suppe erreicht, weil irgendein Dödel vergessen hat, bei der Erweiterung getName zu implementieren.
Eine bessere Lösung ist es, für Suppe eine abstrakte Methode einzurichten. Das bedeutet, dass es die Methode gibt, aber keine Implementierung. Die abstrakte Methode getName besagt also, dass die Klasse Suppe selbst keinen Namen hat, aber eine speziele Suppe immer einen Namen haben muss.
Da die Klasse Suppe die Methode nicht implementiert, kann man auch kein Objekt der Klasse Suppe mehr erzeugen, sondern nur Objekte der Erweiterung von Suppe, die auch getName nun implementieren müssen.
Eine Klasse die mindestens eine abstrakte Methode hat, ist selbst eine abstrakte Klasse. Beides wird mit dem Schlüsselwort abstract deklariert. Eine abstrakte Methode Sie implementiert nicht, sondern deklariert die Schnittstelle.
- Java
-
Java kennzeichnet eine abstrakte Methode mit dem
Schlüsselwort abstract. Statt des Methodenrumpfs hat steht hinter
der abstrakten Methode ein Semikolon.
abstract class Suppe { private double preis = 0.70; public double kostet() { return preis; } abstract public String getName(); }
- C++
-
In C++ wird eine abstrakte Methode eine Null zugewiesen.
Da abstrakte Methoden nur sinnvoll sind, wenn man sie über die Polymorphie
aufrufen kann, benötigt eine abstrakte Methode auch das dafür in C++
verwendete Schlüsselwort virtual.
class Suppe { private: double preis = 0.70; public: double kostet() { return preis; } virtual void std::string getName() = 0; }
Interfaces
Was bei der Suppe so gut geklappt hat, funktioniert bei der Mahlzeit auch.abstract class Mahlzeit { abstract public double kostet(); abstract public String getName(); }Die Klasse Mahlzeit hat nur noch abstrakte Methoden. Das ist auch durchaus passend, eine Mahlzeit weiß weder, wie sie heißt, noch, wie teuer sie ist. Dass eine Mahlzeit abstrakt ist, merkt man spätestens, wenn man im Restaurant eine Mahlzeit bestellt.
Abstrakte Klassen, die nur abstrakte Methoden und keine Attribute enthalten, sind ein Spezialfall, den man in Java Interface nennt.
interface Mahlzeit { public double kostet(); public String getName(); }Da ein Interface nur abstrakte Methoden enthalten darf, werden diese nicht mehr als abstract gekennzeichnet.
Das Interface Mahlzeit gibt die abstrakten Methoden vor. Die abstrakte Klasse Suppe implementiert Mahlzeit.
abstract class Suppe implements Mahlzeit { public double kostet() { return 0.70; } }Erst Tomatensuppe implementiert alle abstrakten Methoden.
class Tomatensuppe extends Suppe { public String getName() { return "Tomatensuppe"; } }Zusammenfassung:
- Ein Interface enthält nur abstrakte Methoden. Darum muss seinen Methoden kein abstract vorangestellt werden.
- Ein Interface hat keine Variablen. static final ist kein Problem.
- Ein Interface wird nicht erweitert (hat ja nix), sondern implementiert.
- Eine Klasse implementiert ein Interface, aber erweitert eine Basisklasse.
- {extends}: Die Basisklasse hat was und kann was, was die Klasse verwenden will.
- {implements}: Die Klasse soll sich nach der Formvorgabe richten, die das Interface aufstellt.
- Eine Klasse kann nur eine Klasse erweitern (extends), aber beliebig viele Interfaces implementieren (implements}.
- Java erlaubt keine Ableitung aus mehreren Klassen, sondern realisiert dies durch die Implementierung von Interfaces. Vorteil: Es kann keine widerstreitenden Implementierungen der Basisklassen geben.
Wochenspeiseplan
Mahlzeit[] speise = new Mahlzeit[5]; speise[0] = new Tomatensuppe(); speise[1] = new Schnee(); speise[2] = new Gulaschsuppe(); speise[3] = new Purree(); speise[4] = new Erbsensuppe(); for (int i=0; i<speise.length; i++) { System.out.print(speise[i].getName()); System.out.print(": "); System.out.println(speise[i].kostet()); }Interfaces werden genutzt, um Implementierungen austauschbar zu machen.
interface Speicher { public void saveKunde(Kunde k); public void saveBestellung(Bestellung b); }Die Klasse SpeicherInDatenbank implementiert das Speichern in einer Datenbank.
class SpeicherInDatenbank implements Speicher { // ... }Der Aufrufer gibt nur an einer Stelle an, welche Implementierung verwendet wird.
Speicher speicher = new SpeicherInDatenbank(); // ... speicher.saveKunde(kunde);
Klassenattribute und -methoden
Ein Attribut gehört zum Objekt. Die Klasse Auto beschreibt beispielsweise das Attribut ps. Jedes Auto hat seine eigene PS-Zahl. Ein R4 hat eine andere PS-Zahl als ein Maserati.class Auto { int ps; }Der Zugriff auf das Attribut ps erfolgt über ein Objekt der Klasse Auto:
Auto r4 = new Auto(); r4.ps = 34;Während ps also eine Objekt-Attribut ist, gibt es auch Klassenattribute. Ein Klassensttribut wird mit dem Schlüsselwort static deklariert und existiert einmalig für alle Objekte der Klasse. Ein Beispiel könnte die Anzahl der Zulassungen sein.
class Auto { static int zulassungen = 0; int ps; public Auto() { zulassungen++; } }Bei jedem Konstruktoraufruf von Auto wird die Anzahl der Zulassungen erhöht. Ihr Wert wird in dem Klassenattribut zulassenungn gespeichert.
Auto r4 = new Auto(); Auto a4 = new Auto(); r4.ps = 34; System.out.println(r4.ps); // Zugriff auf das Objekt-Attribut System.out.println(Auto.zulassungen); // Klassen-Attribut System.out.println(r4.zulassungen); // Auch per Objekt möglich! System.out.println(a4.zulassungen); // dasselbe wie bei r4 System.out.println(Auto.ps); // Das knallt und gibt CompilerfehlerEin mit static gekennzeichnet Attribut existiert genau ein Mal pro Klasse. Dabei ist die Existenz eines Objekts dieser Klasse nicht erforderlich.
Das statische Attribut wird typischerweise über den Klassennamen referenziert, kann aber auch über jedes Objekt der Klasse angesprochen werden.
Klassenmethoden
So wie es Klassenattribute gibt, gibt es auch Klassenmethoden. Auch diese existieren genau einmal für jede Klasse und auch dann, wenn kein Objekt der Klasse existiert. Auch sie werden mit static deklariert.Die bekannteste Klassenmethode ist sicherlich main, die wir schon die ganze Zeit als Startpunkt für unsere Programme verwenden. Da zum Startzeitpunkt des Programms noch kein Objekt existiert, muss die Methode ohne Objekt auskommen und darum ist sie als static deklariert.
public static void main(String[] args) {
Aufgrund ihrer Eigenschaft, dass sie auch ohne Objekt existieren kann, kann eine Klassenmethode nicht auf Objektattribute zugreifen oder Objektmethoden aufrufen, sondern nur auf Klassenattribute oder Klassenmethoden.
- Eine static-Methode existiert und ist aufrufbar, auch wenn noch kein Objekt erzeugt wurde.
- Die Methode main wird als Einsprungpunkt benötigt, bevor das erste Objekt angelegt wurde.
- Pro Klasse darf es nur einen Einsprungpunkt geben (nicht pro Objekt).
Static erzwingt static
- Eine Klassenmethode kann nur Klassenmethoden aufrufen
- Eine Klassenmethode kann nur Klassenvariablen zugreifen
- Objektmethoden können sowohl Klassenmethoden als auch Klassenvariablen verwenden.
Beispiel für Klassenmethoden
Die Klasse Math stellt Methoden zur Verfügung, braucht keine Attribute. Math} enthält mathematische Funktionen, die kein Objekt benötigen.minimum = Math.min(a, b);
- double abs(double a): Absolutbetrag
- double min(doube a, double b): Minimum, auch max
- double sin(double a): Sinus, auch asin, cos, acos, tan, atan
- long round(double a): Ganzzahliger gerundeter Wert von a
- double ceil(double a): Kleinster ganzzahliger Wert, der größer als a ist
- double floor(double a): Größter ganzzahliger Wert, der kleiner als a ist
- double exp(double a): $e^a$
- double log(double a): Natürlicher Logarithmus von a
- double log10(double a): Logarithmus von a zur Basis 10
- double pow(double a, double b): $a^b$
- double sqrt(double a): Quadratwurzel aus a
- double random(): Zufälliger Wert größergleich 0 und kleiner als 1
Klassenkonstanten
Konstanten (also Variablen mit dem Modifizierer final werden typischerweise als Klassenvariablen deklariert, also auch mit einem static versehen.public static final int MINEN = 12;Der Zugriff kann so direkt über den Klassennamen erfolgen:
private Minen[] minen = new Minen[Spiel.MINEN];
Klassenkonstruktor
Der Vollständigkeit halber: Es gibt auch einen Klassenkonstruktor. Er kommt in der Praxis selten vor. Er entsteht durch die Kennzeichnung eines Blocks durch static, kein Typ, kein Name, gefolgt von einem Block.class MeineKlasse { static { // ... } // ... }Ein static-Element wird über den Klassennamen aufgerufen. Da Klassennamen mit einem Großbuchstaben beginnen und Objekte mit Kleinbuchstaben, kann am Aufruf erkannt werden, ob das Element static ist.
Singleton
Soll ein Element exakt einmal vorkommen, gibt es neben static die Möglichkeit, ein Singleton zu verwenden. Ein Singleton ist eine Klasse, von der genau ein Objekt angelegt werden kann. Damit das gewährleistet ist, muss Folgendes umgesetzt werden:- Der Konstruktor darf nicht frei zugreifbar sein, also wird er private.
- Es darf nur eine Instanz der Klasse geben. Also speichert die Klasse sie selbst in einem static-Attribut.
- Damit die Instanz nicht von außen verändert wird, ist sie private.
- Erzeugung und Lieferung der Instanz erfolgt mit einer statischen Methode, die nach Konvention meist getInstance genannt wird.
class Singleton { private Singleton() { } private static Singleton instance = null; static public Singleton getInstance() { if (instance==null) { instance = new Singleton(); } return instance; } }
Verwendung eines Singleton
Singleton obj = Singleton.getInstance();
- Die Singleton-Klasse kann durch beliebige nicht-statische Attribute und Methoden ergänzt werden.
- Diese sind nicht statisch, da sie über das durch getInstance aufgerufene Objekt aufgerufen werden.
- Der erste Aufruf von getInstance erzeugt die Instanz.
class Zulassung { private Zulassung() { } private static Zulassung instance = null; static public Zulassung getInstance() { if (instance==null) { instance = new Zulassung(); } return instance; } private int zulassungen = 0; public void zulassen() { zulassungen++; } }Das Auto lässt sich nun etwas anders zu:
class Auto { int ps; public Auto() { Zulassung zulassung = Zulassung.getInstance() zulassung.zulassen(); } }Oder auch kürzer:
class Auto { int ps; public Auto() { Zulassung.getInstance().zulassen(); } }Natürlich ist es kompakter ein einzelnes statisches Klassenattribut zu verwenden. Wenn allerdings das Einzelobjekt komplexer wird oder von mehreren Klassen zugegriffen wird, ist ein Singleton das Mittel der Wahl.
Die Klasse Object
Die Klasse Object ist die Klasse, von der alle Klassen abgeleitet sind, ob sie es wollen oder nicht. Object ist die Basisklasse aller Klassen.Daraus folgt, dass alle Methoden, die in Object definiert sind, über jedes beliebige Objekt jeder beliebigen Klasse aufgerufen werden können. Das sind u. a. equals, clone, toString, wait und notify.
Man kann in der Referenz auf Object die Referenz auf jedes Objekt speichern, denn: Jede Klasse IST EIN Object.
Ausnahme sind die primitiven Typen. Das ist manchmal hinderlich.
Wrapper-Klassen für primitive Typen
Manchmal wären auch primitve Typen gern Klasse. Dann könnten sie auch eine Referenz haben. Sie würden auch Nachfahren von Object werden.Dazu gibt es Wrapper-Klassen. Java wickelt die primitiven Typen in Klassen ein, die extra für sie geschaffen sind.
- Integer umwickelt int
- Long umwickelt long
- Boolean umwickelt boolean
- Double umwickelt double
- Char umwickelt char
Autoboxing
Die primitiven Typen und ihre Wrapper-Klassen sind zuweisungskompatibel. Dieses Verhalten nennt man Autoboxing.int a = 5; Integer b = a*2; a = b*2;Die Wrapper-Klassen liefern einige Klassenattribute und -methoden (static):
Integer.MAX_VALUE ; // Klassenkonstante: 2147483647 Integer.MIN_VALUE ; // Klassenkonstante: -2147483648 int n=Integer.parseInt(txt1.getText()); // String -> Integer
Noch einmal Object
Da es Wrapper gibt, können Arrays der Klasse Object angelegt werden, in der jeder beliebiger Wert abgelegt werden kann. Das Problem ist nur: Man kann eine Kuh einfüllen, aber einen Hund herausholen.
public static void main(String[] args) { Object[] tiere = new Object[5]; for (int i=0; i<5; i++) { tiere[i] = new Kuh(); } Kuh kuh = (Kuh)(tiere[0]); // hier ist Casting erforderlich Hund hund = (Hund)(tiere[1]); // Das gibt Ärger! hund.machMaennchen(); // Das macht keine Kuh mit }Schwierig wird es auch, wenn man den Hund melken will.
Merke: Casting ist nicht nur im Fernsehen gruselig.
Generics
Problemstellung: Wir erstellen eine Klasse, die ein Paar gleicher Objekte speichern soll.class Paar { EinTyp wert1, wert2; public Paar(EinTyp a1, EinTyp a2) { wert1 = a1; wert2 = a2; } }Wir können statt EinTyp, Integer, String oder ein beliebiges Objekt einsetzen. Wir müssen allerdings alle noch einmal von Hand schreiben. Das ist doof. Lösung: Wir verwenden die Klasse \befehl{Object}. Damit gilt die Klasse für beliebige Paare.
class ObjectPaar { Object obj1, obj2; public ObjectPaar(Object o1, Object o2) { obj1 = o1; obj2 = o2; } }Problem:
ObjectPaar op = new ObjectPaar("1", 2); // erlaubtDie Klasse sollte "typisierbar" sein. Genau das macht Generics Es wird für die Klasse in spitzen Klammern eine Typvariable angegeben, die bei der Instanziierung definiert wird. Die Klasse Paar wird für die Typvariable T definiert:
class PaarBei der Instanziierung der Klasse \ident{Paar} wird der Typ angegeben:{ T obj1, obj2; public Paar(T o1, T o2) { obj1 = o1; obj2 = o2; } }
Paarip = new Paar (1, 2); // erlaubt Paar sp = new Paar ("1", "2"); // erlaubt Paar fp = new Paar ("1", 2); // nicht erlaubt