public class Loewe {
public String name;
public double gewicht;
public String futterzeit;
public String maehnenShampooMarke;
public int anzahlBeine;
public void laufe() {}
public void bruelle(int dezibel) {}
}
Das Konzept der objektorientierten Programmierung lehnt sich an unsere Wahrnehmung der realen Welt an: wir strukturieren die Welt in Objekte und die damit verbundenen Attribute und Fähigkeiten.
So können wir alle Objekte vom Typ Auto als solche erkennen und können einem konkreten Auto sinnvolle Attribute, wie Farbe, Anzahl der Türen, etc. zuweisen. Eine Fähigkeit eines Autos wäre beispielsweise das Fahren oder das Öffnen einer Türe.
Ein weiteres wichtiges Konzept ist die Kapselung: Wenn wir beim Beispiel eines Autos bleiben, so ist es für einen Benutzer nicht wichtig zu wissen, wie der Motor oder das Getriebe funktioniert - es genügt, dem Fahrer ein Lenkrad und drei Pedale zur Verfügung zu stellen. Wie die internen Abläufe im Inneren des Autos hingegen aussehen und zusammenspielen bleibt für den normalen Fahrer unsichtbar. Diese Interna sind somit gekapselt, also versteckt. Nur über freigegebene Steuerungsmöglichkeiten ist eine Einflussnahme auf das Objekt möglich (hier z. B. durch die drei Pedale und das Lenkrad).
In der objektorientierten Programmierung (kurz OOP) wird diese für uns natürliche Strukturierung mit Attributen und Fähigkeiten sowie der Möglichkeit der Kapselung von Informationen zur Verfügung gestellt. Darüber hinaus ist sogar die Vererbung von Eigenschaften und Fähigkeiten möglich, als auch die Bereitstellung von Schnittstellen (Interfaces) zur Zusammenarbeit zwischen verschiedenen Objekten.
Die Grundbegriffe der OOP werden in den folgenden Kapiteln anhand eines Zoo-Großhandels eingeführt. Dieser Großhandel vertreibt die unterschiedlichsten Tiere und führt diese in einer Datenbank.
Als erste gehandelte Tierart betrachten wir den Löwen. Um einen Löwen zu beschreiben, könnte man ihm einen Namen geben, das Gewicht wäre noch interessant, vielleicht seine Futterzeiten und natürlich die Shampoo-Marke, die stets für eine glänzende Mähne im Showroom sorgt.
Um zu wissen, wie viele Pfoten in unserem Großhandel jeden Tag gepflegt werden müssen und um die zugehörigen Arbeiten entsprechend zu planen, soll auch noch die Anzahl der Beine des Löwen gespeichert werden.
Natürlich gibt es noch viele weitere Attribute, wie z. B. das Geburtsdatum, aber wir wollen die Beschreibung auf die oben erwähnten Punkte beschränken.
Damit hätte man einen Löwen schon ganz gut beschrieben, allerdings kann ein Löwe auch noch einiges tun, z. B. brüllen (in einer bestimmten Lautstärke) und laufen.
Man könnte diese Attribute und Fähigkeiten, die zu einem Löwen gehören, schematisch folgendermaßen darstellen, wobei wir bei allen Attributen und Fähigkeiten noch die jeweils sinnvollen Java-Typen dazuschreiben:
Eine solche Darstellung der Klasse Loewe, aus der sich Objekte vom Typ Loewe erstellen lassen, wird auch mit der "Unified Modeling Language" (UML) so visualisiert:
Die Attribute einer Klasse (also beispielsweise futterzeit, name, …) werden in der OOP als Felder bezeichnet, die Funktionen als Methoden. |
Wie sieht nun eine solche Definition der Klasse Loewe in Java aus?
Dazu muss eine Datei namens Loewe.java erstellt werden (der Name der Datei ist durch den Namen der Klasse festgelegt), die den folgenden Inhalt enthält:
public class Loewe { public String name; public double gewicht; public String futterzeit; public String maehnenShampooMarke; public int anzahlBeine; public void laufe() {} public void bruelle(int dezibel) {} }
Java
Wunderbar, nun haben wir also die Klasse Loewe erstellt. Aber wie erstellt man daraus nun ein Objekt vom Typ Loewe, also z. B. den Löwen "Leo"?
Dazu erstellen wir beispielsweise im gleichen Verzeichnis eine weitere Datei namens Zoohandlung.java, die den folgenden Code enthält:
public class Zoohandlung { public static void main(String[] s) { Loewe leo = new Loewe(); (1) leo.name = "Leo, der Chef"; (2) leo.futterzeit = "12:00"; leo.gewicht = 123.41; leo.maehnenShampooMarke = "Leoglanz Power 2"; Loewe leonie = new Loewe(); (3) leonie.name = "Leonie, die Bissige"; (4) leonie.futterzeit = "13:00"; leonie.gewicht = 110.12; leonie.maehnenShampooMarke = "Leoglanz Power 2 for girls"; } }
1 | Hier wird ein neues Objekt vom Typ Loewe erstellt und eine Referenz darauf in der Variablen namens leo gespeichert. new erzeugt aus einer Klasse ein neues Objekt; der Typ des neuen Objekts und somit auch der Variablen leo ist hier Loewe. |
2 | Der Name des in der Variablen leo gespeicherten Löwen wird auf "Leo, der Chef" gesetzt. |
3 | Hier wird wieder ein neues Objekt vom Typ Loewe erstellt, dieses Mal aber in einer Variablen namens leonie gespeichert. |
4 | Hier wird der Name der Löwin festgelegt. |
Unterschied zwischen Klasse und Objekt Die Klasse stellt den Bauplan für ein Objekt dar. Erst durch das Schlüsselwort Aus einer Klasse kann man beliebig viele Objekte erzeugen. |
Unter Punkt 1 wurde erwähnt, dass durch die Zeile Loewe leo = new Loewe(); eine Referenz auf das neu erstellte Objekt vom Typ Loewe in der Variablen |
Loewe loewe = new Loewe(); Loewe loewe2 = loewe; (1) System.out.println(loewe2.anzahlBeine); loewe.anzahlBeine = 4; (2) System.out.println(loewe2.anzahlBeine); (3) loewe2.anzahlBeine = 6; (4) System.out.println(loewe.anzahlBeine); (5) loewe = null; (6) System.out.println(loewe2.anzahlBeine); (7)
1 | In loewe2 wird eine weitere Referenz auf das Objekt gespeichert, auf das bereits loewe verweist. Es wird keine Kopie vom Objekt, auf das loewe verweist, in loewe2 gespeichert, sondern nur ein weiterer Zeiger auf das bereits bestehende Objekt. |
2 | Es wird die Anzahl der Beine des Objekts, auf das loewe verweist, auf 4 gesetzt. |
3 | Da es sich dabei um das selbe Objekt handelt, auf das auch loewe2 verweist, werden auch für loewe2 4 Beine ausgegeben. |
4 | Ebenso kann man die Anzahl der Beine auch über loewe2 verändern, was sich auch bei der Ausgabe von |
5 | der Anzahl der Beine von loewe niederschlägt. |
6 | Hier lässt man loewe auf null verweisen, also auf kein bestehendes Objekt mehr. Dadurch wurde unser Objekt jedoch nicht gelöscht, sondern man kann über |
7 | loewe2 noch darauf zugreifen. |
Die Ausgabe des Programms ist somit:
0 4 6 6
Ein Objekt gilt dann als gelöscht, wenn keine Variable mehr darauf verweist. All diese Objekte, auf die keine Referenz mehr besteht, werden im Hintergrund durch den sogenannten garbage collector (Müllsammler) aus dem Speicher gelöscht, der dadurch neuen Objekten zur Verfügung gestellt werden kann. |
Nachdem man ein Objekt aus einer Klasse erstellt hat, also beispielsweise das Objekt mit dem Bezeichner leo aus der Klasse Loewe, kann man über den Namen des Objekts auf die einzelnen Felder zugreifen, indem man hinter den Namen einen Punkt setzt und danach den entsprechenden Feldnamen.
Auf die gleiche Art und Weise kann man den Löwen brüllen und laufen lassen:
leo.bruelle(60); leo.laufe();
Java
Das funktioniert soweit schon ganz gut, allerdings erscheint es etwas umständlich, die ganzen Informationen zu einem erstellten Löwen zeilenweise zu übergeben. Viel schöner wäre doch die Übergabe der Werte bereits beim Erstellen, also so etwas wie:
Loewe leo = new Loewe("Leo","13:00",123.21,4,"Leo Extra");
Java
Das sollen in der Reihenfolge der Name, die Futterzeit, das Gewicht, die Anzahl der Beine und schließlich der Name des Mähnenshampoos sein.
Und wir haben Glück, genau so etwas gibt es, nämlich die sogenannten Konstruktoren.
Ein Konstruktor ist der Teil der Klasse, der beim Erstellen (englisch: to construct) eines Objekts aufgerufen wird. In Java sieht ein Konstruktor fast aus wie eine normale Methode, nur mit zwei Besonderheiten:
Der Konstruktor muss so heißen wie die Klasse, in unserem Fall also Loewe.
Es wird kein Rückgabewert festgelegt, d. h. es steht nicht mal void
davor, sondern einfach nichts.
Der Konstruktor kann mehrere Argumente entgegennehmen, so dass beim Erstellen die entsprechenden Werte übergeben werden können. Um also einen Löwen mittels
Loewe leo = new Loewe("Leo","13:00",123.21,4,"Leo Extra");
Java
erstellen zu können, brauchen wir einen Konstruktor mit folgender Signatur:
Loewe(String name,String futterzeit,double gewicht,int anzahlBeine,String maehnenShampooMarke)
Java
Damit durch den Konstruktor auch die Felder des Objekts richtig belegt werden, müssen die Werte an sie übergeben werden. Dies ist folgendermaßen möglich:
public Loewe(String name, String futterzeit, double gewicht, int anzahlBeine, String msm){ this.anzahlBeine=anzahlBeine; this.futterzeit=futterzeit; this.gewicht=gewicht; maehnenShampooMarke=msm; this.name=name; }
Java
Hier werden die übergebenen Argumente an den Konstruktor weitergegeben. In diesem Beispiel heißen die meisten Variablennamen der übergebenen Argumente genauso wie die Namen der Felder der Klasse Loewe selbst. So gibt es im Konstruktor ein Argument, das in der Variable name
übergeben wird und nachher im Feld name
gespeichert wird.
Damit sich die beiden Bezeichner mit dem gleichen Namen nicht in die Quere kommen, wird das zur Klasse gehörende Feld über das Schlüsselwort this
eindeutig beschrieben. this
verweist stets auf das Objekt, in dem man sich in dieser Programmzeile gerade befindet. So bewirkt die Zeile
this.anzahlBeine=anzahlBeine;
Java
dass das rechte in der Variablen anzahlBeine
übergebene Argument an das Feld des aktuellen Objekts mit dem gleichen Namen übergeben wird, wobei dieses durch das Voranstellen von this.
eindeutig gekennzeichnet ist.
Natürlich muss man den Namen des Parameters in der Signatur des Konstruktors nicht so wählen, wie der Feldname heißt. Aus Bequemlichkeit macht man das aber häufig.
Im obigen Beispiel findet sich auch die Zeile
maehnenShampooMarke=msm;
Java
Hier benötigt man kein this.
vor dem maehnenShampooMarke
, da hier keine Verwechslung möglich ist. Es ist klar, dass maehnenShampooMarke
das Feld des aktuellen Objekts ist und msn
der übergebene Parameter.
Unsere Zoohandlung muss im Normalfall bei der Aufnahme eines neuen Löwen die Futterzeit, die Anzahl der Beine und die Shampoo-Marke nicht gesondert angeben, da sich diese Eigenschaften üblicherweise nicht ändern. Deshalb wäre es praktisch, einen Konstruktor der Signatur
Loewe(String name, double gewicht)
Java
zur Verfügung zu haben und die anderen Werte mit Standardwerten zu belegen.
Wir werden diesen Konstruktor also auch anlegen, den bisherigen aber lassen. Dadurch gibt es zwei Konstruktoren für Loewe:
Loewe(String name, double gewicht)
Java
Loewe(String name,String futterzeit,double gewicht,int anzahlBeine,String maehnenShampooMarke)
Java
Der Java-Compiler hat damit jedoch kein Problem, da beim Aufrufen der Konstruktoren aufgrund der Typen der übergebenen Argumente klar wird, welcher herangezogen werden muss:
Loewe leo = new Loewe("Leo",123.2); Loewe leonie = new Loewe("Leonie","14:00",121.1,4,"Lioness Gold");
Java
Im ersten Fall wird erst ein String und dann eine double-Zahl übergeben, also kommt nur der Konstruktor Loewe(String name, double gewicht)
dafür in Frage, der zweite Aufruf passt dafür genau auf den anderen Konstruktor.
Würde man zusätzlich noch einen Konstruktor wie
nicht entscheiden kann, welchen Konstruktor er wählen soll:
oder
Beide erwarten an erster Stelle einen String und dann einen double-Wert. Ein solcher Fall darf nicht auftreten! |
Loewe(String futterzeit, double gewicht)
Loewe(String name, double gewicht)
Loewe leo = new Loewe("Leo",123.2);
Man bezeichnet die Möglichkeit, mehrere Konstruktoren gleichen Namens - jedoch mit unterschiedlichen Typen - verwenden zu können, als Überladen von Konstruktoren.
Eine Implementierung der beiden Konstruktoren könnte folgendermaßen aussehen:
public Loewe(String name, String futterzeit, double gewicht, int anzahlBeine, String msm) { this.anzahlBeine = anzahlBeine; this.futterzeit = futterzeit; this.gewicht = gewicht; maehnenShampooMarke = msm; this.name = name; } public Loewe(String name, double gewicht) { this.gewicht = gewicht; this.name = name; }
Java
Für die restlichen Werte (Gewicht, Futterzeit und Anzahl der Beine) müssen dazu Standardwerte in der Klasse festgelegt werden, also z. B.
public String name; public double gewicht; public String futterzeit = "13:00"; public String maehnenShampooMarke = "LeoGlanz Power 2"; public int anzahlBeine = 4;
Java
Übrigens gibt es noch eine letzte Möglichkeit, die Erstellung der Konstruktoren zu optimieren. Im letzten Beispiel weisen beide Konstruktoren den Feldern gewicht
und name
ihre Werte zu. Insofern liegt hier doppelter Code vor, der zweimal das Gleiche macht.
Eine Möglichkeit, dies zu umgehen, wäre die folgende Vorgehensweise beim zweiten Konstruktor:
public Loewe(String name, String futterzeit, double gewicht, int anzahlBeine, String msm) { this(name,gewicht); this.futterzeit = futterzeit; this.anzahlBeine = anzahlBeine; maehnenShampooMarke = msm; }
Java
Mit this(name,gewicht)
wird der Konstruktor Loewe(String name, double gewicht)
aufgerufen.
Das funktioniert allerdings nur, wenn dies in der ersten Zeile eines Konstruktors geschieht. Sonst darf ein Konstruktor keinen anderen Konstruktor aufrufen.
Ebenso wie Konstruktoren lassen sich auch Methoden überladen: neben
public void bruelle(int dezibel)
könnte also beispielsweise problemlos noch
public void bruelle(int dezibel, int dauer)
definiert werden.
Auch hier ist zu beachten, dass die Signaturen nicht identisch sein dürfen:
public void bruelle(int dezibel)
und public void bruelle(int dauer)
haben aus Sicht des Compilers identische Signaturen, da sie beide gleich heißen und jeweils genau einen Parameter vom Typ int
erwarten. Das würde somit zu einem Compiler-Fehler führen.
Angenommen, wir geben unser bisheriges Programm an eine andere Zoohandlung weiter. Diese ist begeistert und fängt sofort an, damit zu arbeiten.
Bei der ersten Benutzung schleicht sich leider ein Fehler ein, da ein Löwe fälschlicherweise folgendermaßen erstellt wird:
Loewe leo = new Loewe(); leo.anzahlBeine = -4;
Java
Für die Anzahl der Beine wird also eine negative Zahl eingegeben. Unser Programm lässt das bisher zu, aber natürlich ergibt das keinen Sinn. Sollte der Fehler unbemerkt bleiben und die Fußpediküre für alle Tiere eingeteilt werden, so kann es hier zu Problemen im weiteren Ablauf in der anderen Zoohandlung kommen.
Erkenntnis:
Es ist nicht sinnvoll, dass andere Benutzer unseres Programms Felder von Objekten direkt ändern können. Wenn wir die korrekte Funktionsweise unseres Programms gewährleisten wollen, müssen wir verhindern, dass solche direkten Abänderungen vorgenommen werden können.
Ähnlich verhält es sich heutzutage bei vielen Motoren von Fahrzeugen etablierter Automobilhersteller. Anstatt beim Öffnen der Motorhaube Zugriff auf den gesamten Motor zu erhalten, befindet sich dort häufig ein versiegelter Motorblock mit einem Diagnoseanschluss. Nur der Ölstand und das Kühlwasser lassen sich noch regeln. Ähnlich sollte auch unsere Klasse in der Lage sein, interne Felder vor äußerem Zugriff zu Schützen, diese Informationen von der Außenwelt also zu kapseln. |
Die Sichtbarkeit von Feldern und Methoden lassen sich über die Sichtbarkeitsmodifikatoren public
, private
und protected
steuern, wobei wir zunächst die beiden zuerst genannten betrachten:
public
bedeutet, dass auf diese Felder auch von außerhalb der Klasse zugegriffen werden kann, so wie es in den bisherigen Beispielen der Fall war. Mit Hilfe des Schlüsselworts private
unterbinden wir den Zugriff von außen.
So ändern wir
public class Loewe { public String name; public double gewicht; public String futterzeit = "13:00"; public String maehnenShampooMarke = "LeoGlanz Power 2"; public int anzahlBeine = 4; .... }
Java
ab zu
public class Loewe { private String name; private double gewicht; private String futterzeit = "13:00"; private String maehnenShampooMarke = "LeoGlanz Power 2"; private int anzahlBeine = 4; .... }
Java
und schon ergibt
Loewe leo = new Loewe("Leo",123.3); leo.anzahlBeine = -4;
Java
eine Fehlermeldung, da auf das Feld anzahlBeine
nicht von außerhalb der Klasse Loewe zugegriffen werden kann (die obigen Zeilen befinden sich in der Klasse Zoohandlung, also in einer anderen Klasse als die Klasse Loewe).
Somit ist unser Feld vor unsachgemäßem Gebrauch hiermit geschützt.
Aber bedeutet das, dass man die Anzahl der Beine nach einer Verletzung des Löwen nicht mehr ändern kann?
Doch! Hierfür legt man beispielsweise gesonderte Getter und Setter fest.
Bei Gettern und Settern handelt es sich um Methoden, die einen kontrollierten Zugriff auf private Felder erlauben. So könnte die Anzahl der Beine durch die folgenden Methoden gesetzt (Setter) und abgefragt werden (Getter):
private int anzahlBeine = 4; public void setAnzahlBeine(int anzahl) { if (anzahl >= 0) { anzahlBeine = anzahl; } } public int getAnzahlBeine() { return anzahlBeine; }
Java
Hier sieht man, wie man den Zugriff auf ein privates Feld über öffentliche (public
) Getter und Setter steuern kann. In diesem Fall wird beim Setzen der Beinanzahl überprüft, ob die übergebene Anzahl größer gleich 0 ist und diese nur dann im Feld gespeichert.
Natürlich könnte man nun auch Getter und Setter für die anderen Felder definieren, wir sparen uns das aber der Übersichtlichkeit halber.
Drückt man in Netbeans in einer Klasse bei gedrückter Steuerungstaste die Leertaste, so schlägt Netbeans automatisch die fehlenden Getter und Setter vor und erstellt die notwendigen Codezeilen bei entsprechender Auswahl. Sehr praktisch! |
Grundansatz zur Kapselung:
Alle Felder einer Klasse sollten, sofern nicht gute Gründe dagegen sprechen, von ihrer Sichtbarkeit her auf private
gesetzt werden und nur über öffentliche Methoden (z. B. Getter und / oder Setter) verändert oder ausgelesen werden.
Private Methoden Auch Methoden können mit dem Sichtbarkeitsmodifikator Dazu muss statt |
In UML wird der Sichtbarkeitsmodifikator public
mit einem vorangestellten +
dargestellt, private
hingegen mit einem -
.
Hier das UML-Diagramm zum bisherigen Löwen:
Um das Arbeiten mit Klassen und Objekten in Entwicklungsumgebungen komfortabler zu gestalten, sollte man seinen Code auch stets kommentieren, insbesondere Methoden. Der Vorteil besteht darin, dass in Entwicklungsumgebungen diese Kommentare direkt in der Vorschau zum Befehl erscheinen und dem Programmierer eine Idee vermitteln, was diese Methode eigentlich macht.
Das könnte beispielsweise so aussehen:
Um diese Ausgabe zu erhalten, muss direkt über der Methode in der Klasse die entsprechende Beschreibung Eingang finden. Das würde in diesem Fall für die Methode bruelle(int dezibel)
so aussehen:
/** * Lässt den Löwen brüllen, wobei die Lautstärke über dezibel gesteuert werden kann. * @param dezibel Lautstärke in Dezibel */ public void bruelle(int dezibel) { ... }
Java
Netbeans hilft uns beim Erstellungsprozess von Kommentaren: Sobald man direkt über einer Methode |
Da die Namen der Parameter in die Dokumentation einfließen, sollten sie sprechende Namen sein: also im obigen Beispiel |
Mehr Informationen zur Kommentierung von Klassen und deren Felder und Methoden findet man beispielsweise unter http://scalingbits.com/java/javakurs1/begleitend/javadoc.
Loewe.java:
package zoohandlung; public class Loewe { public String name; public double gewicht; public String futterzeit = "13:00"; public String maehnenShampooMarke = "LeoGlanz Power 2"; public int anzahlBeine = 4; public void laufe() { System.out.println("Ich laufe jetzt."); } public void bruelle(int dezibel) { System.out.println("Brüll"); if (dezibel > 70) { System.out.println("BRÜLL!"); } } public Loewe() { } public Loewe(String name, double gewicht) { this.name = name; this.gewicht = gewicht; } public Loewe(String name, String futterzeit, double gewicht, int anzahlBeine, String msm) { this.name = name; this.futterzeit = futterzeit; this.gewicht = gewicht; this.anzahlBeine = anzahlBeine; maehnenShampooMarke = msm; } public void setAnzahlBeine(int anzahl) { if (anzahl >= 0) { anzahlBeine = anzahl; } } public int getAnzahlBeine() { return anzahlBeine; } }
Java
Zoohandlung.java:
package zoohandlung; public class Zoohandlung { public static void main(String[] s) { Loewe leo = new Loewe(); leo.name = "Leo, der Chef"; leo.futterzeit = "12:00"; leo.gewicht = 123.41; leo.maehnenShampooMarke = "Leoglanz Power 2"; leo.bruelle(60); leo.laufe(); Loewe leonie = new Loewe(); leonie.name = "Leonie, die Bissige"; leonie.futterzeit = "13:00"; leonie.gewicht = 110.12; leonie.maehnenShampooMarke = "Leoglanz Power 2 for girls"; Loewe max=new Loewe("Max",132.32); max.bruelle(80); max.laufe(); } }
Java
Wahlweise kann auch das zugehörige Netbeans-Projekt heruntergeladen werden:
Dateiname |
Zoohandlung.zip |
|
Größe in Bytes |
18396 |
|
Einfügezeitpunkt |
12.1.2018, 22:38:52 |