Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 11 Package java.io
  gp 11.1 Überblick
  gp 11.2 File
  gp 11.3 FileDescriptor
  gp 11.4 Interfaces DataInput und DataOutput
  gp 11.5 RandomAccessFile
  gp 11.6 Stream-Konzept
  gp 11.7 Decorator-Pattern
  gp 11.8 Streams im Einsatz
  gp 11.9 Digitale Signatur für Dokumente (Byte-Streams)
  gp 11.10 Pipe-Streams
  gp 11.11 Zusammenfassung
  gp 11.12 Testfragen

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.


Galileo Computing

11.1 Überblick  downtop

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.

gp  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.

gp  Übertragungsrichtung: Ein Eingabe-Stream, der empfängt, ein Ausgabe-Stream, der sendet.
gp  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):

gp  InputStream, OutputStream (byteorientiert)
gp  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
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

gp  File repräsentiert – unabhängig vom Betriebssystem – einen Datei- oder Verzeichnisnamen mit den auf Dateien und Verzeichnissen üblichen Operationen.

FileDescriptor: Handle

gp  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

gp  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

gp  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

gp  ObjectStreamClass bzw. ObjectStreamField enthalten im Zusammenhang mit der Serialisierung Informationen zur Klasse bzw. den Feldern (und werden eher selten benutzt).

Zugriffserlaubnis

gp  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.


Galileo Computing

11.2 File  downtop

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

gp  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

gp  Ein relativer Pfad identifiziert eine Datei oder ein Verzeichnis nur durch Anhängen an einen aktuellen (Benutzer-)Pfad.

Der aktuelle Pfad ist unter dem Schlüssel "user.dir" in den System-Properties abgelegt (siehe 10.6.6).

File-/Path-
Separator

gp  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

gp  nicht orthogonal: Nicht zu jeder Zugriffs-Methode in der File-Klasse existiert eine korrespondierende Modifikations-Methode.
gp  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).
gp  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
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.


Galileo Computing

11.3 FileDescriptor  downtop

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

gp  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

gp  die Möglichkeit, den FileDescritor für mehrere Streams zu nutzen, um gemeinsam auf eine aktuelle Dateiposition zuzugreifen (FileDescriptor-Einsatz).

Galileo Computing

11.4 Interfaces DataInput und DataOutput  downtop

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.

gp  Die beiden Interfaces sollten symmetrisch sein.

Sie sind es jedoch »not quite«, wie man an der Gegenüberstellung der Methoden in der Tabellen 11.2 sehen kann. Die fehlenden Methoden werden dann sinnigerweise in den Klassen deklariert.

Methoden zu
DataInput, DataOutput

DataInput DataOutput 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

gp  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.

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
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
Abbildung 11.4   Ausschnitt aus der Ausnahme-Hierarchie in java.io
gp  Alle Methoden der Interfaces können eine IOException auslösen.

Galileo Computing

11.5 RandomAccessFile  downtop

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.

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

gp  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); }
  }
}

Galileo Computing

11.6 Stream-Konzept  downtop

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
Abbildung 11.5   Abstraktion der Stream-Kommunikation

Stream-Eigenschaften:
One Way FIFO synchrones I/O

Alle Java-Streams haben folgende Eigenschaften gemeinsam:

gp  Sie können nur lesen oder schreiben.
gp  Sie lesen oder schreiben sequenziell nach dem FIFO-Prinzip11  .
gp  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.

gp  Marshaling13  bezeichnet beim Austausch von Daten die Transformation der Daten-Objekte in einen Block von Bytes.
gp  Unmarshaling bezeichnet den umgekehrten Prozess der Wiederherstellung der Daten aus dem Block von Bytes.

Network-Byte-Order, Big-Endian-Format

gp  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

gp  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.

gp  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

Icon

Den Austausch von Daten kann man anhand der Diskussion im letzten Abschnitt grob in zwei Ebenen unterteilen.

Low-Level:
Byte- bzw. Device-Ebene

gp  Low-Level: Kommunikation auf Byte-Ebene direkt mit dem (physikalischen) I/O-Device (Gerät).

High-Level:
Typ-Ebene

gp  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.

gp  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).
gp  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
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:

gp  Datei (FileOutputStream, FileInputStream)
gp  Byte-Array (ByteArrayOutputStream, ByteArrayInputStream)
gp  Pipe, d.h. ein asynchroner Kommunikationskanal zwischen Threads (PipedOutputStream, PipedInputStream)
gp  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.

gp  Filter-Streams bieten immer eine zusätzliche Funktionalität an. Man unterscheidet:
    gp  BufferedOutputStream bzw. BufferedInputStream, die als zusätzliche Puffer auf Low-Level-Streams aufgesetzt werden.
    gp  Filter-Streams, die entweder auf Buffered-Streams oder Low-Level-Streams aufsetzen.
gp  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
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

Icon

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

gp  Low-Level: Anbindung an Dateien, speicherbasierende Zeichen-Arrays oder Pipes
gp  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
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.

gp  Im Fall der Klassen PrintStream und PrintWriter sind die Aufgaben nicht äquivalent, sondern identisch. Mit Ausnahme von System.out ist immer PrintWriter zu verwenden.

Galileo Computing

11.7 Decorator-Pattern  downtop

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:

gp  Jede Klasse enthält eine Eigenschaft.
gp  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
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
Abbildung 11.10   Klassen-Diagramm zum Decorator-Pattern

Der Nachteil gegenüber einen reinen Vererbungslösung ist offensichtlich:

Nachteil des Dekorations-Konzepts

gp  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.


Galileo Computing

11.8 Streams im Einsatz  downtop

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

gp  die Anbindung eines weiteren Streams an dieselbe Datei.
gp  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
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

Icon

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

gp  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().


Galileo Computing

11.9 Digitale Signatur für Dokumente (Byte-Streams)  downtop

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

gp  Der Sender signiert die Dokumente mit seinem privaten Schlüssel. Dies erzeugt eine digitale Signatur, d.h. eine Bit-Sequenz.
gp  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
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).


Galileo Computing

11.9.1 Package java.security   downtop

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
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.


Galileo Computing

11.10 Pipe-Streams  downtop

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:

gp  Der Server-Thread schreibt in eine Pipe, ohne den Client zu kennen.
gp  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:

gp  Terminiert der Konsumenten-Thread, während der Produzenten-Thread in die Pipe schreibt, gibt es eine IOException mit der Meldung "Read end dead".
gp  Ist der Puffer voll, d.h. der Konsumenten-Thread zu langsam, blockiert der Produzenten-Thread beim Schreiben.
gp  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
Abbildung 11.14   Bildschirmfenster zur Pipe-Demo

Histogramm

gp  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
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
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); }
  }
}

Icon

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:

gp  Sie kehrt sofort mit der Anzahl der zurzeit lesbaren Bytes in der Pipe zurück.
gp  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.


Galileo Computing

11.11 Zusammenfassung  downtop

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.


Galileo Computing

11.12 Testfragen  toptop

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).

  

Perl – Der Einstieg




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de