Mit den bisher vorgestellten Programmen haben wir im Wesentlichen eine prozedurale Sichtweise verfolgt: Die Programme bestehen aus Prozeduren bzw. Funktionen, die Daten manipulieren. Ein Beispiel hierfür sind die Sortierroutinen aus Abschnitt 5.2, die Felder von int-Werten verarbeiten. Der wesentliche Nachteil eines solchen Ansatzes ist, dass die Struktur der Daten bekannt sein muss und keine logische Verbindung zwischen Daten und den darauf definierten Operationen existiert. Wird beispielsweise eine Sortierroutine für beliebige Felder (etwa mit Zeichenketten oder mit komplexeren Datentypen wie Bücher, Studenten usw.) benötigt, so muss entweder eine neue spezifische Prozedur implementiert oder eine Fallunterscheidung für die verschiedenen Typen vorgesehen werden. Das objektorientierte Paradigma, das in Java konsequent umgesetzt ist, schafft hier Abhilfe.
Objekte
Bei der objektorientierten Sichtweise wird die Trennung von Daten und Operationen aufgehoben. Im Mittelpunkt steht nicht das Wie – die Ausführung als Folge von Anweisungen –, sondern das Was, d.h. die in einer Anwendung existierenden Objekte sowie deren Struktur und Verhalten. Objekte sind somit besondere Daten- und Programmstrukturen, die Eigenschaften und darauf definierte Operationen (Methoden) besitzen.
Kapselung
Schnittstelle
Wartung von Programmen
Ein weiteres mit dem Objektbegriff verbundenes Konzept ist die Kapselung. Dies bezeichnet das Verbergen von internen Eigenschaften von Objekten, indem der Zugriff auf diese Eigenschaften nur über bestimmte eigene Methoden des Objektes zugelassen wird. Die Gesamtheit der Methoden bildet somit die Schnittstelle des Objektes. Auf diese Weise kann die interne Repräsentation der Objekte »geheim gehalten« werden (siehe Geheimnisprinzip von ADTs auf Seite 286), um etwa inkonsistente Änderungen (z.B. die Änderung der Matrikelnummer bei Studenten) zu verhindern. Dies vereinfacht auch die Wartung von Programmen, da die Abhängigkeiten zwischen Objekten auf die Kenntnis der Schnittstellen beschränkt sind. Wird die interne Struktur eines Objektes geändert, so hat dies keinen Einfluss auf andere Objekte, solange die Schnittstelle unverändert bleibt.
Zwei Beispiele sollen dieses Prinzip verdeutlichen. Betrachten wir ein Zeichenprogramm mit geometrischen Elementen wie Linien, Kreisen oder Rechtecken: Objekte repräsentieren in diesem Kontext die zu zeichnenden Elemente. Jedes Objekt hat dabei gewisse Eigenschaften: ein Kreis z.B. einen Mittelpunkt und einen Radius, eine Linie einen Anfangs- und einen Endpunkt usw. Weiterhin »weiß« jedes Objekt, wie es sich darzustellen bzw. zu zeichnen hat. Eine Zeichnung besteht somit aus einer Menge von Objekten und wird dargestellt, indem die spezifischen Zeichenroutinen der einzelnen Objekte aufgerufen werden.
Ein zweites Beispiel ist die Sortierung von Objekten wie Zeichenketten oder Studenten. Hierfür wird eine Vergleichsmöglichkeit benötigt: Bei Zeichenketten wird dies auf Basis der lexikographischen Ordnung erfolgen, bei Studenten kann z.B. die Matrikelnummer für den Vergleich genutzt werden. Wenn nun jedes Objekt »weiß«, wie es mit anderen Objekten verglichen werden muss, kann die Sortierung unabhängig von der konkreten internen Repräsentation der Objekte implementiert werden.
Der Begriff des Objektes kann insgesamt wie folgt beschrieben werden:
Ein Objekt ist die Repräsentation eines »Dings« der realen oder Vorstellungswelt, das gekennzeichnet ist durch:
Identität
Attribut
Methode

Objekt vs. Wert
Durch das Konzept der Identität unterscheiden sich Objekte auch von einfachen Werten, wie z.B. Zahlen. Die Identität eines Objektes ist unabhängig vom aktuellen Wert der Attribute. Eine Änderung des Namens eines Studenten führt so nicht zu einem neuen Studenten, sondern verändert nur eine Eigenschaft des Objektes. Demgegenüber resultiert die Änderung eines Wertes in einem neuen Wert. In Programmiersprachen wie Java wird intern als Identität eines Objektes meist einfach die Adresse des Objektes im Speicher verwendet, so dass die Eindeutigkeit gewährleistet ist.
In Abbildung 12–1 ist ein Beispiel für ein Objekt dargestellt, das eine geometrische Figur »Rechteck« repräsentiert. Eine solche Figur besitzt Eigenschaften wie Farbe, Position und Abmessung sowie Methoden zum Verschieben oder Berechnen der Fläche. Einzelne Objekte entsprechen nun konkreten Rechtecken, die verschiedene Eigenschaftswerte (z.B. verschiedene Positionen) haben können. Die Abbildung soll auch verdeutlichen, dass die Eigenschaften der Objekte gekapselt sind, d.h., der Zugriff darauf kann nur über die öffentlichen Methoden erfolgen.
Abb. 12–1 Objekt am Beispiel
Nachrichtenaustausch
Die Objekte einer Anwendung interagieren idealerweise ausschließlich durch den Austausch von Nachrichten. Ein Beispiel dafür ist eine Nachricht an Rechteck #1 »Verschieben um 10 mm nach rechts und 20 mm nach oben«. Eine solche Nachricht ist mit dem Aufruf einer Methode des Empfängerobjektes verbunden – in diesem Beispiel also der Methode »Verschieben« – und kann die Änderung des Zustandes (hier: der Position) bewirken.
In den meisten Fällen macht es relativ wenig Sinn, für jedes Objekt einzeln Struktur und Verhalten zu definieren. Vielmehr werden gleichartige Objekte, wie beispielsweise alle Rechtecke oder alle Studenten, zu sogenannten Klassen zusammengefasst.
Eine Klasse ist eine Menge von Objekten mit gleichen Eigenschaften (Attributen) und gleichen Verhalten (Methoden).

Somit legt eine Klasse auch den Datentyp zur Beschreibung der Eigenschaften der zugehörigen Objekte fest.
Im Beispiel in Abbildung 12–2 sind verschiedene Rechtecke dargestellt, die sich durch ihre Position, Größe und Farbe unterscheiden. Alle diese geometrischen Objekte haben aber einige Gemeinsamkeiten: Sie sind Rechtecke mit den Eigenschaften (Attributen) Position (x, y), Ausdehnung (w und h) sowie Farbe. Auch hinsichtlich möglicher Methoden lassen sich Gemeinsamkeiten finden. So berechnet sich der Flächeninhalt immer aus A = w · h und eine Verschiebung des Rechtecks um den Vektor (dx, dy) hat folgende Auswirkungen: x = x + dx, y = y + dy.
Instanz
Wir können daher sagen, dass die Rechtecke R1, R2 und R3Objekte oder Instanzen der Klasse Rechteck sind. An diesem Beispiel wird aber auch ersichtlich, dass alle Objekte einer Klasse bezüglich ihrer Struktur und ihres Verhaltens gleich sind, sich aber in ihrer Ausprägung, d.h. den konkreten Werten der Attribute, unterscheiden können.
Abb. 12–2 Klassenkonzept
Als objektorientierte Programmiersprache unterstützt Java natürlich die im vorigen Abschnitt eingeführten Konzepte. Wie eine Klasse in Java definiert wird, haben wir schon in Abschnitt 1.4 kurz vorgestellt. Programm 12.1 zeigt noch einmal die grundlegende Struktur:
Attribute
Konstruktor
package geom;
// Definition der Klasse Rechteck
public class Rechteck {
// Attributdeklarationen
int x, y, b, h;
int farbe;
// Konstruktoren
public Rechteck() {
x = y = 0;
b = h = 10;
}
public Rechteck(int xp, int yp, int br, int ho) {
x = xp; y = yp;
b = br; h = ho;
}
// Methode zum Verschieben
public void verschieben(int dx, int dy) {
x += dx; y += dy;
}
// Methode zum Berechnen der Fläche
public int berechneFlaeche() {
return b * h;
}
}
Klassendefinitionen können auch geschachtelt werden, d.h., eine Klasse kann innerhalb einer anderen Klasse definiert werden. Die Identifizierung solcher inneren Klassen erfolgt durch ÄußereKlasse. InnereKlasse. Dies ist insbesondere praktisch für Hilfsklassen, da durch die Schachtelung Namenskonflikte mit anderen Klassen vermieden werden können. Objekte innerer Klassen haben eine implizite Referenz auf das Objekt der äußeren Klasse, durch das sie mit erzeugt wurden. Ähnlich wie statische Klassenmethoden können aber auch innere Klassen ohne diese Referenz definiert werden. Hierzu muss die Klasse als static deklariert werden.
Erzeugen von Objekten
Konstruktoraufruf
Objekte müssen in Java mithilfe des new-Operators erzeugt werden, der gleichzeitig den benötigten Speicherplatz allokiert. Dazu ist nach dem Operator der Klassenname gefolgt von Klammern anzugeben. Innerhalb der Klammern können weitere Parameter übergeben werden. Hinter dieser Notation verbirgt sich der Aufruf eines Konstruktors der Klasse. Demzufolge muss zur Parameterfolge des Aufrufs ein kompatibler (bezüglich Anzahl und Typ der Parameter) Konstruktor definiert sein. Das Ergebnis des Aufrufs ist eine Referenz auf das neu erzeugte Objekt, die einer Referenzvariablen vom Typ der Klasse zugewiesen werden kann. Im folgenden Beispiel werden zwei Objekte der Klasse Rechteck erzeugt und den Variablen r1 und r2 zugewiesen. Im ersten Fall wird der Standardkonstruktor ohne weitere Parameter verwendet, im zweiten Fall der Konstruktor, der Position und Ausdehnung übernimmt.
Rechteck r1 = new Rechteck();
Rechteck r2 = new Rechteck(10, 10, 100, 20);
Zugriff auf Objekteigenschaften
Über die Referenzvariablen kann auf die Eigenschaften der Objekte zugegriffen werden. Die Notation hierfür ist die Punktschreibweise: refvar. eigenschaft, wobei eigenschaft sowohl für Methoden als auch Attribute steht. Der Zugriff ist natürlich nur entsprechend der definierten Sichtbarkeit der Eigenschaft möglich. Für die Klasse Rechteck kann so beispielsweise die Methode verschieben aufgerufen werden:
r1.verschieben(50, 30);
Es sei angemerkt, dass der Aufruf dieser Methode nur die Position des Objektes verändert, das durch die Variable r1 referenziert wird. Der Zugriff auf Attribute eines Objektes erfolgt ähnlich wie der Zugriff auf Variablen, allerdings wiederum durch Voranstellen der Objektreferenz. Für den Fall, dass die Attribute der Klasse Rechteck als öffentlich (public) deklariert wären, könnte auf diese auch wie folgt zugegriffen werden:
this
Dagegen kann innerhalb von Objektmethoden auf Attribute bzw. andere Methoden ohne Referenzvariable zugegriffen werden. Sollten dabei Konflikte zwischen den Attributen und den Parametern der Methode auftreten, kann durch Voranstellen des Schlüsselwortes this als Referenz auf das »eigene« Objekt das Attribut explizit gekennzeichnet werden. So könnte die Methode verschieben auch wie folgt implementiert werden:
public void verschieben(int x, int y) {
this.x += x; this.y += y;
}
Speicherbereinigung
Ein explizites Löschen von Objekten ist nicht notwendig, da Java über eine automatische Speicherbereinigung (engl. Garbage Collection) verfügt. Hierbei wird ein Objekt automatisch gelöscht, wenn es nicht mehr benötigt wird, d.h., wenn keine Variablen oder ein anderes Objekt darauf verweisen. Dies kann z.B. am Ende eines Blockes auftreten, wenn der Gültigkeitsbereich der dort deklarierten Variablen beendet ist oder wenn eine Referenzvariable explizit auf null gesetzt wird. In diesem Fall wird der Verweis auf das eventuell zuvor referenzierte Objekt gelöscht und das Objekt wird somit nicht mehr benutzt. Die Java-VM bzw. der Garbage Collector durchsucht regelmäßig den Speicher nach solchen »unbenutzten« Objekten und entfernt diese.
Klasseneigenschaften
Neben den »normalen« Objekteigenschaften, die Zustand und Verhalten einzelner Objekte betreffen, können auch Eigenschaften definiert werden, die global für alle Objekte einer Klasse gelten. Diese Klasseneigenschaften werden in Java durch Voranstellen des Schlüsselwortes static definiert. Typische Anwendungsfälle sind die Definition von Konstanten (die zusätzlich noch die Eigenschaft final haben, die eine Änderung des Wertes verbietet – siehe auch Abschnitt 12.3) und Hilfsfunktionen sowie von Methoden, die ohne Objekte aufgerufen werden sollen. Letzteres ist z.B. bei der main-Methode der Fall, da beim Programmstart noch kein Objekt vorhanden ist. Klasseneigenschaften müssen außerhalb der eigenen Klasse durch Voranstellen des Klassennamens identifiziert werden. Definiert man etwa für die Klasse Rechteck Standardgrößen:
public class Rechteck {
public static final int BREITE = 100;
public static final int HOEHE = 100;
...
}
so können diese Klasseneigenschaften wie folgt genutzt werden:
Rechteck r3 = new Rechteck(10, 10,
Rechteck.BREITE, Rechteck.HOEHE);
Standardbibliothek
Neben der Möglichkeit, eigene Klassen zu implementieren, bietet die Java-Umgebung bereits eine Vielzahl von vordefinierten und wiederverwendbaren Klassen an, die in der Standardbibliothek zusammengefasst sind. So gibt es u.a.:
Wrapper-Klassen
Nützlich sind auch die sogenannten Wrapper-Klassen java.lang.Integer, java.lang.Double, java.lang.Byte usw., die zur Kapselung der primitiven Datentypen int, double, byte usw. genutzt werden können. Damit lassen sich überall dort, wo eigentlich Objekte erwartet werden, auch Werte primitiver Typen einsetzen. Dies ist beispielsweise im Zusammenhang mit generischen Datenstrukturen wie Listen, Mengen oder Stapel nötig (siehe Kapitel 13). Wir wollen die Nutzung dieser Wrapper-Klassen im Folgenden kurz am Beispiel der Klasse java.lang.Integer illustrieren.
Zur Erzeugung von Integer-Objekten stehen zwei Konstruktoren zur Verfügung:
Weitere Methoden erlauben u.a. den Vergleich mit anderen Integer-Objekten (int compareTo(Integer i)) sowie das Auslesen des int-Wertes (int intValue()). Im folgenden Beispiel werden diese Methoden genutzt, um zunächst zwei Objekte i1 und i2 zu erzeugen, die anschließend verglichen werden. Ein Rückgabewert 0 der Methode compareTo bedeutet hierbei Gleichheit der Werte (ein Wert <0 steht für »kleiner«, >0 für »größer«). Schließlich wird der durch i2 repräsentierte int-Wert ausgelesen und der Variablen ival zugewiesen.
Integer i1 = new Integer(20);
Integer i2 = new Integer("20");
boolean equal = (i1.compareTo(i2) == 0);
int ival = i2.intValue();
Neben den erwähnten Klassen gibt es eine Vielzahl weiterer Bibliotheken, z.B. für die Arbeit mit Datenbanken, zur Erstellung von Webanwendungen oder anspruchsvollen 2D- und 3D-Grafiken. Informationen hierzu sind u.a. auf Java-Websites wie https://docs.oracle.com/en/java/ oder http://openjdk.java.net zu finden.
Vererbung
Oberklasse Unterklasse
Ein weiteres mächtiges Konzept der Objektorientierung ist die Vererbung, die der Definition von Spezialisierungs- bzw. Generalisierungsbeziehungen zwischen Klassen dient. Vererbung ermöglicht die Erweiterung bestehender Klassen und somit den Aufbau von Klassenhierarchien. Der Zusammenhang dieser Beziehungen ist in Abbildung 12–3 dargestellt. Ausgehend von einer Klasse (der sogenannten Oberklasse oder auch Super- bzw. Basisklasse) wird eine neue Klasse (die Unterklasse oder Subklasse) durch das Hinzufügen neuer Attribute und Methoden abgeleitet. Andere Bezeichnungen hierfür sind »erweitern« oder »spezialisieren«. Gleichzeitig erbt die Unterklasse alle Eigenschaften von der Oberklasse, d.h., alle Attribute und Methoden der Oberklasse sind auch in der Unterklasse verfügbar.
Abb. 12–3 Generalisierung vs. Spezialisierung
Generalisierung
Betrachtet man die Gegenrichtung der Beziehung, so kann die Oberklasse auch als eine Generalisierung der Unterklasse(n) aufgefasst werden. Die Oberklasse definiert dabei die gemeinsamen Attribute und Methoden aller Unterklassen. Im Rahmen unseres Zeichenprogramms besitzen alle Klassen von geometrischen Objekten auch gemeinsame Attribute, wie z.B. die Position oder die Farbe, sowie Methoden wie Verschieben, Vergrößern oder Anzeigen. Für all diese Klassen kann nun eine gemeinsame Oberklasse GeomObjekt eingeführt werden, die diese Eigenschaften definiert. Die Klassen Rechteck, Kreis usw. werden von dieser Klasse abgeleitet und erben damit die Eigenschaften.
Bei der Vererbung lassen sich zwei Formen unterscheiden (Abbildung 12–4):
Einfachvererbung
Mehrfachvererbung
Abb. 12–4 Einfach- vs. Mehrfachvererbung
In Java wird Vererbung durch die Notation
class Unterklasse extends Oberklasse
ausgedrückt. Hierbei muss Oberklasse eine bereits existierende Klasse sein, deren Code für den Compiler verfügbar ist. In Programm 12.2 ist Vererbung am Beispiel der Klasse Rechteck dargestellt. Wie bereits angedeutet, definiert GeomObjekt als Oberklasse allgemeine Eigenschaften geometrischer Objekte wie Position und Farbe sowie die Methode »Verschieben«. Da diese Eigenschaften von der Oberklasse vererbt werden, müssen sie in der Unterklasse nicht erneut definiert werden.
package geom;
// Definition der Klasse Rechteck als
// Unterklasse von GeomObjekt
public class Rechteck extends GeomObjekt {
// Attributdeklarationen
int b, h;
// Konstruktor
public Rechteck() {
b = h = 10;
}
// Methode zum Berechnen der Fläche
public int berechneFlaeche() {
return b * h;
}
}
Wurzelklasse
Wird keine Oberklasse angegeben, so wird implizit java.lang.Object als Oberklasse angenommen. Diese Klasse ist auch die Wurzelklasse der gesamten Java-Klassenhierarchie und definiert einige wichtige Methoden für alle Klassen:
Konstruktor-Verkettung
super
Wird eine Klasse von einer anderen Klasse abgeleitet, so muss sichergestellt werden, dass beim Erzeugen eines Objektes auch der Konstruktor der Oberklasse aufgerufen wird, um die dort notwendigen Intialisierungen vorzunehmen. Dies muss für alle Oberklassen erfolgen, und zwar in der Reihenfolge von der Wurzelklasse Object bis hinunter zur Klasse des Objektes. Dieses als Konstruktor-Verkettung (engl. constructor chaining) bezeichnete Prinzip wird in Java durch eine einfache Regel verwirklicht: Wenn die erste Anweisung im Konstruktor kein Aufruf eines Konstruktors der Oberklasse ist, so wird dieser implizit von der Java-VM aufgerufen. Für den Aufruf des Konstruktors der jeweiligen Oberklasse wird die spezielle Anweisung super() verwendet, die in dieser Form den Standardkonstruktor ohne Parameter aufruft. Soll dagegen ein anderer Konstruktor verwendet werden, so muss die passende Parameterliste mit super angegeben werden.
Im folgenden Beispiel ist dies für die Klassen GeomObjekt und Rechteck verdeutlicht. In GeomObjekt sind zwei Konstruktoren definiert: ein Standardkonstruktor, der die Position auf (0, 0) setzt, und ein Konstruktor, der die Position als Parameter erwartet:
class GeomObjekt {
...
GeomObjekt() {
x = y = 0;
}
GeomObjekt(int xp, int yp) {
x = xp; y = yp;
}
}
In der Klasse Rechteck sind ebenfalls zwei Konstruktoren definiert. Im ersten Standardkonstruktor ist kein super-Aufruf angegeben. Demzufolge wird hier als Erstes der Konstruktor GeomObjekt() implizit aufgerufen. Für den zweiten Konstruktor gibt es keine äquivalente Form in der Oberklasse. Deshalb wird hier explizit der Konstruktor GeomObjekt(xp, yp) durch den entsprechenden super-Aufruf ausgeführt:
class Rechteck extends GeomObjekt {
...
Rechteck() {
b = h = 0;
}
Rechteck(int xp, int yp, int bb, int hh) {
super(xp, yp);
b = bb; h = hh;
}
}
Bezüglich des Zugriffs auf Methoden und Attribute der Oberklasse(n) gelten die gleichen Regeln wie für eigene Eigenschaften. Einschränkungen sind nur durch Zugriffsmodifikatoren wie private für in der Oberklasse definierte Eigenschaften möglich, die wir bereits in Abschnitt 3.6.2 vorgestellt haben.
Polymorphie
Ein weiteres wichtiges Konzept der Objektorientierung, das speziell auch im Zusammenhang mit Vererbung zum Einsatz kommt, ist Polymorphie. Allgemein bedeutet der Begriff »Polymorphie« so viel wie »Vielgestaltigkeit«, im Kontext der Objektorientierung meint man damit, dass ein Konzept, wie z.B. eine Methode, unterschiedliche Formen annehmen kann. So ist etwa die Methode »Zeichnen« für verschiedene geometrische Primitive in unterschiedlicher Weise implementiert, denn schließlich wird ein Kreis anders gezeichnet als ein Rechteck. Ein anderes Beispiel für Polymorphie ist die Verwendung des »+«-Operators in Java: Angewendet auf Zahlen verbirgt sich dahinter die Addition, während bei Zeichenketten eine Konkatenation der Operanden stattfindet. Diese beiden Beispiele zeigen bereits, dass Polymorphie in Programmiersprachen in verschiedenen Formen zum Einsatz kommt. Die wichtigsten Formen sind hierbei:
Überladen
Auswahl der Methodenimplementierung
Überladen (engl. overloading) bezeichnet das mehrfache Verwenden eines Methodennamens innerhalb einer Klassendefinition mit unterschiedlichen Parametertypen bzw. -anzahl, wobei jedoch der Ergebnistyp immer gleich sein muss. So lassen sich verschiedene Implementierungen einer Methode für unterschiedliche Parameter(typen) angeben. Die Auswahl der tatsächlich aufzurufenden Methode erfolgt zur Laufzeit anhand des Typs der aktuellen Parameter. So sind in der folgenden Klasse verschiedene print-Methoden für Integer- und Gleitkommawerte sowie für Zeichenketten definiert:
class Printer {
void print(int i) {
System.out.println("int = " + i);
}
void print(double d) {
System.out.println("double = " + d);
}
void print(String s) {
System.out.println("String = " + s);
}
}
Werden diese Methoden nun mit verschiedenen Parametern aufgerufen, so wird anhand der Ausgabe deutlich, welche Implementierung jeweils wirklich gewählt wurde:
Printer p = new Printer();
...
p.print(12); // Ausgabe: int = 12
p.print("Hallo"); // Ausgabe: String = Hallo
p.print(42.0); // Ausgabe: double = 42.0
Der »+«-Operator ist in Java übrigens der einzige Fall eines überladenen Operators. In Gegensatz zu C++, wo für alle Operatoren einschließlich des new-Operators eine neue Implementierung vereinbart werden kann, ist das Überladen von Operatoren (engl. operator overloading) in Java nicht möglich.
Überschreiben
Unter Überschreiben (engl. overriding) versteht man, dass eine Methode in einer Unterklasse bezüglich ihres Namens, des Ergebnistyps sowie Typ und Anzahl der Parameter identisch zu einer Methode der Oberklasse ist. Man gibt somit zu einer bereits definierten und vererbten Methode eine neue Implementierung an und »überschreibt« daher die ursprüngliche Methode. So kann beispielsweise in einer Klassenhierarchie für geometrische Primitive in der Wurzelklasse GeomObjekt die Methode »Zeichnen« definiert und in jeder Unterklasse in spezifischer Weise neu implementiert werden. Werden nun Objekte dieser Klassen in einem Dokument zusammengefasst, so kann das gesamte Dokument dargestellt werden, indem für alle Objekte die »Zeichnen«-Methode aufgerufen wird. Da jede Klasse eine eigene Implementierung dieser Methode bereitstellt, ist keine Fallunterscheidung bezüglich der verschiedenen Objektarten notwendig. Die »richtige« Implementierung der Methode wird zur Laufzeit anhand des Typs des Objektes ausgewählt.
Das folgende Beispiel soll dieses Prinzip noch einmal illustrieren. Gegeben sei eine Klasse KlasseA mit einem Attribut a und einer Methode print sowie eine Klasse KlasseB als Unterklasse von KlasseA mit einem zusätzlichen Attribut b. In der print-Methode der Klasse KlasseB soll der Wert des zusätzlichen Attributes mit ausgegeben werden:
class KlasseA {
int a;
public KlasseA(int v) { a = v; }
public void print() {
System.out.println("A[" + a + "]");
}
}
class KlasseB extends KlasseA {
int b;
public KlasseB(int v1, int v2) {
super(v1); b = v2;
}
public void print() {
System.out.println("B[" + a + "," + b + "]");
}
}
Werden nun von diesen Klassen Objekte erzeugt, so liefern die Aufrufe der print-Methoden die folgenden Ausgaben:
KlasseA obj1 = new KlasseA(12);
KlasseB obj2 = new KlasseB(42, 17);
obj1.print(); // Ausgabe: A[12]
obj2.print(); // Ausgabe: B[42, 17]
Die Auswahl der auszuführenden Methode hängt dabei nicht vom Typ der Objektreferenz ab, die auf das Objekt verweist, sondern ausschließlich vom Typ des aufgerufenen Objektes, wie das folgende Beispiel zeigt:
obj1 = obj2;
obj1.print(); // Ausgabe: B[42, 17]
Objektreferenz
Dieses Beispiel demonstriert auch gleichzeitig die dritte Form von Polymorphie: Eine Objektreferenz kann auch auf Objekte von Unterklassen der eigenen Klasse verweisen. Dabei stehen natürlich nur die Eigenschaften zur Verfügung, die in der eigenen Klasse definiert sind. Allerdings kommt beim Methodenaufruf wieder das Prinzip des Überschreibens zur Anwendung.
Zugriffauf überschriebene Methoden
Das Überschreiben von Methoden wirft auch die Frage auf, wie auf überschriebene Methoden der Oberklasse zugegriffen werden kann. Dies kann beispielsweise notwendig werden, wenn die Attribute der Oberklasse als private deklariert sind und die überschriebene Methode die einzige Möglichkeit zum Zugriff bildet oder wenn die Methodenimplementierung der Oberklasse nicht ersetzt, sondern nur erweitert werden soll. Hier hilft – wie bereits beim Konstruktoraufruf – die Verwendung des Schlüsselworts super. Mit super. Methodenname ( Parameter ) kann direkt die Methode Methodenname der Oberklasse aufgerufen werden, auch wenn diese in der aktuellen Klasse überschrieben wurde. So wird im folgenden Beispiel die print-Methode der Klasse KlasseB unter Verwendung der Methode der Oberklasse implementiert (allerdings mit einer etwas modifizierten Ausgabe):
class KlasseB extends KlasseA {
...
public void print() {
System.out.print("B[");
super.print();
System.out.println(b + "]");
}
}
Wiederverwendung
Insgesamt sind Vererbung und Polymorphie mächtige objektorientierte Konzepte, die für die Implementierung von Datentypen sehr hilfreich sind und den Erstellungsaufwand durch die Möglichkeit der Wiederverwendung deutlich reduzieren können. Dies ist beispielsweise auch an der Java-Klassenbibliothek zu erkennen, wo diese Konzepte konsequent angewendet werden.
Abstrakte Methoden
Manchmal kann für eine Menge von Klassen eine gemeinsame Methode in einer Oberklasse definiert werden, aber es macht keinen Sinn, diese dort zu implementieren. So werden beispielsweise die Methoden »BerechneFläche« und »Zeichnen« für alle geometrischen Elemente benötigt und daher in der Klasse GeomObjekt definiert, aber es kann keine Implementierung angegeben werden, da diese Methoden in den konkreten Unterklassen überschrieben werden. Solche Methoden ohne Implementierung bezeichnet man als abstrakte Methoden und Klassen mit abstrakten Methoden demzufolge als abstrakte Klassen. In Java wird dies durch Voranstellen des Schlüsselwortes abstract vor die Methode bzw. die Klasse notiert:
abstract class GeomObject {
...
public void verschieben(int dx, int dy) { ... }
public abstract void zeichnen();
}
Abstrakte Klassen können sowohl konkrete (d.h. mit Implementierung) als auch abstrakte Methoden umfassen. Der Anweisungsblock entfällt dabei bei abstrakten Methoden, so dass die Methodendeklaration stattdessen durch ein Semikolon abgeschlossen wird. Eine wesentliche Eigenschaft von abstrakten Klassen ist, dass sie nicht instantiierbar sind, d.h., es können keine Objekte dieser Klassen erzeugt werden!
Anwendung
Eine sinnvolle Anwendung von abstrakten Klassen ergibt sich überall dort, wo eine gemeinsame Oberklasse definiert wird, ohne dass dabei für alle Methoden bereits eine konkrete Implementierung angegeben werden kann, da diese abstrakten Methoden in den Unterklassen spezifisch implementiert werden müssen. Das oben skizzierte Beispiel mit der abstrakten Oberklasse GeomObjekt ist somit eine geeignete Anwendung. Hier können – auch aufgrund der Polymorphie – die Instanzen der konkreten Klassen (Rechteck, Kreis, Linie usw.) über Objektreferenzen der Oberklasse manipuliert werden, da alle benötigten Methoden bereits definiert sind. Auf diese Weise kann etwa ein Dokument aus geometrischen Objekten als Feld von Objekten der Klasse GeomObjekt behandelt werden. Zu beachten ist jedoch, dass von GeomObjekt keine Instanzen erzeugt werden können, sondern hierfür die konkreten Klassen zu verwenden sind:
// Objekte erzeugen und in Dokument aufnehmen
GeomObjekt[] dokument = new GeomObjekt [3];
dokument[0] = new Rechteck();
dokument[1] = new Kreis();
dokument[2] = new Linie();
// alle Objekte zeichnen
for (int i = 0; i < dokument.length; i++)
dokument[i].zeichnen();
Mehrfachvererbung
Bereits bei der Einführung des Vererbungsprinzips haben wir darauf hingewiesen, dass Mehrfachvererbung in Java nicht möglich ist. Dies ist zunächst eine durchaus schwerwiegende Einschränkung, wie das folgende Beispiel (Abbildung 12–5) verdeutlicht. Ausgehend von unserer Klassienhierarchie mit geometrischen Objekten soll die Möglichkeit der Speicherung der Objekte vorgesehen werden. Nehmen wir weiter an, dass es hierfür bereits eine geeignete Basisklasse Speicherbar gibt, von der alle speicherbaren Klassen abzuleiten sind. Allerdings würde dies bedeuten, dass unsere Klassen sowohl von GeomObjekt als auch von Speicherbar abgeleitet werden müssten, was aber nicht möglich ist. Auch die Änderung der Hierarchie, so dass GeomObjekt eine Unterklasse von Speicherbar ist, kann nicht als Ausweg angesehen werden, wenn etwa GeomObjekt eine vordefinierte Klasse ist, deren Quelltext nicht verfügbar ist.
Abb. 12–5 Probleme ohne Mehrfachvererbung
Schnittstelle
Allerdings verfügt Java mit dem Konzept der Schnittstelle über einen Mechanismus, der das Problem der fehlenden Mehrfachvererbung aufwiegt. Schnittstellen sind ähnlich wie abstrakte Klassen, nur dass sie ausschließlich abstrakte, öffentliche Methoden sowie Konstantendefinitionen umfassen. Schnittstellendefinitionen werden durch das Schlüsselwort interface eingeleitet:
interface Name[ extends IName1, IName2 ...] {
Methodendeklarationen
}
Weiterhin können Schnittstellen von anderen Schnittstellen abgeleitet werden (ausgedrückt durch extends gefolgt von einer Liste von Schnittstellenbezeichnern IName1, IName2 usw.). Hierbei ist im Gegensatz zu Klassen auch Mehrfachvererbung möglich. Das bedeutet, dass eine Schnittstelle mehrere direkte Basisschnittstellen haben kann.
Die im obigen Beispiel eingeführte Funktionalität für speicherbare Objekte könnte so durch eine Schnittstelle Speicherbar realisiert werden:
interface Speicherbar {
void speichern(OutputStream out);
void laden(InputStream in);
}
Implementierung einer Schnittstelle
Eine Klasse kann nun eine oder mehrere Schnittstellen implementieren, d.h., dass diese Klasse für jede Methode der Schnittstelle(n) eine Implementierung bereitstellt. Die Implementierung einer Schnittstelle wird bei der Klassendefinition durch das Schlüsselwort implements ausgedrückt:
class Klasse implements Schnittstelle1,
Schnittstelle2 ... {
...
}
Auf diese Weise können unsere Klassen für die geometrischen Elemente um die Methoden zum Speichern und Laden erweitert werden:
class Rechteck extends GeomObjekt
implements Speicherbar {
...
void zeichnen() { ... }
void speichern(OutputStream out) { ... }
void laden(InputStream in) { ... }
}
Schnittstellentyp
Da Schnittstellen auch einen Typ definieren, können sie auch als Referenzdatentyp zur Deklaration von Referenzvariablen eingesetzt werden. Eine solche Variable eines Schnittstellentyps S kann auf Objekte aller Klassen verweisen, die die Schnittstelle S implementieren. So kann beispielsweise das Laden und Speichern von Objekten völlig unabhängig von den konkreten Klassen erfolgen, indem ausschließlich die Schnittstelle Speicherbar verwendet wird:
Rechteck r = new Rechteck();
...
Speicherbar sobj = r;
sobj.speichern(stream);
ADT als Schnittstelle
Mit abstrakten Klassen und Schnittstellen stehen uns somit leistungsfähige Konzepte zur Definition von Datentypen zur Verfügung. Ausgehend von der Spezifikation eines ADT kann zunächst eine Schnittstelle in Java definiert werden, die zwar die möglichen Operationen festlegt, aber die konkrete Implementierung noch offen lässt. Zu dieser Schnittstelle lassen sich schließlich spezielle – eventuell auf besondere Anforderungen optimierte – Klassen als konkrete Datentypen implementieren.
Exception
Zur Behandlung von Situationen, die Abweichungen vom normalen Programmablauf darstellen, wird in Java der Exception-Mechanismus verwendet. Eine Exception oder Ausnahme ist ein Signal, das das Eintreten einer besonderen Bedingung meldet, z.B. einen Fehler beim Öffnen einer Datei oder beim Zugriff auf ein Feld außerhalb der definierten Grenzen. Eine Ausnahme wird mithilfe der throw-Anweisung ausgelöst und zur Behandlung in einem catch-Block abgefangen. Wird eine Ausnahme nicht direkt behandelt, so wird diese den Aufrufstack entlang (d.h. »aufwärts«) propagiert, bis sie abgefangen wird – im ungünstigsten Fall erst durch den Java-Interpreter selbst, was mit einem Programmabbruch verbunden ist.
Deklaration und Signalisierung
Im Java-API sind für viele mögliche Situationen bereits eine ganze Reihe von Ausnahmen vordefiniert. Alle Ausnahmen sind Objekte einer Subklasse von java.lang.Throwable und werden nach folgendem Schema erzeugt, wobei MeineException als entsprechende Klasse definiert ist:
void eineMethode() throws MeineException {
...
throw new MeineException();
}
Beim Erzeugen der Ausnahme wird gleichzeitig der Konstruktor aufgerufen, wodurch zusätzliche Parameter übergeben werden können, die den Fehler näher beschreiben. Weiterhin muss jede Ausnahme, die im Rahmen einer Methode auftreten kann und nicht von Error oder RuntimeException abgeleitet ist, in der Signatur der Methode nach dem Schlüsselwort throws deklariert werden. Konkrete Beispiele für Ausnahmen sind u.a.:
Behandlung
Die Behandlung einer Ausnahme erfolgt durch einen try/catch/finally-Block:
try {
// Aufruf einer Methode, die eine Ausnahme
// der Klasse MeineException melden kann
}
catch (MeineException exc) {
// Fehlerbehandlung
}
finally {
// Aufräumen
}
Im try-Block werden Anweisungen ausgeführt, die möglicherweise zu einer Ausnahme führen können. Tritt eine solche Ausnahme auf, wird der Kontrollfluss an dieser Stelle unterbrochen und der korrespondierende catch-Block angesprungen. Demzufolge muss zu jeder möglichen Exception-Klasse ein entsprechender Block definiert sein. Allerdings lassen sich auch die Superklassen einer konkreten Ausnahme einsetzen, so dass eine verallgemeinerte Behandlung möglich ist. Im optionalen finally-Block, der unabhängig davon ausgeführt wird, ob der try-Block vorzeitig verlassen und welche Ausnahme signalisiert wurde, können Aufräumarbeiten wie das Schließen von Dateien oder das Freigeben von Ressourcen durchgeführt werden.
Abb. 12–6 Kontrollfluss beim Auslösen einer Ausnahme
Der Kontrollfluss beim Auslösen und Abfangen einer Ausnahme ist in Abbildung 12–6 dargestellt. In diesem Beispiel wird in einem try-Block eine Methode eineMethode aufgerufen. Diese Methode signalisiert im Rahmen der Abarbeitung aufgrund eines hier nicht näher betrachteten Fehlers eine Ausnahme. Die Ausnahme führt zum Verlassen des aufrufenden try-Blockes an der aktuellen Stelle und wird im korrespondierenden catch-Block »abgefangen«. Nachdem dieser Block vollständig abgearbeitet wurde, wird die Ausführung nach dem letzten, zum auslösenden try-Block gehörenden catch- bzw. finally-Block fortgesetzt.
Werden für spezielle Zwecke eigene Ausnahmen benötigt, so können diese auf einfache Weise als Klasse implementiert werden, die von java.lang.Exception bzw. java.lang.RuntimeException abgeleitet wird. Programm 12.3 demonstriert dies an einem Beispiel zur Signalisierung von Fehlern in Form von MeineException.
public class MeineException extends RuntimeException {
public MeineException(String msg) { super(msg); }
public MeineException() {}
}
Implementierung von Datentypen
Zum Abschluss dieses Kapitels wollen wir an einem einfachen Beispiel zeigen, wie ein abstrakter Datentyp in Form einer Java-Klasse realisiert werden kann. Objektorientierte Programmiersprachen wie Java bieten hervorragende Möglichkeiten, das Konzept von ADTs umzusetzen: Die Idee der Kapselung ist direkt im Klassenkonzept verwirklicht, und durch Schnittstellen und/oder Vererbung lassen sich unterschiedliche Implementierungen zu einem Datentyp trotzdem in gleicher Weise verwenden. Dies führt einerseits zu einer erhöhten Stabilität der restlichen Programmteile gegenüber Änderungen an der Implementierung eines Datentyps, erlaubt aber auch gleichzeitig die Auswahl einer optimalen Implementierungsvariante.
Zur Umsetzung von ADTs auf Klassen bietet sich weitgehend eine direkte Abbildung der Konzepte an. So entsprechen Typen den Klassen und Schnittstellen in Java, während Funktionen mit Methoden korrespondieren. Auch Konstruktoren lassen sich direkt in Java umsetzen. Nur für Axiome gibt es kein entsprechendes Konzept: Die Einschränkungen müssen operational, d.h. in Form von Java-Code, implementiert werden. Falls mehrere Implementierungsvarianten möglich sind, ist es sinnvoll, den ADT zunächst in Form einer Java-Schnittstelle umzusetzen und darauf aufbauend verschiedene Klassen mit möglichen Implementierungen bereitzustellen.
Betrachten wir zunächst einen einfachen abstrakten Datentyp für rationale Zahlen, wobei die Axiome weggelassen sind:
type RatNumber
import Int, Bool
operators
mk_rat: Int × Int → RatNumber
nom: RatNumber → Int
denom: RatNumber → Int
equal: RatNumber × RatNumber → Bool
is_zero: RatNumber → Bool
add: RatNumber × RatNumber → RatNumber
normalize: RatNumber → RatNumber
Funktionen
Als Funktionen definieren wir
Da es für die Implementierung dieses Datenyps sicher nicht viele Alternativen gibt, können wir direkt eine Java-Klasse mit Konstruktoren und Selektoren angeben (Programm 12.4).
public class RatNumber {
private int num = 0;
private int denom = 1;
public RatNumber() {}
public RatNumber(int n, int d)
throws InvalidDenominatorException {
num = d > 0 ? n : − n;
denom = Math.abs(d);
normalize();
}
public int numerator() { return num; }
public int denominator() { return denom; }
public boolean isZero() {
return num == 0;
}
public boolean equals(Object o) {
if (o instanceof RatNumber) {
RatNumber r = (RatNumber) o;
return numerator() * r.denominator() ==
denominator() * r.numerator();
}
else
return false;
}
public String toString() {
return "[" + num + "/" + denom + "]";
}
public RatNumber add(RatNumber r)
throws InvalidDenominatorException {
n = numerator() * r.denominator() +
r.numerator() * denominator();
d = denominator () * r.denominator ();
return new RatNumber(n, d);
}
private void normalize()
throws InvalidDenominatorException {
if (num == 0)
return;
if (denom > 0) {
int g = ggt(Math.abs(num), denom);
num = num / g;
denom = denom / g;
}
else
throw new InvalidDenominatorException();
}
private int ggt(int x, int y) {
while (x != y) {
if (x > y)
x − = y;
else
y − = x;
}
return x;
}
}
Für die Implementierung der Funktion add gibt es dagegen mehrere Möglichkeiten.
Die dritte Variante wurde hier gewählt, da sie einerseits eine »natürliche« Objektmethode ist und andererseits auch das Weiterverarbeiten des Ergebnisses durch Aneinanderhängen von Aufrufen ermöglicht, wie dies im folgenden Beispiel verdeutlicht wird:
// Erzeugen von rationalen Zahlen
RatNumber r1, r2, r3;
try {
r1 = new RatNumber(1, 3);
r2 = new RatNumber(1, 4);
r3 = new RatNumber(1, 2);
} catch (InvalidDenominatorException e) {
// Fehlerbehandlung
}
// Addieren: res = r1 + r2 + r3
RatNumber res = r1.add(r2).add(r3);
// Ausgabe des Ergebnisses
System.out.println("Ergebnis = " + res);
Dies ist natürlich nur ein sehr einfaches Beispiel eines Datentyps. Im folgenden Kapitel werden wir komplexere Datenstrukturen wie Keller oder Warteschlange genauer betrachten und zeigen, wie diese in Java implementiert werden können.
Lambda-Ausdruck
Seit der Version 8 unterstützt Java auch sogenannte Lambda-Ausdrücke. Lambda-Ausdrücke sind keine neue Erfindung, sondern eines der zentralen Konzepte der in Abschnitt 3.2.5 erwähnten funktionalen Programmiersprachen, die auf dem applikativen Paradigma basieren.
Lambda-Kalkül
Die formale Basis von Lambda-Ausdrücken bildet der Lambda-Kalkül. Hierbei handelt es sich um ein formales Modell (siehe auch Kapitel 6), das in den 30er Jahren von Church und Kleene entwickelt wurde. Der Name »Lambda« kommt von der Notation zur Definition (anonymer) Funktionen:
λx . A
Hierbei steht x für eine freie Variable und A für einen Ausdruck, der als Funktionskörper dient und sich typischerweise (aber nicht zwingend) auf x bezieht. Dies entspricht im Prinzip der Funktionsdefinition, wie wir sie für applikative Algorithmen in Abschnitt 3.2.5 eingeführt haben, wobei mit dem obigen Lambda-Ausdruck eine anonyme (d.h. nicht benannte) Funktion definiert wird. Das einfachste Beispiel ist die Identitätsfunktion:
λx . x
ein weiteres Beispiel die Quadratfunktion:
λx . x · x
An dieser Stelle soll die Theorie zum Lambda-Kalkül nicht weiter vertieft werden – wir verweisen dafür auf die entsprechende Literatur [Bar12]. Stattdessen wollen wir die Einführung von Lambda-Ausdrücke in Java mit einem kleinen Beispiel motivieren, das an das Java-Tutorial [Cor13] angelehnt ist. Wir nehmen an, dass wir ein Feld von rationalen Zahlen verarbeiten müssen, z.B. alle Zahlen kleiner 1 suchen und ausgeben. Dies kann durch folgenden Programmabschnitt implementiert werden:
RatNumber[] numbers = {
new RatNumber(1, 3),
new RatNumber(1, 2),
new RatNumber (3, 2) };
for (RatNumber rn : numbers) {
if (rn.numerator() < rn.denominator())
System.out.println(rn);
}
Sollen auch andere Verarbeitungsaufgaben gelöst werden, bietet sich eine generische Lösung an, bei der der Test (hier »Zahl kleiner 1?«) in eine Klasse ausgelagert wird. Dazu benötigen wir zunächst eine entsprechende Schnittstelle:
interface NumberPredicate {
boolean check(RatNumber rn);
}
sowie eine Verarbeitungsmethode:
static void printNumbers(RatNumber[] numbers,
NumberPredicate pred) {
for (RatNumber rn : numbers) {
if (pred.check(rn))
System.out.println(rn);
}
}
Nun kann eine kleine Klasse implementiert werden, die diese Schnittstelle unterstützt und den obigen Test durchführt:
class CheckLessThanOne implements NumberPredicate {
boolean check(RatNumber rn) {
return rn.numerator() < rn.denominator();
}
}
Mit dieser Klasse kann nun die Verarbeitungsmethode printNumbers aufgerufen werden:
printNumbers(numbers, // Feld von rationalen Zahlen
new CheckLessThanOne()); // Prädikat
Anonyme Klasse
Soll nun ein anderer Test implementiert werden, muss wiederum eine eigene Klasse erstellt werden. Dies kann durch sogenannte anonyme Klassen vereinfacht werden – hierbei erfolgt die Klassendefinition direkt im Zusammenhang mit der Instanziierung des Objektes. Man beachte die Notation aus runden Klammern (Konstruktoraufruf) und geschweiften Klammern (Klassendefinition):
printNumbers(numbers, new NumberPredicate() {
boolean check(RatNumber rn) {
return rn.numerator() < rn.denominator();
}
}
);
Anonyme Klassen sind seit Java 5 verfügbar und vereinfachen die Schreibweise deutlich – dennoch muss immer noch eine entsprechende Schnittstelle definiert werden.
Funktionsschnittstelle
Die oben eingeführte Schnittstelle NumberPredicate ist ein typisches Beispiel einer Funktionsschnittstelle, also einer Schnittstelle, die nur eine einzige Methode umfasst.
Syntax von Lambda-Ausdrücken
Lambda-Ausdrücke bieten nun einen sehr bequemen Weg, Objekte von Funktionsschnittstellen zu definieren. Syntaktisch wird ein Lambda-Ausdruck durch eine Parameterliste params und einen Ausdruck oder einen {...}-Anweisungsblock definiert, die durch -> verbunden sind:
( params ) -> {...}
Hierbei kann die Parameterliste auch leer sein (()) bzw. wenn nur ein Parameter angegeben wird, können die runden Klammern auch entfallen. Weiterhin kann der Typ der Parameter ebenfalls weggelassen werden. Der obige Test auf eine rationale Zahl < 1 wird daher wie folgt als Lambda-Ausdruck notiert:
Man beachte, dass es sich bei der rechten Seite um einen Ausdruck handelt und so kein return notwendig ist. Eine alternative Schreibweise mit der Angabe des Parametertyps und einem Anweisungsblock auf der rechten Seite wäre:
(RatNumber rn) ->
{ return rn.numerator() < rn.denominator(); }
Damit ein solcher Lambda-Ausdruck in einer Methode wie unser Beispiel printNumbers eingesetzt werden kann, muss diese natürlich eine vordefinierte und zum Lambda-Ausdruck passende Schnittstelle nutzen. In Java 8 sind im Paket java.util.function entsprechende Schnittstellen vordefiniert, so u.a. auch eine Funktionsschnittstelle für Prädikate:
interface Predicate<T> {
boolean test(T t);
}
Parametrisierbare Datentypen
Die Notation <T> bezeichnet einen Typparameter für sogenannte Generics – parametrisierbare Datentypen, die in Abschnitt 13.7 näher erläutert werden. An dieser Stelle ist nur wichtig, dass printNumbers wie folgt angepasst werden muss, um diese Funktionsschnittstelle zu nutzen:
static void printNumbers2(RatNumber[] numbers,
Predicate<RatNumber> pred) {
for (RatNumber rn : numbers) {
if (pred.test(rn))
System.out.println(rn);
}
}
Lambda-Ausdruck
Nun kann printNumbers2 direkt mit unserem Lambda-Ausdruck aufgerufen werden:
printNumbers2(
numbers, // Feld von rationalen Zahlen
rn -> rn.numerator() < rn.denominator() // Lambda-Ausdruck
);
was deutlich kompakter als die ursprüngliche Notation ist.
Thread
Dass Lambda-Ausdrücke nicht auf Prädikate beschränkt sind, soll das folgende Beispiel demonstrieren, das die Klasse Thread (siehe dazu Abschnitt 9.4) nutzt. Neben der Ableitung einer eigenen Klasse von Thread lässt sich das Verhalten eines Threads auch über die Implementierung der vordefinierten Schnittstelle Runnable definieren:
Wie im ersten Beispiel kann man nun klassisch eine eigene Klasse schreiben, welche diese Schnittstelle (konkret die Methode run) implementiert. Alternativ (und bequemer) ist der Einsatz eines Lambda-Ausdrucks:
Runnable r = () -> { System.out.println("poch");
Thread.sleep(1000); };
new Thread(r).start();
Natürlich kann dieser Ausdruck auch direkt als Parameter des Thread-Konstruktors angegeben werden:
new Thread(() -> { System.out.println("poch");
Thread.sleep(1000); }).start();
Für weitere Aspekte wie insbesondere die Integration in das Java-Typsystem, Gültigkeitsbereiche und weitere Funktionsschnittstellen sei auf die aktuelle Dokumentation verwiesen.