Kapitel 9: Objektorientierte Programmierung
Objektorientierte Programmierung (OOP) steht vor allem für einen Wechsel der Perspektive, aus welcher wir die Dinge betrachten, die wir implementieren wollen. Bis hierhin haben wir Daten und darauf operierende Methoden weitestgehend isoliert betrachtet. So haben wir beispielsweise Temperaturen und Einkommen auf ein und demselben Datentypen abgebildet. Für beide, Temperaturen und Einkommen, haben wir dann jeweils eine Methode zu deren jeweiliger Klassifikation geschrieben. – Diese Sicht- und Vorgehensweise entspricht dem sogenannten Paradigma der Prozeduralen Programmierung.
Die OOP begründet ein neues Paradigma, welches auf der Prozeduralen Programmierung aufsetzt. Sie betrachtet die zu implementierenden Strukturen jeweils als ein Ganzes. Daten und darauf operierende Methoden bilden eine Einheit, sogenannte Objekte. Daher rührt die Bezeichnung Objektorientierte Programmierung.
Klassen beschreiben die Konstruktion der Objekte, welche Daten sie beinhalten, welche Methoden auf diesen Daten operieren. Eine Klasse beschreibt den „Bauplan" eines Objekts. Ein konkretes Objekt ist eine Instanz einer bestimmten Klasse. Die Begriffe Objekt und Instanz werden im Folgenden weitgehend synonym verwendet.
Beispiel:
Sie schreiben ein Programm zur Verwaltung von Studenten und Mitarbeitern an Ihrer Universität. Ihnen allen ist gemeinsam, Personen zu sein. Sie haben einen Vornamen, einen Nachnamen, ein Geburtsdatum und andere Dinge mehr. Ein Student ist eine Person. Neben den genannten Attributen (Datenfeldern) besitzt er aber auch eine Matrikelnummer. Ein universitärer Mitarbeiter hat eine Personalnummer. Je nach dem, ob er in der Verwaltung oder der Lehre tätig ist, gehört er einem Dezernat oder einem Lehrstuhl an.
Das sind eine Menge verschiedener Einzelinformationen, wenn Sie das skizzierte Beispiel weiter denken. Statt nun die Einzelinformationen in einem Programm einfach in Form primitiver Datentypen zu speichern und zu verarbeiten, ist es die Idee der OOP, Datentypen für jeden betrachteten Objekttyp (Personen, Studenten usw.) zu definieren. Diese verwalten die ihnen zugehörigen Daten selbst.
Eine Person wird als Datentyp mit den ihr eigenen Attributen beschrieben. Dazu kommen, direkt zur Person gehörige Methoden, die auf diesen Datenfeldern operieren. Selbiges gilt für Studenten usw.
Jeder dieser identifizierten Datentypen stellt in der OOP eine Klasse dar. Die beschreibt, welche Datenfelder erfasst werden und wie diese manipuliert werden. Analog gilt das auch für alle anderen identifizierten Klassen.
Ist nun Susanne Müller eine Studentin, dann handelt es sich bei ihr um eine konkrete Ausprägung der Klasse Student. Das bedeutet, auf Basis des Bauplans für einen Studenten, der Klasse Student, wird eine Instanz (Objekt) mit den Attributen, die Susanne Müller beschreiben, erzeugt. Das ist das Datenobjekt, mit dem unser Programm dann arbeitet, in welchem Susanne Müller verwaltet wird.
Das Beispiel stellt zwischen Personen, Mitarbeitern und Studenten eine Beziehung her. Mitarbeiter sind Personen. Studenten sind Personen. Offensichtlich sind Mitarbeiter und Studenten Spezialisierungen von Personen.
Das bringt uns zu einer weiteren Grundidee der OOP: Die Modellierung eines Ausschnitts der realen Welt geschieht anhand einer Klassenhierarchie. D. h., gleichartige Objekte werden zu Klassen zusammengefasst, von denen durch Vererbung Spezialisierungen abgeleitet werden. Gleichartigkeit bedeutet die Übereinstimmung von objektbezogenen Datenfeldern und objektbezogenen Methoden. Eine abgeleitete Klasse erbt von der Oberklasse die dort definierten Datenfelder und Methoden, fügt gegebenenfalls eigene hinzu und kann ihnen durch Überschreiben eine neue Bedeutung geben.
Um nun Objekte zu instanziieren, reicht es nicht mehr aus, einfach eine Wertzuweisung vorzunehmen, wie beispielsweise i = 42, wenn verb|i| eine Variable vom Typ int ist.
Objekte werden durch Konstruktoren erzeugt. Jede Klasse besitzt einen oder auch mehrere Konstruktoren, die für das Instanziieren ihrer Objekte zuständig sind. Wird kein eigener Konstruktor definiert, existiert automatisch der vom System bereitgestellte Standard-Konstruktor (ohne Parameter). Ein Konstruktor wird beim Erzeugen eines neuen Objekts automatisch ausgeführt. Sinn und Zweck eines Konstruktors ist es, die Datenfelder eines Objekts so zu initialisieren, dass das Objekt in einem konsistenten Zustand ist. Beispielsweise darf ein Datumsobjekt nicht das Datum „29.02.1900" repräsentieren. Dieses Datum gab es nicht, weil das Jahr 1900 kein Schaltjahr war.
Sichtbarkeit von Datenfeldern und Methoden
Datenfelder, die mit dem Schlüsselwort static deklariert werden, heißen Klassenvariable. Sie existieren pro Klasse genau einmal (unabhängig von der Anzahl kreierter Instanzen) und alle Objekte dieser Klasse können auf sie zugreifen.
Ein Datenfeld, welches ohne das Schlüsselwort static deklariert wird, ist eine sogenannte Instanzvariable. Eine Instanzvariable existiert je Instanz (also je Objekt) genau einmal und kann entsprechend für jede Instanz einen anderen Wert annehmen. Eine Instanzvariable stellt eine Eigenschaft eines jeweiligen Objektes dar.
Hinweis: Variablen, die innerhalb einer Methode deklariert werden, bezeichnet man als lokale Variablen.
Für Methoden gilt das oben Gesagte ganz analog. D. h., wir unterscheiden Klassenmethoden und Instanzmethoden. Klassenmethoden können nicht auf Instanzmethoden oder Instanzvariablen zugreifen.
Die Sichtbarkeit von (Instanz- und Klassen-) Variablen und Methoden wird mit Hilfe von Modifizierern geregelt. Ist ein Element einer Klasse mit keinem der Schlüsselworte public, private oder protected deklariert, dann ist es nur innerhalb von Klassen desselben Pakets sichtbar. Der Sinn und Zweck der hierdurch etablierten Schutzmechanismen ist es, die Datenkonsistenz eines Objekts, auch als Invariante bezeichnet, zu gewährleisten. Zudem gilt: Der Benutzer einer Klasse, eines Objekts, braucht nicht zu wissen, wie es intern implementiert ist. Es ist ausreichend zu wissen, wie es genutzt werden kann. Die Nutzbarkeit wird durch die Attribute und Methoden definiert, die für den Benutzer sichtbar sind.
Unter einem Paket versteht man alle Klassen, die in einem bestimmten Verzeichnis liegen. Sinnvollerweise werden logisch zueinander gehörige Klassen in ein Verzeichnis gepackt. Diese Verzeichnisse werden wiederum logisch zusammengehörig in weitere Verzeichnisse geschachtelt, so dass eine ganze Verzeichnisstruktur entsteht. Nach Java-Konvention sollten die Paketnamen (und damit die Verzeichnisse) immer klein geschrieben sein.
Das Standardpaket besteht aus allen Klassen im aktuellen Arbeitsverzeichnis. Eigene Pakete können beispielsweise angelegt werden, indem man
package a.b.c;
am Anfang einer Klasse schreibt und diese Klasse auch in dem angegebenen Pfad a/b/c ablegt. Eine so definierte Klasse kann nach dem Übersetzen durch
$ java a.b.c.MeineKlasse
aufgerufen werden, wobei der Aufrufer sich in dem Verzeichnis befindet, welches das Verzeichnis a enthält.
Die Klasse MeineKlasse kann von anderen Klassen durch
import a.b.c.MeineKlasse;
importiert werden.
Die folgende Tabelle zeigt Umstände, unter denen Klassenelemente der vier Sichtbarkeitstypen für verschiedene Klassen erreichbar sind.
Tabelle 9.1 Sichtbarkeitsmodifizierer
| erreichbar für | public |
protected |
Paket | private |
|---|---|---|---|---|
| dieselbe Klasse | ja | ja | ja | ja |
| andere Klasse im selben Paket | ja | ja | ja | nein |
| Subklasse in anderem Paket | ja | ja | nein | nein |
| keine Subklasse, anderes Paket | ja | nein | nein | nein |
- Elemente des Typs
publicsind in der Klasse selbst, in Methoden abgeleiteter Klassen und für den Aufrufer von Instanzen der Klasse sichtbar. - Elemente des Typs
protectedsind in der Klasse selbst und in Methoden abgeleiteter Klassen sichtbar. Der Aufrufer einer Instanz der Klasse hat nur Zugriff, wenn er in demselben Paket definiert wurde. - Elemente vom Typ
privatesind nur in der Klasse selbst sichtbar. - Elemente ohne Modifizierer gelten als friendly und werden als
protectedeingestuft mit der Einschränkung, dass sie in Unterklassen anderer Pakete unsichtbar sind.
Anmerkung: Die Paketsichtbarkeit wird oftmals ebenso als friendly bezeichnet.
Erste Beispiele
Ich möchte eingeschränkt das Beispiel aus dem vorangegangenen Abschnitt aufgreifen. Wir wollen im Folgenden Personen (Person) und Studenten (Student) modellieren und implementieren. Unter anderem sei beiden ein Geburtsdatum gemeinsam.
Die Klasse Date
Ein Geburtsdatum ist ein Datum, bezogen auf eine bestimmte Person, ein ebenso bestimmter Datumswert. Es gibt weitere Datumswerte, wie ein Einstellungsdatum, vielleicht ein Hochzeitsdatum usw. All diesen verschiedenen Datumswerten ist gemeinsam, dass sie ein Datum darstellen. Und ein Datum besteht aus den Datenfeldern Tag, Monat und Jahr. Das sind jeweils ganze Zahlen, die jedoch nicht willkürlich sind. Die Tageszahl eines Datums liegt im Intervall $[1, 31]$. Dabei hat jedoch nicht jeder Monat $31$ Tage. Es kommt auf den konkreten Monat an, wie viele Tage er jeweils hat. Auch das Jahr ist für die zulässige Tageszahl ausschlaggebend. In Schaltjahren hat der Februar $29$ ansonsten $28$ Tage. Schaltjahre wurden mit dem Julianischen Kalender 45 v. Chr. eingeführt. Aufgrund einer Ungenauigkeit wurde dieser 1582 durch den Gregorianischen Kalender ersetzt. Der definiert die Schaltjahresregeln so, wie wir sie heute kennen und anwenden. – Wir bemerken: Ein Datumswert ist nur dann valide, wenn die einzelnen Attributwerte bestimmten Regeln genügen. Ein Datum muss zu Beginn seiner ersten Notation, im Sinne der OOP bei seiner Instanziierung, valide sein und es auch im Laufe seines Lebenszyklus bleiben. Ändert sich ein Datum, muss es nach der Änderung immer noch den für Datumswerte geltenden Regeln entsprechen. – In der Fachterminologie sprechen wir bezüglich dieser Regeln von der sogenannten Invariante. Im ersten Schritt werden wir noch keine Methoden implementieren, die einen Datumswert modifizieren. Später ergänzen wir solche Methoden, weshalb wir bereits jetzt darauf achten, die Regeln so zu implementieren, sie später weiter verwenden zu können.
Wir entwickeln die Klasse Date in mehreren aufeinander aufbauenden Schritten.
Schritt 1
Objekte nutzen zur Instanziierung den Bauplan, der durch die Klasse beschrieben wird. Der definiert die Attribute und Methoden. Attribute beschreiben den Zustand eines Objekts, Methoden beschreiben sein Verhalten.
Der Zustand eines Objekts muss stets valide sein. Was ein valider Zustand ist, bestimmt die Invariante. Das sind die Regeln, die definieren, welche Zustände erlaubt sind.
Ein Objekt wird durch einen Konstruktor erzeugt. Dieser hat sicherzustellen, dass die Attribute eines instanziierten Objekts, der Invariante entsprechend, initialisiert werden.
Im konkreten Fall bedeutet das, dass wir das Folgende zu prüfen haben:
- Um Schaltjahre korrekt behandeln zu können, so wie wir das im Alltag gewohnt sind, kann unsere Klasse
Datenur Datumswerte ab dem Jahr 1582 repräsentieren. - Ein Jahr hat maximal 12 Monate.
- Die minimale Tagesangabe eines Datumswertes ist 1. Die Monate Januar bis Dezember haben die folgenden maximal zulässigen Tageszahlen: 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31. Der Februar hat in Schaltjahren nicht 28 Tage, er hat dann 29 Tage.
- Schaltjahre: Ein Schaltjahr tritt im gregorianischen Kalender generell alle vier Jahre auf, um die Zeitrechnung an das Sonnenjahr anzupassen. Die Regeln lauten: Ein Jahr ist ein Schaltjahr, wenn es durch 4 teilbar ist. Ist es jedoch durch 100 teilbar, ist es kein Schaltjahr, es sei denn, es ist gleichzeitig durch 400 teilbar (z. B. 2000).
Zur Implementierung der Klassen dieses Seminarabschnitts erstellen wir ein Paket namens oop. Das ist ein Verzeichnis mit eben diesem Namen im aktuellen Projektverzeichnis. Hier legen wir die Klasse Date, und später auch Person und Student, ab.
1package oop;
2
3/**
4 * Die Klasse {@code Date} implementiert ein Datum, welches dem
5 * Gregorianischen Kalender entspricht.
6 */
7public class Date {
8 // konstante Klassenvariable: Basis-Jahr des Gregorianischen
9 // Kalenders
10 private static final int BASEYEAR = 1582;
11
12 // Instanzvariablen:
13 private int day, month, year;
14
15 /**
16 * Konstruktor für ein neues Datumsobjekt.
17 *
18 * @param day Tag
19 * @param month Monat
20 * @param year Jahr
21 * @throws IllegalArgumentException unzulässige Datumsangabe
22 */
23 public Date(int day, int month, int year) {
24 int y = checkYear(year);
25 int m = checkMonth(month);
26 int d = checkDay(day, m, y);
27 this.year = y;
28 this.month = m;
29 this.day = d;
30 }
31
32 /*
33 * interne Methoden, zur Prüfung und Sicherstellung
34 * der Invariante (Datumsregeln).
35 */
36
37 /**
38 * Prüfung, ob ein übergebenes Jahr ein Schaltjahr ist.
39 *
40 * @param year zu prüfendes Jahr
41 * @return {@code true}, wenn Schaltjahr, ansonsten {@code false}
42 */
43 private static boolean isLeapYear(int year) {
44 boolean is_leap = (year % 4 == 0);
45 is_leap = is_leap && (year % 100 != 0);
46 is_leap = is_leap || (year % 400 == 0);
47 return is_leap;
48 }
49
50
51 /**
52 * @param year zu prüfende Jahresangabe
53 * @throws IllegalArgumentException unzulässige Jahresangabe
54 * @return validiertes Jahr
55 */
56 private static int checkYear(int year) {
57 if (year < BASEYEAR)
58 throw new IllegalArgumentException("Jahre vor "+ BASEYEAR +
59 " sind unzulässig");
60 return year;
61 }
62
63
64 /**
65 * Prüfung, ob Monat zulässig.
66 *
67 * @param month Monat
68 * @throws IllegalArgumentException unzulässige Monatsangabe
69 * @return Monat
70 */
71 private static int checkMonth(int month) {
72 if ((month < 1) || (month > 12))
73 throw new IllegalArgumentException("Ungültiger Monat");
74 return month;
75 }
76
77
78 /**
79 * Prüfung, ob Tag zulässig.
80 *
81 * @param day Tag
82 * @param month Monat
83 * @param year Jahr
84 * @throws IllegalArgumentException ungültige Datumsangabe
85 * @return Tag
86 */
87 private static int checkDay(int day, int month, int year) {
88 final int[] DAYS = {31, 28, 31, 30, 31, 30,
89 31, 31, 30, 31, 30, 31};
90
91 int days = DAYS[month-1]; // regulär zulässige Anzahl Tage im
92 // Monat
93 if (month == 2 && isLeapYear(year))
94 days = days + 1; // eventuelle Schaltjahreskorrektur
95
96 if (day < 1 || day > days)
97 throw new IllegalArgumentException("Ungültiger Tag");
98 return day;
99 }
100
101
102 /**
103 * Rückgabe einer formatierten Zeichenkettendarstellung des aktuellen
104 * Datumswertes.
105 *
106 * @return Datum als Zeichenkette
107 */
108 public String get() {
109 String dateString = String.format("%02d.%02d.%d",
110 day,
111 month,
112 year);
113 return dateString;
114 }
115}
Listing 9.1: oop.Date.java (Version 1)
Erläuterungen
- Die Klasse
Dateistpublic, also für jeden sicht- und nutzbar. - Die Variable
BASEYEARistprivate, um sie generell vor Modifikationen Dritter zu schützen. Sie istfinal, um auch versehentlich interne Modifikationen zu unterbinden.BASEYEARist konstant. Die Variable ist mittelsstaticals Klassenvariable gekennzeichnet. Unabhängig von einer konkreten Instanz bildet das Jahr 1582 die Basis des Gregorianischen Kalenders. - Die Instanzvariablen, individuell von
Date-Instanzen mit Werten belegt, sindprivate, um sie vor unkontrollierten Modifikationen zu schützen. Nur Instanzen der KlasseDatedürfen das, weil nur hier die Invariante, die Validitätsregeln, bekannt sind und deren Einhaltung überwacht werden kann. - Ein Konstruktor hat in Java keinen Rückgabewert. Sein Bezeichner ist zwingend identisch mit dem Bezeichner der Klasse.
- Der Konstruktor erwartet drei Parameter: Tag, Monat und Jahr.
- Instanzvariablen werden, wenn deren Bezeichner mit dem Bezeichner einer lokalen Variablen oder dem eines Parameters kollidieren, mit
thisqualifiziert. - Die Methoden zur Sicherstellung der Invariante (
check*()) sindprivate. Nutzer der Klasse brauchen sie nicht, um die Klasse zu verwenden. - Aufgrund der Abhängigkeiten ist die Reihenfolge der Prüfungen stets: Jahr, Monat, Tag.
- Die Methoden zur Prüfung wurden so gestaltet, den jeweils zu validierenden Wert als Rückgabewert zurückzuliefern. Begründung: Wir können die Klasse später leichter erweitern, um beispielsweise Datumswerte validiert ändern zu können.
- Ein Datumsobjekt kann generiert werden und es kann, in Form eines formatierten Strings, über seinen aktuellen Status informieren.
get()implementiert das Verhalten, über sich selbst informieren zu können.
Testclient
Den Testclient, ein kurzes Programm, welches die Klasse Date testet, implementieren wir im regulären Projektverzeichnis. Wir nennen das Programm, im vorauseilenden Gehorsam, OOPTest.java. Wir werden es im Laufe dieses Seminarabschnitts mehrfach modifizieren. – Nutzen Sie es für eigene Experimente.
1import de.pakad.udemy.StdOut;
2import oop.Date;
3
4public class OOPTest {
5 public static void main(String[] args) {
6 Date d1 = new Date(1, 1, 1900); // neue Date-Instanz
7
8 StdOut.println(d1.get());
9 }
10}
Experimentieren Sie:
- Erzeugen Sie weitere Datumsinstanzen.
- Versuchen Sie ungültige Datumswerte zu erzeugen.
- Versuchen Sie auf die privaten Attribute einer Datumsinstanz zuzugreifen, z. B.
d1.day. Es wird Ihnen gewollt nicht gelingen. - Versuchen Sie eine Datumsinstanz auszugeben, ohne dabei die
get()-Methode aufzurufen.
Schritt 2
Wenn Sie meinen Anregungen gefolgt sind, eigene Experimente mit Date auszuführen, werden Sie auch versucht haben, eine Date-Instanz ohne den Methodenaufruf get() auszugeben. Sie haben solche eine Ausgabe erhalten:
oop.Date@14dad5dc
Das ist die in Java vorkonfigurierte String-Darstellung einer Date-Instanz. Java spendiert jedem Objekt eine String-Darstellung. Sie besteht in der Regel aus dem Klassennamen, hier oop.Date, weil die Klasse im Paket oop definiert ist. Dem folgt, hinter dem @-Zeichen ein Identifizierer, der das Objekt eindeutig kennzeichnet.
In der Regel wünscht man sich eine für den Menschen aussagekräftigere Darstellung, eine Darstellung, die den Erwartungen gerecht wird. Deshalb haben wir die Methode get() geschrieben.
Allerdings verleitet uns die obige Ausgabe dazu, darüber nachzudenken, wie Java die Standarddarstellung erzeugt hat. Jedes Objekt besitzt in Java, auch wenn wir sie nicht explizit formulieren, eine Methode namens toString(). Diese wird regelmäßig seitens Java aufgerufen, wenn ein Objekt in Form eines Strings dargestellt werden soll. So rufen alle Ausgaberoutinen, wie print(), println() oder \verbprintf()|, diese Methode automatisch auf, wenn sie ein bloßes Objekt zur Ausgabe übergeben bekommen.
Wir können unsere Klasse diesem Java-Standard einfach anpassen. Alles, was wir von einer Zeichenkettendarstellung erwarten, haben wir in der Methode get() bereits implementiert. Wir brauchen Sie lediglich umzubenennen. Zum guten Stil gehört es zudem, die eigene Implementierung als solche zu kennzeichnen. Dazu dient die Annotation @Override. Sie wird direkt vor dem Methodenkopf notiert. Dies vorausgeschickt, können wir get() ändern. Es wird das Folgende daraus:
/**
* Rückgabe einer formatierten Zeichenkettendarstellung des aktuellen
* Datumswertes.
*
* @return Datum als Zeichenkette
*/
@Override
public String toString() {
String dateString = String.format("%02d.%02d.%d", day, month, year);
return dateString;
}
Ändern Sie Ihren Code und versuchen Sie nochmals eine Datumsinstanz direkt auszugeben.
Date d1 = new Date(1, 1, 1900); // neue Date-Instanz
StdOut.println(d1);
Jetzt wird, wenn Sie den Testclient ausführen,
01.01.1900
angezeigt. - Kleine Änderung, große Wirkung. Die Klasse ist nun bezüglich der Ausgabe Java-konform und hat an Komfort gewonnen.
Es ist jedoch noch deutlich unkomfortabel, einzelne Datumskomponenten, wie Tag, Monat und Jahr, abzufragen. Dafür definieren wir sogenannte Getter-Methoden. Sie heißen so, weil ihre Bezeichner in der Regel mit einem get beginnen.
/**
* Jahr zurückgeben.
*
* @return Jahr
*/
public int getYear() {
return year;
}
/**
* Monat zurückgeben.
*
* @return Monat
*/
public int getMonth() {
return month;
}
/**
* Tag zurückgeben.
*
* @return Tag
*/
public int getDay() {
return day;
}
Diese Methoden liefern jeweils das private Instanzattribut zurück. Da hier keine Verwechselungen möglich sind, können wir auf eine Qualifikation mit this verzichten. In der vorigen Version von Date konnten wir nicht auf diese Attribute zugreifen, weil Sie private deklariert sind. Das können wir auch jetzt nicht. Die Getter-Methoden gewähren uns jedoch einen mittelbaren, nur lesenden Zugriff, auf diese Attribute.
Insgesamt stellt sich Date nach diesen Modifikationen bzw. Erweiterungen, wie folgt dar.
1package oop;
2
3/**
4 * Die Klasse {@code Date} implementiert ein Datum, welches dem
5 * Gregorianischen Kalender entspricht.
6 */
7public class Date {
8
9 // konstante Klassenvariable: Basis-Jahr des Gregorianischen
10 // Kalenders
11 private static final int BASEYEAR = 1582;
12
13 // Instanzvariablen:
14 private int day, month, year;
15
16 /**
17 * Konstruktor für ein neues Datumsobjekt.
18 *
19 * @param day Tag
20 * @param month Monat
21 * @param year Jahr
22 * @throws IllegalArgumentException unzulässige Datumsangabe
23 */
24 public Date(int day, int month, int year) {
25 int y = checkYear(year);
26 int m = checkMonth(month);
27 int d = checkDay(day, m, y);
28 this.year = y;
29 this.month = m;
30 this.day = d;
31 }
32
33 /*
34 * interne Methoden, zur Prüfung und Sicherstellung
35 * der Invariante (Datumsregeln).
36 */
37
38 /**
39 * Prüfung, ob ein übergebenes Jahr ein Schaltjahr ist.
40 *
41 * @param year zu prüfendes Jahr
42 * @return {@code true}, wenn Schaltjahr, ansonsten {@code false}
43 */
44 private static boolean isLeapYear(int year) {
45 boolean is_leap = (year % 4 == 0);
46 is_leap = is_leap && (year % 100 != 0);
47 is_leap = is_leap || (year % 400 == 0);
48 return is_leap;
49 }
50
51
52 /**
53 * @param year zu prüfende Jahresangabe
54 * @throws IllegalArgumentException unzulässige Jahresangabe
55 * @return validiertes Jahr
56 */
57 private static int checkYear(int year) {
58 if (year < BASEYEAR)
59 throw new IllegalArgumentException("Jahre vor "+ BASEYEAR +
60 " sind unzulässig");
61 return year;
62 }
63
64
65 /**
66 * Prüfung, ob Monat zulässig.
67 *
68 * @param month Monat
69 * @throws IllegalArgumentException unzulässige Monatsangabe
70 * @return Monat
71 */
72 private static int checkMonth(int month) {
73 if ((month < 1) || (month > 12))
74 throw new IllegalArgumentException("Ungültiger Monat");
75 return month;
76 }
77
78
79 /**
80 * Prüfung, ob Tag zulässig.
81 *
82 * @param day Tag
83 * @param month Monat
84 * @param year Jahr
85 * @throws IllegalArgumentException ungültige Datumsangabe
86 * @return Tag
87 */
88 private static int checkDay(int day, int month, int year) {
89 final int[] DAYS = {31, 28, 31, 30, 31, 30,
90 31, 31, 30, 31, 30, 31};
91 int days = DAYS[month-1]; // regulär zulässige Anzahl Tage im
92 // Monat
93 if (month == 2 && isLeapYear(year))
94 days = days + 1; // eventuelle Schaltjahreskorrektur
95
96 if (day < 1 || day > days)
97 throw new IllegalArgumentException("Ungültiger Tag");
98 return day;
99 }
100
101
102 /**
103 * Rückgabe einer formatierten Zeichenkettendarstellung des aktuellen
104 * Datumswertes.
105 *
106 * @return Datum als Zeichenkette
107 */
108 @Override
109 public String toString() {
110 String dateString = String.format("%02d.%02d.%d", day,
111 month,
112 year);
113 return dateString;
114 }
115
116 /*
117 * Getter-Methoden
118 */
119
120 /**
121 * Jahr zurückgeben.
122 *
123 * @return Jahr
124 */
125 public int getYear() {
126 return year;
127 }
128
129
130 /**
131 * Monat zurückgeben.
132 *
133 * @return Monat
134 */
135 public int getMonth() {
136 return month;
137 }
138
139
140 /**
141 * Tag zurückgeben.
142 *
143 * @return Tag
144 */
145 public int getDay() {
146 return day;
147 }
148}
Listing 9.2: oop.Date.java (Version 2)
Schritt 3
Instanzen unserer bisherigen Date-Klasse können erzeugt, abgefragt und ausgegeben werden. Ein bestehendes Datum können wir aber nicht ändern. Für viele Anwendungen ist das auch nicht zwingend notwendig. Hat man beispielsweise ein falsches Geburtsdatum erfasst kann man dieses durch eine neue Instanz mit den richtigen Datumsangaben ersetzen.
Date dob = new Date(23, 9, 2001);
...
// wir bemerken das falsche Geburtsdatum, wir korrigieren es
dob = new Date(24, 9, 2001)
...
Sehr häufig möchte man jedoch keine neue Datumsinstanz erzeugen, der Wunsch ist es, ein bestehendes Datum zu modifizieren. Dazu dienen sogenannte Setter-Methoden. Bei diesen ist es extrem wichtig, die etablierte Invariante nicht zu verletzen. Bei der Instanziierung haben wir dafür Sorge getragen, nur valide Datumswerte zu akzeptieren. Das muss natürlich auch bei den Setter-Methoden gewährleistet sein.
/**
* Neues Jahr setzen.
*
* @throws IllegalArgumentException ungültige Datumsangabe
* @param year Jahr
*/
public void setYear(int year) throws IllegalArgumentException {
// neues Jahr prüfen
int tmpYear = checkYear(year);
// wenn neues Jahr ok, Verträglichkeit Tag-Monat-Jahr prüfen
checkDay(day, month, tmpYear );
// Kombination Tag-Monat-Jahr ist korrekt, andernfalls
// wäre eine Ausnahme geworfen worden -> wir können übernehmen
this.year = tmpYear;
}
/**
* Neuen Monat setzen.
*
* @throws IllegalArgumentException ungültige Datumsangabe
* @param month Monat
*/
public void setMonth(int month) throws IllegalArgumentException {
// neuen Monat prüfen
int tmpMonth = checkMonth(month);
// wenn ok, Verträglichkeit Tag-Monat-Jahr prüfen
checkDay(day, tmpMonth, year);
// Kombination Tag-Monat-Jahr ist korrekt, andernfalls
// wäre eine Ausnahme geworfen worden -> wir können übernehmen
this.month = tmpMonth;
}
/**
* Neuen Tag setzen.
*
* @throws IllegalArgumentException ungültige Datumsangabe
* @param day Tag
*/
public void setDay(int day) throws IllegalArgumentException {
// prüfen, ob Tag (auch in Kombination mit Monat und Jahr)
// korrekt - wenn ja, zuweisen
this.day = checkDay(day, month, year);
}
Vorausschauend haben wir unsere check*()-Methoden so gestaltet, sie an dieser Stelle wiederverwenden zu können. Die Prüfung der Invariante ist zentral im Code der Klasse Date abgebildet.
Die Setter-Methoden sind, wie die Getter-Methoden, öffentliche Instanzmethoden. Über sie kann der Zustand einer Datumsinstanz modifiziert werden.
Bereits in der Vergangenheit haben Sie erfahren, das Java im Fehlerfall Ausnahmen wirft. Sie haben ebenso an den Kommentaren sowie im Code von Date entsprechende throw-Anweisungen bemerkt. Solange Sie als Programmierer allein verantwortlich sind, ein Objekt zu instanziieren, seinen Zustand zu modifizieren, können Sie falsche Wertbelegungen wirksam verhindern. Was ist jedoch, wenn Sie in einem Programm die Klasse Date verwenden und ein Programmanwender gibt interaktiv ungültige Datumswerte ein? – Die check*()-Methoden verhindern wirksam Werteingaben, die zu ungültigen Datumswerten führen. Das Programm wird seitens Java gewaltsam abgebrochen. Das ist eine sehr rigorose Maßnahme.
Sie können einen Programmabbruch verhindern, wenn Sie eine geworfene Ausnahme auffangen und behandeln. Nicht alle Ausnahmen können erfolgreich behandelt, geheilt, werden. Eine falsche Datumsangabe kann jedoch geheilt werden. Wir müssen lediglich den Programmanwender auffordern, seine Eingabe zu korrigieren.
In Java „probiert" man etwas, was eine Ausnahme werfen könnte, aus - try. Wird eine Ausnahme geworfen, fängt man diese auf (catch) und behandelt sie. – Eine Ausnahme vom Typ IllegalArgumentException ist eine spezielle Form der RuntimeException. Diese Art von Ausnahme ist in Java unchecked. D. h., es ist dem Programmierer freigestellt, ob er kritische Anweisungen in einem try-catch-Block mit eventueller Behandlung ausführt.
Ich möchte Ihnen dafür ein einfaches Beispiel an die Hand geben, falsche Datumseingaben aufzufangen und geeignet zu behandeln.
public static void main(String[] args) {
Date d = new Date(1, 1, 2000);
boolean error = false;
do {
StdOut.printf("Aktueller Wert: %s\n", d);
try{
d.setYear(StdIn.readlnInt("Neues Jahr: "));
d.setMonth(StdIn.readlnInt("Neuer Monat: "));
d.setDay(StdIn.readlnInt("Neuer Tag: "));
// wenn wir hier ankommen, ist das neue Datum korrekt
error = false;
} catch (IllegalArgumentException e) {
StdOut.println(e);
error = true;
}
} while (error);
}
Hier präsentiere ich Ihnen den Code der Klasse Date nochmal als Ganzes.
1package oop;
2
3/**
4 * Die Klasse {@code Date} implementiert ein Datum, welches dem
5 * Gregorianischen Kalender entspricht.
6 */
7public class Date {
8 // konstante Klassenvariable: Basis-Jahr des Gregorianischen
9 // Kalenders
10 private static final int BASEYEAR = 1582;
11
12 // Instanzvariablen:
13 private int day, month, year;
14
15 /**
16 * Konstruktor für ein neues Datumsobjekt.
17 *
18 * @param day Tag
19 * @param month Monat
20 * @param year Jahr
21 * @throws IllegalArgumentException unzulässige Datumsangabe
22 */
23 public Date(int day, int month, int year) {
24 int y = checkYear(year);
25 int m = checkMonth(month);
26 int d = checkDay(day, m, y);
27 this.year = y;
28 this.month = m;
29 this.day = d;
30 }
31
32 /*
33 * interne Methoden zur Prüfung und Sicherstellung
34 * der Invariante (Datumsregeln).
35 */
36
37 /**
38 * Prüfung, ob ein übergebenes Jahr ein Schaltjahr ist.
39 *
40 * @param year zu prüfendes Jahr
41 * @return {@code true}, wenn Schaltjahr, ansonsten {@code false}
42 */
43 private static boolean isLeapYear(int year) {
44 boolean is_leap = (year % 4 == 0);
45 is_leap = is_leap && (year % 100 != 0);
46 is_leap = is_leap || (year % 400 == 0);
47 return is_leap;
48 }
49
50
51 /**
52 * @param year zu prüfende Jahresangabe
53 * @throws IllegalArgumentException unzulässige Jahresangabe
54 * @return validiertes Jahr
55 */
56 private static int checkYear(int year) {
57 if (year < BASEYEAR)
58 throw new IllegalArgumentException("Jahre vor "+ BASEYEAR +
59 " sind unzulässig");
60 return year;
61 }
62
63
64 /**
65 * Prüfung, ob Monat zulässig.
66 *
67 * @param month Monat
68 * @throws IllegalArgumentException unzulässige Monatsangabe
69 * @return Monat
70 */
71 private static int checkMonth(int month) {
72 if ((month < 1) || (month > 12))
73 throw new IllegalArgumentException("Ungültiger Monat");
74 return month;
75 }
76
77
78 /**
79 * Prüfung, ob Tag zulässig.
80 *
81 * @param day Tag
82 * @param month Monat
83 * @param year Jahr
84 * @throws IllegalArgumentException ungültige Datumsangabe
85 * @return Tag
86 */
87 private static int checkDay(int day, int month, int year) {
88 final int[] DAYS = {31, 28, 31, 30, 31, 30,
89 31, 31, 30, 31, 30, 31};
90
91 int days = DAYS[month-1]; // regulär zulässige Anzahl Tage im
92 // Monat
93 if (month == 2 && isLeapYear(year))
94 days = days + 1; // eventuelle Schaltjahreskorrektur
95
96 if (day < 1 || day > days)
97 throw new IllegalArgumentException("Ungültiger Tag");
98 return day;
99 }
100
101
102 /**
103 * Rückgabe einer formatierten Zeichenkettendarstellung des aktuellen
104 * Datumswertes.
105 *
106 * @return Datum als Zeichenkette
107 */
108 @Override
109 public String toString() {
110 String dateString = String.format("%02d.%02d.%d",
111 day,
112 month,
113 year);
114 return dateString;
115 }
116
117 /*
118 * Getter-Methoden
119 */
120
121 /**
122 * Jahr zurückgeben.
123 *
124 * @return Jahr
125 */
126 public int getYear() {
127 return year;
128 }
129
130
131 /**
132 * Monat zurückgeben.
133 *
134 * @return Monat
135 */
136 public int getMonth() {
137 return month;
138 }
139
140
141 /**
142 * Tag zurückgeben.
143 *
144 * @return Tag
145 */
146 public int getDay() {
147 return day;
148 }
149
150 /*
151 * Setter-Methoden
152 */
153
154 /**
155 * Neues Jahr setzen.
156 *
157 * @throws IllegalArgumentException ungültige Datumsangabe
158 * @param year Jahr
159 */
160 public void setYear(int year) throws IllegalArgumentException {
161 // neues Jahr prüfen
162 int tmpYear = checkYear(year);
163 // wenn neues Jahr ok, Verträglichkeit Tag-Monat-Jahr prüfen
164 checkDay(day, month, tmpYear );
165 // Kombination Tag-Monat-Jahr ist korrekt, andernfalls
166 // wäre eine Ausnahme geworfen worden -> wir können übernehmen
167 this.year = tmpYear;
168 }
169
170
171 /**
172 * Neuen Monat setzen.
173 *
174 * @throws IllegalArgumentException ungültige Datumsangabe
175 * @param month Monat
176 */
177 public void setMonth(int month) throws IllegalArgumentException {
178 // neuen Monat prüfen
179 int tmpMonth = checkMonth(month);
180 // wenn ok, Verträglichkeit Tag-Monat-Jahr prüfen
181 checkDay(day, tmpMonth, year);
182 // Kombination Tag-Monat-Jahr ist korrekt, andernfalls
183 // wäre eine Ausnahme geworfen worden -> wir können übernehmen
184 this.month = tmpMonth;
185 }
186
187
188 /**
189 * Neuen Tag setzen.
190 *
191 * @throws IllegalArgumentException ungültige Datumsangabe
192 * @param day Tag
193 */
194 public void setDay(int day) throws IllegalArgumentException {
195 // prüfen, ob Tag (auch in Kombination mit Monat und Jahr)
196 // korrekt - wenn ja, zuweisen
197 this.day = checkDay(day, month, year);
198 }
199}
Listing 9.3: oop.Date.java (Version 3)
Schritt 4
In diesem Schritt wollen wir unsere Date-Klasse „rund" machen und abschließen.
Von den primitiven Datentypen sind Ihnen Zuweisungen der folgenden Gestalt bekannt:
int a = 42;
int b = a;
a = 13;
StdOut.println("a: " + a); // gibt 13 aus
StdOut.println("b: " + b); // gibt 42 aus
Versuchen wir etwas Analoges in unserem Testprogramm OOPTest mit Datumsobjekten:
Date d1 = new Date(1, 1, 2000);
Date d2 = d1;
d2.setYear(1900);
StdOut.println("d1: " + d1); // gibt 01.01.1900 aus
StdOut.println("d2: " + d2); // dto.
Augenscheinlich bewirkt eine an d2 vorgenommene Änderung auch eine Modifikation bei d1. Das wirkt im ersten Moment seltsam, ist jedoch einfach zu erklären. In Java sind alle Datentypen, die nicht primitiv sind, sogenannte Referenzdatentypen. Ich bin bereits bei der Behandlung von Arrays und deren Übergabe an Methodenparameter darauf eingegangen. Das dort Gesagte gilt auch hier. Unsere Date-Klasse ist, weil nicht primitiv, ein Referenzdatentyp.
Bei Referenzdatentypen wird in einer Variablen, beispielsweise d1, nicht das Datumsobjekt direkt gespeichert. In d1 wird ein Verweis, eine Referenz, auf das Objekt abgelegt.
Weisen wir nun den in d1 gespeicherten Wert einer Variablen d2 zu, so erhält d2 ebenfalls eine Referenz auf das Datumsobjekt von d1. Faktisch wird ein und dieselbe Datumsinstanz nun von zwei Variablen referenziert. Es verwundert also nicht, dass ein modifizierender Zugriff über d2 die gleiche Wirkung hat, wie ein entsprechender Zugriff über d1.
Man sagt auch: d2 ist ein Alias in Bezug auf d1 (und umgekehrt).
In der Regel möchte man, wenn man eine Zuweisung tätigt, zwei unabhängige Datumsinstanzen gleichen Werts haben. Das geht auch, jedoch nicht mit einer einfachen Zuweisung. Die Lösung besteht darin, ein neues Datumsobjekt (mit den Werten von d1) zu erzeugen und dieses an d2 zuzuweisen.
Wir erzeugen ein wertgleiches Objekt mithilfe eines sogenannten Kopierkonstruktors. Wie bereits eingangs erläutert, darf in Java eine Klasse mehrere Konstruktoren definieren.
Der Kopierkonstruktor gehorcht (wie jeder Konstruktor) auf den Namen der Klasse. Er benötigt als Parameter eine Instanz des zu kopierenden Objekts.
* Kopierkonstruktor - erzeugt aus der übergebenen Instanz
* eine neue und unabhängige Datumsinstanz
* @param date Datum
*/
public Date(Date date) {
year = date.year; // Übernahme der Werte
month = date.month; // aus der übergebenen Instanz
day = date.day;
}
Auf eine Validierung der Datumskomponenten darf der Kopierkonstruktor verzichten. Es gibt, dank der von uns implementierten Invariante und den Schutzmaßnahmen, nicht unkontrolliert auf die Attribute eines Datums zugreifen zu können, keine Datumsobjekte, die nicht valide wären.
Nachdem Sie den Kopierkonstruktor implementiert und die Klasse Date neu übersetzt haben, ändern Sie den Testcode in OOPTest minimal ab.
Date d1 = new Date(1, 1, 2000);
Date d2 = new Date(d1);
d2.setYear(1900);
StdOut.println("d1: " + d1); // gibt 01.01.2000 aus
StdOut.println("d2: " + d2); // gibt 01.01.1900 aus
Die Klasse Date ist damit fertiggestellt. Wir können Sie in unseren kommenden Programmen, in denen wir tiefer in die OOP eintauchen, verwenden. Abschließend möchte ich zur Klasse Date noch das Folgende bemerken:
Dateimplementiert einen eigenen in sich geschlossenen Datentyp mit einem genau definierten Wertebereich.- Regeln (Invariante) gewährleisten, dass eine Datumsinstanz über ihren gesamten Lebenszyklus valide Datumswerte repräsentiert.
- Durch geeignete Schutzmaßnahmen sind die Attribute der Klasse vor unberechtigten und unkontrollierten Zugriffen geschützt.
- Es sind lediglich die Methoden für Dritte sicht- und nutzbar, die für die Nutzung erforderlich sind. Dritte sehen nur das, was sie benötigen. Wie die Klasse intern arbeitet, bleibt verborgen. Das nennen wir auch Geheimnisprinzip oder *(*Information Hiding).
Hier ist nochmals der komplette Code der Klasse Date.java abgedruckt, um ihn für Sie leichter nachvollziehbar zu machen.
1package oop;
2
3/**
4 * Die Klasse {@code Date} implementiert ein Datum, welches dem
5 * Gregorianischen Kalender entspricht.
6 */
7public class Date {
8
9 // konstante Klassenvariable: Basis-Jahr des Gregorianischen
10 // Kalenders
11 private static final int BASEYEAR = 1582;
12
13 // Instanzvariablen:
14 private int day, month, year;
15
16 /**
17 * Konstruktor für ein neues Datumsobjekt.
18 *
19 * @param day Tag
20 * @param month Monat
21 * @param year Jahr
22 * @throws IllegalArgumentException unzulässige Datumsangabe
23 */
24 public Date(int day, int month, int year) {
25 int y = checkYear(year);
26 int m = checkMonth(month);
27 int d = checkDay(day, m, y);
28 this.year = y;
29 this.month = m;
30 this.day = d;
31 }
32
33
34 /**
35 * Kopierkonstruktor - erzeugt aus der übergebenen Instanz
36 * eine neue und unabhängige Datumsinstanz
37 * @param date Datum
38 * @throws IllegalArgumentException wenn Datum gleich {@code null}
39 */
40 public Date(Date date) {
41 if (date == null)
42 throw new IllegalArgumentException(
43 "Es darf nicht null übergeben werden.");
44 year = date.year;
45 month = date.month;
46 day = date.day;
47 }
48
49 /*
50 * interne Methoden zur Prüfung und Sicherstellung
51 * der Invariante (Datumsregeln).
52 */
53
54 /**
55 * Prüfung, ob ein übergebenes Jahr ein Schaltjahr ist.
56 *
57 * @param year zu prüfendes Jahr
58 * @return {@code true}, wenn Schaltjahr, ansonsten {@code false}
59 */
60 private static boolean isLeapYear(int year) {
61 boolean is_leap = (year % 4 == 0);
62 is_leap = is_leap && (year % 100 != 0);
63 is_leap = is_leap || (year % 400 == 0);
64 return is_leap;
65 }
66
67
68 /**
69 * @param year zu prüfende Jahresangabe
70 * @throws IllegalArgumentException unzulässige Jahresangabe
71 * @return validiertes Jahr
72 */
73 private static int checkYear(int year) {
74 if (year < BASEYEAR)
75 throw new IllegalArgumentException("Jahre vor "+ BASEYEAR +
76 " sind unzulässig");
77 return year;
78 }
79
80
81 /**
82 * Prüfung, ob Monat zulässig.
83 *
84 * @param month Monat
85 * @throws IllegalArgumentException unzulässige Monatsangabe
86 * @return Monat
87 */
88 private static int checkMonth(int month) {
89 if ((month < 1) || (month > 12))
90 throw new IllegalArgumentException("Ungültiger Monat");
91 return month;
92 }
93
94
95 /**
96 * Prüfung, ob Tag zulässig.
97 *
98 * @param day Tag
99 * @param month Monat
100 * @param year Jahr
101 * @throws IllegalArgumentException ungültige Datumsangabe
102 * @return Tag
103 */
104 private static int checkDay(int day, int month, int year) {
105 final int[] DAYS = {31, 28, 31, 30, 31, 30,
106 31, 31, 30, 31, 30, 31};
107 int days = DAYS[month-1]; // regulär zulässige Anzahl Tage im
108 // Monat
109 if (month == 2 && isLeapYear(year))
110 days = days + 1; // eventuelle Schaltjahreskorrektur
111
112 if (day < 1 || day > days)
113 throw new IllegalArgumentException("Ungültiger Tag");
114 return day;
115 }
116
117
118 /**
119 * Rückgabe einer formatierten Zeichenkettendarstellung des aktuellen
120 * Datumswertes.
121 *
122 * @return Datum als Zeichenkette
123 */
124 @Override
125 public String toString() {
126 String dateString = String.format("%02d.%02d.%d",
127 day,
128 month,
129 year);
130 return dateString;
131 }
132
133 /*
134 * Getter-Methoden
135 */
136
137 /**
138 * Jahr zurückgeben.
139 *
140 * @return Jahr
141 */
142 public int getYear() {
143 return year;
144 }
145
146
147 /**
148 * Monat zurückgeben.
149 *
150 * @return Monat
151 */
152 public int getMonth() {
153 return month;
154 }
155
156
157 /**
158 * Tag zurückgeben.
159 *
160 * @return Tag
161 */
162 public int getDay() {
163 return day;
164 }
165
166 /*
167 * Setter-Methoden
168 */
169
170 /**
171 * Neues Jahr setzen.
172 *
173 * @throws IllegalArgumentException ungültige Datumsangabe
174 * @param year Jahr
175 */
176 public void setYear(int year) throws IllegalArgumentException {
177 // neues Jahr prüfen
178 int tmpYear = checkYear(year);
179 // wenn neues Jahr ok, Verträglichkeit Tag-Monat-Jahr prüfen
180 checkDay(day, month, tmpYear );
181 // Kombination Tag-Monat-Jahr ist korrekt, andernfalls
182 // wäre eine Ausnahme geworfen worden -> wir können übernehmen
183 this.year = tmpYear;
184 }
185
186
187 /**
188 * Neuen Monat setzen.
189 *
190 * @throws IllegalArgumentException ungültige Datumsangabe
191 * @param month Monat
192 */
193 public void setMonth(int month) throws IllegalArgumentException {
194 // neuen Monat prüfen
195 int tmpMonth = checkMonth(month);
196 // wenn ok, Verträglichkeit Tag-Monat-Jahr prüfen
197 checkDay(day, tmpMonth, year);
198 // Kombination Tag-Monat-Jahr ist korrekt, andernfalls
199 // wäre eine Ausnahme geworfen worden -> wir können übernehmen
200 this.month = tmpMonth;
201 }
202
203
204 /**
205 * Neuen Tag setzen.
206 *
207 * @throws IllegalArgumentException ungültige Datumsangabe
208 * @param day Tag
209 */
210 public void setDay(int day) throws IllegalArgumentException {
211 // prüfen, ob Tag (auch in Kombination mit Monat und Jahr)
212 // korrekt - wenn ja, zuweisen
213 this.day = checkDay(day, month, year);
214 }
215}
Listing 9.4: oop.Date.java (Version 4)
Die Klasse Person
Weil ein Student eine Person ist, wie eingangs vorgestellt, betrachten wir jetzt, der Sachlogik folgend, die Klasse Person, bevor wir uns dann im nächsten Abschnitt der Klasse Student zuwenden.
Eine Person ist bei uns, aus Gründen der Überschaubarkeit, auf drei Attribute, die deren Zustand widerspiegeln, beschränkt. Diese Attribute sind ein Vorname, ein Nachname und ein Geburtsdatum. Vor- und Nachname sind Zeichenketten, jeweils vom Datentyp String. Das Geburtsdatum bilden wir mithilfe der soeben implementierten Klasse Date ab.
Person hat ein Date. Eine solche Beziehung zwischen zwei Objektarten heißt Assoziation. Es wird eine Beziehung zwischen den Beteiligten Objekten hergestellt.
Es sollen folgende Validitätsregeln für Personenobjekte gelten:
- Vor- und Nachnamen dürfen keine leeren Zeichenketten sein.
- Vorname, Nachname und Geburtsdatum sind verpflichtend mit Werten zu belegen.
nullist jeweils nicht erlaubt. - Der Vorname und das Geburtsdatum sollen nach der Instanziierung eines Personenobjekts unveränderlich sein.
- Lediglich der Nachname eines Personenobjekts darf geändert werden, um beispielsweise Anwendungsfälle wie eine Namensänderung bei Eheschließung o. ä. abbilden zu können.
Diese Regeln definieren die Invariante eines Personenobjekts.
Als mögliches Verhalten legen wir fest:
- Die Attribute Vorname, Nachname und Geburtsdatum können von einem Personenobjekt erfragt werden.
- Eine Person soll sich im Falle einer Ausgabe selbst darstellen können (
toString()). - Namensänderungen sollen auf zweierlei Art und Weise möglich sein:
- Freie Zuweisung eines neuen Nachnamens.
- Übernahme des Nachnamens einer bereits existierenden Personeninstanz
Schritt 1
Wir beginnen damit, das grundlegende Gerüst der Klasse Person zu implementieren. Dazu gehören deren Konstruktor und die Methode toString().
1package oop;
2
3/**
4 * Die Klasse {@code Person} implementiert eine einfache Person.
5 */
6public class Person {
7 private final String fname; // nicht änderbar
8 private String lname; // bei Hochzeit o.ä. änderbar
9 private final Date dob; // Geburtsdatum
10
11 /**
12 * Konstruktor für eine Person.
13 *
14 * @param firstname Vorname
15 * @param lastname Nachname
16 * @param birthday Geburtsdatum
17 * @throws IllegalArgumentException wenn Parameter {@code null} oder
18 * leere Zeichenketten sind
19 */
20 public Person(String firstname, String lastname, Date birthday) {
21 // Parameter prüfen: keine NULL-Werte, keine leeren Zeichenketten
22 if (firstname == null || (firstname.trim()).isEmpty() ||
23 lastname == null || (lastname.trim()).isEmpty() ||
24 birthday == null )
25 throw new IllegalArgumentException(
26 "Ungültige Personendaten.");
27 // Parameter ok -> zuweisen
28 fname = firstname;
29 lname = lastname;
30 dob = birthday;
31 }
32
33
34 /**
35 * Rückgabe einer String-Darstellung der aktuellen Person.
36 *
37 * @return Person als Zeichenkettenkettendarstellung
38 */
39 @Override
40 public String toString() {
41 return String.format("%s, %s (%s)", lname, fname, dob );
42 }
43}
Listing 9.5: oop.Person.java (Version 1)
Die Klasse weist in dieser Implementierung einen eklatanten Fehler auf. Die drei Attribute fname, lname und dob sind als private gekennzeichnet, um sie gegenüber der Außenwelt unsichtbar zu machen. fname und dob sind zusätzlich als final deklariert, um deren Unveränderbarkeit zu kennzeichnen und durchzusetzen. – Die getroffenen Maßnahmen sind jedoch nicht hinreichend, die vereinbarte Invariante zu schützen.
import de.pakad.udemy.StdOut;
import oop.*;
public class OOPTest {
public static void main(String[] args) {
Date date = new Date(1, 1, 2000);
Person p = new Person("Karsten", "Brodmann", date);
StdOut.printf("%s\n", p);
// Geburtsdatum ändern
date.setYear(1900);
date.setMonth(12);
date.setDay(31);
StdOut.printf("%s\n", p);
}
}
Ganz offensichtlich ist es uns möglich, das Geburtsdatum, entgegen den getroffenen Vereinbarungen zu ändern. private und final bieten nicht den von uns gewünschten Schutz.
Ursächlich für dieses Phänomen ist die Tatsache, dass es sich beim Geburtsdatum um ein Objekt der Klasse Date handelt. Wir haben es mit einem Referenzobjekt zu tun. D. .h., im Attribut dob wird nicht das Objekt selbst gespeichert, wir speichern eine Referenz auf ein Datumsobjekt. Diese Referenz ist private und final, unsichtbar und, wenn sie einmal initialisiert wurde, unveränderbar. Wir haben jedoch mit der Variablen date, eine weitere Referenz auf das Geburtsdatum. Die haben wir bei dessen Instanziierung in date abgelegt (Zeile 6). Diese Referenz wird dann in Zeile 7 an die zu instanziierende Person übergeben, wo sie dann in deren Konstruktor (Zeile 30, Person.java) an die private Instanzvariable dob zugewiesen wird. Das Datumsobjekt wird also von zwei Variablen referenziert, date und dob.
Damit der angedachte Schutzmechanismus funktionieren kann, müssen wir die Referenz innerhalb der Klasse Person von einer eventuell bestehenden Referenz außerhalb eines Personenobjekts trennen. Das erreichen wir dadurch, ein im Konstruktor übergebenes Datumsobjekt zu kopieren.
Die korrekte Zuweisung im Personen-Konstruktor lautet daher:
dob = new Date(birthday);
Übernehmen Sie die Änderung in den Code der Personenklasse. Modifizieren Sie dann auch den Testclient wie folgt, um den Effekt der Maßnahme deutlich zu machen.
import de.pakad.udemy.StdOut;
import oop.*;
public class OOPTest {
public static void main(String[] args) {
Date date = new Date(1, 1, 2000);
Person p = new Person("Karsten", "Brodmann", date);
StdOut.printf("%s\n", p);
// Geburtsdatum ändern
date.setYear(1900);
date.setMonth(12);
date.setDay(31);
StdOut.println(date);
StdOut.printf("%s\n", p);
}
}
Die Verwendung des Kopierkonstruktors von Date sorgt dafür, in Person eine unabhängige Datumsinstanz abzulegen. Das postulierte Ziel, das Geburtsdatum unveränderbar zu gestalten wird nun erreicht.
Grundsätzlich sind als Parameter übergebene Referenzobjekte zu kopieren, nicht bloß deren bestehende Referenz zuzuweisen!
Sie werden sich fragen, wieso das bei Strings nicht erforderlich ist. Das sind schließlich auch Referenzobjekte. Kurze Antwort: Strings sind immutable. Bedingt durch ihre grundsätzliche Unveränderbarkeit, jede String-Operation erzeugt einen neuen String, kann das beobachtete Phänomen bei ihnen nicht auftreten.
Schritt 2
Nun wollen wir die Getter-Methoden implementieren. Hierbei haben wir den soeben beobachteten Effekt bezüglich Referenzobjekten ebenso zu beachten. Bei Strings tritt dieser Effekt nicht auf, wie ich soeben erläutert haben. Im Falle der Rückgabe der Instanz des Geburtsdatums müssen wir jedoch darauf achten, die interne Referenz zu schützen. D. h., wir haben eine Kopie an den Aufrufer der Methode getBirthday() zurückzugeben.
Wir können uns die Handhabung von Referenzobjekten mithilfe einer kleinen Gedächtnisstütze merken:
Copy In — Copy Out
Unsere Klasse Person sieht nun wie folgt aus. Die toString()-Methode habe ich dahingehend modifiziert, jetzt nur noch Vor- und Nachname zurückzugeben. Die gleichzeitige Rückgabe des Geburtsdatums diente lediglich dazu, den besprochenen Sachverhalt der Handhabung von Referenzen illustrieren zu können.
1package oop;
2
3/**
4 * Die Klasse {@code Person} implementiert eine einfache Person.
5 */
6public class Person {
7
8 private final String fname; // nicht änderbar
9 private String lname; // bei Hochzeit o.ä. änderbar
10 private final Date dob; // Geburtsdatum
11
12 /**
13 * Konstruktor für eine Person.
14 *
15 * @param firstname Vorname
16 * @param lastname Nachname
17 * @param birthday Geburtsdatum
18 * @throws IllegalArgumentException wenn Parameter {@code null} oder
19 * leere Zeichenketten sind
20 */
21 public Person(String firstname, String lastname, Date birthday) {
22 // Parameter prüfen: keine NULL-Werte, keine leeren Zeichenketten
23 if (firstname == null || (firstname.trim()).isEmpty() ||
24 lastname == null || (lastname.trim()).isEmpty() ||
25 birthday == null )
26 throw new IllegalArgumentException("Ungültige Personendaten.");
27 // Parameter ok -> zuweisen
28 fname = firstname;
29 lname = lastname;
30 dob = new Date(birthday);
31 }
32
33
34 /**
35 * Rückgabe einer String-Darstellung der aktuellen Person.
36 *
37 * @return Person als Zeichenkettenkettendarstellung
38 */
39 @Override
40 public String toString() {
41 return String.format("%s, %s", lname, fname);
42 }
43
44 /*
45 * Getter-Methoden
46 */
47
48 /**
49 * Rückgabe des Vornamens der aktuellen Person.
50 *
51 * @return Vorname
52 */
53 public String getFirstname() {
54 return fname;
55 }
56
57
58 /**
59 * Rückgabe des Nachnamens der aktuellen Person.
60 *
61 * @return Nachname
62 */
63 public String getLastname() {
64 return lname;
65 }
66
67
68 /**
69 * Rückgabe des Geburtsdatums als Kopie.
70 * <p>
71 * Die Rückgabe einer Kopie verhindert unerwünschte Referenzierung.
72 * </p>
73 * @return Kopie des Geburtsdatums
74 */
75 public Date getBirthday() {
76 return new Date(dob);
77 }
78}
Listing 9.6: oop.Person.java (Version 2)
Versuchen Sie ein von Person zurückgegebenes Datum zu modifizieren. Es wird Ihnen nicht gelingen. Modifizieren Sie den Code jedoch und geben die interne Referenz des Geburtsdatums an den Aufrufer zurück, schaffen Sie ein Einfallstor, die Invariante von Personenobjekten zu verletzen. – Probieren Sie das gerne aus.
Schritt 3
Im letzten Schritt wollen wir die vereinbarten Setter-Methoden implementieren. Das sind changeLastname() und adoptLastname(), um den Nachnamen einer bereits bestehenden Personeninstanz zu übernehmen. Diese Methoden bedürfen keiner weiteren Erläuterung.
1package oop;
2
3/**
4 * Die Klasse {@code Person} implementiert eine einfache Person.
5 */
6public class Person {
7 private final String fname; // nicht änderbar
8 private String lname; // bei Hochzeit o.ä. änderbar
9 private final Date dob; // Geburtsdatum
10
11 /**
12 * Konstruktor für eine Person.
13 *
14 * @param firstname Vorname
15 * @param lastname Nachname
16 * @param birthday Geburtsdatum
17 * @throws IllegalArgumentException wenn Parameter {@code null} oder
18 * leere Zeichenketten sind
19 */
20 public Person(String firstname, String lastname, Date birthday) {
21 // Parameter prüfen: keine NULL-Werte, keine leeren Zeichenketten
22 if (firstname == null || (firstname.trim()).isEmpty() ||
23 lastname == null || (lastname.trim()).isEmpty() ||
24 birthday == null )
25 throw new IllegalArgumentException("Ungültige Personendaten.");
26 // Parameter ok -> zuweisen
27 fname = firstname;
28 lname = lastname;
29 dob = new Date(birthday);
30 }
31
32
33 /**
34 * Rückgabe einer String-Darstellung der aktuellen Person.
35 *
36 * @return Person als Zeichenkettenkettendarstellung
37 */
38 @Override
39 public String toString() {
40 return String.format("%s, %s", lname, fname);
41 }
42
43 /*
44 * Getter-Methoden
45 */
46
47 /**
48 * Rückgabe des Vornamens der aktuellen Person.
49 *
50 * @return Vorname
51 */
52 public String getFirstname() {
53 return fname;
54 }
55
56
57 /**
58 * Rückgabe des Nachnamens der aktuellen Person.
59 *
60 * @return Nachname
61 */
62 public String getLastname() {
63 return lname;
64 }
65
66
67 /**
68 * Rückgabe des Geburtsdatums als Kopie.
69 *
70 * Die Rückgabe einer Kopie verhindert unerwünschte Referenzierung.
71 *
72 * @return Kopie des Geburtsdatums
73 */
74 public Date getBirthday() {
75 return new Date(dob);
76 }
77
78 /*
79 * Use-Case: Änderung des Nachnamens
80 */
81
82 /**
83 * Person heiratet eine nicht bekannte Person oder ändert aus anderen
84 * Gründen ihren Nachnamen.
85 *
86 * @param newLastname neuer Nachname
87 * @throws IllegalArgumentException wenn Parameter {@code null} oder
88 * leere Zeichenketten sind
89 */
90 public void changeLastname(String newLastname) {
91 // Parameter prüfen: keine NULL-Werte, keine leeren Zeichenketten
92 if (newLastname == null || (newLastname.trim()).isEmpty() )
93 throw new IllegalArgumentException(
94 "Ungültiger Nachname.");
95 // Parameter ok -> zuweisen
96 lname = newLastname;
97 }
98
99
100 /**
101 * Übernahme des Nachnamens vom Ehepartner.
102 *
103 * @param spouse Ehepartner
104 */
105 public void adoptLastname(Person spouse) {
106 // Parameter prüfen: keine NULL-Werte
107 if (spouse == null)
108 throw new IllegalArgumentException(
109 "Es darf nicht null übergeben werden.");
110 // Nachname von 'spouse' ist validiert -> direkte Übernahme
111 lname = spouse.lname;
112 }
113}
Listing 9.7: oop.Person.java (Version 3)
Implementieren wir einen kurzen Test mithilfe unseres Testclients.
import de.pakad.udemy.StdOut;
import oop.*;
public class OOPTest {
public static void main(String[] args) {
Person p1 = new Person("Karl", "Krösus", new Date(16, 7, 1995));
Person p2 = new Person("Susi", "Sorglos", new Date(10, 3, 2000));
StdOut.printf("%s\n", p1);
StdOut.printf("%s\n", p2);
// Susi heiratet Karl und nimmt dessen Nachnamen an
p2.adoptLastname(p1);
StdOut.printf("%s\n", p2);
}
}
Die Klasse Student
Ein Student ist eine Person. Er soll jedoch ein weiteres Attribut besitzen, welches das von ihm belegte Studienfach widerspiegelt. Weitere Verhaltensweisen, die über jene einer Person hinausgehen, soll ein hier implementierter Student nicht besitzen. Einzige Ausnahme: Er soll über sein Studienfach berichten können.
Ein Student besitzt, so wie wir ihn hier beschrieben haben, die gleichen Attribute wie eine Person: Vorname, Nachname und Geburtsdatum. Wir haben demnach viele Gemeinsamkeiten bezüglich der Attribute von Personen und Studenten. Betrachten wir das jeweilige Verhalten, so bestätigt sich diese Übereinstimmung. Ein Student weicht lediglich durch das Attribut Studienfach und das zusätzliche Verhalten, das Studienfach zurückgeben zu können, von einer Person ab. – Ein Student ist eine Spezialisierung einer Person.
Ein erster Gedanke zur Implementierung einer Klasse Student könnte sein, eine Klasse Student anzulegen und mittels Copy & Paste den Code aus Person zu übernehmen und lediglich um die Features zu ergänzen, die einem Studenten gegenüber einer Person noch fehlen. Da es bei einem Studenten Sinn machen kann, in einer textuellen Darstellung nicht nur seinen Namen, sondern auch das Studienfach, auszugeben, modifizieren wir die Methode toString().
Eine solche Vorgehensweise bedeutet dann jedoch:
- Duplikation bestehenden Codes. Das erschwert die Wartung und erhöht deren Aufwand.
- Wollen wir einer Person zusätzliche Features verleihen oder auch nur Fehler beheben, müssen wir das bereits an zwei Stellen tun, in der Klasse
Personund der KlasseStudent.
Führen Sie den Gedanken weiter. Stellen Sie sich vor, Sie wollen weitere Personen modellieren, z. B. Professoren, Verwaltungsangestellte u. .a. m. – Es ist offensichtlich, das eine Lösung der Aufgaben via Copy & Paste keine gute Idee sein kann.
Besinnen wir uns auf die Kernaussage dieses Abschnitts: Ein Student ist eine Person.
Genau dieses Verhältnis zwischen zwei Klassen kann mittels OOP elegant zum Ausdruck gebracht werden. Das Feature heißt Vererbung.
In Java drücken wir das syntaktisch und auch semantisch zutreffend mit
class Student extends Person {
...
}
aus. Ein Student erweitert eine Person. Das bedeutet ganz praktisch:
- Bereits implementierte Zustände und implementiertes Verhalten können von
Personübernommen werden. - Wir brauchen nur die Features ergänzen, die ein Student gegenüber einer Person zusätzlich haben soll.
- Wir können Verhaltensweisen, die anders als bei
Personausgestaltet sein sollen, modifizieren.
Die Vererbung, Student ist ein Nachfahre von Person, ähnelt damit der biologischen Vererbung. Kinder erben Attribute (Haarfarbe, Augenfarbe usw.) von Ihren Eltern. Ebenso erben Sie in der Regel verschiedene Verhaltensweisen, Fähigkeiten, wie beispielsweise Musikalität, die Art und Weise zu sprechen u. .a. .m.
Im Gegensatz zur biologischen Vererbung, bei welcher die Kinder zwei Eltern haben, Vater und Mutter, hat in Java ein Nachfahre lediglich einen Vorfahren. Das ist die sogenannte Elternklasse, von welcher der Nachfahre abgeleitet ist. Er erbt von seiner Elternklasse alle Attribute und Methoden, die in der Elternklasse, auch Ober- oder Superklasse genannt, nicht private sind. Ebenso werden Konstruktoren der Oberklasse nicht an eine von ihr abgeleitete Unterklasse vererbt. Jeder Nachfahre hat seine eigenen Konstruktoren zu definieren.
Dies vorausgeschickt, können wir mit der Implementierung der Klasse Student beginnen.
1package oop;
2
3/**
4 * Die Klasse {@code Person} implementiert eine einfache Person.
5 */
6public class Student extends Person {
7
8 private String studySubject;
9
10 /**
11 * Konstruktor für einen neuen Studenten.
12 *
13 * @param firstname Vorname
14 * @param lastname Nachname
15 * @param birthday Geburtsdatum
16 * @param studySubject Studienfach
17 * @throws IllegalArgumentException wenn Studienfach {@code null}
18 * oder leere Zeichenketten
19 */
20 public Student(String firstname, String lastname, Date birthday,
21 String studySubject) {
22 // Konstruktor der Oberklasse {@link Person} aufrufen.
23 // - validiert auch die übergebenen Parameter
24 super(firstname, lastname, birthday);
25 // Studienfach validieren
26 if (studySubject == null || (studySubject.trim()).isEmpty())
27 throw new IllegalArgumentException(
28 "Ungültiges Studienfach.");
29 this.studySubject = studySubject;
30 }
31
32
33 /**
34 * Rückgabe des Studienfachs des aktuellen Studenten.
35 *
36 * @return Studienfach
37 */
38 public String getStudySubject() {
39 return studySubject;
40 }
41
42
43 /**
44 * Rückgabe einer String-Darstellung des aktuellen Studenten.
45 *
46 * @return Student als Zeichenkettendarstellung
47 */
48 @Override
49 public String toString() {
50 String studentStr = String.format("%s [%s]", super.toString(),
51 studySubject);
52 return studentStr;
53 }
54
55 /*
56 * Use-Case: Wechsel des Studienfachs
57 */
58
59 /**
60 * Student wechselt das Studienfach.
61 *
62 * @param newStudySubject neues Studienfach
63 * @throws IllegalArgumentException wenn Parameter {@code null} oder
64 * leere Zeichenketten sind
65 */
66 public void changeStudySubject(String newStudySubject) {
67 // Parameter prüfen: keine NULL-Werte, keine leeren Zeichenketten
68 if (newStudySubject == null ||
69 (newStudySubject.trim()).isEmpty() )
70 throw new IllegalArgumentException(
71 "Ungültiges Studienfach.");
72 // Parameter ok -> zuweisen
73 studySubject = newStudySubject;
74 }
75}
Listing 9.8: oop.Student.java
Ich habe hier gleich die fertige Klasse Student wiedergegeben. Im Wesentlichen brauchen wir, mit einigen wenigen Ausnahmen, die wir gleich detailliert besprechen, nicht viel Neues erlernen, um Student umzusetzen.
Student erweitert Person
Die Klasse Student erweitert die bereits bestehende Klasse Person. Wir drücken das mittels extends im Kopf der Klasse Student aus. Damit machen wir klar: Ein Student ist eine Person mit erweiterten und ggf. modifizierten Features. – Die Klasse Student erbt von der Klasse Person und wird dadurch ein Nachfahre dieser Klasse. Person ist die Oberklasse von Student.
Weil Konstruktoren in Java nicht vererbt werden, muss die Klassen Student einen eigenen Konstruktor definieren. Wie auch bei Person ist dieser Konstruktor dafür verantwortlich, die Invariante von Studentenobjekten initial zu gewährleisten. Für einen Studenten gilt die Invariante von Personen, erweitert um die Regeln, die erforderlich sind, die im Studenten angelegten Erweiterungen zu sichern. Bildlich gesprochen stellt die Invariante eine Vertragserweiterung der von Person bekannten Invariante dar. Um das gewährleisten zu können, erfolgt die Initialisierung in Java immer in zwei Schritten.
-
Aufruf des Konstruktors der Elternklasse
-
spezifische Initialisierung der abgeleiteten Klassen.
Die Reihenfolge ist zwingend einzuhalten!
Ein Student wird bei dessen Instanziierung also erst zu einer Person, die dann durch weitere Initialisierungen zum Spezialfall einer Person, zu einem Studenten, wird. Daher ist der Konstruktor der Elternklasse zwingend zuerst aufzurufen.
super(firstname, lastname, birthday); // Aufruf des Eltern-Konstruktors
Diese Abfolge, im Rahmen von Vererbungsbeziehungen Objekte zu initialisieren, gilt in Java generell.
Das neu hinzugefügte Verhalten, das Studienfach wechseln zu können, folgt dem gleichen Schema wie der Namenswechsel einer Person.
Darüber hinaus, ohne es neu implementieren zu müssen, hat Student alle Eigenschaften und Verhaltensweisen von Person geerbt.
Interessant ist es nun, ererbtes Verhalten modifizieren zu können. Dazu wird eine ererbte Methode einfach überschrieben. Wir demonstrieren das an der Methode toString(). Neben seinem Namen soll ein Student auch sein Studienfach mitteilen, wenn er textuell ausgegeben wird.
Um das zu erreichen, definieren wir eine neue toString()-Methode. Dabei müssen wir unbedingt darauf achten, das überschreibende und überschriebene Methode den gleichen Bezeichner und identische Parameterlisten haben. Durch die Kennzeichnung Override teilen wir Java unser Vorhaben mit. Java prüft dann, ob wir korrekt überschrieben haben.
/**
* Rückgabe einer String-Darstellung des aktuellen Studenten.
*
* @return Student als Zeichenkettendarstellung
*/
@Override
public String toString() {
String studentStr = String.format("%s [%s]", super.toString(),
studySubject);
return studentStr;
}
Analog zum Konstruktor können wir direkt auf die Implementierung der Oberklasse zugreifen und diese nutzen. super.toString() ruft die toString()-Methode in Person auf. Student ergänzt lediglich das Studienfach. Ist eine Wiederverwendung der Eltern-Implementierung nicht gewünscht, kann eine Methode im Nachfahren auch gänzlich anderslautend implementiert werden. – Wichtig: Die Signatur, Name und Parameterliste, überschreibender und überschriebener Methode müssen identisch sein.
Testen wir mithilfe unsere Testclients, ob die Klasse Student das Versprechen erfüllt, eine Spezialisierung von Person zu sein.
import de.pakad.udemy.StdOut;
import oop.*;
public class OOPTest {
public static void main(String[] args) {
Student s1 = new Student("Karl", "Krösus",
new Date(16, 7, 1995), "Mathematik");
Student s2 = new Student("Susi", "Sorglos",
new Date(10, 3, 2000), "BWL");
StdOut.printf("%s\n", s1);
StdOut.printf("%s\n", s2);
// Susi heiratet Karl und nimmt dessen Nachnamen an
s2.adoptLastname(s1);
StdOut.printf("%s\n", s2);
// Karl wechselt das Studienfach
s1.changeStudySubject("Informatik");
StdOut.printf("%s\n", s1);
}
}
Ganz offensichtlich erfüllt Student unsere Erwartungen.
Polymorphie
Wir haben festgestellt, dass ein Student tatsächlich eine Person ist. Wenn dem so ist, was augenscheinlich der Fall ist, dann müsste es doch auch möglich sein, eine Gesamtheit von Personen zu betrachten, ohne auf die speziellen Ausprägungen der Personen Rücksicht zu nehmen. Wir haben lediglich zwei Ausprägungen, Person und Student.
Wenn wir diese gemeinsam betrachten und uns auf die Untermenge der Gemeinsamkeiten beschränken, sollte es möglich sein, z. B. Instanzen von Person und Student in einem als Person typisierten Array gemeinsam zu verwalten und dabei auch auf Verhalten zugreifen zu können, welches allen Instanzen im Array gemeinsam ist.
Ein Array von Personen sollte auch Studenten speichern können, weil Studenten Personen sind. Alle Methoden von Personen sollten dem Nutzer des Arrays verfügbar sein.
Und das geht! - Das nennt sich Polymorphie (Vielgestaltigkeit).
import de.pakad.udemy.StdOut;
import oop.*;
public class OOPTest {
public static void main(String[] args) {
Person p = new Person("Karl", "Krösus",
new Date(16, 7, 1995));
Student s = new Student("Susi", "Sorglos",
new Date(10, 3, 2000), "BWL");
StdOut.printf("%s\n", p);
StdOut.printf("%s\n", s);
// Susi heiratet Karl und nimmt dessen Nachnamen an
s.adoptLastname(p);
StdOut.printf("%s\n", s);
Person[] person = {p, s};
for (int i = 0; i < person.length; i++)
StdOut.println(person[i]);
}
}
Java verwendet zur Ausgabe einer Person, ob bloße Person oder Student, die jeweils spezifische Methode toString(), um die jeweils passende Zeichenkettendarstellung zu generieren. – Das ist ein wahrhaft tolles Feature!
OOP-Zusammenfassung
Die Modellierung eines Ausschnittes der realen Welt geschieht durch eine Klassenhierarchie, d. h., gleichartige Objekte werden zu Klassen zusammengefasst, von denen durch Vererbung Spezialisierungen abgeleitet werden. Gleichartigkeit bedeutet die Übereinstimmung von objektbezogenen Datenfeldern und objektbezogenen Methoden. Eine abgeleitete Klasse erbt von der Oberklasse die dort definierten Datenfelder und Methoden, fügt ggf. eigene hinzu und kann ihnen ggf. durch Überschreiben eine neue Bedeutung geben.
In Java gibt es die Klasse Object. Das ist in Java die „Mutter aller Klassen". Auch ohne eine extends-Klausel sind alle Klassen Nachfahren von Object. Aus diesem Grund haben wir von Beginn an toString() mit @Override gekennzeichnet. Diese Methode wird nämlich erstmalig von Object an jede Klasse vererbt, die sie bei Bedarf redefiniert, also überschreibt.
Objekte werden durch Konstruktoren erzeugt. Jede Klasse besitzt einen oder mehrere Konstruktoren, die für das Instanziieren ihrer Objekte zuständig sind. Wird kein eigener Konstruktor definiert, existiert automatisch der vom System bereitgestellte Default-Konstruktor (ohne Parameter).