Kapitel 11 Package java.io
Hinter dem Begriff »io« verbirgt sich nicht nur die Ein- bzw. Ausgabe (I/O) in Dateien.
I/O umfasst vielmehr die gesamte Kommunikation über Kanäle wie Netzwerke, Speicher(bausteine) bzw. Pipes mit anderen Prozessen, Threads oder Geräten wie Bildschirm, Drucker und Tastatur.
Das zugehörige Klassensystem ist sehr umfangreich, sodass selbst Teilbereiche wie die Netzwerkprogrammierung eigene Buchthemen bilden.
Diese Einführung setzt deshalb wieder den Schwerpunkt auf das Konzeptionelle und die verwendeten Design-Muster.
11.1 Überblick
Betrachtet man die I/O-Klassen-Hierarchie, so ist sie grob in die Bereiche Streams, unterstützende Interfaces und einzelne Klassen mit speziellen Diensten eingeteilt.
Stream: unidirektionale Übertragung von Daten
Die weitaus meisten I/O-Klassen sind Streams.
|
Ein Stream besteht konzeptionell aus einer Quelle (source), die Bytes oder Zeichen an ein Ziel (destination) überträgt. Damit ist die Übertragung unidirektional. |
Quelle/Source,
Ziel/Destination: Ressourcen
Der Begriff »Quelle« bzw. »Ziel« wird dabei sehr weit gefasst, ist also nicht nur eine Datei auf einem externen Speicher. Hierunter versteht man alle möglichen Ressourcen wie Speicherpuffer, Netzwerkverbindungen, Tastatur, Bildschirm oder Drucker.
Stream-Sicht:
Übertragungsrichtung & Datenart
Streams können in zweierlei Hinsicht betrachtet werden.
|
Übertragungsrichtung: Ein Eingabe-Stream, der empfängt, ein Ausgabe-Stream, der sendet. |
|
Datenart: Die übertragenen Daten sind Bytes oder reine Zeichen. |
Damit gliedert sich das Stream-Subsystem in zwei Bereiche mit jeweils zwei Unterbereichen, die auf abstrakten Basisklassen beruhen (siehe auch Abb. 11.1):
|
InputStream, OutputStream (byteorientiert) |
|
Reader, Writer (zeichenorientiert) |
Reader/Writer: zeichenbasierte Streams
Das zeichenorientierte Subsystem mit Reader und Writer wurde nachträglich in Java 1.1 hinzugefügt und stellt komplementär zur Input- bzw. Output-Hierarchie Klassen und Methoden bereit.
Dies hielt man für notwendig, da ein rein byteorientierter Stream weder einfach noch effizient Unicode handhaben kann (siehe hierzu die Unterabschnitte zu 10.6).
Konzeptionelle Unterteilung von java.io
Abbildung 11.1 Konzeptionelle Unterteilung des Packages java.io
Der Rest der Klassen in java.io erfüllt dann spezielle Aufgaben:
File: Verzeichnis- oder Dateiname
|
File repräsentiert – unabhängig vom Betriebssystem – einen Datei- oder Verzeichnisnamen mit den auf Dateien und Verzeichnissen üblichen Operationen. |
FileDescriptor: Handle
|
FileDescriptor stellt ein Handle – ein Ident vom Typ int – zu einer bereits geöffneten Datei dar und lässt keine Anlage von Instanzen zu (siehe auch FileDescriptor-Einsatz). |
RandomAccessFile: wahlfreier Dateizugriff
|
RandomAccesFile ist eine von den Streams isolierte Klasse, die auf eine rudimentäre Weise Bytes, primitive Typen und Zeichen an beliebigen Stellen in einer Datei lesen und schreiben kann. |
StreamTokenizer
zerlegt in Token
|
StreamTokenizer ist eine recht nützliche Klasse für die lexikalische Analyse von Zeichen-Streams, um sie in Token – logischen Einheiten wie z.B. Wörter und Zahlen – zu zerlegen. |
ObjectStreamClass/Field für Serialisierungs-Infos
|
ObjectStreamClass bzw. ObjectStreamField enthalten im Zusammenhang mit der Serialisierung Informationen zur Klasse bzw. den Feldern (und werden eher selten benutzt). |
Zugriffserlaubnis
|
FilePermission bzw. SerializablePermission regeln die Erlaubnis für Dateizugriffe bzw. Serialisierung (wird wenig benötigt!). |
Interfaces
Serializable, Externalizable
Das Marker-Interface Serializable bzw. sein Subinterface Externalizable spielen ein wichtige Rolle bei der Serialisierung, der Umwandlung von Objekten in einen Byte-Stream.
Die Serialisierung baut auf den Basisdiensten der Streams auf und wird durch viele eingebaute Mechanismen in der JVM unterstützt. Sie wird eingehend in Kapitel 12, Serialisierung, behandelt.
Interfaces spielen – ausgenommen bei Serialisierung – in der Klassen-Hierarchie von java.io eine eher bescheidene Rolle, da zum Zeitpunkt der Entstehung ihre Fähigkeiten wohl unterschätzt wurden.1
11.2 File
File: immutable
Die Klasse File ist immutable und kapselt den Datei- oder Verzeichnisnamen, um den Code für Datei- und Verzeichniszugriffe so weit wie möglich vom jeweiligen Betriebssystem zu entkoppeln.
Abstraktion von Datei- und Verzeichnisnamen
Damit beschränkt sich die Abhängigkeit vom Betriebssystem nur auf die File-Konstruktoren, da diesem die Strings für Datei- bzw. Verzeichnisnamen des jeweiligen Betriebssystems übergeben werden.
Die Anlage einer File-Instanz ist völlig unabhängig von der Anlage oder Existenz einer realen Datei bzw. eines Verzeichnisses, es stellt nur ein mögliches reales Objekt dar.
Bei der Angabe eines Datei- oder Verzeichnisnamens unterscheidet man zwischen einem absoluten und einem relativen Pfad.
Absoluter Pfad
|
Bei einem absoluten Pfad sind keine weiteren Angaben notwendig, um die Datei oder das Unterverzeichnis zu identifizieren. |
Unter Unix beginnt ein absoluter Pfad immer mit "/", unter Windows mit "laufwerk:\" oder "\\", gefolgt vom Hostnamen.
Relativer Pfad
|
Ein relativer Pfad identifiziert eine Datei oder ein Verzeichnis nur durch Anhängen an einen aktuellen (Benutzer-)Pfad.2 |
Der aktuelle Pfad ist unter dem Schlüssel "user.dir" in den System-Properties abgelegt (siehe 10.6.6).
File-/Path-
Separator
|
Das Separations-Zeichen innerhalb eines Pfads (File-Separator) oder zwischen verschiedenen Pfaden (Path-Separator) ist als Konstante (final static) abgelegt: |
char separatorChar bzw. String
separator
char pathSeparatorChar bzw. String
pathSeparator
Da das Separations-Zeichen gerade bei Windows und Unix abweicht, gibt es eine einheitliche Angabe eines Pfads in Form der Unix-Variante auch für Windows-Anwendungen, die dann intern transformiert wird:
Einheitliche
Pfadangabe
Konstruktoren
Es gibt drei Konstruktoren.
File-Konstruktoren
Dabei ist pathname ein Datei- oder Verzeichnisname, der absolut oder relativ zu dem aktuellen Verzeichnis angegeben werden kann.
public File(String parentPath, String child)
Dabei ist child ein relativer Pfad bezüglich parentPath (durch Anhängen an parentPath) oder auch ein Dateiname.
public File(File directoryObj, String child)
Diese Form erzeugt eine neue File-Instanz auf Basis einer existierenden Instanz, welche ein Verzeichnis repräsentieren muss, und child.
Aktuelles Verzeichnis
Das aktuelle Verzeichnis wird durch das File-Objekt new File(".") repräsentiert.
Methoden
Entsprechend den umfangreichen Datei- und Verzeichnis-Operationen gibt es zurzeit 40 Methoden. Davon wurden 25 häufiger benötigte Methoden – gruppiert nach Funktionalität – in der Tabelle 11.1 aufgelistet. Folgende Anmerkungen sind hilfreich.
Unterscheidung von File-Methoden
|
nicht orthogonal: Nicht zu jeder Zugriffs-Methode in der File-Klasse existiert eine korrespondierende Modifikations-Methode. |
|
absoluter Pfad: Viele Methoden liefern nur dann korrekte Ergebnisse, wenn die File-Instanz eindeutig ist, d.h. auf einem absoluten Pfad beruht (mit [A] gekennzeichnet).3 |
|
Datei vs. Verzeichnis: Manche Methoden sind nur für Dateien [D] sinnvoll, andere nur für Verzeichnisse [V]. |
Selbsterklärende Methoden werden nicht kommentiert.
File-Methoden:
Information Navigation Modifikation
Art |
Methode |
Anmerkung |
Info |
boolean exists() |
[A] Datei/Verzeichnis existiert |
boolean isDirectory() |
[A] |
boolean isFile() |
[A] |
boolean isHidden() |
[A] |
boolean isAbsolute() |
Pfadname ist absolut |
boolean canRead() |
[A] |
boolean canWrite() |
[A] |
long lastModified() |
[A] in ms seit dem 01.01.1970 |
long length() |
[A,D] Länge in Byte |
Nav. |
String getName() |
nur Dateiname oder letztes Unterverzeichnis im Pfad |
String getPath() |
der gesamte (relative) Pfad |
String getAbsolutePath() |
der absolute Pfad |
File getAbsoluteFile() |
eine absolute File-Instanz |
String getParent() |
Dateipfad oder Oberverzeichnis |
String[] list() |
[A,V] Alle File-Objekte im Verzeichnis als String |
File[] listFiles() |
[A,V] Alle File-Objekte im Verzeichnis |
File[] listRoots() |
statisch, Root-Verzeichnis(se) |
Mod. |
boolean createNewFile() |
[D] legt leere Datei an, sofern nicht existent, atomare Operation |
renameTo() |
|
deleteOnExit() |
Löscht File-Objekt bei Terminierung der Applikation |
boolean setReadOnly() |
[A] |
boolean setLastModified() |
[A] |
boolean mkdir() |
[V] legt Verzeichnis an, sofern Oberverzeichnisse existieren |
boolean mkdirs() |
[V] legt Verzeichnis und notfalls Oberverzeichnisse an |
File createTempFile() |
statisch, temporäre Datei |
Beispiele
Das nachfolgende Code-Fragment liefert die Root-Verzeichnisse für WinNT (für Unix würde es nur "/" liefern):
listRoots(),
getPath()
File[] rf= File.listRoots();
for (int i=0; i< rf.length; i++)
System.out.print(rf[i].getPath()
+ File.pathSeparator); // :: A:\;C:\;D:\;
Die in Abb. 11.2 dargestellte Verzeichnisstruktur wird im folgenden Beispiel verwendet.
Abbildung 11.2 Verzeichnisstruktur zum Beispiel
Properties.
setProperty()
// aktuelles Verzeichnis kann nicht in File geändert
werden
System.getProperties().setProperty("user.dir","C:/Winnt");
getAbsolutePath()
// relative Pfadangabe
File fd= new File("Hp");
System.out.println(fd.getAbsolutePath()); // :: C:\Winnt\Hp
exists()
// liefert bei relativer Pfadangabe false
System.out.println(fd.exists()); // :: false
getAbsoluteFile()
// Umwandlung in eine absolute File-Instanz
fd= fd.getAbsoluteFile();
System.out.println(fd.exists()); // :: true
listFiles()
File[] fdarr= fd.listFiles();
for (int i=0; i<fdarr.length; i++)
System.out.println( // :: 1187gent.exe 3318 KB
getName(), isDirectory(), length()
fdarr[i].getName()+ // :: 1187GENT [Dir]
(fdarr[i].isDirectory()?" [Dir]":
" "+fdarr[i].length()/1024 +" KB"));
isAbsolute()
System.out.println(new File("C:/").isAbsolute()); // :: true
System.out.println(new File("C:").isAbsolute()); // :: false
Im nächsten Beispiel sollen Verzeichnisse und Dateien angelegt werden:
mkdir()
System.out.println(new File("C:/BC").mkdir()); // :: true
mkdirs()
// Voraussetzung: Es gibt kein Oberverzeichnis C:\AB
System.out.println(new File("C:/AB/AB").mkdir()); // :: false
System.out.println(new File("C:/AB/AB").mkdirs()); // :: true
deleteOnExit()
File ff= new File("c:/AB/AB/AB");
ff.deleteOnExit(); // JVM löscht vor Terminierung
try {
createNewFile()
System.out.println(ff.createNewFile()); // :: true
} catch (IOException e) { System.out.println(e); }
System.out.println(ff.exists()); // :: true
Erklärung: Die Methode mkdir() legt ein neues (nicht vorhandenes) Verzeichnis nur an, wenn das Oberverzeichnis bereits existiert, mkdirs() erzeugt notfalls zusätzlich alle Oberverzeichnisse.
Eine Datei kann mit deleteOnExit() als zu löschen gekennzeichnet werden, wenn die Applikation terminiert.4
11.3 FileDescriptor
FileDescriptor: Handle
Der Konstruktor von FileDescriptor ist private. Eine FileDescriptor-Instanz wird indirekt durch FileInputStream, FileOutputStream oder RandomAccessFile angelegt. Man kann sich dann zu der bereits geöffneten Datei mit Hilfe von getFD() die Instanz geben lassen.
valid(): prüft auf
gültiges Handle
Es gibt nur zwei Instanz-Methoden. Die eine heißt valid() und ist selbsterklärend.
sync(): Anweisung an das OS zum Speichern
Die andere heißt sync() und veranlasst das Betriebssystem, seine mit dem I/O verbundenen Puffer zu leeren. Dies stellt also eine Art flush() auf der Ebene des Betriebssystems dar und setzt voraus, dass vorher ein flush() auf die Puffer der Java-I/O-Instanzen gemacht wurde.
Die Klasse ist eigentlich uninteressant, wäre(n) da nicht
Umlenkung von in, out, err
|
die drei final static FileDescriptor-Instanzen für die Standard-Ein/Ausgaben in, out bzw. err, mit denen man die Standard-I/O auf andere Streams umlenken kann. |
Gemeinsamer Zugriff auf eine Datei
|
die Möglichkeit, den FileDescritor für mehrere Streams zu nutzen, um gemeinsam auf eine aktuelle Dateiposition zuzugreifen (FileDescriptor-Einsatz). |
11.4 Interfaces DataInput und DataOutput
Interfaces spielen in java.io keine tragende Rolle, wie man an den beiden nachfolgenden erkennen kann.
Die Interfaces DataInput bzw. DataOutput definieren gemeinsame Methoden der namensverwandten Streams DataInputStream und DataOutputStream und der Klasse RandomAccessFile.
|
Die beiden Interfaces sollten symmetrisch sein.5 |
Sie sind es jedoch »not quite«, wie man an der Gegenüberstellung der Methoden in der Tabellen 11.2 sehen kann.6 Die fehlenden Methoden werden dann sinnigerweise in den Klassen deklariert.7
Methoden zu
DataInput, DataOutput
DataInput |
DataOutput7 |
Anmerkung |
boolean readBoolean() |
writeBoolean(boolean b) |
boolean |
byte readByte() |
writeByte(int i) |
byte |
short readShort() |
writeShort(int i) |
short |
char readChar() |
writeChar(int i) |
char |
int readInt() |
writeInt(int i) |
int |
long readLong() |
writeLong(long l) |
long |
float readFloat() |
writeFloat(float f) |
float |
double readDouble() |
writeDouble(double d) |
double |
|
write(int i) |
byte |
int readUnsignedByte() |
|
0..255 |
int readUnsignedShort() |
|
0..65535 |
String readUTF() |
writeUTF(String s) |
UTF-codiert |
|
writeBytes(String s) |
low byte
(ISO Latin 1) |
void readFully(byte[] b) |
write(byte[] b) |
byte-Array |
void readFully(byte[] b,
int off, int len) |
write(byte[] b,
int off, int len) |
byte-Array |
String readLine() |
writeChars(String s) |
String |
int skipBytes(int n) |
|
überspringen von n Bytes |
DataInput,
DataOutput: Lesen und Schreiben von primitiven Typen und Strings
|
Neben der einfachen Byte-I/O ist das Lesen und Schreiben von primitiven Typen und Strings Hauptzweck der beiden Interfaces. |
Die Ein- bzw. Ausgabe von primitiven Typen, unabhängig von der Maschine oder dem jeweiligen Programmierer, stellt bereits eine höhere Kommunikationsebene als der reine Austausch von Bytes dar.8
Die drei Klassen in Abb. 11.3, die DataInput bzw. DataOutput implementieren, zählen somit zu den High-Level-Klassen (siehe Low-Level- vs. High-Level-Streams).
Abbildung 11.3 Die Klassen zu DataInput bzw. DataOutput
IOException
Ein- und Ausgaben können grundsätzlich mit Fehlern behaftet sein, die außerhalb der Reichweite des Programmierers liegen.
IOException-
Hierarchie
Somit gibt es eine aufwändige, dreistufige I/O-Ausnahme-Hierarchie von 16 Ausnahme-Klassen in java.io, deren Basisklasse IOException ist (eine checked-Exception, siehe 7.4.2).
Abbildung 11.4 Ausschnitt aus der Ausnahme-Hierarchie in java.io
|
Alle Methoden der Interfaces können eine IOException auslösen. |
11.5 RandomAccessFile
Die Klasse RandomAccessFile ist in der Java-Plattform die einzige Möglichkeit, gleichzeitig Datei-Eingabe und -Ausgabe mit wahlfreiem Zugriff auf beliebige Positionen durchzuführen.
RandomAccessFile: Lesen und Schreiben in Dateien
RandomAccessFile hat aufgrund seiner Fähigkeiten eine Sonderstellung, d.h. steht isoliert außerhalb der Stream-Klassen-Hierarchien.
Diese Isolation begründet sich im einfachen Vererbungskonzept von Java. Damit kann RandomAccessFile nicht gleichzeitig von einem der Input- und Output-Streams abgeleitet werden.9
Konstruktoren
Es gibt zwei Konstruktoren, die entweder mit Hilfe eines Dateinamens oder einer File-Instanz ein Objekt von RandomAccessFile erzeugen.
public RandomAccessFile(String name, String mode)
public RandomAccessFile(File file, String mode)
Anstatt eines boolean-Typs (oder einer Konstanten) wird der Mode, in dem man eine Datei öffnen möchte, als String übergeben. "r" steht für »nur Lesen erlaubt« und "rw" steht dann für »Lesen und Schreiben erlaubt«. Alles andere löst eine IllegalArgumentException aus.
Methoden
RandomAccessFile: DataInput & DataOutput & ...
Die Methoden lassen sich in drei Gruppen einteilen, jeweils eine, die DataInput bzw. DataOutput implementieren (siehe Tabelle 11.2), und die zusätzlichen (siehe Tabelle 11.3).
Die readFully()-Methoden von DataInput werden eher selten benötigt, da sie ein ganzes Array füllen müssen oder ansonsten eine IOException auslösen.
Ergo gibt es in RandomAccessFile noch drei normale read()-Methoden, die nur bis zum Dateiende (EOF) lesen und die Anzahl der gelesenen Bytes zurückliefern.10
Die restlichen Methoden lesen oder setzen die Länge, die Dateiposition oder schließen die Datei. Alle Längen- und Positionsangaben erfolgen in Byte.
Methode |
Anmerkung |
int read() |
bei EOF: Rückgabe –1 |
int read(byte b[]) |
bei EOF: Rückgabe –1 |
int read(byte b[],int off,int len) |
bei EOF: Rückgabe –1 |
long getFilePointer() |
aktuelle Dateiposition |
long length() |
Dateilänge in Byte |
void setLength(long newLength) |
setzt Dateilänge
entweder Datenverlust/
undefinierter Inhalt) |
void seek(long pos) |
setzt aktuelle Dateiposition |
void close() |
schließt Datei |
FileDescriptor getFD() |
zugehöriger FileDescriptor |
Es gibt nur eine Positionierung mit seek(), die den FilePointer – die aktuelle Dateiposition – relativ zum Anfang der Datei setzt.
File-Pointer:
aktuelle Dateiposition
|
Jede Lese- oder Schreib-Operation beginnt an der aktuellen Dateiposition und bewegt den File-Pointer um die Anzahl der gelesen oder geschriebenen Bytes. |
Beispiele
Erweiterung von RandomAccessFile
Die Klasse ERandomAccessFile erweitert RandomAccessFile um Positionierung relativ zum FilePointer oder zum Dateiende:
class ERandomAccessFile extends RandomAccessFile
{
public ERandomAccessFile(String name, boolean
readOnly)
throws IOException { super(name,readOnly?"r":"rw"); }
public ERandomAccessFile(File file, boolean readOnly)
throws IOException { super(file,readOnly?"r":"rw"); }
public void seekFilePointer(long pos) throws IOException
{
seek(getFilePointer()+pos); // kein Check!
}
public void seekEnd(long pos)throws IOException {
seek(length()-pos); // kein Check!
}
}
Lesen und Schreiben in eine temporäre Datei
Die Klasse Test legt eine temporäre Datei "Tst*.tmp" im Temp-Unterverzeichnis an (mit Property-Schlüssel "java.io.tmpdir" abzufragen) und demonstriert Lesen und Schreiben von primitiven Typen (mit und ohne Fehler) und einem UTF-String:
public class Test {
public static void main(String[] args) {
System.out.println(
System.getProperties().getProperty("java.io.tmpdir"));
try {
// legt Tst#####.tmp an, wird bei Terminierung gelöscht
File fd= File.createTempFile("Tst",null);
fd.deleteOnExit();
ERandomAccessFile raf= new ERandomAccessFile(fd,false);
Lesen und Schreiben von UTF8-Zeichen
raf.writeBoolean(true); // 1 Byte: 0=false
1=true
raf.writeDouble(1.0); // 8 Bytes
raf.writeLong(1); // 8 Bytes
raf.writeBytes("abc\n"); // 4 Bytes
raf.writeChars("ab"); // 4 Bytes: Unicode
// zuerst Länge, dann UTF-Zeichen laut Tabelle 10.1
raf.writeUTF("aä"); // 5 Bytes
System.out.println(raf.length()); // :: 30
raf.seekEnd(13); // positioniert auf
abc\n
System.out.println(raf.readLine()); // :: abc
System.out.print((char)raf.readChar()); // :: a
System.out.println((char)raf.readChar()); // :: b
System.out.println(raf.readUTF()); // :: aä
// wenn MS-DOS-Konsole (siehe 10.6.1), dann ::
aõ
raf.seek(0);
System.out.println(raf.readByte()); // :: 1
raf.seekFilePointer(8);
System.out.println(raf.readLong()); // :: 1
Keine Typsicherheit beim Lesen
raf.seekFilePointer(-8);
// eine long wird "aus Versehen" als double gelesen
System.out.println(raf.readDouble()); // :: 4.9E-324
raf.close();
} catch (IOException e) { System.out.println(e); }
}
}
11.6 Stream-Konzept
Stream: Ende eines Kommunikationskanals
Der Begriff Stream ist eine Abstraktion und bedeutet das Ende eines gerichteten Kommunikationskanals, wobei das eine Ende als Input-Stream und das andere als Output-Stream bezeichnet wird.
Abbildung 11.5 Abstraktion der Stream-Kommunikation
Stream-Eigenschaften:
One Way FIFO synchrones I/O
Alle Java-Streams haben folgende Eigenschaften gemeinsam:
|
Sie können nur lesen oder schreiben. |
|
Sie lesen oder schreiben sequenziell nach dem FIFO-Prinzip11 . |
|
Lese- und Schreib-Operationen sind in der Regel blockierend, d.h., der Thread muss warten (Ausnahme siehe z.B. Pipe-Streams). |
Der letzte Punkt ist nicht unbedingt angenehm, da es somit keine direkte Stream-Unterstützung von Lese- oder Schreib-Operationen gibt, die bei einem nicht bereiten Kommunikationskanal sofort abbrechen.12
Marshaling und Unmarshaling
Solange die Kommunikation ausschließlich auf Byte-Ebene stattfindet, kann jedes Byte als isolierte Einheit interpretiert werden. Jedoch bestehen die meisten Daten aus einem Verbund von Bytes. Das einzelne Byte macht keinen Sinn, erst die Interpretation des Ganzen. Sender und Empfänger müssen natürlich alles gleich interpretieren.
Marshaling, Unmarshaling:
Ver- und Entpacken von Daten
Deshalb verlangen bereits primitive Typen eine einheitliche Konvention dafür, in welcher Reihenfolge ihre Bytes im Stream angeordnet werden.
|
Marshaling13 bezeichnet beim Austausch von Daten die Transformation der Daten-Objekte in einen Block von Bytes. |
|
Unmarshaling bezeichnet den umgekehrten Prozess der Wiederherstellung der Daten aus dem Block von Bytes. |
Network-Byte-Order, Big-Endian-Format
|
Marshaling von primitiven Typen geschieht in Java im Network-Byte-Order-Format, auch als Big-Endian-Format bezeichnet. |
Nach dieser Konvention, die sprachübergreifend auch für C/C++ gilt, werden die höherwertigen Bytes immer zuerst geschrieben.
Somit hält sich auch die Klasse RandomAccessFile an diese Konvention:
// Auszug aus SUN-Original-Code in RandomAccessFile
public final void writeInt(int i) throws IOException {
write((i >>> 24) & 0xFF); write((i >>> 16) &
0xFF);
write((i >>> 8) & 0xFF); write((i >>> 0) &
0xFF);
}
Network-Byte-Order beseitigt nicht etwa die Kommunikationsprobleme, sondern verlagert sie nur.
Problem Metadaten:
Daten über Daten
|
Marshaling, das nur auf Network-Byte-Order beruht, übermittelt keine Metadaten, d.h. Informationen zum Typ der ausgetauschten Daten. |
Das einfache Beispiel zu RandomAccessFile in Methoden zeigt recht deutlich das Problem. Es wurde ein Wert vom Typ long geschrieben, aber als double-Wert gelesen.
|
Beim Marshaling liegen entweder beim Sender und Empfänger die Metadaten bereits vor, oder sie müssen in den Daten-Objekten selbst enthalten sein. |
Low-Level- vs. High-Level-Streams
Den Austausch von Daten kann man anhand der Diskussion im letzten Abschnitt grob in zwei Ebenen unterteilen.
Low-Level:
Byte- bzw. Device-Ebene
|
Low-Level: Kommunikation auf Byte-Ebene direkt mit dem (physikalischen) I/O-Device (Gerät). |
High-Level:
Typ-Ebene
|
High-Level: Kommunikation, basierend auf primitiven Typen oder Objekten-Typen, ohne direkten Zugriff auf das I/O-Device. |
Dieser Klassifizierung folgt auch das Stream-System des Packages java.io bei den byteorientierten Streams.
|
Die Basisklassen OutputStream bzw. InputStream bilden nur abstrakte Ein- bzw. Ausgänge, ohne das I/O-Device festzulegen, in das geschrieben bzw. von dem gelesen wird (siehe Abb. 11.5). |
|
Die konkreten Subklassen werden in Low- oder High-Level-Streams unterteilt, wobei High-Level-Streams in der Regel auf der Basis von Low-Level-Streams operieren (siehe Abb. 11.6). |
Abbildung 11.6 Service-Relation zwischen Stream-Ebenen am Beispiel
Low-Level-Byte-Streams
Low-Level
Kommunikationsarten
Die Low-Level-Streams unterscheiden sich durch die Art des Kommunikationskanals bzw. I/O-Devices:
|
Datei (FileOutputStream, FileInputStream) |
|
Byte-Array (ByteArrayOutputStream, ByteArrayInputStream) |
|
Pipe, d.h. ein asynchroner Kommunikationskanal zwischen Threads (PipedOutputStream, PipedInputStream) |
|
Netzwerk (java.net.Socket, OutputStream, InputStream) |
Die Netzwerk-Kommunikation ist in einem gesonderten Package java.net ausgelagert. Die internen Socket-Instanzen werden nach außen über Referenzen der Basisklassen angesprochen.
High-Level-Byte-Filter-Streams
High-Level:
Filter-Streams, Object-Streams
High-Level-Streams arbeiten auf Basis der Low-Level-Streams (siehe Abb. 11.6) und zählen entweder zur Gruppe der Filter-Streams oder zu den Object-Streams.
|
Filter-Streams bieten immer eine zusätzliche Funktionalität an. Man unterscheidet: |
|
|
BufferedOutputStream bzw. BufferedInputStream, die als zusätzliche Puffer auf Low-Level-Streams aufgesetzt werden. |
|
|
Filter-Streams, die entweder auf Buffered-Streams oder Low-Level-Streams aufsetzen. |
|
ObjectOutputStream bzw. ObjectInputStream schreiben bzw. lesen einzelne oder zusammengehörige Objekte mit Hilfe eines anderen Byte-Streams. |
Damit wäre das Konzept zumindest für die Byte-Streams umrissen.
Im Diagramm der Abb. 11.7 wurde zur Übersichtlichkeit die Klasse ObjectStreamConstants weggelassen, die nur Konstanten vereinbart.
Byte-Stream-
Hierarchie: Low- vs. High-Level
Abbildung 11.7 Byte-Stream-Hierarchie
Gruppe der
Filter-Streams
FilterInputStream sowie FilterOutputStream sollen extern wie abstrakte Klassen wirken, da nur ihre Subklassen eine zusätzliche Funktionalität implementieren. Deshalb wurden ihre Konstruktoren protected deklariert.
Zu den Filter-Streams zählt logisch gesehen auch der SequenzInputStream. Er kann nur aufgrund seiner Funktionalität nicht von FileInputStream abgeleitet werden.
Der SequenzInputStream erlaubt die sequenzielle Verarbeitung mehrerer Input-Streams, so als seien sie ein einziger großer Stream. Dies macht eine direkte Ableitung von InputStream sinnvoller als eine von FilterInputStream.
Reader- und Writer-Hierarchie
Die Hierarchie der Unicode-Streams übernimmt das Klassifizierungs-Muster von Byte-Streams, angepasst an die Übertragung von Zeichen. Man unterscheidet wieder die Reader- bzw. Writer-Klassen nach
Unterscheidung von Low- und High-Level
|
Low-Level: Anbindung an Dateien, speicherbasierende Zeichen-Arrays oder Pipes |
|
High-Level: Arbeiten auf Basis von Low-Level-Zeichen-Streams |
Die Ein- bzw. Ausgabe in Dateien erfolgt bei Low-Level-Zeichen-Streams auf Basis von entsprechenden Byte-Streams, d.h., die eigentliche Übertragung wird delegiert.
Zeichen-Stream-Hierarchie:
Low- vs. High-Level
Abbildung 11.8 Zeichen-Stream-Hierarchie
Zusätzlich:
StringReader/-Writer
Wie aus Abb. 11.8 zu erkennen, ist die Hierarchie nur analog, aber nicht identisch. Es gibt keine Objekt-Klassen, dafür aber z.B. die spezialisierten Klassen StringReader bzw. StringWriter.
Die folgende Tabelle 11.4 ist eine Gegenüberstellung der Byte- und Zeichen-Streams, die äquivalente Aufgaben erledigen.
Gegenüberstellung: Byte- und Zeichen-Streams
Art |
Byte-Stream |
Zeichen-Stream |
Merkmal |
|
InputStream |
Reader |
abstrakt |
Low |
FileInputStream |
FileReader |
Datei |
Low |
ByteArrayInputStream |
CharArrayReader |
Array |
Low |
PipedInputStream |
PipedReader |
Thread-Kanal |
Low |
|
StringReader |
String |
High |
|
InputStreamReader |
Datei+Codierung |
High |
FilterInputStream |
FilterReader |
abstrakt |
High |
BufferedInputStream |
BufferedReader |
Daten-Puffer |
High |
PushbackInputStream |
PushbackReader |
ungelesen zurück |
High |
|
LineNumberReader |
Zeilen zählen |
High |
DataInputStream |
|
primitive Typen |
High |
SequenceInputStream |
|
mehrere Streams |
High |
ObjectInputStream |
|
Objekte |
|
OutputStream |
Writer |
abstrakt |
Low |
FileOutputStream |
FileWriter |
Datei |
Low |
ByteArrayOutputStream |
CharArrayWriter |
Array |
Low |
PipedOutputStream |
PipedWriter |
Thread-Kanal |
Low |
|
StringWriter |
String |
High |
|
OutputStreamWriter |
Datei+Codierung |
High |
FilterOutputStream |
FilterWriter |
abstrakt |
High |
BufferedOutputStream |
BufferedWriter |
Daten-Puffer |
High |
PrintStream |
PrintWriter |
print(), println() |
High |
DataOutputStream |
|
primitive Typen |
High |
ObjectOutputStream |
|
Objekte |
PrinterWriter ersetzt PrintStream
(Ausnahme System.out)
Freie Tabelleneinträge zeigen die Spezialisierung beider Hierarchien.
|
Im Fall der Klassen PrintStream und PrintWriter sind die Aufgaben nicht äquivalent, sondern identisch. Mit Ausnahme von System.out ist immer PrintWriter zu verwenden. |
11.7 Decorator-Pattern
Decorator-Pattern
Die Umsetzung des Stream-Konzepts baut auf ein bekanntes Decorator-Pattern auf, das eine Alternative zur Vererbung anbietet, sofern diese zu einer »kombinatorischen Explosion« führt.
Die Vererbungslehre und die kombinatorische Explosion
Denn neue Eigenschaften (bzw. neues Verhalten) erzwingen in der »reinen« OO-Lehre eine neue Subklasse. Dies wird aber zum Problem, wenn mehrere Eigenschaften auftauchen, die in verschiedenen Kombinationen Sinn machen. Dann ist man gezwungen, für jede sinnvolle Eigenschaftskombination eine Subklasse anzulegen.
Dekorations-Konzept
Hier hilft das Dekorations-Konzept:
|
Jede Klasse enthält eine Eigenschaft. |
|
Um eine Klasse mit einer gewünschten Eigenschaftskombination zu erhalten, umhüllt man eine (Basis-)Klasse mit einer grundlegenden Eigenschaft so lange mit anderen, bis sich die gewünschte Kombination von Eigenschaften ergibt. |
Die Umsetzung in Java läuft am einfachsten über Konstruktoren, die zumindest eine gemeinsame abstrakte Basisklasse oder – besser noch – ein Interface als Parameter haben.
Decorator-Pattern am Beispiel Byte-Stream
Abbildung 11.9 Decorator-Pattern am Beispiel Byte-Stream
Im Fall von Kommunikation fängt man mit einem Low-Level-Stream wie z.B. FileInputStream an, dekoriert diesen zuerst mit einem »höheren« BufferedInputStream und diesen wiederum mit einem DataInputStream (Abb. 11.9).
Effekt: Das normale Lesen von Bytes wird also um »effiziente Pufferung der Daten« und »Filtern von primitiven Typen« erweitert.
Klassen-Diagramme zum Decorator-Pattern variieren je nach Anforderung, basieren aber – wenn möglich – auf einem Interface-Design.
Die Klassen mit Basiseigenschaften – im Fall I/O die Low-Level-Byte-Klassen – erkennt man daran, dass sie keine anderen Klassen dekorieren (können). Sie sind der Startpunkt. Die anderen dekorieren zumindest einen anderen Service (Abb. 11.10).
Decorator-Struktur auf Basis eines Interfaces
Abbildung 11.10 Klassen-Diagramm zum Decorator-Pattern
Der Nachteil gegenüber einen reinen Vererbungslösung ist offensichtlich:
Nachteil des Dekorations-Konzepts
|
Die Zusammenstellung bzw. Reihenfolge der Dekorierung ist nicht festgelegt und kann zu unerlaubten oder unsinnigen Kombinationen führen. |
Bei vielen Eigenschaften überwiegt aber der Vorteil, wobei sich der Nachteil eventuell mit Ausnahmen bekämpfen lässt.
Decorator vs. Delegation
Dekoration, eine
Variante der Delegation
Das Decorator-Pattern ist an sich eine spezielle Variante des Delegations-Patterns (siehe 6.7).
Dekoratoren fügen besondere Eigenschaften und Methoden zu einem allgemeinen Service hinzu und delegieren den Rest an die nächste Klasse. Diese kann rekursiv wieder ein Dekorator sein oder eine terminierende Basisklasse.
Der Benutzer stellt in seiner Anwendung den Service zusammen.
11.8 Streams im Einsatz
In den Abschnitten 11.6 und 11.7 wurden das Stream-Konzept, ein Überblick über die Klassen und das zugehörige Pattern zur Umsetzung vorgestellt. Details zu den einzelnen Klassen findet man in der Referenz oder – besser noch – in den kommentierten Quellen des java.io-Packages.
Aufbauend auf den Konzepten folgen zuerst fünf kleinere Beispiele und anschließend digitales Signieren bzw. Pipes.
FileDescriptor-Einsatz
FileDescriptor: Handle einer geöffneten Datei
Der FileDescriptor ist ein Handle zu einer offenen Datei. Wird dieses Handle bzw. dieser FileDescriptor zur Anlage eines anderen File-Streams verwendet, bedeutet dies
Verwendung des Handles
|
die Anbindung eines weiteren Streams an dieselbe Datei. |
|
die gemeinsame Verwendung einer Datei-Position. |
Ein Handle für FileInputStream, RandomAccessFile
Dieser Effekt wird zur Markierung einer Position in einem FileInputStream genutzt:
class MarkedFileInputStream extends FileInputStream
{
protected RandomAccessFile raf;
private long mark= 0L;
protected MarkedFileInputStream (RandomAccessFile
raf)
throws IOException {
super(raf.getFD()); this.raf= raf;
}
public MarkedFileInputStream (String s) throws IOException {
this(new RandomAccessFile(s,"r"));
}
public MarkedFileInputStream(File file) throws IOException {
this(new RandomAccessFile(file,"r"));
}
public boolean markSupported() { return true;
}
public synchronized void mark(int readlimit) {
try { this.mark= raf.getFilePointer();
}
catch (IOException e) {}
}
public synchronized void reset() throws IOException {
raf.seek(mark);
}
}
Erklärung: Die Klasse MarkedFileInputStream erweitert also FileInputStream um Markierung und verwendet hierzu die Methoden eines RandomAccessFile.
Einerseits muss zuerst ein RandomAccessFile angelegt werden, da RandomAccessFile keine Anlage mit Hilfe eines FileDescriptors zulässt. Andererseits muss bei der Anlage des MarkedFileInputStream zuerst der Konstruktor von FileInputStream aufgerufen werden.
Dies führt dann zwangsläufig zu einem Hilfs-Konstruktor, der super() enthält und von den beiden public-Konstruktoren aufgerufen wird.
Dekorieren des MarkedFileInputStreams
Das nächste Code-Fragment testet die Klasse MarkedFileInputStream aus FileDescriptor-Einsatz. Die Anlage der Instanzen von DataOutputStream bzw. DataInputStream demonstrieren dann recht eindrucksvoll die Dekorierung (siehe auch Abb. 11.9).
try {
File f= new File("C:/Temp/Tst.dat");
f.createNewFile();
DataOutputStream dos= new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(f)));
dos.writeBoolean(true);
dos.writeInt(1);
dos.writeBytes("hallo\n");
dos.close();
Dekoration eines markierten
FileInputStreams
DataInputStream dis= new DataInputStream(
new BufferedInputStream(
new MarkedFileInputStream(f)));
System.out.println(dis.readBoolean());
dis.mark(0);
System.out.println(dis.readInt());
System.out.println(dis.readLine());
dis.reset();
System.out.println(dis.readInt());
dis.close();
}
catch (IOException e) { System.out.println(e); }
Dekorieren mit einem Zip-Stream
ZipOutputStream
ZipInputStream
In dem kleinen Package java.util.zip gibt es zwei Filter-Klassen ZipOutputStream und ZipInputStream, die recht einfach verwendet werden können.
Das nachfolgende Code-Fragment legt eine Zip-Datei mit einem Eintrag "1" an, welcher die ASCII-Zeichen eines Strings enthält:
try {
File f= new File("C:/Temp/Tst.zip");
f.createNewFile();
ZipOutputStream zos= null;
// Dreifach-Dekorierung!
DataOutputStream dos= new DataOutputStream(
zos= new ZipOutputStream(
new BufferedOutputStream(
new FileOutputStream(f))));
zos.putNextEntry(new ZipEntry("1"));
dos.writeBytes("Dieser String wird gezippt!");
dos.close();
}
catch (IOException e) { System.out.println(e); }
Das Ergebnis kann man sich dann mit einem gewöhnlichen Zipper betrachten oder »Retro-Code« schreiben.
DOS-Konsole, BufferedReader und PrintWriter
Benutzt man System.in bzw. System.out für die Ein- bzw. Ausgabe in einer DOS-Konsole mit deutschen Sonderzeichen, so ist das Ergebnis nicht sonderlich berauschend.
Default-
Codierung:
Probleme mit Umlauten
Der Grund liegt darin, dass Java auch für die DOS-Konsole die Default-Codierung Cp1252 (Windows Latin 1) verwendet, DOS aber mit Cp850 (DOS Latin 1) arbeitet (vgl. hierzu auch 10.6.4 bzw. 10.6.5). Die Ausgabe von
s= "äöü"; System.out.println(s);
// DOS-Konsole :: õ÷³
kann also in einer DOS-Konsole anders ausfallen, als die Ausgabe in der Entwicklungsumgebung wie z.B. JBuilder.
Genauso unangenehm für Sonderzeichen sind Eingaben nur mit Hilfe des Byte-Streams System.in.
In diesem Fall hilft die Dekorierung mit Zeichen-Streams, die eine explizite Angabe der Codierung zulassen.
Das nachfolgende Beispiel basiert auf InputStreamReader bzw. OutputStreamReader.
Abbildung 11.11 Unicode-Konvertierung mit Hilfe von Codierung
Ein- und Ausgabe sind auf die DOS-Konsole ausgerichtet:
/* --- Default-Codierung für WinNT Cp1252 ---
BufferedReader br= new BufferedReader(
new InputStreamReader(System.in));
*/
PrintWriter pw= null;
String s= null;
Dekorieren von System.in und System.out für Umlaute
try {
// PrintWriter mit automatischen Flush (true)
pw= new PrintWriter(
new OutputStreamWriter(System.out,"Cp850"),true);
BufferedReader br=
new BufferedReader(
new InputStreamReader(System.in,"Cp850"));
s= br.readLine();
}
catch (IOException e) { System.out.println(e); }
pw.println(s);
// alternative geht auch:
// System.out.write(sb.toString().getBytes("Cp850"));
// keine gute Idee wäre:
// System.out.println(s);
Wie man erkennt, bieten die High-Level-Streams InputStreamReader bzw. PrintWriter alle Möglichkeiten von System.in bzw. System.out sowie den Vorteil der Codierung.
Deployment von Ressource-Dateien
Nicht alle Applikations-Eigenschaften können hart codiert werden.
Sehr oft werden in einer Textdatei als Schlüssel/Werte-Paare Informationen bzw. Ressourcen einer Applikation eingetragen, die mit der Applikation an den Kunden übergeben werden, um dann z.B. mit Hilfe der Klasse Properties ausgewertet zu werden.
Deployment-Problem
Java-App-Deployment: Finden von Ressource-Dateien
Man kann zwar den Dateinamen der Ressource-Datei mit den Eigenschaften in der Applikation festlegen, aber nicht das Verzeichnis des Client-Rechners, zumal wenn das Betriebssystem unbekannt ist.
Das Ressource-Verzeichnis muss sich dann an den lokalen Applikationspfad und die Gegebenheiten des lokalen Rechners anpassen.
Lösung
Mit Hilfe der Instanz-Methode
Laden der
Ressource-Datei über Class-Objekt
public InputStream getResourceAsStream(String name);
der Klasse Class können Ressource-Dateien applikations- und rechnerabhängig gefunden werden.
Dazu muss der Name der Ressource-Datei übergeben werden. Dieser String wird von der Class-Instanz an den aktuellen ClassLoader übergeben, der alle Verzeichnisse, die im Java-Klassenpfad14 des Rechners eingetragen sind, nach der Datei durchsucht und sie – sofern dort abgelegt – lädt.
Absolut oder relativ zum Package
|
Der Name der Ressource-Datei kann dabei absolut oder relativ zum Package, zu dem die Ressource gehört, angegeben werden. |
Beginnt der Name mit "/", so wird nur nach genau diesem Namen gesucht, im anderen Fall wird der Name des Package vorangestellt15 :
package firma.project;
//...
// sucht in allen Verzeichnissen nach firma/project/app.prop
getResourceAsStream("app.prop");
getResourceAsStream("/app.prop"); // sucht nach
app.prop
Beispiel
Der statischen Methode get() der Klasse AppProperties wird der Name der Ressource-Datei übergeben, die diese nach dem oben beschrieben Muster sucht und lädt.
Laden der
Ressource über Class
Die Klasse wird dann in MyApp benutzt, um einen unter dem Schlüssel "File" abgelegten Dateinamen zu holen, die Datei zu öffnen und auf der Konsole anzuzeigen.
class AppProperties {
protected AppProperties() { }
public static Properties get(String s) {
Properties p= new Properties();
try {
p.load(AppProperties.class.getResourceAsStream(s));
¨
} catch (Exception e) {
throw new IllegalArgumentException(s); // gut?
}
return p;
}
}
class MyApp {
public static void propLookup() {
String s;
Angabe der
Ressource-Datei ohne Verzeichnis
try {
Properties p= AppProperties.get("/Temp.prop");
if ( (s= p.getProperty("File")) != null) {
BufferedReader br= new BufferedReader(
new InputStreamReader(
new FileInputStream(s)));
while ((s= br.readLine()) != null)
System.out.println(s);
}
} catch (Exception e) { System.out.println(e); }
}
}
Zu ¨: Die Methode getResourceAsStream() des Class-Objekts, zugehörig zu AppProperties, übergibt einen InputStream an load().
11.9 Digitale Signatur für Dokumente (Byte-Streams)
Authentizität
von Daten
Zur Überprüfung der Authentizität (Echtheit) von Daten werden asymmetrische Verfahren verwendet, die auf Schlüsselpaaren beruhen. Die Fälschung von Dokumenten während der Übertragung kann hiermit wie folgt bekämpft werden (Abb. 11.12):
Digitale Signatur:
asymmetrisches Verfahren
|
Der Sender signiert die Dokumente mit seinem privaten Schlüssel. Dies erzeugt eine digitale Signatur, d.h. eine Bit-Sequenz. |
|
Das Dokument sowie die digitale Signatur werden zum Empfänger übertragen, der das Dokument anhand der Signatur und dem öffentlichen Schlüssel des Senders auf seine Authentizität hin verifiziert. |
Abbildung 11.12 Dokumentenprüfung anhand einer digitalen Signatur
Charakteristiken der digitalen Signatur
Charakteristiken der digitalen Signatur:
1. |
Öffentlicher und privater Schlüssel des Senders bilden ein unverwechselbares Schlüsselpaar, wobei der private Schlüssel sich nicht aus dem öffentlichen berechnen lässt. |
2. |
Die Signaturfunktion liefert unterschiedliche Ergebnisse für unterschiedliche private Schlüssel oder Daten. |
3. |
Die Verifikationsfunktion liefert genau dann den Wert true, wenn der öffentliche Schlüssel zum privaten gehört und die Signatur sowie die Daten unverändert sind. |
Problem:
Schlüsselübergabe bzw. Identität des Senders
Solange also der private Schlüssel vor fremden Zugriffen geschützt ist, ist die Übermittlung fälschungssicher.
Problem: Übergabe des öffentlichen Schlüssels an den Empfänger bzw. Feststellung der Identität des Senders (von Seiten des Empfängers).
Authentifizierung
Authentifizierung: Identität des Senders
Unter Authentifizierung versteht man die Ermittlung der Identität des Senders. Bei den meisten Kommunikationen können sich Sender und Empfänger nicht persönlich zur Übergabe des öffentlichen Schlüssels treffen. Das Authentifizierungsproblem besteht also darin, dass man vom öffentlichen Schlüssel nicht auf die Identität des Absender schließen kann.
Eine Lösungsvariante besteht in der Zertifizierung des öffentlichen Schlüssels durch einen vertrauenswürdigen Dritten (offiziell durch zertifizierte Trustcenter (u.a. Post und Telekom).
11.9.1 Package java.security
Mit Hilfe des Packages java.security wird das Signieren und Verifizieren von Dateien demonstriert (Abb. 11.13).
Package java.
security: Signieren und Verifizieren von Dateien
Abbildung 11.13 Klassen zum Signieren und Verifizieren von Daten
Factory-Klassen: KeyPairGenerator, Signature
Die Klassen KeyPairGenerator und Signature variieren das Factory-Pattern. Sie fungieren als Fabrik ihrer eigenen Instanzen. Mit Hilfe von getInstance() haben sie eine größere Kontrolle über die Objekt-Anlage.
Wie der Name besagt, stellt die Klasse SecurityDSAUtil (der Client in Abb. 11.13) drei statische Methoden auf Basis des DSA-Algorithmus16 zur Verfügung.
class SecurityDSAUtil {
Erzeugung des KeyPairs
public static KeyPair genKeyPair()
{
try {
KeyPairGenerator kpg= KeyPairGenerator.getInstance("DSA");
// Schlüsselstärke (nicht Länge!): 512 Bits im
Modulo
kpg.initialize(512);
return kpg.generateKeyPair();
} catch (Exception e) { return null; }
}
// Signieren der Datei fileName
// Die Signatur wird als Ergebnis und/oder Datei geliefert
Schlüsselübergabe und
Signieren
public static byte[] signFile(PrivateKey
key,String fileName,
String signFileName) {
byte[] buf= new byte[2048];
int num= 0;
try {
Signature sig= Signature.getInstance("DSA");
sig.initSign(key); // Schlüsselübergabe
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(fileName));
while (bis.available() != 0) {
num= bis.read(buf);
sig.update(buf,0,num); // Datenübergabe an Signatur
}
bis.close();
buf=sig.sign();
if (signFileName!= null) {
FileOutputStream fos=new FileOutputStream(signFileName);
fos.write(buf);
fos.close();
}
return buf;
} catch (Exception e){ return null; }
}
// Verifizieren der Datei fileName mit Hilfe des
// öffentlichen Schlüssels und der Signatur-Datei
Verifizieren mit Hilfe des öffentlichen Schlüssels
public static boolean verifyFile(PublicKey
key,
String fileName,
String signFileName) {
byte[] buf= new byte[2048];
int num= 0;
try {
FileInputStream fis=new FileInputStream(signFileName);
byte[] signPattern= new byte[fis.available()];
fis.read(signPattern);
fis.close();
Signature sig= Signature.getInstance("DSA");
sig.initVerify(key);
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(fileName));
while (bis.available() != 0) {
num= bis.read(buf);
sig.update(buf,0,num);
}
bis.close();
return sig.verify(signPattern);
} catch (Exception e){ return false; }
}
}
Es folgt ein kurzer Test anhand einer lokalen Text-Datei.
Lokaler Test
public class Test {
public static void main(String[] args) {
KeyPair kp= SecurityDSAUtil.genKeyPair();
System.out.println( // :: 46
SecurityDSAUtil.signFile(kp.getPrivate(),
"C:/Temp/Krypt.txt","C:/Temp/Krypt.sgn").length);
System.out.println( // :: true
SecurityDSAUtil.verifyFile(kp.getPublic(),
"C:/Temp/Krypt.txt", "C:/Temp/Krypt.sgn"));
}
}
Natürlich ist mit der Klasse SecurityDSAUtil noch nicht das Authentifizierungsproblem gelöst.
11.10 Pipe-Streams
Die Kommunikation zwischen Threads ist nicht unbedingt trivial. Sollten Threads in einem Produzenten-Konsumenten-Verhältnis stehen, kann die Kommunikation mittels Pipes vorgenommen werden.
Pipes für Produzenten/
Konsumenten-Threads
Pipes dienen ausschließlich der Kommunikation zwischen Threads und haben den Vorteil der losen Kopplung:
|
Der Server-Thread schreibt in eine Pipe, ohne den Client zu kennen. |
|
Der Client-Thread liest aus einer Pipe, ohne den Server zu kennen. |
Da ein Stream nicht gleichzeitig lesen und schreiben kann, muss man Pipes immer in Paaren verwenden.
Zur Konstruktion des Übertragungskanals erschafft man z.B. zuerst einen PipedOutputStream für die Byte-Übertragung und dann einen PipedInputStream, dem man im Konstruktor den PipedOutputStream übergibt.
Muster für Produzenten/
Konsumenten-Threads
Man braucht zumindest zwei Threads. Für das folgende Code-Muster wird ein Produzenten- und ein Konsumenten-Thread verwendet, denen man im Konstruktor jeweils eine Pipe übergibt, um sie anschließend zu starten.
try {
PipedOutputStream pOut= new PipedOutputStream();
PipedInputStream pIn= new PipedInputStream(pOut);
RunnableProducer prod= new RunnableProducer(pOut);
RunnableConsumer cons= new RunnableConsumer(pIn);
new Thread(prod).start();
new Thread(cons).start();
//...
}
catch (IOException e) { System.out.println(e); }
Damit sind Input- und Output-Pipe verbunden und die Datenübertragung zwischen den Threads kann beginnen.
Für eine Zeichenübertragung verwendet man analog das Paar PipedWriter bzw. PipedReader.
Pipe-Zustände bzw. -Reaktionen
Pipe-Implementation: zirkuläre Puffer
Die Pipe-Kommunikation beruht auf einem zirkulären Puffer im Speicher, in den geschrieben bzw. aus dem gelesen wird.
Pipe-Aktionen:
IOException Suspendierung
Hieraus resultieren verschiedene unangenehme Situationen, auf die die Pipes wie folgt reagieren:
|
Terminiert der Konsumenten-Thread, während der Produzenten-Thread in die Pipe schreibt, gibt es eine IOException mit der Meldung "Read end dead". |
|
Ist der Puffer voll, d.h. der Konsumenten-Thread zu langsam, blockiert der Produzenten-Thread beim Schreiben. |
|
Ist der Puffer leer, blockiert der Konsumenten-Thread beim Lesen. |
Beispiel: Pixel-Austausch zwischen Threads per Pipe
Produzent:
GIF-Bilder versenden – Konsument: Histogramm erstellen
Ein Produzent soll ein GIF-Bild einlesen, es als Grauskala-Bild darstellen und die Pixel an einen Konsumenten übertragen, der wiederum das zugehörige Histogramm zeichnet. Dabei sollen Produzenten- und Konsumenten-Thread in dasselbe Fenster schreiben.
Programm und Ausgabe sind möglichst schlicht gehalten, da im Wesentlichen nur die Pipes und nicht das AWT demonstriert werden sollen (Abb. 11.14).
Abbildung 11.14 Bildschirmfenster zur Pipe-Demo
Histogramm
|
Ein Histogramm gibt zu jedem Farb- bzw. Grauskala-Wert oder Intervall die Anzahl der zugehörigen Pixel eines Bildes an. |
Zu dem o.a. Bild in 256 Graustufen (0: schwarz,.., 255: weiß) gehören somit 256 Werte, die die Anzahl der Pixel dieser Graustufe widergeben.
Die beiden Klassen ImageRaster und Histogramm übernehmen als Threads die Hauptaufgabe der Kommunikation (siehe Abb. 11.15).
Kommunikations-Ablauf
Abbildung 11.15 Überblick über die Pipe-Demo-Applikation
ImageRaster erzeugt ein Fenster von SimpleWindow, über das Interface UsableFrame holt sich anschließend Histogramm das Fenster. Das Bild bzw. das Histogramm wird in der Klasse ImagePanel bzw. HistogrammPanel gezeichnet. Die Panels werden in den jeweiligen Threads als Komponenten in das Fenster SimpleWindow eingefügt (siehe Abb. 11.16).
Klassen-Diagramm zur Pipe-Kommunikation
Abbildung 11.16 Klassen-Diagramm zur Pipe-Demo-Applikation
Histogramme werden in der Regel so normiert, dass die Summe aller Histogramm-Werte Eins ergibt.17 In der Klasse HistogrammPanel werden die Werte recht unorthodox mit Hilfe des zweiten Maximums auf die Fensterhöhe angepasst.
Die erste Aufgabe besteht darin, die Fenster-Klasse JFrame so zu erweitern, dass sie auf das Schließen-Ereignis mit Beenden der Applikation reagiert (siehe Kapitel 15).
Hierzu wird eine anonyme Klasse von der abstrakten Klasse WindowAdapter abgeleitet und die zugehörige Methode windowClosing() überschrieben.
EJFrame reagiert auf Schließen-Ereignis
class EJFrame extends
JFrame {
public EJFrame(String title) {
super(title);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e)
{ System.exit(0); }
});
}
}
Ein Fenster wie JFrame nimmt nicht direkt Komponenten auf, sondern überlässt dies der enthaltenen JRootPane, die diese in ihren Container contentPane einfügt. Man holt ihn direkt mit getContentPane().
SimpleWindow nimmt als Subklasse visuelle Komponenten auf und weist contentPane durch den Layout-Manager FlowLayout an, sie bevorzugt nebeneinander (ansonsten untereinander) darzustellen.
SimpleWindow:
Bilddarstellung im FlowLayout
class SimpleWindow
extends EJFrame {
public SimpleWindow(String title, Component c) {
super (title);
getContentPane().setLayout(new FlowLayout());
getContentPane().add(c);
pack(); show();
}
public Component add(Component c) {
getContentPane().add(c);
pack(); repaint();
return c;
}
}
Ein JPanel ist eine einfache grafische Komponente, in die man Bilder zeichnen kann. Für jede Komponente kann man mit setPreferredSize() die gewünschte Größe in Pixel angeben.
Es gibt zwei spezialisierte Panel, ImagePanel, um das GIF-Bild darzustellen, und HistogrammPanel, um das Histogramm zu zeichnen:
ImagePanel:
JPanel für GIF-Bilder
class ImagePanel extends
JPanel {
private BufferedImage image;
ImagePanel(BufferedImage image) {
this.image= image;
this.setPreferredSize(
new Dimension(image.getWidth(),image.getHeight()));
}
public void paintComponent(Graphics g) {
// Cast notwendig, da neues 2D-AWT verwendet wird
((Graphics2D)g).drawImage(image,null,0,0);
}
}
HistogrammPanel:
Pixel-Auswertung und Darstellung der Grauverteilung
class HistogrammPanel
extends JPanel {
private int[] pixhisto;
HistogrammPanel(int[] histogramm, int h) {
pixhisto= (int[]) histogramm.clone();
int max= 0, max2=0;
// 2. Maximum suchen!
for (int i=0; i<pixhisto.length; i++)
if (pixhisto[i]>max) { max2= max; max= pixhisto[i]; }
// Anpassen: 2. Maximum erhält
90% der Panel-Höhe
for (int i=0; i<pixhisto.length; i++)
pixhisto[i]= (int)(pixhisto[i]*0.9*h/max2);
this.setPreferredSize(
new Dimension(pixhisto.length+20,h));
}
public void paintComponent(Graphics g) {
Graphics2D g2d= (Graphics2D)g; // 2D-AWT notwendig
// normales Koordinaten-System setzen
g2d.translate(0.0,getPreferredSize().height);
g2d.scale(1.0,-1.0);
for (int i=0; i<pixhisto.length; i++)
g2d.drawLine(i,0,i,pixhisto[i]);
}
}
Es folgt das ImageRaster als Produzenten-Thread:
interface UsableFrame {
JFrame getFrame(); }
ImageRaster:
Produzenten-Thread
class ImageRaster implements
Runnable, UsableFrame {
private BufferedImage bi;
private OutputStream os; // Pipe!
private JFrame win;
public ImageRaster(String gifName, OutputStream
os) {
this.os= os; // Übergabe der Pipe!
Mit Pipe
verbinden BufferedImage schreiben Thread starten
// Bild holen, BufferedImage anlegen
Image pic= new ImageIcon(gifName).getImage();
bi= new BufferedImage(pic.getWidth(null),
pic.getHeight(null),BufferedImage.TYPE_BYTE_GRAY);
// Bild in das BufferedImage schreiben
Graphics2D g2= bi.createGraphics();
g2.drawImage(pic,0,0,null);
new Thread(this).start(); // startet sich
selbst!
}
public JFrame getFrame() { return win; }
Bild rastern,
Pixel-Array erstellen
public byte[] getRaster() {
// Rastern und in das Byte-Array pixel schreiben
// Die Bytes stellen unsigned Werte (0..255) dar!
Raster wr= bi.getRaster();
byte[] pixel= new byte[wr.getWidth()*wr.getHeight()];
wr.getDataElements(0,0,wr.getWidth(),wr.getHeight(),
pixel);
return pixel;
}
Pixel-Array an Pipe übergeben
public void run() {
// Fenster erschaffen, ImagePanel mit Bild übergeben
win= new SimpleWindow("Test",new ImagePanel(bi));
try {
// Pixel-Array in die Pipe schreiben und vergessen
DataOutputStream dos= new DataOutputStream(os);
dos.write(getRaster());
dos.close();
} catch (IOException e) { System.out.println(e); }
}
}
Es folgt das Histogramm als Konsumenten-Thread:
Histogramm:
Konsumenten-Thread
class Histogramm implements
Runnable{
private InputStream is; // Pipe!
private UsableFrame uf; // Fenster mit Bild
int h; // Fensterhöhe
Mit Pipe verbinden
Thread starten
public Histogramm(InputStream is, UsableFrame
uf,
int height) {
this.is= is; this.uf= uf; h= height;
new Thread(this).start(); // startet sich selbst!
}
public void run() {
byte[] pixel= null;
int[] hist= new int[256];
try {
DataInputStream dis= new DataInputStream(is);
In-Memory-Stream für Pixel
// da Größe des
Pixel-Arrays unbekannt, werden die
// Bytes erst einmal in Memory-Stream geschrieben
ByteArrayOutputStream ba= new ByteArrayOutputStream();
byte[] buf= new byte[1024]; // angepasst
an Pipe
int num;
// siehe abschließende Anmerkung
while ((num= dis.read(buf,0,1024))> 0) {
ba.write(buf,0,num);
}
pixel= ba.toByteArray(); // Memory-Stream ‡ Array
} catch (Exception e) { System.out.println(e); }
for (int i=0; i<pixel.length; i++)
Umwandeln von signed in unsigned Bytes
// leider(!) besteht das
Pixel-Array aus signed Bytes
// enthalten aber unsigned Bytes, also umwandeln!
hist[((pixel[i]>>>4)&0xF)*16+(pixel[i]&0xF)]++;
if (uf!=null && uf.getFrame()!=null)
// dadurch, dass ImageRaster das SimpleWindow anlegt,
// bevor es die Daten in die Pipe schickt, ist
eine
// Race-Condition eigentlich ausgeschlossen
uf.getFrame().add(new HistogrammPanel(hist,h));
else System.out.println("dumm gelaufen");
}
}
Produzent und Konsument sind aktive Objekte
Es verbleibt die obligatorische Test-Klasse. Sie legt die Pipes an, verbindet sie miteinander und erzeugt anschließend die Produzenten und Konsumenten. Sie starten sich als aktive Objekte selbst.
public class Test {
public static void main(String[] args) {
try {
PipedOutputStream POut= new PipedOutputStream();
PipedInputStream PIn= new PipedInputStream(POut);
new Histogramm(PIn,
new ImageRaster("C:/Temp/pic.gif",POut),
300);
} catch (IOException e) { System.out.println(e); }
}
}
available(): Vorsicht asynchron!
available():
asynchrone I/O-Operation mit Tücken
Die Klasse Histogramm kann natürlich vor der eigentlichen Lese-Operation aus der Pipe die Anzahl der vorhandenen Bytes mit der Methode available() abfragen.
Dagegen spricht allerdings, dass available() eine der wenigen asynchronen Methoden ist. Asynchrones I/O ist sicherlich eine Bereicherung.
Die Methode available() reagiert wie folgt:
|
Sie kehrt sofort mit der Anzahl der zurzeit lesbaren Bytes in der Pipe zurück. |
|
Die Chancen sind hoch, dass available() die Anzahl 0 liefert, obwohl wenig später der Puffer wieder von der Input-Pipe gefüllt wird. |
Die Methode read()blockiert dagegen, bis Daten bereitstehen, und ist für das obere Beispiel ohne Zweifel besser geeignet.
Unsigned Byte: der vergessene Typ
byte vs. ubyte
Wie an der Histogramm-Klasse zu erkennen, macht das Fehlen eines Typs ubyte (unsigned Byte) Schwierigkeiten. Gerade im Umgang mit Graphik benötigt man diesen Typ dringend.
Unsigned-Byte-Umwandlung
Man muss sich dann mit so unschönen Umwandlungen wie
((pixel[i]>>>4)&0xF)*16+(pixel[i]&0xF)
behelfen, um einen ubyte aus einem byte zu extrahieren.
11.11 Zusammenfassung
Nach einem kurzen Überblick über das umfangreiche Klassensystem werden zuerst die Klassen File und FileDescriptor besprochen. File ist die notwendige Abstraktion einer Datei bzw. eines Verzeichnisses, FileDescriptor fungiert nur als Datei-Handle.
Die Interfaces DataInput und DataOutput stellen allgemeine Methoden zum Lesen und Schreiben von Bytes und primitiven Typen bereit.
Die Implementation und der Einsatz der Methoden dieser Interfaces wird anhand der Klasse RandomAccessFile demonstriert.
Als zentrales Thema dieses Kapitels folgt die Erläuterung von Streams.
Hier wird zuerst die Problematik Marshaling und Unmarshaling angesprochen, bevor auf die Klassifizierung der Streams in Low- und High-Level eingegangen wird.
Byte-Streams sowie Unicode/Zeichen-Streams werden anhand der Klassifizierung behandelt.
Das wichtige Decorator-Pattern, das das Design der I/O-Hierarchie maßgeblich beeinflusst hat, beendet die Besprechung der Streams.
Es schließen sich kleine, aber wichtige Beispiele an, die den Einsatz von Streams und des Decorator-Patterns zeigen. Hierzu zählen Zip-Files, FileDescriptor-Einsatz zum Markieren eines Streams, Dekorieren von System.in bzw. out und Deployment von Ressource-Dateien.
Abschließend wird das Konzept der digitalen Signaturen und Thread-Kommunikation auf Basis von Pipes anhand zweier umfangreicher Beispiele behandelt. Für das Pipe-Beispiel werden AWT/Swing-Komponenten und -Klassen verwendet.
11.12 Testfragen
Zu jeder Frage können jeweils eine oder mehrere Antworten bzw. Aussagen richtig sein.
1. Welche Aussagen zu File sind richtig?
A: Mit Hilfe einer Instanz-Methode von File kann ein Verzeichnis angelegt werden.
B: Mit Hilfe einer Instanz-Methode von File kann ein Verzeichnis nur gelöscht werden, wenn es leer ist.
C: Mit Hilfe einer Instanz-Methode von File kann ein Verzeichnis inklusive seiner übergeordneten Verzeichnisse angelegt werden (sofern diese nicht bereits vorhanden sind).
D: Wird dem Konstruktor von File bei Anlage einer Instanz der Name einer nicht vorhandenen Datei übergeben, so wird diese angelegt.
E: Mit Hilfe einer Instanz-Methode von File kann der gesamte Inhalt eines Verzeichnisses (als String-Array) ausgegeben werden.
2. Welche Aussagen zu Zeichen sind richtig?
A: Alle ASCII-Zeichen sind in der gleichen Anordnung im Unicode-Zeichensatz enthalten.
B: UTF8 codiert Zeichen in ein oder zwei Bytes.
C: UTF8 codiert alle ASCII-Zeichen in nur einem Byte.
D: Byte-Streams sind nicht in der Lage, Unicode-Zeichen zu lesen bzw. zu schreiben.
3. Welche Aussagen zu Reader und Writer sind richtig?
A: Ein Reader kann nur Unicode-Zeichen lesen.
B: Ein Reader kann keinen Wert vom primitiven Typ int lesen.
C: Ein Writer kann Unicode-Zeichen direkt in eine Datei schreiben.
D: Ein Writer kann mit Hilfe eines Low-Level-Byte-Streams Zeichen in verschiedenen Codierungen in eine Datei schreiben.
4. Welche Aussagen zu Filter sind richtig?
A: Filter-Klassen sind High-Level-Streams.
B: Filter-Klassen gibt es nur für Lese-Operationen.
C: Filter-Klassen operieren immer auf Basis eines Low-Level-Streams.
D: System.in ist ein Filter-Stream.
E: Filter-Klassen sind immer Decorator-Klassen.
5. Welche Aussagen sind zu folgender Applikation richtig?
public class Testfragen {
public static void main(String[] args) {
try {
DataOutputStream is= new DataOutputStream(
new FileOutputStream("tmp.dat"));
DataInput os= new RandomAccessFile("tmp.dat","rw"); ¨
is.writeByte(1); is.writeByte(1);
is.close();
System.out.println(os.readByte());
System.out.println(os.readBoolean()); ¦
// os.writeByte(1); Æ
// ((RandomAccessFile)os).writeByte(1); Ø
} catch (IOException e) { System.out.println(e); }
}
}
A: Zeile ¦ erzeugt zur Laufzeit eine Ausnahme, da eine boolean statt byte gelesen wird.
B: Zeile ¦ erzeugt die Ausgabe: true
C: Die Datei tmp.dat enthält zwei Bytes.
D: Existiert bereits eine Datei tmp.dat vor Start der Applikation, so wird diese geöffnet und es werden die ersten zwei Bytes überschrieben.
E: Die Applikation wird wegen Zeile ¨ überhaupt nicht kompiliert, denn os hat den falschen Typ.
F: Werden (nur) die Kommentar-Zeichen in Zeile Æ entfernt, gibt es einen Compiler-Fehler.
G: Werden (nur) die Kommentar-Zeichen in Zeile Ø entfernt, gibt es zur Laufzeit eine Ausnahme aufgrund von Ø.
5. Welche Aussagen sind zu folgendem Code-Fragment richtig?
File fd= File.createTempFile("Tst","tmp");
fd.deleteOnExit();
A: Es wird eine neue Datei mit Namen Tst.tmp angelegt.
B: Die Datei wird automatisch bei Beendigung der Applikation gelöscht.
C: Die Datei wird im aktuellen Verzeichnis angelegt.
D: Die Datei wird, abhängig vom Betriebssystem, in einem speziellen Verzeichnis für temporäre Dateien angelegt.
6. Welche Aussagen sind zu folgender Applikation richtig?
public class Testfragen {
public static void main(String[] args) {
try {
Writer ws= new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("tmp.dat"),"UTF8"));
ws.write("abcäöü");
ws.close();
System.out.println(new File("tmp.dat").length()); ¨
RandomAccessFile os= new RandomAccessFile("tmp.dat","r");
System.out.println(os.readLine()); ¦
Reader rs= new BufferedReader(
new InputStreamReader(
new FileInputStream("tmp.dat"),"UTF8"));
char[] carr= new char[100];
System.out.println(new String(carr,0,rs.read(carr))); Æ
} catch (IOException e) { System.out.println(e); }
}
}
A: Zeile ¨ erzeugt die Ausgabe: 6
B: Zeile ¨ erzeugt die Ausgabe: 9
C: Zeile ¦ erzeugt die Ausgabe: abcäöü
D: Zeile ¦ erzeugt einen Ausgabe-String, der in keinem Zeichen mit dem String abcäöü übereinstimmt.
E: Zeile Æ erzeugt die Ausgabe: abcäöü
7. Für die folgenden Anweisungen wird das Abfangen der Ausnahmen im umgebenden Code vorausgesetzt. Welche Aussagen sind richtig?
A: Mit der Anweisung
RandomAccessFile raf= new RandomAccessFile("tmp.dat","w");
wird die Datei tmp.dat zum Schreiben geöffnet.
B: Mit der Anweisung
RandomAccessFile raf= new RandomAccessFile("tmp.dat","rw");
wird die Datei tmp.dat zum Lesen und Schreiben geöffnet.
C: Mit der Anweisung
RandomAccessFile raf= new RandomAccessFile("tmp.dat","rw");
wird eine bereits existierende Datei tmp.dat überschrieben.
D: Mit der Anweisung
new File("tmp.dat").delete();
kann eine entsprechend existierende Datei tmp.dat gelöscht werden.
E: Mit der Anweisung
new File("C:/tmp/tmp.dat").delete();
kann eine im Verzeichnis C:\tmp von Windows NT existierende Datei tmp.dat gelöscht werden.
F: Die Anweisung
new File("C:/tmp/tmp.dat").delete();
löst eine Ausnahme aus, sollte die Datei tmp.dat im angegebenen Verzeichnis nicht existieren.
G: Es sei raf wie in Frage C deklariert. Dann werden mit den folgenden Anweisungen insgesamt 14 Bytes geschrieben:
raf.writeFloat(1.23f);
raf.writeDouble(1.23);
raf.writeChar('a');
1 Dies steht z.B. im krassen Gegensatz zu dem Package java.util, welches das Glück der »späten Geburt« auf seiner Seite hat.
2 Es hängt also vom aktuellen Benutzerpfad ab, was das File-Objekt repräsentiert.
3 Eine Ausnahme bildet der Spezialfall new File("."). Eine relative File-Instanz f lässt sich mit Hilfe von f.getAbsoluteFile() in eine absolute umwandeln.
4 Die Methode wird meistens mit createTempFile() eingesetzt (Beispiel in Methoden).
5 Zu jeder Methode im einen Interface gibt es eine komplementäre im anderen.
6 readFully() ist z.B. nicht äquivalent zu write().
7 Alle write-Methoden liefern void zurück.
8 Bereits die Reihenfolge, in der man die vier Bytes eines Integers liest, schreibt oder speichert, sollte einer festen Konvention folgen und nicht dem Zufall überlassen werden.
9 Das I/O-Konzept basiert eben nicht auf einer flexiblen Interface-Hierarchie.
10 Damit ist die Symmetrie in RandomAccessFile im Gegensatz zu DataInput
und DataOutput wiederhergestellt
11 FIFO (First-In/First-Out): Daten, die zuerst geschrieben werden, werden als erste gelesen.
12 Eine single-threaded Applikation »friert« also für den Anwender ein.
13 Marshal: aufstellen bzw. ordnen.
14 Abzufragen mit System.getProperty("java.class.path").
15 Wobei die Punkte im Package-Namen durch "/" ersetzt werden.
16 DSA: Digital Signature Algorithm, der einzige von SUN implementierte Algorithmus.
17 Diese Histogramme nennt man auch PMF (Probability Mass Function).
|