Java Quick Intro
Java Quick Intro
J a
i n
n g
h r u e r tz
fü lan
k
E i n min B
ja
i ne nd
Be
n
l e
K era R ö hr
u
V
T ECHNISCHE U NIVERSITÄT B ERLIN
V ORLESUNGSSKRIPT
1 Einführung 1
1.1 Annäherung an Java . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.1 Übersicht über die Unterschiede C – Java . . . . . . . . . . 2
1.2 Programmieren in Java . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.1 Primitive Datentypen . . . . . . . . . . . . . . . . . . . . . . 5
1.2.2 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2 Konzepte 10
2.1 Objektorientierte Programmierung (OOP) . . . . . . . . . . . . . . 10
2.1.1 Das Konzept von Klassen und Objekten . . . . . . . . . . . 10
2.1.2 Klassen in Java . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.1.3 Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.1.4 Syntaktische und Semantische Gleichheit von Objekten . . 18
2.2 Klassenhierarchien und Vererbung (inheritance) . . . . . . . . . . . 19
2.2.1 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2.2 Vereinheitlichte Modellierungssprache (Unified Modeling
Language; UML) . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2.3 Vererbungshierarchie von Klassen . . . . . . . . . . . . . . 20
2.3 Polymorphismus . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.4 Abstraktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.4.1 Abstrakte Methoden und Klassen . . . . . . . . . . . . . . . 24
2.4.2 Generics: Von einzelnen Spielsteinen zu einer Kollektion . 24
2.4.3 Datenabstraktion . . . . . . . . . . . . . . . . . . . . . . . . 26
2.4.4 Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3 Datenstrukturen 29
3.1 Die Schnittstellen Iterator und Iterable . . . . . . . . . . . . . 29
3.2 Collections in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.3 Warteschlangen (FIFO) . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.4 Laufzeiten bei einfachen Java Datenstrukturen . . . . . . . . . . . 34
3.5 Die Schnittstellen Comparable und Comparator . . . . . . . . . . 36
iii
iv Inhaltsverzeichnis
4 Gute Praxis 40
4.1 Umgang mit Fehlern und Ausnahmen . . . . . . . . . . . . . . . . 40
4.1.1 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.1.2 Ausnahmenbehandlung . . . . . . . . . . . . . . . . . . . . 41
4.1.3 Assertionen . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.1.4 Modultests (JUnit) . . . . . . . . . . . . . . . . . . . . . . . 46
4.2 Ein Kommentar zu Kommentaren . . . . . . . . . . . . . . . . . . . 46
4.2.1 Standardisierte Javadoc Kommentare . . . . . . . . . . . . 46
Abbildungsverzeichnis 51
Listings 51
Index 52
Literaturverzeichnis 55
KAPITEL 1
Einführung
1
2 Kapitel 1 Einführung
Um das Erlernen von Java zu erleichtern und eine gewisse Vertrautheit in der
Sprache zu erreichen, hat man sich stark an C++ (und C) orientiert. Deshalb ist
die Syntax mitunter sehr ähnlich. Dennoch gibt es offensichtlich Unterschiede
zwischen den Programmiersprachen, vor allem zu C. Neben den konzeptionellen
Unterschieden zwischen einer objektorientierten (Java) und einer prozedualen
(C) Programmiersprache, gibt es in C keine JVM, keine automatische Speicherver-
waltung etc. Im folgenden sind einige der wichtigsten Unterschiede aufgelistet:
1.2 Programmieren in Java 3
C Java
Ausführung Compiler erzeugt Maschinencode Byte Code, muss interpretiert
werden
Boolean int mit 0 für false und ! = 0 für boolean mit Werten true und
true false
Array Deklaration int *x = malloc(N*sizeof(*x));int[] x = new int[N];
Array Größe unbekannt für das Array x.length
Zeichenketten ’\0’ terminiertes char Array Datentyp String
Datenstruktur struct class - Klassen mit Methoden
definieren
Bibliotheken laden #include <stdio.h> import java.io.File;
Bibilotheksfunktionen #include "math.h" x = Math.sqrt(3.14);
nutzen x = sqrt(3.14); Funktionen haben namespaces
Funktionen sind global
Speicher referenz. Zeiger (*, &, +) Referenzen
Speicher reservieren malloc new
Speicher freigeben free automatische
Speicherbereinigung
Generischer void * Object
Datentyp
Null NULL null
Variablen nicht garantiert Instanzvariablen und Array
automatisch Elemente initialisiert mit 0, null,
initialisiert bzw. false
Variablen am Anfang eines Blocks irgendwo, vor der Benutzung
deklarieren
Variablennamen mean_square_error meanSquareError
Konvention
Dateinamen stack.c, stack.h Stack.java übereinstimmend
mit Klassennamen
Bildschirmausgabe #include<stdio.h> System.out.println("I am
printf("I am C\n"); Java\n");
Kommentare /* ... */ oder vorangestelltes // /* ... */ oder vorangestelltes //
In jedem dieser Schritte kann und wird es Fehler geben, die man eingebaut hat,
inklusive des letzten Schrittes: dem (nur manchmal richtigen) Ergebnis. Es sind
also in jedem Level Kontrollschleifen in Form vom Debugging des Quellcodes
zu durchlaufen (Siehe Abbildung 1.1), bevor der Bytecode fehlerfrei von der
JVM ausgeführt werden kann und das richtige Ergebnis herauskommt.
Editor
Hello.java
(Quellcode)
Übersetzen Übersetzungs-
> javac Hello.java Java Compiler
fehler
Hello.class Debugging
(Bytecode)
Ausführen Laufzeit-
> java Hello JVM
fehler
unerwünschtes
Ergebnis
Ergebnis
IDE Kompiler- oder Übersetzungsfehler meist sofort als Fehler markiert, sodass
sie direkt beim Schreiben behoben werden können. Zum Kompilieren und Aus-
führen muss kein Terminal aufgemacht werden, beides ist bereits in der IDE
intergriert. Zum Finden der Ursache von Laufzeitfehlern oder einem falschen
Ergebnis, kann man mit dem Debugger den Code Schritt für Schritt und Zwi-
schenergebnis für Zwischenergebnis durchgehen. (siehe auch Abschnitt 4.1.1)
In den nächsten Abschnitten wiederholen wir kurz primitive Datentypen und
Arrays, welche aus dem 1. Semester bekannt sein sollten, für Java.
Datentypen legen fest, um was für eine Art Variable es sich handelt, welche
Operationen darauf ausgeführt werden können und wie die Repräsentation
der Variablen im Speicher aussieht, d.h. welches Bit welche Bedeutung hat und
wie viele es davon gibt. Primitive Datentypen (primitive data types) setzen sich
nicht aus einfacheren Datentypen zusammen, sie sind die Grundbausteine der
Datentypen. In Java gibt es die folgenden :
Wenn diesen Datentypen kein Wert zugewiesen wird, werden sie automatisch
initialisiert und zwar auf false oder 0 bzw. den null-character für char. Am häu-
figsten werden boolean, int und double verwendet, sowie die Klasse (abstrak-
ter Datentyp) String, welche Zeichenketten implementiert.
Mit Hilfe dieser Datentypen und Klassen, welche wir in Abschnitt 2.1 kennenler-
nen werden, können nun reale Größen und Zusammenhänge dargestellt werden.
Damit diese Darstellung akkurat ist, sollten Datentypen mit Bedacht gewählt
6 Kapitel 1 Einführung
werden, was diese Anekdote von Youtube aus dem Jahr 2014 illustriert:
1.2.2 Arrays
Arrays oder Felder (array) speichern eine bestimmte Anzahl an Objekten eines
bestimmten Datentyps, wobei jedes Objekt eine Nummer bekommt, nämlich
den Index, an dem es zu finden ist. Um in Java einen Array anzulegen, bedarf es
dieser drei Schritte:
I Deklaration mit Angabe von Namen und Typ
Was die Variable als Array kennzeichnet sind die eckigen Klammern hinter dem
Datentyp. Um ein neues Objekt zu erzeugen, wird dann der new Operator aufge-
rufen und die Anzahl der Elemente der Array (hier K) wieder in den Klammern
hinter dem Datentyp angegeben. Die Initialisierung auf 0.0 in der for-Schleife
ist aber in diesem Fall nicht notwendig, da die automatische Initialisierung von
Java die Werte bereits vorher auf 0.0 gesetzt hatte. Um einen Array der Länge
K zu initialisieren, der mit Nullen gefüllt ist, brauchen wir also tatsächlich nur
eine Zeile:
Datentyp[] x = new Datentyp [K];
Dabei ist zu beachten, dass die Indizierung von Arrays in Java (wie in vielen
Programmiersprachen) mit 0 beginnt. Mit obiger Definition gibt es also die
Elemente x[0] bis x[K-1], ein Element x[k] existiert jedoch nicht.
Eine zweite Möglichkeit der Initialisierung, die sinnvoll ist, wenn die Elemente
nicht Null sind und die Initialisierung über eine for-Schleife nicht sinnvoll ist,
ist die Deklaration mit Initialisierung durch eine Werteliste. Dafür werden die
Werte einfach in Listenform angegeben:
int[] fibo = { 0, 1, 1, 2, 3, 5, 8 };
Um auf eine bestimmte Stelle eines Arrays zuzugreifen, gibt man den spezifi-
schen Index in eckigen Klammern an. Zum Beispiel: fibo[5], wenn wir das mit
folgendem Code ausführen würden, käme 5 heraus.
System.out.println(fibo[5])
Mehrdimensionale Arrays
In Java gibt es keine ‘echten’ mehrdimensionalen Arrays, stattdessen werden
Arrays von Arrays (von Arrays ...) gebildet. Das hat viele wichtige Konsequen-
zen, z.B. beim Kopieren und Vergleichen, die später erläutert werden. Zunächst
8 Kapitel 1 Einführung
schauen wir uns ein Beispiel für die Erzeugung eines 2-dimensionalen Arrays
an:
// Deklaration 2-dim Array:
int[][] x;
// Erzeugen eines 2-dim Arrays
x = new int[4][3];
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
x[i][j] = i - j;
}
}
Das funktioniert im Prinzip, wie bei eindimensionalen Arrays, nur dass wir jetzt
zwei Indizes beachten müssen. Aber da wir eigentlich nur Arrays von Arrays
bilden, müssen mehrdimensionale Arrays tatsächlich nicht rechteckig sein.
D. h. jeder Array, der in dem ersten Array gespeichert ist, kann eine andere
Länge haben. Das sieht dann z. B. so aus:
Es besteht auch hier wieder die Möglichkeit der Initialisierung durch eine Werte-
liste, genau genommen sogar zwei Möglichkeiten:
Oder man iteriert direkt über das Array, ein Mechanismus, auf den wir an
anderer Stelle genauer eingehen werden.
In dem Codeabschnitt durchläuft die Variable zahl alle Elemente des Arrays
folge. Direkte Iteration funktioniert auch über mehrdimensionale Arrays, nur
dass die Elemente, über die iteriert wird, in der ersten Schleife auch noch Arrays
sind:
int[][] triangle = {{1}, {2, 3}, {4, 5, 6}};
int sum = 0;
for (int[] row : triangle) { //die Elemente von triangle sind int-Arrays
for (int element : row) { //die Elemente von row sind int
sum += element;
}
}
KAPITEL 2
Konzepte
10
2.1 Objektorientierte Programmierung (OOP) 11
Position auf dem Spielbrett etc. Wenn wir jetzt ein Spiel programmieren würden,
sind uns die meisten dieser Eigenschaften nicht wichtig, die Position auf dem
Spielbrett könnte aber relevant sein. Letzteres wird dann ein Attribut der Klasse.
Hierbei ist anzumerken, dass in der Klasse keine spezifische Position festgehal-
ten wird, nur dass es eine gibt. Denn die Klasse wird der Bauplan werden, aus
dem man viele verschiedene gesetzte Spielsteine instanzieren kann.
Man kann Spielsteine in der Realität auf dem Brett bewegen und so kann man
der entsprechenden Klasse Token eine Methode geben, die festlegt wie ein Stein
auf dem Brett bewegt werden kann. Solange es aber kein spezifisches Objekt
gibt, dass bewegt wird, beschreibt die Methode nur die allgemeine Möglichkeit
Spielsteine zu bewegen. Um die Methode ausführen zu können, muss ein Objekt
instanziert werden, zum Beispiel das Token weißerKreisMitgrünemPunkt. D. h.
es muss dafür einen Platz im Speicher geben (der new Operator findet hier
seine Anwendung). Außerdem bekommt weißerKreisMitgrünemPunkt eine
bestimmte Position zugewiesen und kann nun auch auf dem Brett bewegt wer-
den. Objekte sind also durch folgende Aspekte charakterisiert:
I Identität: Speicherbereich des Objektes
I Zustand: Wert des Datentyps = spezifische Position (Instanzvariablen)
I Verhalten: Definiert durch die Objektmethoden
12 Kapitel 2 Konzepte
I Klassenvariablen sind Datenfelder, die als static definiert werden. Sie exis-
tieren nur ein einziges Mal pro Klasse und alle Objekte der Klasse können auf
sie zugreifen.
I Lokale Variablen werden innerhalb von Methoden definiert und existieren nur
für die Dauer des Methodenaufrufs.
I Klassenmethoden sind Methoden, die als static definiert werden. Sie bezie-
hen sich auf kein konkretes Objekt der Klasse (können allerdings als ein
Argument ein Objekt der eigenen Klasse nehmen). Daher können sie nur auf
Klassenvariablen und nicht auf Instanzvariablen zugreifen.
Klassenmethoden ruft man über den Klassennamen und den Punkt Operator
auf, z. B. Math.log(17).
2.1 Objektorientierte Programmierung (OOP) 13
class Token {
static int counter; // Klassenvariable
int xPos, yPos; // Instanzvariablen
Token() { // 1. Konstruktor
counter++;
}
Token(int x, int y) { // 2. Konstruktor
this(); // Verkettung
xPos = x;
14 Kapitel 2 Konzepte
yPos = y;
}
// Methode
void moveTo(int x, int y) {
xPos = x;
yPos = y;
}
}
Hier fehlen noch die für Java typischen Zugriffsmodifizierer, siehe 2.1.2. In einer
main-Methode könnte man folgenden Code ausführen, um die Funktionalität
der Klasse zu überprüfen.
// Array deklarieren
Token[] spielstein = new Token[3];
// Konstruktor mit new aufrufen,
// um Objekte zu erstellen
spielstein[0] = new Token(0, 0);
spielstein[1] = new Token(2, 3);
spielstein[2] = new Token(4, 6);
// Methode aufrufen, um
// Objekt zu veraendern
spielstein[1].moveTo(0, 3);
// Zugriff auf Klassenvariable
System.out.println(Token.counter);
Die Klassenvariable gehört zur ganzen Klasse. Daher wird sie nicht mit einem
Objekt, sondern dem Klassennamen aufgerufen.
public Token() {
counter++;
}
public Token(int x, int y) {
this();
xPos = x;
yPos = y;
}
Auf primitive Datentypen wird diese Konvention nicht immer angewendet. Hier
muss der Nutzen gegen den zusätzlichen Aufwand abgewogen werden. Der
zeitliche Aufwand ist sehr gering, wenn man die entsprechende Unterstützung
einer IDE nutzt (bei IDEA: Cursor in dem Variablennamen positionieren und
Alt+Insert drücken, oder Rechtsklick, dann Generate, dann Getter bzw. Setter
auswählen).
2.1.3 Objekte
Objekte sind Instanzen von Klassen. Sie werden per new durch den Konstruk-
tor erzeugt. Bei einem Aufruf von new wird Speicher reserviert, Werte initiali-
siert durch den Konstruktor und eine Referenz auf das Objekt zurückgegeben.
Wenn new mehrfach aufgerufen wird, werden mehrere Objekte derselben Klasse
erzeugt, jedes mit einer eigenen Identität, also einem eigenen Platz im Speicher
(siehe Beispiel Klasse auf S. 13). Der Zugriff auf Objekte geschieht über Refe-
renzen. Daher werden die nicht-primitiven Datentypen auch Referenztypen
genannt.
Token stein1;
Token stein2;
stein1= new Token(1, 1);
stein2= new Token(2, 2);
stein2= stein1;
stein2.moveTo(2,6);
System.out.println("Stein1: " + stein1);
System.out.println("Stein2: " + stein2);
Token stein1;
Token stein2;
stein1= new Token(1, 1);
stein2= new Token(2, 2);
stein2= stein1;
stein2.moveTo(2,6);
System.out.println("Stein1: " + stein1);
System.out.println("Stein2: " + stein2);
Warum ist das so? Wir haben zuerst zwei unterschiedliche Objekte erzeugt,
beide haben eine eigene Identität und eine eigene Referenz, siehe Abbildung 2.2
(a)–(c). Dann haben wir aber stein2 die Referenz auf stein1 zugewiesen (d).
Das heißt, egal welche von beiden Steinen wir bewegen (e), es wird immer auch
die gleiche Referenz zugegriffen und damit auf das gleiche Objekt, welches nun
hinter beiden Variablen steckt. Auf das zweite Objekt dagegen haben wir den
Zugriff komplett verloren (Garbage (d-e)).
(d) (e)
Garbage Collection
Durch Zuweisungen von Referenztypen kann ein Objekt im Speicher “verloren”
gehen, wenn keine Referenz auf das Objekt mehr existiert (siehe Seite 16). Solche
Objekte werden als Garbage (Müll) bezeichnet, da auf sie nicht mehr zugegriffen
werden kann. Bei Java läuft im Hintergrund eine Speicherbereinigung (Garbage
Collection), die den Speicherplatz automatisch wieder freigibt.
Manche Spielsteine
können Kugeln als
Last tragen.
Sichtbarkeit:
Token Klassenname
− private − xPos: int
# protected − yPos: int Attribute
~ package
+ public + moveTo() Methoden
abgeleitet von
(extends)
Carrier
geerbte Attribute
− load: int und Methode
+ addLoad() werden nicht
wiederholt
Beispiel erbt die Klasse Elektrogerät von der Klasse Gerät, vererbt aber an
Waschmaschine und Trockner:
Gerät
+ benutzen()
ElektrischesGerät
+ angeschaltet: boolean
Waschmaschine Trockner
+ benutzen() + benutzen()
Wenn wir die genaue Implementierung der Methoden weglassen, könnte der
dazugehörige Code so aussehen:
class Geraet {
public void benutzen() {};
}
Allerdings würde das Programm mit oder ohne Implementierung nichts tun,
denn die Waschmaschine ist ausgeschaltet. (boolean wird mit false initiali-
siert). Die Klassen könnten auch public deklariert werden. Dann müsste aller-
22 Kapitel 2 Konzepte
dings jede Klasse in einer eigenen Datei stehen, die denselben Namen wie die
Klasse hat.
In Java gibt es Einschränkungen in den Kombinationsmöglichkeiten der Ver-
erbung: Eine Klasse kann nur eine (direkte) Oberklasse besitzen. Das heißt
folgendes Szenario (Mehrfachvererbung) ist nicht erlaubt:
Gerät
+ benutzen()
ElektrischesGerät
+ angeschaltet: boolean
Waschmaschine Trockner
+ benutzen() + benutzen()
Waschtrockner
Der Grund ist folgender: Würde eine Klasse von zwei Oberklassen abgeleitet,
die eine Methode unterschiedlich implementieren, so wäre unklar welche Imple-
mentation vererbt wird. Welches benutzen() sollte Waschtrockner erben? Wir
werden später Schnittstellen (Interfaces) kennenlernen, die einen Mechanismus
für Mehrfachvererbung bieten, allerdings nur für Schnittstellenvorgaben, nicht
für Implementierungen. Es gibt noch eine weitere Einschränkung: das Stichwort
final in einer Klassendeklaration verbietet die Ableitung von Unterklassen. Auf
diese Weise kann z. B. aus Sicherheitsgründen das Überschreiben von Methoden
verhindert werden.
2.3 Polymorphismus
Polymorphismus (Vielgestaltigkeit) bezeichnet in der Biologie die Variation
(individuelle Unterschiede) innerhalb einer Population. In der Programmierung
ist es das Konzept, dass ein Bezeichner (Variable, Operator, Methode) Kontext-
abhängig unterschiedliche Datentypen annehmen kann. Durch die Möglichkeit
der Vererbung und damit z.B. des Überschreibens von Methoden ist der Polymor-
phismus eines der Grundkonzepte von Java, auf welches wir auch bereits viele
Male zurückgegriffen haben, ohne es zu benennen. Das betrifft insbesondere
die erste Form des Polymorphismus, die wir uns jetzt anschauen: Im Ad-hoc
Polymorphismus können Operatoren und Methoden mit unterschiedlichen
Signaturen überladen werden und abhängig von den Datentypen der Parameter
2.3 Polymorphismus 23
Es gibt noch eine weitere Form des Ah-Hoc Polymorphismus, nämlich die
Implizite Typumwandlung ((ad-hoc) coertion polymorphism). Dabei werden die
Daten eines ‘kleineren’ Datentyps automatisch in einen ‘größeren’ Datentyp
umgewandelt (widening conversion), wenn der Kontext es erfordert (z. B. int
nach double). Die andere Richtung (narrowing conversion) geht in Java nur durch
explizites Casting und zählt somit nicht zu Polymorphismus. Die zwei anderen
großen Gruppen des Polymorphismus sind der parametrische Polymorphismus
und der Subtyppolymorphismus. Im parametrischen Polymorphismus können
Datentypen und Methoden Argumente variablen Typs haben. Das ist in Java mit
Generics umgesetzt, welche in Abschnitt 2.4.2 besprochen werden. Im Subtyp
Polymorphismus können Objekte den Typ ihrer Oberklasse annehmen. Eine
Methode, die als Argument ein Objekt des Typs T erwartet, kann auch mit einem
Objekt des Types S aufgerufen werden, wenn S eine Unterklasse von T ist:
class T { } // Klasse T
class S extends T { } // Subklasse S
class X {
static void method(T t)
{
System.out.println("Hash of " +
t + " is " + t.hashCode());
}
}
class SubtypPolymorphismDemo {
// ...
T t = new T();
S s = new S();
// Aufruf nach Signatur für T Objekt:
X.method(t);
// Durch Polym. auch für S Objekt:
X.method(s);
}
}
Hier ist X.method() eine statische Methode. Das Prinzip gilt genauso, wenn es
eine Objektmethode wäre.
24 Kapitel 2 Konzepte
2.4 Abstraktion
Im Prinzip der Abstraktion werden Konzept und Implementierung getrennt.
Es wird gezeigt, was gemacht wird, ohne zu klären, wie es gemacht wird. Es
wird keine Implementation vorgegeben, nur ein Umriss einer Funktion, ohne
irgendwelche Details anzugeben. Man sagt auch, dass die Details versteckt
werden.
I Jede abgeleitete Klasse muss sich bei der Implementation an die vorgegebene
Signatur halten.
Wenn eine abstrakte Klasse ausschließlich abstrakte Methoden hat, wird sie auch
rein abstrakte Klasse gennant, andernfalls partiell abstrakte Klasse.
Bevor wir nun Abstrakte Datentypen betrachten, werden wir einmal Generics
einführen.
Störend bei dem Array ist, dass die Anzahl der Spielsteine festgelegt werden
muss. Eine dynamische Veränderung (Löschen und Hinzufügen von Spielstei-
nen) ist nicht direkt möglich. Dies Problem ließe sich zwar auch mit Arrays lösen,
aber aus Gründen, die im letzten Semester besprochen wurden, bevorzugen wir
eine elegantere Lösung, z.B. mit einem Stapel basierend auf einer verketteten
Liste. Im letzten Semester wurden Stapel/Listen von int Werten behandelt,
2.4 Abstraktion 25
nun brauchen wir Stapel/Listen von Objekten, hier für die Klasse Token. Zur
Erinnerung: Stapel funktionieren nach dem LIFO (last in first out) -Prinzip.
Damit generelle Datenstrukturen wie Stapel nicht für jeden Objekttyp neu pro-
grammiert werden müssen, gibt es in Java das Konzept der generischen Typen
(Generics). Man kann Klassen definieren, bei denen der Typ einer Variablen selbst
variabel ist, eine Typvariable oder formaler Typparameter (formal type parame-
ter). Dazu schreibt man bei der Klassendeklaration die Typvariable in spitzen
Klammern hinter den Klassennamen, z.B. <E>. Konvention ist dabei: einzelne
Großbuchstaben, z.B. “T” für Typ (allgemein), “E” für Element, “K” für Schlüs-
sel, “V” für Wert. Dann kann die Typvariable wie eine normale Typbezeichnung
benutzt werden. Das heißt beispielsweise statt Token, einer spezifischen Typbe-
zeichnung, wird E, eine Typvariable, benutzt. E ist hierbei nur ein Platzhalter, E
ist keine Klasse und hat auch keinen Konstruktor, sie müssen mit einer spezifi-
schen Typbezeichnung ersetzt und damit instanziert werden.
Dabei gilt es zu beachten, dass Typparameter nur als Referenztypen instanzi-
iert werden können. Daher gibt es für die primitiven Datentypen so genannte
Wrappertypen, nämlich Boolean, Integer, Short, Long, Double, Float, Byte,
Character für boolean, int, short, long usw. Das automatische Casting eines
primitiven Typs auf den entsprechenden Referenztypen nennt man autoboxing,
das Casting zurück unboxing
Nun schauen wir uns einmal die Implementierung eines Stapels mit und ohne
Generics an. Zunächst ohne Generics (nur für Token):
public class TokenStack
{
private Node head;
Dies ist aber nur für die Klasse Token verwendbar. Wenn man Generics benutzt,
ist die Klasse Stack allgemein verwendbar:
‘Allgemein verwendbar’ heißt, dass man statt der Typvariable E jeden beliebigen
Referenztyp einfügen kann, also auch Token:
2.4.3 Datenabstraktion
Ein Datentyp ist die Kombination aus einer Wertemenge und Operationen. Bei
dem primitiven Datentyp int z. B. sind das die ganzen Zahlen von −231 bis
231 − 1 und die Rechenoperationen, Vergleichsoperationen etc. Abstrakte Daten-
typen (abstract data type; ADT) sind Datentypen und damit ebenfalls eine Kom-
bination aus Wertemengen und Operationen. Aber der Zugriff auf (Teile der)
Wertemenge erfolgt nur über festgelegte Operatoren. Diese Festlegung ist die
Spezifikation, mit der wir uns im nächten Abschnitt befassen werden. Das führt
dazu, dass die Nutzer- und Implementationssicht getrennt sind (man kann den
Datentyp verwenden, ohne die Implementierung zu kennen). ADTs sind dem-
nach durch ihre Semantik (Perspektive der Benutzerin; ‘was?’) gekennzeichnet.
Daraus ergeben sich ein paar wichtige Vorteile:
I Kapselung (encapsulation): Nutzer- und Implementationssicht (was und wie)
sind getrennt. Der client-code kann sich auf die ADT Beschreibung verlassen,
und braucht keine Kenntnis über die Implementation.
2.4.4 Schnittstellen
Schnittstellen haben weder Datenfelder noch Konstruktoren. Sie können aller-
dings Konstanten definieren. Bei Methoden wird nur die Signatur definiert.
Implementationen sind in Schnittstellen generell nicht möglich, es handelt sich
also immer um abstrakte Methoden.
Eine Klasse „erbt“von einer Schnittstelle mit dem Schlüsselwort implements,
was auch als Schnittstellenvererbung (subtyping) bezeichnet wird. In diesem
Fall muss die Klasse alle Methoden der Schnittstelle Signatur-konform imple-
mentieren. Viele Klassen können dieselbe Schnittstelle implementieren. Aber im
Unterschied zur Vererbung von Klassen und abstrakten Klassen (subclassing),
kann eine Klasse mehrere Schnittstellen implementieren. Es können auch Schnitt-
stellen von Schnittstellen abgeleitet werden, mit dem Schlüsselwort extends.
KAPITEL 3
Datenstrukturen
Was ein Iterator leisten muss, ist in der Schnittstelle Iterator festgelegt:
Das klingt zunächst etwas kompliziert, ist aber in der Anwendung sehr prak-
tisch.
Wenn eine Klasse die Schnittstelle Iterable implementiert, kann man mit einer
for Schleife einfach über die Elemente iterieren.
Lautet also die Deklaration unserer Stack Klasse (siehe 2.4.2, bzw. 3.1)
public class Stack<E> implements Iterable
dann ist dadurch festgelegt, dass wir folgendermaßen elegant und einfach über
unsere Tokenliste iterieren können:
29
30 Kapitel 3 Datenstrukturen
Da die Klasse die Iterable Schnittstelle erbt, brauchen die geerbten Metho-
den nicht explizit in der API erwähnt zu werden, sie müssen aber durchaus
implementiert werden. Die Implementierung der Klasse Stapel wird hier mit
der Implementierung der Methode iterator() und der Klasse ListIterator
diesbezüglch erweitert:
public E pop() {
E item = head.item;
head = head.next;
N--;
return item;
}
Interface. Das Beispiel dient trotzdem nur der Illustrierung der Interfaces
Iterable und Iterator. Eine saubere und vollständige Implmentation von
Stapeln findet man hier: https://algs4.cs.princeton.edu/code/edu/princeton/cs/
algs4/Stack.java.html
Da die Klasse die Iterable Schnittstelle erbt, brauchen die geerbten Methoden,
wie bereits bei Stapeln beschrieben, nicht explizit in der API erwähnt zu werden.
import java.util.Iterator;
public class Queue<E> implements Iterable<E>
{
private Node head;
private Node tail;
private int N;
public E dequeue()
{ // Entfernt das Element vom Anfang der Schlange
E item = head.item;
head = head.next;
if (--N == 0) // Abfrage auf leer fehlt, siehe Bemerkung unten
tail = null;
return item;
}
wastail.next = tail;
}
Achtung: Es gelten die Hinweise wie bei der Stapel Implementation, siehe 3.1.
Insbesondere fehlen essentielle Überprüfungen, z.B. am Anfang von dequeue()
ob die Schlange leer ist.
Der Zugriff auf Elemente erfolgt nur über den Iterator. Die Implementation kann
exakt von Stapel übernommen werden, wobei pull() weggelassen und push()
in add() umbenannt wird. Daher könnte natürlich auch ein Stapel an Stelle
einer Bag benutzt werden.
Der Zusatz worst case bei der Laufzeit ist insbesondere eine Abgrenzung zu einer
amortisierten Laufzeit, siehe Seite 13 des AlgoDat Skriptes, bei der die Operationen
manchmal eine längere Laufzeit haben, z. B. wenn nach einer gewissen Anzahl
von Einfügungen ein Array vergrößert und der Inhalt kopiert werden muss.
Bei den Java Klassen Stack und Queue ist zu beachten, dass diese auch Methoden
implementieren, die keine konstante sondern eine lineare Laufzeit besitzen, wie
remove().
Altertnativ gibt es noch die Varianten ArrayList und ArrayDeque, die ähnliche
Funktionalität mit Laufzeiten in derselben Wachstumsordnung zur Verfügung
stellen. ArrayList bietet den Vorteil der direkten Indizierung und ArrayDeque
ist ansonsten die schnellste Variante innerhalb der jeweiligen Wachstumsord-
nung. Allerdings haben Einfügungen bei beiden Array Varianten nur amortisiert
36 Kapitel 3 Datenstrukturen
I compare(v, w) == -compare(w, v)
I Aus compare(u, v) < 0 und compare(v, w) < 0 folgt compare(u, w) < 0.
3.5 Die Schnittstellen Comparable und Comparator 37
Implementationsbeispiel Comparator
Wir betrachten nun folgende Beispielklasse Person:
import java.util.ArrayList;
Für diese Klasse gibt es mehrere Möglichkeiten, nach denen verglichen werden
könnte, nämlich nach Namen, nach Alter und nach Größe. Dafür können wir
drei Klassen schreiben, die jeweils Comparator implementieren. Da sie den Vor-
gaben der Schnittstelle folgen, muss jeweils die Methode compare implementiert
38 Kapitel 3 Datenstrukturen
werden. Hierbei nutzen wir aus, dass die Klasse Integer, String und Double
bereits eine compare-Methode implementiert haben.
import java.util.Comparator;
/* Die Comparator könnten auch als anonyme Klassen
im Aufruf implementiert werden.
siehe Beispiele im git in Material/Code/Lecture03
*/
public class SortByAge implements Comparator<Person> {
public int compare(Person person1, Person person2) {
return Integer.compare(person1.age, person2.age);
}
}
personen.sort(new SortByAge());
System.out.println("Sorted by Age:\n" + personen);
personen.sort(new SortByName());
System.out.println("Sorted by Name:\n" + personen);
personen.sort(new SortByHeight());
System.out.println("Sorted by Height:\n" + personen);
personen.sort(new SortByHeight().reversed());
System.out.println("Descending by Height:\n" + personen);
}
3.5 Die Schnittstellen Comparable und Comparator 39
Sorted by Age:
[(Peter, 80y, 175.8cm), (Paul, 81y, 178.7cm), (Mary, 82y, 177.2cm)]
Sorted by Name:
[(Mary, 82y, 177.2cm), (Paul, 81y, 178.7cm), (Peter, 80y, 175.8cm)]
Sorted by Height:
[(Peter, 80y, 175.8cm), (Mary, 82y, 177.2cm), (Paul, 81y, 178.7cm)]
Descending by Height:
[(Paul, 81y, 178.7cm), (Mary, 82y, 177.2cm), (Peter, 80y, 175.8cm)]
Implementationsbeispiel Comparable
Alternativ kann man sich auch auf eine Vergleichsmethode festlegen und die
Klasse Person selbst als Implementation der Schnittstelle Comparable schreiben.
4.1.1 Debugging
Bei der Korrektur von insbesondere logischen Fehlern ist ein Debugger eine
immense Hilfestellung. Im Debugger kann das Programm dann Zeilenweise
ausgeführt und Variableninhalte inspiziert werden.
Wir betrachten nun im speziellen den Debugger von IDEA. Falls ein Programm
mit einer Exception (siehe Abschnitt 4.1.2) abbricht, kann einfach der Debugger
gestartet werden, und er wird bei der verursachenden Zeile stehen bleiben. Läuft
das Programm durch, liefert aber nicht das gewünschte Ergebnis, setzt man
einen Breakpoint und startet dann den Debugger. In IDEA kann man dann den
Programmablauf mit F8, F7, Shift+F8 und Alt+F9 steuern sowie mit weiteren
Shortcuts oder Schaltknöpfen im Debugger Fenster. Im ’Variables’ Fenster des
Debuggers kann der Inhalt komplexerer Variable durch Klicken auf das Dreieck
aufgeklappt werden. Durch Klicken auf ‘+’ kann man arithmetische Ausdrücke
als watch hinzufügen.
40
4.1 Umgang mit Fehlern und Ausnahmen 41
Fehlervermeidung
Es gibt unterschiedliche Methoden, um Fehler und unerwartetes Verhalten zu
vermeiden. Zum einen sind APIs und Dokumentation bei der Verwendung
von vorhandenen Implementationen, wie abstrakten Datentypen, hilfreich bei
der Vermeidung von Fehlern. Schnittstellen und abstrakte Methoden machen
gewisse Vorgaben, die ebenfalls zur Vermeidung von Fehlern beitragen können.
Das Testen von Code ist eine weitere Strategie zur Vermeidung von Fehlern,
speziell JUnit Tests werden dafür häufig verwendet. Mit ihnen werden wir uns
im Laufe dieses Abschnittes noch beschäftigen. Das als nächstes vorgestellte
Modell Design-by-Contract ergänzt diese Verfahren.
Programmiermodel Design-by-Contract
Gemäß dem Programmiermodel Design-by-Contract (dt. Entwurf gemäß Vertrag)
sieht vor, dass für jede Komponente einer Software, präzise und nachvollziehbare
Bedingungen formuliert werden, die diese Komponente erfüllen muss. Die Idee
basiert auf einem Vertrag zwischen den einzelnen Softwarekomponenten, sodass
wenn eine Methode aufgerufen wird, die aufrufende Methode genau weiß, was
zu erwarten ist. Dabei wird für jede Methode definiert:
I Vorbedingungen (preconditions): Bedingungen, die der Client beim Aufruf
einhalten muss.
4.1.2 Ausnahmenbehandlung
Trotz aller Bemühungen Fehler zu vermeiden, viele Fehler lassen sich erst zur
Laufzeit feststellen.
In C zeigen Funktionen Fehler die, z.B. durch den Aufruf mit unzulässigen
Parametern verursacht werden durch einen speziellen Rückgabewert an, z.B. -1,
0 oder NULL. Damit liegt die Verantwortung, Fehler zu behandeln ganz in der
Verantwortung der Programmierenden.
In Java können Methoden bei Ausnahmen und Fehler eine exception auslösen,
bzw. ‘werfen’ (throw). Die aufrufende Methode kann Exceptions mit try ...
42 Kapitel 4 Gute Praxis
Falls die Eingabe nicht in einen int umgewandelt werden kann, bricht die
Methode Integer.parseInt() mit einer NumberFormatException ab. Wäh-
rend mit einem try... catch-Block das Programm nicht abbricht und stattdessen
den Fehler abfängt und mit ihm umgeht:
Wie bereits erwähnt können Exceptions auch von einer Methode geworfen wer-
den, sodass ein Programm nicht vollständig durchgeführt wird, wenn bereits
ein Fehler gefunden wurde. Dazu ergänzen wir unser Programm mit der Bedin-
gung, dass eine Zahl in einem bestimmten Intervall einzugeben ist. Wenn diese
Bedingung nicht erfüllt ist, werfen wir eine InputMismatchException.
4.1.3 Assertionen
Mit einer Assertion (assertion) kann angezeigt werden, dass an der entspre-
chenden Stelle im Programmcode die angegebene Bedingung erfüllt sein muss.
Damit können mit Assertionen frühzeitig Bedingungen abgefangen werden, die
später zu Ausnahmen führen (würden) und eine aussagekräftige Reaktion des
Programmes hervorrufen. Dadurch sind Assertationen dazu geeignet, wichtige
Aspekte des Design-by-Contract umzusetzen (siehe Abschnitt 4.1.2), denn so
lassen sich Vor- und Nachbedingungen im Code sicherstellen. Die von Assertio-
nen ausgelösten Ausgaben werden immer angezeigt, d.h. sowohl bei positiven,
44 Kapitel 4 Gute Praxis
als auch bei negativem Ausgang, wenn man das Programm in dem Modus
-enableassertions ausführt, siehe auch S. 45.
Als Anwendung einer Assertion gehen wir zurück zur unserem Beispielspiel.
Darin soll die Methode moveTo() der Token Klasse sicherstellen, dass die Ziel-
position innerhalb des Spielfeldes liegt. Bisher kennt die Token Klasse allerdings
die Größe des Spielfeldes gar nicht. Daher führen wir noch eine Board Klasse ein.
Das Spielbrett (Board) enthält einen Stapel von Spielsteinen (Token) als Attribut.
Außerdem hat jeder Token das Board als Attribut. So hat jeder Spielstein Infor-
mation über die Brettgröße und moveTo() kann mit Assertionen sicherstellen,
dass der Stein nicht vom Brett gezogen wird.
yPos= y;
}
}
Dabei ist zu beachten, dass in der Grundeinstellung Assertionen bei der Ausfüh-
rung ausgeschaltet sind. Mit der Option -ea bzw. -enableassertions werden
sie in der JVM eingeschaltet. In IDEA gibt man dies bei VM Options unter Run |
Edit Configurations ... an.
Man kann mit Assertionen auch eigene Annahmen über den Zustand in einer
bestimmen Codezeile überprüfen. Schauen wir uns folgendes Beispiel an:
if (i % 3 == 0) {
// ...
} else if (i % 3 == 1) {
// ...
} else {
assert i % 3 == 2 : i;
// ...
}
In dem Beispiel scheint die assert Bedingung sicher erfüllt zu sein. Sie stimmt
aber für i<0 nicht, da % der Divisionsrest ist und immer das Vorzeichen des
Dividenden annimmt.
Besonders bei komplexeren Fallunterscheidungen (if.. else oder switch) kann
ein assert hilfreich sein. Der Code im letzten else-Block hier könnte z.B. bei
Änderungen in den oberen if-Bedingungen auch ungültig werden.
46 Kapitel 4 Gute Praxis
Wie JUnit Testing in IDEA funktioniert, können Sie sich in dem Video unter
diesem ISIS Link anschauen.
Wir schauen uns nun ein Beispiel an, welches Sie unter https://algs4.cs.princeton.
edu/code/edu/princeton/cs/algs4/Stack.java.html finden können. Wir betrachten
eine Dokumentation einer Klasse Stack. Die Beschreibung der Klasse gibt an,
was die Klasse implementiert, welche Datenformate benutzt werden und wel-
che Parameter im Konstruktor gebraucht werden (@param). Sie steht vor der
Deklaration der Klasse:
/**
* The {@code Stack} class represents a last-in-first-out (LIFO) stack of generic items.
* It supports the usual <em>push</em> and <em>pop</em> operations, along with methods
* for peeking at the top item, testing if the stack is empty, and iterating through
* the items in LIFO order.
* <p>
* This implementation uses a singly linked list with a static nested class for
* linked-list nodes. See {@link LinkedStack} for the version from the
* textbook that uses a non-static nested class.
* See {@link ResizingArrayStack} for a version that uses a resizing array.
* The <em>push</em>, <em>pop</em>, <em>peek</em>, <em>size</em>, and <em>is-empty</em>
* operations all take constant time in the worst case.
* <p>
* For additional documentation,
* see <a href="https://algs4.cs.princeton.edu/13stacks">Section 1.3</a> of
* <i>Algorithms, 4th Edition</i> by Robert Sedgewick and Kevin Wayne.
*
* @author Robert Sedgewick
* @author Kevin Wayne
*
* @param <Item> the generic type of an item in this stack
*/
public class Stack<Item> implements Iterable<Item> {
// ...
// two exemplary methods of class Stack (taken from Sedgewick & Wayne as indicated below)
/**
* Adds the item to this stack.
*
* @param item the item to add
*/
public void push(Item item) {
Node<Item> oldfirst = first;
first = new Node<Item>();
first.item = item;
first.next = oldfirst;
n++;
}
/**
* Removes and returns the item most recently added to this stack.
*
* @return the item most recently added
* @throws NoSuchElementException if this stack is empty
*/
public Item pop() {
if (isEmpty()) throw new NoSuchElementException("Stack underflow");
Item item = first.item; // save item to return
first = first.next; // delete first node
n--;
return item; // return the saved item
}
Wenn man sich die generierte HTML Dokumentation dann anschaut, sieht sie so
aus:
Quellenverweise
Für die Einführung in Java wurden Informationen aus vielen Quellen geschöpft,
insbesondere aus [Ullenboom, 2018], [Sedgewick and Wayne, 2014] und etwas
aus [Vornberger, 2016]. Des Weiteren wurde die folgenden Webseiten verwendet:
I https://javapapers.com/core-java/java-history
I https://introcs.cs.princeton.edu/java/faq/c2java.html
I https://de.wikipedia.org/wiki/Klassendiagramm
I https://javapapers.com/core-java/java-polymorphism
I https://en.wikipedia.org/wiki/Abstract_data_type
I https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html
I https://en.wikipedia.org/wiki/Abstract_data_type
I https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html
I https://www.javatpoint.com/collections-in-java
I https://docs.oracle.com/javase/8/docs/api/java/util/LinkedList.html
I https://en.wikipedia.org/wiki/Javadoc
50 Kapitel 4 Gute Praxis
I http://www.oracle.com/technetwork/java/javase/tech/index-137868.html
I https://algs4.cs.princeton.edu/code/edu/princeton/cs/algs4/Stack.java.html
Abbildungsverzeichnis
Listings
51
Index
52
Index 53
integrity, 27 Paket, 14
interface, 27 Polymorphismus, 22
Iterable, 29, 33 postconditions, 41
Implementation, 30 preconditions, 41
Iterator, 29 primitive data types, 5
Primitiver Datentyp, 5, 25
Javadoc, 46 PriorityQueue, 32
JUnit Tests, 46 private, 14
protected, 14
Kapselung, 26
public, 14
Klasse, 10, 11
abstrakte, 24 Queue, 32
Klassenhierarchie, 14, 19, 22 Implementation, 33
Klassenmethoden, 12
Klassenvariablen, 12 Referenztyp, 16
Kommentare, 46 Referenztypen, 16, 25
Konstruktor, 12
Verkettung, 12 Schnittstelle, 27
Anwendungsprogrammierung, 27
Laufzeit Vererbung, 28
ArrayDeque, 35 semantische Gleichheit, 18
ArrayList, 35 Set, 32
LinkedList, 35 Sichtbarkeit
Queue, 35 von Variablen, 14
Stack, 35 side effects, 41
LinkedList, 32, 35 Sie stimmt aber für i<0 nicht, 45
List, 32 Speicherbereinigung, 18
Stack, 29, 32
Methode, 12 Implementation, 30
abstrakte, 24 Stapel, 24
Modelle, 10 Implementation, 30
Modellierung, 10 static, 12
modularity, 26 subclassing, 19, 28
Modularität, 26 Subtyp Polymorphismus, 23
Modultests, 46 subtyping, 28
narrowing conversion, 23 super, 18
new, 12, 16 syntaktische Gleichheit, 18
try, 41
Typparameter
formaler, 25
Typvariable, 25
überschreiben, 19
UML, 20
unboxing, 25
Unified Modeling Language, 20
unit tests, 46
Unterklasse, 14, 19
Variable
Sichtbarkeit, 14
Vereinheitlichte Modellierungsspra-
che, 20
Vererbung, 14, 19, 20, 22
Warteschlange
Implementation, 33
widening conversion, 23
Wrappertypen, 25
Zuweisung, 16
Literaturverzeichnis
[Ullenboom, 2018] Ullenboom, C. (2018). Java ist auch eine Insel. Rheinwerk
Computing, 13 edition.
55