Strukturiert Programmieren Lernen Mit Java
Strukturiert Programmieren Lernen Mit Java
Strukturiert Programmieren Lernen Mit Java
(Grundkonzepte)
www.programmieraufgaben.ch
9
Vorwort
Das Buch «Programmieren lernen» [GFG11] (S. Literaturliste auf Seite 197) ist weitgehend un-
abhängig von einer Programmiersprache. Glauben Sie jedoch nicht, dass Sie ohne eine Program-
miersprache des Programmierens mächtig werden; Sie lernen auch nicht Auto fahren, indem Sie
Fahrstunden ohne Wagen absolvieren; welche Automarke bzw. welche Programmiersprache Sie
dazu auswählen ist jedoch sekundär.
Damit die Übungen aus [GFG11] fürs Java-Selbststudium durchführbar sind, ist eine minimale
Einführung in Java nötig. Dies wird hiermit zur Verfügung gestellt.
Diese Java-Einführung zeigt diejenigen Syntaxelemente, welche es braucht, um einfache struk-
turierte Programme zu schreiben. Damit sollten die meisten Aufgaben aus
http://www.programmieraufgaben.ch lösbar sein.
Alles wird neu Die Grundkonzepte der Programmierung sind für Lehrlinge wie auch für alte
Hasen gleichzeitig unerlässlich. Da ich sowohl Anfänger wie auch Profis unterrichte, behaupte
ich folgendes: Je besser die Grundlagen sitzen, umso eher ist man in der Lage, sich auf Neues
einzustellen. Wer vorwiegend mit «copy & paste» programmiert1 und sich dann zufrieden gibt,
wenn «es läuft» und dabei nicht versucht, zu verstehen was wirklich passiert, hat wenig Chancen
weiterzukommen. Schlimmer: Solche Programmierer werden früher oder später von einer neuen
Technik überholt und ausrangiert. Dieses Buch richtet sich an Anfänger, die das Kunsthandwerk
der Computerprogrammierung von Grund auf lernen wollen, ebenso wie an langjährige Querein-
steiger, die nie die Gelegenheit hatten, sich mit den Grundlagen moderner Programmiersprachen
auseinander zu setzen und gleichzeitig interessiert sind, eine für sie neue Sprache (hier Java)
kennen zu lernen.
Ich kann mit meinen gut 30 Jahren Programmiererfahrung2 wohl noch nicht behaupten, dass
ich zu den wirklich alten Hasen gehöre. Etwas habe ich dennoch erfahren: Programmiersprachen
kommen und gehen. Von Assembler bis hin zu Python und Grails habe ich gesehen, dass alles im
Wandel ist. Doch etwas Wichtiges habe ich auch gelernt: Die Grundkonzepte von Program-
miersprachen haben sich nicht verändert! Auch heute denke ich oft noch in Anweisungen,
Variablen (Attributen und Entitäten), Selektionen, Iterationen, Methoden und Schnittstellen. Die
Sprachen der 3. Generationen (BASIC, C, Java) wurden immer mächtiger3 , die Fehlerquellen
minimiert, die Entwicklerwerkzeuge immer hilfreicher, die Fehlersuche immer ausgeklügelter; doch
die Grundlagen blieben dieselben. Ich versuche mit diesem Buch dem Leser diejenigen Konzepte
beizubringen, von denen ich überzeugt bin, dass sie noch einmal 40 Jahre überdauern werden.
1
Mit «copy & paste» zu arbeiten, ist im Übrigen nicht programmieren.
2
Die ersten Programme schrieb ich 1982.
3
Einige Sprachen sind hier ausgenommen. Sprachen höherer Generationen wie SQL und PROLOG haben
ihre eigenen speziellen Konzepte. Dennoch bin ich überzeugt, dass die Grundlagen von 3.-Generationsprachen
unerlässlich sind, um das Handwerk der Computerprogrammierung wirklich zu verstehen.
So kann ich meinen Vorsprecher nur unterstützen in der Aussage, indem ich hier den ersten Insider
Tipp abgebe.
Geek Tipp 1
Wenn Du nicht wirklich verstanden hast, warum ein Programm läuft, dann läuft es nichta !
a
Warum sollte es dann nicht laufen? Natürlich läuft es; für einen Moment. Oft aber nur solange wir als Program-
mierer gerade dabei sind, dieses Programm zu testen. Sobald unser Programm in eine andere Umgebung kommt,
so ist ein Versagen gut möglich. Wenn wir jetzt versuchen, das Programm zu reparieren, oder noch schlimmer,
jemand anders versucht es zu verstehen und danach zum Laufen zu bringen, treten die wirklichen Probleme auf:
Das Programm ist nicht mehr wartbar!
In obigem Sinne möchte ich den Leser anweisen, mindestens zu versuchen, den ersten Teil dieses
Skripts (bis und mit Kapitel 7 «Zeichenketten» auf Seite 93) wirklich zu verstehen. Natürlich sind
wir nach dem Erlernen der Grundlagen noch lange keine guten Programmierer. Wir haben jedoch
das beste Werkzeug in der Hand, alle darauf aufbauenden Konzepte schneller zu erfassen und
mit sicherem Gefühl anwenden zu können. Sollte später etwas nicht so funktionieren, wie wir das
erhofft haben, so haben wir immer noch die Grundlagenwerkzeuge, die uns (meist) ermöglichen,
das entstandene Problem selbst zu lösen, oder aber gekonnt zu umgehen.
11
Und was heißt bitte schön «objektorientiert»?
Strukturiert programmieren mit Java? Geht das überhaupt? Java sei doch eine objektori-
entierte Sprache? Ja, auch für Dich, der schon viel mehr weiß, habe ich einen Antwortversuch
bereit:
Nun, formulieren wir es einmal so. Mit einer objektorientierten Sprache wie Java kann man
unten anderem:
• test-orientiert programmieren
• service-orientiert programmieren
• objektorientiert programmieren
• strukturiert programmieren
• unstrukturiert programmieren
• fehleranfällig programmieren
Die Art des Vorgehens hat nichts mit der Sprache zu tun. Auch wenn es in Assembler oder C viel
schwieriger ist, objektorientiert vorzugehen, so ist dies sehr wohl möglich. Jede objektorientierte
Sprache ist aber auch eine prozedurale Sprache und somit sind alle Konzepte, welche für die
strukturierte Programmierung nötig sind in Java auch vorhanden. Genau genommen sind mittels
return , break und continue sogar unstrukturierte Vorgehensweisen in Java zugelassen4 .
Ob es nicht sinnvoller wäre, in Java direkt objektorientiert mit der Kunst des Programmierens
einzusteigen, ist eine didaktische Grundsatzdiskussion. Es geht beides. Begonnen beim «Bit», wie
in diesem Buch, oder direkt begonnen bei «Fenstern», «Knöpfen» und der «Ereignisverarbeitung».
Wer schnell ein Ergebnis auf dem Schirm sehen will, ist mit diesem Lehrmittel wohl nicht gut
bedient. Wer jedoch möglichst alle Grundlagen kennen lernen will, fährt besser mit der in die-
sem Buch vorgeschlagenen «bottom up»-Methode; also derjenigen Vorgehensweise, bei der man
vom innersten Kern einer Programmiersprache (Datenstrukturen, Variable, Sequenzen, Ein- und
Ausgabe, ...) sich langsam in die große Welt der Computerprogrammierung hineinarbeitet.
Ein Gerüst, um in Java weitgehend strukturiert zu programmieren findet sich im Anhang
(s. Kap. A.4 auf Seite 137).
Aufruf Hier noch ein Aufruf an Programmierer anderer Sprachen: Bitte stellt uns solche Ein-
führungen auch für andere Sprachen zur Verfügung. Python ist der nächste Kandidat. C , Ruby
und VisualBASIC werden auch oft gewünscht. Wer sich an einer «Übersetzung» beteiligen will,
soll sich doch bitte mit mir in Verbindung setzen: Philipp Gressly Freimann ( [email protected] ).
4
Es sind sogar mittels else-if innerhalb einer Endlosschleife while mit einer «jump»-Variable beliebige
Sprünge denkbar. Somit braucht es gar keine Spezialbehandlungen, um mit einer «strukturierten»-Sprache eben
«nicht strukturiert» zu programmieren.
13
| Ein erstes Java-Programm
Bevor wir mit den Themen (also den Kapiteln aus [GFG11]) beginnen, möchte ich alle Leser
ermuntern, ein erstes Java-Programm zu schreiben5 :
public class MeinErstes {
public static void main ( String [] args ) {
new MeinErstes (). top ();
}
void top () {
System . out . println ( " Hallo ␣ Welt " );
}
}
Eine detaillierte Installationsanleitung und Tipps zur Fehlersuche finden Sie im Anhang (s. Kap.
A.1 auf Seite 129). Ein generisches strukturiertes Java-Programm ist im Kapitel über Abfolgen
zu finden (s. Kap. A.4 auf Seite 138).
Bemerkung
Die Anweisung System.out.println(...) bezeichnet in Java die Ausgabe einer Textzeile auf
der Konsole.
5
«Didaktischen Hinweis für Lehrer» Es ist natürlich genauso gut möglich, auf die schlecht erklärbare Instanzie-
rung new MeinErstes().top(); zu verzichten und danach bei jeder Subroutine und bei jeder globalen Variable
das Java-Schlüsselwort static anzufügen. Dies kann im Unterricht jedoch zu Fragen führen, die noch nicht
beantwortet werden können, denn das Gegenteil (nämlich der dynamische Code) wird – aus Zeitgründen – ja
allenfalls gar nie erklärt werden können.
Egal, wie wir es in Java drehen und wenden; in Java müssen wir uns immer auf die eine oder andere Weise
mit Objekten beschäftigen.
Wenn wir nun den Lernenden einfach sagen, dass obiger Starter (oder aber das Schlüsselwort static vor jeder
Funktion und vor jeder globalen Variable) einfach immer dazu gehört, so haben wir kritische Fragen nur von
denjenigen Kursteilnehmern, welche sich sowieso schon vertieft um die Materie gekümmert hatten, und für diese
kleine Gruppe ist eine Erklärung dann rasch gegeben.
15
1 | Ausdrücke und Datentypen
1.1 Ausdrücke und Operatoren
Beginnen wir doch gleich mit demselben Ausdruck wie in [GFG11]. Nur, dass wir diesmal den
Java-Code angeben6 :
Ein zusammengesetzter Ausdruck besteht stets aus einfacheren Ausdrücken. Die Grundbausteine
(Literal, Variable, Funktionsaufruf und Operator) werden ab der nächsten Seite erklärt.
6
Im Beispiel ist die Funktion len() keine Standardfunktion von Java! Dieses Beispiel zeigt lediglich den
Aufbau eines Ausdrucks.
17
1 Ausdrücke und Datentypen
1.1.1 Ausdrücke
Ausdrücke werden in vier Kategorien eingeteilt. Im Wesentlichen kann man es sich folgender-
maßen merken. Ein «Ausdruck» ist etwas, was einen Wert hat. Werte werden fast ausschließlich
einer Variable zugewiesen: Entweder geschieht dies mit einer Zuweisung («=» in Java) oder als
Argument in einem Subroutinen-Aufruf7 .
Ausdrücke sind
• Literale8 Zahlen, Buchstaben und Zeichenketten werden als Konstante (bzw. konstante
Werte) direkt in den Code geschrieben.
Beispiele: 5 , "Hallo" , ’c’ , -4.85 , true
• Variable bezeichnen Speicherstellen. An ihrer Stelle können (binär kodierte) Werte stehen.
Jede Variable hat einen Namen (Bezeichner), einen Datentypen und allenfalls einen Wert.
Beispiele: x , chfPer31Dez
• Funktionsresultat Jedes Unterprogramm, das einen Wert zurück liefert, kann als Aus-
druck angesehen werden.
Beispiele: random() , Math.abs(-3.75) , Math.sin(x)
7
Dieses Argument wird dabei dem entsprechenden Subroutinen-Parameter (also auch wieder einer Variable)
übergeben.
8
Literale werden auch als Konstanten bezeichnet. Ebenfalls als Konstanten bezeichnet werden aber auch Varia-
ble, deren Werte nicht verändert werden dürfen; daher verwende ich lieber den Namen «Literal», auch wenn der
Name nicht besonders üblich ist.
Einige Operatoren werden in Java mit den üblichen mathematischen Symbolen dargestellt.
Multiplikation verwendet hingegen einen Stern ( * ) und die Division eine Schrägstrich ( / ).
Die wichtigsten Operatoren in Java sind:
• Addition ( + )
• Subtraktion ( - )
• Vorzeichen ( - )
• Multiplikation ( * )
• Division ( / )
Die Restbildung wird in Java mit dem Zeichen % dargestellt. Wie wir alle wissen, geht nicht
jede Division «auf». Manchmal interessieren wir uns aber nicht für das Divisionsresultat, sondern
lediglich für den Divisionsrest, eben das, was beim ganzzahligen Dividieren übrig bleibt. Die
Modulo-Operation liefert diesen «Rest».
In den C-Sprachen (C, C++, Java, ...) wird hierzu der %-Operator verwendet:
18 % 7 ergibt 4 (denn 18 durch 7 gibt 2 Rest 4)
27 % 4 ergibt 3 (denn 27 durch 4 gibt 6 Rest 3)
19
1 Ausdrücke und Datentypen
1.3 Vorrangregeln
Java-Ausdrücke werden in der Regel von links nach rechts geklammert. So wird folgender Aus-
druck ...
a + b - c - 4.5 + a
Beim Auswerten von Ausdrücken gilt in auch in Java die «Punkt-Vor-Strich»-Regel. Operatoren
2. Stufe (Level) werden zuerst geklammert:
a * b - c - 4.5 * a
Eine vollständige Liste der Operatoren und deren Klammerung in Java finden wir hier:
http://www.santis-training.ch/java/javasyntax/operatoren.php
Bemerkung 1.1. Geroge Boole (1815-1864) begründete die nach ihm benannte Boole’sche Algebra,
die nur mit zwei Zuständen (0 = false, 1 = true) auskommt, daher der Name boolean .
Eine Zusammenstellung aller Datentypen in Java findet sich im Anhang (s. Kap. A.2 auf Seite
134).
Zahlen werden in Java im Binärsystem dargestellt. Java kennt die Ganzzahltypen byte ,
short , int und long , die sich lediglich in der Anzahl ihrer Bits9 unterscheiden. Ganzzahltypen
können in Java positive, wie auch negative ganze Zahlen darstellen. Wegen den negativen Zahlen
(Vorzeichen - ) nennen wir diese Datentypen auch «(mit) Vorzeichen behaftet».
Nun können wir uns fragen, wie denn ein Computer (Rechner) mit den Zahlen rechnen kann. Am
Beispiel des Halbaddierers soll gezeigt werden, wie dies funktioniert.
Ein Halbaddierer ist eine grundlegende elektronische Schaltung, die zwei Bit zusammenzählen
kann. Dabei werden Stromspannungen als Bit repräsentiert (z. B. 0 Volt = 0-Bit und 5 Volt =
1-Bit) und mit einem elektronischen AND (und) und einem elektronischen XOR (exklusives oder)
miteinander verknüpft:
A B Übertrag Summe
0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 0
Ein Halbaddierer kann einfach mit Relais oder Transistoren gebaut werden. Eine Nachbildung
befindet sich im SANTIS Museum of Computing History (SMOCH):
http://smoch.santis-basis.ch/index.php/halbaddierer
9
Bit = Binary Digit = Ziffer im Zweiersystem
21
1 Ausdrücke und Datentypen
Gebrochene Zahlen (Zahlen mit Nachkommastellen oder Brüche) werden in der Praxis an ver-
schiedensten Orten angetroffen. Meist handelt es sich dabei um Messgrößen (z. B. Volumen
[Liter], Längen [Meter, mm], Temperatur [Celsius], ...). In Java ist der Datentyp einer solchen
Variable mit double (oder float ) festgelegt. Beachten Sie insbesondere, dass es sich in Java
bei nicht abbrechenden Dezimalzahlen immer um eine Annäherung handelt. So ist es also nicht
möglich die Zahl Pi, oder 1/7 damit exakt darzustellen. Mehr noch: nicht einmal die Zahl 0.1
ist damit genau darstellbar, da die Zahlen im Binärsystem abgebildet werden. Dabei ist eben 0.1
(also ein Zehntel) ein nicht abbrechender «Binärbruch».
Wissenschaftliche Notation Erschrecken Sie nicht, wenn Zahlen wie folgt daher kommen
sollten:
• 5.7e6
• 4.35353535354e-2
• -3.66666666667e-20
Keine Angst: das Zeichen «e» bedeutet hier nichts esoterisches. Diese Schreibweise wird auch als
Exponentialschreibweise bezeichnet (daher wohl das «e»). Dies sagt einfach aus, um wie viele
Stellen der Dezimalpunkt nach rechts verschoben werden soll (bzw. links, falls die Zahl nach dem
«e» negativ ist). Somit sind die obigen Zahlen wie folgt zu lesen:
Typumwandlung (Casting) In typisierten Sprachen wie Java muss oft zwischen gebroche-
nen und ganzen Zahlen umgeschaltet werden. Aus einer ganzen Zahl eine gebrochene zu erhalten,
ist in allen mir bekannten Sprachen trivial: Dies geschieht automatisch. Die Umwandlung in die
andere Richtung bedarf meist einer Zustimmung durch den Programmierer (Casting = expli-
zite Typumwandlung), da durch die Umwandlung in der Regle Information verloren geht. Was
geht verloren? Allfällige Nachkommastellen.
Beispiel:
int i;
double d ;
In Java existieren zwei weitere Datentypen, welche mit beliebiger Genauigkeit rechnen können:
BigInteger Ein BigInteger kann beliebig große ganze Zah-
len darstellen. Dabei sind 300stellige Zahlen keine
Seltenheit, aber auch kein Problem für Java.
BigDecimal Dagegen kann ein BigDecimal mit beliebig vie-
len Nachkommastellen umgehen. Die Zahl π auf
tausend Nachkommastellen zu berechnen ist mit
Java also gut möglich.
Im folgenden Beispiel wird die Zahl 10 durch 1.234 geteilt und 30 Nachkommastellen werden
ausgegeben:
BigDecimal bd = BigDecimal . TEN ;
bd = bd . setScale (30);
bd = bd . divide ( BigDecimal . valueOf (1.234) , BigDecimal . ROUND_UP );
System . out . println ( bd . toPlainString ());
1.4.5 Funktionsresultate
Funktionsresultate können in Java auch als Ausdrücke angesehen werden und werden wie Va-
riable und Konstanten (Literale) verwendet:
double y ;
y = Math . sin ( x ) + 2 * Math . cos ( Math . ln ( x ) + phi * Math . min (a , b ));
23
1 Ausdrücke und Datentypen
1.5 Variable
Es gibt im Umgang mit Variablen grundsätzlich zwei Arten von Programmiersprachen. Bei typi-
sierten Sprachen (C, Java, ...) können einer Variable nur Werte zugewiesen werden, die in einem
vorgegebenen Wertebereich (z. B. «ganze Zahl») liegen. Bei untypisierten Sprachen (PHP, Java-
Script, ...) kann einer Variable jeder mögliche Wert der Programmiersprache zugewiesen werden.
Beispiel in Java:
int x ;
x = 7 + 5 * 33;
Java ist eine typisierte Sprache. Die Variablen müssen daher vor dem ersten Gebrauch deklariert
werden. Deklarieren heißt hier: Wir geben dem Programm an, um welchen Datentyp es sich bei
der Variable handelt. Jeder Variable liegt also ein Datentyp zugrunde. Dies ist vorteilhaft damit
die Programmierer nicht versehentlich falsche Werte in die Variable einfüllen.
In Java wird eine Variable deklariert, indem zunächst der Datentyp angegeben wird, und danach
folgt der Name der Variable.
Beispiele:
boolean eingabeKorrekt ;
int anzahlTreffer ;
double blutMengeInLitern ;
char steuerzeichen ;
String name ;
In Java können pro Deklaration mehrere Variable angegeben werden, und Variable können in
der Deklarationsanweisung auch einen Initialwert erhalten.
Beispiele:
int x, y, z;
Die folgende Tabelle veranschaulicht den Zusammenhang von Variablen und ihren Datentypen:
Eine Variable kann man sich als Kiste vorstellen. In diese Kiste
werden die Werte der Variable gelegt. Die Variable hat einen
Datentypen. Wir können uns eine Variable als Fernsteuerung
für die Inhalte vorstellen.
25
1 Ausdrücke und Datentypen
Variable werden mit sogenannten Bezeichnern referenziert. Bezeichner (en. identifier) sind Namen.
Diese Namen sind beinahe beliebig, müssen sich in Java jedoch an folgende Regeln halten:
• Bezeichner dürfen keine reservierten Wörter sein, z. B. double , class oder while (s. Kap.
A.5 auf Seite 139).
{
eingabe ( " Alter " );
berechne_Jahrgang ();
ausgabe ( " Jahrgang " );
}
Semikolon
Die UML-Notation wurde bereits teilweise in [GFG11] verwendet und eine Zusammenfassung der
wichtigsten UML-Elemente findet sich im Anhang des PDF-Buches [Fre15]. Hier ein Beispiel einer
einzelnen Anweisung in Java. Jede Anweisung in Java wird mit einem Strichpunkt (Semikolon
; ) beendet:
System.out.println("Hallo Welt");
Bemerkung 2.1. Die Subroutine println() gibt bekanntlich einen Text auf der Konsole aus.
10
Typischerweise schreibt man pro Zeile eine Anweisung (= Befehl).
11
UML = Unified Modelling Language
29
2 Sequenzen (Anweisungen und Abfolgen)
2.2 Anweisungen
Die wichtigsten vier Arten von Anweisungen12 sind:
int x, y ;
char zeichen ;
String begruessung ;
• Zuweisen von Werten an Variable (s. Kap. 2.5 auf Seite 33):
• Prozeduraufruf :
Ausgabe von Text und Zahlen auf die Konsole:
System.out.println("Hallo Welt");
Warten auf das Beenden eines anderen Prozesses:
threadXY.join();
Setzen von (System)eigenschaften:
setDate("2015-02-05");
• Kontrollflusssteuerung:
Selektion ( if / else ) (s. Kap. 3 auf Seite 37) und Iteration ( while / for ) (s. Kap. 4 auf
Seite 55).
12
Synonyme: Anweisung, Befehl, Statement
Neben den soeben erwähnten vier häufigsten Anweisungen der prozeduralen Programmierspra-
chen (Deklaration, Zuweisung, Prozeduraufruf, Kontrollflusssteuerung) gibt es in Java weitere
Anweisungen:
• throw -Anweisung
2.3 Deklarationen
Die standardmäßige Variablendeklaration in Java ist ein Datentyp gefolgt von einem Bezeichner,
welcher als Variablennamen dient:
int plz ;
String eingabe ;
double masse ;
Bemerkung 2.2. Durch Komma abgetrennt, können in Java pro Deklaration mehrere Variable
angegeben werden:
int plz , jahrgang ;
String vorname , familienname ;
2.3.1 Initialisierung
Bemerkung 2.3. Es ist gestattet, wie bereits im Kapitel über Variable erwähnt, Variable gleich
bei ihrer Deklaration zu definieren, also mit einem Initialwert zu versehen. Die beiden folgenden
Code-Blöcke sind identisch:
String begruessung ; // <- Deklaration
begruessung = " Hallo " ; // <- Definition
Abkürzend kann die Deklaration und die Definition in einem Schritt, der sog. Initialisierungs-
anweisung angegeben werden:
String begruessung = " Hallo " ;
31
2 Sequenzen (Anweisungen und Abfolgen)
2.4 Prozeduraufrufe
Bei einem Prozeduraufruf (z. B. println() )13 wird eine andere Stelle im Programm ausgeführt,
von wo nach beenden ebendieser Prozedur wieder an dieselbe Stelle im Ablauf zurückgekehrt
wird.
Neben bereits besprochenen vorgegebenen Subroutinen gibt es in JDK14 1.7 mehr als 7000 Modu-
le mit beinahe 40 000 Hilfsfunktionen. Zum Beispiel existieren dutzende von Kalenderfunktionen,
Funktionen für Zufallszahlen und trigonometrische Funktionen, Funktionen für die Computersi-
cherheit und fürs Verschlüsseln und Entpacken von Daten, Funktionen fürs Erstellen von graphi-
schen Benutzerschnittstellen, für den Zugriff aufs Internet und Datenbanken, für die Bearbeitung
und Formatierung von Text, Erstellen und Lesen von XML-Dateien, Behandeln von regulären
Ausdrücken, und und und.
13
Subroutinen sind: Prozeduren, Unterprogramme, Funktionen, Methoden (s. Kap. 5 auf Seite 65).
14
JDK = Java Developers Kit
x = 2010 + j;
Die Zuweisung wird mathematisch mit einem ’:=’ oder mit dem Sym-
bol. ’←’ geschrieben. In Java hingegen wird die Zuweisung durch ein
einfaches Gleichheitszeichen «=» dargestellt.
Bemerkung: Das einfache Gleichheitszeichen ist keine Gleichheit im mathematischen Sinne. Dabei
bedeutet x = 9*x + 3; keinesfalls, dass eine Gleichung in der Variable x zu lösen ist (Einige
Sprachen tun dies und x erhält den Wert -0.375)! In Java wird hier als erstes der Wert rechts
des Gleichheitszeichens berechnet und dieser Wert wird danach in die Variable gespeichert.
Zuerst erhält a den Wert 4. Danach erhält b den Wert 4, also den
Wert der Variable a . Zuletzt wird a auf 5 gesetzt. Beachten Sie, dass
b immer noch den Wert 4 hat, auch wenn die Variable a und b ver-
meintlich gleich gesetzt wurden. Merke: Die Java Zuweisung bedeutet
«Variable wird zu Wert» und nicht «Variable wird zu Ausdruck».
Zur Ein- und Ausgabe von Werten über die Tastatur siehe Anhang A.6 über die Ein- und Ausgabe
auf Seite 140.
33
2 Sequenzen (Anweisungen und Abfolgen)
Aufgabe 2.1 « int » Hier noch eine kleine Übung zu den Java-Datentypen. Programmieren
Sie die folgende Sequenz und erklären Sie das Ergebnis:
int x ;
x = 2 _147_483_647 ;
x = x + 1 ;
System . out . println ( " x : ␣ " + x );
Neben der sequenziellen Ausführung von Anweisungen ist die Selektion15 ein elementares Kon-
zept der Programmierung. Eine Selektion steuert den Verlauf eines Programms. Bestimmte Teil-
sequenzen werden nur unter einer gegebenen Bedingung ausgeführt.
Die Selektion wird in Java durch das Schlüsselwort if eingeleitet. So wird in folgendem Beispiel
die 2. Sequenz ( sequenz2() ) nur ausgeführt, wenn die Bedingung ( bedingung ) wahr ist. Die
sequenz1() am Anfang und die sequenz3() am Schluss werden jedoch immer ausgeführt.
sequenz1 ();
if ( bedingung )
{
sequenz2 ();
}
sequenz3 ();
15
Selektion wird auch Verzweigung, Entscheidung oder Auswahl genannt.
37
3 Selektion (Verzweigung)
3.1.2 Syntax
Dabei bezeichnet der <BLOCK> eine Sequenz mit beliebig vielen Anweisungen. Ein Block sollte
in Java immer in geschweifte Klammern (« { »,« } ») gestellt werden.
Der else -Teil ist optional (daher habe ich ihn in eckige Klammern gestellt; diese eckigen Klam-
mern sind in Java natürlich nicht einzugeben).
Ein Bit kann in der realen Welt nun die verschiedensten Bedeutungen haben:
Zustand 1 Zustand 2 Bedeutung
Falsch (false) Wahr (true) In verschiedenstem Kontext
Nein Ja Wahrheitswert auf eine Aussage
0 1 als Zahlen
nicht archiviert archiviert als sog. Flag (Markierung) einer Datenstruktur
Rot Grün bei einer Signalsteuerung
Geschlossen Offen Bei Barrieren / Türen / . . .
Schwarz Weiß Als Figurenfarbe im Go-Spiel
Feststelltaste nicht ak- Feststelltaste Diodenanzeige «CAPS-LOCK»
tiv eingeschaltet
... ... ...
16
Zürich unterscheidet darüberhinaus die Geschlechter im Wort “Zwei”: “zwoo Fraue”, “zwee Mane”, “zwei
Chinde”
39
3 Selektion (Verzweigung)
Das Bit kann aber nicht nur Gegenteile darstellen. Nehmen wir zur Verdeutlichung ein Trinkglas.
Sein Zustand kann auf zwei Arten modelliert werden, und es ist wichtig, dass wir die beiden Fälle
Unterscheiden:
Variante 1:
boolean voll ; // false = leer
Variante 2:
boolean voll ; // false = nicht voll
Im ersten Fall kann ich mich auf den Standpunkt stellen: Ein Trinkglas, das noch etwas darin
hat, ist voll. Ein solches Glas darf z. B. vom Kellner noch nicht abgeräumt werden.
Im zweiten Fall interessiert mich wohl eher, ob ein Glas ganz voll ist. ¾-voll wäre in diesem Umfeld
eben nicht mehr voll. Mit anderen Worten: Das Gegenteil von «voll» ist eben nicht immer ganz
«leer»; sicher aber immer «nicht voll»!
Es ist auf dieser Ebene die Aufgabe von uns Programmierern die reale Welt sinnvoll abzubilden.
Wir müssen abklären, ob «nicht voll» gleich «leer» ist, ob «nicht rot» gleich «grün» ist etc.
3.3.2 Vergleichsoperatoren
Vergleichsoperatoren vergleichen numerische Werte miteinander und liefern als Resultat entweder
true (bei Zutreffen) oder false (bei Nicht-Übereinstimmung).
Operator in Java Bedeutung
< < ist kleiner als
≤ <= kleiner oder gleich
> > ist größer als
≥ >= größer oder gleich
= == identisch, gleich
6= != ungleich
Die Operatoren && und || sind im mathematischen Kontext zu verstehen. && heißt, dass beide
Operanden links und rechts wahr sein müssen, damit der Gesamtausdruck wahr wird; wohingegen
bei || verstanden wird, dass mindestens einer der beiden Operatoren wahr sein muss. Sprachlich
ist dies je nach Formulierung aber genau umgekehrt und manchmal wollen wir nur ein exklusives
Oder, was in Java mit ˆ bewerkstelligt wird.
Hier ein Beispiel, bei dem in der Umgangssprache ein && oder ein || steht, je nach der Satz-
stellung:
Ein «Kind» ist ein Mädchen oder ein Junge.
Mädchen und Jungen sind somit beides Kinder.17
Nichts verstanden? Da es in den allermeisten Fällen keine Rolle spielt, ob sie & oder && als
logischen Operator verwenden, gewöhnen Sie sich lieber gleich den Doppeloperator && an.
17
Das Beispiel ist aus [Knu97] und dort in englisch formuliert; es zeigt aber, dass es in deutsch genau dieselben
sprachlichen Unklarheiten gibt.
41
3 Selektion (Verzweigung)
In der Praxis werden Vergleichsoperatoren ( <= , > , ...) oft mit logischen Operatoren ( && , ˆ ,
...) verknüpft. Ein klassisches Beispiel ist die Eingrenzung eines Zeichens (z. B. Buchstabe oder
Ziffer) in untere und obere Schranken:
if (( ’a ’ <= ch ) && ( ch <= ’z ’ ))
{
System . out . println ( " Kleinbuchstabe " );
}
• if(isOK) {...}
Oftmals ist es einfacher in einem AND, statt in einem OR zu denken. Gerade, wenn eine Bedingung
verneint werden soll.
So gelten die beiden folgenden De Morganschen Regeln18 :
! ( a & b ) == (! a ) | (! b )
! ( a | b ) == (! a ) & (! b )
Beachten Sie die Vertauschung von & und | bei der Verneinung.
18
Die beiden Regeln werden nach Augustus De Morgan (1806-1871) benannt, obschon diese bereits vor dessen
Lebzeiten bekannt waren.
43
3 Selektion (Verzweigung)
3.4 else
Alternativ auszuführender Code kann meist mit der folgenden Erweiterung der if -Struktur
erreicht werden. Ein else bezeichnet einen Block, der genau dann ausgeführt werden soll, wenn
die if -Bedingung falsch ist. Wir können somit zwei alternative Code-Blöcke schreiben, ohne die
Bedingung explizit zu verneinen.
sequenz1 ();
if ( bedingung )
{
sequenz2A ();
}
else
{
sequenz2B ();
}
sequenz3 ();
Bemerkung: In Java darf die geschweifte Klammerung (im if - sowie im else -Teil) unter
gewissen Umständen weggelassen werden. Mehr dazu im Anhang (s. Kap. A.7 auf Seite 146).
Dabei wird <BLOCK 1> genau dann ausgeführt, wenn die bedingung1 wahr ist, <BLOCK 2>
genau dann, wenn bedingung1 falsch war, jedoch bedingung2 stimmt etc. Der letzte Block
( <STANDARD-BLOCK> ) wird dann ausgeführt, wenn keine der Bedingungen zutrifft.
45
3 Selektion (Verzweigung)
3.5.1 switch
Eine Code-Auswahl aufgrund eines Variablen-Wertes kann mit der switch() -Anweisung verein-
facht werden.
Beispiel:
int note ; /* Leistungsbeurteilung */
...
switch ( note )
{
case 1:
print ( " sehr ␣ schwach " );
break ;
case 2:
print ( " schwach " );
break ;
case 3:
print ( " ungenuegend " );
break ;
case 4:
print ( " genuegend " );
break ;
case 5:
print ( " gut " );
break ;
case 6:
print ( " sehr ␣ gut " );
break ;
default :
print ( " unbekannte ␣ Note " + note );
}
Beachten Sie die break -Anweisung. Diese verlässt den switch -Block augenblicklich. Eine gän-
gige Fehlerquelle ist es, diese break -Anweisung zu vergessen.
Die letzte case -Marke19 wird hier von einer default -Marke gefolgt. Hier werden alle Fälle
behandelt, zu denen es kein explizites case gibt.
19
Sprungmarken werden oft auch Labels genannt.
Einige trickreiche Programmierer machen sich das Weglassen der break -Anweisung zu Nutze um
weniger Code schreiben zu müssen. Wird, wie im folgenden Programm, die break -Anweisung
weggelassen, (wie hier im Fall 1, 3 und 6), so wird einfach beim nächsten case -Block weiterge-
fahren.
int note ; /* Leistungsbeurteilung */
...
switch ( note )
{
case 1:
print ( " sehr ␣ " ); /* falls through */
case 2:
print ( " schwach " );
break ;
case 3:
print ( " un " ); /* falls through */
case 4:
print ( " genuegend " );
break ;
case 6:
print ( " sehr ␣ " ); /* falls through */
case 5:
print ( " gut " );
break ;
default :
print ( " unbekannte ␣ Note " + note );
}
Dies mag auf den ersten Blick verwirren, auf den zweiten Blick genial erscheinen. Beachten Sie
jedoch, dass dieser Code aus mehreren Gründen gefährlich ist: Das Programm später abzuän-
dern, ist heikel, denn es können dabei diverse Fälle in Vergessenheit geraten. Eine Übersetzung
in die meisten Sprachen ist hier gar unmöglich. Ich rate allen davon ab, nicht derart abartigen
Programmcode zu produzieren.
Geek Tipp 2
Schreiben Sie Ihren Code in erster Linie ohne gefährliche Tricks!
47
3 Selektion (Verzweigung)
Vergleichen Sie gebrochene Zahlen nie auf ihre Identität (in Java also nie mit "==" ). Denn
zwei Zahlen können unter Umständen dasselbe Resultat bedeuten, sich durch Rundungsfehler
aber auf den letzten Binärstellen unterscheiden. Fragen Sie sich beim Vergleich immer, wie genau
sollen die beiden Zahlen beieinander liegen? Normalerweise sucht man sich eine sehr kleine Zahl
(z. B. epsilon = 0.00001 )20 und schaut, ob sich die beiden ursprünglichen Zahlen um maximal
diese kleine Zahl unterscheiden. Ist der Unterschied kleiner als dieses Epsilon, so kann davon
ausgegangen werden, dass die Zahlen dieselben Resultate bedeuten:
Wie werden die beiden double Zahlen zahl_1 und zahl_2 nun miteinander verglichen? Etwa
so?
if ( zahl_1 == zahl_2 )
{
// beide Zahlen sind gleichwertig ???
...
Nein, bitte nicht. Streichen Sie obigen Block rot durch. Ein Vergleich von reellen Zahlen sollte
Rundungsfehler auf den letzten Stellen berücksichtigen. So ist’s
BRAV21 :
double epsilon = Math . max ( zahl_1 , zahl_2 ) * 0.00001;
double differenz = Math . abs ( zahl_1 - zahl_2 ) ;
if ( differenz < epsilon )
{
// beide Zahlen sind gleichwertig
...
Wie auch bei Vergleichen mit gebrochenen Zahlen kann in der Regel von einer Variable nicht
einfach gesagt werden, ob diese eine ganze Zahl darstellen soll. Rundungsfehler auf den letzten
Binärziffern machen uns das Leben schwer. Wir können jedoch wieder eine Genauigkeit angeben,
um damit auf Rundungsfehler zu prüfen. Verwenden Sie die folgende Funktion, um eine gebrochene
Variable mit großer Wahrscheinlichkeit auf Ganzzahligkeit zu prüfen:
double bruch = ... ;
double epsilon = 0.00001;
if ( Math . abs ( bruch - Math . round ( bruch )) < epsilon )
{
... // Der bruch ist wohl ganzzahlig
20
Der Variablenname epsilon geht hier auf die sog. Epsilontik zurück, welche mit beliebig kleinen positiven
Zahlen arbeitet.
21
BRAV = Brüche rücksichtsvoll mit Abstandsfunktion vergleichen.
Betrachten Sie auch die folgende Grafik: Die obere if/else -Variante ist absolut identisch mit
dem unteren direkten Aufruf (Dabei handelt es sich beim blauen Ausdruck um irgendeinen Boo-
le’schen Ausdruck und bei der roten Teilanweisung um einen beliebigen Code-Ausschnitt):
22
LOGIK = Löse offensichtliche Gebundenheit im Kontext.
49
3 Selektion (Verzweigung)
Beispiel Dazu gleich noch ein klassisches, immer wieder auftauchendes Beispiel aus der Praxis.
Dabei geht es um eine Maske, bei der sich der Benutzer anmelden kann. Es hat zwei Textfelder.
Im ersten Feld username kann der Benutzer seinen Namen ein-
geben und im zweiten Feld passwd kann er sein Passwort ein-
tippen. Darunter sind zwei Knöpfe (Buttons). Der erste (reset)
ist dazu da, die Felder zurückzusetzen; mit anderen Worten, den
Inhalt der Textfelder wieder zu löschen. Der zweite Knopf, der
login-Button, wird gedrückt, um sich am System anzumelden.
Dabei ist der reset-Button immer aktiv, wenn mindestens eines
der beiden Felder einen Text enthält; der login-Button hingegen
ist nur dann aktiv, wenn beide Felder Text enthalten. Betrachten
Sie nun die folgenden beiden Codestücke (zunächst wieder ohne
LOGIK):
// username UND passwort eingetragen :
if ( filled ( username ) && filled ( passwd ))
{
reset . setEnabled ( true );
login . setEnabled ( true );
}
// nur passwort leer gelassen :
if ( filled ( username ) && ! filled ( passwd ))
{
reset . setEnabled ( true );
login . setEnabled ( false );
}
// nur username leer :
if ( ! filled ( username ) && filled ( passwd ))
{
reset . setEnabled ( true );
login . setEnabled ( false );
}
// beide Felder leer :
if ( ! filled ( username ) && ! filled ( passwd ))
{
reset . setEnabled ( false );
login . setEnabled ( false );
}
Doch nun besser (Streichen Sie obigen Code bitte rot durch!), diesmal mit LOGIK. Ist dieser
Code nicht DUFTE23 ?
// mind . ein Feld ist eingetragen ( ODER ):
reset . setEnabled ( filled ( username ) || filled ( passwd ) );
// beide Felder sind eingetragen ( UND )
login . setEnabled ( filled ( username ) && filled ( passwd ) );
23
DUFTE = Direkt Unverändert False/True-Werte eingesetzt.
Der Code kann kürzer und effektiver wie folgt geschrieben werden, denn f() und g() sind
unabhängig (also invariant) im Bezug auf die die Selektion ( if ); dies nenne ich «selektionsinva-
riant».
Betrachten und diskutieren Sie nun den korrigierten Code:
f (); // pre - Code
if ( A )
{
x ();
}
else
{
y ();
}
g (); // post - Code
Eine spannende Möglichkeit, jedes Bit innerhalb eines int -Wertes als Wahrheitswert anzusehen
ist die Bit-Maskierung (s. Kap. A.16 auf Seite 162).
51
3 Selektion (Verzweigung)
3.10 Aufgaben
• Welche der obigen Abfragen (die kompilieren) erfüllen die geforderte Bedingung?
53
4 | Schleifen
Eines der wesentlichen Konzepte der Programmierung überhaupt ist die Möglichkeit ein Stück
Code beliebig oft ausführen zu lassen. In der Maschinensprache wird dies mit einem sog. Sprung
(Jump) an eine vorangehende Programmzeile gelöst. In Java wird dies generell durch eine
while -Schleife (manchmal auch durch eine for -Schleife24 ) vollzogen.
Wir hatten mit der Selektion (s. Kap. 3 auf Seite 37) ein Verfahren kennengelernt, bei dem ein
Stück Programmcode nur bei Eintreffen einer gegebenen Bedingung ausgeführt wird. Man könnte
dies als bedingten Sprung nach vorn auffassen, bei dem ein Stück Code eventuell ausgelassen
wird. Analog gibt es den «Sprung zurück», mit dem sich abbrechende oder nicht abbrechende
Schleifen erzeugen lassen.
Für eine Wiederholung steht in Java i. d. R. das Schlüsselwort while . In folgendem Beispiel
wird die 2. Sequenz ( sequenz2() ) so lange ausgeführt, wie die Bedingung ( bedingung ) wahr
ist.
Wieder gilt: Die sequenz1() wird immer ausgeführt. Die sequenz3() hingegen erst, wenn die
Bedingung nicht mehr erfüllt ist.
sequenz1 ();
while ( bedingung )
{
sequenz2 ();
}
sequenz3 ();
Die Bedingung steuert also Häufigkeit des Durchlaufs bzw. den Abbruch. Die bedingung ist ein
boolescher Ausdruck (der als Resultat true oder false aufweist). Einige Sprachen erlauben
auch Zahlen und werten diese verschieden aus. Die Programmiersprache «C» z. B. kann als Be-
dingung eine ganze Zahl entgegennehmen und führt die Anweisungen nur dann aus, wenn die
Zahl nicht Null (0) ist; Null bedeutet also «falsch», alles andere bedeutet «wahr». Nur wenn die
Bedingung wahr ( true in Java) ist, so werden die Anweisungen ausgeführt. Beim Teil, der mit
sequenz2() gekennzeichnet ist, kann es sich um beliebig viele Anweisungen handeln (also auch
keine oder eine).
24
Schleifen werden in der Schweizer Mundart oft auch Schlaufen genannt, was den Vorteil hat, es nicht mit einer
Schleifmaschine oder einer Rutschbahn zu verwechseln ;-)
55
4 Schleifen
Die Schleife ist in allen Programmiersprachen der 3. Generation25 vorhanden. Die while() -
Schleife ist die grundlegendste Ablaufsteuerung in Java. Alle anderen ( if , do , for ; ja sogar
switch und break , . . . ) ließen sich damit simulieren.
25
Java ist eine objektorientierte Programmiersprache der 3. Generation. Höhere Generationen (wie z. B. Pro-
log oder SQL) verwenden Schleifen zur Lösungsfindung implizit, also ohne, dass sich der Programmierer darum
kümmern müsste.
• Suchen
• mehrere Eingabezeilen (ab User-Input, Files oder Netzwerk) lesen und verarbeiten
Geek Tipp 3
Wir tippen nichts zweimal. Wir kopieren niemals Code innerhalb eines Projektes!
Kopierter Code ist böse: Er verbreitet Mühsal, Verderbnis und Tod!
4.2 Syntax
Dabei ist die <BEDINGUNG> ein boolescher Ausdruck. Der <BLOCK> beinhaltet wie bei der
Selektion ( if ) beliebig viele in geschweiften Klammern eingeschlossene Anweisungen.
Ebenso existiert in Java eine Fuß-gesteuerte Schleife, die in der Praxis aber selten Anwendung
findet:
do
< BLOCK >
while ( < BEDINGUNG > );
Diese Bedingung muss genau gleich wie bei der kopfgesteuerten while() -Schleife den Wert
false aufweisen um den Zyklus zu durchbrechen. Es handelt sich also nicht um eine Ab-
bruchbedingung sondern auch um eine Wiedereinstiegsbedingung.
57
4 Schleifen
4.2.1 Beispiel
Hier das Zinseszinsbeispiel aus dem Buch [GFG11] Seite 45; diesmal in Java:
int anzahljahre = 5;
int jahr = 0;
endkapital = kapital ;
Beachten Sie, dass eine Division ein Risiko mit sich bringt. Sind beide Zahlen ganzzahlig (wie z. B.
7/3 ), so wird in Java eine ganzzahlige Division durchgeführt: 7 / 3 = 2 . Soll ein Dezimal-
bruch errechnet werden, so ist der Dividend oder der Divisor auch als Dezimalbruch anzugeben:
7.0 / 3 = 7 / 3.0 = 2.333...
Sehr oft wird in einer Schleife ein Zähler benötigt, um den Abbruch steuern zu können (wie jahr
im Beispiel «Zinseszins» auf Seite 58).26
Meist wird dazu eine ganze Zahl verwendet; welche in einer sog. Zählervariable (im Folgenden
int i ) gespeichert wird:
int i = 0;
while ( i < 20)
{
...
i = i + 1;
}
Weil solche Zähler-Schleifen so häufig sind, benutzt Java hier die Konstruktion aus der Pro-
grammiersprache C:
Bemerkung: Anstelle von i = i + 1; wird abkürzend meist i++; verwendet (s. Kap. 2.2.1 auf
Seite 31).
Aber Achtung: Verwenden Sie den ++ -Operator entweder nur in die-
sem Zusammenhang, oder aber erst dann, wenn Sie ihn vollständig ver-
standen haben, denn i++ ist eine Anweisung und gleichzeitig ein Aus-
druck mit dem Nebeneffekt, den Wert der Variable zu erhöhen. Probieren
Sie einmal folgendes aus, staunen Sie und lernen Sie daraus:
int i = 5;
i = i ++;
System . out . println ( " Neues ␣ i : ␣ " + i );
26
Natürlich kann die Abbruchbedingung auch ganz anders definiert sein: Ziel erreicht, Maximum überschritten,
Benutzereingabe, Spielende, ...
59
4 Schleifen
Geek Tipp 4
Abkürzungen in Programmiersprachen sind für Fortgeschrittene. Verwenden Sie solche erst, wenn
Sie sie vollständig verstanden haben und sicher gehen, dass auch niemals Anfänger Ihren Code
jemals warten (also verstehen) müssen. Mit anderen Worten: Verzichten Sie so oft als möglich auf
Abkürzungen.
Die allgemeine Syntax der for -Schleife ist links gegeben und entspricht 1:1 der while -Schleife
rechts:
for while
Gleich ein Beispiel (hier werden die ersten zehn Quadratzahlen berechnet und ausgegeben):
Mit for Mit while
int i = 1;
for ( int i = 1; i <= 10; i = i + 1) while ( i <= 10)
{ {
System . out . println ( i + System . out . println ( i +
" : ␣ " + ( i * i )); " : ␣ " + ( i * i ));
i = i + 1;
} }
61
4 Schleifen
Bemerkung 4.1. Weitere Spezialitäten von Java-Schleifen finden sich im Anhang (s. Kap. A.8
auf Seite 147).
63
5 | Unterprogramme (Subroutinen)
Neben der Selektion und der Iteration bietet sich mit Unterprogrammen (Subroutinen) eine dritte
Möglichkeit an, den Kontrollfluss zu steuern.
Bereits im Kapitel über Anweisungen (s. Kap. 2.2 auf Seite 30) sind uns Unterprogramme begeg-
net. Wir hatten dort vorgegebene Systemfunktionen aufgerufen:
• System.out.println("...") )
• Math.sqrt(...)
Neu ist in diesem Kapitel, dass wir solche Teilprogramme auch selbst definieren können.
Ein Unterprogramm27 ist eine mit einem Namen versehene Sequenz. Eine Unterprogramm kann
später aus beliebigen Stellen des Programmes aufgerufen werden.
Unterprogramme sind der Kern der «Strukturierten Programmierung». Damit werden komplexe
Abläufe in fassbare kleine Problemstellungen aufgeteilt. Dies macht den Code überblickbar und
besser wartbar.
Der Begriff Divide et Impera ist lateinisch und steht für Teile und Herrsche28 . Wir verwenden
das Prinzip von kleinen Teilprogrammen, um ein größeres Problem in den Griff zu bekommen29 .
Das Konzept ist auch bekannt unter dem Namen funktionale Dekomposition. In der Praxis spricht
man von schrittweiser Verfeinerung oder einfach vom Verwenden von Unterprogrammen. Diese
kommen in verschiedenen Sprachen mit diversen «Möglichkeiten» vor:
Ist die Ausführung der Unterprogramm-Sequenz abgeschlossen, so wird das Programm dort wei-
tergefahren, woher der Unterprogramm Aufruf stattgefunden hat. Unterprogramme brauchen nur
einmal definiert zu werden, auch wenn sie an verschiedenen Stellen aufgerufen werden, oder wenn
sie innerhalb einer Schleife mehrmals aufgerufen werden.
27
Ich werde im folgenden den Begriff Unterprogramm synonym zu Subroutine verwenden.
28
Der Begriff wird Ludwig XI (1423-1483) zugeschrieben. Streue Zwietracht unter die Feinde und herrsche
ungestört. Doch bereits 2000 Jahre zuvor ergänzt Jia Lin das Buch von Sun Tzu [Tzu00] um Methoden, die dem
Feind schaden; insbesondere: «Säe Zwiespalt zwischen dem (gegnerischen) Herrscher und seinen Ministern».
29
Schon Sun Tzu rät in seinem Buch «Die Kunst des Krieges», die Armeen aufzuteilen, um eine große Streitmacht
zu führen. In dieser Bedeutung war der Begriff «Divide et Impera» definitiv schon im römischen Reich - wenn
vielleicht auch nicht unter diesem Namen bekannt.
65
5 Unterprogramme (Subroutinen)
• Das Programm ist einfacher zu lesen und dadurch auch besser wartbar und veränderbar.
• Einzelne Subroutinen können auch an anderen Stellen und sogar in anderen Programmen
unverändert eingesetzt werden.
• Programme, die in Subroutinen aufgeteilt werden, sind weniger fehleranfällig und somit
robuster.
• Kürzere Entwicklungszeiten bei häufiger Nutzung der selben Subroutine. Ebenso kann die
Zeit zur Fehlersuche reduziert werden.
Nachteile:
• Mehr Tipparbeit
• Programme werden (wenn auch meist kaum messbar) langsamer in der Ausführung (Per-
formanz).
Geek Tipp 5
Wer komplizierten Code schreibt, wird oft zu Unrecht bewundert: Es ist weitaus schwieriger
verständlichen Code zu schreiben.
Die Krux der Applikationsentwickler ist die Entscheidung, ob komplizierter, unverständlicher
Code geschrieben wird, damit man im Projekt unentbehrlich wird, oder ob man besser – und dies
ist weitaus schwieriger – sauberen verständlichen Code schreibt, mit der Tatsache im Auge, dass
man nun jederzeit ersetzbar ist.
Tun Sie sich selbst den Gefallen und schreiben Sie verständlichen Code!
• beep(); • currentTimeMillis()
• exit(); • getUserName()
• logTimestamp();
• pause();
• println("Hallo"); • sin(360)
• wait(millisecods); • dreiecksflaeche(3, 4, 5)
• printFibonacciNumbers(25);
Beachten Sie, dass die Prozeduren mit einem Strichpunkt ( ; ) beendet wurden. Dabei handelt
es sich in aller Regel um eine Anweisung, wohingegen die Funktionen als Ausdrücke verwendet
werden (sollten) und somit nicht als Anweisungen stehen werden.
67
5 Unterprogramme (Subroutinen)
• Log: Den aktuellen Zeitpunkt (Timestamp) in Erfahrung bringen und diesen z. B. in einer
Log-Datei ausgeben (z. B. zur Geschwindigkeitsprüfung).
• ...
Geek Tipp 6
Teile den Code in überblickbare Teilprogramme. Ein Maximum von sieben Codezeilen ist ein
guter Richtwert.
In Java können bei einer Subroutine beliebig viele30 Parameter deklariert werden. Jeder Para-
meter hat einen Datentypen und einen formalen Parameternamen.
Beispiel 5.1. Hier einige Funktionsköpfe (Prototypen) mit keinem, einem oder mehreren Parame-
tern:
void mainLoop ()
void println ( String text )
void wait ( long milliseconds )
void wait ( long milliseconds , int nanoseconds )
void : Prozeduren, also Subroutinen, welche keinen Wert zurückgeben, werden in Java mit
dem Schlüsselwort void gekennzeichnet.
Beim Aufruf der Subroutinen geben wir den Parametern Werte mit. Diese aktuellen Werte der
Parameter nennt man auch «Argumente».
Hier einige Prozeduraufrufe:
mainLoop () ; // Ohne Argumente
println ( " Hallo ␣ Welt " ); // Argument ist der String " Hallo Welt "
wait (2000) ; // Argument ist "2000 ( Millisekunden )"
wait (2000 , 500 + x ) ; // Argumente sind hier zwei Zahlen
x = 5200 ;
wait ( x ) ; // Argument ist die Zahl 5200 ( nicht die Variable x !)
Beispiel 5.2. Das folgende Beispiel soll einfach die ersten Quadratzahlen auf der Konsole ausgeben.
Dazu ist lediglich die Zahl max wichtig, welche angibt, bei welcher Quadratzahl dann das Ende
des Programmes erreicht sein soll:
void zeigeQuadratzahlen ( int max )
{
for ( int i = 0; i <= max ; i ++)
{
System . out . println ( " i ␣ : ␣ " + ( i * i ));
}
return ;
}
Bemerkung 5.1. Subroutinen können denselben Namen, aber eine andere Anzahl formaler Para-
meter aufweisen. Diesen Sachverhalt nennen wir entweder «statischen Polymorphismus» oder das
überladen von Subroutinen (s. Kap. A.11 auf Seite 151).
Bemerkung 5.2. Der Befehl return auf der letzten Zeile sagt dem Prozessor, dass die Subroutine
beendet ist, und dass die Kontrolle zum aufrufenden Programm zurückkehren (re-turn) muss. Die
meisten Sprachen (so auch Java) setzen dieses « return » automatisch ein, sodass es mittlerweile
von allen Programmiererinnen und Programmierern weggelassen wird.
30
Beliebig viele heißt hier aber auch: Eine Subroutine kann ohne Parameter auskommen.
69
5 Unterprogramme (Subroutinen)
Wie erwähnt, nennen wir die übergebenen Werte31 Argumente. Argumente sind in Java-
Unterprogrammen nicht veränderbar. Ob ein Parameterwert mittels Literal, Variable, Funk-
tionsresultat oder als zusammengesetzter Ausdruck mitgegeben wird, kann die Funktion nicht
unterscheiden, auch dann nicht, wenn die aufrufende Variable denselben Namen hat:
Beispiel 5.3. Gegeben ist die folgende Funktion:
void change ( int x )
{
System . out . println ( " start ␣ x : ␣ " + x );
x = x + 1;
System . out . println ( " end ␣ ␣ ␣ x : ␣ " + x );
return ;
}
Aufrufe:
int i = 6;
int x = 4;
// Ausgabe :
change (6); // -> 6, 7
change ( i + x * 2); // -> 14 , 15
change ( x ); // -> 4, 5
change ( x ); // -> 4, 5 !! x wird NICHT veraendert !
Beachten Sie: Die Variable x aus dem Aufruf (zweiter Programmblock), wird innerhalb der
Methode change() niemals verändert! Es handelt sich dort um eine andere Variable x , welche
zufällig denselben Namen trägt.
Bemerkung 5.3. Die Parameterwerte (Attribute) und Werte der lokalen Variablen32 liegen in der
Maschine auf dem sog. Stack (Stapel)33 und haben nur während der Ausführung der Subroutine
ihre Gültigkeit.
Bemerkung 5.4. Auch hier ist das Schreiben der return -Anweisung optional;
Bemerkung 5.5. Zur Syntax von Java-Subroutinen siehe hier (s. Kap. A.10 auf Seite 150).
31
Java übergibt immer die Werte der Ausdrücke, niemals die Variablen, auch dann nicht, wenn ein Ausdruck
genau aus einer Variable besteht. Ein sog. Variablenparameter ist in Java also nicht möglich.
32
Für lokale Variabel Siehe Seite 76
33
Stack Siehe Anhang auf Seite 152
Subroutinen können nicht nur Attributwerte als Parameter entgegen nehmen; sie können auch
Werte zurückgeben.
Unterprogramme, welche einen Wert zurückgeben nennen wir auch Funktionen.
Beispiel:
currentTimeMillis() liefert die Systemzeit in Millisekunden (gezählt ab dem 1. Januar 1970).
Obige Funktion berechnet die Differenz zwischen zwei Jahreszahlen. Diese Differenz wird als
alter() zurückgegeben.
Gleich ein zweites Beispiel, welches den Jahrgang via Konsole erfragt und als int zurückgibt:
int jahrgang () {
System . out . println ( " Bitte ␣ Jahrgang ␣ eingeben : ␣ " );
String jahrString = new Scanner ( System . in ). nextLine ();
int jahr = Integer . parseInt ( jahrString );
return jahr ;
}
Bemerkung 5.6. Beachten Sie, dass in Java bei Funktionsresultaten die return -Anweisung
angegeben werden muss!
34
Leider unterscheidet sich hier der Java-Funktionsname je nach Betriebssystem. Unter MS-Windows lautet
der Befehl getName() .
71
5 Unterprogramme (Subroutinen)
Eine klassische Funktion berechnet einen Wert abhängig von einer Eingabe nach dem EVA-
Prinzip. EVA bezeichnet hier Eingabe-Verarbeitung-Ausgabe. Bei Java-Funktionen ist die Ein-
gabe die Werteliste der Argumente, die Verarbeitung wird durch den Funktionsrumpf (Body)
erreicht und die Ausgabe ist der Wert, welcher dem aufrufenden Programm zurückgegeben wird.
Beispiel 5.4.
Das folgende Unterprogramm berechnet das Volumen eines Quaders:
double volumen ( double breite , double hoehe , double tiefe )
{
return breite * hoehe * tiefe ;
}
Für die Rückgabe steht der Java virtuellen Maschine nur ein Register (4 Byte) zur Verfügung35 .
Dies reicht für Zahlen und Zeichen. Die Rückgabe in Java erfolgt mit dem Schlüsselwort return .
Da Java eine typisierte Sprache ist, muss der zurückgegebene Wert denjenigen Datentypen
aufweisen, der im Prototypen der Funktion (also in der Kopfzeile) angegeben wurde:
Beispiel 5.5. Die folgende Funktion halbiert Zahlen und muss somit auch gebrochene Werte (De-
zimalbrüche) zurückgeben können:
double halbieren ( int zahl ) {
double haelfte ;
haelfte = zahl / 2.0;
return haelfte ;
}
Der Aufruf eines selbst geschriebenen Unterprogrammes ist identisch mit den vorgegebenen Sys-
temfunktionen (s. Kap. 1.4.5 auf Seite 23). Auch hier kann die Subroutine wie ein Ausdruck
verwendet werden:
double d ;
d = halbieren (4) + halbieren (3) +
halbieren (3*8 + (( int ) halbieren (6)));
35
Ausnahmen: für double und long werden 8 Byte benötigt, also zwei Register. Andere Programmiersprachen
(z. B. Python oder Ruby) können via return gleich mehrere Werte zurückgeben.
73
5 Unterprogramme (Subroutinen)
Beispiel 5.7. Das Unterprogramm nextFibo() (rechter Teil der Grafik) berechnet aus einer
Fibonacci-Zahl seinen Nachfolger in der Fibonacci-Reihe. 36
36
Die Fibonacci-Reihe beginnt mit 1, 1, 2, 3, ... Jeder Nachfolger wird aus der Summe der beiden letzten Zahlen
der Reihe berechnet. Beachten Sie auch die rekursive Berechnung aus [GFG11] dort: Aufgabe 11.2 auf Seite 143
und beachten Sie weiter die Bemerkungen in diesem Skript (s. Kap. 11.1 auf Seite 123). Die direkte Berechnung
(also ohne Iteration oder Rekursion) ist sicher die rascheste Berechnung für große Werte.
75
5 Unterprogramme (Subroutinen)
void top () {
int b = 4; // lokal
a = fct1 ( b );
System . out . println ( " A = " + a + " ␣ B = " + b );
}
Geek Tipp 7
Lokale Variable sind globalen vorzuziehen!
37
S. S. 152
38
Natürlich können wir Werte im Hauptprogramm auch mittels Funktionsrückgabe verändern, indem wir diese
als Ausdrücke verwenden; doch hier geht es im Moment ausschließlich um das Verändern von Variablen.
Jedes Funktionsresultat ist ein Ausdruck und keine Anweisung. Ausdrücke sollten wenn immer
irgendwo verwendet werden. So erstaunt dieser ignorierende Code jeden erfahrenen Programmie-
rer:
int x ;
x = 4;
x = x * 6;
doppelDecker ( x ); // *** ??? ***
x = x + 2;
Die Methode doppelDecker() hat eine Aufgabe zu erfüllen. Sie berechnet eine Zahl (hier
resultat genannt). Dieses Resultat wird der aufrufenden Programmsequenz übergeben, aber ...
oh Wunder ... dort (an der mit *** ??? *** bezeichneten Stelle) nicht verwendet. Der Program-
mierer, der die Funktion doppelDecker() aufruft, vernichtet geringschätzig (oder mangels Ver-
ständnis des Funktionsresultates) die Arbeit derjenigen Person, die die Methode doppelDecker()
implementiert hatte. Mit demselben Resultat39 hätte unser Ignorant folgendes schreiben können:
int x;
x = 4;
x = x * 6;
50;
x = x + 2;
Programmiersprachen, die das Vernichten von Funktionsresultaten überhaupt nicht erlauben sind
hier hoch zu loben. In den wenigen Fällen, wo das Resultat tatsächlich nicht verwendet wird, ist
das mit einem Kommentar zu erklären. Ein call -Schlüsselwort wie in anderen Programmier-
sprachen gibt es in Java leider nicht!
39
Natürlich wird an der mit ***???*** markierten Stelle die Funktion als Anweisung ausgeführt. Dadurch
wird der Prozessor aufgewärmt. Doch mehr als heiße Luft produziert es nicht, wenn wir eine Funktion nicht als
Ausdruck, sondern als Anweisung verwenden!
77
5 Unterprogramme (Subroutinen)
Geek Tipp 8
Verwenden Sie Prozeduren stets als Anweisungen!
Verwenden Sie Funktionen stets als Ausdrücke!
Ein Aufruf von return beendet die aktuelle Methode sofort. Dieses Wissen kann verwendet
werden, um sog. Wächterabfragen in den Code einzubauen, ohne dass der Code bei jeder Abfrage
um eine Einrückung weiter geschoben werden muss.
Gehen wir einmal von der folgenden bekannten Subroutine aus, welche den Schalttag zurückgibt.
Ist ein Jahr ein Schaltjahr, so wird Eins ( 1 ) zurückgegeben, in allen anderen Fällen die Null
( 0 ):
int schalttag ( int jahr ) {
if ( jahr % 4 != 0) { return 0; }
if ( jahr % 400 == 0) { return 1; }
if ( jahr % 100 == 0 && jahr > 1582) { return 0; }
return 1;
}
Nun wollen wir den letzten Tag in einem Monat berechnen und benötigen dazu sogar noch das
Jahr; nämlich für den letzten Tag jeweils im Februar. Um mögliche fehlerhafte Eingaben zu
vermeiden, müssen wir sowohl das Jahr, wie auch den Monat auf Gültigkeit prüfen.
int letzterTagImMonat ( int monat , int jahr ) {
if ( jahr != 0)
{
if ( monat >= 1 && monat <= 12)
{
if (4 == monat || 6 == monat || 9 == monat || 11 == monat )
{
return 30;
} else {
if ( monat != 2) {
return 31;
}
}
return 28 + schalttag ( jahr );
}
}
// Fehler :
return 0;
}
79
5 Unterprogramme (Subroutinen)
Doch mit etwas GRIPS40 könnten wir Wächterabfragen einbauen, welche die Fehler von vornherein
ausschließen und den Code doch einiges besser lesbar machen:
int letzterTagImMonat ( int monat , int jahr ) {
// Fehlerbehandlung ( Waechter )
if ( jahr == 0) return 0;
if ( monat < 1 || monat > 12) return 0;
// Hauptteil
if (4 == monat || 6 == monat || 9 == monat || 11 == monat )
{
return 30;
}
if ( monat != 2)
{
return 31;
}
return 28 + schalttag ( jahr );
}
Bemerkung: Streng genommen unterwandern wir mittels return aus einer Kontrollstruktur ( if ,
while ) die «Strukturierte Programmierung». Es handelt sich hier lediglich um eine Fehlerbe-
handlung, welche nach Funktionsende zum selben Punkt zurück gelangt, wie wenn der Fehler
nicht aufgetaucht wäre. Somit können wir hier ein Auge zudrücken.
Bemerkung 5.7. Die vorab gegebene Funktion schalttag() hatte sich dieses Wissen bereits zu
nutze gemacht.
Geek Tipp 9
Schreiben Sie Ihren Code wenn immer möglich mit etwas GRIPS.
40
GRIPS = Generelles Return Im Prozedur-Start.
// aufgerufene Methode f ()
void f ( int a , int b , int c )
{
a = a + b;
b = b + c;
c = a + b;
print ( " c ␣ = ␣ " + c );
}
5.7.2 x, y oder z
// aufgerufene Methode f ()
void f ( int z , int y , int x )
{
int r ;
x = x + y;
y = x + 2* z ;
r = x + y + z;
print ( " r ␣ = ␣ " + r );
}
81
5 Unterprogramme (Subroutinen)
• «Viele Programme vor allem aus Kap. 2 lassen sich nun als Funktionen lösen (Sinusfunktion,
Satz von Pick, Abbildungsgleichung, ...»
83
6 | Felder (Arrays)
Arrays (selten auch Felder, Reihungen oder Vektoren genannt) sind eindimensionale Tabellen
von Variablen mit gleichem Typ. Auf die Elemente eines Arrays wird wahlweise mit Hilfe eines
ganzzahligen Indexes zugegriffen. Wir sprechen von einem «random access41 ».
Einführungsbeispiel
Eine Methode printDatum(tag, monat, jahr) , welche ein Kalenderdatum in lesbarer Form
ausgibt, könnte wie folgt aussehen:
void printDatum ( int tag , int monat , int jahr ) {
Beachten Sie den um 1 verminderten Monatsindex (monat - 1). Dieser ist in Java wichtig, denn
in Java werden Arrays immer von Null (0) an indexiert. Der Monat mit der Nummer 1 soll in
diesem Beispiel (und nach allgemeinem Verständnis) jedoch der Januar sein.
Die erste Zeile erzeugt eine Liste (Feld, Array) von String s. Dies wird mit dem Symbol []
hinter dem Datentypen bezeichnet. Ein Array von Ganzzahlen würde also wie folgt deklariert:
int [] groesse ;
Um auf die Felder zuzugreifen (Zuweisen oder Auslesen) wird der sog. Indexoperator benutzt:
[ ... ] .
41
«random» bezeichnet hier nicht «zufällig» sondern eher «nach beliebiger Wahl».
85
6 Felder (Arrays)
Buch Beispiel
Das Beispiel aus dem Buch ([GFG11] dort Seite 74) wird hier nochmals aufgegriffen. Zum besseren
Verständnis nenne ich die Variable hier wotage (statt einfach x ).
Zugriff
Der Zugriff auf die einzelnen Komponenten geschieht mittels Index in eckigen Klammern (’[’, ’]’).
b) schrittweise (einzeln)
String [] wotage ;
Deklaration
Deklariert werden Arrays indem eckige Klammern hinter den Datentypen gestellt werden:
int [] s ;
Achtung: In diesem Beispiel wird für die int -Elemente noch kein Speicherplatz geschaffen! Es
wird erst deklariert, dass die Variable s später mit einem int -Array gefüllt werden darf.
Definition
Bevor wir die einzelnen int -Werte abfüllen können, müssen wir vom Betriebssystem Speicher
anfordern. Dies geschieht mit dem new -Operator:
s = new int [5];
Mit dieser Definition wird Speicher für fünf int -Werte angelegt.
Defaultwerte
Im Gegensatz zu anderen Sprachen werden Arrays von Zahlen (hier int ) je mit Null initialisiert.
Es gilt unmittelbar nach dem Definieren des Arrays s also:
0 == s [0]
0 == s [1]
0 == s [2]
...
Indexieren
Der kleinste Index ist in Java immer 0 (Null). Somit ist beispielsweise der höchste Index des
obigen Arrays s gleich 4.
Beispiele:
s [0] = 17;
s [1] = 9 * s [0];
s [4] = s [1] + s [2]; // s [2] == 0 ( Defaultwert )
s [5] = 77; // FEHLER : s indexiert nur von 0 bis 4!
Die einzelnen Komponenten ( s[0] , s[1] , ...) werden je wie eigene Variable behandelt.
87
6 Felder (Arrays)
Länge
Arrays können ihre Länge (Anzahl der Elemente) mit dem Attribut length bekannt geben. In
unserem Beispiel ist « s.length == 5 ». Die Länge wird oft in Schleifen verwendet:
for ( int i = 0; i < s . length ; i ++) {
s [ i ] = i * i ; // z. B. Quadratzahltabelle
}
Um zu zeigen, wie damit Arrays verwendet werden, können wir sogleich die Daten (Wochentage)
aus den beiden obigen Einführungsbeispielen (s. Kap. 6 auf Seite 85) ausgeben:
for ( String w : wotage ) {
System . out . println ( w );
}
6.1.2 Index
Manchmal benötigt man den Index, also die Position des Elementes. Dies ist nun bei Arrays (aber
auch bei ArrayLists und Lists) dank der bekannten Länge einfach zu bewerkstelligen:
int letzterIndex = wotage . length - 1;
for ( int i = 0; i <= letzterIndex ; i ++) {
System . out . println ( i + " : ␣ " + wotage [ i ]);
}
Wie Java dies intern technisch umsetzt, finden wir im Anhang (s. Kap. A.18.2 auf Seite 164).
Weitere Spezialitäten und Eigenheiten von Arrays und Tabellen im Anhang (s. Kap. A.18 auf
Seite 164);
89
6 Felder (Arrays)
Zeichenketten werden in Java mit der String -Klasse umgesetzt. Das String-Literal ist nichts
anderes als eine Zeichenkette, die in (doppelten) Apostrophen ( " ) als Begrenzungszeichen ein-
gefasst ist:
" Hallo ␣ Welt "
Wie man Zahlen einer int -Variable zuweisen kann, so kann man Strings auch String -Variablen
zuweisen oder ganz generell als Ausdrücke (s. Kap. 1 auf Seite 17) verwenden:
String a , b ;
a = " Hallo " ;
b = " Welt " ;
System . out . println ( a + " ␣ " + b );
93
7 Zeichenketten
Länge
String txt = " Hallo ␣ Welt " ;
int len ;
len = txt . length ();
Zeichen ermitteln
char ch = txt . charAt (5);
Kopieren
String a = " Hallo ␣ Welt " ;
String b = new String ( a );
Prüfen Einfach ist es in Java zu prüfen, ob ein String auf eine vorgegebene
Endung aufhört:
if ( txt . endsWith ( " Welt " )) {...}
Extrahieren
txt . substring (2 , 5); -> " llo "
Achtung: Bei substring() ist die erste Position inklusive jedoch die
zweite Position exklusive!
Den Code eines Zei- ...einfach einer int -Variable zuweisen:
chens zu ermitteln ist
char zeichen = ’x ’;
in Java geschenkt...
int code = zeichen ;
Zahlen in Ziffernfolgen
(Strings) verwandeln int zahl = 2012;
String zahlString = " " + zahl ;
95
7 Zeichenketten
try ( BufferedReader br =
new BufferedReader ( new FileReader ( halloFile )))
{
String line ;
line = br . readLine (); // vorausgehendes Lesen
while ( null != line ) {
System . out . println ( " Gelesen : ␣ " + line );
line = br . readLine (); // wiederholtes Lesen
}
} catch ( IOException iox ) {
System . err . println ( " Fehler : ␣ " + iox );
}
• Zeichenweise Lesen und Schreiben in Java funktioniert analog mit den Befehlen read()
und write() .
• Das Anfügen an eine bestehende Datei ist im Anhang beschrieben (s. Kap. A.20.3 auf Seite
173).
42
Weitere Infos im Anhang: (s. Kap. A.20 auf Seite 169)
Beachten Sie jedoch, dass die folgenden Vergleiche unterschiedliche Resultate liefern:
String a = " xy " ;
String b1 = " xy " ;
String b2 = new String ( " xy " );
if ( a == b1 ) --> true
if ( a == b2 ) --> false
if ( a . equals ( b2 )) --> true
Der + -Operator (Plus) auf Zahlen oder char angewendet, zählt die (binären) Werte zusammen.
Ist jedoch mindestens einer der beiden Operanden ein String, so wird auch der andere als String
betrachtet und die beiden Strings werden aneinandergereiht:
’# ’ + 4 + 2 + " ␣ != ␣ " + ’4 ’ + 2
97
7 Zeichenketten
7.6 Unicode
Java verwendet für Strings den Datentypen char (16 Bit) um Unicode43 Zeichen zu speichern.
Damit können bis 65 000 Zeichen unterschieden werden44 .
Unicode 6= UTF-8
Da in Files meist UTF-8 oder Latin (also 8-Bit-Zeichen) zur Textkodierung verwendet wird, so
muss man sich trotz Unicode45 bewusst sein, dass es zum Lesen bzw. Schreiben von Zeichen eine
Umkodierung braucht.
Zu den Literalen ist noch zu erwähnen, dass die char-Literale auch für Strings eingesetzt werden.
Die folgenden Escape-Sequenzen sind unterstützt:
• \n (new Line)
• \b (backspace)
• \f (formfeed)
• \t (tabulator)
• \r (carriage return)
• \\ (backslash)
7.7 Bemerkung
Weitere Besonderheiten von Zeichenketten im Anhang (s. Kap. A.19 auf Seite 166).
43
http://www.unicode.org
44
Mit Escape-Sequenzen und diakritischen Zeichen («˜ », «ˆ» , ...) können noch viele weitere daraus zusammen-
gesetzt werden.
45
Auch wenn sich (im Falle von LATIN bzw. ASCII) diese Umkodierung z. B. lediglich auf das Weglassen der
ersten 8 (bzw. 9) Bit beschränken kann!
46
\u leitet eine 4-Stellige Hex-Zahl ein (= 16 Bit). Diese werden aber oft nicht als Escape-Sequenzen bezeichnet,
sondern einfach als char-Literale.
• Die Lösungen zur Aufgabe 7. 1 aus «Programmieren lernen» sind oben abgedruckt (s. Kap.
7.2 auf Seite 94).
99
8 | Datenstrukturen und Sammelobjekte
Eigene Datentypen können aus den vorgegebenen Standardtypen (s. Kap. A.2 auf Seite 134)
zusammengesetzt werden. Links sehen Sie die Attribute, rechts die zugehörige UML47 -Notation.
Ein Clubmitglied weist z. B. die fol- UML-Notation:
genden Attribute auf :
• Ehrenmitglied: boolean
int postleitzahl ;
int eintrittsjahr ;
int geburtsjahr ;
boolean ehrenmitglied ;
}
Neue Clubmitglieder (Objekte) werden mit dem new -Operator geschaffen. Dieser holt sich vom
System (Heap) genügend Speicher, um die Attributwerte festhalten zu können:
Clubmitglied c1 , c2 , c3 ;
47
UML=Unified Modelling Language [Fre15].
101
8 Datenstrukturen und Sammelobjekte
8.1.1 Zugriff
Auf die Attribute ( name , vorname , ...) wird nun analog zu den bekannten Attributen ( .length ,
...) mit dem . -Operator zugegriffen:
Person p ;
p = new Person ();
p . name = " Freimann " ;
System . out . println ( " Familienname : ␣ " + p . name );
Aufgabe 8.1 «Personen Erfassen» Schreiben Sie hierzu ein Java-Programm, das zwei Personen
erfasst und deren Daten inklusive alter (nicht Jahrgang) ausgibt.
• List: Die Elemente einer List sind linear geordnet, d. h. in der Reihenfolge, wie sie eingefügt
wurden. Meist wird die ArrayList verwendet.
List < String > namen ;
namen = new ArrayList < String >();
namen . add ( " Meier " );
namen . add ( " Mueller " );
namen . add ( " Keller " );
Beispiel einer List ist die ArrayList (s. Kap. 8.2.2 auf Seite 106).
• LinkedList: Geordnete Liste, bei der am Anfang und am Ende hinzugefügt, abgeholt und
gelöscht werden kann. Somit kann eine LinkedList auch als Queue (First-in/First-out War-
teschlange) oder Stack (First-in/Last-out Kellerspeicher) verwendet werden. Das Einfügen
in der Mitte geht sehr rasch, da nur zwei Referenzvariable (Pointer) gesetzt werden müssen.
Zugriff via Index und das Auffinden einzelner Elemente ist jedoch zeitaufwändig.
48
Der größte Nachteil von Arrays ist die Unveränderbarkeit seiner Länge.
103
8 Datenstrukturen und Sammelobjekte
• (Hash)Map (Assoziative Arrays): Speichern der Objekte und Auffinden mit einem Schlüssel
(Key).
Beispiel Bücherliste:
Details zur Java- HashMap : (s. Kap. 8.2.3 auf Seite 106)
• Tree: Baumstrukturen erlauben ein rasches, sortiertes Einfügen und Suchen. Zugriff via
Index ist ähnlich aufwändig wie bei Listen.
In Java bieten sich die beiden Sammelcontainer TreeSet und HashSet an. TreeSet
fügt die Elemente automatisch sortiert ein (implementiert also SortedSet ). Details (s. Kap.
8.2.4 auf Seite 107).
Als konkretes Beispiel von Sammelobjekten wird hier die abstrakte Liste aus Java angegeben
( java.util.List ):
List < String > lst ;
lst = Arrays . asList ( " Montag " , " Dienstag " , " Mittwoch " ,
" Donnerstag " , " Freitag " , " Samstag " ,
" Sonntag " );
Listen werden in Schleifen auf die Selbe Weise wie Arrays (s. Kap. 6.1 auf Seite 88) durchaufen:
for ( String w : lst ) {
System . out . println ( w );
}
105
8 Datenstrukturen und Sammelobjekte
8.2.2 ArrayList
Der am häufigsten verwendete Sammelcontainer (Collection) in Java ist die ArrayList . Die
ArrayList hat im Gegensatz zu Arrays den Vorteil, dass Sie dynamisch erweiterbar sind. Mit
anderen Worten: Der Programmierer muss sich nicht von vornherein auf eine Maximalgröße fest-
legen.
Eine Liste von Punkten ( Point ) kann z. B. mit einem ArrayList -Objekt folgendermaßen kurz
und robust geschrieben werden:
ArrayList < Point > points = new ArrayList < Point >();
.... // Eintragen der Elemente in de Liste ...
while ( hatNochPunkte ()) {
points . add ( naechsterPunkt ());
}
8.2.3 HashMap
Als weiteres Beispiel wollen wir alle versicherten Personen einer mittleren49 Versicherungsgesell-
schaft in einer Datenstruktur speichern. Die AHV Nummer als Index bietet sich an und soll gleich
als Suchschlüssel verwendet werden. Arrays sind hier eine ganz schlechte Alternative. ArrayLists
sind besser, jedoch das Suchen kann sich als zeitaufwändig herausstellen. Mit ArrayLists könn-
te man die Elemente nach Suchschlüssel (hier der AHV Nummer) sortieren und dann eine binäre
Suche starten.
Das Einfügen von Elementen am Anfang oder in der Mitte wirkt sich bei ArrayLists ungünstig
auf die Performance aus.
Eine Lösung zu obigem Problem ist die Map .
HashMap map = new HashMap ();
map . put ( ahv1 , person1 );
map . put ( ahv2 , person2 );
// ...
p = ( Person ) map . get ( " 407.266.244.44 " );
Die HashMap speichert die Objekte in kurzen und rasch auffindbaren linearen Listen.
49
1 000 000 mittelgroße Datensätze können problemlos im RAM eines modernen Laptops verarbeitet werden.
Ersetzen Sie in folgendem Code «ArrayList» durch «TreeSet» und besprechen Sie die Verände-
rungen in der Ausgabe. Zur Erinnerung: Ein Set (zu Deutsch eine Menge) kann jedes Element
nur einmal enthalten.
import java . util .*;
Collection < String > namensListe = new ArrayList < String >();
void top () {
einlesenNamen ();
ausgabeNamensListe ();
}
void ausgabeNamensListe () {
for ( String str : namensListe ) {
System . out . println ( str );
}
}
Weitere Infos zum Java Collection Framework findet sich im Anhang (s. Kap. A.21 auf Seite
181).
107
8 Datenstrukturen und Sammelobjekte
Wie Sie erkennen, ist der Algorithmus zum Wurzelziehen in Java bereits implementiert.
Viele Algorithmen, welche Programmierer immer und immer wieder selbst gelöst hatten, sind
heute in die Programmiersprachen eingebaut. Dazu gehören nicht nur die mathematischen Be-
rechnungen wie obige Wurzel, trigonometrische Funktionen und dergleichen. Auch das Sortieren
von Objekten, das Erstellen von Eingabefeldern bis hin zum Schattenwurf dreidimensionaler Ob-
jekte gehört heute zum besseren Programmiergerüst.
Bemerkung 9.1. Wir lösen hier (in vergangenen und folgenden Aufgaben) vorwiegend Probleme,
welche schon gelöst sind. In der Praxis wird dies wohl kaum getan. Viele dieser Problemstellungen
sind einfacher mit einer Tabellenkalkulation oder mit einem Taschenrechner lösbar.
Dennoch: Zum Lernen gehört es dazu, viel zu üben und zu erkennen, welche Denkarbeit und
welche Konzepte hinter einer bestehenden Lösung stecken.
Wir lernen im Mathe-Unterricht das Ziehen einer Wurzel oder das Auflösen eines Gleichungs-
systems mit mehreren Unbekannten ja auch nicht nur zum Spaß von Hand.50 Es geht darum,
bestehende Verfahren zu kennen und daraus zu lernen; nur so ist es möglich, neue, bessere Algo-
rithmen zu entwickeln.
50
Wurzelziehen und Gleichungssysteme Lösen können Computer heute viel schneller; mehr noch: Ha-
ben wir keine Fehler in der Eingabe, so sind die Computerlösungen meistens auch fehlerfrei. Siehe z. B.
http://www.wolframalpha.com .
111
9 Algorithmen
9.1 Leseaufgaben
Das Lesen von Programmcode fördert das exakte Vorgehen und – sofern der Code sauber ge-
schrieben ist – zeigt gleichzeitig auf, wie Code geschrieben werden sollte.
Die folgenden Übungen (9.1.1 - 9.1.7) sind immer nach demselben Muster zu lösen:
• Gehen Sie Schrittweise und exakt vor. Der Zeigefinger der linken51 Hand dient hier als
Programmzeiger, also dieses Register, das auf den nächsten auszuführenden Befehl zeigt.
Die rechte Hand verwenden Sie, um die Wertetabelle nachzuführen.
• Machen Sie eine erste Aussage, was das Programm tun könnte.
9.1.1 Selektion
51
Für Linkshänder: «Der Zeigefinger der rechten Hand»
113
9 Algorithmen
9.1.4 Zahlenspielerei
115
9 Algorithmen
Die Lösungen zu diesen Aufgaben finden wir im Anhang (s. Kap. B auf Seite 191).
117
10 | Simulationen
Java wurde nicht wegen seiner Syntax bekannt. Java machte sich dank seiner Hilfsklassen und
seiner enorm großen API populär. Für alle gängigen Probleme hat Java eine Lösung bereit, wie
auch z. B. für das Erzeugen sog. Pseudozufallszahlen:
10.1 Random
Wer seine eigenen Pseudozufallszahlen braucht, kann Aufgabe 10.1 aus [GFG11] lösen. Das dort
beschriebene Verfahren der «linearen Kongruenzmethode» funktioniert in allen Programmierspra-
chen. Doch Java bringt schon mächtige Zufallszahl-Implementationen mit sich:
Pseudozufallszahlen können in Java auf zwei Arten gefunden werden. Die einfache und schnelle
Art ist die Methode Math.random() , die eine Zahl zwischen 0 und 1 liefert52 .
Um z. B. Prüfungsnoten zu simulieren kann ein Lehrer nun wie folgt vorgehen:
int note = ( int ) ( Math . random () * 6 + 1);
Eine zweite Variante ist das Verwenden eines Objektes der Klasse java.util.Random . Hiermit
können häufig verwendete Zufallsgrößen ( int , long , boolean ) einfach erzeugt werden.
Ein weiterer Vorteil der Klasse Random ist die Methode setSeed() . Diese erlaubt es, in Test-
simulationen immer wieder dieselbe Zufallssequenz zu erzeugen. Die Methode nextGaussian()
liefert eine normal verteilte Zufallszahl mit Mittelwert 0.0 und Standardabweichung 1.0.
52
Null ist dabei eingeschlossen, 1 ist ausgeschlossen. Mathematisch: [0, 1)
119
10 Simulationen
• Mit dem Auto, dem Zug, dem Fahrrad oder zu Fuß? (Web-Code: tfod-exee)
Natürlich kann das mit einer Schleife (iterativ) programmiert werden. Die Funktion fib() soll
das Element an einer vorgegebenen Position ausgeben (z. B. fib(7) = 13).
Iterative Lösung
public int fib ( int pos )
{
if ( pos < 3)
{
return 1;
}
int a = 1 , b = 1;
for ( int i = 2; i < pos ; i ++)
{
int next = a + b ;
a = b;
b = next ;
}
return b ;
}
Erklärung: In obigem Code bezeichnen a , b und next drei aufeinander folgende Glieder der
Fibonacci-Reihe. Nach dem Berechnen von next werden a und b entsprechend neu «abgefüllt»,
sodass next jeweils nur als temporäre Variable einen gültigen Wert aufweist.
53
Leonardo «Fibonacci» von Pisa (ca. 1180 – 1241) über das Wachstum einer Kaninchenpopulation (S. Wikipe-
dia)
123
11 Rekursion
Rekursive Lösung
Rekursiv ist das Programm aber einfacher, intuitiver und somit weniger fehleranfällig und besser
wartbar (leider aber auch weniger performant).
public int fib ( int pos )
{
if ( pos < 3)
{
return 1;
}
return fib ( pos - 2) + fib ( pos - 1);
}
Wir sehen, dass die rekursive Programmierung viel weniger fehleranfällig ist. Der Code wird kurz
und verständlich. In diesem Fibonacci-Beispiel hingegen ist der rekursive Code nicht wirklich
performant (= rasch in der Ausführung). Die iterative Lösung (Beispiel davor) ist hier immens
schneller, denn jede Zahl wird nur einmal berechnet. Wird in einer Rekursion die Funktion – wie
hier – gar doppelt aufgerufen, so wächst die Laufzeit quadratisch an.
Lösung mit Formel
Am schnellsten (wenn auch am wenigsten verständlich) ist eine (geschlossene) mathematische
Formel.
double phi = 1 . 6 1 8 0 3 3 9 8 8 7 4 9 8 9 4 8 4 8 2 0 45 8 6 8 3 4 3 6 5 6 3 8 1 1 7 7 2 0 3 0 9 1 7 98 ;
double wurzel5 = 2 . 2 3 6 0 6 7 9 7 7 4 9 9 7 8 9 6 9 6 4 0 9 1 7 3 6 6 8 7 3 1 2 7 6 2 3 5 4 4 0 6 1 8 3 5 9 6 ;
public int fib ( int pos )
{
double result ;
result = Math . pow ( phi , pos ) / wurzel5 ;
// Runden und zurueckgeben :
return ( int ) ( result + 0.5);
}
Das zweite Beispiel teilt eine Ebene mit beliebigen (also nicht parallelen) Geraden in Teilebenen.
Eine einzige Gerade teilt die Ebene in zwei Teilebenen, zwei Geraden teilen die Ebene bereits
in vier Teile, und kommt eine dritte Gerade hinzu, so kommen drei Teilebenen hinzu; die Ebene
wird also insgesamt in sieben Teile geschnitten (s. obige Grafik).
Begründen Sie, warum bei der n-ten Gerade genau n Teilebenen hinzukommen.
Begründen und implementieren Sie die folgenden Algorithmen (iterativ, rekursiv, mit Formel):
Iterative Lösung
int teilebenen ( int geraden )
{
int teilebenen = 1;
for ( int i = 1; i <= geraden ; i ++)
{
teilebenen = teilebenen + i ;
}
return teilebenen ;
}
Rekursive Lösung
int teilebenen ( int geraden )
{
if (0 == geraden )
{
return 1;
}
return geraden + teilebenen ( geraden - 1);
}
125
11 Rekursion
127
A | Anhang
«Was ich sonst noch alles sagen wollte»
Der Java-Compiler, wie auch die JVM, sind Teile des «Java Developer Kits», kurz JDK.
129
A Anhang
A.1.1 Installation
http://www.oracle.com/technetwork/java/javase/downloads/index.html
Starten die das Installationsprogramm für Ihr Betriebssystem.
Die Installation kann einfach in einer Konsole (command-line, cmd, shell, BASH) überprüft wer-
den. Dazu öffnen Sie die Konsole (Windows: Start -> Ausführen -> cmd; Mac/Unix/Linux:
Terminal oder xterm).
Tippen Sie nun folgendes in die Konsole
javac
Sollte Java nicht korrekt installiert sein, so erscheint ein Meldung im folgenden Stil:
Der Befehl " javac " ist entweder falsch geschrieben oder
konnte nicht gefunden werden .
oder
No command ’ javac ’ found , did you mean :
...
Falls Java nicht korrekt installiert ist, so könnten die folgenden Tipps weiterhelfen:
• Aufsuchen des Programms javac und den gefundenen Pfad in die Systemvariable %PATH%
zu verankern. Danach Neustart der Konsole ( cmd ).
• Suche im Internet nach «Installation JAVA PATH». Und geben Sie dabei noch den Namen
Ihres Betriebssystems an (z. B. Linux, Ubuntu, MacOS, Windows, ...).
Tippen Sie das erste Programm aus dem Einleitungskapitel (S. Seite 15) mit einem Texteditor
ab und speichern Sie dieses unter dem Namen « MeinErstes.java ».
Öffnen Sie die Konsole und springen Sie mit dem Kommando cd 54 ins Verzeichnis, wohin sie die
Quelldatei MeinErstes.java gespeichert hatten.
Tippen Sie nun
javac MeinErstes . java
Sollte gleich wieder die Kommandozeile (Eingabeaufforderung) erscheinen, so ist alles gut gelau-
fen.
Bei Erfolg gibt der Java-Compiler ( javac ) keine Meldung aus. Allfällige Fehlermeldungen
werden ausgegeben und müssen zuerst verbessert werden (s. Kap. A.1.3 auf Seite 132).
Werden keine Fehler gemeldet, so können Sie nun mit dem Befehl
java MeinErstes
das Programm starten. Nun sollte « Hallo Welt » in der Konsole erscheinen. Geschieht dies nicht,
versuchen Sie auch:
java - cp . MeinErstes
Sollte « Hallo Welt » immer noch nicht erscheinen, lesen Sie auf der nächsten Seite weiter im
Kapitel «Fehlerbehandlung».
54
cd bedeutet so viel wie «change directory» oder eben «wechsle das Verzeichnis».
131
A Anhang
A.1.3 Fehlerbehandlung
Wie überall, wo die Gattung «Homo» am Werk ist, können Fehler passieren. Die folgende Aufzäh-
lung zeigt, welche Fehlermeldungen bei den ersten Java-Programmen auftauchen können und
wie diese zu beheben sind.
• cannot find symbol : Ein Name wurde falsch geschrieben. Zum Beispiel string statt
String . (Achtung: Java unterscheidet zwischen Groß- und Kleinschreibung).
• package system does not exist : system wurde klein statt groß geschrieben.
• reached end of file while parsing : Könnte eine fehlende geschweifte Klammer sein.
• ; expected : Es wird ein Strichpunkt erwartet. Oft wird dieses Ende einer Anweisung
vergessen.
java.lang.NoSuchMethodError: main"
Dies wiederum bedeutet, dass die main() Methode entweder falsch geschrieben wurde, nicht
static ist oder falsche Parameter aufweist: Korrekt ist:
public static void main ( String [] args )
Bei Problemen lesen Sie dieses Kapitel nochmals durch bzw. suchen Sie im Internet nach der
exakten Fehlermeldung. Bei “www.google.com” wird man meistens fündig. Sollte es jedoch in
dieser Anleitung Fehler haben, so melden Sie dies bitte direkt dem Autor [email protected] .
133
A Anhang
A.2 Datentypen
Java kennt die folgenden Datentypen55 : Streng objektorientierte Sprachen kennen keine «pri-
mitive Datentypen»: Java wählte diesen Weg aufgrund der Performance (also der Rechenge-
schwindigkeit).
Datentyp Beschreibung Beispiele zugehöriger Java-Literale
(Konstanten)
boolean wahr oder falsch true , false
byte ganze Zahl -128 bis 127 55 , -83
short ganze Zahl (2 Byte), 1000 , -4000
-32 768 bis 32 767
int ganze Zahl (4 Byte) -200_000 , 1_500_000_000
long ganze Zahl (8 Byte) 7_000_000_000_000_000
float Dezimalbruch (4 Byte) 4.3f , 3.882f , -2.8e3f
double Dezimalbruch (8 Byte) Doppelte -3.141592653589793 , 3.48e-150
Genauigkeit
char Zeichen ’x’ , ’7’ , ’@’ , ’-’ , ’#’ , \n , \uCAFE ,
...
String Zeichenkette, Wort "Hallo Welt"
55
In Java sind alle Zahlen positiv oder negativ darstellbar; mit anderen Worten handelt es sich um «Vorzeichen
behaftete» Zahlen
• Java verwendet für die Ganzzahltypen ( byte , short , int , long ) das so genannte Zwei-
erkomplement (S. [GFG11] Aufgabe 1.9). Die Zahl -2 wird also im Bitmuster ...11 1111 1110
dargestellt.
• Große Zahlen 2_000_000 dürfen in Java, der verbesserten Leserlichkeit zuliebe, mit Un-
terstrichen ( _ ) versehen werden.
• Bemerkung zu float und double : Diese beiden Datentypen unterscheiden sich lediglich
durch die Genauigkeit. float ist weniger genau, benutzt auch weniger Speicher und ist in
der Regel auch schneller bei Berechnungen als der double . Sowohl bei float , wie auch bei
double kann mit einem hinten angestellten f (bzw. d ) der Datentyp spezifiziert werden.
• Ein Java- char ist ein Unicode-Zeichen, das mit 16 Bit kodiert ist. Mit \n wird das
«Neue-Zeile» Symbol geschrieben, was für einen Zeilenumbruch sorgt. Unicode-Werte kön-
nen auch Hexadezimal eingegeben werden. Dabei wird ein \u vorne hin gestellt. Beispiel
\u03C6 = ϕ
• Bemerkung zum String : der String ist kein primitiver Java-Datentyp. Dabei han-
delt es sich um eine Klasse, welche den Regeln der Objektorientierung folgt. Ich habe den
String dennoch aufgeführt, denn auch der String besitzt in Java eigene Literale (z. B.
"Hallo Welt" ).
135
A Anhang
56
1. Sequenz, 2. Selektion, 3. Iteration, 4. Unterprogramme, 5. alle Programmabläufe sind ohne «goto» (also
«Jumps») möglich.
137
A Anhang
/* *
* Java Starter , damit Sie rein strukturiert loslegen koennen ,
* ohne sich um Objekte kuemmern zu muessen .
* Aendern Sie den Filenamen ,
* den Klassennamen und
* den top () - Aufruf .
* " Ihre Dokumentation kommt hier ... "
*/
public class JavaStarter { // <- hier anpassen
public static void main ( String [] args )
{
new JavaStarter (). top (); // <- hier anpassen
}
139
A Anhang
Die Ausgabe von Werten in Java auf der Konsole ist relativ einfach:
System.out.println(...);
Texte und Zahlen werden in der Regel so formatiert, wie wir es gerne hätten. Um Ausgaben
aneinander zu fügen, verwenden wir den Plus-Operator:
int x;
String s ;
x = 4;
s = " Hallo ␣ Welt " ;
System . out . println ( " Ausgabe : ␣ " + x + s );
Zur Erinnerung ans Kapitel «Zeichenketten» (s. Kap. 7.5 auf Seite 97)
sei hier nochmals ein einfacheres Zahlenbeispiel gegeben.
Da der Plus Operator (+) auch für Zahlen verwendet wird, ist beim
ersten Betrachten nicht klar, was hier ausgegeben wird:
int x , y ;
x = 4;
y = 5;
System . out . println ( x + y + " ␣ ist ␣ " + x + y );
Zunächst werden die Zahlen (von links nach rechts) als Zahlen zusammen-
gezählt und danach wird die Zeichenkette " ist " angefügt. Schließlich
wird x und y je in eine Zeichenkette verwandelt und dem bestehenden
hinzugefügt. Resultat: 9 ist 45 !
Erinnern Sie sich an die Klammerungsreihenfolge von Links nach rechts.
Der «+»-Operator fügt genau dann Zeichenketten zusammen, wenn einer der beiden Operanden
eine Zeichenkette ist. Ansonsten werden einfach die Zahlen zusammengezählt.
Daten für die Programmausführung werden entweder aus persistenten Speichern (Datenbanken,
Files, . . . ) geladen, sie werden beim Programmstart mitgegeben, oder der Benutzer kann sie zur
Laufzeit eingeben oder laden lassen.
Alle Texte und Zahlen, die in einem Programm verwendet werden, gehören nicht in den Quellcode.
Texte und Zahlen können sich bei einer weiteren Ausführung des Programmes verändern. Somit
müsste ja das Programm angepasst werden
Hier einige Beispiele von Texten und Zahlen, die nicht in den Quellcode gehören:
• Beschriftungen von Buttons (Knöpfen) und Labels (Diese müssen Sprachunabhängig gehal-
ten werden.)
• Konstanten (z. B. Die Zahl π besser aus java.lang.Math verwenden statt 3.1415926 in
den Code fix eingeben.)
Geek Tipp 10
Nur die Zahlen Null (0) und Eins (1) bzw. die Literale false und true dürfen im Code auf-
tauchen. Andere Zahlen oder sprachabhängige Zeichenketten im Quelltext stören die Programm-
entwicklung permanent.
Persistente Daten werden aus Datenbanken oder Eigenschaftsdateien (sog. Property-Files) gele-
sen. Transiente Daten werden zur Laufzeit z. B. vom Anwender erfragt.
141
A Anhang
Das Einlesen von Text aus der Konsole gestaltet sich schon schwieriger. Gleich ein Beispiel:
import java . util . Scanner ;
String name ;
int alter ;
void top ()
{
System . out . print ( " Geben ␣ Sie ␣ Ihren ␣ Namen ␣ ein : ␣ " );
name = new Scanner ( System . in ). nextLine ();
System . out . println ( " Hallo ␣ " + name );
Ein komplexeres Beispiel wie dies gemacht werden könnte, finden Sie hier:
https://github.com/pheek/javaInput/blob/master/Input.java
143
A Anhang
Die Ausgabe System.out , sowie die Eingabe System.in können leicht in Dateien umgeleitet
werden.
Betrachten wir eine kleine Modifikation des vorangehenden Programmes (Beachten Sie, dass der
Scanner nun nur einmal mit new erstellt wird):
import java . util . Scanner ;
String name ;
String alterString ;
int alter ;
void top ()
{
System . out . print ( " Geben ␣ Sie ␣ Ihren ␣ Namen ␣ ein : ␣ " );
name = sc . nextLine ();
System . out . println ( " Hallo ␣ " + name );
Erstellen Sie zudem die folgende Datei ( daten.txt )mit den gewünschten eingaben:
James Gosling
60
Die Daten werden nun nicht mehr aus von der Konsole, sondern aus der Datei daten.txt gelesen.
Natürlich können die Resultate auch in eine Datei umgeleitet werden, anstatt auf der Konsole
ausgegeben zu werden:
java EinAus < daten . txt > ausgabe . txt
145
A Anhang
Die geschweiften Klammern (« { », « } ») bei if , else und while können in Java weggelassen
werden, sofern es sich im auszuführenden Anweisungsblock um genau eine einzige Anweisung
handelt.
Eine ausführliche Beschreibung dieser «Falle» habe ich hier veröffentlicht:
http://www.santis-training.ch/java/pitfalls.php#c
(Siehe dort im Kapitel «Iteration, Selektion und Strichpunkte».)
147
A Anhang
A.8.2 Schleifeninvarianten
In obigem Code wird die Zahl r jedes Mal neu berechnet, obschon sie von den Schleifenvaria-
blen ( i und z ) unabhängig also invariant ist. Der Code kann klarer und auch performanter
(=schneller in der Ausführung) wie folgt geschrieben werden:
int i = 1;
int k = 17;
int r = 5 * k + 3; // von i unabhaengig
while ( i <= 10)
{
int z = r + i;
print ( z );
}
Bemerkung: Ein guter Compiler kann das optimieren, sodass unter Umständen beide obigen
Programmteile gleich schnell ablaufen werden – gehen Sie jedoch nicht ungeprüft davon aus!
Die Selektion kann als Spezialfall der Iteration aufgefasst werden. Zusammen mit der while() -
Schleife und dem Konzept von Variablen kann ein if() wie folgt erzeugt werden:
boolean firstTime = true ;
while ( firstTime && bed ())
{
doSomething ();
firstTime = false ;
}
57
Erweiterte Backus-Naur-Form (s. Wikipedia)
149
A Anhang
(Die Syntaxstruktur findet sich hier: (s. Kap. A.9 auf Seite 149))
Dabei haben die Felder folgende Bedeutungen:
<modifier> :== Modifizierer: public, protected, private, static, strictfp, native, abstract,
final, synchronized.
<return-type> :== Rückgabewert: void | <Datentyp>
<name> :== Identifier: Name der Methode
<parameter> :== Übergabewerte: [final] <Datentyp> <Name>
Mit Kommata (,) getrennt.
<exception> :== Exception Type: Name der Exceptionklassen
Auch mit Kommata (,) getrennt.
<Block> :== Anweisungen
Bemerkung: In Java können Subroutinen nur innerhalb von Klassen stehen. Es gibt keine De-
klaration von Subroutinen außerhalb von Klassen, aber auch nicht direkt innerhalb von anderen
Subroutinen.
151
A Anhang
• ... werden die Parameterwerte (also die Argumente) auf den Stack kopiert und sind via
Parameter (hier die Parameter-Variable a ) zugreifbar,
• ... wird Platz für die lokalen Variable (hier s ) geschaffen, damit die Subroutine auch
temporäre Zwischenresultate speichern kann und
• ... wird zudem auch die Rücksprungadresse auf den Stack gelegt. Dies ist wichtig, damit
das Programm bei Beenden der Subroutine auch weiß, wo im Hauptprogramm weitergefah-
ren werden muss.
Denn: Wenn wir die return -Anweisung am Ende einer Subroutine weglassen, so wird diese vom
Compiler automatisch eingefügt.
Das return beendet die Subroutine automatisch, gibt den Speicher der temporären Variable
auf dem Stack wieder frei und springt zum aufrufenden Programm zurück (daher wurde die
Rücksprungadresse auch gespeichert).
Der Leserlichkeit halber wird diese abschließende return -Anweisung in der Praxis üblicherweise
weggelassen.
153
A Anhang
A.13.1 Kommentare
An jeder Stelle59 im Quelltext können Kommentare geschrieben werden. Diese dienen dem Ver-
ständnis des Codes und werden vom Compiler ignoriert.
• Alles was ab /* bis und mit */ steht, wird als Kommentar gesehen und gehört nicht zum
ausführbaren Programmcode.
• Das Symbol // bezeichnet einen Kommentar: Alles bis zum Zeilenende wird vom Compiler
ignoriert.
58
Für die EBNF Notation (s. Kap. A.9 auf Seite 149)
59
... mit Ausnahme innerhalb von Operatoren (z. B. ++ ) und Bezeichnern (z. B. Variablennamen) ...
Innerhalb einer Klasse ( class ) können die folgenden Klassenelemente (Members) auftreten (wo-
bei nur die ersten drei wirklich wichtig sind):
Member Beschreibung Beispiel Bemerkung
Attribut Objekt- String name; Jedes Objekt hat sei-
Eigenschaft nen eigenen internen Zu-
stand.
void add(float smd)
{
Methode Ereignisse Ereignisse verändern ty-
(Events), summe += smd; pischerweise den inter-
die auf die Objekte } nen Zustand (die Attri-
wirken können bute) der Objekte.
Konstruktoren Spezielle Methode, public Person () {...} Jedes Objekt wird durch
um Objekte zu er- einen Konstruktor (mit-
zeugen tels new ) erzeugt.
Statisches Klasseneigenschaft static int obCount; Jede Klasse kann auch
Attribut globale Variable aufwei-
sen, die für alle Objekte
verwendbar sind.
Statische Code, der bei Klas- static {...} Dieser Code wird beim
Initialisierer senerzeugung aus- Laden der Klasse ausge-
geführt werden soll führt. Ohne das Schlüs-
selwort static wird
der Code bei der Objek-
terzeugung ausgeführt.
Dynamischer Code, der nach { ...} Dynamische Initialisie-
Initialisierer super() , aber rer werden selten ge-
vor dem Rest braucht, da den Ent-
des Konstruktors wicklern oft nicht klar
ausgeführt wird. ist, wann diese ausge-
führt werden. Finger da-
von lassen!
Statische Klassenereignisse static int getYear() Werden meist für klas-
Subroutinen sische Funktionen, die
nicht von einem Objekt-
zustand abhängig sind,
verwendet.
Innere Klassen Klassen innerhalb class Xyz {...} Beispiel: AHV Nummer
bestehender Klas- als innere Klasse von
sen Person.
155
A Anhang
float getGewicht ()
{
return this . gewicht ;
}
}
/* imports */
import ch . programmieraufgaben . util .*;
import java . util . HashMap ;
Bemerkung A.1. Neben class sind auch interface oder enum als Inhalt einer Kompilations-
einheit möglich. Zu enum gibt es ein eigenes Kapitel (s. Kap. A.15 auf Seite 159).
157
A Anhang
A.14 Variable
Variable bezeichnen Speicherstellen. An ihrer Stelle können (binär kodierte) Werte stehen. Jede
Variable hat einen Namen (Bezeichner), einen Datentypen und allenfalls einen Wert. In JAVA
kommen Variable an verschiedenen Stellen zum Einsatz:
Lokale Variable Jeder Block (Subroutinen, Kontrollstrukturen, aber auch namenlose Blöcke)
kann lokale Variable deklarieren. Wichtig: Die lokalen Variablen erhalten
keinen Standardwert (Default). Sie müssen explizit definiert werden (z. B.:
int x; x = 7; ).
Parameter Methoden und Konstruktoren können mit einer formalen Parameterliste ver-
sehen sein. Diese Variablen sind nur innerhalb der Methode sichtbar und
leben nur solange (auf dem Stack) wie sich der ausführende Thread (Pro-
grammzeiger) innerhalb der Methode bzw. des Konstruktors befindet. Para-
meter haben keine Defaultwerte. Die Werte werden beim Aufruf der Methode
oder des Konstruktors vom aufrufenden Programmteil übergeben. Genauer:
Das aufrufende Programm wertet die Argumente aus, und die Werte (z. B.
Zahlen, Referenzen) werden auf den Stack geschrieben.
Attribute Variable, die innerhalb von Klassen, aber außerhalb von Methoden oder Kon-
struktoren deklariert werden, heißen (sofern nicht static ) Attribute. Diese
erhalten ihren Speicherplatz, sobald ein Objekt der entsprechenden Klasse
(oder einer Subklasse) generiert wird ( new ). Attribute erhalten beim Gene-
rieren (sofern nicht anders definiert) ihren Defaultwert (z. B. 0 für int ).
Klassenvariable Mit static markierte Felder sind Klassenvariable. Diese Variablen können
von allen Instanzen (Objekten) dieser Klasse verwendet werden und können
im Gegensatz zu Attributen für verschiedene Objekte keine verschiedenen
Werte annehmen. Pro Klasse existiert genau eine Speicherstelle für diese
Variable, die von allen Objekten geteilt wird. Auch dürfen die statischen
Methoden (Klassenfunktionen) auf diese Variable zugreifen. Egal wie viele
Objekte es gibt, die Klassenvariable kommt nur einmal vor und ist in die-
sem Sinne global. In einer Interface-Deklaration braucht das Schlüsselwort
static nicht angegeben zu werden. Die Variable erhält ihren Speicherplatz,
sobald der Classloader die Klasse in die virtuelle Maschine lädt. Klassenva-
riable erhalten - sofern keine Definition angegeben ist - automatisch den
Defaultwert.
Arraywerte Arrays (selten auch Felder genannt) bestehen aus Variablen desselben Da-
tentyps. Wenn ein Array generiert wird ( new A[<size>] ), erhalten alle
Elemente Speicherplatz und ihren Defaultwert. Objektreferenzen werden mit
null initialisiert.
Exception- Jeder Exceptionhandler ( catch(Exception e) {...} ) enthält ein Argu-
Handler ment des Datentyps der entsprechenden Exception (Ausnahme). Sobald die
Parameter Exception auftritt, wird ein Objekt der entsprechenden Klasse generiert. Die-
ses Objekt wird dem Argument übergeben und kann innerhalb des catch -
Blocks benutzt werden.
Bei dieser einfachen Implementation nominaler Werte wird jedem möglichen Wert eine ganze Zahl
zugeordnet:
// Beschreibung : 0= Montag , 1= Dienstag , 2= Mittwoch , ...
int ferienanfang = 4; // Freitag
...
// Freitag :
if (4 == ferienanfang )
{
...
}
Eine weitere Möglichkeit besteht darin, für jede vorkommende Zahl eine Variable (falls möglich
eine Konstante, die in Java mit dem Schlüsselwort final gekennzeichnet werden) zuzuweisen:
final int MONTAG = 0;
final int DIENSTAG = 1;
final int MITTWOCH = 2;
...
Zwar müssen wir nun die Bedeutungen nicht permanent nachschlagen; ein Nachteil bleibt trotz-
dem: der Variable ferienanfang kann jeder int -Wert zugewiesen werden. Um das etwas zu
umgehen, kann man auch mit Konstanten Strings arbeiten:
159
A Anhang
Die nun vorgestellte Variante unterscheidet sich zur vorangehenden dadurch, dass anstelle von
ganzen Zahlen ( int ) Strings (also Zeichenketten) verwendet werden. Gleich ein Beispiel:
final String MONTAG = " Montag " ;
final String DIENSTAG = " Dienstag " ;
final String MITTWOCH = " Mittwoch " ;
...
String ferienanfang = FREITAG ;
Vorteil: Eine künstliche Zuordnung zu den Zahlen fällt weg. Nachteil: Weiterhin kann jeder be-
liebige String der Variable ferienanfang zugewiesen werden.
Verwendet wird dies dann genau so wie im Beispiel vorhin. Nun ist Wochentag ein eigener Da-
tentyp und wird von der Syntax wie alle anderen Datentypen verwendet (siehe unten).
Im Gegensatz zur Lösung mit int oder mit String kann nun kein ungültiger mehr Wert zuge-
wiesen werden und den Werten sieht man ihre Bedeutung direkt an (z. B. Wochentag.FREITAG ).
Beispiel A.1. Beispiel: Ich will ausschließlich einen der vier folgenden Werte für meine Variable
zulassen
Geek Tipp 11
Verwende immer Enumerationstypen (enum) für nominale Werte!
161
A Anhang
Ist ein Kamel innerhalb einer Karawane nun Wasser-, Gepäck- und Personenträger (aber kein
Verpflegungsträger), so schreiben wir dafür das Bitmuster 1101 in eine Variable. Bemerke: Das
erste Bit (Bit mit Nummer 1) steht ganz rechts.
Mit sogenannten Masken-Konstanten können wir einem Kamel eine Eigenschaft hinzufügen, eine
Eigenschaft wegnehmen oder einfach auf eine Eigenschaft prüfen. Hier die vier benötigten Masken-
Konstanten:
int WASSER_TRAEGER = 1; // Bit 1 = 0000 0001
int VERPFLEGUNGS_TRAEGER = 2; // Bit 2 = 0000 0010
int GEPAECK_TRAEGER = 4; // Bit 3 = 0000 0100
int PERSONEN_TRAEGER = 8; // Bit 4 = 0000 1000
b) auf Nichtübereinstimmung:
if ( GEPAECK_TRAEGER != ( typ & GEPAECK_TRAEGER )) {...}
Tippfehler? if(4 = x)
Beachten Sie, dass 4 == x natürlich mit x == 4 mathematisch identisch ist. Die zweite Variante
läuft jedoch in einzelnen Sprachen (C, ...) Gefahr, sich bei Tippfehlern fatal zu verhalten. Die
Variante 4 == x ist hier etwas gewöhnungsbedürftig.
Generell: Wer die Konstanten links lässt, schreibt robusteren Code:
String s = ...;
if ( " abc " . equals ( s ))
{
...
}
Umgekehrt ( s.equals("abc") ) kann es hier sein, dass die Variable s noch nicht definiert ist
und eine sog. NullPointerException würde das Programm unsanft beenden.
Bemerkung: Je nach Programmiersprache ist eine andere Art die sinnvollste. Wichtig ist, dass Sie
die obige Tabelle verstehen und in Ihrer Programmiersprache versuchen, die Vor- und Nachteile
der einzelnen Bedingungen (Boole’sche Ausdrücke) kritisch zu hinterfragen.
163
A Anhang
Arrays können seit der JAVA 1.1 Version auch anonym definiert werden. Falls z. B. eine Metho-
de public int sum(int a[]) besteht, die alle Werte eines int -Arrays zusammenzählt, kann
diese Methode mit der Syntax für anonyme Arrays ...
int summe = sum ( new int []{1 , 5 , 3 , 2 , 4 , 4});
Tabellen oder Matrizen kennt Java von sich aus nicht. Da aber ein Array auch aus Arrays
bestehen kann, sind Tabellen einfach zu verwirklichen.
Hier nochmals das Beispiel aus dem Theorieteil (s. Kap. 6.2 auf Seite 89). Intern sieht die Um-
setzung technisch wie folgt aus:
Handelt es sich bei den Indizes nicht um Zahlen, so können (selbst für mehrdimensionale Tabellen)
in Java Map s verwendet werden. Hier eine Umsatztabelle nach Quartalen (q1 - q4):
q1 q2 q3 q4
Meier 48 000 36 000 52 000 68 000
Freimann 12 000 11 000 8 000 24 000
Huber 50 000 51 000 53 000 46 000
Müller 5 500 5 500 3 800 2 200
Guggisberg 38 000 36 000 48 000 54 000
Dabei wird eine Zeile wie folgt zugewiesen:
HashMap < String , Integer > meier ;
meier = new HashMap < String , Integer >();
meier . put ( " q1 " , 48 _000 );
meier . put ( " q2 " , 36 _000 );
meier . put ( " q3 " , 52 _000 );
meier . put ( " q4 " , 68 _000 );
Um die Zeilen nun eine «Tabelle» zu füllen, kann wieder eine Map verwendet werden und wir
erhalten mit diesem «Trick» die gewünschte Tabelle:
HashMap < String , HashMap < String , Integer > > tabelle ;
tabelle = new HashMap < String , HashMap < String , Integer > >();
In folgendem Beispiel besteht die Tabelle aus drei unterschiedlich langen Zeilen:
int [][] matrix =
{
{37 , 42} ,
{12 , 25 , 18} ,
{3 , 9}
};
165
A Anhang
Java verwendet für Strings Arrays (Felder) von Unicode Zeichen ( char ). Ein Unicode-Zeichen
benötigt 2 Byte und Arrays werden mit int angesprochen. Somit könnte ein Java-String theo-
retisch 4GiB an Speicher einnehmen (2 Milliarden Zeichen!). Leider konnte ich dies nicht tes-
ten, da solche Tests in der Regel zunächst den Heap komplett auffressen. Stringlängen von über
600 000 000 Zeichen sind jedoch auf heutigen PCs kein Problem. Mit anderen Worten: Sie können
in einer einzigen String-Variable den kompletten Text von über 500 Büchern speichern!
Die Länge eines Strings wird in Java mit der length() -Methode ermittelt:
meinString.length();
A.19.2 Teilstring
str.substring(5, 11)
Das Ende ( end ) wird in Java bei der String-Extraktion nicht mit eingeschlossen. end zeigt
auf das erste Zeichen im String, das nicht mehr zum Teilstring gehören soll. Dieser offensichtliche
Fehler ist ein Relikt aus der Programmiersprache C. Auch wenn es tatsächlich Algorithmen gibt,
die damit effizient arbeiten können, so ist der gewählte Variablenname ( end ) mit Sicherheit
falsch.
Aufgabe A.1 «Teilstring» Schreiben Sie ein Java-Programm, das vom Benutzer einen Text
abverlangt, schneiden Sie den ersten und den letzten Buchstaben ab und geben Sie den Teilstring
aus.
Strings sind nicht veränderbar. Mit diesem Wissen erstaunt der Output des folgenden Codes nicht:
String a = " Hallo ␣ ␣ " ;
String b = " ␣ ␣ ␣ Welt " ;
a . trim (); b . trim ();
System . out . println ( a + b ); // liefert " Hallo Welt "
Was ist geschehen? Die trim() -Funktion ändert nicht den gegebenen String, sondern es wird ein
neuer String erzeugt, welcher getrimmt, also frei von führenden und nachfolgenden Leerzeichen,
ist.
A.19.4 StringBuilder
Aus oben genannten Gefahren und weil zudem das Zusammenfügen von String s in Java eine
teure Angelegenheit ist, verwendet man häufig auch den StringBuilder . Dieser reserviert sich
von vornherein 16 Zeichen (32 Byte) und muss erst Speicher neu allozieren, wenn diese 16 Zeichen
aufgebraucht sind.
Beispiel:
StringBuilder sb = new StringBuilder ( " Hallo " );
sb . append ( " ␣ " );
sb . append ( " Welt " );
System . out . println ( sb );
167
A Anhang
A.19.5 equals
Wir haben im Kapitel über Zeichenketten bereits gesehen, dass Strings nicht mit dem « == »-
Operator verglichen werden sollten, wenn wir uns dafür interessieren, ob zwei Strings die selbe
Zeichenfolge beinhalten.
Hier aber noch eine Möglichkeit, bei der der « == »-Operator trotzdem funktioniert. Hat man es
in einem Programm sehr häufig mit denselben Zeichenketten zu tun60 , dann kann die intern -
Funktion sehr nützlich sein:
a = "Hallo"; b = "Hallo"; a == b
a = new String(b); a != b
a = a.intern(); a == b
Im internen Konstanten String-Pool, kommt jeder Text genau einmal vor. Zum einen werden
dort alle Literale aus dem Programmcode abgelegt. Mit der Funktion intern() kann ein Text
ebenfalls dahin verschoben werden!
60
Dies ist der Fall, wenn wenige Strings jeweils mehrere hundert Male im System auftauchen sollten.
Die Java-File Klasse beinhaltet also lediglich einen Filenamen. Ob die Datei existiert und ob
es sich dabei um ein Textfile, ein Bild oder gar um ein Verzeichnis handelt, interessiert Java
vorerst nicht. Interessant sind daher vor allem folgende Methoden, wenn wir uns im Dateisystem
bewegen:
exists ()
isDirectory ()
listFiles ()
isFile ()
canRead ()
canWrite ()
Aufgaben zu File
Aufgabe A.2 « java.io.File » Schreiben Sie ein Programm, das ein Verzeichnis einliest und
alle darin enthaltenen Files ausgibt.
Zusatz 1: Geben Sie von jedem Inhalt jeweils an, ob es sich um ein Verzeichnis oder eine Datei
handelt.
Zusatz 2: Tauchen Sie rekursiv in alle Unterverzeichnisse ab.
169
A Anhang
Eine typische Anwendung einer File-Verarbeitung ist das zeilenweise Einlesen, Verarbeiten und
Ausgeben. Die folgende Demo-Anwendung liest eine Datei (Eingabe) ein, schreibt vor jede Zeile
die Zeilennummer und schreibt dies in eine neue Datei (Ausgabe). Um das ganze etwas span-
nender zu machen, sollen bei Leerzeilen keine Zeilennummern ausgegeben werden, jedoch soll die
Zeilennummer dennoch erhöht werden.
import java . io .*;
String line ;
int lineNumber = 0;
void top () {
try (
Reader r = new FileReader ( IN_FILE_NAME );
LineNumberReader in = new LineNumberReader ( r ) ;
Writer out = new FileWriter ( OUT_FILE_NAME );
)
{
line = in . readLine ();
while ( null != line ) {
lineNumber = lineNumber + 1;
if ( line . trim (). length () > 0) {
out . write ( lineNumber + " . ␣ " );
}
out . write ( line + " \ n " );
line = in . readLine ();
}
}
catch ( IOException iox ) {
System . out . println ( " Fehler ␣ mit ␣ Datei : ␣ " + iox );
}
} // end method top ()
171
A Anhang
Eine spezielle Möglichkeit, das Vor- bzw. Nachlesen abgekürzt zu notieren, bietet Java im
while() -Header.
Anstelle von
String line = in . readLine (); // Vor - Lesen
while ( null != line ) {
behandle ( line );
line = in . readLine (); // Nach - Lesen
}
Hier wird zunächst ein String eingelesen und in die Variable line geschrieben. Da in Java
eine Zuweisung gleichzeitig ein Ausdruck (Term) mit einem Wert ist, kann dieser Wert gleich im
Vergleich eingesetzt werden. Sobald der Wert null von readLine() zurückgegeben wird, so
bezeichnet dies das Ende der Datei.
Dies funktioniert, da die Zuweisungs-Anweisung in Java auch als Ausdruck verwendet werden
kann.
Zeilen oder Zeichen können auch ans Ende einer bestehenden Datei angefügt werden. Dazu be-
nötigt der FileWriter beim Erstellen einen zweiten Parameter ( true ).
Das folgende Beispiel zeigt, wie Textzeilen ans Ende der Datei Ausgabe.txt angefügt werden:
File out = new File ( " Ausgabe . txt " );
try ( FileWriter fw = new FileWriter ( out , true )) {
fw . append ( " Eine ␣ letzte ␣ Zeile \ n " );
} catch ( IOException iox ) {
System . err . println ( " Fehler : ␣ " + iox );
}
Aufgabe A.3 «Top» Schreiben Sie ein Programm namens «Top», das von einer Text-Datei die
ersten sieben Zeilen ausgibt.
Zusatz: Lesen Sie den Filenamen über die main -Parameter ein (s. Kap. A.23 auf Seite 187).
Aufgabe A.4 «Zähler» Schreiben Sie ein Programm, das von einer Text-Datei alle Zeilen zählt
und diese Anzahl auf der Konsole ausgibt.
Zusatz: Zählen Sie alle Zeilen, Zeichen, alle Buchstaben und alle Wörter.
Zum Testen können Sie folgenden Text verwenden:
173
A Anhang
Java verwendet für die Datenverarbeitung sog. Filter. Ein Filter kann man sich vorstellen wie
eine Umwandlung von einem Datenstrom in einen anderen. So kann ein Filter zuständig sein,
dass die Daten zunächst aus einer *.zip -Datei gelesen werden. Der zweite Filter verwandelt die
Bytes anhand einer Kodierung in Unicode-Character ( char ). Ein dritter Filter ist zuständig, um
die Datei zeilenweise einzulesen und so fort:
Im klassischen EVA-Prinzip61 werden die Daten typischerweise von einem Datenstrom (Datei
oder Netzwerk-Ressource) gelesen62 . Nach dem Filtern in unser gewünschtes Format werden die
Daten verarbeitet. Zum Schluss wird die Ausgabe aus unserem Programm wieder in ein neues
gewünschtes Format gefiltert und so in den Ausgabedatenstrom gesendet:
61
EVA = Eingabe-Verarbeitung-Ausgabe
62
Ein Datenstrom ist in Java eine Aneinanderreihung von byte . Beim Lesen aus einem Datenstrom, insbe-
sondere bei Musik oder Videos von einer Netzwerk-Ressource, sprechen wir oft auch von streamen.
Das folgende Beispiel (nächste Seite) zeigt ein Filter-Chaining, um aus einer Datei einen Text
zeilenweise auslesen zu können. Ein FileReader ist im Grunde nichts anderes als das Chaining
eines FileInputStream mit einem InputStreamReader . Der FileInputStream liest byteweise
von einer Datei (im Gegensatz z. B. von einem Stream einer URLConnection , welche Daten von
einer Netzwerkressource liest). Der InputStreamReader ist zuständig, die Text-Kodierung (utf-
8, Latin, EBCDIC, ...) aufzulösen und um aus den byte char herzustellen. Zu guter Letzt ist
ein BufferedReader eingesetzt, der die Zeilenenden erkennt und im Wesentlichen eine Funktion
readLine zur Verfügung stellt:
175
A Anhang
Klassisches Filter-Chaining
void top ()
{
String inFileName = " test . txt " ;
String encoding = " utf -8 " ;
// Verwenden Sie zum Test die folgenden Kodierungen :
// " utf -8" , " iso -8859 -1" , " Cp437 ", " Cp850 ", " Cp500 "
try (
FileInputStream fis = new FileInputStream ( inFileName ) ;
InputStreamReader isr = new InputStreamReader ( fis , encoding );
BufferedReader br = new BufferedReader ( isr ) ;
)
{
String line ;
while ( null != ( line = br . readLine ()))
{
System . out . println ( " Gelesen : ␣ " + line );
}
} catch ( IOException iox ) {
System . out . println ( " Fehler ␣ in ␣ der ␣ Fileverarbeitung : ␣ " + iox );
} // end of try - catch
} // end of top ()
Bemerkung A.2. Für die Bedingung in der while() -Schleife siehe Seite 172.
Den Readern und Writern aus dem vorangehenden Kapitel liegen sog. Datenströme zugrunde. Ein
Datenstrom ist eine Reihe von Bytes, wohingegen die Reader und Writer Zeichen (char) liefern
und somit zur Textverarbeitung geeignet sind.
Viele Anwendungen gehen aber tiefer als die reine Behandlung von Texten und Verarbeiten die
Daten byteweise. Dabei kann es sich um Files, aber auch um Datenströme von einem Netzwerk
handeln.
Die folgende Tabelle hilft zu entscheiden, welche der Java-Objekte im Zusammenhang mit
Readers , Writers und Streams verwendet werden sollte.
Das folgende Hauptprogramm gibt eine Datei Byteweise auf der Konsole aus:
void top () {
try (
FileInputStream fis = new FileInputStream ( " inFile . bin " );
) {
int inByte ;
while ( -1 != ( inByte = fis . read ()))
{
System . out . println ( inByte + " ␣ " );
}
} catch ( IOException iox ) {
System . out . println ( " Fehler ␣ in ␣ der ␣ Dateibehandlung : ␣ " + iox );
}
}
Beachten Sie, dass der Befehl read() die Zahl -1 zurückgibt, wenn das Ende der Datei erreicht
ist. Die Bytes werden in Werten von 0 bis 255 wiedergegeben und nicht wie sonst in Java üblich
von -128 bis +127.
Bemerkung A.3. Die Bedingung in der while() -Schleife ist hier analog zur readLine() -Funktion
für Zeichenketten (s. Kap. A.20.2 auf Seite 172).
177
A Anhang
Encoding Zeichen werden durch Bytes (in Java 8-bit / ASCII 7-Bit) bzw. durch Characters
(in Java 16 Bit) repräsentiert. Bytes werden vorwiegend in Datenströmen (Files bzw. Netzwerk-
verbindungen TCP/IP) verwendet.
Nun können wir uns die Frage stellen, warum es dann die Unterscheidung in byte und char
überhaupt braucht. Java- byte sind einfach eine Aneinanderreihung von je 8 Bit. Welche Buch-
staben sich dahinter verbergen ist aber von System zu System verschieden. Die Zeichen ( char )
werden durch sogenannte Kodierungen (encodings) festgelegt. Ein FileReader , wie wir ihn oben
verwendet hatten, verwandelt eine Datei ( byte -Strom) in einen Zeichenstrom, indem die Kodie-
rung des aktuellen Betriebssystems verwendet wird. Auf modernen Betriebssystemen ist das meist
die UTF-8 Kodierung.
Unicode 16-Bit Zeichen werden in Java im RAM verwendet. Je nach Kodierung liegt einem
Zeichen ein anderes Bitmuster (oben als Hex-Wert angegeben) zugrunde. Im RAM der Java
virtuellen Maschine (JVM) wird jedoch stets Unicode verwendet. So brauchen wir uns lediglich
beim Lesen und Schreiben außerhalb des Programmes (Files, Network I/O, . . . ) um die Kodierung
zu kümmern.
Die folgende Tabelle zeigt den Sachverhalt anhand der Zeichen «K» und «ä»:
Files / Networking ( Streams ) Java (char / Strings)
( Readers / Writers )
Bytes ( byte ) Characters ( char )
8 Bit (= 1 Oktett) 16 Bit (= 1 char)
256 mögliche Zeichen 65’536 mögliche Zeichen
Zeichen Kodierungen: Kodierungen in Java:
Immer Unicode ( www.unicode.org )
K #4B = ASCII ’K’ 0x004B = ’K’ (= «00» + ASCII)
#D2 = EBCDIC ’K’
ä #E4 = ANSI ’ä’ 0x00E4 = ’ä’ (= «00» + ANSI)
#8A = Mac ’ä’
#84 = Cp-437 ’ä’
#C3A4 = UTF-8 ’ä’
Java verwendet intern für char (und somit auch für Strings) die Unicode-Kodierung. Siehe
http://www.unicode.org .
Aufgabe A.6 «Filevergleich» Zwei Dateien sollen geöffnet werden und der Inhalt byteweise ver-
glichen werden. Wenn die Dateien identisch sind, dann und nur dann soll true zurückgegeben
werden - ansonsten false .
Aufgabe A.7 «Lesbar» Eine Datei soll zum Lesen geöffnet werden. Daraus soll eine neue Da-
tei geschrieben werden, die nur die Buchstaben und die Leerschläge und die Satzzeichen Punkt
und Komma enthält, welche in der Originaldatei vorhanden waren. Alle anderen Zeichen sollen
ignoriert werden.
179
A Anhang
A.20.7 Randomaccess-Files
Manchmal wollen wir eine Datei aber nicht nur sequentiell lesen sondern innerhalb der Datei
Ausschnitte lesen bzw. schreiben.
Gleich ein Beispiel:
File dataFile = new File ( " dataFile . raw " );
RandomAccessFile f = new RandomAccessFile ( dataFile , " rw " );
Aufgabe A.8 «PNG» Schreiben Sie ein Programm, das von einer PNG (Portable Network Gra-
phics) Datei die Header ausgibt.
Das PNG-Fileformat ist wie folgt aufgebaut:
– Ein Chunk beginnt mit der Chunk Größe (4 Byte), danach kommt der Chunk Name (4
Byte), danach kommen die Chunk Daten (die Länge wurde in der Größe angegeben).
und zuletzt folgen nochmals 4 Byte Prüfsumme (CRC).
– Der Letzte Header hat Länge 0 und heißt IEND. Sobald dieser Header erreicht ist,
folgen keine PNG relevanten Daten mehr.
• List: Die Elemente sind geordnet, d. h. in derselben Reihenfolge, wie sie eingefügt werden.
• ArrayList: Auf die Elemente kann via Index zugegriffen werden. Das Einfügen am Anfang
oder in der Mitte ist zeitaufwändig.
• LinkedList: Geordnete Sequenz, bei der am Anfang und am Ende hinzugefügt, abgeholt
und gelöscht werden kann. Somit kann eine LinkedList auch als Queue , Deque oder
Stack verwendet werden. Das Einfügen in der Mitte geht sehr rasch.
• HashSet: Set, der mit einer Hash-Tabelle Elemente einfügt und wieder finden kann. Es
entspricht etwa einer HashMap , bei der aber nur Keys und keine Values eingefügt werden.
• SortedSet, TreeSet: Set, der die Elemente sortiert. Bei der Implementation TreeSet wird
ein Baum als abstrakter Datentyp eingesetzt. Die compareTo() Funktion wird verwendet,
um Elemente an die richtige Position einzufügen.
181
A Anhang
Oftmals reichen ganze Zahlen nicht als vernünftige Indizes aus. Hier kommen Maps zum Zuge.
• HashMap: Klassische assoziative Arrays. Bei assoziativen Arrays sind die Indizes nicht
Zahlen, sondern beliebige Schlüsselobjekte.
• LinkedHashMap: Wie die HashMap , jedoch wird zusätzlich eine Liste geführt, in welcher
Reihenfolge die Objekte hinzugefügt wurden.
• WeakHashMap: Objekte dürfen bei Nichtgebrauch vom Garbage Collector vernichtet wer-
den.
Oft ist es nicht einfach zu sehen, welche Collection bzw. Map dann für ein Problem die geeignetste
Form ist. Hier eine Hilfe in Zweifelsfällen.
Ist ein schneller Zugriff über Schlüsselobjekte nötig?
• Map
Dürfen länger nicht benutzte Objekte vom Garbage Collector abgeräumt werden?
• WeakHashMap
• List
• LinkedHashMap
Werden nur ab und zu, bzw. beim Initialisieren Daten hinzugefügt bzw. entfernt?
• ArrayList
Gibt es eine natürliche Ordnung (z. B. alphabetisch) auf den Daten bzw. auf den Schlüsseln?
Kann jedes Objekt nur einmal gespeichert werden? (Duplikate sind nicht gestattet.)
• Set
Natürlich ist es auch möglich, eigene generische Datentypen herzustellen. Ein Beispiel findet sich
im Anhang: (s. Kap. A.22 auf Seite 184)
183
A Anhang
Das erste Kettenglied hat keinen vorgaenger (wie auch das letzte keinen nachfolger hat).
Dies ist in der Grafik durch einen NIL-Pointer64 dargestellt.
63
Eine Ausnahme bilden die Sammelcontainer, welche die enthaltenen Objekte sortieren können. Hier wird meist
noch eine Funktion zum Vergleich der Objekte gefordert (z. B. gleich() , kleinerAls() , vergleicheMit() ).
64
NIL = «not in list» wird in Java mit dem null -Pointer implementiert.
public Kette () {
listeLeeren ();
}
185
A Anhang
public T getData () {
return data ;
}
} // end of class Kettenglied
Der generische Datentyp <T> sagt Java, dass hier ein beliebiges Objekt verwendet werden darf.
Es muss sich jedoch bei allen Objekten um Objekte derselben Klasse handeln.
So erhält die Variable String[] args in der Java-spancodemain-Methode die drei Argumente
Ach ja, hier noch ein möglicher Quelltext des Programmes "Hitch":
package buchBeispiele . anhang ;
187
A Anhang
Oft werden die String[] args verwendet um Filenamen (z. B. inFileName und outFileName )
anzugeben. Wenn jemand einen der beiden Namen vergisst, so soll eine Angabe zur typischen
Verwendung (usage) ausgegeben werden:
public class MainUsage {
Mittlerweile gibt es auch einige Frameworks, die einem das Parsen von umfassenden Kommando-
Argumenten erleichtern65 .
65
Siehe z. B. Usage Scenarios bei http://commons.apache.org/proper/commons-cli/
• int (s. Kap. 2.1 auf Seite 34) Hier wird ein Überlauf (Overflow) generiert.
191
C | Link-Verzeichnis
• http://commons.apache.org/proper/commons-cli/ : Umgang mit Kommandozeilenar-
gumenten
• http://www.oracle.com/technetwork/java/javase/downloads/index.html : Download
Java
• http://www.programmieraufgaben.ch : Aufgabenbuch zu diesem Skript
193
D | Literaturverzeichnis
195
| Literatur
[Fre15] Freimann, Philipp G.: Objekte und Klassen. 2015
[Him01] Himanen, Pekka: Die Hacker- Ethik. Und der Geist des Informations- Zeitalters. Rie-
mann Verlag, 2001. – ISBN 978–3570500200
[Knu97] Knuth, Donald E.: The Art of Computer Programming. Addison Wesley, 1997 http:
//www.oose.de/uml. – ISBN 0–201–89683–4
197
E | Stichwortverzeichnis
++ -Operator, 31, 59 BRAV, 48
BufferedReader , 96, 176
−− -Operator, 31
byte , 21, 134
< , 40
<= , 40 casting, 22
cd : change directory, 136
== , 40, 97, 168
char , 21, 134, 177
> , 40
Literal, 98
>= , 40 Chunk, 180
&& , 41 Classloader, 158
CMD, 136
ˆ , 41 Collection, 103
|| , 41 Collection , 107, 181
collection framework, 181
Abbruchbedingung, 57 Collection Framework, 107
Abfolge, 29 Compiler, 129
Abkürzung, 41
Dank, 13
add() -Methode, 103
Datei, 96, 169
Algorithmus, 111 Daten
Aneinanderreihung, 29 metrische, 22
Anweisung, 29, 30 Datenstrom, 177
Argument, 69, 70 Datenstruktur, 101
Array, 85 Datentyp, 17, 25, 134
assoziatives, 104 Datentypen, 21
Syntax, 87 De Morgan’sche Regeln, 43
ArrayList , 103, 106, 181 debuggen, 126
Arrays, 164 Dekomposition
anonyme, 164 funktionale, 65
Arrays und Schleifen, 88 del : Delete, 136
ASCII, 178
Deque , 181
Ausdruck, 17, 18, 75
Ausgabe, 140 dir : Directory Listing, 136
Auswahl, 37 Divide et Impera, 65
Auswertungsreihenfolge, 20 do , 57
BASH, 130, 136 DOS, 136
Befehl, 29 double , 21, 134
Bezeichner, 26 DUFTE, 50
BigInteger , 23
eclipse, 136
binär, 19 Eingabe, 140
Bit, 39 Einlesen, 140
Bit, Binary Digit, 21 else , 38
byte , 177 Encoding, 178
Boole, George, 21 End of File, 171
boolean , 21, 40, 134 Entscheidung, 37
199
STICHWORTVERZEICHNIS
201
STICHWORTVERZEICHNIS
Subroutine, 65 Wörter
Überladen, 151 reservierte, 139
Java-Syntax, 150 Wächter, 79
Subroutinen while , 55, 57
Anmerkungen, 147 Wissenschaftlichen Notation, 22
Substring, 166 write() -Funktion, 170
switch , 46 Writer, 170
Syntax Writer , 177
Arrays, 87
System.in , 142 Zahlen, 21
ganze, 21
System.out.println , 15, 29, 140
gebrochene, 22, 48
Zählervariable, 59
Tabelle, 89
Zeichenketten, 93
Teile und Herrsche, 65
Teil, 166
Terminal, 136
Vergleich, 97
ternär, 19
Zufallszahl, 119
Tree, 104
Zugriff
TreeSet , 104, 181 auf Attribute, 102
Typumwandlung, 22 Zusammenfügen
von Strings, 94
Überladen Zuweisung, 33
Unterprogramm, 151 Zweiersystem, 21
unär, 19
Unicode, 98, 135, 166
Unterprogramm, 65
Unveränderbarkeit
von Strings, 167
Usage der main-args , 188
UTF-8, 98, 178