Fortsetzung von letzter Seite
Wiederholungsstrukturen
Des öfteren ist es erforderlich, dass die gleichen Anweisung mehr
als einmal ausgeführt werden müssen. Man könnte nun die
Anweisungen manuell durch Mehrfacheingabe wiederholen. Das ist jedoch
sehr umständlich und sehr oft nicht zweckmäßig. Was sollte
man z.B. machen, wenn man solange etwas wiederholen möchte, bis der
Benutzer eine bestimmte Taste drückt.
Für solche und andere Fälle gibt es Wiederholungsstrukturen.
Diese wiederholen eingeschlossene Anweisungen abhängig von einer
Bedingung (oder eben nicht).
Schleife
mit Anfangsabfrage
Wie immer gibt es verschiedene Methoden Anweisungen zu wiederholen. Die
erste ist die mit Anfangsabfrage, d.h. die Bedingung wird immer VOR der
Ausführung der Wiederholung geprüft. Diese Schleifenart wird
auch kopfgesteuerte Schleife genannt, da die Anweisung im Schleifenkopf steht. Neben dem Schleifenkopf gibt es noch den Schleifenkörper,
in dem die Anweisung(en) stehen.
Bei dieser Schleifenart kann es vorkommen, dass die Schleife nie ausgeführt
wird, da die Schleifenbedingung von Anfang an nicht erfüllt wird. Solange die Schleifenbedingung aber von Anfang an wahr ist, solange wird die Schleife durchlaufen.
In der obigen Definition habe ich die Worte solange und wahr kursiv geschrieben. Diese Worte entsprechen nämlich ungefähr
dem Java-Syntax für eine Schleife mit Anfangsabfrage. Die exakte
Syntax sieht so aus:
while(Bedingung) Anweisung;
Zu deutsch also: Solange (Bedingung =
wahr) Anweisung. Wie schon bei den vorhergehenden Kontrollstrukturen
ist auch bei den Schleifen die mehrzeilige Anweisung in geschweifte Klammern
zu schreiben.
Bei allen Schleifen besteht die Gefahr einer Endlosschleife. Bei der Endlosschleife
ist die Bedingung falsch gewählt und zwar so, dass die Bedingung
immer wahr ist und somit die Schleife unendlich oft wiederholt wird. Da
der Benutzer keinen Einfluss mehr auf das Programm hat, dass immer weiter
die gleichen Anweisungen ausführt, gilt das Programm als abgestürzt.
Im schlimmsten Fall muss der Computer neugestartet werden. Aus diesem
Grund ist es unbedingt erforderlich, dass die Schleifenbedingung genau
überprüft wird, damit es nicht zu Endlosschleifen kommt.
Noch schlimmer ist nur noch, wenn man auch noch den Fehler begangen hat,
ein Semikolon hinter while (Bedingung) zu setzen, wenn danach noch in
geschweiften Klammern die Anweisungen stehen. Dann nämlich ist für
den Benutzer nicht ersichtlich, was der Computer gerade macht. Da er immer
unentwegt nichts macht, kommt es zu einem Hänger, der die gleichen
Auswirkungen wie die Endlosschleife hat.
Schleife
mit Endabfrage
Die zweite Möglichkeit der Wiederholung von Anweisungen ist die Schleife
mit Endabfrage. Die Schleife wird auch fußgesteuerte Schleife genannt,
da die Bedingung im Schleifenfuß steht. Die Schleife mit Endabfrage
wird IMMER mindestens einmal durchlaufen. Die Schleife wird solange
wiederholt, solange die Bedingung wahr ist. Somit ergibt sich folgende
Java-Syntax:
do { Anweisung; } while (Bedingung);
Ansonsten gelten die gleichen Grundlagen wie bei der Schleife mit Anfangsabfrage.
Zählschleife
Die Zählschleife ist eine Schleife, bei der von Anfang an feststeht,
wie viele Wiederholungen ausgeführt werden. Dabei wird die Anzahl
der Wiederholungen von der Laufvariable (dem Zähler) gesteuert. Die
Laufvariable erhöht sich bei jeder Ausführung um eine festgelegte
Größe. Die Schleife wird solange wiederholt, bis der vorher
festgelegte Endwert von der Laufvariable erreicht worden ist.
Die Syntax sieht etwas komplizierter aus, ist aber halb so schlimm:
for (Laufvariable=Anfangswert; Laufvariable<=Endwert; Laufvariable++)
Anweisung;
Die Anweisung lässt sich in fünf Teile zerteilen.
Das erste ist das Schlüsselwort for. In der Klammer stehen die nächsten
drei jeweils getrennt durch ein Semikolon.
Erst kommt die Initialisierung der Laufvariable (Laufvariable=Anfangswert;),
der Laufvariable wird der Anfangswert zugewiesen. Es folgt die Bedingung
(Laufvariable<=Endwert;), bei der selbstverständlich die Laufvariable
kleiner oder gleich dem Endwert sein muss. Ist das nicht mehr der Fall,
wird die Schleife verlassen.
Die letzte in der Klammer stehende Anweisung (Laufvariable++) zeigt einen
der Ursprünge von Java. Es handelt sich um einen sogenannten Postinkrementoperator,
den der gebildete Programmierer aus C++ (gibt C++ seinen Namen) kennt.
Dieser Postinkrementoperator tut nichts anderes, als die Laufvariable
um eins zu erhöhen.
Nach der Klammer folgt dann selbstverständlich die Anweisung.
Arrays
Bis jetzt bestanden Variablen nur aus einem Wert, einer Zahl
oder einem String. Arrays aber können eine beliebige Anzahl
von Werten des gleichen Typs beinhalten. Bei der Erzeugung eines
Arrays wird auch die Größe des Arrays festgelegt, d.h. wie
viele Elemente gleichen Typs im Array gespeichert werden können.
Das Array selbst besteht aus Feldern, diese sind mit dem sogenannten Index
durchnummeriert und werden über den Index angesprochen. Der Index
ist also die Adresse der Elemente im Array. Der Index beginnt mit null,
und endet mit n-1. Dass heißt wenn man ein Array von der Größe
sechs erzeugt, dann beginnt es mit null und endet mit fünf.
Das Array selbst wird, wie eine Variable auch, über seinen Namen
angesprochen, da es aber viele Elemente (n + 1) des gleichen Typs enthält,
wird zusätzlich noch der Index mit angegeben. Die Namensgebung des
Arrays unterliegt den selben Beschränkungen wie eine Variable.
So jetzt wollen wir aber in die Java-Syntax einsteigen. So wird allgemein
ein Array erzeugt:
Datentyp arrayName[];
Also z.B. int noten[];
So deklariert man zwar das Array, jedoch hat man es noch nicht dimensioniert.
Das geht allgemein so:
arrayName = new Datentyp[Anzahl der Elemente];
Also z.B. noten = new int[10];
Wenn man es also ausführlich machen will sieht eine komplette
Deklaration so aus:
int noten[];
noten = new int[10];
Man kann sich jedoch auch weniger Arbeit machen und das ganze aufeinmal
machen:
int noten[] = new noten[10];
Mehrdimensionale
Arrays
Es ist außerdem möglich, sogenannte mehrdimensionale Arrays
mit Java zu erzeugen. Die Syntax ist der eines eindimensionalen Arrays
sehr ähnlich. Die Syntax lautet:
Datentyp arrayName[] [] = new Datentyp [wert1] [wert2];
Also z.B. int noten[] [] = new int [5] [7];
Der Zugriff auf ein mehrdimensionales Array erfolgt genauso wie bei
dem eindimensionalen Array mit dem Arraybezeichner und den Indices.
Exception
Handling
In jedem noch so guten Programm kann auch mal ein Fehler auftreten (z.B.
von außen: Papier von Drucker leer). Der Fehler ist nur noch halb
so schlimm wenn man dafür eine gute und wirksame Fehlerbehandlung
(Exception Handling) vorsieht.
In Java ist die Fehlerbehandlung nicht besonders schwierig. Fehleranfällige
Programmteile werden von den Schlüsselwörtern try und catch umgeben. Das sieht ungefähr so aus:
try { Anweisungen } catch() { Anweisungen bei Fehlern }
Wie zu sehen ist steht im Block nach try der Programmcode
der auf Fehler zu überwachen ist und hinter catch folgt
der Programmteil, der dann ausgeführt wird, wenn der Fehler abgefangen
wurde. Es sind auch mehrere catch-Blöcke möglich.
Im catch-Block muss die Art der Exception angegeben werden.
Möglich Exception-Arten sind: catch(StringIndexOutOfBoundsException
e) für Fehler die beim Zugriff auf Strings entstehen; catch(NumberFormatException
e) für Fehler die beim Umwandeln in einen anderen numerischen
Datentyp entstehen oder catch(Exception e) für alle anderen
Fehler. Die mögliche Art der Exception lässt sich herausfinden,
indem man nach den eingesetzten Anweisungen (z.B. Integer.parseInt)
in der JDK-Dokumentation sucht. In der Beschreibung befindet sich auch
die mögliche Exception Art. Nach der Exception-Art muss noch eine
Variable stehen über die dann Informationen über den Fehler
ausgegeben werden können (z.B. e.toString()).
Werden mehrere catch-Blöcke eingesetzt, so sollte immer
der allgemeine catch-Fall (catch(Exception e)) zu letzt stehen,
da alle catch-Fälle der Reihe nach aufgerufen werden bis
der erste auf den Fehler zutrifft. Steht der allgemeine nicht am Ende,
so kann man keine spezifische Fehlerbehandlung vornehmen.
Sollen Anweisungen im Fehlerfall nochmals wiederholt werden (z.B. Eingabe
von Zahlen), so muss man diese in eine Endlosschleife packen, die könnte
so aussehen:
while(true) { System.out("Ganze Zahl: "); try { i = Integer.parseInt(in.readLine()); break; } catch (IOException e) { System.out.println("IOException\n"+e); System.exit(0); } catch (NumberFormatException e) { System.out.println("NumberFormatException\n"+e); } }
Mit System.exit(0) wird das Programm abgebrochen.
Das von dem ImputStreamReader bekannte throws IOException nach der main-Deklaration
bewirkt, dass Fehler an die nächst höhere Ebene weitergegeben
werden. So braucht man pro Klasse nur ein Exception Handling zu schreiben.
Wird in der nächsthöheren Ebene ebenfalls kein Exception Handling
durchgeführt, dann kommt es zu einer Laufzeitfehlermeldung durch
den Interpreter.
Auswerfen
von Exceptions
Bis jetzt sind Fehler immer nur vom Laufzeitsystem "geworfen"
worden, man kann jedoch selbst Exceptions erzeugen! Wenn man z.B. nur
Zahlen kleiner Zehn haben möchte kann man eine Exception erzeugen,
wenn die Zahl größer als zehn ist. Dies geht mit dem Schlüsselwort throw. Eine Anweisung mit throw könnte so aussehen:
throw new SecurityException(zahl +" ist zu groß!");
Die Aufrufe müssen dann natürlich im try-Block stehen!
Eigene
Exception-Klassen entwerfen
Man kann auch eigene Exception-Klassen definieren. Diese erben dann
alle von der Klasse Exception. Eine eigene Exception-Klasse könnte
Exceptions auslösen wenn eine negative Zahl eingegeben wird. Eine
solche Exception könnte so aussehen:
public class NegativeIntegers extends Exception { public NegativeIntegers() { super(); } public NegativeIntegers(int i) { super ("Die Zahl " + i + " ist negativ! Das ist nicht erlaubt!"); } }
Die selbstentworfene Exception wird nun ganz normal in der Hauptklasse
verwendet. Es wird dabei mit throws in der Methoden-Definition gearbeitet.
Und eine Exception nach einer Überprüfung geworfen.
Lesen
und schreiben von externen Dateien
Bis jetzt bestanden unsere Programme immer aus einer Eingabe von Hand
oder war bereits im Quelltext gemacht worden, das Ganze wurde dann auf
dem Bildschirm ausgegeben. Es ist jedoch mit Java auch möglich externe
Dateien einzulesen, bzw. in diese Ausgaben zu machen.
In der Eingabe von Hand ist schon ein Schlüsselwort eingebaut, das
bei der Ein- und Ausgabe in Dateien mit Java eine große Rolle spielt
den 'Stream' (z.B. in InputStreamReader). Der Stream
ist ein Datenfluss von der Eingabe hin zur Ausgabe, spielt also bei beidem
eine Rolle.
In Java gibt es im Paket java.io.* eine Vielzahl von Streams (über
30); ich möchte vorerst nur den FileInputStream, den FileOutputStream,
den FileWriter und den BufferedReader behandeln.
FileInputStream
Für einfache Dateieingaben benutzt man die Klasse FileInputStream.
Man bindet mit dieser Klasse eine Datei an einen Datenstrom.
Allgemein sieht die FileInputStream-Anweisung so aus: FileInputStream(String)
Damit wird ein FileInputStream mit einen gegebenen Dateinamen erstellt.
Im Quelltext könnte das dann so eingesetzt werden:
[...] FileInputStream in = new FileInputStream("Readme.txt"); int zeichen =0; while ((zeichen=in.read())!=-1) System.out.print((char)zeichen); in.close(); [...]
Hier sieht man warum der FileInputStream nur für
einfache Dateieingaben verwendet wird. Er liest Byte für Byte aus
der Datei aus und liefert int-Werte (Die dem ASCII-Key entsprechen) zurück.
Da alles Byte für Byte geschieht, muss man eine Schleife erstellen,
die den Vorgang solange wiederholt bis die Datei zu Ende ist (ASCII-Key
-1 bedeutet Dateiende). Für die Ausgabe müssen die int-Werte
mittels Typecast ((char)zeichen) in Buchstaben umgewandelt werden.
Als Exceptions können FileNotFoundException und IOException auftreten. Ersteres ist als Datei nicht vorhanden und letzteres als Lesefehler
zu interpretieren.
FileOutputStream
Der FileOutputStream ist das Gegenstück zum FileInputStream.
Auch er ist nur für einfache Dateiausgaben geeignet.
Allgemein wird so ein FileOutputStream aus einem gegebenen
Dateinamen erzeugt: FileOutputSteam(String);
Im Quelltext könnte man ihn z.B. so einsetzen:
BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); FileOutputStream fileOut = new FileOutputStream("line.txt"); System.out.println("Text eingeben:"); String text = in.readLine(); fileOut.write(text.getBytes(), 0, text length()); fileOut.close();
Hierfür musste ich etwas ausholen, zuerst muss natürlich
etwas eingegeben werden, dies geschieht über den BufferedReader.
Dieser Text wird in einer String-Variable gespeichert und mit der write-Methode
des FileOutputStream-Objektes in eine Datei geschrieben.
Wie im Quelltextschnipsel zu sehen ist, sind Parameter zwingend erforderlich!
Die write-Methode ist also folgendermaßen aufgebaut: write(Daten,
Anfang, Länge)
Daten sind ein Array, jedoch kein String-Array, sondern ein Byte-Array,
aus diesem Grund werden die Daten mittels .getBytes() ins Byte-Format
konvertiert; der Anfang ist bei 0 wenn die Datei noch leer ist
und die Länge wird mittels .length() bestimmt.
FileWriter
Wesentlich einfacher geht das Schreiben in Dateien aber mit dem FileWriter.
Er ermöglicht das sofortige Schreiben in Dateien. So sieht die
Anweisung in Aktion aus:
FileWriter fw = new FileWriter("fileWriter.txt"); fw.write("Testzeile in Textdatei"); fw.close;
Mit den hier gezeigten Einstellungen wird eine Datei erzeugt, besteht
diese schon, so wird sie gelöscht. Möchte man, dass die Textzeilen
hinten angehängt werden, dann muss man einen zweiten Parameter
setzen, das sieht dann so aus:
FileWriter fw = new FileWriter("fileWriter.txt", true)
Es reicht also schon ein true anzuhängen und schon ist
auch dieses Problem gelöst. Es müssen nur IOExceptions abgefangen
werden.
BufferedReader
/ FileReader
Der BufferedReader ist ein alter Bekannter aus der Eingabe
über Tastatur. Er ist jedoch so universal einsetzbar, dass er auch
aus Dateien lesen kann. Wenn wir uns nochmals an die BufferedReader-Deklaration
erinnern wollen:
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
So nun vergleich wir das mit dieser Funktion die für das Einlesen
aus Dateien zuständig ist:
BufferedReader in = new BufferedReader(new FileReader("Textdatei.txt"));
Wir erkennen, dass sich die zwei Anweisungen nur durch
den Parameter unterscheiden. Beim Einlesen von Textdateien ist dieser
der FileReader.
Auch der Einsatz des BufferedReaders hat Vorteile gegenüber
dem Einsatz des FileInputStreams. Er kann Zeilenweise einlesen! Das macht
ihn natürlich sehr viel schneller.
So könnte man ihn im Quelltext verwenden:
[...] BufferedReader in = new BufferedReader(new FileReader("Textdatei.txt")); String str; while((str=in.readLine())!=null) System.out.println(str); in.close; [...]
Als Exceptions können FileNotFoundException und IOException auftreten
Abschluss
Damit sind die Grundlagen der Programmierung mit Java abgeschlossen, ich
empfehle jedoch für anspruchsvolleres Programmieren auch die Workshops
zu Klassen und der Grafikprogrammierung mit dem AWT
|