Java-Einführung von Hubert Partl


System-Interfaces


Ein-Ausgabe (IO)

Wir haben bereits gelernt, wie die Ein- und Ausgabe in Graphischen User-Interfaces programmiert wird. Nun wollen wir uns auch damit beschäftigen, wie wir Daten von Dateien einlesen und in Dateien speichern können.

Das Lesen und Schreiben von lokalen Dateien (Files) ist im Allgemeinen nur für Applikationen, aber nicht für Applets erlaubt. Applets können im Allgemeinen nur Files auf dem Server ansprechen, von dem sie geladen wurden (siehe die Kapitel über das Lesen von einem Web-Server und über Networking).

Für die Verwendung der Ein-Ausgabe-Klassen muss das Package java.io importiert werden.

Alle Ein-Ausgabe-Operationen können bestimmte Fehlersituationen auslösen (IOException, SecurityException, FileNotFoundException u.a.), die mit try und catch abgefangen werden sollen (siehe das Kapitel über Exceptions).

Dateien (Files) und Directories

Die folgenden Datenströme ("Systemdateien") stehen innerhalb der System-Klasse statisch zur Verfügung und müssen nicht explizit deklariert werden:

Andere Files kann man im einfachsten Fall ansprechen, indem man den Filenamen im Konstruktor eines Datenstromes angibt (siehe unten).

Für kompliziertere Aktionen mit Dateien und Directories können File-Objekte mit den folgenden Konstruktoren angelegt und mit den folgenden Methoden verwendet werden:

Deklaration eines Files:

File f = new File (filename);

File f = new File (directory, filename);

Der Filename ist als String anzugeben. Er kann auch den kompletten Pfad enthalten, also mit Schrägstrichen oder Backslashes oder Doppelpunkten, je nach Betriebssystem, die Applikation ist dann aber nicht plattformunabhängig. Statt diesen File-Separator explizit im String anzugeben, sollte man deshalb immer den am jeweiligen System gültigen Separator mit dem statischen Datenfeld
File.separator
ansprechen und mit + in den String einbauen.

Die Version des Konstruktors mit zwei Parametern ist dann sinnvoll, wenn man mehrere Files in einem Directory ansprechen will, oder wenn das Directory innerhalb des Programms bestimmt oder vom Benutzer ausgewählt wird. Für den Directory-Namen kann entweder ein String (wie oben) oder ein File-Objekt, das ein Directory beschreibt, angegeben werden.

Das Öffnen oder Anlegen des Files erfolgt erst dann, wenn das File-Objekt im Konstruktor eines Datenstromes (InputStream, OutputStream, Reader, Writer, siehe unten) verwendet wird.

Deklaration eines Directory:

Directories werden wie Files deklariert und "geöffnet":

File dir = new File (dirname);

File dir = new File (parentdir, dirname);

Mit den Methoden isFile() und isDirectory() kann abgefragt werden, ob es sich um ein File oder um ein Directory handelt. Das Anlegen eines Directory erfolgt mit der Methode mkdir().

Methoden

Die wichtigsten Methoden für File-Objekte (Dateien und Directories) sind:

Datenströme (stream)

Im Sinne der Grundsätze der objekt-orientierten Programmierung gibt es in Java verschiedene Klassen und Objekte für Datenströme, die je nach der Anwendung entsprechend kombiniert werden müssen. Dabei sind die Spezialfälle jeweils Unterklassen der einfachen Fälle (Vererbung), alle Ein-Ausgabe-Operationen können also direkt am Objekt der Unterklasse durchgeführt werden.

Es gibt in Java zwei verschiedene Gruppen von Datenströmen:

InputStream (Eingabe)

InputStream ist die Oberklasse für das Lesen von Datenströmen, also für die Byte-orientierte Eingabe (siehe auch die Alternative Reader für die Zeichen- und Zeilen-orientierte Eingabe).

Es werden zwei Arten von InputStream-Klassen verwendet:

Bei den Klassen der ersten Gruppe wird im Konstruktor angegeben, wo der Datenstrom herkommt (File, Internet-Adresse, Speicherbereich).

Bei den Klassen der zweiten Gruppe wird im Konstruktor irgendein anderer InputStream angegeben. Diese Klassen und die von ihnen unterstützten Funktionalitäten können daher beliebig kombiniert werden.

InputStream - Methoden

Die wichtigsten Methoden für InputStreams sind:

Byte-weises Lesen

Ein typisches Beispiel für das Byte-weise Lesen eines Daten-Files hat folgenden Aufbau:

int b;
try {
  BufferedInputStream in = new BufferedInputStream (
    new FileInputStream ("filename.dat") );
  while( (b = in.read()) != -1 ) {
    // do something ...
  }
  in.close();
} catch (Exception e) {
    System.out.println("error " + e);
}
Wenn die Daten von einer anderen Quelle kommen, muss nur der Parameter im Konstruktor von BufferedInputStream entsprechend geändert werden, der Rest des Programms bleibt unverändert (siehe Lesen von einem Web-Server).

FileInputStream

FileInputStream ist die grundlegende Klasse für das Lesen von Dateien (siehe auch die Alternative FileReader).

Das Öffnen des Lese-Stroms bzw. des Files erfolgt mit einem Konstruktor der Form

FileInputStream infile = new FileInputStream (filename);

Der Filename kann entweder als String oder als File-Objekt angegeben werden (siehe oben).

BufferedInputStream

Um die Eingabe effizienter und schneller zu machen, soll nicht jedes Byte einzeln von der Hardware gelesen werden, sondern aus einem Pufferbereich. Daher sollte fast immer ein BufferedInputStream über den einfachen FileInputStream gelegt werden.

Konstruktoren:

Beispiel:
BufferedInputStream in = new BufferedInputStream
  ( new FileInputStream (filename) );

Lesen einer Datei von einem Web-Server (URL)

Mit den Klassen URL (uniform resource locator) und HttpURLConnection im Paket java.net kann eine beliebige Datei von einem Web-Server oder FTP-Server gelesen werden (bei Applikationen von einem beliebigen Server, bei einem Applet im Allgemeinen nur von dem Web-Server, von dem es geladen wurde). Mit der Methode openStream() erhält man einen InputStream, der dann wie ein FileInputStream verwendet werden kann.

Beispiel:

import java.io.*;
import java.net.*;
...
BufferedInputStream in;
URL url;
try {
  url = new URL( "http://www.xxx.com/yyy" );
  in = new BufferedInputStream ( url.openStream() );
  while( (b = in.read()) != -1 ) {
    // do something ...
  }
  in.close();
} catch (Exception e) {
    System.out.println("error " + e);
}
Dies eignet sich bis JDK 1.1 primär für das Lesen von Textfiles (siehe InputStreamReader) und Datenfiles (siehe DataInputStream). Ab JDK 1.2 gibt es auch eigene Klassen für die Interpretation von HTML-Files (JEditorPane, HTMLDocument, HTMLReader).

Komplexere Möglichkeiten für Ein-Ausgabe-Operationen über das Internet sind im Kapitel über Networking beschrieben.

OutputStream (Ausgabe)

OutputStream ist die Oberklasse für das Schreiben von Datenströmen, also für die Byte-orientierte Ausgabe (siehe auch die Alternative Writer für die Zeichen- und Zeilen-orientierte Ausgabe).

Es werden zwei Arten von OutputStream-Klassen verwendet:

Bei den Klassen der ersten Gruppe wird im Konstruktor angegeben, wo der Datenstrom hingeht (File, Internet-Adresse, Speicherbereich).

Bei den Klassen der zweiten Gruppe wird im Konstruktor irgendein anderer OutputStream angegeben. Diese Klassen und die von ihnen unterstützten Funktionalitäten können daher beliebig kombiniert werden.

OutputStream - Methoden

Die wichtigsten Methoden für OutputStreams sind:

Byte-weises Schreiben

Ein typisches Beispiel für das Byte-weise Schreiben eines Daten-Files hat folgenden Aufbau:

try {
  BufferedOutputStream out = new BufferedOutputStream (
    new FileOutputStream ("filename.dat") );
  ...
  out.write(...);
  ...
  out.flush();
  out.close();
} catch (Exception e) {
    System.out.println("error " + e);
}

FileOutputStream

FileOutputStream ist die grundlegende Klasse für das Schreiben von Dateien (siehe auch die Alternative FileWriter).

Das Öffnen des Ausgabe-Stroms bzw. des Files erfolgt mit Konstruktoren der Form

Der Filename kann entweder als String oder als File-Objekt angegeben werden (siehe oben). In den ersten beiden Fällen wird die Datei neu geschrieben oder überschrieben, im dritten Fall (Appendmode true) werden die Informationen an das Ende einer bereits existierenden Datei hinzugefügt.

In JDK 1.0 ist die Angabe des Appendmode nicht möglich, dort muss für das Hinzufügen an eine bereits bestehende Datei stattdessen die Klasse RandomAccessFile verwendet werden (siehe unten).

BufferedOutputStream

Um die Ausgabe effizienter und schneller zu machen, soll nicht jedes Byte einzeln auf die Hardware geschrieben werden, sondern aus einem Pufferbereich. Daher sollte fast immer ein BufferedOutputStream über den einfachen FileOutputStream gelegt werden.

Konstruktoren:

Beispiel:
BufferedOutputStream out = new BufferedOutputStream
  ( new FileOutputStream (filename) );

PrintStream

Die Klasse PrintStream enthält die Methoden print und println, mit denen Datenfelder und Objekte jeweils in die für Menschen lesbare Textdarstellung umgewandelt werden. Sie dient seit JDK 1.1 nur mehr für die Text-Ausgabe auf die System-Console mit System.out und System.err. In allen anderen Fällen soll man für die zeichen- und zeilenorientierte Ausgabe lieber die Klasse PrintWriter verwenden (siehe unten)

ByteArrayOutputStream und ByteArrayInputStream

Die Klasse ByteArrayOutputStream dient dazu, Bytes oder Daten mit write-Befehlen in einen Speicherbereich statt in ein File zu schreiben.

Die Klasse ByteArrayInputStream dient dazu, mit read-Befehlen die Bytes oder Daten aus einem mit ByteArrayOutputStream beschriebenen Speicherbereich zu lesen, also in seine Einzelteile zu zerlegen.

Data- und Object-Streams (Serialisierung)

Wenn man nicht einzelne Bytes sondern ganze Datenfelder oder Objekte lesen und schreiben will, kann man dafür die Klassen Data- und Object- Input- und Output-Stream verwenden. Dies entspricht dem Begriff "binäre Datenfiles" in anderen Programmiersprachen.

Im Gegensatz zu anderen Programmiersprachen ist bei Java die Bit- und Byte-weise Speicherung von Datenfeldern und Objekten nicht systemabhängig sondern für alle Java-Programme einheitlich festgelegt. Man kann also ein solches binäres Datenfile, das mit Java auf einem Computer erstellt wurde, mit Java auf jedem beliebigen anderen Computer lesen und verarbeiten.

DataOutputStream

Der DataOutputStream erlaubt es, bestimmte primitive Datentypen direkt in ihrer (plattformunabhängigen) Java-internen Darstellung zu schreiben ("binäres Datenfile", kann dann mit DataInputStream gelesen werden).

Konstruktor:

Beispiel:
DataOutputStream out = new DataOutputStream
  ( new BufferedOutputStream
    ( new FileOutputStream (filename) ) );
Methoden:

DataInputStream

Der DataInputStream erlaubt es, bestimmte primitive Datentypen direkt einzulesen, d.h. die Daten müssen in der plattformunabhängigen Java-internen Darstellung im Input-Stream stehen ("binäres Datenfile", z.B. mit DataOutputStream geschrieben).

Konstruktor:

Beispiel:
DataInputStream in = new DataInputStream
  ( new BufferedInputStream
    ( new FileInputStream (filename) ) );
Methoden: Es gibt hier auch eine String-Methode readLine(), es wird aber empfohlen, für die Verarbeitung von Textzeilen lieber die Klasse BufferedReader zu verwenden (siehe unten).

ObjectOutputStream (Serialisierung)

Der ObjectOutputStream hat die gleiche Funktionalität wie der DataOutputStream, erlaubt es aber zusätzlich auch, ganze Objekte in der Java-internen Darstellung zu schreiben (kann dann mit ObjectInputStream gelesen werden). Es ist üblich, solche Filenamen mit der Extension ".ser" zu versehen.

Dazu muss das Objekt das Interface Serializable implementieren, das angibt, dass sich das Objekt "serialisieren", also als eine Folge von Bits und Bytes speichern lässt, die alle seine Datenfelder enthalten. Das Interface Serializable ist ein sogenanntes Marker-Interface, d.h. man muss keine Methoden implementieren, sondern es kommt nur darauf an, ob das Interface geerbt wird oder nicht. Fast alle Objekte sind serialisierbar.

Konstruktor:

Methoden: Anmerkung: Object ist die Superklasse für alle Klassen, der Parameter von writeObject kann also ein Objekt einer beliebigen Klasse sein. Beispiel:
Date d;
...
out.writeObject (d);

ObjectInputStream

Der ObjectInputStream hat die gleiche Funktionalität wie der DataInputStream, erlaubt es aber zusätzlich auch, ganze Objekte zu lesen, die im Input-Stream in der Java-internen Darstellung stehen (Serialisierung, z.B. mit ObjectOutputStream geschrieben, Filenamen der Form xxxx.ser).

Konstruktor:

Methoden: Anmerkung: Object ist die Superklasse für alle Klassen. Das Ergebnis von readObject() muss mit "Casting" auf den Typ des tatsächlich gelesenen Objekts umgewandelt werden. Beispiel:
Date d;
...
d = (Date) in.readObject();

RandomAccessFile (Ein- und Ausgabe)

Random-Access-Files erlauben es, Daten nicht nur in einem Datenstrom vom Anfang bis zum Ende zu lesen oder zu schreiben, sondern an bestimmte Stellen eines Files zu "springen" und dort direkt zu lesen und zu schreiben. Dabei ist auch ein Abwechseln zwischen Lese- und Schreiboperationen möglich.

Konstruktor:

Der Filename kann als String oder als File-Objekt angegeben werden.

Der Mode ist ein String, der entweder nur "r" oder "rw" enthält. Im ersten Fall kann der File-Inhalt nur gelesen werden, im zweiten Fall (auch) geschrieben bzw. verändert werden.

Methoden:

Es stehen alle für DataInputStream und für DataOutputStream definierten Methoden zur Verfügung, also insbesondere read, write, close, flush, und außerdem die folgenden Methoden:

Wenn man Daten an das Ende einer bereits existierenden Datei hinzufügen will, kann man diese Datei als RandomAccessFile mit mode "rw" öffnen und mit
file.seek( file.length() );
an das File-Ende positionieren, bevor man den write-Befehl ausführt. Ab JDK 1.1 kann man dies aber einfacher mit dem Appendmode-Parameter im Konstruktor des FileOutputStream bzw. FileWriter erreichen.

Reader und Writer (Text-Files)

Java als plattformunabhängige Sprache verwendet den Zeichencode "Unicode" (16 bit pro Zeichen) für die Verarbeitung von allen weltweit verwendeten Schriftzeichen. Die Reader- und Writer-Klassen unterstützen die Umwandlung zwischen diesem 16 bit Unicode und dem auf dem jeweiligen Rechnersystem verwendeten Zeichencode für Textfiles (meist 8 bit). Dies kann der in englisch-sprachigen und westeuropäischen Ländern verwendete Zeichencode Iso-Latin-1 (ISO-8859-1) oder ein anderer Zeichencode sein, z.B. für osteuropäische lateinische Schriften (Iso-Latin-2) oder für kyrillische, griechische, arabische, hebräische oder ostasiatische Schriften, oder die Unicode-Kodierung mit UTF-8, oder auch ein Computer-spezifischer Code wie z.B. für MS-DOS, MS-Windows oder Apple Macintosh.

Für die Verarbeitung von Textfiles sollten daher ab JDK 1.1 Reader und Writer statt Input-Streams und Output-Streams verwendet werden. Diese Klassen enthalten nicht nur Methoden für das Lesen und Schreiben von einzelnen Zeichen (read, write) sondern auch zusätzliche Methoden für das Lesen und Schreiben von ganzen Zeilen (readLine, newLine) sowie für die Darstellung von Zahlen und anderen Datentypen als menschenlesbare Texte (print, println).

Reader

Reader ist die Oberklasse für das Lesen von Texten, also für die zeichen- und zeilenorientierte Eingabe.

Es werden zwei Arten von Reader verwendet:

Bei den Klassen der ersten Gruppe wird im Konstruktor angegeben, wo der Datenstrom herkommt (File, InputStream, Speicherbereich).

Bei den Klassen der zweiten Gruppe wird im Konstruktor irgendein anderer Reader angegeben. Diese Klassen und die von ihnen unterstützten Funktionalaitä:ten können daher beliebig kombiniert werden.

Reader - Methoden

Die wichtigsten Methoden bei den Reader-Klassen sind:

zeichenweises Lesen

Ein typisches Beispiel für das zeichenweise Lesen eines Text-Files hat folgenden Aufbau:

int ch;
try {
  BufferedReader in = new BufferedReader (
    new FileReader ("filename.txt") );
  while( (ch = in.read()) != -1 ) {
    // do something ...
  }
  in.close();
} catch (Exception e) {
    System.out.println("error " + e);
}

zeilenweises Lesen

Ein typisches Beispiel für das zeilenweise Lesen eines Text-Files hat folgenden Aufbau:

String thisLine;
try {
  BufferedReader in = new BufferedReader (
    new FileReader ("filename.txt") );
  while( (thisLine = in.readLine()) != null ) {
    // do something ...
  }
  in.close();
} catch (Exception e) {
    System.out.println("error " + e);
}

FileReader

Der FileReader ist die grundlegende Klasse zum text- und zeilen-orientierten Lesen von Dateien (Files). Aus Effizienzgründen sollte er innerhalb eines BufferedReader verwendet werden (siehe unten).

Konstruktor:

Der Filename ist als String oder als File-Objekt anzugeben.

Das Encoding kann hier nicht angegeben werden, es wird immer das lokale Encoding des Systems angenommen. Falls Sie das Encoding (z.B. "8859_1") explizit angeben wollen, müssen Sie statt dem FileReader eine Kombination von InputStreamReader und FileInputStream verwenden (siehe unten).

BufferedReader

Um die Eingabeeffizienter und schneller zu machen, soll nicht jedes Byte einzeln von der Hardware gelesen werden, sondern aus einem Pufferbereich. Daher sollte fast immer ein BufferedReader über den einfachen FileReader oder InputStreamReader gelegt werden.

Konstruktoren:

Beispiel:
BufferedReader infile = new BufferedReader
  (new FileReader (infileName) );

InputStreamReader

Die Klasse InputStreamReader dient dazu, einen byte-orientierten InputStream zum Lesen von Texten und Textzeilen zu verwenden. Dies ist für Spezialfälle notwendig, die nicht mit dem FileReader abgedeckt werden können. Aus Effizienzgründen sollte sie innerhalb eines BufferedReader verwendet werden.

Konstruktor:

Der String im zweiten Fall gibt den Zeichencode an, z.B. "8859_1". Im ersten Fall wird der am jeweiligen Rechnersystem "übliche" Zeichencode verwendet. Beim Lesen von Dateien über das Internet soll immer der Code der Datei explizit angegeben werden, damit man keine bösen Überraschungen auf Grund von lokalen Spezialfällen am Client erlebt.

Beispiele:

BufferedReader stdin = new BufferedReader
  (new InputStreamReader (System.in) );

BufferedReader infile = new BufferedReader
  (new InputStreamReader
    (new FileInputStream("message.txt"), "8859_1" ) )

String fileUrl = "ftp://servername/dirname/filename";
BufferedReader infile = new BufferedReader
  (new InputStreamReader
    ( ( new URL(fileUrl) ).openStream(), "8859_1" ) )

Writer

Writer ist die Oberklasse für das Schreiben von Texten, also für die zeichen- und zeilenorientierte Ausgabe.

Es werden zwei Arten von Writer verwendet:

Bei den Klassen der ersten Gruppe wird im Konstruktor angegeben, wo der Datenstrom herkommt (File, InputStream, Speicherbereich).

Bei den Klassen der zweiten Gruppe wird im Konstruktor irgendein anderer Writer angegeben. Diese Klassen und die von ihnen unterstützten Funktionalaitä:ten können daher beliebig kombiniert werden.

Writer - Methoden

Die wichtigsten Methoden bei den Writer-Klassen sind:

Anmerkung: Um plattformunabhängige Dateien zu erzeugen, sollten Zeilenenden stets mit der Methode newLine() oder println() geschrieben werden und nicht mit "\n" oder "\r\n" innerhalb von Strings.

zeichen- und zeilenweises Schreiben

Ein typisches Beispiel für das Schreiben eines Text-Files hat folgenden Aufbau:

try {
  BufferedWriter out = new BufferedWriter (
    new FileWriter ("filename.txt") );
  ...
  out.write(...);
  out.newLine();
  ...
  out.flush();
  out.close();
} catch (Exception e) {
    System.out.println("error " + e);
}
oder mit Verwendung von PrintWriter (siehe unten).

FileWriter

Der FileWriter ist die grundlegende Klasse zum text- und zeilen-orientierten Schreiben von Dateien (Files). Aus Effizienzgründen sollte er innerhalb eines BufferedWriter verwendet werden (siehe unten).

Konstruktoren:

Der Filename ist als String oder als File-Objekt anzugeben. In den ersten beiden Fällen wird die Datei neu geschrieben oder überschrieben, im dritten Fall (Appendmode true) werden die Informationen an das Ende einer bereits existierenden Datei hinzugefügt.

Das Encoding kann hier nicht angegeben werden, es wird immer das lokale Encoding des Systems angenommen. Falls Sie das Encoding (z.B. "8859_1") explizit angeben wollen, müssen Sie statt dem FileWriter eine Kombination von OutputStreamWriter und FileOutputStream verwenden (siehe unten).

BufferedWriter

Um die Ausgabe effizienter und schneller zu machen, soll nicht jedes Byte einzeln auf die Hardware geschrieben werden, sondern aus einem Pufferbereich. Daher sollte fast immer ein BufferedWriter über den einfachen FileWriter oder OutputStreamWriter gelegt werden.

Konstruktoren:

Beispiel:
BufferedWriter outfile = new BufferedWriter
  (new FileWriter (outfileName) );

OutputStreamWriter

Die Klasse OutputStreamWriter dient (analaog zum InputSreamReader) dazu, einen byte-orientierten OututStream zum Schreiben von Texten und Textzeilen zu verwenden. Dies ist für Spezialfälle notwendig, die nicht mit dem FileWriter abgedeckt werden können. Aus Effizienzgründen sollte sie innerhalb eines BufferedWriter verwendet werden.

Konstruktor:

Der String im zweiten Fall gibt den Zeichencode an, z.B. "8859_1". Im ersten Fall wird der am jeweiligen Rechnersystem "übliche" Zeichencode verwendet.

Beispiele:

BufferedWriter stdout = new BufferedWriter
  (new OutputStreamWriter (System.out) );

BufferedWriter outfile = new BufferedWriter
  (new OutputStreamWriter
    (new FileOutputStream("message.txt"),
      "8859_1" ) )

PrintWriter

Der PrintWriter ist eine spezielle Klasse zum text- und zeilen-orientierten Schreiben von Datenströmen oder Files. Sie enthält zusätzliche Methoden für die Umwandlung von Zahlen und anderen Objekten in menschenlesbare Texte (print, println, siehe unten).

Konstruktoren:

Das Encoding kann hier nicht angegeben werden, es wird immer das lokale Encoding des Systems angenommen. Der PrintWriter hat zusätzlich zu den Writer-Methoden auch die folgenden Methoden: Wenn Sie angeben wollen, mit wie vielen Nachkommastellen Float- und Double-Zahlen ausgegeben werden sollen, können Sie die Klassen DecimalFormat oder NumberFormat aus dem Package java.text verwenden. Beispiel:
double x = ...;
DecimalFormat dec = new DecimalFormat ("#,###,##0.00");
System.out.println("x = " + dec.format(x));

Ein typisches Beispiel für das Schreiben eines Text-Files mit PrintWriter hat folgenden Aufbau:

try {
  PrintWriter out = new PrintWriter (
    new BufferedWriter (
      new FileWriter ("filename.txt") ) );
  ...
  out.print(...);
  out.println(...);
  ...
  // out.flush(); not needed with auto-flush PrintWriter
  out.close();
} catch (Exception e) {
    System.out.println("error " + e);
}

StringWriter und StringReader

Die Klasse StringWriter dient dazu, einen langen String mit mehreren write-Befehlen zusammen zu setzen. Die write-Befehle schreiben also - ähnlich wie bei ByteArrayOutputStream (siehe oben) - in einen Speicherbereich statt in ein File.

Der mit StringWriter erzeugte lange String kann dann entweder mit den Methoden der Klasse String oder mit StringTokenizer oder mit StringReader verarbeitet, also in seine Einzelteile zerlegt werden.

Übung: zeilenweises Ausdrucken eines Files

Schreiben Sie eine einfache Applikation, die ein Text-File (ihr eigenes Java-Source-File) Zeile für Zeile liest und auf die Standard-Ausgabe ausgibt.

Dieses Programm kann dann als Muster für kompliziertere Programme verwendet werden.

Übung: zeichenweises Kopieren eines Files

Schreiben Sie eine einfache Applikation, die ein Text-File (ihr eigenes Java-Source-File) Byte für Byte oder Zeichen für Zeichen auf ein neues File ("test.out") kopiert.

Diese beiden Programmvarianten können dann als Muster für kompliziertere Programme verwendet werden.

Übung: Lesen eines Files über das Internet

Schreiben Sie eine einfache Applikation, die ein kurzes Text-File von einem Web-Server oder FTP-Server liest und Zeile für Zeile auf die Standard-Ausgabe ausgibt.


Networking

Java unterstützt standardmäßig nicht nur das Lesen und Schreiben von lokalen Dateien (siehe oben) sondern auch die Kommunikation über Computer-Netze mit der Technik des Internet-Protokolls TCP/IP.

Die wichtigsten Möglichkeiten, Pakete und Klassen sind:

Für die Details wird auf die Online-Dokumentation verwiesen. Beispiele für die Verwendung der Klasse URL finden Sie oben. Weitere Hinweise zu einigen dieser Klassen finden Sie nachfolgend.

Java im Web-Server (CGI, Servlets)

Web-Server können nicht nur fertige Files liefern sondern auch Programme ausführen. Dazu dient die Schnittstelle Common Gateway Interface (CGI). Die CGI-Programme können, eventuell in Abhängigkeit von Benutzer-Eingaben, bestimmte Aktionen ausführen und die Ergebnisse über das Hypertext Transfer Protocol HTTP an den Web-Browser senden.

CGI-Programme können im HTML-File oder Applet entweder über ein Hypertext-Link aufgerufen werden (nur Ausgabe an den Client) oder über ein Formular oder GUI (Eingabe vom Client an das CGI-Programm, Ausgabe an den Client).

CGI-Programme können in jeder beliebigen Programmier- oder Script-Sprache geschrieben werden, auch in Java. In diesem Fall besteht das CGI-Programm aus einem Shell-Script (Batch-Datei), in dem die Java Virtual Machine aufgerufen wird, die den Bytecode der Java-Applikation interpretiert, etwa in einer der folgenden Formen:

java Classname
java Classname Parameter
java -Dvariable=wert Classname
Dies bedeutet, dass bei jedem Aufruf des CGI-Programms die Java Virtual Machine neu gestartet werden muss, was eventuell zu längeren Wartezeiten führen kann.

Diesen Nachteil kann man vermeiden, wenn man einen Web-Server verwendet, der die Java Virtual Machine integriert enthält und Java-Programme sofort direkt aufrufen kann (z.B. die neueren Versionen von Apache, Netscape Enterprise Server und vielen anderen).

Diese Java-Programme werden als Servlets bezeichnet. Der Name "Servlet" ist analog zu "Applet" gebildet: So wie Applets von einer Java Virtual Machine innerhalb des Web-Browsers ausgeführt werden, so werden Servlets von einer Java Virtual Machine innerhalb des Web-Servers ausgeführt.

Dafür gibt es die Packages javax.servlet und javax.servlet.http sowie ein Java Servlet Development Kit JSDK mit einem ServletRunner zum Testen von Servlets, bevor sie im echten Web-Server eingebaut werden.

Die wichtigsten Methoden von Servlets sind:

Servlets können wie Java-Applikationen auch auf lokale Files, Programme und Systemfunktionen am Web-Server zugreifen.

Für ausführlichere Informationen wird auf die Referenzen und die Literatur verwiesen. Hier nur eine kurze Skizze für den typischen Aufbau eines Servlet:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ... extends HttpServlet {

  public void init (ServletConfig config)
      throws ServletException {
    super.init( config );
    ...
  }

  public void service
      (HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {

    String xxx = req.getParameter("xxx");
    ...

    resp.setContentType("text/html");
    PrintWriter out =
      new PrintWriter( resp.getOutputStream() );
    out.println (" ... ");
    ...
    out.println (" ... ");
    out.flush();
    out.close();
  }

  public String getServletInfo() {
    return "...";
  }
}

Internet-Protokoll, Server und Clients

Java unterstützt die Kommunikation über das weltweite Internet und über interne Intranets und Extranets mit den Internet-Protokollen TCP und UDP. Dazu muss das Package java.net importiert werden.

Grundbegriffe

Die programmtechnischen Mechanismen für Netzverbindungen werden Sockets (vom englischen Wort für Steckdose) genannt.

Für die Adressierung werden Hostname und Portnummer verwendet.

Der Hostname ist eine weltweit bzw. netzweit eindeutige Bezeichnung des Rechners (Name oder Nummer).

Die Portnummer gibt an, welches Programm auf diesem Rechner die über das Netz übertragenen Informationen verarbeiten soll. Portnummern unter 1024 haben eine vordefinierte Bedeutung und können nur mit Supervisor-Privilegien (root unter Unix) verwendet werden. Portnummern über 1024 sind "frei". Für Java-Anwendungen, die von gewöhnlichen Benutzern geschrieben werden, kommen also meist nur Portnummern über 1024 in Frage, und man muss sicherstellen, dass nicht jemand anderer auf dem selben Rechner die selbe Portnummer schon für andere Zwecke verwendet.

Server sind die Rechner, die ein bestimmtes Service bieten und damit die Kunden (Clients) "bedienen".

Clients ("Kunden") sind die Benutzer, die das Service des Servers in Anspruch nehmen wollen, bzw. die von ihnen dafür benützten Rechner.

Vorgangsweise

Der Server "horcht" (listen) mit Hilfe einer ServerSocket auf eine Portnummer, d.h. er wartet darauf, dass ein Client etwas von ihm will ("einen Request sendet"). In diesem Fall baut er, meist in einem eigenen Thread, eine Verbindung (connection) mit dem Client über eine Socket auf, liest eventuell vom Client kommende Befehle und Dateneingaben und sendet jedenfalls Meldungen und Ergebnisse an den Client.

Clients bauen über eine Socket eine Verbindung (connection) zum Server auf, senden eventuell Befehle oder Daten an den Server und lesen jedenfalls alle vom Server kommenden Informationen.

Sockets

ServerSocket (listen, accept)

Die Klasse ServerSocket dient dazu, auf Client-Requests zu warten (listen) und bei Bedarf eine Verbindung (connection, Socket) zum Client aufzubauen.

Konstruktor:

Mit portnumber gibt man an, auf welche Portnummer die ServerSocket "horcht". Mit max kann man angeben, wie viele Verbindungen maximal gleichzeitig aktiv sein dürfen (default 50).

Methoden:

Die Konstruktoren und Methoden können Fehlersituationen werfen, die Unterklassen von IOException sind, z.B. wenn bereits ein anderes Programm diese Portnummer verwendet.

Socket (connection)

Die Klasse Socket dient für die Verbindung (connection) zwischen Client und Server.

Konstruktor:

baut eine Verbindung zum angegebenen Rechner (z.B. Server) unter der angegebenen Portnummer auf.

Methoden:

Die Konstruktoren und Methoden können Unterklassen von IOException oder eine InterruptedException werfen, z.B. wenn der Rechnername ungültig ist oder der Server keine Verbindung unter dieser Portnummer akzeptiert oder wenn beim Lesen ein Timeout passiert.

Die Datenströme können genauso wie "normale" Datenströme (siehe oben) zum Lesen bzw. Schreiben verwendet werden und zu diesem Zweck mit weiteren Datenströmen wie BufferedInputStream, BufferedOutputStream, DataInputStream, InputStreamReader, OutputStreamWriter, BufferedReader, BufferedWriter, PrintWriter kombiniert werden.

Lese-Befehle (read, readLine) warten dann jeweils, bis entsprechende Daten von der Gegenstelle gesendet werden und über die Verbindung ankommen.

Um eine effiziente Übertragung über das Netz zu erreichen, wird die Verwendung von Pufferung empfohlen, dann darf aber beim Schreiben die Methode flush() nicht vergessen werden.

Für die Verarbeitung von "Befehlen", die aus mehreren Wörtern oder Feldern bestehen, kann die Klasse StringTokenizer oder StreamTokenizer verwendet werden (siehe die Online-Dokumentation).

Beispiel: typischer Aufbau eines Servers

ServerSocket

Das Hauptprogramm des Servers, das auf die Portnummer "horcht", hat folgenden Aufbau:

import java.net.* ;

public class ServerMain {
  public static void main (String[] args) {

    ServerSocket server = null;
    Socket s = null;
    ServerCon doIt;
    Thread t;
    int port = ...;

    try {
      server = new ServerSocket (port);
      while(true) {
        s = server.accept();
        doIt = new ServerCon (s);
        t = new Thread (doIt);
        t.start();
      }
    } catch (Exception e) {
        try { server.close(); } catch (Exception e2) {}
        System.out.println("ServerMain " +e);
        System.exit(1);
    }
  }
}

Socket

Das für jede Verbindung aufgerufene Server-Programm ist ein Thread mit folgendem Aufbau:

import java.net.* ;
import java.io.* ;

public class ServerCon implements Runnable {

  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;

  public ServerCon (Socket s) {
    socket = s;
  }

  public void run() {
    try {
      out = new PrintWriter
        (new BufferedWriter
          (new OutputStreamWriter
            (socket.getOutputStream() ) ) );
      in = new BufferedReader
        (new InputStreamReader
          (socket.getInputStream() ) );
      ...
      ... in.readLine() ...
      ...
      out.println(...);
      out.flush();
      ...
      in.close();
      out.close();
      socket.close();
    } catch (Exception e) {
        System.out.println("ServerCon " + e);
    }
  }
}

Beispiel: typischer Aufbau eines Client

import java.net.* ;
import java.io.* ;
public class ClientCon {
  public static void main (String[] args) {
    int port = ...;
    String host = "...";
    Socket socket;
    BufferedReader in;
    PrintWriter out;
    try {
      socket = new Socket (host, port);
      out = new PrintWriter
        (new BufferedWriter
          (new OutputStreamWriter
            (socket.getOutputStream() ) ) );
      in = new BufferedReader
        (new InputStreamReader
          (socket.getInputStream() ) );
      ...
      out.println(...);
      out.flush();
      ...
      ... in.readLine() ...
      ...
      out.close();
      in.close();
      socket.close();
    } catch (Exception e) {
        System.out.println ("ClientCon " + e);
    }
  }
}

Übung: einfache Client-Server-Applikation

Schreiben Sie einen Server (als Applikation), der auf Verbindungsaufnahmen wartet und an jeden Client zunächst einen Willkommensgruß und dann - 6 mal (also ca. 1 Minute lang) in Abständen von ungefähr 10 Sekunden - jeweils zwei Textzeilen liefert, die einen kurzen Text und Datum und Uhrzeit enthalten, und schließlich die Verbindung zum Client schließt.

Für die Datums- und Zeitangabe verwenden Sie das Standard-Format der aktuellen Uhrzeit, das Sie mit
( new Date() ).toString()
erhalten, auch wenn dabei nicht die richtige Zeitzone verwendet wird. (Mehr über die Problematik von Datums- und Zeitangaben finden Sie im Kapitel über Datum und Uhrzeit).

Schreiben Sie einen Client (als Applikation), der mit diesem Server Verbindung aufnimmt und alle Informationen, die vom Server gesendet werden, Zeile für Zeile auf die Standard-Ausgabe ausgibt.

Testen Sie dieses System. Die Beendigung des Servers (und auch des Clients, falls er Probleme macht) erfolgt durch Abbruch des Programmes mittels Ctrl-C.

Für diese Übung müssen Sie die Hostnamen der verwendeten Rechner (innerhalb des lokalen Netzes) kennen und sich auf eine Portnummer einigen.


System-Funktionen und Utilities

Einer der großen Vorteile von Java ist die umfangreiche Klassenbibliothek, die für eine Unmenge von Anwendungsbereichen bereits fertige Programme und Klassen enthält, die einfach und bequem verwendet werden können.

Ein paar typische Beispiele für besonders interessante Klassen sind:

Für die Details wird auf die Online-Dokumentation verwiesen. Zu einigen dieser Klassen finden Sie nachfolgend ein paar Hinweise.

Sammlungen, Listen und Tabellen (Collection)

Seit JDK 1.2 gibt es mehrere Interfaces und Klasen, die eine flexiblere und objekt-orientierte Alternative zu Arrays darstellen:

Jedes Interface wird von mehreren Klassen implementiert, die intern verschieden arbeiten. Bei manchen Klasen ist der Zugriff optimiert und das Einfügen oder Löschen von Elementen weniger effizient, bei anderen Klassen ist es umgekehrt. Deshalb wird empfohlen, in Datenfeldern und Methodenparametern immer nur den Interface-Namen zu deklarieren, damit man die konkrete Klasse je nach der Performance ändern kann. Beispiel:

List personen = new ArrayList();

Für den Zugriff auf alle Elemente einer Collection gibt es die Interfaces Iterator und Enumeration.

Für das Sortieren von Listen gibt es die Interfaces Comparable und Comparator sowie die Hilfsklassen Collections und Arrays.

Für die Details wird auf die Online-Dokumentation und die Referenzen verwiesen. Hier nur ein einfaches Beispiel für die Anwendung dieser Interfaces und Klassen, eine Liste von Personen:

    List pers = new ArrayList();
    pers.add ( new Person ("Schmidt", "Anna") );
    pers.add ( new Student ("Schmidt", "Barbara", "TU Wien") );
    pers.add ( new Student ("Fischer", "Georg", "BOKU") );
    pers.add ( new Person ("Schlosser", "Wilhelm") );
    Collections.sort( pers );
    Iterator i = pers.iterator();
    while (i.hasNext()) {
       System.out.println( i.next() );
    }
Zu diesem Zweck muss die Klasse Person das Interface Comparable implementieren und die folgenden Methoden enthalten:
   public boolean equals (Object other) {
      return ( other instanceof Person ) &&
      ( this.vorname .equals ( ((Person)other).vorname ) ) &&
      ( this.zuname .equals ( ((Person)other).zuname ) );
   }

   public int compareTo (Object other)
              throws ClassCastException {
      Person otherP = (Person)other; // ClassCastException
      if ( this.zuname .equals( otherP.zuname ) ) {
         return this.vorname .compareTo ( otherP.vorname );
      } else {
         return this.zuname .compareTo ( otherP.zuname );
      }
   }

Datum und Uhrzeit (Date)

Für die Verarbeitung von Datums- und Zeitangaben muss das Package java.util importiert werden, für die DateFormat-Klassen zusätzlich das Package java.text.

Im JDK ab Version 1.1 muss für die "richtige", für alle Länder der Welt brauchbare Verarbeitung von Datums- und Zeitangaben eine Kombination von mehreren Klassen für Datum und Uhrzeit, Datums-Berechnungen (Kalender), Zeitzonen, Sprachen und Ausgabeformate verwendet werden. Ein kurzes Beispiel folgt unten.

Die Verwendung ist dementsprechend kompliziert, außerdem enthalten die meisten Implementierungen auch verschiedene Fehler (Bugs).

Für einfache Anwendungen können die alten Funktionen der Date-Klasse Version 1.0 verwendet werden (siehe anschließend).

Date Version 1.0

Die hier beschriebenen Funktionen der Klasse Date werden vom Compiler mit der Warnung "deprecated" versehen, sind aber für einfache Anwendungen ausreichend und wesentlich einfacher zu programmieren als die neuen Funktionen von Version 1.1 (siehe unten).

Konstruktoren

Jahre müssen relativ zum Jahr 1900 angegeben werden, also
-8 bedeutet das Jahr 1892,
98 bedeutet das Jahr 1998,
101 bedeutet das Jahr 2001,
1990 bedeutet das Jahr 3890.

Monate müssen im Bereich 0 bis 11 angegeben werden, also
0 für Jänner,
1 für Februar, usw. bis
11 für Dezember.
Dies ist für die Verwendung als Array-Index günstig, führt aber sonst leicht zu Irrtümern. Es wird daher empfohlen, statt der Zahlenangaben lieber die in der Klasse Calendar (Version 1.1) definierten symbolischen Namen zu verwenden:
Calendar.JANUARY für Jänner,
Calendar.FEBRUARY für Februar,
Calendar.MARCH für März, usw. bis
Calendar.DECEMBER für Dezember.

Tage innerhalb der Monate werden wie gewohnt im Bereich 1 bis 31 angegeben.

Wochentage werden im Bereich 0 bis 6 angegeben, dafür sollten aber lieber die symbolischen Namen
Calendar.SUNDAY für Sonntag,
Calendar.MONDAY für Montag, usw. bis
Calendar.SATURDAY für Samstag
verwendet werden.

Stunden werden im Bereich 0 bis 23 angegeben, Minuten und Sekunden im Bereich 0 bis 59.

Für alle bisher genannten Zahlen wird der Typ int verwendet.

Für die Berechnung von Zeitdifferenzen wird eine Größe in Millisekunden verwendet, deren Nullpunkt am 1. Jänner 1970 um 0 Uhr liegt. Dafür wird der Typ long verwendet.

Methoden

Außerdem gibt es eine statische Methode mit der ein Datums-String in einem genormten Format (z.B. Unix- oder Internet-Format, siehe oben) in den entsprechenden Zeitpunkt in Millisekunden umgewandelt wird.

Wenn man mit den vorgefertigten Ausgabeformaten nicht zufrieden ist, kann man eine eigene Version einer DateFormat-Klasse schreiben, die die gewünschte Datums-Darstellung liefert. Damit die Verwendung zur Klasse DateFormat von Version 1.1 kompatibel ist, sollte diese Methode die folgende Signature haben:

Beispiel für ein einfaches, selbst geschriebenes DateFormat:

import java.util.*;
public class MyDateFormat { // Date Version 1.0
  public String format (Date d) {
    String s;
    // like SimpleDateFormat("d. M. yyyy")
    s = d.getDate() + ". " +
      (d.getMonth()+1) + ". " +
      (d.getYear()+1900);
    return s;
  }
}

Beispiel für die einfache Verarbeitung von Datums-Angaben in Version 1.0:

import java.util.*;

public class Date10 {  // Date Version 1.0
  public static void main (String[] args) {
    MyDateFormat df = new MyDateFormat();

    Date today = new Date();
    long todayInt = today.getTime();
    int currentYear = today.getYear() + 1900;
    System.out.println ("Today is " +
      df.format(today) );
    System.out.println ("The current year is " +
      currentYear );

    long yesterdayInt = todayInt - 24*60*60*1000L;
    Date yesterday = new Date (yesterdayInt);
    System.out.println ("Yesterday was " +
      df.format(yesterday) );

    long next30Int = todayInt + 30*24*60*60*1000L;
    Date next30 = new Date (next30Int);
    System.out.println ("30 days from today is " +
      df.format(next30) );

    Date marriage = new Date (90, Calendar.FEBRUARY, 7);
    long marriageInt = marriage.getTime();
    int marriageYear = marriage.getYear() + 1900;
    System.out.println ("Married on " +
      df.format(marriage) );
    System.out.println ("Married for " +
      (currentYear-marriageYear) + " years." );
    System.out.println ("Married for " +
      (todayInt-marriageInt)/(24*60*60*1000L) +
      " days." );

    Date silver= new Date (marriageInt);
      silver.setYear( marriage.getYear()+25 );
      // works for all dates except Feb 29th
    System.out.println ("Silver marriage on " +
      df.format(silver) );

    if ( silver.getYear() == today.getYear() &&
         silver.getMonth() == today.getMonth() &&
         silver.getDate() == today.getDate() )
    System.out.println ("Congratulations!");

  }
}

Date und Calendar Version 1.1

Klassen

Im JDK 1.1 soll für die "richtige", für alle Länder der Welt brauchbare Verarbeitung von Datums- und Zeitangaben eine Kombination der folgenden Klassen verwendet werden:

Die Klasse Date soll in diesem Fall nur für die Speicherung eines Zeitpunkts verwendet werden. Von den oben für Version 1.0 angeführten get- und set-Methoden sollen nur diejenigen verwendet werden, die den Zeitpunkt in Millisekunden angeben. Für die Angabe von Jahr, Monat, Tag und Uhrzeit sollen stattdessen die "besseren" Methoden des Kalenders verwendet werden.

Die richtige Kombination und Verwendung dieser Klassen ist einigermaßen kompliziert. Außerdem enthalten manche Implementierungen ein paar Fehler (Bugs) oder unterstützen nicht alle Länder und Sprachen. Deshalb greifen viele Java-Benutzer auf die alte Version 1.0 oder auf selbst geschriebene Klassen wie z.B. BigDate zurück.

Hier wird nur ein kurzes Beispiel für typische Anwendungen gegeben. Für die Details wird auf die Online-Dokumentation und auf die Fragen und Antworten in den einschlägigen Usenet-Newsgruppen verwiesen (siehe Referenzen).

Beispiel für Datums- und Zeit-Angaben und -Berechungen in Version 1.1:

import java.util.*;
import java.text.*;

public class Date11 {  // Date Version 1.1
  public static void main (String[] args) {

    DateFormat df = new SimpleDateFormat ("d. MMMM yyyy",
      Locale.GERMANY);
    DateFormat tf = new SimpleDateFormat ("HH.mm",
      Locale.GERMANY);

    SimpleTimeZone mez = new SimpleTimeZone( +1*60*60*1000,
      "CET");
    mez.setStartRule (Calendar.MARCH, -1, Calendar.SUNDAY,
      2*60*60*1000);
    mez.setEndRule (Calendar.OCTOBER, -1, Calendar.SUNDAY,
      2*60*60*1000);

    Calendar cal = GregorianCalendar.getInstance (mez);
    cal.setLenient(false); // do not allow bad values

    Date today = new Date();
    System.out.println ("Heute ist der " +
      df.format(today) );
    System.out.println ("Es ist " +
      tf.format(today) + " Uhr");

    cal.setTime(today);
    int currentYear = cal.get(Calendar.YEAR);
    System.out.println ("Wir haben das Jahr " +
      currentYear );

    cal.add (Calendar.DATE, -1);
    Date yesterday = cal.getTime();
    System.out.println ("Gestern war der " +
      df.format(yesterday) );

    try {
      cal.set (1997, Calendar.MAY, 35);
      cal.setTime(cal.getTime()); // to avoid a bug
      Date bad = cal.getTime();
      System.out.println ("Bad date was set to " +
        df.format(bad) );
    } catch (Exception e) {
      System.out.println ("Invalid date was detected "
        + e);
    }

    cal.set (1996, Calendar.FEBRUARY, 29);
    cal.setTime(cal.getTime()); // to avoid a bug
    Date marriage = cal.getTime();
    System.out.println ("geheiratet am " +
      df.format(marriage) );

    long todayInt = today.getTime();
    long marriageInt = marriage.getTime();
    long diff = todayInt - marriageInt;
    System.out.println ("seit " +
      diff/(24*60*60*1000L) +
      " Tagen verheiratet" );
    int marriageYear = cal.get(Calendar.YEAR);
    System.out.println ("seit " +
      (currentYear-marriageYear) +
      " Jahren verheiratet" );

    cal.setTime(marriage);
    /* bypass leap year error in add YEAR method: */
    if ( (cal.get(Calendar.MONTH) == Calendar.FEBRUARY) &&
         (cal.get(Calendar.DATE) == 29) ) {
      cal.add (Calendar.DATE, 1);
    }
    /* end of leap year error bypass */
    cal.add (Calendar.YEAR, 25);
    Date silverMarriage = cal.getTime();
    System.out.println ("Silberne Hochzeit am " +
      df.format(silverMarriage) );

    String todayDay = df.format(today);
    String silverDay = df.format(silverMarriage);
    // compare only day, not time:
    if ( silverDay.equals(todayDay) )
      System.out.println ("Herzlichen Glueckwunsch!");
  }
}

Zeitmessung

Für die Berechnung von Laufzeiten kann man entweder Date-Objekte (siehe oben) oder die folgende statische Methode verwenden:

Beispiel:

public static void main (String[] args) {
  long startTime = System.currentTimeMillis();
  ... // do something
  long endTime = System.currentTimeMillis();
  long runTime = endTime - startTime;
  float runSeconds = ( (float)runTime ) / 1000.F;
  System.out.println ("Run time was " + runSeconds + " seconds." );
}

Ausdrucken (PrintJob, PrinterJob)

PrintJob

Das AWT Version 1.1 enthält auch eine Klasse PrintJob, mit der ein Printout erzeugt und mit Hilfe des systemspezifischen Printer-Selection-Dialogs auf einem Drucker ausgedruckt werden kann.

Dazu dienen die folgenden Methoden in den verschiedenen Klassen:

Beispielskizze:

Frame f = new Frame ("Test");
f.setLayout(...);
f.add(...);
...
Toolkit t = f.getToolkit();
PrintJob pJob = t.getPrintJob (f, "Test", null);
Graphics g = pJob.getGraphics();
f.printAll(g); // or g.drawxxx ...
g.dispose();
pJob.end();

Wenn man ein Applet ausdrucken will, muss man mit getParent() das zugehörige Frame bestimmen und dann dieses in getPrintJob angeben. Außerdem muss der Benutzer dem Applet mit dem SecurityManager die Erlaubnis zum Drucken am Client-Rechner geben.

PrinterJob

Ab JDK 1.2 gibt es eine neue Klasse PrinterJob mit den Interfaces Printable und Pageable im Package java.awt.print.

Dazu dienen unter anderem die folgenden Klassen und Methoden:

Beispielskizze:
public class Xxxx extends Frame
  implements ActionListener, Printable {
  ...
  public void actionPerformed(ActionEvent e) {
    if (e.getSource() == printButton) {
      PrinterJob printJob = PrinterJob.getPrinterJob();
      printJob.setPrintable(this);
      try {
       printJob.print();
      } catch (PrintException ex) {
      }
    }
 }
 public int print(Graphics g, PageFormat pageFormat, int pageIndex) {
   if (pageIndex == 0) {
      g.translate(100, 100);
      paint(g); // or: this.printAll(g); or: g.drawxxx ...
      return Printable.PAGE_EXISTS;
   }
   return Printable.NO_SUCH_PAGE;
 }
}

Außerdem können mit PrinterJob auch Objekte, die das Interface Pageable implementieren, ausgedruckt werden, das sind Dokumente, die aus mehreren Seiten (Page) bestehen, mit Seiteninhalt, Kopf- und Fußzeilen und Seitennummern. Die Klasse Book ist eine Klasse, die dieses Interface implementiert.

Sonstige Möglichkeiten

Weitere Alternativen zur Verwendung von PrintJob sind die Ausgabe auf das Pseudo-File mit den Filenamen "lpt1" oder die Verwendung der Print-Screen-Funktion durch den Benutzer, um den aktuellen Fensterinhalt (z.B. eine Web-Page mit einem Applet mit Benutzer-Eingaben) auszudrucken. In manchen Fällen kann es auch günstig sein, die Information, die ausgedruckt werden soll, als HTML-File zu erzeugen, das dann vom Benutzer in seinem Web-Browser angezeigt und ausgedruckt werden kann.

Ausführung von Programmen (Runtime, exec)

Mit der Klasse Runtime und deren Methode exec kann man innerhalb von Applikationen andere Programme (Hauptprogramme, Prozesse) starten. Dies hat den Vorteil, dass man alle Funktionen des Betriebssystems ausnützen kann, und den Nachteil, dass die Java-Applikation dadurch nicht mehr Plattform- oder Rechner-unabhängig ist

Applets können im Allgemeinen keine anderen Programme starten.

Für die Verwendung von Runtime und Process muss das Package java.util importiert werden, im Fall von Ein-Ausgabe-Operationen auch java.io.

Die für diesen Zweck wichtigsten Methoden der Klasse Runtime sind:

Die exec-Methode kann die Fehlersituation IOException werfen, wenn es den Befehl (das Programm) nicht gibt.

Die wichtigsten Methoden der Klasse Process sind:

Die waitFor-Methode kann die Fehlersituation InterruptedException werfen, wenn das Programm während des Wartens abgebrochen wird.

Beispiel (Unix):

try {
  Process p = Runtime.getRuntime().exec
    ("/usr/local/bin/elm");
  p.waitFor();
} catch (Exception e) {
    System.err.println("elm error " +e);
}

Das analoge Beispiel für einen PC enthält

Process p = Runtime.getRuntime().exec
  ("c:\\public\\pegasus\\pmail.exe");

Befehlsnamen müssen im Allgemeinen mit dem Pfad angegeben werden.

Auf PCs muss man beachten, dass man als Befehlsname nur "echte" exe- oder com-Programme angeben kann. Die meisten DOS-Befehle wie DIR oder COPY sind aber nicht eigene Programm-Files sondern werden von der "Shell" COMMAND.COM (unter DOS, Windows 3 und Windows 95) bzw. CMD.EXE (unter Windows NT) ausgeführt. Beispiel:

Process p = Runtime.getRuntime().exec
  ("command.com /c dir");

Wenn man unter Unix eine Eingabe- oder Ausgabe-Umleitung oder Pipe für den Befehl angeben will, muss man analog zuerst eine Unix-Shell aufrufen und dieser Shell dann den kompletten Befehl (einschließlich der Umleitung) als einen einzigen String-Parameter angeben. Beispiel:

Process p = Runtime.getRuntime().exec (new String[] {
  "/usr/bin/sh", "-c", "/usr/bin/ls > ls.out" } );

Mit Hilfe der Methode getOutputStream kann man Eingaben vom Java-Programm an den Prozess senden. Mit Hilfe der Methoden getInputStream und getErrorStream kann man die vom Prozess erzeugte Ausgabe im Java-Programm verarbeiten. Beispiel:

try {
  String cmd = "/usr/bin/ls -l /opt/java/bin";
  String line = null;
  Process p = Runtime.getRuntime().exec(cmd);
  BufferedReader lsOut = new BufferedReader
    (new InputStreamReader
      (p.getInputStream() ) );
  while( ( line=lsOut.readLine() ) != null) {
    System.out.println(line);
  }
} catch (Exception e) {
    System.err.println("ls error " +e);
}
Wenn man allerdings zwei oder alle drei dieser Eingabe- und Ausgabe-Ströme lesen bzw. schreiben will, muss man das in getrennten Threads tun, weil sonst ein auf Eingabe wartendes Read die anderen blockiert.

Verwendung von Unterprogrammen (native methods, JNI)

Man kann innerhalb von Java-Applikationen auch Unterprogramme aufrufen, die in einer anderen Programmiersprache geschrieben sind, insbesondere in den Programmiersprachen C und C++. Solche Unterprogramme werden als "eingeborene" (native) Methoden bezeichnet, das entsprechende Interface als Java Native Interface (JNI). Der Vorteil liegt darin, dass man sämtliche von dieser Programmiersprache unterstützten Funktionen und Unterprogramm-Bibliotheken verwenden kann. Der Nachteil liegt darin, dass die Java-Applikation dann im Allgemeinen nicht mehr Plattform- oder auch nur Rechner-unabhängig ist.

Der Vorgang läuft in folgenden Schritten ab:

Zunächst wird eine Java-Klasse geschrieben, die folgende Elemente enthält:

Beispiel:
public class ClassName {
  public native void name() ;
  static {
    System.loadLibrary ("libname");
  }
  ...
}

Diese Klasse kann wie jede normale Klasse verwendet werden, d.h. man kann Objekte dieser Klasse mit new anlegen und ihre Methoden für dieses Objekt aufrufen.

Als nächstes werden mit dem Hilfsprogramm javah Header-Files und ein sogenanntes Stub-File erstellt. Diese Files enthalten die entsprechenden Deklarationen in C-Syntax, die dann vom C-Programm verwendet werden. Es gibt auch Umwandlungs-Programme für die Umwandlung von Java-Strings in C-Strings und umgekehrt.

Unter Verwendung dieser Hilfsmittel wird nun das C-Programm geschrieben und übersetzt und in einer Library (Bibliotheks-File) für dynamisches Laden zur Laufzeit gespeichert. Diese Library muss in die Environment-Variable LD_LIBRARY_PATH hinzugefügt werden.

Schließlich werden die Java-Klassen mit dem Java-Compiler javac übersetzt und mit dem Befehl java ausgeführt. Dabei wird das C-Programm automatisch aus der vorher erstellten Library dazugeladen.

Für die Details wird auf die Online-Dokumentation und auf die einschlägige Literatur verwiesen.


Datenbanken

Java eignet sich besonders gut für Graphische User-Interfaces und für Internet-Anwendungen. Datenbanksysteme eignen sich besonders gut für die Speicherung von komplexen und umfangreichen Datenmengen. Wie kann ich diese beiden Technologien "verheiraten"?

Zu den wichtigsten Anwendungsgebieten von Java zählen User-Interfaces zu Datenbanksystemen.

Das Java-Programm kann dabei ein Applet, eine Applikation oder ein Servlet sein und kann am selben Rechner wie die Datenbank laufen oder auch auf einem anderen Rechner und von dort über das Internet oder ein Intranet auf die Datenbank zugreifen (siehe Datenbank-Anwendungen über das Internet).

Die "Java Database Connectivity" (JDBC) ist im Sinne der Plattformunabhängigkeit von Java so aufgebaut, dass das Java-Programm von der Hard- und Software des Datenbanksystems unabhängig ist und somit für alle Datenbanksysteme (MS-Access, Oracle etc.) funktioniert.

Mit den im JDBC enthaltenen Java-Klassen (Package java.sql) kann man Daten in der Datenbank so bequem speichern und abfragen wie beim Lesen und Schreiben von Dateien oder Sockets. Auf diese Weise kann man die Vorteile von Java, die vor allem bei der Gestaltung von (graphischen und plattformunabhängigen) User-Interfaces und von Netz-Verbindungen liegen, mit der Mächtigkeit von Datenbanksystemen verbinden.

Relationale Datenbanken

Relationale Datenbanken bestehen aus Tabellen (Relationen). Die Tabellen entsprechen in etwa den Klassen in der Objektorientierten Programmierung. Beispiele: Eine Personaldatenbank enthält Tabellen für Mitarbeiter, Abteilungen, Projekte. Eine Literaturdatenbank enthält Tabellen für Bücher, Zeitschriften, Autoren, Verlage.

Diese Tabellen können in Beziehungen zueinander stehen (daher der Name "Relation"). Beispiele: Ein Mitarbeiter gehört zu einer Abteilung und eventuell zu einem oder mehreren Projekten. Jede Abteilung und jedes Projekt wird von einem Mitarbeiter geleitet. Ein Buch ist in einem Verlag erschienen und hat einen oder mehrere Autoren.

Beispielskizze für eine Tabelle "Mitarbeiter":

Abteilung Vorname Zuname Geburtsjahr Gehalt
EDV-Zentrum Hans Fleißig 1972 2400.00
EDV-Zentrum Grete Tüchtig 1949 3200.00
Personalstelle Peter Gscheitl 1968 1600.00

Jede Zeile der Tabelle (row, Tupel, Record) enthält die Eigenschaften eines Elementes dieser Menge, entspricht also einem Objekt. In den obigen Beispielen also jeweils ein bestimmter Mitarbeiter, eine Abteilung, ein Projekt, ein Buch, ein Autor, ein Verlag. Jede Zeile muss eindeutig sein, d.h. verschiedene Mitarbeiter müssen sich durch mindestens ein Datenfeld (eine Eigenschaft) unterscheiden.

Jede Spalte der Tabelle (column, field, entity) enthält die gleichen Eigenschaften der verschiedenen Objekte, entspricht also einem Datenfeld. Beispiele: Vorname, Zuname, Geburtsjahr und Abteilung eines Mitarbeiters, oder Titel, Umfang, Verlag und Erscheinungsjahr eines Buches.

Ein Datenfeld (z.B. die Sozialversicherungsnummer eines Mitarbeiters oder die ISBN eines Buches) oder eine Gruppe von Datenfeldern (z.B. Vorname, Zuname und Geburtsdatum einer Person) muss eindeutig sein, sie ist dann der Schlüssel (key) zum Zugriff auf die Zeilen (Records) in dieser Tabelle. Eventuell muss man dafür eigene Schlüsselfelder einrichten, z.B. eine eindeutige Projektnummer, falls es mehrere Projekte mit dem gleichen Namen gibt.

Die Beziehungen zwischen den Tabellen können auch durch weitere Tabellen (Relationen) dargestellt werden, z.B. eine Buch-Autor-Relation, wobei sowohl ein bestimmtes Buch als auch ein bestimmter Autor eventuell in mehreren Zeilen dieser Tabelle vorkommen kann, denn ein Buch kann mehrere Autoren haben und ein Autor kann mehrere Bücher geschrieben haben. Das Gleiche gilt für die Relation Mitarbeiter-Projekt.

Das Konzept (Design) einer Datenbank ist eine komplexe Aufgabe, es geht dabei darum, alle relevanten Daten (Elemente, Entities) und alle Beziehungen zwischen ihnen festzustellen und dann die logische Struktur der Datenbank entsprechend festzulegen. Die logisch richtige Aufteilung der Daten in die einzelnen Tabellen (Relationen) wird als Normalisierung der Datenbank bezeichnet, es gibt dafür verschiedene Regeln und Grundsätze, ein Beispiel ist die sogenannte dritte Normalform.

Fast alle Datenbanksysteme seit Ende der 70er- oder Beginn der 80er-Jahre sind relationale Datenbanksysteme und haben damit die in den 60er- und 70er-Jahren verwendeten, bloß hierarchischen Datenbanksysteme abgelöst.

Eine zukunftweisende Weiterentwicklung der Relationalen Datenbanken sind die "Objektrelationalen Datenbanken", bei denen - vereinfacht gesprochen - nicht nur primitive Datentypen sondern auch komplexe Objekte als Datenfelder möglich sind, ähnlich wie in der objektorientierten Programmierung.

Datenschutz und Datensicherheit

Datenbanken enthalten meist umfangreiche und wichtige, wertvolle Informationen. Daher muss bei Datenbanksystemen besonderer Wert auf den Datenschutz und die Datensicherheit gelegt werden.

Die Datenbank-Benutzer werden in 2 Gruppen aufgeteilt: den Datenbankadministrator und die Datenbankanwender.

Der Datenbankadministrator legt mit der Data Definition Language (DDL) die logischen Struktur der Datenbank fest und ist für den Datenschutz, die Vergabe der Berechtigungen an die Datenbankanwender und für die Datensicherung verantwortlich.

Die Datenbankanwender können mit einer Data Manipulation Language (DML) oder Query Language die Daten in der Datenbank speichern, abfragen oder verändern.

Mit der Hilfe von Usernames und Passwörtern werden die einzelnen Benutzer identifiziert, und der Datenbankadministrator kann und muss sehr detailliert festlegen, welcher Benutzer welche Aktionen (lesen, hinzufügen, löschen, verändern) mit welchen Teilen der Datenbank (Tabellen, Spalten, Zeilen) ausführen darf.

Datenkonsistenz

Die in einer Datenbank gespeicherten Informationen stehen meistens in Beziehungen zueinander, bestimmte Informationen hängen in eventuell recht komplexer Weise von anderen, ebenfalls in der Datenbank gespeicherten Informationen ab. Es muss sichergestellt werden, dass die gesamte Datenbank immer in einem gültigen Zustand ist, dass also niemals ungültige oder einander widersprechende Informationen darin gespeichert werden.

Dies wird mittels Transaktionen erreicht: Unter einer Transaktion versteht man eine Folge von logisch zusammengehörenden Aktionen, die nur entweder alle vollständig oder überhaupt nicht ausgeführt werden dürfen.

Beispiel: Wenn zwei Mitarbeiter den Arbeitsplatz tauschen, muss sowohl beim Mitarbeiter 1 der Arbeitsplatz von A auf B als auch beim Mitarbeiter 2 der Arbeitsplatz von B auf A geändert werden. Würde bei einer dieser beiden Aktionen ein Fehler auftreten, die andere aber trotzdem ausgeführt werden, dann hätten wir plötzlich 2 Mitarbeiter auf dem einen Arbeitsplatz und gar keinen auf dem anderen.

Ein anderes Beispiel: Wenn in der Datenbank nicht nur die Gehälter der einzelnen Mitarbeiter sondern auch die Summe aller Personalkosten (innerhalb des Budgets) gespeichert ist, dann müssen bei jeder Gehaltserhöhung beide Felder um den gleichen Betrag erhöht werden, sonst stimmen Budgetplanung und Gehaltsauszahlung nicht überein.

Um solche Inkonsistenzen zu vermeiden, müssen zusammengehörende Aktionen jeweils zu einer Transaktion zusammengefasst werden:

Structured Query Language (SQL)

SQL hat sich seit den 80er-Jahren als die von allen Datenbanksystemen (wenn auch eventuell mit kleinen Unterschieden) unterstützte Abfragesprache durchgesetzt, und die Version SQL2 ist seit 1992 auch offiziell genormt.

SQL umfasst alle 3 Bereiche der Datenbankbenutzung:

Für eine komplette Beschreibung von SQL wird auf die Fachliteratur verwiesen, hier sollen nur ein paar typische Beispiele gezeigt werden.

Datentypen

SQL kennt unter anderem die folgenden Datentypen:

Definition der Datenbankstruktur (DDL)

CREATE = Einrichten einer neuen Tabelle.

Beispiel:

CREATE TABLE Employees (
  INT      EmployeeNumber ,
  CHAR(30) FirstName ,
  CHAR(30) LastName ,
  INT      BirthYear ,
  FLOAT    Salary
)
definiert eine Tabelle "Employees" (Mitarbeiter) mit den angegebenen Datenfeldern. Die Erlaubnis dazu hat meistens nur der Datenbankadministrator.

ALTER = Ändern einer Tabellendefinition.

DROP = Löschen einer Tabellendefinition.

Änderungen an den Daten (Updates)

INSERT = Speichern eines Records in einer Tabelle.

Beispiel:

INSERT INTO Employees
  (EmployeeNumber, FirstName, LastName, BirthYear, Salary)
  VALUES ( 4710, 'Hans', 'Fleißig', 1972, 2400.0 )
INSERT INTO Employees
  (EmployeeNumber, FirstName, LastName, BirthYear, Salary)
  VALUES ( 4711, 'Grete', 'Tüchtig', 1949, 3200.0 )
speichert zwei Mitarbeiter-Records mit den angegebenen Daten.

UPDATE = Verändern von Datenfeldern in einem oder mehreren Records.

Beispiel:

UPDATE Employees
  SET Salary = 3600.0 WHERE LastName = 'Tüchtig'
setzt das Gehalt bei allen Mitarbeitern, die Tüchtig heißen, auf 3600.

DELETE = Löschen eines oder mehrerer Records

Beispiel:

DELETE FROM Employees WHERE EmployeeNumber = 4710
löscht den Mitarbeiter mit der Nummer 4710 aus der Datenbank.

Abfrage von Daten

SELECT = Abfragen von gespeicherten Daten.

Beispiele:

SELECT * FROM Employees
liefert alle Datenfelder von allen Records der Tabelle Employees.
SELECT LastName, FirstName, Salary FROM Employees
liefert die angegebenen Datenfelder von allen Records der Tabelle Employees.
SELECT LastName, Salary
  FROM Employees WHERE BirthYear <= 1970
  ORDER BY Salary DESC
liefert den Zunamen und das Gehalt von allen Mitarbeitern, die 1970 oder früher geboren sind, sortiert nach dem Gehalt in der umgekehrten Reihenfolge (höchstes zuerst).
SELECT * FROM Employees
  WHERE LastName = 'Fleißig' AND FirstName LIKE 'H%'
liefert alle Daten derjenigen Mitarbeiter, deren Zuname Fleißig ist und deren Vorname mit H beginnt.

Datenbank-Zugriffe in Java (JDBC)

Mit Hilfe der "Java Database Connectivity" (JDBC) kann man innerhalb von Java-Programmen auf Datenbanken zugreifen und Daten abfragen, speichern oder verändern, wenn das Datenbanksystem die "Standard Query Language" SQL verwendet, was bei allen wesentlichen Datenbanksystemen seit den 80er-Jahren der Fall ist.

Im Java-Programm werden nur die logischen Eigenschaften der Datenbank und der Daten angesprochen (also nur die Namen und Typen der Relationen und Datenfelder), und die Datenbank-Operationen werden in der genormten Abfragesprache SQL formuliert (Version SQL 2 Entry Level).

Das Java-Programm ist somit von der Hard- und Software des Datenbanksystems unabhängig. Erreicht wird dies durch einen sogenannten "Treiber" (Driver), der zur Laufzeit die Verbindung zwischen dem Java-Programm und dem Datenbanksystem herstellt - ähnlich wie ein Drucker-Treiber die Verbindung zwischen einem Textverarbeitungsprogramm und dem Drucker oder zwischen einem Graphikprogramm und dem Plotter herstellt. Falls die Datenbank auf ein anderes System umgestellt ist, braucht nur der Driver ausgewechselt werden, und die Java-Programme können ansonsten unverändert weiter verwendet werden.

Der JDBC-Driver kann auch über das Internet bzw. Intranet vom Java-Client direkt auf den Datenbank-Server zugreifen, ohne dass man eine eigene Server-Applikation (CGI oder Servlet) schreiben muss.

Das JDBC ist bereits im Java Development Kit JDK enthalten (ab 1.1), und zwar im Package java.sql. Mit dem JDK 1.2 kam eine neue Version JDBC 2.0 heraus, die eine Reihe von zusätzlichen Features enthält. Im Folgenden werden aber nur die (wichtigen) Features beschrieben, die sowohl in JDBC 1.x als auch in JDBC 2.0 enthalten sind.

Die Software für die Server-Seite und die JDBC-Driver auf der Client-Seite müssen vom jeweiligen Software-Hersteller des Datenbanksystems erworben werden (also z.B. von der Firma Oracle). Dafür gibt es dann eigene Packages wie z.B. sun.jdbc oder com.firma.produkt. Das JDK enthält auch eine sogenannte JDBC-ODBC-Brücke für den Zugriff auf ODBC-Datenbanken (Open Database Connectivity, z.B. MS-Access).

Die wichtigsten Programmelemente

Beispielskizze für eine Datenbank-Abfrage (select)

Hier eine Skizze für den Aufbau einer typischen Datenbank-Anwendung mit einer Abfrage von Daten. Mehr Informationen über die darin vorkommenden Klassen und Methoden finden Sie in den nachfolgenden Abschnitten und in der Online-Dokumentation.

import java.sql.*;
...
try {
  Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
  String username="admin";
  String password="geheim";
  Connection con = DriverManager.getConnection
    ("jdbc:odbc://hostname/databasename", username, password);
  con.setReadOnly(true);
  Statement stmt = con.createStatement();

  ResultSet rs = stmt.executeQuery
    ("SELECT LastName, Salary, Age, Sex FROM Employees");
  System.out.println("List of all employees:");
  while (rs.next()) {
    System.out.print(" name=" + rs.getString(1) );
    System.out.print(" salary=" + rs.getDouble(2) );
    System.out.print(" age=" + rs.getInt(3) );
    if ( rs.getBoolean(4) ) System.out.print(" sex=M");
    else                    System.out.print(" sex=F");
    System.out.println();
  }

  rs.close();
  stmt.close();
  con.close();
} catch ...

Beispielskizze für einzelne Updates der Datenbank (insert, update, delete)

import java.sql.*;
...
try {
  Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
  String username="admin";
  String password="geheim";
  Connection con = DriverManager.getConnection
    ("jdbc:odbc://hostname/databasename", username, password);
  Statement stmt = con.createStatement();
  int rowCount = stmt.executeUpdate
    ("UPDATE Employees " +
     "SET Salary = 5000.0 WHERE LastName = 'Partl' ");
  System.out.println(
     rowCount + "Gehaltserhoehungen durchgefuehrt.");
  stmt.close();
  con.close();
} catch ...
oder bei mehreren Änderungen analog mit:
 int rowCount = 0;
 for (int i=0; i<goodPerson.length; i++) {
   rowCount = rowCount + stmt.executeUpdate
    ("UPDATE Employees SET Salary = " + goodPerson[i].getSalary() +
     " WHERE LastName = '" + goodPerson[i].getName() + "' ");
  }
  System.out.println(
    rowCount + "Gehaltserhoehungen durchgefuehrt.");

Beispielskizze für umfangreiche Updates der Datenbank (PreparedStatement)

import java.sql.*;
...
try {
  Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
  String username="admin";
  String password="geheim";
  Connection con = DriverManager.getConnection
    ("jdbc:odbc://hostname/databasename", username, password);
  con.setAutoCommit(false);

  PreparedStatement ps = con.prepareStatement
    ("UPDATE Employees SET Salary = ? WHERE LastName = ? ");
  for (int i=0; i<goodPerson.length; i++) {
    ps.setDouble (1, goodPerson[i].getSalary() );
    ps.setString (2, goodPerson[i].getName()   );
    ps.executeUpdate();
  }
  ps.close();
  con.commit();

  con.close();
} catch ...

Driver

Vor dem Aufbau einer Verbindung zur Datenbank muss der JDBC-Driver in das Java-Programm geladen werden,

Typische Drivernamen sind zum Beispiel:
sun.jdbc.odbc.JdbcOdbcDriver
oracle.jdbc.driver.OracleDriver

Connection

Die Klasse Connection dient zur Verbindung mit einer Datenbank. Erzeugt wird diese Verbindung vom JDBC DriverManager in der Form

Connection con =
   DriverManager.getConnection (URL, username, password);
Der URL hat eine der folgenden Formen: Der Username und das Passwort sind als String-Parameter anzugeben. Aus Sicherheitsgründen empfiehlt es sich, das Passwort nicht fix im Programm anzugeben sondern vom Benutzer zur Laufzeit eingeben zu lassen, am besten in einem TextField mit setEchoCharacter('*') oder in Swing mit einem JPasswordField.

Die wichtigsten Methoden der Klasse Connection sind:

Beispielskizze:
String username="admin";
String password="geheim";
Connection con = DriverManager.getConnection
  ("jdbc:odbc://hostname/databasename", username, password);
Statement stmt = con.createStatement();
con.setReadOnly(true);
...
stmt.close();
con.close();
Innerhalb einer Connection können mehrere Statements geöffnet werden, eventuell auch mehrere gleichzeitig.

Statement

Die Klasse Statement dient zur Ausführung eines SQL-Statements oder von mehreren SQL-Statements nacheinander. Erzeugt wird dieses Statement von der Connection in der Form

Statement stmt = con.createStatement();
Die wichtigsten Methoden der Klasse Statement sind: Beispielskizze für eine Abfrage:
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery
  ("SELECT LastName, Salary, Age, Sex FROM Employees");
...
rs.close();
stmt.close();
Beispielskizze für ein Update:
Statement stmt = con.createStatement();
int rowCount = stmt.executeUpdate
  ("UPDATE Employees" +
   "SET Salary = 5000.0 WHERE LastName = 'Partl' ");
...
stmt.close();
Innerhalb eines Statements darf zu jedem Zeitpunkt immer nur höchstens 1 ResultSet offen sein.

ResultSet

Ein ResultSet ist das Ergebnis einer SQL-Abfrage, im Allgemeinen das Ergebnis eines SELECT-Statements. Es enthält einen oder mehrere Records von Datenfeldern und bietet Methoden, um diese Datenfelder ins Java-Programm hereinzuholen.

Die wichtigsten Methoden der Klasse ResultSet sind:

Beispielskizze:
ResultSet rs = stmt.executeQuery
  ("SELECT LastName, Salary, Age, Sex FROM Employees");
System.out.println("List of all employees:");
  while (rs.next()) {
    System.out.print(" name=" + rs.getString(1) );
    System.out.print(" salary=" + rs.getDouble(2) );
    System.out.print(" age=" + rs.getInt(3) );
    if ( rs.getBoolean(4) ) System.out.print(" sex=M");
    else                    System.out.print(" sex=F");
    System.out.println();
  }
rs.close();
Anmerkungen:

Innerhalb eines Statements darf zu jedem Zeitpunkt immer nur höchstens 1 ResultSet offen sein. Wenn man mehrere ResultSets gleichzeitig braucht, muss man dafür mehrere Statements innerhalb der Connection öffnen (sofern das Datenbanksystem das erlaubt).

Die Zugriffe auf die Felder sollen in der Reihenfolge erfolgen, wie sie von der Datenbank geliefert werden, also in der Reihenfolge, in der sie im SELECT-Befehl bzw. bei * im Record stehen. Bei JDBC 1.x können die Records auch nur in der Reihenfolge, in der sie von der Datenbank geliefert werden, verarbeitet werden (mit next).

Ab JDBC 2.0 enthält die Klasse ResultSet zahlreiche weitere Methoden, die auch ein mehrmaliges Lesen der Records (z.B. mit previous) und auch Datenänderungen in einzelnen Records (z.B. mit updateString, updateInt) erlauben.

Ansonsten kann man ein mehrmaliges Abarbeiten der Resultate erreichen, indem man sie in einer Liste oder einem Vector zwischenspeichert. Beispielskizze:

ResultSet rs = stmt.getResultSet();
ResultSetMetaData md = rs.getMetaData();
int numberOfColumns =  md.getColumnCount();
int numberOfRows = 0;
Vector rows = new Vector();
while (rs.next()) {
  numberOfRows++;
  Vector newRow = new Vector();
  for (int i=1; i<=numberOfColumns; i++) {
    newRow.addElement(rs.getString(i));
  }
  rows.addElement (newRow);
}
rs.close();

PreparedStatement

Wenn man viele Updates nacheinander ausführt und dazu jedesmal mit executeUpdate einen kompletten INSERT- oder UPDATE-SQL-Befehl an die Datenbank sendet, muss jedesmal wieder der SQL-Befehl interpretiert und dann ausgeführt werden. Bei einer großen Anzahl von ähnlichen Updates kann dies sehr viel unnötige Rechenzeit in Anspruch nehmen.

Um den Update-Vorgang zu beschleunigen, kann man in diesem Fall mit der Connection-Methode prepareStatement ein Muster für den SQL-Befehl an das Datenbanksystem senden, in dem die variablen Datenfelder mit Fragezeichen gekennzeichnet sind, und dann mit den Methoden der Klasse PreparedStatement nur mehr die Daten in diese vorbereiteten SQL-Statements "einfüllen".

Beispielskizze:

con.setAutoCommit(false);
PreparedStatement ps = con.prepareStatement
  ("UPDATE Employees SET Salary = ? WHERE LastName = ? ");
for (int i=0; i<goodPerson.length; i++) {
  ps.setFloat  (1, newSalary[i]  );
  ps.setString (2, goodPerson[i] );
  ps.executeUpdate();
}
con.commit();
con.close();

Stored Procedure und CallableStatement

Eine effiziente Alternative zu SQL-Befehlen ("ad hoc queries") sind sogenannte "stored procedures", das sind Abfragen oder Update-Aktionen, die in der Datenbank bereits fertig ausführbar gespeichert sind.

In diesem Fall kann man mit der Conection-Methode prepareCall ein Muster für den Aufruf der Stored Procedure an das Datenbanksystem senden, in dem die variablen Datenfelder mit Fragezeichen gekennzeichnet sind, und dann mit den Methoden der Klasse CallabaleStatement nur mehr die Parameterwerte in den Aufruf "einfüllen" und das Ergebnis "herausholen".

Beispielskizze für eine Stored Procedure mit einfachem Rückgabewert:

CallableStatement cs = con.prepareCall
  ("{ ? = call PROCNAME(?,?) }");
cs.registerOutParameter(1, Types.DOUBLE);
cs.setDouble (2, 0.00  );
cs.setString (3, "..." );
cs.execute();
double result = cs.getDouble(1);
cs.close();

Beispielskizze für eine Stored Procedure mit ResultSet (Spezialfall Oracle):

CallableStatement cs = con.prepareCall
  ("{ ? = call PROCNAME(?,?) }");
cs.registerOutParameter(1, OracleTypes.CURSOR);
cs.setDouble (2, 0.00  );
cs.setString (3, "..." );
cs.execute();
ResultSet rs = ( (OracleCallableStatement) cs).getCursor(1);
while (rs.next()) {
  ... // get values from result set ...
}
rs.close();
cs.close();

DatabaseMetaData und ResultSetMetaData

Mit den Klassen DatabaseMetaData und ResultSetMetaData kann man Informationen über die Datenbank bzw. das ResultSet erhalten, also z.B. welche Tabellen (Relationen) definiert sind, wie die Datenfelder heißen und welchen Typ sie haben, und dergleichen.

Den Zugriff auf die DatabaseMetaData erhält man mit der Methode getMetaData in der Connection. Damit kann man dann alle möglichen Eigenschaften des Datenbanksystems, des JDBC-Drivers, der Datenbank und der Connection abfragen (siehe die Online-Dokumentation).

Den Zugriff auf die ResultSetMetaData erhält man mit der Methode getMetaData im ResultSet. Ein paar typische Methoden der Klasse ResultSetMetaData sind:

Datenbank-Anwendungen über das Internet

Wie kann das Konzept bzw. die "Architektur" einer Java-Datenbank-Anwendung aussehen, wenn die Benutzer über das Internet oder ein Intranet von ihren Client-Rechnern aus auf die Datenbank zugreifen sollen?

Ein solches Client-Server-System kann z.B. aus folgenden Komponenten zusammengesetzt werden:

Die Applikation auf dem Server (bzw. das Servlet) greift auf das Datenbanksystem und damit auf die Daten zu. Es erhält vom Client Abfragen oder Daten, führt die entsprechenden Datenbank-Abfragen oder Daten-Eingaben durch und liefert die Ergebnisse an den Client.

Das Applet stellt das User-Interface dar. Es sendet die Abfragen oder Dateneingaben des Benutzers an den Server und gibt die von dort erhaltenen Daten oder Meldungen an den Benutzer aus.

Das Applet kann von den Benutzern als Teil einer Web-Page mit dem Web-Browser über das Internet geladen werden. Der Benutzer braucht für den Datenbankzugriff also keine andere Software als nur seinen Java-fähigen Web-Browser.

Die Kommunikation zwischen Applet und Server kann je nach der verfügbaren Software auf verschiedene Arten erfolgen:

Die Kommunikation zwischen Server-Applikation (bzw. Servlet) und Datenbank kann erfolgen. Dabei können das Server-Programm und das Datenbanksystem

Eine weitere Möglichkeit wäre es, dass das Applet selbst mit JDBC über das Internet direkt auf die Datenbank zugreift, mit einem geeigneten JDBC-Driver, der in diesem Fall allerdings auf jedem Client installiert sein oder mit dem Applet mitgeladen werden muss.

Außerdem wäre es auch möglich, statt eines Applet eine Java-Applikation am Client zu installieren, die dann wiederum entweder mit Umweg über ein Server-Programm oder direkt mit einem JDBC-Driver auf die Datenbank zugreift.

Fortgeschrittene Java-Programmierer können auch RMI (Remote Method Invocation) oder EJB (Enterprise Java Beans) oder CORBA (Common Object Request Broker Architecture) verwenden.

Bei wichtigen oder sensiblen Daten muss jeweils auf die Datensicherheit und auf den Datenschutz geachtet werden (Passwort-Schutz, Verschlüsselung der Daten bei der Übertragung).

Übung: eine kleine Datenbank

Diese Übung besteht aus mehreren Schritten und kann nur dann ausgeführt werden, wenn Sie ein Datenbanksystem und den zugehörigen JDBC-Driver auf Ihrem Rechner installiert haben und wenn Sie wissen, wie man mit diesem Datenbanksystem Datenbanken definiert und verwendet. Wenn dies nicht der Fall ist, können Sie "nur" das Konzept dieser Übungsbeispiele überlegen und die Java-Programme nur schreiben und compilieren, aber nicht ausführen.

Für diese Übung werden die Datenbank und die Java-Applikationen am selben Rechner angelegt und ausgeführt, also ohne Internet-Verbindung.

Vorbereitung

Legen Sie - zum Beispiel mit MS-Access - eine Datenbank an, die eine Tabelle "Mitarbeiter" mit den folgenden Datenfeldern enthält: Abteilung (Text, String), Vorname (Text, String), Zuname (Text, String), Geburtsjahr (Zahl, int), Gehalt (Zahl, double).

Füllen Sie diese Tabelle mit ein paar Datenrecords, etwa wie im Beispiel im Kapitel über relationale Datenbanksysteme.

Registrieren Sie diese Datenbank auf Ihrem Rechner, zum Beispiel mit dem ODBC-Manager der MS-Windows Systemsteuerung als DSN (Datenquellenname). Dann können Sie den einfachen JDBC-ODBC-Driver verwenden, der im JDK enthalten ist (aber manchmal unerklärliche Fehler liefert).

Bei der Verwendung von anderen Datenbanksystemen müssen Sie sicherstellen, dass ein entsprechender JDBC-Driver verfügbar ist, und die Datenbank dementsprechend anlegen und registrieren.

Einfache Übrungsbeispiele

1. Liste der Geburtsjahre

Schreiben Sie eine einfache Java-Applikation, die eine Liste aller Mitarbeiter mit Vorname und Geburtsjahr auf den Bildschirm ausgibt.

2. Berechnung des Durchschnittsgehalts

Schreiben Sie eine einfache Java-Applikation, die das Durchschnittsgehalt der Mitarbeiter berechnet und auf den Bildschirm ausgibt. Zu diesem Zweck fragen Sie das Gehalt von allen Mitarbeitern ab, bilden die Summe, zählen die Anzahl und dividieren schließlich die Summe durch die Anzahl.

Komplizierte Übrungsbeispiele

Wenn Sie wollen, können Sie das einfache Beispiel auch durch die folgenderen, etwas aufwändigeren Aufgaben ergänzen.

3. Speichern

Schreiben Sie eine einfache Java-Applikation, die ein paar (mindestens 3, höchstens 10) Mitarbeiter-Records in dieser Datenbank speichert.

4. Liste (Abfrage)

Schreiben Sie eine einfache Java-Applikation, die eine Liste aller Mitarbeiter mit Vorname, Zuname und Alter (in Jahren) ausgibt.

5. Berechnungen (Abfrage)

Schreiben Sie eine einfache Java-Applikation, die die Anzahl der Mitarbeiter, das Durchschnittsalter und die Gehaltssumme (Summe der Monatsgehälter) ausgibt.

6. Einfache GUI-Applikation (Abfrage)

Schreiben Sie eine Java-GUI-Applikation, bei der der Benutzer in einem TextField einen Zunamen eingeben kann und dann entweder alle Daten über diesen Mitarbeiter am Bildschirm aufgelistet erhält oder eine Fehlermeldung, dass es keinen Mitarbeiter mit diesem Namen gibt. Das GUI soll auch einen Exit-Button haben, mit dem man die Applikation beenden kann.

Username und Passwort geben Sie der Einfachheit halber direkt im Programm an, auch wenn man das bei echten Datenbanken aus Sicherheitsgründen nicht tun sollte.

7. Aufwändigere GUI-Applikation (Updates)

Schreiben Sie eine Java-GUI-Applikation, bei der der Benutzer zunächst mit 2 Textfeldern seinen Username und sein Passwort eingibt, um mit der Datenbank verbunden zu werden, und dann in einem neuen Fenster mit 4 Textfeldern alle Daten für einen zusätzlichen Mitarbeiter eingeben und mit einem Store-Button in der Datenbank speichern kann. Jedes dieser Fenster soll auch einen Exit-Button haben, mit dem man die Applikation beenden kann.

Fügen Sie mit dieser Applikation ein paar (mindestens 2, höchstens 5) weitere Mitarbeiter in die Datenbank ein und führen Sie dann die Übungsprogramme 4, 5 und 6 neuerlich aus, um zu testen, ob alle Mitarbeiter richtig gespeichert wurden.

Wenn Sie sehr viel mehr Zeit investieren wollen, können Sie auch überlegen, wie ein graphisches User-Interface aufgebaut sein müsste, um damit nicht nur die Speicherung von neuen Records sondern auch Abfragen von gespeicherten Records, Datenänderungen an einzelnen Records und Löschungen von einzelnen Records durchführen zu können.

8. Löschen

Schreiben Sie eine einfache Java-Applikation, die alle Mitarbeiter-Records aus der Datenbank löscht.

Führen Sie dieses Programm aus und testen Sie dann mit Programm 1 oder 4, ob tatsächlich keine Mitarbeiter mehr gespeichert sind.


Copyright Hubert Partl - nächstes Kapitel - Inhaltsverzeichnis