Nostalgia: Das JavaBeans Development Kit (BDK)

Das BDK ist (war) eine Testumgebung für anzeigeorientierte Beans und Komponenten ohne visuelle Repräsentation wie Datenstrukturen und Container. Es ist nicht mehr im Original, sondern nur noch in der Version unter https://java.net/projects/bean-builder erhältlich.

Die Bean-Box bietet eine Arbeitsfläche, auf der die Komponenten platziert und verbunden werden können. Wird in der Bean-Box eine Komponente auf dem Bildschirm gesetzt, so liest Java aus der Bean die Informationen (die Properties genannt werden) aus und stellt sie dar. Nun können mehrere Beans miteinander verbunden werden, sodass etwa eine Komponente ein Ereignis auslöst, auf das die zweite Komponente reagiert. Alle Aktionen können direkt ohne eine Zeile Programmcode entworfen werden. Wir entwerfen zunächst im Design-Modus die Verbindungen, um sie dann im Laufzeit-Modus zu testen. Damit reiht sich die Bean-Box in die Reihe der grafischen Entwicklungsumgebungen wie etwa IBMs Visual Age ein. Ganz so komfortabel ist die Bean-Box dann aber auch nicht. Sie ist von Sun lediglich zum Testen und Anzeigen von Beans entworfen worden. Dies zeigt sich auch daran, dass beim Verlassen der Umgebung nicht einmal nach dem Speichern gefragt wird.

Die Bean-Box starten

In der BDK-Version 1.1 wechseln wir in das bdk/beanbox/bin-Verzeichnis und rufen die Skripte run.bat (Windows) oder run.sh (Unix) auf. Dann startet die Umgebung.

Eine Beispielsitzung im BDK

Das BDK ist nach dem Start in drei Fenster unterteilt. Dies sind die ToolBox, die Bean-Box und das Eigenschaften-Feld. Das ToolBox-Fenster enthält einige Beispielkomponenten, die wir verwenden können. Die Beispiele werden zur Laufzeit aus dem bdk/jars/-Verzeichnis genommen. Eigene Komponenten können wir als Jar-Dateien einfach dort hineinkopieren. Die eigentliche Bean-Box stellt die Zeichenfläche dar. Sie ist der wichtigste Teil des Pakets, weil auf ihr Komponenten platziert und getestet werden können. Wir wollen dies anhand der Komponente Molecule zeigen. Wird sie in der ToolBox angeklickt, so verwandelt sich der Cursor in einen Positionszeiger. Klicken wir in der Bean-Box auf eine Stelle, wird ein Molekül angezeigt. Es lässt sich verschieben und interaktiv in der Größe ändern. Die Eigenschaften jeder Komponente lassen sich im Property-Sheet ändern. Sie bestimmen das Aussehen und das Verhalten. Ist das Molekül aktiviert, erscheint im Eigenschaften-Dialog eine Auswahlliste mit einigen Molekülen. Eine Auswahl wirkt sich direkt in der Anzeige aus.

Hinweis: Leider können selektierte Komponenten nicht mit der Entfernen-Taste von der Oberfläche beseitigt werden. Hier ist nur der Gang über das Edit-Menü erfolgversprechend.

Das Zusammenspiel der Komponenten lässt sich testen, indem wir den Design-Modus verlassen. Dazu aktivieren wir im Menüpunkt View den ersten Punkt Disable Design Mode (der sich dann in Enable Design Mode umbenennt). Jetzt kann das Molekül mit der Maus verschoben werden.

Verknüpfungen zwischen Komponenten

Die hervorragendste Eigenschaft der grafischen Programmierung ist das visuelle Setzen von Beziehungen zwischen Komponenten. Dabei werden Ereignisse an Methoden anderer Beans gebunden. Wir wollen das mit der Komponente JellyBean und dem Molekül testen. Wenn wir nun mit der Maus auf die JellyBean klicken (also ein Mouse-Event auslösen), dann soll eine bestimmte Methode des Bean-Moleküls aufgerufen werden, sodass es sich auf dem Bildschirm dreht. Wir verbinden hier also einfach eine Ereignisquelle mit einer Ereignisbehandlung einer anderen Komponente. Die Bedienung der Bean-Box ist dabei so intuitiv, dass sie zum Spielen und Ausprobieren einlädt. Wir aktivieren die erste Schaltfläche und wählen den Menüpunkt Edit > Events > mouse > mousePressed.

Jetzt erscheint eine rote Linie, die mit der Maus bewegt wird. Nun klicken wir auf unser Molekül. Daraufhin erscheint eine Liste mit Funktionen, die bei der Mausbewegung ausgelöst werden können. Wir entscheiden uns für rotateOnX.

Beans speichern

In der Bean-Box lässt sich unter dem File-Menü die Bean in ein Jar-Archiv für ein Applet einwickeln. Der Speicherdialog schlägt standardmäßig ein Unterverzeichnis der Bean-Box vor. Zusätzlich zur serialisierten Klasse befinden sich im Archiv noch die vom BDK erstellten Metainformationen, die Quellcodes zu der Bean in einem Extra-Verzeichnis, eine HTML-Datei sowie eine Readme-Datei mit den Informationen darüber, welche Dateien zur Komponente gehören. Die HTML-Datei erhält eine Referenz auf die Klassendatei, sodass ein Applet-Viewer oder ein Browser direkt die Bean darstellen kann. Alte Browser unterstützen das Jar-Format im Applet-Tag der HTML-Datei nicht.

Die kleinste Bohne der Welt

Wir wollen uns nicht lange mit theoretischem Schnickschnack aufhalten – dafür bleibt noch genügend Zeit. Beginnen wir mit einer Komponente, die wir FirstBean nennen wollen.

package com.javatutor.insel.bean;

import java.applet.Applet;
import java.awt.*;

public class FirstBean extends Applet
{
 @Override
 public void paint( Graphics g )
 {
  g.drawString( "Hui; so kurz?", 20, 20 );
 }
}

Zweifellos wird die Kürze dieser Lösung verwundern. Das kommt daher, dass sich die Beans gut in die vorhandene Technologie einfügen und schon viele Komponenten als Beans gebaut sind.

Beginnen wir nun, das schon in der Einführung als einfaches Beispiel für Komponenten identifizierte Applet in unseren Application-Builder einzubauen. Dazu muss die Softwarekomponente in einem speziellen Format vorliegen: in Jar-Dateien.

Jar-Archive für Komponenten

Eine Jar-Datei ist ein Archivformat, das Sun zum Verbund von Klassen und Mediendateien einführte. Nach dem Compilieren der Klasse FirstBean wird die Datei mit einem zusätzlichen Manifest in eine Jar-Datei gepackt. Damit der Application-Builder erkennen kann, dass es sich dabei um eine Bean handelt, erwartet er eine bestimmte Information in einer Manifest-Datei, die automatisch beim Zusammenpacken erzeugt wird. Stünden diese Informationen nicht dabei, könnte der Application-Builder die Bean nicht erkennen.

Wir wollen die Jar-Datei mit dem Dienstprogramm jar von Hand erzeugen. Zunächst legen wir eine Manifest-Datei an, die wir nach der Komponente benennen.

FirstBean.mf

Manifest-Version: 1.0
Name: FirstBean.class
Java-Bean: True

Hinweis: Vor den Doppelpunkten dürfen keine Leerzeichen stehen, sonst erkennt Jar die erste Option nicht und bemängelt den fehlerhaften Header. Leider ist Jar nicht sehr fehlertolerant.

Befinden sich im Jar-Archiv mehrere Klassen, sollten ihre Dateinamen mit Return getrennt sein. Bei Paketnamen ist eine Trennung mit / vorgesehen, nicht mit dem Windows-\.

Im nächsten Schritt packen wir mit jar die Klassen sowie die Manifest-Datei zu einem Archiv zusammen. Hier müssen wir die Optionen cfm nutzen. Merken wir uns die Option im englischen Satz »Create a File with a Manifest«, übersetzt »Lege die Datei mit Manifest an« .

$ jar cfm FirstBean.jar FirstBean.mf FirstBean.class

Für eine genauere Beschreibung des Dienstprogramms jar sei auf Kapitel in der Insel verwiesen.

Wurde das Jar-Archiv erfolgreich erstellt, muss dem Application-Builder die Bean bekannt gemacht werden. Hier unterscheiden sich die Produkte der einzelnen Hersteller voneinander. Im BDK von Sun müssen alle Beans im Unterverzeichnis jars liegen, damit sie das BDK automatisch erkennt. Kopieren wir unsere Bean dort hinein. Unter Windows schreiben wir:

$ copy FirstBean.jar c:\bdk\jars

Wenn wir jetzt die Bean-Box starten, erscheint unsere Komponente mit in der Liste. Im jars-Verzeichnis stehen auch die anderen Beans.

Tipp: Eine etwas schnellere Vorgehensweise besteht darin, die Jar-Datei nicht in das jars-Verzeichnis, sondern in ein eigenes Verzeichnis zu legen. Das hat den Vorteil, dass die Bean-Box schneller startet, weil sie dann nicht die zusätzliche Bean auswerten muss, was immer Zeit kostet. Wenn wir Beans nicht benötigen, können wir sie auch verschieben oder löschen, was die Ladezeit zusätzlich erhöht. Damit in diesem Fall das eigene Jar-Archiv eingebunden wird, laden wir es unter dem Menüpunkt File > Load Jar.

Wenn wir wiederholt Beans in ein Archiv packen und keine integrierte Entwicklungsumgebung diese Arbeit erledigt, kann uns ein kleines Skript helfen. Ein Beispiel für Windows wäre:

a.bat

javac zzz.java
jar cfm zzz.jar zzz.mf zzz.class
copy zzz.jar c:\bdk\jars

Bean-Eigenschaften anpassen

In einem visuellen Entwicklungswerkzeug lassen sich alle ausgezeichneten Eigenschaften einer Bean anpassen. Dazu bietet die Umgebung oft ein zweigeteiltes Fenster an. Auf der einen Seite befinden sich die Eigenschaften und auf der anderen Seite die Editoren für jede Eigenschaft. Die folgende Abbildung zeigt einige Properties und deren Anzeigen.

Haben wir zum Beispiel als Eigenschaft eine Zeichenkette (wie der angezeigte Text eines Label-Objekts), können wir diese einfach in das Feld eintragen. Mit einem eigenen Property-Editor sind wir jedoch nicht auf die einfachen Datentypen beschränkt. Was ist, wenn etwa eine Komponente die Auswahl zwischen DM und Euro anbieten will? Wir können dem Benutzer nicht zumuten, dies in Zahlen einzugeben. Auch bei einem Label gibt es zum Beispiel die Möglichkeit, einen Cursor aus einer Liste auszuwählen.

Für ungewöhnliche Eigenschaften können wir einen eigenen Property-Editor definieren. Dazu kann der Editor den Anzeigebereich als Grafikfläche beschreiben.

Customizer

Reicht auch der Editor nicht aus, zum Beispiel bei einer Farbe, die wir gerne aus einem Farbkreis auswählen wollen, lässt sich zudem ein Customizer definieren, der noch einen Schritt weiter geht, denn für einen Customizer ist ein eigenes Fenster vorgesehen. Er soll mit einer einfachen Benutzerführung den internen Zustand ändern. IBM sieht für seine Beans zum Beispiel ein Hilfe-Feld vor. In WebGain wird dieser Customizer durch eine Schaltfläche mit drei Punkten angezeigt.

Property-Editoren

Für jede sichtbare Eigenschaft einer Bean gibt es einen Property-Editor, der den Wert darstellen und ihn bei einer veränderbaren Eigenschaft auch editieren kann. Bekommt die Eigenschaft den Fokus, wird automatisch der passende Editor ausgewählt. Für alle primitiven Datentypen gibt es standardmäßig Editoren, und zusätzlich kommen Auswahlmöglichkeiten für String, Color und Font hinzu. Ein Editor gilt für jeweils eine Property. Die Editoren werden an einer zentralen Stelle, dem Property-Editor-Manager, registriert und dann beim Fokus ausgewählt und aktiviert. Das ist Aufgabe der Entwicklungsumgebung.

Damit die Einstellmöglichkeit nicht auf diese einfachen Typen beschränkt bleibt, können wir für eine Eigenschaft einen eigenen Property-Editor konstruieren. Der kann eine ganz einfache Textzeile darstellen oder auch eine eindrucksvolle Multimedia-Komponente. Der Editor übernimmt die Anzeige und ist gleichzeitig das Modell (also gibt es keine Trennung von Daten und Visualisierung an dieser Stelle). Der Wert ist als Object in unserem Editor gespeichert. Außerdem muss der Editor die Schnittstelle java.beans.PropertyEditor implementieren oder alternativ die Klasse java.beans.PropertyEditorSupport erweitern, was meist einfacher ist.

BeanInfo

Durch Introspection/Reflection existiert ein leistungsfähiger Mechanismus, um die Eigenschaften und Ereignisse zur Laufzeit auszulesen. In der Regel nimmt die Entwicklungsumgebung dafür Methoden, die sich an die Namenskonvention halten. Es gibt aber noch eine zweite Möglichkeit, und die lässt sich über eine Bean-Information-Klasse nutzen. Sie bietet folgende Funktionalität:

  • explizites Auflisten der freigegebenen Leistungen, die nach außen sichtbar sein sollen
  • Zuordnung eines mit der Bean verwendeten Icons
  • Anmeldung einer Customizer-Klasse

Sind die freigegebenen Leistungen aufgelistet, wird damit eine Untersuchung der Bean-Klassen auf die Namensgebung verhindert. Es gibt daher zur Freigabe der Eigenschaften und Ereignisse spezielle Methoden, die von uns gefüllt werden, indem wir jede Eigenschaft auflisten. Jede Methode nutzt zur Beschreibung der Leistungen so genannte Deskriptoren. Gibt es keinen Deskriptor, wird die jeweilige Eigenschaft, Methode oder das Ereignis nicht veröffentlicht. Gibt die Anfrage-Methode aus der Bean-Information-Klasse null zurück, wird für die jeweilige Eigenschaft/Event/Methode Reflection genutzt. Der Java-Code für dieses Doktor-Spielchen liegt in java.beans.Introspection. Ein kleines Beispiel, das alle get-Methoden der Klasse Point ausgibt:

BeanInfo beanInfo = Introspector.getBeanInfo( Point.class );
for ( PropertyDescriptor p : beanInfo.getPropertyDescriptors() )
System.out.println( p.getReadMethod() );

Die Ausgabe liefert vier Zeilen:

public final native java.lang.Class java.lang.Object.getClass()
public java.awt.Point java.awt.Point.getLocation()
public double java.awt.Point.getX()
public double java.awt.Point.getY()

Beliebte Fehler

Das Programmieren von Beans ist zwar einfach, doch schleichen sich immer wieder Fehler ein. Dieser Abschnitt soll auflisten, welche Fehler für eine Bean letal sind, sodass sie von einer Umgebung nicht erkannt werden kann.

  • Die Klasse muss öffentlich (public) und darf nicht abstrakt sein. Abstrakte Klassen können keine Exemplare bilden, was für eine Bean-Box aber unumgänglich ist.
  • Die Klasse muss vom Klassenlader ohne Folgefehler geladen werden können. Der Vorgang wird während des Ladens durch Fehler im static-Block oder durch fehlende Klassen möglicherweise abgebrochen.
  • Die Bean muss einen öffentlichen Standard-Konstruktor anbieten.
  • Die Klasse muss Serializable implementieren, muss also serialisierbar sein. Das hat zur Konsequenz, dass alle Attribute, die nicht transient sind, ebenso serialisierbar sein müssen. Eine Verletzung dieser Regel liegt beispielsweise vor, wenn eine grafische Komponente eine Referenz auf ein Image-Objekt hält, denn Image-Objekte sind nicht serialisierbar. Wir müssen daher entweder ImageIcon nutzen oder eigene Methoden wie writeObject(), readObject() implementieren. Wenn schon eine Oberklasse serialisierbar ist, dann muss unsere Klasse nicht noch einmal Serializable implementieren, weil wir dann selbst automatisch instanceof Serializable durch die Oberklasse sind. Es bietet sich jedoch zum Zweck der Lesbarkeit an, implements Serializable zu schreiben, damit das auf den ersten Blick sichtbar ist.

Was sind Markierungsschnittstellen (Marker-Interfaces)?

Markierungsschnittstellen kann man als Design-Pattern ansehen. Sie tauchen zwar nicht so häufig auf, doch Java kennt mit Serializable und Remote zwei prominente Vertreter.

Es anderes Beispiel soll hergestellt werden: Eine Textverarbeitung speichert in erster Linie Text. Dazu kommen noch weitere Datentypen wie Tabellen, Grafiken oder Formeln, aber wie der Name Textverarbeitung zeigt, ist der Datentyp im Kern Text. Etwas genauer betrachtet besteht der Text aus Seiten, Absätzen, Zeilen und Zeichen.

Frage: Nehmen wir an, dass die Textverarbeitung für jedes Zeichen ein Objekt vorsieht, welches in einer langen verketteten Liste gespeichert ist. Beim Eintippen neuer Buchstaben werden neue Objekte in die Liste eingefügt und beim Löschen entfernt. Stellt die Textverarbeitung eine Seite dar, wird die Liste sequentiell abgelaufen und jedes Element auf dem Bildschirm ausgegeben. Jedes ausgegebene Zeichen bekommt eine Position und diese wird laufend angepasst.

Zu den normalen druckbaren Zeichen wie Buchstaben und Ziffern kommen Sonderzeichen hinzu. Ein Objektmodell kann dann so aussehen, dass eine Schnittstelle notwendiges für alle Zeichen vorschreibt. Nennen wir die Schnittstelle WordCharacter und geben wir ihr die Methoden getChar(), getCharWidth(), getCharHeight() und drawChar(). Als Unterklassen könnte dann OrdinaryCharacter die druckbaren Zeichen behandeln und MetaCharacter die Sonderzeichen wie Tabulator, Leerzeichen, Return. Läuft ein Algorithmus zur Visualisierung durch die Datenstruktur, besorgt er sich jedes Zeichen, fragt wie lang es ist und ob es mit dem Wort noch in die Zeile passt. Wenn ja, wird die Zeichenposition auf der x-Achse um die Breite des Zeichens erweitert und das nächste Zeichen wird ausgegeben.

Ein Leerzeichen nimmt unter den Sonderzeichen eine besondere Stelle ein. Das liegt auch daran, dass der Leerraum unter Umständen noch erweitert wird, etwa beim Blocksatz. Dann kommt zu jeder Breite eines Leerzeichens noch zusätzlicher Weißraum dazu.

Wie wird die Textverarbeitung normale Zeichen und Leerzeichen verarbeiten, wenn eine Zeile ausgeben wird? Da Leerzeichen nichts auf dem Bildschirm ausgehen, sollen sie besonders behandelt werden.

Eine Lösung: Wenn in der Datenstruktur die Zeichen der Reihe nach vorliegen, wird man in einer Fallunterscheidung feststellen können, ob es sich um ein Leerzeichen handelt oder um anderes Zeichen.

für alle Zeichen einer Zeile
  Character c ist das aktuelle Element
  ist c.getChar() != ' '
    dann verarbeite das Element weiter

Problem: Falls es nun weitere Elemente gibt, die eine ähnliche Sonderbehandlung haben wie das Leerzeichen, wird die Fallunterscheidung immer länger werden. Das wird sie auch, denn es gibt neben dem Standard-Leerzeichen noch weitere Zeichen, die für die Weißraum zuständig sind. Dazu zählt der Tabulator oder das nicht-trennende Leerzeichen. Wenn diese Fallunterscheidung auch noch an anderen Stellen eingesetzt wird, etwa beim Export der Daten in HTML, PDF, müssen die Entwickler darauf achten, Änderungen an der if-Anweisung auch konsistent an den anderen Stellen anzuwenden.

Pattern: Unsere bisherige Unterscheidung war nur auf Grund des Zeichencodes. Interessant ist, wenn das Leerzeichen noch einen weiteren Typ besitzt, denn wenn ein Weißraum-Objekt etwa den Typ WhitespaceChracter haben würde, ließe sich wiederum instanceof einsetzen, um herauszufinden, ob ein Zeichen-Objekt ein Leerzeichen ist oder nicht. Eine Fallunterscheidung mit vielen if-Anfragen fäll raus und es bleibt eine einfache instanceof-Abfrage. Alle Weißraum-Objekte legen wir dann so aus, dass sie einen speziellen Typ haben. Dazu kann ein Leerzeichen-Objekt eine Schnittstelle implementieren, die keine Funktionen vorschreibt.

Eine leere Schnittstelle nennt sich Marker-Interface oder Tag-Interface. Sie markiert ein Objekt mit einem Typ, der anschließend mit instanceof überprüft werden kann. In Java. Es gibt ein Java Marker-Interfaces an einigen Stellen. Die interessanteste Schnittstelle ist java.io.Serializable. Sie markiert alle Objekte, die über die Standardserialisierung automatisch geschrieben werden können. Eine Klasse, die nicht Serializabe ist, die aber trotzdem geschrieben werden soll, wird bei dem Serialisierungs-Vorgang einen Ausnahme melden.

Ein zweites Beispiel ist die Schnittstelle Remote. Sie wird benötigt, wenn Objekte über RMI verteilt angeboten werden sollen. Nur Objekte, die Remote implementieren werden RMI-fähig sein. Eine weitere Schnittstelle ist Clonable. Sie ermöglichst erst das Klonen von Objekten mit einer clone()-Funktion.

Seit Java 5 sind Annotationen eine Alternative zu Markierungsschnittstellen und daher seltener zu finden.

Das oberste Stack-Element duplizieren

Die Klasse Stack besitzt zwar die Basisfunktionalität, die ein Stapel besitzen sollte, aber auch nicht mehr. Hin und wieder wünschen wir uns aber eine Funktion, die das oberste Stack-Element dupliziert, kurz dup(). Allerdings ist das schwerer als gedacht, und das erklärt auch, warum es so eine Methode nicht gibt.

Bei der Implementierung treten zwei Fragen auf, mit denen zwei völlig unterschiedliche Lösungsansätze verbunden sind. Da die Klasse Stack wie die anderen Datenstrukturen auf Objekte ausgelegt ist, müssen wir uns darüber Klarheit verschaffen, wie das obere Objekt dupliziert werden soll. Soll eine Kopie der Objekt-Referenz neu auf den Stapel gelegt werden oder etwa das gesamte Objekt geklont werden?

Die einfache Lösung

Die einfachste Lösung besteht darin, das oberste Objekt einfach mittels der schon vorhandenen Stack-Methoden push() und peek() draufzulegen. Nehmen wir an, wir haben eine Unterklasse DupStack, dann sieht die erste Variante zum Clonen so aus:

void dup() /* throws EmptyStackException */
{
  push( peek() );
}

peek() gibt aber lediglich eine Referenz auf das Objekt zurück. Und das anschließende push() speichert diese Referenz dann auf dem Stapel. Nehmen wir an, wir haben zwei StringBuffer-Objekte auf dem Stapel. Wenn wir nun dup() aufrufen und den String ändern, der oben auf dem Stapel liegt, so ändern wir automatisch das zweite Element gleich mit. Dies ist aber nicht unbedingt beabsichtigt, und wir müssen uns Gedanken über eine alternative Lösung machen. Wir sehen, dass dup() in der Klasse Stack fehlt, weil seine Implementierung davon abhängt, ob eine Referenz- oder eine Wertsemantik für Kellerelemente gewünscht ist.

Die kompliziertere Lösung mit Klonen

Um das oberste Stack-Element zu kopieren, bietet sich die clone()-Methode von Object an. All die Objekte, die sich klonen lassen, und das sind längst nicht alle, implementieren das Interface Cloneable. Nun ließe sich einfach folgern: Wenn das zu duplizierende Objekt ein Exemplar von Cloneable ist, dann können wir einfach die clone()-Methoden aufrufen und das zurückgegebene Objekt mittels push() auf den Stapel bringen.

void dup2() throws CloneNotSupportedException
{
  try
  {
    Object top = peek();

    if ( top instanceof Cloneable )
      push( top.clone() );

  }
  catch ( EmptyStackException e ) { }
}

Beziehungsweise

void dup3() throws CloneNotSupportedException /*, EmptyStackException */
{
  push( peek().clone() );
}

Dies funktioniert für die meisten Objekte, allerdings nicht für Objekte der Klasse Object. Denn clone() der Klasse Object ist protected – wir dürfen also von außen nicht dran, nur eine Unterklasse und die Klasse selbst. Hier haben wir also zwei Probleme.

  • Leider lässt sich nur mit Aufwand überprüfen, ob das Objekt auf dem Stapel auch wirklich ein pures Object ist, denn alle Objekte sind instanceof Object. Glücklicherweise gibt es kaum eine Anwendung, wo reine Object-Elemente gesichert werden müssen.
  • Was machen wir mit Objekten, die nicht klonbar sind? Leider gibt es für diese Frage keine direkte Antwort. Eine universelle Stack-Klasse mit einer uneingeschränkten dup()-Methode gibt es nicht. Wir müssen als Stack-Benutzer festlegen, dass das oberste Element Cloneable ist, um zumindest eine eigene Implementierung nutzen zu können. Oder wir bleiben dabei, bei nicht klonbaren Objekten doch nur die Referenz zu duplizieren. Das wäre zumindest für eineindeutige Objekte mit Wertsemantik die ideale Lösung.

True Type Fonts in Swing/AWT-Anwendungen

Grafische Oberflächen stellen selbstverständlich wie Drucker Zeichensätze dar. Doch der Weg von der Datei bis zur Darstellung ist lang und führt unweigerlich über die Firma Adobe, die erstmalig die standardisierte Zeichendefinition PostScript öffentlich machte. Genauer gesagt, definiert PostScript noch etwas mehr, doch das soll uns hier nicht interessieren. Die erste kommerzielle Zeichensatzrevolution begann 1985, als der Drucker LaserWriter von Apple das Adobe-Format PostScript rastern konnte. Die Definition eines Zeichensatzes lag bis zu dieser Zeit nur in Bitmaps vor, doch die PostScript-Zeichensätze wie auch die TrueType-Zeichensätze, um die es später gehen soll, lagen als Punktbeschreibung vor. Die Rasterung übersetzte diese Punkte in eine Bitmap, die dann entweder auf dem Bildschirm oder auf dem Drucker ausgegeben wurde. Durch die Punktbeschreibung waren also nicht mehr größenabhängige Beschreibungen vorhanden, sondern die Zeichen (auch Glyphs genannt) wurden durch Linien und Kurven in kubischen Bézier-Kurven beschrieben.

Die Visualisierung der Zeichensätze machte Microsoft und Apple Sorgen, weil Adobe mehrere Definitionen der PostScript-Zeichensätze pflegte, darunter Type 1 (PS-1) und Type 3 (PS-3). Type 1 nutzt so genannte Hints (Hinweise), um auch bei unterschiedlichen Größen und grafischen Oberflächen optimale Darstellungen zuzulassen. Diese Definition war jedoch geheim. Zeichensätze des Type 3 sehen zwar auf dem Papier gut aus, nicht aber auf dem Bildschirm mit niedriger Auflösung – hier fehlen die Informationen aus den Hints. Microsoft und Apple wollten nun ihre Zeichensatzausgabe nicht der Firma Adobe überlassen (die natürlich einen Type-1-Rasterer im Programm hatte), sondern definierten ihre eigene Font-Technologie, die nicht mehr auf Bézier-Kurven, sondern auf quadratischen B-Splines basierte. Apple machte dabei den Anfang mit Royal, welches später in TrueType (TT) umgetauft wurde. Dies war sechs Jahre nach den PostScript Fonts. Der einzige Hersteller, der dennoch bei PostScript-Type 1-Zeichensätzen geblieben ist, ist IBM mit dem Betriebssystem OS/2. Daneben nutzte auch NeXtStep diese Zeichensatzdefinitionen, doch das System hallte nicht lange nach.

Nachdem Apple den Anfang mit TT gemacht hatte und es 1991 in MacOS integrierte, übernahm auch Microsoft, wo ein bis dahin wenig lauffähiger PostScript-Clone (»TrueImage«) zum Einsatz gekommen war, die Technologie für Windows 3.1. Adobe erkannte früh die Konsequenz dieser Allianz und öffnete die Spezifikation für PostScript-Type-1-Zeichensätze im März 1990. Mitte des Jahres lieferte Adobe zusätzlich den Adobe Type Manager (ATM) aus, der Type-1-(aber keine Type-3-)PostScript-Zeichensätze für den Bildschirm und für nicht PostScript-fähige Drucker darstellte. Heutzutage existieren beide Definitionen immer noch parallel, und für Drucker ist die Frage, welche nun besser ist, nicht zu beantworten. Moderne Drucker haben auch ein eigenes TrueType-Raster im ROM eingebaut. In Zukunft wird die Unterscheidung wohl auch unwichtiger werden, da Microsoft die »offene« OpenType-Spezifikation (auch »TrueType Open Version 2« genannt) nach vorne bringt. Der Zeichensatz PS-1 oder TrueType wird hier in einer OpenType-Datei gekapselt und dem Rasterer übergeben und berechnet. Dabei übernimmt Adobe, wo eine Zusammenarbeit mit Microsoft unterstützt wird, die PS-1-Rasterung, und Microsoft die TT-Rasterung. In Zukunft möchten Microsoft und Adobe Zeichensätze im OpenType unterstützen und deren Verbreitung fördern.

TTF in Java nutzen

Eine Einschränkung mit den gegeben vordefinierten Standard-Zeichensätzen (Dialog, DialogInput, Monospaced, Serif, SansSerif, Symbol) ist, dass dies zu wenig sind. Doch die Font Kasse bietet die statisch Methode createFont() an, die zu einen Eingabestrom auf ein TrueType Zeichensatz das entsprechende Font-Objekt zurückgibt.

Font f = Font.createFont( Font.TRUETYPE_FONT,
new FileInputStream("f.ttf") );

Der erste Parameter ist die fest vorgeschriebene Konstante Font.TRUETYPE_FONT, andere Parameter sind nicht definiert und führen zu einer IllegalArgumentException("font format not recognized"). Der zweite Parameter ist ein Eingabestrom zu der Binärdatei mit den Zeichensatzinformationen. Die Daten werden ausgelesen und zu einem Font Objekt verarbeitet. Da die Daten intern über einen gepufferten Datenstrom in eine temporäre Datei geschrieben wird, ist eine eigene Pufferung über einen BufferedInputStream nur doppelter Overhead. Waren die Beschreibungsinformationen in der Datei ungültig, so erzeugt die Fontklasse eine FontFormatException("Unable to create font – bad font data"). Dateifehler fallen hier nicht drunter und werden extra über eine IOException angezeigt. Der Datenstrom wird anschließend nicht wieder geschlossen.

Wir wundern uns vielleicht an dieser Stelle, dass die Methode createFont() von der Arbeitsweise mit dem Konstruktor ähnlich sein müsste, aber der Parameterliste die Attribute fehlen. Das liegt daran, dass die Methode automatisch einen Zeichensatz der Größe 1 im Stil PLAIN erzeugt. Um daher einen größeren Zeichensatz zu erzeugen, müssen wie ein zweites Font Objekt anlegen. Dies geschieht am einfachsten mit der Methode deriveFont().

font = f.deriveFont( 20f );

Der Parameter ist ein float und kein double.

AWTEventMulticaster

Löst die Bean AWT-Ereignisse aus, kann sie dafür java.awt.AWTEventMulticaster nutzen. Diese Klasse ist für effizientes Multicast-Benachrichtigen bei AWT-Ereignissen gedacht. Genau genommen verbindet sie dazu nur zwei EventListener miteinander, sodass eine Verkettung entsteht.

protected ActionListener listeners;
public void addActionListener( ActionListener l ) {
  listeners = AWTEventMulticaster.add( l, listeners );
}
public void removeActionListener( ActionListener l ) {
  listeners = AWTEventMulticaster.remove( l, listeners );
}

Da AWTEventMulticaster alle möglichen AWT-Listener implementiert, können wir die Methode actionPerformed(), die die Schnittstelle ActionListener vorschreibt, aufrufen. Wenn wir ACTION_PERFORMED-Nachrichten damit generieren, schreiben wir Folgendes in unsere Bean-Klasse:

protected void fireActionEvent () {
  if ( listeners != null )
    listeners.actionPerformed( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, null) );
}

Die angemeldeten Listener bekommen auf diese Weise ein ActionEvent geliefert. Der letzte Parameter im Konstruktor, der hier mit null belegt ist, kann zusätzlich eine Referenz übermitteln.

class java.awt.event.AWTEventMulticaster

implements ComponentListener, ContainerListener, FocusListener, KeyListener, MouseListener, MouseMotionListener, WindowListener, ActionListener, ItemListener, AdjustmentListener, TextListener, InputMethodListener, HierarchyListener, HierarchyBoundsListener

  • static ActionListener add( ActionListener a, ActionListener b )

    Verbindet Listener a und b und liefert ein neues ActionListener-Objekt zurück.
  • static ActionListener remove( ActionListener l, ActionListener oldl )

    Entfernt Listener oldl von l und liefert den neuen Multicast-Listener zurück.

class java.awt.event.ActionEvent

extends AWTEvent

  • ActionEvent( Object source, int id, String command )

    Erzeugt ein ActionEvent mit einer Quelle, die das Ereignis ausgelöst hat, einem Identifizierer und einem Kommando.

Oracles Sicherheits-Check für signierte Java-Applets nicht sicher

Zitat: Heise Online:

„Oracles Sicherheits-Check für Java-Applets lässt sich umgehen, weil die geforderte Signatur wichtige Informationen nicht schützt. Zwar erhalten Java-Benutzer seit einiger Zeit eine Warnmeldung, wenn sie unsignierten Java-Code durch ein Browser-Plug-in ausführen, und bei signierten Applets wird der Benutzer gefragt, ob er der Quelle auch vertraut. Die angezeigten Informationen lassen sich aber gezielt manipulieren. So wird ein Szenario denkbar, bei dem Benutzern ein Applet untergeschoben wird, mit dem sie ausspioniert werden.“

weiterlesen…

Mit dem MediaTracker arbeiten

Der MediaTracker ist eine sehr alte Java-AWT-Klasse, bietet aber Dinge, über die auch einfachere Bildlademethoden nicht verfügen. Ein paar Details.

Um ein MediaTracker-Objekt zu erzeugen, rufen wir seinen Konstruktor mit einem einzigen Parameter vom Typ Component auf:

MediaTracker tracker = new MediaTracker( this );

Wenn wir Applet oder Frame erweitern, kann dies – so wie im Beispiel – der this-Zeiger sein. Dies zeigt aber schon die Einschränkung der Klasse auf das Laden von Bildern, denn was hat eine Musik schon mit einer Komponente zu tun?

Bilder beobachten

Nachdem ein MediaTracker-Objekt erzeugt ist, fügt die addImage(Image)-Methode ein Bild in eine Warteliste ein. Eine weitere überladene Methode addImage(Image, Gruppe ID) erlaubt die Angabe einer Gruppe. Dieser Identifizierer entspricht gleichzeitig einer Priorität, in der die Bilder geholt werden. Gehören also Bilder zur gleichen Gruppe, ist die Priorität immer dieselbe. Bilder mit einer niedrigeren Gruppennummer werden mit einer niedrigeren Priorität geholt als Bilder mit einer höheren ID. Eine dritte Methode von addImage() erlaubt die Angabe einer Skalierungsgröße. Nach dieser wird das geladene Bild skaliert und eingefügt. Sehen wir uns einmal eine typische Programmsequenz an, die dem Medienüberwacher ein Hintergrundbild sowie einige animierte Bilder überreicht:

Image bg     = getImage( "background.gif" ),
      anim[] = new Image[MAX_ANIM];

MediaTracker tracker = new MediaTracker( this );
tracker.addImage( bg, 0 );

for ( int i = 0; i < MAX_ANIM; i++ ) {
 anim[i] = getImage( getDocumentBase(), "anim" + i + ".gif" );
 tracker.addImage( anim[i], 1 );
}

Das Hintergrundbild wird dem MediaTracker-Objekt hinzugefügt. Die ID, also die Gruppe, ist 0. Das Bild-Array anim[] wird genauso gefüllt und überwacht. Die ID des Felds ist 1. Also gehören alle Bilder dieser Animation zu einer weiteren Gruppe.

Um den Ladeprozess anzustoßen, benutzen wir eine der Methoden waitForAll() oder waitForID(). Die waitForID()-Methode wird benutzt, um Bilder mit einer bestimmten Gruppe zu laden. Die Gruppennummer muss natürlich dieselbe vergebene Nummer sein, die bei der addImage()-Methode verwendet wurde. Beide Methoden arbeiten synchron, bleiben also so lange in der Methode, bis alle Bilder geladen wurden oder ein Fehler beziehungsweise eine Unterbrechung auftrat. Da das also das ganze restliche Programm blockieren würde, werden diese Ladeoperationen gerne in Threads gesetzt. Wie diese Methoden in einem Thread verwendet werden, zeigt das folgende Programmsegment. Der Block ist idealerweise in einer run()-Methode platziert oder, bei einem Applet, in der init()-Methode.

try {
  tracker.waitForID( 0 );
  tracker.waitForID( 1 );
}
catch ( InterruptedException e ) { return; }

Die waitForID()-Methode wirft einen Fehler aus, falls sie beim Ladevorgang unterbrochen wurde. Daher müssen wir unsere Operationen in einen try- und catch-Block setzen.

Während das Bild geladen wird, können wir seinen Ladezustand mit den Methoden checkID()überprüfen. checkID() bekommt als ersten Parameter eine Gruppe zugeordnet und überprüft dann, ob die Bilder, die mit der Gruppe verbunden sind, geladen wurden. Wenn ja, gibt die Methode true zurück, auch dann, wenn der Prozess fehlerhaft ist oder abgebrochen wurde. Ist der Ladeprozess noch nicht gestartet, dann veranlasst checkID(Gruppe) dies nicht. Um dieses Verhalten zu steuern, regt die überladene Funktion checkID(Gruppe, true) das Laden an. Beide geben false zurück, falls der Ladeprozess noch nicht beendet ist.

Eine weitere Überprüfungsfunktion ist checkAll(). Diese arbeitet wie checkID(), nur, dass sie auf alle Bilder in allen Gruppen achtet und nicht auf die ID angewiesen ist. Wie checkID() gibt escheckAll() ebenfalls in zwei Varianten. Die zweite startet den Ladeprozess, falls die Bilder noch nicht geladen wurden.

Die MediaTracker-Klasse verfügt über vier Konstanten, die verschiedene Flags vertreten, um den Status des Objekts zu erfragen. Einige der Methoden geben diese Konstanten ebenso zurück.

Konstante

Bedeutung

LOADING

Ein Medien-Objekt wird gerade geladen

ABORTED

Das Laden eines Objekts wurde unterbrochen

ERRORED

Ein Fehler trat während des Ladens auf

COMPLETE

Das Medien-Objekt wurde erfolgreich geladen

Tabelle: Flags der Klasse MediaTracker

Mit der Methode statusID(), welche ja den Zustand des Ladens überwacht, können wir leicht die Fälle herausfinden, in denen das Bild erfolgreich beziehungsweise nicht erfolgreich geladen werden konnte. Dazu verknüpfen wir einfach durch den Und-Operator die Konstante mit dem Rückgabewert vonstatusAll() oder statusID():

if ( (tracker.statusAll() & MediaTracker.ERRORED) != 0 )

Wie wir sehen, können wir durch solche Zeilen leicht herausfinden, ob bestimmte Bilder schon geladen sind. MediaTracker.COMPLETE sagt uns "ja", und wenn ein Fehler auftritt, dann ist der Rückgabewert MediaTracker.ERRORED. Wir wollen diese Flags nun verwenden, um in einer paint()-Methode das Vorhandensein von Bildern zu überprüfen, und wenn möglich, diese dann anzuzeigen. Erinnern wir uns daran, dass in der Gruppe 0 ein Hintergrundbild lag und in Gruppe 1 die zu animierenden Bilder. Wenn ein Fehler auftritt, zeichnen wir ein rotes Rechteck auf die Zeichenfläche und signalisieren damit, dass etwas nicht funktioniert.

public void paint( Graphics g )
{
  if ( tracker.statusID(0, true) == MediaTracker.ERRORED )
  {
    g.setColor( Color.RED );
    g.fillRect( 0, 0, size().width, size().height );
    return;
  }
  g.drawImage( bg, 0, 0, this );
  if ( tracker.statusID(1) & MediaTracker.COMPLETE) )
    g.drawImage( anim[counter%MAX_ANIM], 50, 50, this );
}

class java.awt.MediaTracker

implements Serializable

  • static final int ABORTED

    Flag, welches anzeigt, dass das Medium nicht geladen werden konnte. Rückgabewert von statusAll() oder statusID().
  • static final int ERRORED

    Während des Ladens gab es Fehler. Rückgabewert von statusAll() und statusID().
  • static final int COMPLETE

    Medium konnte geladen werden. Rückgabewert von statusAll() und statusID().
  • MediaTracker( Component comp )

    Erzeugt einen MediaTracker auf einer Komponente, auf der das Bild möglicherweise angezeigt wird.
  • void addImage( Image image, int id )

    Fügt ein Bild nichtskaliert der Ladeliste hinzu. Ruft addImage(image, id, -1, -1) auf.
  • void addImage( Image image, int id, int w, int h )

    Fügt ein skaliertes Bild der Ladeliste hinzu. Soll ein Bild in einer Richtung nicht skaliert werden, ist -1 einzutragen.
  • public boolean checkAll()

    Überprüft, ob alle vom MediaTracker überwachten Medien geladen worden sind. Falls der Ladeprozess noch nicht angestoßen wurde, wird dieser auch nicht initiiert.
  • boolean checkAll( boolean load )

    Überprüft, ob alle vom MediaTracker überwachten Medien geladen worden sind. Falls der Ladeprozess noch nicht angestoßen wurde, wird dieser dazu angeregt.
  • boolean isErrorAny()

    true, wenn eines der überwachten Bilder einen Fehler beim Laden verursachte.
  • Object[] getErrorsAny()

    Liefert eine Liste aller Objekte, die einen Fehler aufweisen. null, wenn alle korrekt geladen wurden.
  • void waitForAll() throws InterruptedException

    Das Laden aller vom MediaTracker überwachten Bilder wird angestoßen, und es wird so lange gewartet, bis alles geladen wurde oder ein Fehler beim Laden oder Skalieren auftritt.
  • boolean waitForAll( long ms ) throws InterruptedException

    Startet den Ladeprozess. Die Funktion kehrt erst dann zurück, wenn alle Bilder geladen wurden oder die Zeit überschritten wurde. true, wenn alle korrekt geladen wurden.
  • int statusAll( boolean load )

    Liefert einen mit Oder verknüpften Wert der Flags LOADING, ABORTED, ERRORED und COMPLETE. Der Ladeprozess wird bei load auf true gestartet.
  • boolean checkID( int id )

    Überprüft, ob alle Bilder, die mit der ID id verbunden sind, geladen wurden. Der Ladeprozess wird mit dieser Methode nicht angestoßen. Liefert true, wenn alle Bilder geladen sind oder ein Fehler auftrat.
  • boolean checkID( int id, boolean load )

    Wie checkID(id). Allerdings werden nur die Bilder geladen, die bisher noch nicht geladen wurden.
  • boolean isErrorID( int id )

    Liefert den Fehlerstatus von allen Bildern mit der ID id. true, wenn eines der Bilder beim Laden einen Fehler aufweist.
  • Object[] getErrorsID( int id )

    Liefert eine Liste aller Medien, die einen Fehler aufweisen.
  • void waitForID( int id ) throws InterruptedException

    Startet den Ladeprozess für die gegebene ID. Die Methode wartet solange, bis alle Bilder geladen sind. Bei einem Fehler oder Abbruch wird angenommen, dass alle Bilder ordentlich geladen wurden.
  • boolean waitForID( int id, long ms ) throws InterruptedException

    Wie waitForID(), nur stoppt der Ladeprozess nach einer festen Anzahl von Millisekunden.
  • int statusID( int id, boolean load )

    Liefert einen mit Oder verknüpften Wert der Flags LOADING, ABORTED, ERRORED und COMPLETE. Ein noch nicht geladenes Bild hat den Status 0. Ist load gleich true, dann werden die Bilder geladen, die bisher nocht nicht geladen wurden.
  • void removeImage( Image image )

    Entfernt ein Bild von der Liste der Medienelemente. Dabei werden alle Objekte, die sich nur in der Skalierung unterscheiden, entfernt.
  • public void removeImage( Image image, int id )

    Entfernt das Bild mit der ID id von der Liste der Medienelemente. Dabei werden auch die Objekte entfernt, bei denen sich die Bilder nur in der Skalierung unterscheiden.
  • public void removeImage( Image image, int id, int width, int height )

    Entfernt ein Bild mit den vorgegebenen Ausmaßen und der ID id von der Liste der Medienelemente. Doppelte Elemente werden ebenso gelöscht.

Alles wird bunt mit Farbmodellen

Ein besonderer Produzent, der sich um alles kümmert, was das Bilderzeugen angeht, ist der Image Producer. Im Gegensatz dazu sind es die Image Consumer, die etwaige Bilddaten benutzen. Bei den Produzenten- und Konsumentenmodell von Image-Objekten stehen die Daten der Pixel immer in einem Byte- oder Integer-Feld zur Verfügung und stehen im Mittelpunkt des Interesses. Oft übersprungen wird das Farbmodell bei MemoryImageSource und einem createImage(). Doch das wollen wir uns nun genauer anschauen.

Die Einträge der Felder sind Pixel, und die Werte standen für Farbinformationen, genauer gesagt für Rot, Grün und Blau. Stillschweigend wird angenommen, dass diese in 24 Bit abgelegt sein müssen. Dies muss jedoch nicht so sein, und die Interpretation der Farbwerte in einem Informationswort bestimmt ein Farbmodell. Für Farbmodelle gibt es in Java die Klasse ColorModel. Mit der Klasse lassen sich dann aus einem Pixel die roten, grünen, blauen und transparenten Anteile bestimmen. Der transparente Teil, auch Alpha-Komponente genannt, bestimmt, in welcher Intensität die Farbinformationen wirken. Alpha-Werte lassen sich nur in Zusammenhang mit Bildern anwenden. Mit der Graphics-Klasse lässt sich ein Alpha-Wert nicht einstellen, der dann alte Zeichenoperationen beeinflusst. Bei den Farbmodellen ist der Anteil der Transparenz genauso lang wie ein Farbwert, nämlich acht Bit. Ein Wert von 255 sagt aus, dass der Farbwert zu 100% sichtbar ist. Ist der Wert 0, so ist die Farbe nicht zu sehen.

Java macht das Programmierleben so plattformunabhängig wie möglich. Bei wenigen oder vielen Farben auf der Zielplattform wird eine optimale Annäherung an unsere Wunschfarben errechnet. So können wir alles in 24 Bit Farbtiefe errechnen, die Dislay-Komponente sucht die wichtigsten Farben heraus und fasst Gruppen ähnlicher Farben zusammen.

Die abstrakte Klasse ColorModel

Die abstrakte Klasse ColorModel beschreibt alle Methoden für konkrete Farbklassen, sodass die Informationen über die Farbwerte und die Transparenz erreichbar sind. Obwohl die Klasse abstrakt ist, besitzt sie zwei Konstruktoren, die von den Unterklassen benutzt werden. Direkte Unterklassen sind ComponentColorModel, IndexColorModel und PackedColorModel.

abstract class java.awt.image.ColorModel
implements Transparency

  • ColorModel( int pixel_bits, int bits[], ColorSpace cspace, boolean hasAlpha, boolean isAlphaPremultiplied, int transparency, int transferType )
  • ColorModel(int bits)

Der zweite Konstruktor ist praktisch, da dieser nur die Farbtiefe in Bits erwartet. Diese abstrakte Klasse besitzt jedoch die statische Fabrik-Methode getRGBdefault(), die ein ColorModel-Objekt zurückliefert. Das Standardfarbmodell, auch sRGB genannt, ist ein Farbmodell, welches die Werte als 24-Bit-Tupel mit den Komponenten Alpha, Rot, Grün und Blau enthält. Dieses Farbmodell lässt sich etwa für ein Memory-Image einsetzen. Der erste Konstruktor ist noch leistungsfähiger und seit Java 1.2 dabei. Mit seiner Hilfe muss ein Farbwert nicht zwingend in einem Integer kodiert sein.

Die Methode getPixelSize() liefert die Farbtiefe eines Farbmodells. Das Standardmodell besitzt eine Tiefe von 32 Bit (24 für die Farben und dann noch den Alpha-Kanal). So gibt auch die folgende Zeile als Anwort auf die Frage nach der Anzahl der Farben im Standardmodell 32 Bit aus:

System.out.println( ColorModel.getRGBdefault().getPixelSize() );

Die Hauptaufgabe einer Farbmodell-Klasse ist die Auswertung der Farbinformationen aus einem Speicherwort. Mit drei Methoden lassen sich die verschiedenen Farben auslesen. getRed(int pixel), getGreen(int pixel) und getBlue(int pixel), hinzu kommt noch getAlpha(int pixel). Jede dieser Methoden ist abstrakt und liefert eine Ganzzahl mit dem Farbwert zurück. Wie wir später sehen werden, ist das einfachste Modell genau jenes, das wir bisher immer benutzt haben. Dieses liest nämlich genau von den Stellen 24, 16 und 8 die Farbwerte aus. Da die Methoden abstrakt sind, müssen Unterklassen dieses Verhalten programmieren.

Eine weitere Methode ist getRGB(), welche ein int mit allen Farben im entsprechenden Farbformat zurückliefert. Die Implementierung basiert auf den Anfrage-Methoden.

public int getRGB(int pixel) {
  return (getAlpha(pixel) << 24) | (getRed(pixel) << 16) | (getGreen(pixel) << 8) | (getBlue(pixel) << 0);
}

Im Folgenden eine Auflistung der wichtigsten Methoden:

abstract class java.awt.image.ColorModel

implements Transparency

  • abstract int getAlpha( int pixel )

    Liefert den Alpha-Wert im Bereich 0 bis 255.
  • abstract int getBlue( int pixel )

    Liefert den Blauanteil des Pixels.
  • ColorSpace getColorSpace()

    Liefert den Farbraum, der mit dem ColorModel verbunden ist.
  • int[] getComponents( int pixel, int components[], int offset )

    Liefert ein Feld mit nicht normalisierter Farb- und Alpha-Komponente für ein Pixel.
  • abstract int getGreen( int pixel )

    Liefert den Grünanteil.
  • int getNumColorComponents()

    Gibt die Anzahl der Farben zurück.
  • int getNumComponents()

    Liefert die Anzahl der Komponenten (mit Alpha).
  • int getPixelSize()

    Wie viele Pixel beschreiben eine Farbe?
  • abstract int getRed( int pixel )

    Liefert den Rotanteil.
  • int getRGB( int pixel )

    Gibt Farb- und Alpha-Komponente des Pixels im sRGB-Farbmodell wieder.
  • static ColorModel getRGBdefault()

    Liefert ein DirectColorModel mit dem sRGB-Modell.
  • int getTransparency()

    Liefert die Art der Transparenz. Dies ist entweder OPAQUE, BITMASK oder TRANSLUCENT. Es sind Konstanten aus der Schnittstelle Transparency. Sie können aber auch über ColorModel verwendet werden, da ColorModel diese Schnittstelle implementiert.
  • boolean hasAlpha()

    Fragt an, ob das Farbmodell Transparenz unterstützt.
  • boolean isCompatibleRaster( Raster raster )

    Liefert true, falls das Raster mit dem Farbmodell kompatibel ist.

Nun lassen sich auf der Basis dieser Klassen verschiedene Farbmodelle entwerfen. Einige sind von den Entwicklern der Java-Bibliotheken schon vorgefertigt, wie etwa eine Farbklasse, die die Informationen gleich im Pixel selbst speichert, wie im Beispiel RGB, oder eine Klasse, die einen Index auf einen Farbwert verwaltet. Als eigene Ergänzung können wir Farbklassen implementieren, die Graustufen direkt unterstützen oder etwa andere Farbräume wie HSB (Hue, Saturation, Brightness). Die einzige Aufgabe, die uns als Implementierer der abstrakten Methoden übrig bleibt, ist, die Farbwerte aus dem Pixelwert zu extrahieren. Im Fall von HSB ist das einfach. Die Methoden getRed(), getGreen() und getBlue() müssen nur aus dem internen HSB-Wert den Anteil liefern.

Farbwerte im Pixel mit der Klasse DirectColorModel

Mit Hilfe der Klasse DirectColorModel werden die Farbwerte Rot, Grün, Blau und Alpha direkt aus dem Farbtupel extrahiert. Die Klasse gehört zu einer der größten im Image-Paket. Als Beispiel für das direkte Format kennen wir Standard-RGB. Für dieses gilt, dass die Farben jeweils acht Bit in Anspruch nehmen. Das muss aber nicht so sein, und im Konstruktor von DirectColorModel lässt sich bestimmen, wie und an welcher Stelle die Bits für die Farben sitzen. Wir dürfen dies jedoch nicht damit verwechseln, dass wir die Anzahl der Bits angeben. Nur die Positionen sind möglich. Daraus ergibt sich auch, dass die Werte zusammenhängend sind und nicht etwa Folgendes auftreten kann: 0xrrbgbg. Die Bitanzahl kann aber für die Farben unterschiedlich sein. Auch der Alpha-Wert kann frei gewählt werden. Für das Standardmodell ergibt sich eine einfache Zeile:

DirectColorModel rgbModel = new DirectColorModel(32,
0xff0000, 0x00ff00, 0x0000ff, 0xff000000);

Ist das Objekt einmal angelegt, so sind nun die Anfrage-Methoden wie getRed() möglich, da DirectColorModel als konkrete Klasse, von der auch ein Exemplar erzeugt werden kann, diese abstrakten Methoden alle überschreibt und mit Implementierung versieht. Eine wichtige Eigenschaft dieser Methoden ist, dass sie final sind und ihren Farbwert mit dem Alpha-Wert kombinieren. Da sie final sind, können sie von Unterklassen nicht mehr überschrieben werden. Letzteres verlangt aber die aktuelle Implementierung der AWT-Bibliothek.

Beispiel Implementierung von getRed()

final public int getRed(int pixel) {
  int r = ((pixel & maskArray[0]) >>> maskOffsets[0]);
  if (scaleFactors[0] != 1.)
    r = (int)(r * scaleFactors[0]);
  if (isAlphaPremultiplied) {
    int a = getAlpha(pixel);
    r = (a == 0) ? 0 : (r * 255/a);
  }
  return r;
}

Im Parameter pixel ist die Farbe Rot an einer Bitposition (meistens ab 24 Bit) abgelegt. Damit wir diesen Wert auslesen und mit dem Alpha-Wert kombinieren können, muss er zunächst ausmaskiert werden. Daher wird pixel mit der Maske verknüpft, sodass nur die Bits übrig bleiben, die auch wirklich die Farbe Rot beschreiben. Anschließend verschieben wir die Rot-Pixel so weit nach rechts, dass die Grün- und Blau-Werte verschwinden. Die Felder maskArray und maskOffsets sowie scaleFactors sind in der direkten abstrakten Oberklasse PackedColorModel angelegt. Doch bleiben wir bei getRed(). Hier sehen wir noch deutlich, wie der Alpha-Wert in die Berechnung mit eingeht. Ist der Farbwert 0, so ist auch das Ergebnis 0. Ist er ungleich 0, so wird die Farbe nach dem Apha-Wert gewichtet. Der Skalierungsfaktor skaliert die Werte auf 256. Denn haben wir beispielsweise nur zwei Bits für einen Farbwert, dann müssen wir mit 128 multiplizieren, um wieder eine Acht-Bit-Darstellung zu bekommen.

Die Klasse IndexColorModel

Im Gegensatz zur Klasse DirectColorModel verwaltet ein IndexColorModel die Farben und Transparenzen nicht im Pixel, sondern in einer eigenen Tabelle, die auch Color-Map oder Palette genannt wird. Das Modell ist vergleichbar mit dem Dateiformat GIF. Dort stehen maximal 256 Farben in einer Tabelle zur Verfügung und alle Punkte in einem GIF-Bild müssen einer dieser Farben entsprechen. Eine GIF-Datei mit zwei Farben definiert etwa eine Farbe mit schweinchenrosa und eine zweite Farbe mit hornhautumbra. Der Pixel selbst ist dann nur ein Index auf einen Eintrag. Dieses Verfahren ist sehr speicherschonend, ein Kriterium, das vor ein paar Jahrzehnten noch zählte. An Stelle von 24 Bit für einen Pixel wird der Index etwa zehn Bit breit gemacht und stellt dann bis zu 1.024 Farben dar. Das ist immerhin eine Reduktion des Bildschirmspeichers um die Hälfte. Leider sind damit aber auch hohe Berechnungskosten verbunden. Für eine Verwendung dieser Klasse spricht die Abstraktion von den konkreten Farben. Ein Beispiel dafür wäre ein Fraktalprogramm. Einer berechneten Zahl wird direkt ein Farbwert zugeordnet. Somit lässt sich leicht eine Farbverschiebung programmieren, die sich auf Englisch color-cycle nennt.

Wenn wir ein IndexColorModel verwenden wollen, geben wir im Konstruktor eine Anzahl Bits pro Pixel zusammen mit einer Tabelle an, die die Komponenten Rot, Grün und Blau sowie optional die Transparenzen enthält. Die Farbtabelle, die über einen Index die Farbe verrät, kann maximal 256 Farben aufnehmen. Dies ist leider eine Einschränkung, beschränkt aber den Speicher, da nur ein byte an Stelle eines short belegt wird.

class java.awt.image.IndexColorModel

extends ColorModel

  • IndexColorModel( int bits, int size,

    byte r[], byte g[], byte b[], byte a[] )
  • IndexColorModel( int bits,int size,

    byte r[], byte g[], byte b[], int trans )
  • IndexColorModel( int bits, int size

    byte r[], byte g[], byte b[] )
  • IndexColorModel( int bits, int size, byte cmap[],

    int start, boolean hasalpha, int trans )
  • IndexColorModel( int bits, int size, byte cmap[],

    int start, boolean hasalpha )
  • IndexColorModel( int bits, int size, int cmap[],

    int start,boolean hasalpha, int trans,

    int transferType )

An den Konstruktoren lässt sich ablesen, dass mehrere Wege gegangen werden können. Die Farben können als Einzelfelder einem IndexColorModel übergeben werden oder als zusammengepacktes Feld. Dann erfolgt die Speicherung nach dem Standard-RGB-Modell. Vorsicht ist bei einem Alpha-Wert geboten. Dieser folgt nach dem Blauton. So ist die Reihenfolge bei Transparenz 0xRRGGBBAA. Das ist sehr verwirrend, da wir es gewohnt sind, den Alpha-Wert vor dem Rotwert zu setzen.

Intern werden die Werte in einem Feld gehalten. Der erste Wert gibt die Anzahl der Bits an, die einen Pixel beschreiben. Er darf acht Bit nicht überschreiten, da die Längenbeschränkung 2^8 = 256 maximale Farben vorgibt. Der nächste Wert size ist die Größe der Tabelle. Sie sollte mindestens 2^bits groß sein. Andernfalls werden Farben fehlerhaft zugeordnet. Präziser heißt dies, dass sie Null sind, da ja der new-Operator das Feld automatisch mit Null-Werten belegt. Sind in der Farbtabelle Apha-Werte abgelegt, dann sollte hasalpha den Wert true annehmen. Sind alle Werte in einer Tabelle, berechnet sich der Farbwert zu einem Index wie folgt: Betrachten wir keinen Alpha-Wert und unser Pixel hat den Wert f(arbe),

  • dann ist der Rotwert an der Stelle colorMap[start+3*f] und
  • der Grünwert an der Stelle colorMap[start+3*f+1] und
  • der Blauwert schließlich bei colorMap[start+3*f+2].

Um Informationen über die internen Werte und die Größe der Tabelle zu erhalten, reicht ein toString(). Die Größe der Tabelle liefert die Methode getMapSize().

Mit den finalen Methoden getReds(byte redArray[]), getGreens(byte greenArray[]), getBlues(byte blueArray[]) und getAlphas(byte alphaArray[]), deren Rückgabewert void ist, lassen sich die Farbinformationen auslesen und als Ergebnis in das Feld legen. Die Felder müssen schon die passende Größe haben, die sich jedoch mit final int getMapSize() erfragen lässt. Die Methode getTransparentPixel() liefert den Index des transparenten Pixels. Gibt es keinen, ist der Wert -1.

Werfen wir zur Demonstration noch einen Blick auf die Methode getGreens(). Wir sehen deutlich, dass das Feld eine passende Größe haben muss.

final public void getGreens(byte g[]) {
  for (int i = 0; i < map_size; i++)
  g[i] = (byte) (rgb[i] >> 8);
}

An getRed() sehen wir ebenso, dass der Pixel auch direkt ein Index für das private Feld rgb ist. Wenn der Index über die Feldgröße läuft, müssen wir den Fehler selbst behandeln.

final public int getRed(int pixel) {
  return (rgb[pixel] >> 16) & 0xff;
}

Wenden wir unsere Aufmerksamkeit auf ein Programm, welches ein Bytefeld erzeugt und aus sechs Farben die Pixel in das Feld schreibt. Zum Schluss konvertieren wir das Bytefeld mit einem MemoryImageSource in ein Image-Objekt. Für diese Klasse können wir ein IndexColorModel angeben, das dann folgendes Format hat:

ColorModel cm = IndexColorModel( 8, colorCnt, r, g, b );

Hier handelt es sich um ein Farbmodell mit acht Bits und sechs Farben. Die folgenden Werte zeigen auf die drei Felder mit den Farbwerten. Anschließend erzeugt createImage() mit diesem Farbmodell das Image-Objekt.

Image i = createImage( new MemoryImageSource(w,h,cm,pixels,0,w) );

Zum kompletten Beispiel:

import java.awt.*;
import java.awt.image.*;

public class IndexColorModelDemo extends Frame
{
  Image i;
  private final static int w = 400, h = 400;

  int pixels[] = new int [w*h];

  Color colors[] = {
    Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.magenta
  };

  IndexColorModelDemo()
  {
    int colorCnt = colors.length;

    byte r[] = new byte[colorCnt],
    g[] = new byte[colorCnt],
    b[] = new byte[colorCnt];

    for ( int i = 0; i < colorCnt; i++ ) {
      r[i] = (byte) colors[i].getRed();
      g[i] = (byte) colors[i].getGreen();
      b[i] = (byte) colors[i].getBlue();
    }

    int index = 0;
    for ( int y = 0; y < h; y++ )
      for ( int x = 0; x < w; x++ )
        pixels[index++] = (int)(Math.random() * colorCnt);
    i = createImage( new MemoryImageSource( w, h, new IndexColorModel(8, colorCnt, r, g, b), pixels, 0, w) );
  }

  public void paint( Graphics g )
  {
    if ( i != null )
      g.drawImage( i, 0, 0, this );
  }

  public static void main( String args[] )
  {
    IndexColorModelDemo d = new IndexColorModelDemo();
    d.setSize( w, h );
    d.show();
  }
}

Offtopic: Forward Secrecy

Mit den NSA-Enthüllungen nimmt es kein Ende, und so rücken  noch schärfere Algorithmen in den Vordergrund, etwa bei SSL auf Forward Secrecy zu setzen. Kurz gesagt geht es darum, den geheimen Schlüssel, mit dem beide Parteien ihre Daten verschlüsseln, so zu bilden, dass der Schlüssel nach der Verbindung verfällt. Heise weißt unter http://www.heise.de/security/artikel/Zukunftssicher-Verschluesseln-mit-Perfect-Forward-Secrecy-1923800.html etwa darauf hin. Meiner Bank, der 1822direkt, habe ich eine E-Mail geschickt und bin gespannt auf die Antwort, denn die nutzt das laut https://www.ssllabs.com/ssltest/analyze.html?d=https%3A%2F%2Fwww.1822direkt.com%2F nicht. Ich denke jeder solle “seine” Diente einmal abklopfen und die Provider darauf hinweisen FS zu nutzen.

2 Tage, am Sonntag abend, kommt die erste Antwort der Bank:

Leider wird die Klärung des Vorganges noch etwas Zeit in Anspruch nehmen. Sobald wir ein abschließendes Ergebnis vorlegen können, werden wir Sie umgehend informieren

Es dauerte noch ein paar Tage und dann kann die undetailierte Antwort:

Sie haben eine Frage zu unseren Sicherheitszertifikaten gestellt, die wir Ihnen gerne beantworten. Die von Ihnen genannte Situation ist uns bekannt. Die angegebene Prüfseite wird von uns, neben anderen Prüfseiten, regelmäßig verwendet. Momentan müssen wir hierbei zwischen dem Schutz vor mannigfaltigen Attacken sowie Browserunterstützung abwägen. Im unwahrscheinlichen Fall, dass sich ein Angreifer Zugriff verschaffen kann, sind unsere Sicherheitsschlüssel zusätzlich geschützt. Wir bitten um Ihr Verständnis, dass wir Ihnen hierzu aus Sicherheitsgründen keine detaillierteren Informationen erteilen können.

Der Verweis auf den Browser ist natürlich irreführend, denn Server und Browser „verhandeln“ das jeweilige Verfahren auf und wie die SSL-Lab-Seite schön zeigt, bietet der Server gewisse Verfahren an da kann Foward Secrecy natürlich noch dazukommen.

Ziemliches fettes Java-Problem….

… wenn man die Systemzeit im laufenden Java-Betrieb ändert. In den letzten Tagen kam die Diskussion auf der Java-Champ-Liste auf, hier noch einmal zusammengefasst: http://bbossola.wordpress.com/2013/09/04/jvm-issue-concurrency-is-affected-by-changing-the-date-of-the-system/, http://bbossola.wordpress.com/2013/09/05/jvm-issue-concurrency-is-affected-by-changing-the-date-of-the-system-part-2/

Tiefe Objektkopien (Deep Copy)

Die clone() Methode liefert nur flache Kopien eines Objektes. Mit Hilfe der Serialisierung kommt man schnell auch zu tiefen Kopien. Klassen können die clone() Methode von Object überschreiben und so eine Kopie der Werte liefern. Die Standardimplementierung ist jedoch so angelegt, dass diese Kopie flach ist, was bedeutet, Referenzen auf Objekte die von dem zu klonenden Objekt ausgehen, werden beibehalten und diese Objekte nicht extra kopiert. Als Beispiel kann die einfache Datenstruktur eines Feldes genügen, welches auf Vector Objekte verweiset. Eine Klon dieses Feldes ist lediglich ein zweites Feld, dessen Elemente auf die gleichen Vektoren zeigen. Eine Änderung wird also beiden Felder bewusst.

Möchten wir das Verhalten ändern und eine tiefe Kopie anfertigen, so haben wir mit einem kleinen Trick keine Mühe damit. Die Idee ist, dass wir das zu klonende Objekt einfach Serialisieren und dann wieder auspacken. Die zu klonenden Objekte müssen dann nur das Serializable Interface implementieren.

public static Object deepCopy( Object o ) throws Exception
{
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  new ObjectOutputStream( baos ).writeObject( o );

  ByteArrayInputStream bais = new ByteArrayInputStream( baos.toByteArray() );

  return new ObjectInputStream(bais).readObject();
}

Das einzige was wir zum Gelingen der Methode deepCopy() machen müssen, ist das Objekt in ein Bytefeld zu serialisieren und dann wieder auszulesen und zu einem Objekt konvertieren. Den Einsatz eines ByteArrayOutputStream haben wir schon gesehen, als wir die Länge eines Objektes herausfinden wollten. Nur fügen wir nun das Feld wieder einem ByteArrayInputStream zu, aus dessen Daten dann der ObjectInputStream wieder das Objekt rekreieren kann.

Überzeugen wir uns an Hand eines kleines Programms, dass die tiefe Kopie tatsächlich etwas anderes als ein clone() ist. (Verwendet werden Raw-Types um das Bsp. kurz zu halten.)

public static void main( String args[] ) throws Exception
{
  Map map = new HashMap() {{
    put( "Cul de Paris", "hinten unter dem Kleid getragenes Gestell oder Polster" );
  }};

  LinkedList l1 = new LinkedList(); 
  l1.add( map );

  List l2 = (List) l1.clone();

  List l3 = (List) deepCopy( l1 );

  map.clear();

  System.out.println( l1 );
  System.out.println( l2 );
  System.out.println( l3 );
}

Zunächst erstellen wir eine Map, die wie anschießend in eine Liste packen. Die Map enthält ein Pärchen. Klonen wir mit clone() die Liste, so wird zwar die Liste selbst kopiert, aber nicht die Map. Die tiefe Kopie kopiert neben der Liste auch gleich die Map mit. Das sehen wir dann, wenn wir den Eintrag aus dem Map löschen. Dann ergibt l1 genauso wie l2 eine leere Liste, da l2 zur die Verweise auf die Map gespeichert hat, die dann aber geleert ist. Anders ist dies bei l3, der tiefen Kopie; hier ist das Paar noch vorhanden. Die Ausgabe ist dann:

[{}]
[{}]
[{Cul de Paris=hinten unter dem Kleid getragenes Gestell...}]

An diesem Beispiel sehen wir, wie wunderbar die Stream-Klassen zusammenarbeiten. Einzige Voraussetzung zum Gelingen ist die Implementierung der Schnittstelle Serializable. Da aber die zu klonenden Klassen auch clone() implementieren müssen, gilt in der Regel, dass sie serialisierbar sind. Daher steht in der implements Zeile die Schnittstelle Clonable und Serializable direkt nebeneinander.

Doppelpufferung (Double-Buffering)

Stellt eine paint()-Methode (und damit paintComponent()) komplexe Zeichnungen dar, die zum Beispiel aus hunderten kleiner Linien und Bögen besteht, so braucht das seine Zeit. Zeichnen wir in paint() immer alles neu, so haben wir große Zeitverzögerungen, wenn sich etwa die Größe des Zeichenbereichs ändert und der Repaint-Manager über ein repaint() wieder zum paint() führt und alles wieder neu gezeichnet wird, ohne dass sich wirklich etwas an der Grafik geändert hat.

Eine einfache und elegante Methode, diesem Problem zu entkommen, ist die Technik der Doppelpufferung (engl. double-buffering). Eine zweite Zeichenebene, so groß wie das Original, wird angelegt und alle Grafikoperationen finden auf diesem Hintergrundbild statt. Immer wenn das zu zeichnende Bild komplett ist, kopieren wir es zur passenden Zeit in den sichtbaren Bereich. Kommt ein Repaint-Ereignis, und hat sich die Grafik bis dahin nicht aktualisiert, so muss nur der entsprechende Teil der Hintergrundgrafik neu gezeichnet werden.

Um ein Programm auf die neue Technik umzustellen, muss zuerst die paint()-Methode umgebaut werden, die direkt die Zeichenbefehle erteilt. Nehmen wir folgende Implementierung an:

private void bigPaint( Graphics g )
{
 Random r = new Random();

 for ( int i = 0; i < 1000; i++ )
 {
  g.drawOval( r.nextInt(getWidth()-100), r.nextInt(getHeight()-100), 100, 100 );
  g.setColor( new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255), r.nextInt(255)) );
 }
}

@Override protected void paintComponent( Graphics g )
{
 bigPaint( g );
}

Um das Programm schon etwas zu vereinfachen, enthält paintComponent() nun schon keine direkten Zeichenbefehle mehr, sondern delegiert an das eigene bigPaint(). Im nächsten Schritt müssen wir das Programm so umbauen, dass es auf das Graphics-Objekt unseres Hintergrundbildes geht. Dazu ist zuerst ein Hintergrundbild nötig. Eine Variante ist, die den Hintergrundpuffer in paintComponent() aufzubauen, denn dann gibt es Zugriff auf die Höhen und Breiten, die sich verändert haben können – natürlich kann auch die Fläche immer gleich groß bleiben.

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class DoubleBuffering extends JPanel
{
 private void bigPaint( Graphics g )
 {
  g.setColor( Color.WHITE ); g.fillRect( 0, 0, getWidth(), getHeight() );

  Random r = new Random();

  for ( int i = 0; i < 1000; i++ )
  {
   g.drawOval( r.nextInt(getWidth()-100), r.nextInt(getHeight()-100), 100, 100 );
   g.setColor( new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255), r.nextInt(255)) );
  }
 }

 private final GraphicsConfiguration gfxConf = GraphicsEnvironment
                         .getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();

 private BufferedImage offImg;

 @Override protected void paintComponent( Graphics g )
 {
  if ( offImg == null || offImg.getWidth() != getWidth() || offImg.getHeight() != getHeight() )
  {
   offImg = gfxConf.createCompatibleImage( getWidth(), getHeight() );
   bigPaint( offImg.createGraphics() );
  }

  g.drawImage( offImg, 0, 0, this );
  // bigPaint( g );
 }

 public static void main( String[] args )
 {
  JFrame f = new JFrame();
  f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
  f.setSize( 800, 600 );
  f.add( new DoubleBuffering() );
  f.setVisible( true );
 }
}

Hinweis: Oft fällt ein Flackern bei Grafikoperationen auf. Das Problem ist, dass die Zeichenoperationen so lange dauern, dass die gesamte Zeichnung nicht im Zyklus einer Bildschirmwiederholfrequenz auf den Schirm kommt. Während Teile gezeichnet werden, sendet die Grafikkarte die Teilbilder zum Display und bei jedem Update sehen wir einen aktualisierten Teil unserer Grafik. Bei aufwändigen Zeichenoperationen sind nun einmal viele Durchläufe nötig, bis das Bild komplett ist.

Kleine Kekse: die Klasse javax.servlet.http.Cookie

Der Server kann den Client veranlassen, diese Information eine bestimmte Zeit lang zu speichern. Betritt der Client die Seite des Anbieters, schickt er dem Server den Cookie als Kennung. Dieser kann anhand der Cookie-Kennung die Sitzung erkennen, sofern er die Information gesichert hat. Name und Technologie der Cookies wurden von Netscape geprägt, als die Firma noch den Browser-Markt revolutionierte. Mittlerweile kümmert sich die HTTP Working Group der Internet Engineering Task Force (IETF) um die Weiterentwicklung.

Das Wort »Cookie« wird gerne mit Keksen assoziiert, was aber nicht beabsichtigt ist. Informatiker kennen den Begriff und meinen damit einfach nur kleine Informationseinheiten. Mehr Informationen rund um Cookies hat David Whalen auf seiner Seite http://www.cookiecentral.com/ gesammelt.

Cookies erzeugen und setzen

Cookies werden für den Benutzer durch die Klasse Cookie verwaltet. Sie bietet Methoden zur Bearbeitung der Informationen, die der Cookie speichert. Damit wir auf der Clientseite Cookies setzen können, müssen wir zunächst ein Cookie-Objekt erzeugen. Dazu bietet die Klasse genau einen Konstruktor mit zwei Parametern an, die dem Cookie einen Namen und einen Wert geben. Der Name muss nach RFC 2109 geformt sein, das heißt vereinfacht aus Buchstaben und Ziffern. Nun muss der Cookie beim Client gesetzt werden. Dies führt die Methode addCookie() auf dem HttpServletResponse-Objekt durch:

Cookie cookie = new Cookie( "key", "value" );
response.addCookie( cookie );

Da es mehrere Einträge geben kann, darf die Methode auch mehrmals aufgerufen werden.

interface javax.servlet.http.HttpServletResponse extends ServletResponse

  • public void addCookie( Cookie cookie )

    Fügt der Antwort einen Cookie-Header zu.

Cookies vom Servlet einlesen

Bei jeder weiteren Kommunikation mit einem Server werden die mit der Server-URL assoziierten Cookie-Daten automatisch mitgeschickt. Um sie zu erfragen, bemühen wir die Methode getCookies() des HttpServletRequest-Objekts. Der Rückgabewert der Methode ist ein Feld von Cookie-Objekten. Jeder Cookie bietet als Objektmethode getName() und getValue() an, um an die Schlüssel-Werte-Paare zu gelangen. Wenn die getCookies()-Methode null liefert, so war noch kein Cookie angelegt, und wir müssen darauf reagieren.

<%@ page import="java.util.*" %>
<%
 String myCookieName = "visited";
 Cookie[] cookies = request.getCookies();
 if ( cookies == null )
 out.println( "Kein Cookie gesetzt!" );
 else {
  boolean visited = false;
  for ( int i = 0; i < cookies.length; i++ ) {
   String cookieName = cookies[i].getName();
   if ( cookieName.equals(myCookieName) )
    visited = true;
%>
Cookie "<%= cookieName %>" hat den Wert "<%= cookies[i].getValue() %>"
<br>
<%
  }
  if ( !visited ) {
   Cookie visCookie = new Cookie( myCookieName, new java.util.Date().toString() );
   response.addCookie( visCookie );
   out.println( "Cookie gesetzt" );
  }
 }
%>

Bekommt der Server eine Anforderung vom Client, kennt der Client natürlich die Server-Adresse. Er schaut in seinem Cookie-Speicher nach, ob mit diesem Server ein Cookie assoziiert ist. Dann schickt er diesen automatisch in einem speziellen Cookie-Feld mit, sodass der Server diesen Wert auslesen kann. Cookies sind für andere Server nicht sichtbar, sodass sie keine direkte Sicherheitslücke darstellen.

Cookie-Status ändern

Im Cookie werden neben einem Namen und dem damit verbundenen Wert noch weitere Informationen gespeichert. Die folgende Aufzählung zeigt die Zugriffsmethoden für Cookies:

class javax.servlet.http.Cookie

implements java.lang.Cloneable

  • void setComment( String purpose )
  • String getComment()

    Eine zusätzliche Beschreibung für einen Cookie, der nicht von jedem Browser unterstützt wird (beispielsweise von Netscape). Bei der Abfragemethode bekommen wir null, falls dem Cookie kein Kommentar zugewiesen wurde.
  • setDomain( String pattern )
  • String getDomain()

    Der Gültigkeitsbereich eines Cookies. Der Domänenname beginnt mit einem Punkt (etwa .kuchenfuerulli.com) und gilt dann für alle direkten Rechner dieser DNS-Adresse, also etwa www.kuchenfuerulli.com, aber nicht a.b.kuchenfuerulli.com.
  • int getMaxAge()
  • setMaxAge() legt fest, wie lange der Cookie existieren soll (in Sekunden). Ist der Wert negativ, wird der Cookie nicht gespeichert, sondern nach der Sitzung, also beim Schließen des Browsers, entfernt. getMaxAge() liefert die Lebensdauer eines Cookies, wobei die oben getätigten Aussagen auch hier zutreffen.
  • void setPath( String uri )
  • public String getPath()

    Der Pfad gibt den Ort für den Client an, an dem der Cookie sichtbar ist. Die Sichtbarkeit gilt für das angegebene Verzeichnis und alle Unterverzeichnisse. Zusätzliche Informationen sind in RFC 2109 abgelegt.
  • void setSecure( boolean flag )
  • public boolean getSecure()

    Mit einer sicheren Verbindung lassen sich Cookies nur über ein sicheres Protokoll wie HTTPS oder SSL übertragen. setSecure(true) sendet den Cookie daher nur, wenn ein sicheres Protokoll verwendet wird. getSecure() liefert false, wenn der Browser den Cookie durch ein beliebiges Protokoll senden kann.
  • void setName( String name )
  • String getName()

    Der Name des Cookies, der nach der Erzeugung nicht mehr geändert werden kann.
  • void setValue( String newValue )
  • String getValue()

    Jeder Cookie speichert einen Wert, der mit setValue() neu gesetzt werden kann, sofern der Cookie existiert. Bei einem binären Wert müssen wir selbstständig eine ASCII-Kodierung finden, zum Beispiel eine BASE64-Kodierung. Mit Cookies der Version 0 sind die Zeichen › ‹, ›(‹, ›)‹, ›[‹, ›]‹, ›=‹, ›,‹, ›’‹, ›"‹, ›\‹, ›?‹, ›@‹, ›:‹, ›;‹ nicht zugelassen. Nicht gesetzte Werte können unterschiedliche Rückgaben des Browsers provozieren.
  • int getVersion()
  • void setVersion( int v )

    Die Version des Cookies, wie in RFC 2109 beschrieben. Version 0 hält sich an die Originalspezifikation von Netscape. Die Version 1 wird in RFC 2109 beschrieben; die Variante ist noch etwas experimentell.

Langlebige Cookies

Für Cookies, die länger als eine Sitzung halten sollen, lässt sich mit setMaxAge() eine Zeit setzen, zu der sie gültig sein sollen. Eine praktische Klasse ist MaxAgeCookie, die im parametrisierten Konstruktor das Alter auf die Höchstzahl von einem Jahr setzt. Dies müssen aber nicht alle Browser so implementieren.

import javax.servlet.http.*;

public class MaxAgeCookie extends Cookie
{
 public MaxAgeCookie( String name, String value )
 {
  super( name, value );
  setMaxAge( 60*60*24*365 );
 }
}

Schnelle Vergleiche mit CollatorKeys

Landestypische Vergleiche können mit der Collator-Klasse gemacht werden. Eine Geschwindigkeitssteigerung ergibt sich durch Collator-Keys. Obwohl sich mit der Collator Klasse sprachspezifische Vergleiche korrekt umsetzen lassen, ist die Geschwindigkeit gegenüber einem normalen String-Vergleich schlechter. Daher bietet die Collator Klasse die Objektmethode getCollationKey() an, die ein CollationKey Objekt liefert, der schnellere Vergleiche zulässt.

Collator col = Collator.getInstance( Locale.GERMAN );
CollationKey key1 = col.getCollationKey( "ätzend" );
CollationKey key2 = col.getCollationKey( "Bremsspur" );

Durch CollationKeys lässt sich die Performance bei Vergleichen zusätzlich verbessern, da der landesspezifische String in einen normalen Javastring umgewandelt wird, der dann schneller verglichen werden kann. Dieses bietet sich zum Beispiel beim Sortieren einer Tabelle an, wo mehrere Vergleiche mit dem gleichen String durchgeführt werden müssen. Der Vergleich wird mit compareTo(CollationKey) durchgeführt. Der Vergleich von key1 und key2 im Beispiel lässt sich demnach durch folgende Zeile ausdrücken:

int comp = key2.compareTo( key1 );

Das Ergebnis ist wie bei der compare() Methode bei Collator Objekten <0, 0 oder >0.

class java.text.CollationKey implements Comparable

  • int compareTo( CollationKey target )

    Vergleicht zwei CollationKey Objekte miteinander.
  • int compareTo( Object o )

    Vergleicht den aktuellen CollationKey mit dem angegeben Objekt. Ruft compareTo((CollationKey)o) auf.
  • boolean equals( Object target )

    Testet die beiden CollationKey Objekte auf Gleichheit.
  • String getSourceString() 
    Liefert den String zum CollationKey.
  • int hashCode()

    Berechnet den Hashcode für den CollationKey.
  • byte[] toByteArray()

    Konvertiert den CollationKey in eine Folge von Bits.

abstract class Collator implements Comparator, Cloneable

  • abstract CollationKey getCollationKey( String source )

    Liefert einen CollationKey für den konkreten String.

Bildfilter mit AWT-ImageFilter

ImageFilter liegen zwischen Produzenten und Konsumenten und verändern Bildinformationen oder nehmen Einfluss auf die Größe. Für Bild-Produzenten treten die Filter als Konsumenten auf, die die Schnittstelle ImageConsumer implementieren und die wichtige Methode setPixel() programmieren.

Grundlegende Eigenschaft von Filtern

Um einen Filter anzuwenden, nutzen wir die Klasse FilteredImageSource. Im Konstruktor geben wir das Bild und den Filter an. Anschließend können wir den zurückgegebenen Produzenten an createImage() übergeben, und wir haben ein neues Bild.

Beispiel: Anwendung eines Filters:

Image src = getImage( "gatesInAlbuquerque.jpg" );
ImageFilter colorfilter = new GrayFilter();
ImageProducer imageprod = new FilteredImageSource( src.getSource(), colorfilter );
Image img = createImage( imageprod );

Konkrete Filterklassen

Es gibt einige Unterklassen der Klasse ImageFilter, die für unsere Arbeit interessant sind:

  • BufferedImageFilter. Diesem Filter lässt sich ein Objekt vom Typ BufferedImageOp übergeben, mit dem unterschiedliche Manipulationen ermöglicht werden. BufferedImageOp ist eine Schnittstelle, die von AffineTransformOp, ConvolveOp, BandCombineOp und LookupOp implementiert wird. AffineTransformOp ist am attraktivsten, da es mit einem AffineTransform konstruiert wird, sodass leicht Vergrößerungen oder Rotationen ermöglicht werden. Über AffineTransform-Objekte erfahren wir im 2D-Abschnitt mehr.
  • CropImageFilter. Bildteile werden herausgeschnitten.
  • ReplicateScaleFilter. Zum Vergrößern oder Verkleinern von Bildern. Ein einfacher Algorithmus wird angewendet. Eine weiche Vergrößerung oder Verkleinerung lässt sich mit der Unterklasse AreaAveragingScaleFilter erreichen.
  • RGBImageFilter. Dieser allgemeine Filter ist für die eigene Filterklasse gedacht. Wir müssen lediglich eine filterRGB()-Methode angeben, die die RGB-Bildinformationen für jeden Punkt (x,y) modifiziert. Benötigt der Filter auch die Nachbarpunkte, können wir nicht mit RGBImageFilter arbeiten.
  • Beispiel: Ein Filter, der den Rot- und Blauanteil in einem Bild vertauscht.

    class RedBlueSwapFilter extends RGBImageFilter
    {
     public RedBlueSwapFilter()
     {
      canFilterIndexColorModel = true;
     }
     public int filterRGB( int x, int y, int rgb )
      {
        return (   (rgb & 0xff00ff00)
                | ((rgb & 0xff0000) >> 16)
                | ((rgb & 0xff) << 16));
      }
    }

    Mit CropImageFilter Teile ausschneiden

    Mit CropImageFilter lassen sich Teile des Bilds ausschneiden. Wir definieren dafür vom Bild einen Ausschnitt mit den Koordinaten x, y und der Breite und Höhe. Wie die anderen Bildfilter, so wird auch CropImageFilter mit dem FilteredImageSource als Produzent verwendet.

    Beispiel Erzeuge für die Grafik big.gif in einem Applet ein neues Image-Objekt. Das Original hat die Größe 100 × 100 Pixel. Das neue Bild soll einen Rand von 10 Pixeln haben.

    Image origImage = getImage( getDocumentBase(), "big.gif" );
    ImageFilter cropFilter = new CropImageFilter( 10, 10, 90, 90 );
    Image cropImage = createImage( new FilteredImageSource(origImage.getSource(),cropFilter) );

    Bildausschnitte über PixelGrabber ausschneiden

    Nicht nur über CropImageFilter lassen sich Bildausschnitte auswählen. Eine andere Lösung geht über PixelGrabber, da dieser auch einen Ausschnitt erlaubt. Darüber lässt sich dann mit MemoryImageSource wieder ein neues Bild erzeugen.

    Beispiel: Schneide aus dem Image img das passende Rechteck mit den Startkoordinaten x, y und der Breite width und der Höhe height aus:

    int[] pix = new int[width * height];
    PixelGrabber pg = new PixelGrabber( img, x, y, width, height,
                                      pix, 0, width );
    try {
      pg.grabPixels();
    }
    catch( InterruptedException e ) {}
    newImg = createImage(new MemoryImageSource(width, height, pix, 0, width) );

    An dieser Stelle sollten wir noch einmal den Unterschied zwischen den beiden Möglichkeiten betonen. PixelGrabber implementiert die Schnittstelle ImageConsumer, sodass er ein Bildkonsument ist und Daten in einem Integer-Feld ablegt. CropImageFilter ist ein Filter, der ein anderes Image-Objekt konstruiert und kein Feld.

    Transparenz

    Um eine bestimmte Farbe eines Bilds durchsichtig zu machen (also die Transparenz zu bestimmen), nutzen wir einen RGBImageFilter. Dabei implementieren wir einen Konstruktor, der die Farbe sichert, die transparent werden soll. Sie wird später in der Implementierung von filterRGB() verwendet. Die Methode, die ja für jeden Bildpunkt aufgerufen wird, liefert dann entweder die Farbe ohne Alpha-Kanal zurück (rgb|0xff000000) oder eben nur den Alpha-Kanal (rgb & 0xffffff) für Transparenz. Eine interessante Erweiterung ist die Einführung einer Toleranzauswertung um einen »Zauberstab«, der ähnlich wie in Photoshop zu realisieren ist.

    import java.awt.*;
    import java.awt.image.*;
    
    public class TransparentFilter extends RGBImageFilter
    {
     private final int transparentRGB;
    
     public TransparentFilter( Color color )
     {
      this.transparentRGB = color.getRGB();
     }
    
     @Override
     public int filterRGB( int x, int y, int rgb )
     {
      if ( rgb != transparentRGB )
        return rgb | 0xff000000;
    
      return rgb & 0xffffff;    //transparent
     }
    }

    Ausgaben mit MessageFormat formatieren

    MessageFormat ist eine konkrete Unterklasse der abstrakten Klasse Format und dient dazu, Nachrichten sprachunabhängig zu erzeugen. Das heißt, die tatsächliche sprachabhängige Ausgabe wird so weit wie möglich nach hinten geschoben und erst dann erzeugt, wenn die Nachricht dem Benutzer angezeigt werden soll. Durch MessageFormat werden nur Formatierungsanweisungen gegeben, und die wirklichen Informationen (also die Objekte als Informationsträger) werden zur Laufzeit eingesetzt. Dabei enthalten die Formatierungsanweisungen Platzhalter für diese Objekte. In der Regel werden Daten (die Argumente) erst zur Laufzeit ermittelt, wie etwa die Zeilennummer einer Fehlerstelle in einer Eingabedatei.

    Beispiel Eine Anwendung des Formatierers. Der format()-Befehl formatiert die Argumente, die in einem Objekt-Feld abgelegt sind, mit dem Aussehen, wie es im Konstruktor des MessageFormat-Objekts angegeben wurde.

    Object[] testArgs = { 31415L, "SchnelleLotte" };
    MessageFormat form = new MessageFormat(
      "Anzahl Dateien auf der Festplatte \"{1}\": {0}." );
    System.out.println( form.format(testArgs) );

    Die Ausgabe mit unterschiedlichen testArgs ist:

    Anzahl Dateien auf der Festplatte "SchnelleLotte": 0.
    Anzahl Dateien auf der Festplatte "SchnelleLotte": 1.
    Anzahl Dateien auf der Festplatte "SchnelleLotte": 31,415.

    Die Argumente aus dem Array werden über die Platzhalter wie {0} in die Nachricht eingefügt. Die Nummern entsprechen der Reihenfolge der Argumente im Array. Einträge im Array können ungenutzt bleiben. Fehlt allerdings das einem Platzhalter entsprechende Element im Feld, so wird eine ParseException ausgelöst.

    class java.text.MessageFormat extends Format

    • MessageFormat( String pattern ). Erzeugt ein MessageFormat-Objekt mit dem angegebenen Pattern.

    Gegenüber anderen Format-Klassen zeigt die Klasse MessageFormat eine Besonderheit beim Erzeugen: MessageFormat-Objekte werden über ihren Konstruktor erzeugt und nicht über getInstance(). Der Grund ist, dass üblicherweise die Erzeugungsfunktionen – damit sind die getInstance()-Varianten gemeint – eine komplexe Initialisierung durchlaufen, die die landesspezifischen Einstellungen festlegen. MessageFormat ist aber an keine bestimmte Sprache gebunden und benötigt folglich auch keine Initialisierung.

    Bildungsgesetz für Message-Formate

    Die Zeichenkette für MessageFormat enthält die Format-Elemente, die in geschweiften Klammern gesetzt sind. Steht dort nur der Index – wie {0} –, ist das der einfachste Fall. Die API-Dokumentation von MessageFormat zeigt jedoch, dass die Angaben auch präziser ausfallen können:

    • { ArgumentIndex, FormatType }: Element wird nach dem angegebenen Format-Typ number, date, time oder choice formatiert. MessageFormat besorgt sich zum Beispiel im Fall vom Format-Typ number über NumberFormat.getInstance(getLocale()) einen passenden Formatierer.
    • { ArgumentIndex, FormatType, FormatStyle }: Neben dem Format-Typ lässt sich der Stil festlegen. Vordefiniert sind short, medium, long, full, integer, currency und percent. Ein eigener Formatierungsstil lässt sich auch angeben, der aber zur Unterscheidung in einfachen Hochkommata eingeschlossen werden muss.

    Abschließend sei ein Beispiel mit MessageFormat gegeben, das das gleiche Argument unterschiedlich formatiert:

    Object[] arguments = {
      new Date(),
      "die Antwort auf alle Fragen",
      42 // Integer object
    };
    String result = MessageFormat.format(
      "Am {0,date} um {0,time} ist {1} wie immer {2,number,integer}.", arguments );
    System.out.println( result );

    Dies erzeugt die Ausgabe:

    Am 21.08.2007 um 15:43:56 ist die Antwort auf alle Fragen wie immer 42.

    Hinweis Bei den geschweiften Klammern besteht Verwechslungsgefahr zwischen Message-Platzhalter und normalem Zeichen. Das ist insbesondere ein Problem, wenn die Nachricht mit den Platzhaltern eine beliebige Datei ist (etwa ein Java-Programm, in dem der Name der Klasse durch einen Platzhalter angedeutet ist). Dann muss jede normale geschweifte Klammer { durch \'{‚ ersetzt werden.

    Bits rotieren

    Java hat zwar Operatoren zum Verschieben der Bit, aber bis Java 5 nicht zum Rotieren. Beim Rotieren werden einige Bit um eine bestimmte Stelle verschoben, doch die herausfallenden Bit kommen auf der anderen Seite wieder hinein.

    Eine Funktion ist leicht geschrieben: Der Trick dabei besteht darin, die herausfallenden Bit vorher zu extrahieren und auf der anderen Seite wieder
    einzusetzen.

    public static int rotateLeft( int v, int n )
    {
      return (v << n) | (v >>> (32 – n));
    }
    
    public static int rotateRight( int v, int n )
    {
      return (v >>> n) | (v << (32 – n));
    }

    Die Funktionen rotieren jeweils n Bit nach links oder rechts. Da der Datentyp int ist, ist die Verschiebung n in dem Wertebereich von 0 bis 31 sinnvoll.

    Seit Java 5 regeln die Funktionen wie Integer.rotateLeft(…) die Rotation.