Der vorgegebene Konstruktor (engl. default constructor) und Begriffsverwirrungen

Wenn wir in unserer Klasse überhaupt keinen Konstruktor angeben, legt der Compiler automatisch einen an. Dieser Konstruktor nennt Sun default constructor, was wir als vorgegebener Konstruktor (selten auch Vorgabekonstruktor) eindeutschen wollen.

Schreiben wir nur

class Player
{
}

macht der Compiler daraus immer automatisch:

class Player
{
Player() { }
}

Der vorgegebene Konstruktor hat immer die gleiche Sichtbarkeit wie die Klasse. Ist also die Klasse public/private/protected, wird auch der automatisch eingeführte Konstruktor public/private/protected sein. Ist die Klasse paketsichtbar, ist es auch der Konstruktor.

Vorgegebener und expliziter Standard-Konstruktor

Ob ein parameterloser Konstruktor vom Compiler oder Entwickler angelegt wurde ist ein Implementierungsdetail, der für Nutzer der Klasse irrelevant ist. Daher ist es im Grunde egal, ob wir einen Standard-Konstruktor selbst anlegen oder wir uns einen vorgegebenen Konstruktor vom Compiler generieren lassen: im Bytecode lässt sich das nicht mehr unterscheiden. Selbst die JavaDoc API-Dokumentation, von einer public class C1 {} und public class C2 { public C2(){} } wäre strukturell gleich.

constructorBytecode1  constructorBytecode2

Bytecode der Klassen C1 und C2 im Vergleich

In der Begriffswelt der Insel heißt ein parameterloser Konstruktor immer Standard-Konstruktor, was natürlich den Unterschied verschwimmen lässt, ob der Standard-Konstruktor von Hand angelegt wurde oder als vorgegebener Konstruktor vom Compiler eingeführt wurde. Um das noch klarer zu unterscheiden, können wir es mit vorgegebener (Standard-)Konstruktor und expliziter Standard-Konstruktor weiter präzisieren.

Auch wenn der Compiler einen vorgegeben Konstruktor anlegt, ist es oft sinnvoll, einen eigenen Standard-Konstruktor anzugeben, auch wenn der Rumpf leer ist. Ein Grund ist, ihn zu mit JavaDoc zu dokumentieren, eine anderer, die Sichtbarkeit explizit zu wählen, etwa wenn die Kasse public ist, aber der Konstruktor nur die Paketsichtbarkeit haben soll.

Begrifflichkeit I In der Java-Language Specification gibt es bei den Konstruktoren nur die Trennungen in no-arg-constructor (parameterloser Konstruktor) und default constructor (vorgegebener Konstruktor), aber den Begriff „standard constructor“ gibt es nicht. Viele Autoren übersetzen die englische Bezeichnung „default constructor“ (unserem vorgegebenen Konstruktor) einfach nur mit „Standard-Konstruktor“.

 

Begrifflichkeit II Einige Autoren nennen nur den vom Entwickler explizit geschriebenen parameterlosen Konstruktor „Standard-Konstruktor“ und trennen dies sprachlich vom Compiler generierten Konstruktor, den sie weiterhin „Default-Konstruktor“ nennen. Beide werden dann zusammengefasst einfach parameterlose Konstruktoren genannt. Wenn also etwa die Frage gestellt wird, ob die Deklaration class C { } einen Standard-Konstruktor enthält, ist die Begrifflichkeit des Autors zu prüfen. Wenn der Autor nur den ausprogrammierten parameterlosen Konstruktor „Standard-Konstruktor“ genannt hat, so hätte die Klasse C nach dessen Definition keinen „Standard-Konstruktor“. Nach der Insel-Definition hätte die Klasse zwar einen vorgegebenen (Standard-)Konstruktor, aber keinen expliziten Standard-Konstruktor.

Labels:

Fork und Join aus Java 7

Gesucht ist ein Framework zum Lösen von parallelen D&C-Algorithmen, die berechnungsintensiv sind. Sun hat in Java 7 das Fork/Join integriert, was im Rahmen von jsr166y (http://gee.cs.oswego.edu/dl/concurrency-interest/) unter maßgeblicher Arbeit von Doug Lea entwickelt wurde. Die grundlegende Idee ist, neben Threads, noch eine andere Arbeitseinheit einzuführen, die Tasks.
  • Threads: Werden vom Betriebssystem verwaltet und laufen entweder pseudo-parallel auf einem Prozessor/Core oder echt parallel. Threads können sich mit anderen Threads koordinieren. Zu viele Threads, die sich im Weg stehen und aufeinander Warten führen zu keiner verbesserten Ausführungszeit gegenüber einer sequentiellen Lösung.
  • Tasks: Werden von Threads bzw. einem Thread-Pool ausgeführt. Sie sind Arbeitseinheiten, die nicht auf andere Tasks warten

Die Tasks sind kleine Arbeitspakete und werden in eine Task-Queue gelegt und dann von Threads abgearbeitet. Hat das System zwei Prozessoren und hat der Thread-Pool die Größe 2, so ist es wahrscheinlich, dass 2 Tasks parallel abgearbeitet werden. Gibt es 4 Prozessoren, können 4 Tasks vielleicht parallel laufen. Tasks lassen sich also grundsätzlich auf einer beliebigen Anzahl Threads und somit Prozessoren/Cores bringen, wobei im Gegensatz die Effektivität von Threads immer mit der physikalischen Anzahl von Prozessoren/Cores assoziiert ist.

Das Fork/Join-Framework löst die Probleme effektiv. Wie der Name schon andeutet, geht es bei Fork um das Erstellen eines neuen Tasks und bei Join um das Zusammenführen der Ergebnisse. Die Fork/Join-Bibliothek bietet dazu die Klasse ForkJoinPool und zwei zentrale Methoden: fork() und join(). Zur Abarbeitung der Tasks stellt das Framework die Threads zu Verfügung, deren Anzahl wir zwar selbst bestimmen können, aber die Anzahl Prozessoren/Cores eine gute Standardgröße ist.[1] Die Methode fork() erzeugt einen neuen Task, der an den Anfang (!) einer Queue gestellt wird. Dabei haben alle Threads eine Queue für ihre Arbeitsaufträge und sollte einmal eine Queue leer gelaufen sein, so nimmt sich der Thread einfach einen Task vom Ende (!) einer anderen nicht-leeren Queue. (Das nennt sich work-stealing und ist in der Realwelt ziemlich selten anzutreffen.) Dass neue Tasks an den Anfang gestellt werden ist einfach zu erklären: Die Tasks werden ja immer kleiner und somit stehen die kleinen, schnell lösbaren Aufgaben vorne. Erst später folgen die größeren Aufgaben, die auf die Ergebnisse der kleinen Aufgaben zurückgreifen, die dann logischerweise schon berechnet wurden.

Zur Theorie ein Beispiel: Es geht darum, mit Fork/Join ein Programm zu haben, welches parallel das Maximum eines Arrays sucht. Der Start ist:

int[] array = { 0, 9, 10, 111, 1, 12, 13, 14, 17 };
System.out.println( MaxElementInArrayFinder.findMax( array ) );

Die eigene Klasse MaxElementInArrayFinder bietet die Methode findMax(), die auf den ForkJoinPool zurückgreift, um mit invoke() den Haupt-Task abzusetzen.

class MaxElementInArrayFinder
{
private static final ForkJoinPool fjPool = new ForkJoinPool();
...
public static int findMax( int[] array )
{
return fjPool.invoke( new MaxElemTask( array, 0, array.length -1 ) );
}
}

Die Klasse MaxElemTask repräsentiert unser Arbeitspakt. Die Tasks referenzieren jeweils das Array, und die Anfangs-/Endeposition, ab der sie nach dem Maximum suchen sollen.

private static class MaxElemTask extends RecursiveTask<Integer>
{
private final int[] array;
private final int start, end;
MaxElemTask( int[] array, int start, int end )
{
assert array != null && start >= 0 && start <= end;
this.array = array;
this.start = start;
this.end = end;
}
@Override protected Integer compute()
{

}
}

Unsere Klasse erweitert die Basisklasse RecursiveTask<Integer> und deutet durch den generischen Typ schon an, das das Ergebnis des Tasks eine Ganzzahl sein wird, nämlich das Feldmaximum aus dem gewünschten Bereich. Der Konstruktor sichert die Werte und compute() führt die eigentliche Arbeit aus: Es löst entweder das Problem direkt, wenn es klein genug ist, oder spannt Unter-Tasks auf und wartet anschließend auf deren Ergebnisse.

@Override protected Integer compute()
{
assert array != null && array.length > 0;
System.out.printf( "max( start=%d, end=%d )%n", start, end );
if ( end - start < 4 )
{
int max = array[start];
for ( int i = start + 1; i <= end; i++ )
if ( array[i] > max )
max = array[i];
return max;
}
int middle = (start + end) / 2;
MaxElemTask leftTask = new MaxElemTask( array, start, middle );
leftTask.fork();
MaxElemTask rightTask = new MaxElemTask( array, middle + 1, end );
int rightMax = rightTask.compute();
int leftMax = leftTask.join();
return Math.max( rightMax, leftMax );
}

[1] Das die Maximalanzahl von Threads beim ForkJoinPool zurzeit 32767 ist, dürfte für normale Nutzer keine Einschränkung sein.

Labels: ,

Algorithmendesign teile und herrsche

Eine effektive Problemlösungsstrategie ist es, zunächst das Problem in Teilprobleme zu legen, dann die Teilprobleme zu lösen und anschließend zur Gesamtlösung zu kommen. Wer morgens im Bett liegt und Hunger verspürt, wird erst dann satt sein, wenn gewisse Teilprobleme gelöst sind. Diese Problemlösungsstrategie wird teile und herrsche (engl. divide and conquer, D&C[1]) genannt. Zunächst wird die Aufgabe ist kleine Häppchen zerlegt und anschließend abgearbeitet.

Teile und herrsche ist nicht nur eine Lösung, wie wir eine große Pizza „verarbeiten“, sondern auch in der Informatik eine beliebte algorithmische Methode: Das Hauptproblem wird in Teilprobleme zerlegt, die Teilprobleme dann gelöst und zur großen Lösung zusammengefügt. Zwei populäre Beispiele sind Sortierungen und die Multiplikation von großen Zahlen.

Sortieren über das Merge-Sort Verfahren

Der von John von Neumann vorgestellte Algorithmus basiert auf der Idee, die zu sortierende Liste ein zwei Teillisten zu zerlegen, diese dann wiederum in zwei Teile zu zerlegen, diese wiederum, usw., bis die Listen so klein sind, dass sie vielleicht nur noch aus zwei Zahlen bestehen, die trivial in eine Reihenfolge zu bringen sind. Ist eine Teilfolge dann sortiert, muss sie mit sortieren Nachbarfolge zusammenfügt (engl. merge) werden. Während also das Zerlegen und Sortieren von oben nach unten erfolgt, läuft das Zusammenlegen der sortierten Teillisten zur neuen größeren sortieren Teillisten von unten nach oben, bis schließlich die Gesamtliste sortierten ist. Der Algorithmus lässt sich sehr gut rekursiv implementieren. Auch das bekannte Quicksort arbeitet ähnlich. (Hier geht es allerdings darum, ein sogenanntes Pivot-Element zu wählen, dann die Liste in zwei Teillisten aufzuspalten, wobei in die erste Liste (erst einmal unsortiert) die Elemente kleiner dem Pivot-Element verschoben werden und in die andere Liste die Elemente größer dem Pivot-Element. Die Auswahl eines neuen Pivot-Elemens und das Kopieren in den richtigen Bereich wird rekursiv für die Unterbereiche wiederholt, was natürlich zu einer Sortierung führt. In der Regel kommt Quicksort mit weniger Speicher aus und ist in der Praxis schneller, da Merge-Sort in der einfachen Implementierung immer neue Teillisten aufbauen muss und Quicksort die Vertauschoperationen auf der originalen Datenstruktur (in-place genannt) ausführen kann.

Multiplikation von großen Ganzzahlen

In Java ist das Multiplizieren von Ganzahlen einfach. Sind die Zahlen klein genug, erledigt der *-Operator die Aufgabe, sind sie größer hilft die Klasse BigInteger und die Methode multiply(). Das sind natürlich hübsche Abstraktionen, aber im Java Bytecode gibt für die Multiplikation von int und long lediglich imul und lmul[2], und alles andere, etwa die Multiplikation von großen Zahlen für RSA-Schlüssel.

Das Produkt von großen Zahlen lässt sich einfach auf das Produkt von kleinern Zahlen mit ein paar Additionen abbilden. Statt Zahlen mit hunderten von Stellen zu nehmen, ein einfacheres Beispiel, was das Prinzip zeigt. Nehmen wir dazu die Zahl A = 1234, die mit B = 5678 multipliziert werden soll. Dann ist AB = (12 · 10^2 + 34) · (56 · 10^2 + 78) = 12 · 56 · 10^4 + (12 · 78 + 34 · 56) · 10^2 + 34 · 78. Waren bei 1234 und 5678 die Zahlen noch vierstellig, sind sie bei der Umschreibung nur noch zweistellig. Zählen wir die Anzahl Multiplikationen – und lassen wir die einfachen Multiplikation mit 10^4 bzw. 10^2 beiseite – so kommen wir auf vier, denn wir müssen 12 · 56, 12 · 78, 34 · 56 und 34 · 78 ausführen. Bei einem rekursiven D&C-Algorithmus ist also das Problem zur Multiplikation von 1234 · 5678 auf die vier Multiplikationen und Additionen abgeschwächt worden. Das können wir dann auch weiter aufspalten bis wir bei einstelligen Zahlen sind. Stehen wir also vor der Aufgabe beliebig große Zahlen mit n Stellen zur multiplizieren, können wir das Abbilden auf eine Multiplikation von Zahlen der Größe n/2 und ein paar Additionen. Kommen wir noch zu einer kleinen Optimierung. Wenn Zahlen sehr groß werden und dann multipliziert werden müssen (etwa zu Schlüsselgenerierung) ist es wichtig, jede überflüssige Operation wegzulassen, da arithmetische Operationen dann bei großen Zahlen und dem häufiger Durchführung doch ihre Zeit kosten. Interessanterweise kann durch geschickte Umstellung kann die Anzahl Multiplikationen von 4 auf 3 gesenkt werden. Zwei der Multiplikationen stammen aus 12 · 56 · 10^4 + (12 · 78 + 34 · 56) · 10^2 + 34 · 78 stammen aus dem Teil 12 · 78 + 34 · 56. Hier können wir etwas umschreiben, denn 12 · 78 + 34 · 56 = (12 + 34) · (56 + 78) – 12 · 56 – 34 · 78. Obwohl das auf den ersten Blick schlimmer aussieht (drei Multiplikationen statt zwei), fällt bei zweiten Blick auf, dass wie die beiden Produkte 12 · 56 und 34 · 78 schon im ersten Schritt berechnet haben. Also ergibt sich letztendlich 12 · 56 · 10^4 + ((12 + 34) · (56 + 78)12 · 5634 · 78) · 10^2 + 34 · 78 und das macht insgesamt drei Multiplikationen für den Preis von ein paar zusätzlichen Subtraktionen, die im allgemeinen billiger ist als die Multiplikation, die bei dem D&C-Ansatz ja recht aufwändig ist.

Die Arbeitsweise von D&C-Algorithmen im Pseudocode sieht wie folgt aus:

löse Problem:

ist Problem klein:

löse Problem direkt

andernfalls:

zerlege das Problem in Teilprobleme

löse die Teilprobleme

setzte Problemlösung aus den Teillösungen zusammen

Attraktiv sind D&C-Algorithmen dann, wenn die Teilprobleme unabhängig voneinander und parallel gelöst werden können.

Bei unserem Eingangsbeispiel mit dem Aufstehen und Essen gibt es eine Abhängigkeit, so dass zwei beide Teilprozesse zwar eine Teilaufgabe des Gesamtproblems lösen, aber ohne Aufstehen man nicht zum Kühlschrank kommt. Das Sortieren über Merge-Sort ist erfüllt dabei das Kriterium, dass wenn die Liste in zwei Unterlisten zerlegt wird, die beiden Unterlisten problemlos parallel sortiert werden können.


[1] Nicht D&G, „ein anspruchsvolles und zeitgemäßes Markenzeichen und Ausdruck einer sich wandelnden Welt“ …

[2] http://java.sun.com/docs/books/jvms/second_edition/html/Mnemonics.doc.html

Labels:

switch auf Strings seit Java 7

Seit Java 7 sind switch-Anweisungen auf String-Objekten möglich.

String input = javax.swing.JOptionPane.showInputDialog( "Eingabe" );

switch ( input.toLowerCase() )

{

case "kekse":

System.out.println( "Ich mag Keeeekse" );

break;

case "kuchen":

System.out.println( "Ich mag Kuchen" );

break;

case "scholokade":

case "lakritze":

System.out.println( "Hm. lecker" );

break;

default:

System.out.printf( "Kann man %s essen?", input );

}

Obwohl Zeichenkettenvergleiche nun möglich sind, fallen Überprüfungen auf reguläre Ausdrücke leider heraus, die insbesondere Skriptsprachen anbieten.

Labels: ,

Ersetze in einem String die alle diakritischen Zeichen (ä->a, ...)

String s = "Müller";
s = Normalizer.normalize( s, Normalizer.Form.NFD );
s = s.replaceAll( "[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+", "" );
System.out.println( s );  // Muller

Die Lösung geht zweistufig vor. Der Normalisier zerlegt zunächst den String und macht die eigentliche Arbeit. replaceAll() entfernt dann übriggebliebene Punke, Striche, Kreise, Häkchen.

Labels:

Swing-Komponenten neu erstellen oder verändern und JLayer

Zum Aufbau neuer Swing-Komponenten kommen eine Reihe von Möglichkeiten in Frage. Wenn passend, lässt sich eine existierende Swing-Komponente als Basisklasse nehmen und um nötige Eigenschaften erweitern, sofern die Basisklassen diese Möglichkeit im Grunde schon bieten. Soll etwa ein Texteingabefeld nur IP-Adressen zulassen, so ist dafür keine völlig neue Textkomponentenimplementierung nötig, sondern nur eine Unterklasse der Standard-Komponente mit passendem Dokumenten-Modell. Oder soll ein Liste nur Kontrollkästen (mit Text) darstellen soll, ist das schon über die JList mit passendem Renderer und Modell möglich.

Unproblematisch ist auch, wenn sich neue Komponenten aus anderen Swing Teilkomponenten zusammenzusetzen lassen und. Dann erweitert die neue Swing-Klasse erweitert einen Container wie JPanel, der einfach die anderen Elemente wie gewünscht platziert. Möglich ist dies zum Beispiel bei einer Statuszeile, da diese nichts großartiges macht, als einfach horizontal andere Komponenten anzuordnen und einen besonderen Rahmen zu setzen. Einen Dialog zur Auswahl eines Zeichensatzes bietet Swing bisher auch nicht an, der lässt sich aber als JDialog mit passenden Swing-Komponenten leicht nachbauen.

Mehr Arbeit ist nötig, wenn sich auf keine allgemeinen Swing-Komponenten zurückgreifen lässt. Die Swing-Bibliothek bietet etwa keine Ribbon-Komponente, keinen wirklich guten HTML-Renderer, oder ein Docking-Framework. Bei Anforderungen dieser Art lässt sich nicht so einfach auf Standardkomponenten zurückgreifen, sondern spezieller Programmcode zum Zeichnen nötig. Der wesentliche Unterschied ist also der, dass sich die Darstellung nicht vollständig an Standardkomponenten delegieren lässt sondern immer etwas einer Java-Code zum Zeichnen nötig ist.
Um es richtig gut zu machen, sind für eine eigene Swing-Komponente drei Dinge nötig: Die Komponentenklasse, eine Modellklasse und ein UI-Delegate. Die Komponentenklasse ist die Hauptklasse und eine JComponent, die der Entwickler auf die Gui setzt. Sie bietet die API zum Setzten der Zustände. Die Modell-Daten werden nicht selbst in der Komponentenklasse gespeichert, sondern idealweise über eine eigene Klasse modelliert. Die Tabelle nimmt zum Beispiel die Zellen aus einem Tabellemodell, eine Textkomponenten den Text aus einem Dokumentenmodell. Als letztes bleibt der UI-Delegate, der das wirkliche Zeichen und die Ereignisbehandlung übernimmt. Es kann sehr anspruchvoll sein ein gutes Aussehen und effektive Navigation zu erreichen und insbesondere wenn die Komponente in verschiedenen Look-and-Feels arbeiten soll, eine Menge Arbeit werden. Und das die eigene Swing-Komponente die UI-Eigenschaften wie Farben, Abstände und Antialiasing-Modus toleriert ist selbstverständlich.

Überlagerungen mit dem Swing-Komponenten-Dekorator JLayer

Können Swing-Komponten überlagert werden, können dadurch interessante Effekte erzieht werden. Ein paar Beispiele:

·    Während ein Text in die Textbox geladen wird, erscheint ein JProgressBar.
·    Bei aufwändigen Operationen wird das Haupt-Panel gesperrt und eine drehende Sanduhr erscheint.
·    Ist die Eingabe in einem Textfeld falsch, erscheint ein kleines Symbol, welches über die invalide Eingabe informiert.
·    Über einer leeren Tabelle liegt eine Beschriftung, die erklärt, dass Doppelklick eine neue Zeile einfügt.

Alle die Darstellungen lassen sich mit Hilfe der in Java 7 eingefügten Klassen JLayer einfach lösen (Nutzer vor Java 7 greifen auf SwingX zurück, denn von dort kommt die Klasse auch; sie heißt nur dort JXLayer. Als Alternative haben Autoren auch oft auf Glass-Pane zurückgegriffen.)
Haupteigenschaft von JLayer ist, sich um existierende Swing-Komponenten zu legen. Soll ein JLayer um ein Textfeld gelegt werden, heißt es:

JLayer layer = new JLayer( textField );
 

Die zu ummantelnde Komponente wird über den Konstruktor angegeben und nicht über add(), da JLayer kein Container ist. Der nächste Schritt ist die Angabe eines Objekts, dass das Zeichnen übernimmt.

layer.setUI( layerUI );

Die Angabe erfordert ein LayerUI-Objekt, welches eine paint()-Methode realisiert. Die Implementierung kann super.paint() aufrufen, um die ummantelte Komponente zu zeichnen, und dann eigenen Programmcode hinzufügen, um etwa einen Sanduhr darzustellen.
Das folgende Beispiel fasst die Schritte zusammen und realisiert ein Programm, welches bei Eingabe von „pu“ einen kleinen roten Kreis anzeigt.

JFrame f = new JFrame();
f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
f.setLayout( new BorderLayout(2, 2) );

f.add( new JSeparator(), BorderLayout.PAGE_START );
f.add( new JLabel( "Name:" ), BorderLayout.LINE_START );

final JTextField textField = new JTextField();

LayerUI layerUI = new LayerUI()
{
  @Override
  public void paint( Graphics g, JComponent component )
  {
    super.paint( g, component );

    if ( textField.getText().equalsIgnoreCase( "pu" ) )
    {
      g.setColor( new Color( 255, 0, 0, 100 ) );
      g.fillOval( 0, component.getHeight() - 10, 10, 10 );
    }
  }
};

JLayer layer = new JLayer( textField );
layer.setUI( layerUI );
f.add( layer );
f.add( new JSeparator(), BorderLayout.PAGE_END );

f.pack();
f.setVisible( true );


Die JLayer kann auch das Hauptpanel dekorieren und die Events auffangen. Das ist eine zweites Anwendungsfeld neben dem Änderung der Darstellung. Die JLayer-Komponente kann einfach Events auffangen und verarbeiten und so zum Beispiel global F1 für die Hilfe abfangen.

Labels: ,

JSON-Serialisierung mit Jackson

Nehmen wir folgende Zeile JavaScript-Code, das ein Person-Objekt mit zwei Properties für Name und Alter definiert. Eine Property wird über ein Schlüssel/Werte-Paar beschrieben:

var person = { "name" : "Michael Jackson", "age" : 50 };

Die Definition eines Objekts geschieht in der JSON (JavaScript Object Notation). Als Datentypen unterstützt JSON Zahlen, Wahreiswerte, Strings, Arrays, null und Objekte – wie unser Beispiel zeigt. Die Deklarationen können geschachtelt sein, um Unterobjekte aufzubauen.
Zum Zugriff auf die JSON-Daten kommt der Punkt zum Einsatz, sodass der Name nach der Auswertung durch person.name zugänglich ist.

Eine Personenbeschreibung wie diese kann auch in einem String stehen, die von JavaScript zur Laufzeit ausgewertet wird.

var json = 'person = { "name" : "Michael Jackson", "age" : 50 };';
eval( json );

Der Zugriff auf person.name liefert wie vorher den Namen, denn nach der Auswertung mit eval() wird JavaScript ein neues Objekt mit person im Kontext anlegen.

JSON ist besonders praktisch, wenn es darum geht, Daten zwischen einem Server und Browser mit JavaScript-Interpreter auszutauschen. Denn wenn der String json nicht von Hand mit einem String initialisiert wurde, sondern ein Server die Zeichenkette person = { … }; liefert, haben wir das, was heutzutage in modernen Ajax-Webanwendungen passiert. Die letzte Frage ist nun, wie elegant der Server Zeichenketten im Datenaustauschformat JSON erzeugt und so Objekte überträgt. Den String per Hand aufzubauen ist eine Lösung, aber es geht besser.

Die Open-Source Bibliothek  Jackson (http://jackson.codehaus.org/) gehört zu den populärsten Lösungen, die JSON-Daten einliest und ausgibt und auf JavaBeans überträgt, sodass eine unkomplizierte Serialisierung in JSON möglich wird.

ObjectMapper mapper = new ObjectMapper();
MyClass myObject = mapper.readValue( input, MyClass.class );
mapper.writeValue( output, myObject );

Der ObjectMapper übernimmt das Lesen/Schreiben. In der zweiten Zeile wird aus der Eingabequelle input gelesen und ein Objekt vom Typ MyClass rekonstruiert. In der dritten Zeile wird es in die Ausgabe output geschrieben.

JSON ist nicht nur für die Objektübertragung zwischen Server und Browser gut, sondern ist eine elegante Alternative zu XML, wenn es etwa um lokale Konfigurationsdateien geht. JSON ist viel kürzer als XML und kann somit zum Beispiel für Konfigurationsdateien übersichtlicher sein.

Labels:

Bean-Zustände kopieren

In mehrschichtigen Anwendungen gibt es oft das Muster, dass eine JavaBean etwa über eine Objekt-Relationale-Mapping-Technologie automatisch aus einer Datenbankzeile aufgebaut wird und dann internen in der Geschäftsschicht verwendet wird. Soll nun diese Information über das Netzwerk an einen anderen Rechner verteilt werden, ist es nicht immer angebracht, diese JavaBean etwa direkt über Serialisierung zu versenden. Stattdessen kann ein Transfer-Objekt aufgebaut werden, eine spezielle JavaBean zum Beispiel, sodass der Empfänger keine Abhängigkeit zu der Bean in der internen Geschäftsschicht hat. Nun werden sich aber diese Geschäftsschicht-Bean und Transfer-Bean sehr ähnlich sein und viele Entwickler scheuen die Mühe, lästigen Kopiercode zu erstellen. Doch manuelle Arbeit ist nicht nötig und eine Lösung  für das Kopierproblem ist über Refection schnell geschrieben. Über die BeanInfo kommen wir an den PropertyDescriptor (siehe dazu „Properties einer Bean erfragen“) und dann liefern getReadMethod() und getWriteMethod() die Setter/Getter. Bei einer eigenen Kopiermethode wie copyProperties(Object source, Object target) müssen wir bei der Quell-Bean jede Property auslesen und entsprechend beim Ziel-Bean nach der Property suchen und den Setter aufrufen. Wenn das ganze ohne Typkonvertierungen programmiert werden soll, sind es nur wenige Zeilen Programmcode. Kommen einfache Konvertierungen dazu, etwa wenn einmal ein Wrapper als Property-Typ genutzt wird und einmal der primitive Datentyp, ist es etwas mehr.
Der Aufwand mit einer eigenen Implementierung ist allerdings nicht nötig, denn zwei populäre Implementierungen können helfen:
•    Apache Commons BeanUtils (http://commons.apache.org/beanutils/). Die Klasse org.apache.commons.beanutils.BeanUtils bietet praktische statische Methoden wie copyProperty(Object bean, String name, Object value), copyProperties(Object dest, Object orig), Object     cloneBean(Object bean) oder populate(Object bean, Map properties).
•    Dozer (http://dozer.sourceforge.net/). Dozer bringt ausgefeilte Mapping-Möglichkeiten mit, die weit über BeansUtils hinausgehen. Das geht soweit, dass es ein Eclipse-Plugin zur Konfiguration der Abbildungen gibt.

Labels:

Process-Ströme in Dateien umlenken

Ist der Unterprozess über start() gestartet, lassen sich über das Process-Objekt die Ein-/Ausgabe-Datenströme erfragen. Die Process-Klasse bietet getInputStream(), mit dem wir an genau die Daten kommen, die der externe Prozess in seinen Ausgabestrom schreibt, denn sein Ausgabestrom ist unser Eingebestrom, den wir konsumieren können. Auch ist getErrorStream() ein InputStream, denn das, was die externe Anwendung in den Fehlerkanal schreibt, empfangen wir in einem Eingabestrom. Mit getOutputStream() bekommen wir einen OutputStream, dass das externe Programm mit Daten füttert. Dies ist der Pipe-Modus, sodass wir einfach mit externen Programmen Daten austauschen können.
Neben diesem Pipe-Modus gibt es seit Java 7 eine Alternative, die Ströme direkt auf Dateien umzulenken. Dazu definiert die ProcessBuilder-Klasse diverse redirectXXX()-Methoden. (Sollte dann ein getXXXStream()-Aufruf gemacht werden, so kommen nicht-aktive Ströme zurück, denn das externe Programm kommuniziert ja dann direkt mit einer Datei und die Java-Pipe hängt nicht dazwischen.)


class java.lang.ProcessBuilder   

§    ProcessBuilder redirectInput( File file )
ProcessBuilder redirectInput( ProcessBuilder.Redirect source )
Der Unterprozess wird die Eingaben aus der angegeben Quelle beziehen.
§    ProcessBuilder redirectOutput( File file )
§    ProcessBuilder redirectOutput( ProcessBuilder.Redirect destination )
Der Unterprozess wird Standardausgaben an das angegebene Ziel senden.
§    ProcessBuilder redirectError( File file )
§    ProcessBuilder redirectError( ProcessBuilder.Redirect destination )
Der Unterprozess wird Fehlerausgaben an das angegebene Ziel senden.

Die redirectXXX(File file)-Methoden bekommen als Ziel ein einfaches File-Objekt. Die redirectXXX()-Methoden sind aber überladen mit einem anderen Typ Redirect, der als innere statische Klasse in ProcessBuilder angelegt ist. Mit Redirect.PIPE und Redirect.INHERIT gibt es zwei Konstanten, und drei statischen Methoden Redirect.from(File), Redirect.to(File), Redirect.appendTo(File) die Redirect-Objekte für die Umleitung zur Datei liefern. Die mit File parametrisierten Methoden greifen auf die Redirect-Klasse zurück, so dass es bei redirectOutput(File file) intern auf ein redirectOutput(Redirect.to(file)) herausläuft.

Labels: ,

NumberFormat, Währungen angeben und die Klasse Currency

Die NumberFormat-Klasse liefert mit getCurrencyInstance() einen Format-Objekt, welches neben der Dezimalzahl auch noch ein Währungssymbol mit anzeigt. So liefert NumberFormat.getCurrencyInstance().format(12345.6789) dann 12.345,68 €, also automatisch mit einem Euro-Zeichen. Dass es ein Euro-Zeichen ist, und kein Yen-Symbol liegt einfach daran, dass Java standardmäßig das eingestellte Land „sieht“ und daraus die Währung ableitet.  Wenn wir explizit den Formater mit einem Land initialisieren, etwa wie in

NumberFormat frmt1 = DecimalFormat.getCurrencyInstance( Locale.FRANCE );
System.out.println( frmt1.format( 12345.6789 ) );         // 12 345,68 €

so ist die Währung automatisch Euro (denn Frankreich nutzt den Euro); schreiben wir DecimalFormat.getCurrencyInstance(Locale.JAPAN) ist sie Yen und wir bekommen ¥12,346. (Es gibt standardmäßig keine Nachkommastellen beim Yen.) Locale-Objekten repräsentieren immer eine sprachliche Region.

DecimalFormat bzw. schon die Oberklasse NumberFormat ermöglicht die explizite Angabe der Währung. In der Java-Bibliothek wird sich durch die Klasse java.util.Currency repräsentiert. NumberFormat liefert mit getCurrency() die eingestellte Currency, die zur Formatierung verwendet wird und setCurrency() setzt sie neu. Das löst Szenarios, in denen etwa ein Euro-Zeichen die Währung darstellt, aber die Zahlenformatierung englisch ist, wie die folgenden Zeilen zeigen:

NumberFormat frmt = DecimalFormat.getCurrencyInstance( Locale.ENGLISH );
frmt.setCurrency( Currency.getInstance( "EUR" ) );
System.out.println( frmt.format( 12345.6789 ) );  // EUR12,345.68

Die Currency-Klasse bietet drei statische Methoden, die Currency-Objekte liefern. Da ist einmal getAvailableCurrencies(), was ein Set liefert und die beiden Fabrikfunktion getInstance(Locale locale) und getInstance(String currencyCode). Currency-Objekte besitzen eine ganze Reihe von Objektfunktionen, die etwa den ISO 4217 Währenscode liefen oder den ausgeschriebenen Währungsnamen (und das auch noch in verschiedenen Sprachen wenn gewünscht).
Folgendes Programm geht über alle Währungen und gibt die zentralen Informationen aus:
 

for ( Currency currency : Currency.getAvailableCurrencies() )
{
  System.out.printf( "%s, %s, %s (%s)%n",
                     currency.getCurrencyCode(),
                     currency.getSymbol(),
                     currency.getDisplayName(),
                     currency.getDisplayName(Locale.ENGLISH) );
}

Wir bekommen dann mehr als 200 Ausgaben, und die Ausgabe beginnt mit:
EGP, EGP, Ägyptisches Pfund (Egyptian Pound)
IQD, IQD, Irak Dinar (Iraqi Dinar)
GHS, GHS, Ghana Cedi (Ghana Cedi)
AFN, AFN, Afghani (Afghani)
MUR, MUR, Mauritius Rupie (Mauritius Rupee)
SGD, SGD, Singapur Dollar (Singapore Dollar)

Labels: ,

Wrapperklassen-Vergleiche durchführen mit compare() und compareTo()

Haben wie zwei Ganzzahlen 1 und 2 vor uns, so ist es trivial zu sagen, dass 1 kleiner 2 ist. Bei Fließkommazahlen ist das ein wenig komplizierter, da es hier „Sonderzahlen“ wie Unendlich oder eine negative bzw. positive null gibt. Da insbesondere Vergleichsalgorithmen die Beantwortung der Frage, ob zwei Werte a und b kleiner, größer oder gleich sind, erwarten, gibt es zwei Typen von Methoden in den Wrapper-Klassen.

·    Sie implementieren eine Objektmethode compareTo(). Die Methode ist nicht zufällig da, denn  Wrapper-Klassen implementieren die Schnittstelle Comparable. (Wir haben die Schnittstelle schon im Kapitel 6 kurz vorgestellt.)

·    Wrapper-Klassen besitzen statische compare()-Methoden.

Die Rückgabe der Methoden ist ein int und es kodiert, ob ein Wert größer, kleiner oder gleich ist.

Beispiel   Teste verschiedene Werte.

System.out.println( Integer.compare(1, 2) );        // -1
System.out.println( Integer.compare(1, 1) );        //  0
System.out.println( Integer.compare(2, 1) );        //  1

System.out.println( Double.compare(2.0, 2.1) );     // -1
System.out.println( Double.compare(Double.NaN, 0) );// 1

System.out.println( Boolean.compare(true, false) ); //  1
System.out.println( Boolean.compare(false, true) ); // -1

Ein true ist „größer“ als als false.   

Die Tabelle fasst von den Wrapper-Klassen die Methoden zusammen.

Klasse    Methode aus Comparable    Statische Methode compare()      
Byte    int compareTo(Byte anotherByte)    int compare(int x, int y)      
Short    int compareTo(Short anotherShort)    int compare(short x, short y)      
Float    int compareTo(Float anotherFloat)    int compare(float f1, float f2)      
Double    int compareTo(Double anotherDouble)    int compare(double d1, double d2)      
Integer    int compareTo(Integer anotherInteger)    int compare(int x, int y)      
Long    int compareTo(Long anotherLong)    int compare(long x, long y)      
Character    int compareTo(Character anotherCharacter)    int compare(char x, char y)      
Boolean    int compareTo(Boolean b)    int compare(boolean x, boolean y)   

Die Implementierung einer statischen Methode WrapperKlasse.compare() ist äquivalent zu WrapperKlasse.valueOf(x).compareTo(WrapperKlasse.valueOf(y)).

Die Klassen BigInteger, BigDecimal implementieren zwar Number und somit Comparable, aber eine statische compare()-Methode bieten sie nicht. Auch String implementiert Comparable, aber eine statische Methode fehlt. Der Grund ist, dass es eine statische Methode Objects.compare() gibt, zwei Objekte mit einem Comperator vergleicht.

Labels: ,

Die Utility-Klasse java.lang.Objects

In Java 7 ist die Klasse Objects hinzugekommen, die einige statische Utility-Funktionen bereithält. Sie führen in erster Linie null-Tests durch.

null-Tests um equals()/hashCode()/toString()

Ist zum Beispiel eine Objektvariable name einer Person null, so kann nicht einfach name.toString() aufgerufen werden, ohne dass eine NullPointerException folgt. Drei Methoden von Objects führen null-Test durch, bevor sie an die Object-Methode equals()/hashCode()/toString() weiterleiten. Eine zusätzliche Hilfsmethode arbeitet mit Comparatoren, die in im Kapitel über Datenstrukturen genauer vorgestellt werden.

class java.lang.Objects
§          static boolean equals( Object a, Object b )
Liefert true wenn beide Argument entweder null sind, oder a.equls(b) ebenfalls true ergibt. Sonst false. Das Objects.equals(null, null) die Rückgabe true ergibt ist sinnvoll und so erspart die Methode einige händische Tests.
§          static int hashCode( Object o )
Liefert 0 wenn o gleich null ist, sonst o.hashCode().
§          static String toString(Object o)
Liefert den String "null" wen das Argument null ist sonst o.toString().
§          static int compare( T a, T b, Comparator c )
Liefert 0, wenn a und b beide entweder null sind, oder der Comparator die Objekte a und b für gleich erklärt. Sind a und b beide ungleich null, so ist die Rückgabe c.compare(a, b). Ist nur a oder b gleich null, so hängt es vom Comparator ab und der Reihenfolge der Parameter ab.
Erinnern wir uns ans hashCode() vom Spieler, wo der Spielername in den Hashcode eingehen soll, so sehen wir, wo die statische Objects.hashCode()-Methode gut untergebracht werden kann.
com/tutego/insel/object/hashcode/Player.java, hashCode() Ausschnitt
result = 31 * result + ((name == null) ? 0 : name.hashCode());
Mit Objects.hashCode() verkürzt sich dies – nicht spektakulär im Sinne von eingesparten Zeichen – zu:
result = 31 * result + Objects.hashCode( name.hashCode() );

Null-Prüfungen mit eingebauter Ausnahmebehandlung

Zu den drei statischen Methoden kommen zwei hinzu, die null-Prüfungen übernehmen und im Fehlerfall eine Ausnahme auslösen. Das ist praktisch bei Konstruktoren oder Settern, die Werte initialisieren sollen, aber verhindern möchten, dass null durchgeleitet wird.

Beispiel   Die Methde setName() soll keine name-Argument gleich null erlauben.
public void setName( String name )
{
 this.name = Objects.nonNull( name );
}
Alternativ ist eine Fehlermeldung möglich:
public void setName( String name )
{
 this.name = Objects.nonNull( name, "name is not supposed to be null" );
}


class java.lang.Objects

§          static T nonNull( T obj )
Löst eine NullPointerExcpetion aus, wenn obj gleich null ist. Sonst lieferte obj als Rückgabe.
§          static T nonNull( T obj, String message )
Wie nonNull(obj), nur das die Meldung der NullPointerExcpeption bestimmt wird.

Labels: ,

equals()/hashCode() tief oder flach und mehrdimensionale Arrays

Tiefe oder flache Vergleiche/Hash-Werte

Referenziert ein Objekt Unterobjekte (etwa eine Person ein String-Objekt für den Namen – keine Primitiven – so geben die Methoden equals() und hashCode()den Vergleich bzw. Berechung des Hashcodes an das referenzierte Unterobjekt weiter (wenn es denn nicht-null ist). Ablesen können wir das an folgendem Ausschnitt unserer equals()-Methode.
com/tutego/insel/object/hashcode/Player.java, equals() Ausschnitt
if ( name == null )
  if ( ((Player)that).name != null )
    return false;
else if ( !name.equals( ((Player)that).name ) )
  return false;

Es ist demnach Aufgabe der String-Klasse (name ist vom Typ String) den Gleichheitstest vorzunehmen. Das heißt, dass zwei Personen problemlos equals()-gleich sein können, auch wenn sie zwei nicht-identische, aber equals()-gleiche String-Objekte referenzieren.
Auch bei hashCode() ist diese Delegation an das referenzierte Unterobjekt abzulesen:
com/tutego/insel/object/hashcode/Player.java, hashCode() Ausschnitt
result = 31 * result + ((name == null) ? 0 : name.hashCode());
Dass eine equals()-Methode bzw. hashCode() einer Klasse den Vergleich bzw. die Hashcode-Berechnung nicht an die Unterobjekte delegiert, sondern selbst umsetzt, ist unüblich.

equals()/hashCode()-Berechnung bei (mehrdimensionalen) Arrays

Einen gewissen Sonderfall bei equals()/hashCode() nehmen mehrdimensionale Arrays ein. Mehrdimensionale Arrays sind nichts anderes als Arrays von Arrays. Das erste Array für die erste Dimension referenziert jeweils auf Unterarrays für die zweite Dimension. Wichtig wird diese Realisierung bei der Frage, wie diese Verweise der ersten Dimension nun bei equals() betrachtet werden sollen. Denn hier stellt sich die Frage, ob die Unterarrays von zwei zu testenden Arrays nur identisch oder auch gleich sein dürfen. Diese Frage hatten wir schon im Kapitel 3, „Felder vergleichen mit Arrays.equals() und Arrays.deepEquals()“ angesprochen.
Enthält unsere Klasse ein Array und es soll in einem equals() mit berücksichtigt werden, so sind prinzipiell drei Varianten zum Umgang mit diesem Array möglich. Felder selbst einfach mit == wie primitive Werte zu vergleichen ist keine gute Lösung, da Arrays Objekte sind, die wie Strings nicht einfach mit == zu vergleichen sind. Während allerdings Objekte ein equals() haben, bieten Arrays keine eigene equals()-Methode, sondern diese ist in die Utility-Klasse Arrays gewandert. Hier gibt es jedoch 2 Methoden die die in Frage kämen. Arrays.equals(Object[] a, Object[] a2)  geht jedes Element von a, also bei mehrdimensionalen Arrays jede Referenz auf ein Unterarray durch, und testet, ob es identisch zu einem zweiten Feld a2 ist. Wenn also zwei gleiche aber nicht-identische Hauptarrays identische Unterarray besitzen, liefert Arrays.equals() die Rückgabe true, aber nicht, wenn die Unterarrays zwar gleich, aber nicht identisch sind. Spielt das eine Rolle, so ist Arrays.deepEquals() die passende Methode, denn sie geht fragt immer mit equals() die Unterarray ab.
Bei der Berechnung vom Hash-Wert gibt es eine vergleichbare Frage. Die Arrays-Klasse bietet zur Berechnung vom Hash-Wert eines ganzes Arrays die Methoden Arrays.hashCode() und Arrays.deepHashCode(). Die erste Methode fragt jedes Unterelement über die von Object angebotene Methode hashCode()  nach dem Hash-Wert. Nehmen wir ein mehrdimensionales Array an. Dann ist das Unterelement ebenfalls ein Feld. Arrays.hashCode() wird dann wie erwähnt nur die hashCode()-Methode auf dem Feld-Objekt aufrufen, während Arrays.deepHashCode() auch Unterarray herabsteigt und solange Arrays.deepHashCode() auf allen Unterfeldern aufruft, bis ein equals()-Vergleich auf einem nicht-Feld möglich ist.
Was heißt das nun für unsere equals()/hashCode()-Methode? Üblich ist der Einsatz von Arrays.equals() und nicht von Arrays.deepEquals() genauso wie Arrays.hashCode() üblicher als Arrays.deepHashCode() ist.
Das folgende Beispiel zeigt das in der Anwendung. Die Methoden wurden von Eclipse generiert und etwas kompakter geschrieben:
com/tutego/insel/object/hashcode/Chess.java, Chess
char[][] chessboard;

@Override public int hashCode()
{
  return 31 + Arrays.hashCode( chessboard );
}

@Override public boolean equals( Object obj )
{
  if ( this == obj )
    return true;
  if ( obj == null )
    return false;
  if ( getClass() != obj.getClass() )
    return false;
  if ( ! Arrays.equals( chessboard, ((Chess) obj).chessboard ) )
    return false;
  return true;
}

Labels:

NIO.2: Wahlfreier Zugriff mit SeekableByteChannel und ByteBuffer

Für den wahlfreien Zugriff auf Bytes in Dateien bietet Java seit Version 1.0 die Klasse RandomAccessFile. Unter Java 7 ist die Klasse nun nicht mehr nötig, und genaugenommen auch schon seit Java 1.4 nicht mehr, denn in Java 1.4 wurde das erste NIO-Paket eingeführt und mit ihm sogenannte Channel-Klassen, die eine offene Verbindung mit einem Datenkanal repräsentieren. Neu in Java 7 ist die Verbindung von Path und einem besonderen Channel für wahlfreie Ein-/Ausgabe, dem SeekableByteChannel, der ebenfalls in Java 7 eingeführt wurde.

SeekableByteChannel

Der SeekableByteChannel deklariert Operationen zum Lesen und Schreiben von Daten und zur Positionierung des Dateizeigers.

public interface java.nio.channels.SeekableByteChannel

extends ByteChannel

§ int read( ByteBuffer dst )

§ int write( ByteBuffer src )

§ long size()

§ long position()

§ SeekableByteChannel position( long newPosition )

§ SeekableByteChannel truncate( long size )

Die Methoden close() und isOpen() kommen aus Channel hinzu.

Es fällt auf, und das ist einer der großen Unterschiede zu RandomAccessFile, dass SeekableByteChannel kein byte-Feld oder einzelne Bytes liest oder schreibt, sondern einen ganz eigenen Typ, einen ByteBuffer, erwartet.

ByteBuffer

Ein ByteBuffer ist einem Byte-Feld sehr ähnlich; seine maximale Größe wird vorher festgelegt und kann später nicht dynamisch wachsen. Ist ein ByteBuffer angelegt, so können über einen Index die einzelnen Bytes gelesen und geschrieben werden, zum Beispiel mit byte get() oder put(byte b) relativ zur letzten Position, oder mit byte get(int index) und put(int index, byte b) absolut. Der wirkliche Unterschied ist aber, dass Java zwei verschiedene Arten von ByteBuffer-Implementierungen bietet (ByteBuffer ist eine abstrakte Klasse):

· Nicht-direkte ByteBuffer sind wie byte[]-Felder, also Java-Objekte, die auf dem Heap Platz einnehmen.

· Bei einem direkten ByteBuffer versucht Java einen Speicherbereich vom Betriebssystem zu bekommen. Während die nicht-direkten ByteBuffer und byte-Arrays auf dem Heap leben, und er normalen GC unterworfen sind, sollten die direkten ByteBuffer vom Betriebssystem verwaltet werden. Im Idealfall sind dadurch hohe Ein-/Ausgabegeschwindigkeiten möglich, denn mit direkten ByteBuffern kann sich das Betriebssystem Kopieroperationen zwischen nativen und Java-Puffern sparen.

Die Methoden auf direkten oder nicht-direkten ByteBuffern sind identisch. Insbesondere speichern die alle Puffer Zustände: Die Position, einen Limit und eine Kapazität. Diesen Eigenschaften wollen wir aber in der Insel nicht nachgehen.

Beispiel mit Path + SeekableByteChannel + ByteBuffer

Das folgende Beispiel fasst alles zusammen: Von einem Path wird über newByteChannel ein SeekableByteChannel erfragt. Anschließen leiten wir aus einer Zeichenkette über das byte[] einen nicht-direkten ByteBuffer ab und schreiben diesen in den SeekableByteChannel, sodass später die Datei Kurt Cobain.txt einen ASCII-Text enthält.

com/tutego/insel/nio2/SeekableByteChannelDemo.java, main()

Path p = Paths.get( "Kurt Cobain.txt" );

SeekableByteChannel raf = p.newByteChannel( StandardOpenOption.CREATE,

StandardOpenOption.WRITE );

String s = "Drugs are bad for you. ";

ByteBuffer byteBuffer = ByteBuffer.wrap( s.getBytes() );

raf.write( byteBuffer );

raf.write( ByteBuffer.wrap( "They will f*ck you up.".getBytes() ) );

raf.position( 34 );

raf.write( ByteBuffer.wrap( new byte[]{'u'} ) );

raf.close();

Das Beispiel zeigt, dass mit ByteBuffer.wrap() aus dem byte[] der Strings ein nicht-direkter Buffer angelegt wird, den write() dann in den Kanal schreibt.

kurt-in-hex

Nur zum Testen schreiben wie ASCII-Zeichen, was aber im „echten Leben“ eher nicht der Fall sein wird, denn wir müssen hier die korrekten Zeichenkodierungen beachten. Auch für sequenzielle Schreiboperationen ist der SeekableByteChannel eher weniger komfortabel – dennoch ist der Einsatz von Kanälen nicht per se falsch. Im nächsten Kapitel werden die Ströme vorgestellt, mit denen das Schreiben, insbesondere von Textdokumenten, viel einfacher wird.

FileChannel

Die Schnittstelle SeekableByteChannel gibt Operationen an, um die aktuelle Position auszulesen und den Positionszeiger neu zu setzen und über ByteBuffer Bytes und Bytefolgen zu lesen und zu schreiben. SeekableByteChannel ist dabei nicht an Dateien gebunden und enthält keine Informationen zu Dateipfaden oder sonstigen tiefer liegenden Schichten. Und da Path grundsätzlich ein Pfad auf alles Mögliche sein kann, etwa auf ein BLOB in der Datenbank, liefert newByteChannel() eine Rückgabe mindestens von Typ SeekableByteChannel, und damit erst ein mal keine Möglichkeiten dateispezifische Operationen vorzunehmen.

Wird allerdings newByteChannel() auf einem Pfad aufgerufen, der eine Datei vom Dateisystem repräsentiert, so ist die Rückgabe nicht einfach nur ein SeekableByteChannel, sondern der Untertyp FileChannel.[1] Ein Typcast ist daher möglich:

Path p = Paths.get( "Kurt Cobain.txt" );

FileChannel channel = (FileChannel) p.newByteChannel( options );

Da FileChannel die Schnittstelle SeekableByteChannel implementiert, bietet natürlich FileChannel alle Methoden zum Lesen, Schreiben und Positionieren. Zusätzlich bietet FileChannel aber Methoden, die explizit an Dateien gebunden sind. Drei Methoden fallen sofort auf:

· lock(): Sperrt die Datei (oder Dateiteile) für andere, soweit es das Betriebssystem unterstützt.

· force(): Updates werden sofort materialisiert, das heißt auf das Dateisystem übertragen.

· map(): Blendet die Datei, oder einen Teil der Datei, in den Speicher ein.

Die Methode map() ist besonders interessant. Damit kann ein FileChannel auf ein ByteBuffer abgebildet werden, sodass unsere Lese-/Schreiboperationen auf dem ByteBuffer direkt aus der Datei kommen oder direkt in die Datei gehen. Das Betriebssystem versucht sein bestes, die Operationen zu optimieren und geeignete Blöcke der Datei in den Speicher zu laden. Java und das Betriebssystem tuen damit ihr Bestes, die Operationen so schnell wie möglich und mit wenigen Kopieroperationen zwischen den internen Puffern vom Dateisystem und den Java-Puffern durchführen.

Das folgende Beispiel bezieht im ersten Schritt über newByteChannel() den FileChannel. Anschließend bildet die Methode map() die gesamte Datei auf einen MappedByteBuffer ab, der ein ByteBuffer ist, wie wir ihn im letzten Beispiel schon kennengelernt haben. Wir könnten nun Methoden auf dem ByteBuffer aufrufen, und die Bytes auslesen, doch hier gehen wir etwa anders vor: Die Bytes des ByteBuffer konvertiert ein CharsetDecoder von ASCII in Java-Unicode; das Ergebnis ist ein CharBuffer. Den CharBuffer laufen wir ab und geben die Zeichen auf der Konsole aus.

com/tutego/insel/nio2/FileChannelDemo.java, main()

Path p = Paths.get( "Kurt Cobain.txt" );

FileChannel fileChannel = (FileChannel) p.newByteChannel( StandardOpenOption.READ );

ByteBuffer byteBuffer = fileChannel.map( FileChannel.MapMode.READ_ONLY,

0, fileChannel.size() );

CharsetDecoder decoder = Charset.forName( "ASCII" ).newDecoder();

CharBuffer charBuffer = decoder.decode( byteBuffer );

while ( charBuffer.hasRemaining() )

System.out.print( charBuffer.get() );

Beim FileChannel gilt das gleiche wie beim SeekableByteChannel. Sequenzieller Lese- oder Schreibzugriff wird am Einfachsten über die Strom-Klassen realisiert. Sie werden im folgenden Kapitel vorgestellt.


[1] Den Typ FileChannel gibt es in Java schon länger, seit Java 1.4. Und vor Java 7 lieferten die Methoden getChannel() von FileInputStream, FileOutputStream und RandomAccessFile den FileChannel.

Labels: ,

Vorhandene Runtime-Fehlertypen kennen und nutzen

Die Java-API bietet eine große Anzahl von Exception-Klassen und so muss nicht für jeden Fall eine eigene Exception-Klasse deklariert werden. Viele Standard-Fälle, wie falsche Argumente, können durch Standard-Exception-Klassen abgedeckt werden. Einige Standard-Runtime-Exception-Unterklassen des java.lang-Pakets in der Übersicht:

IllegalArgumentException

Die IllegalArgumentException zeigt an, dass ein Parameter nicht korrekt angegeben ist. Diesen Fehlertyp lässt sich somit nur bei Konstruktoren oder Methoden ausmachen, denen fehlerhafte Argumente übergeben wurden. Oft ist der Grund die Missachtung des Wertebereiches. Wenn die Werte grundsätzlich korrekt sind, darf dieser Fehlertyp nicht ausgelöst werden.

IllegalStateException

Objekte haben in der Regel Zustände. Gilt es Operationen auszuführen, aber die Zustände sind nicht korrekt, so kann die Methode eine IllegalStateException auslösen und so anzeigen, dass in dem aktuellen Zustand die Operation nicht möglich ist. Wäre der Zustand korrekt, kommt es nicht zu der Ausnahme. Bei statischen Methoden sollte es eine IllegalStateException nicht geben.

UnsupportedOperationException

Implementieren Klassen Schnittstellen oder realisieren Klassen abstrakte Methoden von Oberklassen, so muss es immer eine Implementierung geben, auch wenn die Unterklasse die Operation eigentlich gar nicht umsetzen kann oder will. Statt den Rumpf der Methode nur leer zu lassen und einen potentiellen Aufrufer in den Glauben zu setzen, die Methode führt etwas aus, sollten diesen Methoden eine UnsupportedOperationException auslösen. In den API-Dokumentationen werden Methoden, die Unterklassen vielleicht nicht realisieren wollen, als „optionale Operation“ gekennzeichnet.

<pic: optional operation.png, „Optionale Operationen in der Schnittstelle java.util.List“>

Unglücklicherweise gibt es auch eine javax.naming.OperationNotSupportedException. Doch diese sollte nicht verwendet werden. Sie ist speziell für Namensdienste und auch keine RuntimeException.

IndexOutOfBoundsException

Eine IndexOutOfBoundsException lost die JVM automatisch aus, wenn zum Beispiel die Grenzen eines Arrays missachtet werden. Wir können diesen Ausnahmetyp selbst immer dann nutzen, wenn wir Index-Zugriffe haben, etwa auf eine Zeile in einer Datei, und der Index im falschen Bereich liegt. Von IndexOutOfBoundsException gibt es die Unterklassen ArrayIndexOutOfBoundsException und StringIndexOutOfBoundsException. Programmierer werden diese Typen aber in der Regel nicht nutzen. Inkonsistenzen gibt es beim Einsatz von IllegalArgumentException und IndexOutOfBoundsException. Ist etwa der Index falsch, so entscheiden sich einige Autoren für den ersten Fehlertyp, andere für den zweiten. Beides ist prinzipiell gültig. Die IndexOutOfBoundsException ist aber konkreter und zeigt eher ein Implementierungsdetail an.

Keine eigene NullPointerException

Eine NullPointerException gehört mit zu den häufigsten Ausnahmen. Die JVM löst diesen Fehler etwa bei folgendem Programmstück aus:

String s = null;

s.length();

Eine NullPointerException zeigt immer einen Programmierfehler in einem Stück Code an und so gibt es in der Regel keinen Sinn, diesen Fehler abzufragen – der Programmierfehler muss behoben werden. Aus diesem Grund wird eine NullPointerException in der Regel nie explizit vom Programmierer ausgelöst, sondern von der JVM.

Oft gibt es diese NullPointerException, wenn an Methoden null-Werte übergeben wurden. Hier muss aus der API-Dokumentation klar hervorgehen, ob null als Argument erlaubt ist, oder nicht. Wenn nicht, ist es völlig in Ordnung, wenn die Methode eine NullPointerException auslöst, wenn fälschlicherweise doch null übergeben wurde. Auf null zu prüfen, um dann eine zum Beispiel IllegalArgumentException auszulösen, ist eigentlich nicht nötig. Allerdings gilt auch hier, dass IllegalArgumentException allgemeiner und weniger Implementierungsspezifisch als eine NullPointerException ist.

Labels:

NIO.2: Verzeichnisse im Dateisystem überwachen

Schreibt eine Anwendung etwa Log-Dateien in ein Verzeichnis, und ein anderes Programm soll dies erkennen, so gibt es bis Java 7 nur die Möglichkeit, laufend das Verzeichnis abzulaufen und nach Änderungen zu durchsuchen. Auch der Rückgriff auf native Zusatzbibliotheken wie http://jnotify.sourceforge.net/ ist eine Lösung. Seit Java 7 hat sich das geändert und Sun hat einen WatchService spendiert. Dazu ein Beispiel, um auf Änderungen im Verzeichnis C:/ zu reagieren.

com/tutego/insel/nio2/WatchServiceDemo.java, main()

WatchService watcher = FileSystems.getDefault().newWatchService();

Paths.get( "C:/" ).register( watcher, StandardWatchEventKind.ENTRY_CREATE,

StandardWatchEventKind.ENTRY_DELETE,

StandardWatchEventKind.ENTRY_MODIFY );

while ( true )

{

WatchKey key = watcher.take();

System.out.println( "Found" );

for ( WatchEvent<?> event : key.pollEvents() )

System.out.println( "Kind: " + event.kind() + ", Path: " + event.context() );

key.reset();

}

Ein Ablauf kann so aussehen:

Found

Kind: ENTRY_CREATE, Path: tutego - Kopie.log

Kind: ENTRY_MODIFY, Path: tutego - Kopie.log

Found

Kind: ENTRY_MODIFY, Path: tutego - Kopie.log

Found

Kind: ENTRY_DELETE, Path: tutego - Kopie.log

Kind: ENTRY_CREATE, Path: tutego2.log

Kind: ENTRY_MODIFY, Path: tutego2.log

Found

Kind: ENTRY_DELETE, Path: tutego2.log

Ein WatchService überwacht Änderungen an Watchable-Objekten. Path ist bisher die einzige Klasse, die die Schnittstelle Watchable implementiert. Grundsätzlich ist der Überwachungsdienst nicht an Dateien und Verzeichnissen gebunden, doch Sun hat WatchService und Watchable in das Paket java.nio.file gelegt. Es bleibt abzuwarten, ob Sun dieses API auch noch für andere Dinge nutzen wird.

Das Watchable meldet sich mit register() am WatchService an. Da es verschiede Ereignistypen gibt, ist register() mit einem Varargs-Parametertyp deklariert, der WatchEvent.Kind fordert: die Aufzählung StandardWatchEventKind deklariert drei mögliche WatchEvent.Kind-Typen.

Nach der Registrierung wird der Watcher nach den angefallen Ereignissen gefragt. Hier gibt es eine Variante mit take(), die blockierend wartet, oder poll(), die, falls kein Ereignisgruppe vorliegt, null liefert. Mit close() lässt sich der WachService beenden.

Das mit take() oder poll() entnommene Element ist vom Typ WatchKey. Ein WatchKey ist eine Art Gruppe aus einzelnen WatchEvents. Auf einem WatchKey-Objekt liefert pollEvents() genau diese List<WatchEvent<?>>. Vom WatchEvent erfragt kind() den WatchEvent.Kind, liefert also etwa StandardWatchEventKind.ENTRY_CREATE und context() das Objekt, auf das es sich bezog. Bei Dateisystemen dürfte context()immer ein Path liefert, aber der Rückgabetyp von context() ist mit T parametrisiert, was uns aber nicht hilft, denn key.pollEvents() liefert nur ein WatchEvent<?>, also ohne Typ. Hier zeigt sich, dass Wildcards in der Rückgabe nicht besonders hilfreich sind.

Labels:

NIO.2: Dateien kopieren und verschieben

Zum Kopieren und Verschieben von Dateien und Verzeichnissen bietet die Path-Klasse insgesamt zwei Methoden an:

· Path copyTo( Path target, CopyOption... options ) throws IOException

· Path moveTo( Path target, CopyOption... options ) throws IOException

Da CopyOption ein Vararg ist, ist der Aufruf ohne Zusatzoptionen sehr einfach – nur ein Zielort muss angegeben werden. Beide Operationen führen zu einer IOException, wenn die Dateien/Verzeichnisse nicht kopiert oder verschoben werden konnten. Das ist insbesondere wichtig, wenn sich die Dateiattribute nicht übertragen lassen. Das bringt uns zu den optionalen CopyOption-Elementen. CopyOption ist eine Schnittstelle, die von zwei Aufzählungen StandardCopyOption und LinkOption wie folgt implementiert werden:

java/nio/file/StandardCopyOption.java, StandardCopyOption

public enum StandardCopyOption implements CopyOption

{

REPLACE_EXISTING,

COPY_ATTRIBUTES,

ATOMIC_MOVE;

}

java/nio/file/LinkOption.java, LinkOption

public enum LinkOption implements OpenOption, CopyOption

{

NOFOLLOW_LINKS;

}

StandardCopyOption und LinkOption stellen somit gültige Argumente für copyTo() und moveTo() dar. Die Bedeutung der Aufzählungselemente ist wie folgt:

Argument

Bedeutung bei copyTo()

Bedeutung bei moveTo()

REPLACE_EXISTING

Ersetzt falls vorhanden die Datei bzw. das Verzeichnis am Zielort. Ist das Ziel ein existierender symbolischer Link, so wird nur der Link selbst ersetzt, aber nicht die Datei/der Verzeichnis, auf die der Link zeigt.

COPY_ATTRIBUTES

Versucht alle Attribute zu kopieren.

-

NOFOLLOW_LINKS

Ist der Path ein Link, so wird nur der Link selbst kopiert, aber nicht die Datei, auf die der Link zeigt.

-

ATOMIC_MOVE

-

Führt das Verschieben (also Anlegen der Kopie und Löschen des Originals) atomar durch. Führt zu AtomicMoveNotSupportedException, falls das Dateisystem dies nicht unterstützt.

Falls REPLACE_EXISTING nicht angegeben ist, um im Zielordner schon eine Datei/ein Verzeichnis existiert, lösen copyTo() und moveTo() eine FileAlreadyExistsException aus. Das Kopieren von Dateien ist nicht automatisch atomar und eine Option lässt sich auch nicht setzen.

Im Fall von Verzeichnissen wird copyTo() nur ein leeres Verzeichnis anlegen, aber nicht die Dateien eines Quellverzeichnisses automatisch mitkopieren. Das muss per Hand übernommen werden (Files.walkFileTree() ist für diesen Fall ganz gut geeignet und hilft beim Ablaufen von Verzeichnisbäumen.) Die Semantik bei moveTo() und nicht-leeren Verzeichnissen ist komplizierter, da es hier darauf ankommt, ob es sich um ein Verschieben auf dem lokalen Dateisystem handelt (also eine Art umbenennen), oder ein Verschieben auf zum Beispiel einem anderen Laufwerk. Wenn die Einträge in einem Verzeichnis wirklich auf einem anderen Dateisystem verschoben werden müssen, so übernimmt moveTo() diese Arbeit nicht; hier müssen wird selbst per copyTo() auf der Ebene der einzelnen Einträge kopieren.

Labels:

“Umsetzen der Generics, Typlöschung und Raw-Types”, Generics-Tutorial Teil 2

Zum Verständnis der Generics und was zur Laufzeit an Informationen vorhanden ist, lohnt es sich, anzuschauen, wie der Compiler Generics in Bytecode übersetzt.

1.1.1 Realisierungsmöglichkeiten

Im Allgemeinen gibt es zwei Realisierungsmöglichkeiten von generischen Typen:

  • Heterogene Variante. Für jeden Typ (etwa String, Integer, Point) wird individueller Code erzeugt, also drei Klassendateien. Die Variante nennt sich auch Code-Spezialisierung.
  • Homogene Übersetzung. Aus der parametrisierten Klasse wird eine Klasse erzeugt, die anstelle des Typparameters nur Object einsetzt. Für den konkreten Typparameter werden Typanpassungen in die Anweisungen eingebaut.

Java nutzt die homogene Übersetzung und der Compiler erzeugt nur eine Klassendatei. Es gibt keine multiplen Kopien der Klasse weder im Bytecode noch im Speicher.

1.1.2 Typlöschung (Type Erasure)

Übersetzt der Java-Compiler die generischen Anwendungen, so löscht er dabei alle Typinformationen, da die Java Laufzeitumgebung keine Generics im Typsystem hat. Wir können uns das so vorstellen, dass alles was in eckigen Klammen steht wegfällt und jede Typvariable zu Object wird.[1]

Mit Generics

Nach der Typlöschung

public class Pocket<T>

{

private T value;

public void set( T value ) { this.value = value; }

public T get() { return value; }

}

public class Pocket

{

private Object value;

public void set( Object value ) { this.value = value; }

public Object get() { return value; }

}

So entspricht der Programmcode nach der Typlöschung genau dem, was wir selbst auch ohne Generics am Anfang programmiert haben. Auch bei der Nutzung wird gelöscht:

Mit Generics

Nach der Typlöschung

Pocket<Integer> p = new Pocket<Integer>( 1 );

p.set( 1 );

Integer i = p.get();

Pocket p = new Pocket( 1 );

p.set( 1 );

Integer i = (Integer) p.get();

Beim Herausholen fügt der Compiler genau die explizite Typanpassung ein, die wir in unserem ersten Beispiel noch von Hand eingesetzt haben.

Aber… Wenn der Compiler Bytecode erzeugt, der auch für ältere JVMs keine Probleme bereitet, so stellt sich die Frage, wo denn die Informationen abgespeichert sind, dass ein Typ generisch deklariert wurde oder nicht. Irgendwo muss das stehen, denn der Compiler weiß das ja. Die Antwort ist, dass der Compiler diese Typinformationen, die nicht Teil des Typsystems der JVM sind, als Signature-Attribut in dem Konstantenpool des Bytecodes legt. Das Attribut ist ein UTF-8 Text, der von älteren Compilern als Kommentar überlesen wird. Mit dem Diassembler javap und dem Schalter –verbose lassen sich diese Informationen anzeigen. Interessierte bekommen weitere Informationen unter http://java.sun.com/docs/books/jvms/second_edition/jvms-clarify.html.

Das große Ziel: Interoperabilität

Interoperabilität stand bei der Einführung der Generics ganz oben auf der Liste. Zwei wichtige Anforderungen sind:

  • Die neuen mit Generics deklarieren Klassen – wie List<E> – müssen auf jeden Fall noch vom alten Programmcode, der zum Beispiel mit einem Java 1.4 Compiler erzeugt wurde, nutzbar sein. Es gibt Millionen Zeilen alten Quellcode, die Listen nutzen, und nicht sofort fasst ein Team alle Programmstellen an und führt Typarameter ein. Daher nutzt Java die homogene Übersetzung über Typlöschung. Hätte Sun sich nicht dieses Kompatibilitätsziel auf die Fahnen geschrieben, hätte die Umsetzung auch anders ausfallen können. Denn die Konsequenz der Typlöschung ist, dass es keine Informationen über den Typparameter zur Laufzeit gibt. Das führt zu Überraschungen und Einschränkungen (insbesondere bei Arrays), die wir uns gleich anschauen werden.
  • Auf der anderen Seite gibt es alten Programmcode, der zum Beispiel Listen nutzt, und den wir nicht anfassen können, weil es zum Beispiel in einer gekauften Bibliothek ist. Als Nutzer wollen wir eine Typvariable einführen, auch wenn die Bibliotheksfunktion (noch) keinen formalen Typparameter aufweist.

Java Generics und C++ Templates C++ nutzt die heterogene Variante und generiert für jeden genutzten Template-Typ unterschiedlichen (und wunderbar optimierten) Maschinencode. Im Fall von Java würde die heterogene Variante zu sehr vielen sehr ähnlichen Klassen führen, die sich nur in ein paar Typanpassungen unterschieden. Und da in Java sowieso nur Referenzen als Typvariablen möglich sind, und keine primitiven Typen, ist auch eine besondere Optimierung an der Stelle nicht möglich.

1.1.3 Probleme aus der Typlöschung

Typlöschung ist für die Laufzeitumgebung praktisch, weil sie überhaupt nicht auf die Generics angepasst werden muss. Die seit Java 5 zum Beispiel generisch deklarierten Datenstrukturen sehen nach dem Übersetzungsvorgang genauso aus wie unter Java 1.4 und sind damit voll kompatibel. Sonst aber stellt die Typlöschung ein riesiges Problem dar, weil dann die Typinformationen zur Laufzeit nicht vorhanden sind.[2]

Reified Generics Für Java 7 stand auf der Liste, die generischen Parameter auch zur Laufzeit zugänglich zu machen. Das wurde jedoch verschoben und kommt vielleicht irgendwann, Java 8, Java 9, Java 2020, … Der Stichwort dazu ist Reified Generics.

Kein new T

Da durch die Typlöschung bei Deklarationen wie Pocket<T> die Parametervariable durch Object ersetzt wird, lässt sich zum Beispiel in der Tasche nicht folgendes schreiben, um ein neues Exemplar eines Tascheninhaltes zu erzeugen:

Gedacht: Mit Generics (Compilerfehler!)

Konsequenz aus Typlöschung

class Pocket<T>

{

T newPocketContent() { return new T(); }

}

class Pocket<T>

{

Object newPocketContent() { return new Object(); }

}

Als Aufrufer von newPocketContent() erwarten wir aber nicht immer ein lächerliches Object, sondern ein Objekt vom Typ T.

Kein instanceof

Der instanceof-Operator ist bei parametrisierten Typen ungültig, auch wenn es praktisch wäre, um zum Beispiel auf Grund der tatsächlichen Typen eine Fallunterscheidung vornehmen zu können:

void printType( Pocket<?> pocket )
{

if ( p instanceof Pocket<Number> ) // illegal generic type for instanceof

System.out.println( "Pocket mit Number" );

else if ( p instanceof Pocket<String> ) // illegal generic type for instanceof

System.out.println( "Pocket mit String" );
}

Der Compiler meldet zu Recht einen Fehler – nicht nur eine Warnung –, weil es die Typen Pocket<String> und Pocket<Number> zur Laufzeit gar nicht gibt: Es sind nur typgelöschte Pocket-Objekte. Nach der Typlöschung entstünde:

void printType( Pocket pocket )
{

if ( p instanceof Pocket )


else if ( p instanceof Pocket )

}

Keine Typanpassungen

Typanpassungen wie

Pocket<String> pocket = (Pocket<String>) new Pocket<Integer>();

sind illegal. Wir haben ja extra Generics, damit der Compiler die Typen testet. Und durch die Typlöschung verschwindet der Typparameter, sodass der Compiler erzeugen würde:

Pocket pocket = (Pocket) new Pocket();

Kein .class für generische Typen und keine Class-Objekte mit Typparameter zur Laufzeit

Ein hinter einem Typ gesetztes .class liefert das Class-Objekt zum jeweiligen Typ.

Class<Object> objectClass = Object.class;

Class<String> stringClass = String.class;

Class selbst ist als generischer Typ deklariert.

Bei generischen Typen ist das .class nicht erlaubt. Zwar ist noch (mit Warnung) gültig

Class<Pocket> pocketClass = Pocket.class;

aber nicht mehr:

Class<Pocket<String>> pocketClass = Pocket<String>.class; // Compilerfehler

Die Typlöschung zeigt sich auch daran, dass die Class-Objekte für einen Typ alle gleich sind und keine Information über den Typparameter haben:

Pocket<String> p1 = new Pocket<String>();

Pocket<Integer> p2 = new Pocket<Integer>();

System.out.println( p1.getClass() == p2.getClass() ); // true

Alle Exemplare von generischen Typen haben zur Laufzeit das gleiche Class-Objekt.

Keine statischen Eigenschaften

Statische Eigenschaften hängen nicht an einzelnen Objekten, sondern an Klassen. Pocket kann zum Beispiel einmal als parametrisierter Typ Pocket<String> und einmal als Pocket<Integer> auftauchen, also als zwei Instanzen. Aber kann Pocket auch eine statische Funktion deklarieren, die auf den formalen Typparameter zurückgreift? Nein, das geht nicht. Würden wir in Pocket etwa die folgende Funktion reinsetzen

public static boolean isEmpty( T value ) { return value == null; }

gibt es bei T die Fehlermeldung “Cannot make a static reference to the non-static type T”.

Der Grund ist einfach. Statische Variablen und die Parameter/Rückgaben von statischen Methoden sind ja nicht an ein Exemplar gebunden, welche mit einer Typvariablen verbunden ist. Sie sind „global“ für alle. Bei Pocket.isEmpty(""); zum Beispiel kann der Compiler nicht wissen, dass der Typ String gemeint ist, wenn vorher ein Pocket<String> deklariert wurde.

Besonderheiten beim Überladen

Kommt nach der Typlöschung einfach nur Object raus, kann natürlich keine Methode einmal mit einer Typvariablen und einmal mit Object parametrisiert sein. Folgendes ist nicht erlaubt:

public class Pocket<T>

{

public T value;

public void set( T value ) { this.value = value; }

public void set( Object value ) { this.value = value; } // Compilerfehler!

}

Der Compiler liefert: “Method set(T) has the same erasure set(Object) as another method in type Pocket<T>”.

Ist der Typ spezieller, also etwa String, sieht das wieder anders aus. Dann taucht die Frage auf, welche Methode bei Pocket<String> aufgerufen wird. Die Leser dürfen das gerne prüfen.

1.1.4 Raw-Type

Generische Klassen müssen nicht unbedingt parametrisiert werden; sie sind mit dem Datentyp Object weiterhin gültig. Das ist auch wichtig, da sonst viele parametrisierte neue Klassen nicht mehr mit altem Programmcode verwendet werden könnten. Wenn zum Beispiel Pocket unter Java 1.4 deklariert und mit den Sprachmitteln von Java 5 zu einem generischen Typ verfeinert wurde, kann es immer noch alten Programmcode geben, der wie folgt aussieht:

Pocket p = new Pocket();

p.set( "Drei Pleitegeier, die Taschen voller Sand" );

String content = (String) p.get();

Ein generischer Typ der nicht als parametrisierter Typ, also ohne Typargument genutzt wird, heißt Raw-Type. In unserem Beispiel ist Pocket der Raw-Type von Pocket<T>. Bei einem Raw-Type kann der Compiler die Typkonformität nicht mehr prüfen, denn es ist der Typ nach der Typlöschung; get() liefert Object und set(Object) kann alles annehmen.

Ein unter Java 1.4 geschriebenes Programm nutzt also nur Raw-Types. Trifft ein Java 5 Compiler auf Programmcode, der einen generischen Typ nicht als parametrisierten Typ nutzt, fängt er an zu meckern, denn er wünscht, dass der Typ generisch verwendet wird.

EclipseWarningPocketRawType

Auch bei set() gibt der Compiler eine Warnung, denn er sieht eine Gefahr für die Typsicherheit. set() ist so angedacht, dass sie ein Argument von dem Typ akzeptiert, mit dem sie parametrisiert wurde. Fehlt durch die Verwendung des Raw-Types der konkrete Typ, bleibt Object, und der Compiler gibt bei den sonst mit einem Typ präzisierten Methoden eine Warnung aus:

p.set( " Type safety: The method set(Object) belongs to the " +

"raw type Pocket. References to generic type " +

"Pocket<T> should be parameterized" );

Der Hinweis besagt, die Tasche hätte typisiert werden müssen. Achten wir darauf nicht, kann das schnell zu Problemen führen:

Pocket<String> p1 = new Pocket<String>();

Pocket p2 = p1; // Compiler-Warnung

p2.set( new java.util.Date() ); // Compiler-Warnung

String string = p1.get();

System.out.println( string );

Der Compiler gibt keinen Fehler aber Warnungen aus. Die dritte Zeile ist hochgradig problematisch, denn über die nicht parametrisierte Tasche können wir beliebige Objekte eintüten. Da aber das Objekt hinter p2 und dem typgelöschten p1 identisch ist, haben wir ein Typproblem, das zur Laufzeit zu einer ClassCastException führt:

Exception in thread "main" java.lang.ClassCastException: java.util.Date cannot be cast to java.lang.String

Es kann also nur die Empfehlung ausgesprochen werden, dass Raw-Types in neuen Programmen vermieden werden sollten.

Typanpassungen

Ein Raw-Typ lässt sich automatisch auf eine speziellere Form bringen, wobei es natürlich Warnungen vom Compiler gibt.

Pocket p = new Pocket(); // Warnung

p.set( "Roh macht nicht froh" ); // Warnung

Pocket<String> stringPocket = p; // Warnung

String result = stringPocket.get();

Bei der Variablen p, die wir über den Raw-Typ typisiert haben, prüft der Compiler gar keine Typen in set(), denn er hat sie ja nicht erst kennengelernt. Zeile drei verkauft dem Compiler den Raw-Typ als parametrisierten Typ. Eine Anweisung, die einen Nicht-String-Typ in die Tasche setzt, bringt keinen Fehler zur Übersetzungszeit, und so kann über die Hintertür ein falscher Typ in die Tasche kommen.

EclipseWarningPocketSetRawType

Annotation SuppressWarnings

In seltenen Fällen muss auf den Typ konvertiert werden. Als Beispiel soll cast() dienen:

public <T> T cast( Object obj )

{

return (T) obj; // Type safety: Unchecked cast from Object to T

}

Lässt sich der Cast nicht vermeiden, um dem Compiler den Typ zu geben und ihn somit glücklich zu machen, setzen wir eine @SuppressWarnings-Annotation.

@SuppressWarnings("unchecked")

public <T> T cast( Object obj )

{

return (T) obj;

}

Die Generics bieten uns Möglichkeiten, den Quellcode sicherer zu machen. Wir sollten diese Sicherheit nicht durch ungetypte Schreibweisen kaputtmachen.


[1] Sind Bounds im Spiel – eine Typeinschränkung die gleich noch vorgestellt wird –, wird ein präziserer Typ statt Object genutzt.

[2] Dass diese Informationen übrigens nicht vorliegen, wird auch damit begründet, dass die Laufzeit leiden könnte. Microsoft war das aber egal, dort besteht Generizität in der Common Language Runtime (CLR), also auch in der Laufzeitumgebung. Microsoft ist damit einen klaren Schritt voraus. Doch gab es Generics (Parametric Polymorphism ist der offizielle Name) auch wie in Java nicht von Anfang an; es zog erst in Version 2 in die Sprache und CLR ein. Die alten Datenstrukturen wurden einfach als veraltet markiert und die Entwickler sollten auf die neue generische Variante umsteigen.

Labels:

Null Object Pattern und leere Sammlungen zurückgeben

In Java gilt es als guter Stil, auf null wenn möglich in der Rückgabe zu verzichten. Das Problem ist, dass der Aufrufer dann eine Fallunterscheidung auf null/ungleich null vornehmen muss, ob die Operation durchführbar war. Insbesondere bei Methoden, die Datenstrukturen liefern, kann leicht auf die null-Rückgabe verzichten, denn sie geben einfach eine leere Sammlung zurück. Das nennt sich Null Object Pattern, denn statt null wird ein Objekt ohne Inhalt, eben ein Null-Objekt zurückgegeben.

Ein Beispiel soll dieses Vorgehen zeigen. Eine Funktion words() soll eine Zeichenkette nach Worten zerlegen und diese Worte in einer List zurückgeben.

public static List<String> words( String sentence )

{

if ( sentence == null || sentence.trim().isEmpty() )

return new ArrayList<String>();

return Arrays.asList( sentence.split( "\\p{Punct}?\\s+|\\p{Punct}" ) );

}

Ist ein übergebenes Argument null oder nur Weißraum im String, so soll eine leere Liste zurückgegeben werden. Andernfalls zerlegen wie die Zeichenkette mit split(), wobei als Trennausdruck der Einfachheit halber entweder ein Zeichensetzungsszeichen alleine oder ein Zeichensetzungszeichen gefolgt von Leerraum sein kann. Das in der Anwendung ergibt:

words( "Du bist, was du programmierst! !" ) -> [Du, bist, was, du, programmierst]

words( " \n \t" ) ); -> []

words( null ) ); -> []

Der Vorteil, dass das Null-Objekt, also die leere Liste, eine Fallunterscheidung auf null unnötig macht, ist praktisch, da zum Beispiel einfach die Funktion words() im erweiteren for eingesetzt werden kann:

for ( String word : words("The Eagle has landed.") )

System.out.println( word );

Ist der übergebene „String“ bei words() nun null, so kümmert das die erweitert for-Schleife nicht, denn über eine leere Liste muss das erweiterte for nicht iterieren.

Szenarien, in deren auf Grund von Bedingungen leere Datenstrukturen zurückgegeben werden, gibt es viele. Nun haben alle diese leeren Sammlungen auch eine Sache gemeinsam: Sie sind alle gleich leer. Daher muss, wenn sie als immutable Sammlungen zurückgegeben werden, nicht jede Methode mit new eine leere Datenstruktur aufbauen. Die Funktion words() könnte so zum Beispiel auf das Objekt new ArrayList<String>() über eine statische Variable verweisen und dieses zentrale Objekt dann einfach zurückgeben, wenn es nötig wird. Damit ist das Problem gelöst, dass immer ein neues Objekt aufgebaut wird (was etwas Speicher und Rechenzeit schont). Auf der anderen Seite ist aber die new ArrayList veränderbar und das ist riskant, denn wenn ein Empfänger der Liste nun doch etwas in die Datenstruktur schreibt, so bekommen alle anderen Nutzer diesen Inhalt. Das ist nicht schön. Wir könnten hier Collections.unmodifiableXXX() dazwischen schieben, aber dann hätten wir wieder das Problem, dass jede Klasse, die solche leeren Sammlungen nutzen möchte, eine eigene Sammlung mit einem Wrapper ummantelt. Aber nicht jede Klasse muss sich diese leere Sammlung leisten.

Collections.emptyXXX()

Java bietet in Collections diverse leere immutable Datenstrukturen, für die beschriebenen Fälle wunderbar verwendet werden können. Dabei gibt es zwei Möglichkeiten. Seit Java 1.3 existieren drei statische finale Variablen: Collections.EMPTY_SET liefert ein Set, Collections.EMPTY_LIST eine List und Collections.EMPTY_MAP eine Map. Die Variablen werden wir aber nicht nutzen wollen, denn sie sind alle nicht mit einem generischen Typ deklariert, also als Raw-Type angeboten. Besser ist, auf Methoden zurückzugreifen, die Type-Inference nutzen.

class java.util.Collections

§ static <T> List<T> emptyList()

§ static <K,V> Map<K,V> emptyMap()

§ static <T> Set<T> emptySet()
Liefert eine leere unveränderbare Datenstruktur.

Unser Beispiel mit der Funktion word() kann daher optimiert werden:

public static List<String> words( String sentence )

{

if ( sentence == null || sentence.trim().isEmpty() )

return Collections.emptyList();

return Arrays.asList( sentence.split( "\\p{Punct}?\\s+|\\p{Punct}" ) );

}

Die Performance ist nun ausgezeichnet, denn ist der String leer oder null, muss nun keine neue leere ArrayList mehr aufgebaut werden.

Labels:

NIO.2: Rekursive Abläufe des Verzeichnisbaums (FileVisitor)

Die Utility-Klasse java.nio.file.Files bietet zwei statische Methoden, die beginnend bei einem Startorder rekursiv die Verzeichnisse abläuft. Die erste Funktion ist walkFileTree(Path start, FileVisitor<? super Path> visitor). Der erste Parameter bestimmt den Startordner und der zweite Parameter bestimmt ein Objekt mit Callback-Methoden, die walkFileTree() beim Ablauf des Verzeichnisbaums aufruft.

interface java.nio.file.FileVisitor<T>

§ FileVisitResult postVisitDirectory( T dir, IOException exc )

§ FileVisitResult preVisitDirectory( T dir )

§ FileVisitResult preVisitDirectoryFailed( T dir, IOException exc )

§ FileVisitResult visitFile( T file, BasicFileAttributes attrs )

§ FileVisitResult visitFileFailed( T file, IOException exc )

Die Operation visitFile() ist die wichtigste. Ihr übergibt walkFileTree() beim internen Ablauf den Pfad auf die gefundene Datei/den Ordner (es wird sich in der Regel immer um FileVisitor<Path> handeln) sowie die BasicFileAttributes, die es einfach machen, Attribute wie Dateigröße ohne große Umwege auszuwerten.

Die aufgerufenen Methoden bestimmen über die Rückgabe, ob der Durchlauf fortgeführt oder abgebrochen wird. FileVisitResult ist eine Aufzählung mit den folgenden Konstanten: CONTINUE, SKIP_SIBLINGS, SKIP_SUBTREE, TERMINATE.

Von FileVisitor gibt es mit SimpleFileVisitor eine Standard-Implementierung, mit folgendem Verhalten:

Methode

Implementierung

preVisitDirectory

return FileVisitResult.CONTINUE;

preVisitDirectoryFailed

throw new IOError(exc);

visitFile

return FileVisitResult.CONTINUE;

visitFileFailed

throw new IOError(exc);

postVisitDirectory

if (exc != null) throw new IOError(exc);

return FileVisitResult.CONTINUE;

Kommt es also beim Ablaufen zu einem Fehler, führt dies beim SimpleFileVisitor zu einem IOError (einer Unterklasse von Error, keine Exception!) und der Durchlauf bricht ab.

Finde alle Bilder (und Nemo auch)

In einem Beispiel wollen wir einen Verzeichnisbesucher schreiben, der alle Bilder ab einem Startverzeichnis findet.

com/tutego/insel/nio2/CrawlForImages.java, main()

Files.walkFileTree( Paths.get(System.getProperty("user.home")), new SimpleFileVisitor<Path>()

{

@Override

public FileVisitResult preVisitDirectoryFailed( Path dir, IOException e ) {

return FileVisitResult.SKIP_SUBTREE;

}

@Override

public FileVisitResult visitFile( Path path, BasicFileAttributes attribs )

{

try

{

String mime = Files.probeContentType( path );

if ( mime != null && mime.startsWith( "image/" ) )

System.out.println( path );

}

catch ( IOException e ) { }

return FileVisitResult.CONTINUE;

}

} );

Vom SimpleFileVisitor überschreiben wir zwei Methoden. In visitFile() testen wir mit dem MIME-Typ-Erkenner, ob es sich um eine Grafik handelt. In diesem Fall beginnt der String mit „image/“. Dass wir auch preVisitDirectoryFailed() überschreiben hat den Hintergrund, dass das Standardverhalten von SimpleFileVisitor mit dem Auslösen eines Errors bei Fehlern zu einschränkend ist. Wir wollen nicht, dass die Abarbeitung gänzlich beendet wird, und die JVM mit einem Error abbricht, sondern wir wollen diesen Unterbaum, den wir nicht besuchen können, einfach überspringen. Daher liefern im Fehlerfall user preVisitDirectoryFailed() nur SKIP_SUBTREE. Sollte die Suche abgebrochen werden, ist die Rückgabe TERMINATE.

Zyklen erkennen, Links verfolgen, Tiefen angeben

Die einfache Funktion walkFileTree(Path, FileVisitor) geht in eine beliebige Tiefe den Verzeichnisbaum ab.[1] Zudem erkennt sie standardmäßig keine Links. Für einfache Durchsuchungen ist sich gut geeignet, aber wer mehr Gestaltungsraum sucht, greift zu zweiten walkFileTree()-Funktion in Files: walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor). Startverzeichnis und FileVisitor bleiben die Gleichen, neu sind die maximale Suchtiefe in Verzeichnisebenen (nicht in Anzahl Dateien) und eine Menge von Aufzählungselementen, die bestimmen, ob Zyklen erkannt und symbolischen Links gefolgt werden soll. FileVisitOption ist eine enum mit den Konstanten DETECT_CYCLES und FOLLOW_LINKS. Bisher kommt in der Java-API der Argumenttyp „Menge von Enums“ selten vor. Hier müssen sich Entwickler zurückerinnern, wie Mengen mit Enums einfach aufgebaut werden können: Mit der Klasse EnumSet. Dazu einige Beispiele:

Path p = …;

FileVisitor<? super Path> v = …;

Files.walkFileTree( p, EnumSet.of( FileVisitOption.DETECT_CYCLES ), 2, v );

Files.walkFileTree( p, EnumSet.of( FileVisitOption.DETECT_CYCLES, FileVisitOption.FOLLOW_LINKS ), 2, v );

Files.walkFileTree( p, EnumSet.allOf( FileVisitOption.class ), 2, v );

Die einfache Funktion walkFileTree(Path start, FileVisitor<? super Path> visitor) ist übrigens auch nur eine Weiterleitung mit walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, visitor).


[1] Nur theoretisch durch Integer.MAX_VAULE beschränkt.

Labels:

NIO.2: MIME-Typen testen

Die Erkennung von Dateitypen spielt eine wichtige Rolle, etwa dann, wenn für einen Dateityp ein Programm zum Betrachten oder Bearbeiten aufgerufen werden soll. Relativ früh wurde daher der MIME-Typ (Internet Media Type) eingeführt, der Medientypen kennzeichnet. Die wichtigsten Medientypen sind:

Medientyp

Beispiel mit Subtyp

Bedeutung

text

text/plain, text/xml, text/html

Text

image

image/gif, image/png

Bilder

video

video/mpeg, video/quicktime

Videos

audio

audio/mid, audio/mpeg

Audios

application

application/msword, application/octet-stream

Binärdaten

Der MIME-Typ wird im Idealfall nicht nach ihren Dateiendungen ermittelt, da es Systeme gibt, die nicht mit Dateiendungen arbeiten. Ein guter MIME-Typ-Erkenner (MIME-Sniffer genannt) schaut daher in die Datei und ermittelt den korrekten Typ.

Seit Java 7 bietet NIO.2 FileTypeDetector-Klassen, die MIME-Typen identifizieren können. Alle FileTypeDetector-Objekte werden in einer Liste gesammelt und wenn es darum geht, den MIME-Typ einer bestimmten Datei zu ermitteln, geht eine Schleife über alle angemeldeten Detektoren und fragt jeden Detektor, ob er den MIME-Typ ermitteln kann. Zugang zu diesem Suchalgorithmus bietet die einfache Funktion Files.probeContentType(Path).

Path path1 = Paths.get( "../lyrics.txt" );

System.out.println( Files.probeContentType( path1 ) ); // text/plain

Path path2 = Paths.get( "C:/Windows/Web/Wallpaper/img1.jpg" );

System.out.println( Files.probeContentType( path2 ) ); // image/jpeg

Hinweis Path-Objekte können auch über URI-Objekte aufgebaut werden, sodass es eine naheliegende Lösung ist, zum Ermitteln von MIME-Typen von Web-Ressourcen folgendes zu versuchen:

Path p = Paths.get( new URI( "http://www.tutego.de/index.html" ) );

System.out.println( Files.probeContentType( p ) );

Das Problem ist nur, dass Paths.get() schon eine Ausnahme auslöst, da es keinen Provider für das Protokoll HTTP gibt.

Eigene Detektoren können über den Service-Mechanismus eingebunden werden. Die API-Dokumentation in der Klasse Files gibt dazu Hinweise.

Labels:

NIO.2: Dateisysteme, Dateisystemattribute

Dateisysteme werden durch die Schnittstelle FileSystem beschrieben und die Utility-Klasse FileSystems bietet die wichtige Methode getDefault(), die das Standard-Dateisystem zurückgibt. Da aber auch neue Dateisysteme angemeldet werden können, bietet für diesen Fall FileSystems diverse newFileSystem()-Methoden. Damit diese unterschiedlichen Dateisysteme unterscheidbar sind, gibt es ein URI, bei der das Protokoll ausschlaggebend ist. Bisher gibt es nur das Standard-Dateisytem mit dem Protokoll file, aber beliebige neue Dateisysteme können zum Beispiel für die Protokolle http, svn, memory, usw. aufgebaut werden.

Beispiel Die Aufrufe FileSystems.getDefault() und FileSystems.getFileSystem(new URI("file:/")) führen unter Windows zum gleichen Ergebnis, zur Klasse sun.nio.fs.WindowsFileSystem.

Mit einem FileSystem lässt sich dann über die bekannte Methode getPath() ein Pfad erfragen. Ob ein Dateisystem nur lesbar ist, beantwortet isReadOnly(). Da es unterschiedliche Pfadtrenner je nach Dateisystem geben kann, liefert getSeparator() ein String mit dem Separator.

Eine weitere Methode ist getRootDirectories(), die ein Iterable<Path> liefert für die Wurzelverzeichnisse.

Beispiel Gib alle Wurzelverzeichnisse aus:

for ( Path root : FileSystems.getDefault().getRootDirectories() )

System.out.println( root );

FileSystem

Die Methode getRootDirectories() liefert nur Path-Objekte, aber sonst keine weiteren Informationen zum Dateisystem. Die physikalischen Eigenschaften lassen sich auch nicht über das FileSystem-Objekt erfragen, sondern sind in eine Extraklasse FileStore ausgelagert. Sie liefert schon mehr Informationen. Die FileSystem-Methode getFileStores() liefert eine Iteration über die FileStore-Objekte:

Beispiel Gib alle aktiven Laufwerke aus:

for ( FileStore store : FileSystems.getDefault().getFileStores() )

System.out.println( store + " - " + store.name() + " " + store.type() );

Die Ausgabe können so ausehen:

OS (C:) - OS NTFS

RECOVERY (D:) - RECOVERY NTFS

Removable Disk (I:) - FAT32

tutego (O:) - tutego WebDrive

Public (S:) - Public NTFS

Attribute eines Dateisystems

Informationen wie die Größe des Datenträgers gib es aber immer noch nicht über Methoden der Klasse FileStore. Der Grund ist, dass der Datenträger ja beliebig sein kann und daher völlig unterschiedliche Attribute besitzen kann. Von einem FileStore ist daher kein Weg zu den Attributen gegeben. Stattdessen ist es umgekehrt, dass eine Attributanfrage-Klasse mit dem FileStore gefüttert wird. Für ein Standard-Dateisystem liefert FileStoreSpaceAttributes die interessanten Daten:

for ( FileStore store : FileSystems.getDefault().getFileStores() )

{

FileStoreSpaceAttributes attribs = Attributes.readFileStoreSpaceAttributes( store );

long total = attribs.totalSpace() >> 30;

long available = attribs.usableSpace() >> 30;

System.out.println( store + " - " + available + " GiB frei von " + total + " GiB" );

}

Mit dem Verschiebeoperator >> 30 bekommen wir gerade die Umrechung von Byte nach Gibibyte (also Gigabyte, aber binär gesehen), denn 2^30 Byte = 1.073.741.824 Byte. Die Ausgabe kann dann etwa sein:

OS (C:) - 289 GiB frei von 455 GiB

RECOVERY (D:) - 5 GiB frei von 9 GiB

Removable Disk (I:) - 7 GiB frei von 7 GiB

Labels:

NIO.2: Verzeichnislistings (DirectoryStream) und Filter

Ein Path kann Dateien und Verzeichnisse repräsentieren und so findet sich auch eine Methode an der Klasse Path, die alle Dateien und Unterverzeichnisse in einem gegebenen Verzeichnis auflistet. Drei Methoden bietet Path:

· DirectoryStream<Path> newDirectoryStream()

· DirectoryStream<Path> newDirectoryStream(String glob)

· DirectoryStream<Path> newDirectoryStream(DirectoryStream.Filter<? super Path> filter)

Der DirectoryStream ist ein Iterable<T>, und so unterscheidet sich die Möglichkeit zur Anfrage der Dateien im Ordner grundsätzlich von der Methode list() in der Klasse File, die immer alle Dateien in einem Feld auf einmal zurückliefert.

DirectoryStream<Path> files = Paths.get( "c:/" ).newDirectoryStream();

try

{

for ( Path path : files )

System.out.println( path.getName() );

}

finally

{

files.close();

}

Aus dieser Tatsache heraus, das die Dateien und Unterverzeichnisse nicht auf einem geholt werden, leitet sich die Konsequenz ab, dass der DirectoryStream geschlossen werden muss, da nicht klar ist, ob der Benutzer wirklich alle Dateien abholt oder nach den ersten 10 Einträgen aufhört. Die Schnittstelle DirectoryStream erweitert daher die Schnittstelle Closeable und es ist guter Stil, den DirectoryStream mit close() zu schließen, um blockierte Ressourcen freizugeben.

Filtern

Die unparametrisierte Methode newDirectoryStream() bietet die einfachste Variante für eine Abfrage und liefert immer ungefiltert alle Dateien. Die zwei weiteren parametrisierten newDirectoryStream()-Methoden erlauben zusätzliche Filter. In der einfachen Version ist das Filterkriterium durch eine Zeichenkette beschrieben. Sun nutzt hier die sogenannte Globbing-Syntax, die an reguläre Ausdrücke erinnert. In der API-Dokumentation sind bei FileSystems Methode getPathMatcher() einige Beispiele gegeben.

Nutzen wir diesen, um typische Bildtypen in einem bestimmten Verzeichnis aufzulisten.

Path picturePath = Paths.get( System.getProperty("user.home") ).resolve( "Pictures" );

DirectoryStream<Path> files = picturePath.newDirectoryStream( "*.{gif,jpg,png}" );

try {

for ( Path path : files )

System.out.println( path.getName() );

} finally { files.close(); }

Noch weiter beim Filtern geht die Methode newDirectoryStream(DirectoryStream.Filter<? super Path> filter). Hier lässt sich ein Filter frei programmieren. Nutzen wir einen Filter zum Beispiel, um alle leere Dateien in einem Verzeichnis aufzufinden:

DirectoryStream<Path> files = Paths.get( "c:/Windows" ).newDirectoryStream( new DirectoryStream.Filter<Path>()

{

@Override public boolean accept( Path path ) throws IOException {

BasicFileAttributes attrib = Attributes.readBasicFileAttributes( path );

return attrib.isRegularFile() && attrib.size() == 0;

}

} );

Alle drei newDirectoryStream()-Methoden arbeiten nicht rekursiv. Für die Suche und den rekursiven Ablauf tief in den Verzeichnisbaum gibt es mit FileVisitor eine andere Möglichkeit.

Labels:

NIO.2, Teil 1: FileSystem, Path und Methoden, Dateiattribute

Die bisher vorgestellten Konzepte gibt es im Wesentlichen schon seit Urzeiten von Java, also seit Java 1.0. Wenig ist in den letzten Jahren rund um die File-Klasse passiert. Doch Entwickler quälten sich immer wieder mit ganz zentralen Fragen, die die bisherigen Implementierungen nicht wirklich angingen:

· Wie lässt sich eine Datei einfach und schnell kopieren?

· Wie lässt sich eine Datei verschieben, wobei die Semantik auf unterschiedlichen Plattformen immer gleich ist.

· Wie lässt sich auf eine Änderung im Dateisystem reagieren, so dass ein Callback uns informiert, dass sich eine Datei verändert hat?

· Wie lässt sich einfach ein Verzeichnis rekursiv ablaufen?

· Wie lässt sich ein symbolischer Link anlegen und verfolgen?

· Wie lässt sich realisieren, dass die File-Operationen abstrahiert werden und nicht nur auf dem lokalen Dateisystem basieren, sondern sich direkt übertragen lassen auf entfernte Dateisysteme wie FTP, einem Repository einer Versionsverwaltung, oder in-Memory?

Diese Probleme wurden für Java 7 angegangen und in der JSR 203: „More New I/O APIs for the JavaTM Platform ("NIO.2")“ spezifiziert. Die JSR began schon 2003, und so waren die Erwartungen der Java-Community groß, dass sie nicht so lange warten müssten, aber erst in Java 7 kam es zum großen Wurf. Das macht die „alte“ File-Klasse eigentlich überflüssig, aber vermutlich scheut sich Sun davor ein @deprecated an die Klasse zu setzen, denn sonst würden riesige Mengen Programme plötzlich markiert.

FileSystem und Path

Im Zentrum der neuen Klassen, die sich im Gegensatz zu java.io.File im neuen Paket java.nio.file befinden, sind FileSystem und Path.

· FileSystem beschreibt ein Datensystem und ist eine abstrakte Klasse, die von konkreten Dateisystemen, wie dem lokalen Dateisystem realisiert wird. Die wichtigste Methode ist FileSystems.getDefault() und sie liefert das aktuelle Dateisystem.

· Path repräsentiert einen Pfad zu einer Datei oder einem Verzeichnis, wobei die Pfadangaben relativ oder absolut sein können. Die Methoden erinnern ein wenig an File, doch der große Unterschied ist, dass File selbst die Datei/das Verzeichnis repräsentiert und Anfragemethoden wie isDirectory() oder lastModified() deklariert aber Path nur den Pfad repräsentiert und nur Pfad-bezogenen Methoden anbietet. Modifikationsfunktionen gehören nicht dazu; dazu dienen extra Typen wie BasicFileAttributes für Attribute.

Path erfragen und einfache Pfad-Methode

Ein Path-Objekt lässt sich nicht wie File über einen Konstruktor aufbauen, da die Klasse abstrakt ist. Das FileSytem bietet die entsprechende Methode getPath() für diesen Fall an. (File und Path haben aber gemeinsam, das sie immutable sind.)

com/tutego/insel/nio2/FileSystemPathFileDemo.java, main()

FileSystem fs = FileSystems.getDefault();

Path p = fs.getPath( "C:/Windows/Fonts/" );

System.out.println( p.toString() ); // C:\Windows\Fonts

System.out.println( p.getNameCount() ); // 2

System.out.println( p.isAbsolute() ); // true

System.out.println( p.getRoot() ); // C:\

System.out.println( p.getName() ); // Fonts

System.out.println( p.getParent() ); // Fonts

Dadurch das Path eine hierarchische Liste von Namen für den Pfad speichert lässt sich jedes Segment des Pfades erfragen; das ist die Aufgabe von getName(int n), was wiederum einen Path liefet.

Mit relativen Pfaden gibt es natürlich ein Problem:

Path p2 = fs.getPath( "../.." );

System.out.println( p2.toString() ); // ..\..

System.out.println( p2.getNameCount() ); // 2

System.out.println( p2.isAbsolute() ); // false

System.out.println( p2.getRoot() ); // null

System.out.println( p2.getName() ); // ..

System.out.println( p2.getParent() ); // ..

Aus diesem Grund bietet die Path-Klasse Methoden zum Auflösen der relativen Adressierung an:

System.out.println( p2.toAbsolutePath() ); // S:\Insel\programme\2_14_Files\..\..

try

{

System.out.println( p2.toRealPath( true ) ); // S:\Insel

}

catch ( IOException e ) { e.printStackTrace(); }

Die erste Methode toAbsolutePath() normalisiert nicht, sondern löst einfach nur den relativen Pfad in einen absoluten Pfad auf. Die Auflösung vom ../.. erledigt toRealPath(), wobei das Argument ausdrückt, ob Links verfolgt werden sollen oder nicht. Die Pfade können auch bezüglich eines speziellen Ausgangsverzeichnisses aufgelöst werden.

Beispiel Die Methode toRealPath() führt eine Ausahme aus, wenn eine Auflösung einer Datei versucht wird, die nicht existiert. Es führt zum Beispiel

FileSystems.getDefault().getPath( "../0x" ).toRealPath( true )

zur

java.nio.file.NoSuchFileException: ..\0x.

Die Methoden getPath(), getName(), getRoot(), getParent() und toRealPath() liefern alle wiederum Path-Objekte aus den Bestandteilen eines gegebenen Pfades.

Mit der Methode resolve() lassen sich neue Pfade zusammenhängen. Eine interessante Methode ist auch relativize() – sie liefert aus einer Basisangabe einen relativen Pfad, der zu einem anderen Pfad zeigen lässt.

System.out.println(

fs.getPath( "c:/Windows/Fonts" ).relativize( fs.getPath( "c:/Windows/Cursors" ) )

); // ..\Cursors

Von c:/Windows/Fonts nach c:/Windows/Cursors führt also der Ausdruck ..\Cursors.

Dateiattribute

Die File-Klasse wurde immer mehr zu Sammelbecken aller möglicher Anfragemethoden wie Lesbarkeit, Änderungsdatum, usw. Ein Problem dabei ist, dass gewissen Dinge nicht wirklich auf jedem System identisch sind – etwa die Dateirechte. Mit NIO.2 ändert sich das.

Zum Zugriff auf die Attribute gibt es unterschiedliche APIs. Einer der Wege ist, nach Attributen über einen String-Schlüssel zu fragen. Das gibt die große Flexibilität, dass eine Implementierung neue Attribute veröffentlichen kann, ohne dass die API geändert werden muss. Plattformen können etwa die Information unterbringen, mit dem die Software erstellt wurde (Apple), oder ein zugewiesenen Icon, oder ob die Datei indexiert wurde (Windows Vista).

Die Namen einiger Standard-Attribute verteilen sich auf die Klasse BasicFileAttributeView (sollte für alle Systeme gelten), DosFileAttributeView (Windows) und PosixFileAttributeView (POSIX-Systeme, Unix). Für das Attribute-Management und diese genannten Typen wurde extra das Paket java.nio.file.attribute eingeführt.

Schlüssel

Typ

BasicFileAttributeView (basic)

isRegularFile

Boolean

isDirectory

Boolean

isSymbolicLink

Boolean

isOther

Boolean

fileKey

Object

lastModifiedTime

FileTime

lastAccessTime

FileTime

creationTime

FileTime

size

Long

DosFileAttributeView (dos)

readonly

Boolean

hidden

Boolean

system

Boolean

archive

Boolean

PosixFileAttributeView (posix)

permissions

Set<PosixFilePermission>

group

GroupPrincipal

FileStoreSpaceAttributeView (space)

totalSpace

Long

usableSpace

Long

unallocatedSpace

Long

Für Zeiten gibt es eine spezielle Klasse java.nio.file.attribute.FileTime, die Comparable<FileTime> ist, aber keine Erweiterung von Date oder Calendar. Nur mit der Methode long to(TimeUnit unit) lassen sich die Datei-Zeiten konvertieren.

Dazu ein Beispiel, wie die Methode getAttribute() diverse Attribut erfragt.

com/tutego/insel/nio2/AttributesDemo.java, main()

Path p = FileSystems.getDefault().getPath( "src/lyrics.txt" ).toAbsolutePath();

System.out.println( p.getAttribute( "basic:isRegularFile" ) );// true

System.out.println( p.getAttribute( "isDirectory" ) ); // false

System.out.println( p.getAttribute( "isSymbolicLink" ) ); // false

System.out.println( p.getAttribute( "isOther" ) ); // false

System.out.println( p.getAttribute( "fileKey" ) ); // null

System.out.println( p.getAttribute( "lastModifiedTime" ) ); // 2006-05-23T12:36:54Z

System.out.println( p.getAttribute( "lastAccessTime" ) ); // 2009-07-17T12:24:33Z

System.out.println( p.getAttribute( "creationTime" ) ); // 2006-05-23T12:36:54Z

System.out.println( p.getAttribute( "size" ) ); // 14

System.out.println( p.getAttribute( "dos:readonly" ) ); // false

System.out.println( p.getAttribute( "dos:hidden" ) ); // false

System.out.println( p.getAttribute( "dos:system" ) ); // false

System.out.println( p.getAttribute( "dos:archive" ) ); // true

System.out.println( p.getAttribute( "posix:permissions" ) );// null

System.out.println( p.getAttribute( "posix:group" ) ); // null

Der Attributstring hat einen Präfix, der, falls es sich sind gerade um die Standard-Parameter handelt, mit angegeben werden, also „dos:“ oder „posix:“.

Die XXXFileAttributes-Typen

Der Nachteil bei dieser Lösung ist offensichtlich: Bei den Strings gibt es keine Typsicherheit und die Rückgaben sind immer allgemein vom Typ Object und erfordern einen explizite Typanpassung.

Die Lösung bietet die Utility-Klasse Attributes mit diversen statischen Anfragefunktionen.

· BasicFileAttributes readBasicFileAttributes(FileRef file, LinkOption... options)

· DosFileAttributes readDosFileAttributes(FileRef file, LinkOption... options)

· FileStoreSpaceAttributes readFileStoreSpaceAttributes(FileStore store)

· PosixFileAttributes readPosixFileAttributes(FileRef file, LinkOption... options)

Der Typ FileRef ist eine Schnittstelle, die von Path und auch von URL implementiert wird.

com/tutego/insel/nio2/BasicFileAttributesDemo.java, main()

Path p = FileSystems.getDefault().getPath( "src/lyrics.txt" ).toAbsolutePath();

BasicFileAttributes attrs = Attributes.readBasicFileAttributes( p );

System.out.println( attrs.isRegularFile() ); // true

System.out.println( attrs.isDirectory() ); // false

System.out.println( attrs.isSymbolicLink() ); // false

System.out.println( attrs.isOther() ); // false

System.out.println( attrs.lastModifiedTime() ); // 2006-05-23T12:36:54Z

System.out.println( attrs.lastAccessTime() ); // 2009-07-17T12:24:33Z

System.out.println( attrs.creationTime() ); // 2006-05-23T12:36:54Z

System.out.println( attrs.size() ); // 14

Die Schnittstelle BasicFileAttributes bietet nur Lesefunktionen (und das auch nicht in der Getter-Konvention), aber Modifikationen müssen auf einen anderen Weg gemacht werden. Einfach ist zum Beispiel das Setzten der letzten Zugriffszeit, die Zeit der letzten Modifikation, den Eigentümer oder Dateirechte. Hier bietet die Klasse java.nio.file.attribute.Attributes statische Methoden an:

· setLastAccessTime( FileRef file, FileTime lastAccessTime )

· setLastModifiedTime( FileRef file, FileTime lastModifiedTime )

· setOwner( FileRef file, UserPrincipal owner )

· setPosixFilePermissions( FileRef file, Set<PosixFilePermission> perms )

· setAcl( FileRef file, List<AclEntry> acl )

Die Rückgaben sind immer void. Für sonstige Änderungen bietet Path neben getAttribute() auch setAttribute().

Labels:

XML/HTML-Entities ausmaskieren

In einer XML-Datei dürfen bestimmte Zeichen im normalen Textstrom nicht vorkommen und müssen umkodiert werden.

Zeichen

Umkodierung

"

&quot;

&

&amp;

'

&apos;

<

&lt;

>

&gt;

Eine Konstruktion wie &quot; nennt sich Entity. Die gültigen Entities werden im XML-Standard beschrieben.

Weiterhin gilt, dass bei einer Webseitenkodierung in ISO-8859-1 nur die „sicheren“ Zeichen wie Ziffern und Buchstaben verwendet werden können, aber keine Sonderzeichen wie etwa dem Copyright- oder Euro-Zeichen. Daher bietet HTML eine Umkodierung für Sonderzeichen an, die nicht im Zeichenvorrat von ISO 8859-1 enthalten sind – für das Copyright-Zeichen ist es etwa &copy; und das Euro-Zeichen &euro;. In XML ist diese Umkodierung nicht nötig, da XML leicht als UTF-8 geschrieben werden kann und dann heißt es für das Euro-Zeichen nach der Position in der Unicode-Tabelle einfach &#8364;.[1]

Java-Programme, die XML- oder HTML-Ausgaben erstellen oder XML/HTML-Dokumente lesen, müssen auf die korrekte Konvertierung achten. Die Standard-Biblitohek bringt hier nichts offensichtliches mit, aber Open-Source-Bibliotheken füllen diese Lücke. So etwa Apache Commons Lang (http://commons.apache.org/lang/), das mit der Klasse org.apache.commons.lang.StringEscapeUtils einige Kodierungsmethoden bietet, um einen String in XML/HTML umzukodieren und einen XML/HTML-Strings mit Entities in einen Java-String zu bringen, bei dem insbesondere die HTML-Entities aufgelöst wurden.

 

Beispiel Für eine einfache Kodierung (ohne Hochkommata) lässt sich ein XMLStreamWriter einsetzen (die Klasse wird später im XML-Kapitel genauer vorgestellt).

StringWriter out = new StringWriter();

XMLStreamWriter writer = XMLOutputFactory.newInstance().createXMLStreamWriter(out);

writer.writeCharacters( "<&'Müsli\">" );

System.out.println( out.toString() ); // &lt;&amp;'Müsli"&gt;


[1] Das führt in HTML zu viel mehr Entities als bei XML, sodass es ein Problem werden kann, eine HTML-Datei als XML einzulesen – der XML-Parser meckert dann über die unbekannten Entities.

Labels:

Inselupdate: Das Pascal’sche Dreieck

Das folgende Beispiel zeigt eine weitere Anwendung von nichtrechteckigen Arrays, in der das Pascal’sche Dreieck nachgebildet wird. Das Dreieck ist so aufgebaut, dass die Elemente unter einer Zahl genau die Summe der beiden direkt darüberstehenden Zahlen bilden. Die Ränder sind mit Einsen belegt.

            1

          1   1

        1   2   1

      1   3   3   1

    1   4   6   4   1

  1   5  10  10   5   1

1   6  15  20  15   6   1

Das Pascal’sche Dreieck

In der Implementierung wird zu jeder Ebene dynamisch ein Feld mit der passenden Länge angefordert. Die Ausgabe tätigt printf() mit einigen Tricks mit dem Formatspezifizierer, da wir auf diese Weise führendes Leerzeichen bekommen.

class PascalsTriangle

{

  public static void main( String[] args )

  {

    int[][] triangle = new int[7][];

    for ( int row = 0; row < triangle.length; row++ )

    {

      System.out.printf( "%." + (14 - row*2) +"s", "              " );

      triangle[row] = new int[row + 1];

for ( int col = 0; col <= row; col++ )

{

        if ( (col == 0) || (col == row) )

           triangle[row][col] = 1;

        else

           triangle[row][col] = triangle[row - 1][col - 1] + triangle[row - 1][col];

        System.out.printf( "%3d ", triangle[row][col] );

      }

      System.out.println();

    }

  }

}

Die Anweisung System.out.printf("%." + (14 - row*2) +"s", "              ") produziert Einrückungen. Ohne die Konkatenation liest es sich einfacher:

System.out.printf( "%.14s", "              " ) führt zu "              "

System.out.printf( "%.12s", "              " ) führt zu "            "

System.out.printf( "%.10s", "              " ) führt zu "          "

usw.

Labels:

“Einführung in Java Generics”, Generics-Tutorial Teil 1

1.1.1 Motivation für Generics

In unseren vorangehenden Beispielen drehte sich alles um Spieler und in einem Raum platzierte Spielobjekte. Stellen wir uns vor, der Spieler hat eine Tasche (engl. pocket), in dem er Dinge transportieren kann. Da nicht bekannt ist, was genau er transportiert, müssen wir einen Basistyp nehmen, der alle möglichen Objekttypen repräsentiert. Das soll in unserem ersten Beispiel der allgemeinste Basistyp Object sein, sodass der Benutzer alles in seiner Tasche tragen kann. (Primitive Datentypen können dann nur über Wrapper-Objekte gespeichert werden, was dank Autoboxing akzeptabel ist.)

public class Pocket

{

  private Object value;

  public Pocket() {}

  public Pocket( Object value ) { this.value = value; }

  public void set( Object value ) { this.value = value; }

  public Object get() { return value; }

  public boolean isEmpty() { return value == null; }

  public void empty() { value = null; }

}

Es gibt einen parametrisierten und Standard-Konstruktor, und mit set() können wir ein Objekt in die Tasche setzen und den Wert über die Zugriffsmethode auslesen. Geben wir einem Spieler eine rechte und linke Tasche.         

public class Player

{

  public String name;

  public Pocket rightPocket;

  public Pocket leftPocket;

}

Zusammen mit einem Spieler, der eine rechte und linke Tasche hat, ist ein Beispiel schnell geschrieben. Unser Spieler Michael soll sich in beide Taschen Zahlen setzen[1]. Dann wollen wir sehen, in welcher Tasche er die größere Zahl versteckt hat.

Player michael = new Player();

michael.name = "Omar Arnold";

Pocket pocket = new Pocket();

BigInteger aBigNumber = BigInteger.probablePrime( 64, new Random() );

pocket.set( aBigNumber );                                               // (1)

michael.leftPocket  = pocket;

michael.rightPocket = new Pocket( BigInteger.probablePrime( 64, new Random() ) );

System.out.println( michael.name + " hat in den Taschen " +

                    michael.leftPocket.get() + " und " + michael.rightPocket.get() );

BigInteger val1 = (BigInteger) michael.leftPocket.get();                // (2)

BigInteger val2 = (BigInteger) michael.rightPocket.get();

System.out.println( val1.compareTo( val2 ) > 0 ? "Links" : "Rechts" );

Das Beispiel hat keine besonderen Fallen, allerdings fallen zwei Sachen auf, die prinzipiell unschön sind. Das hat damit zu tun, dass die Klasse Pocket mit dem Typ Object zum Speichern der Tascheninhalte sehr allgemein deklariert wurde und alles aufnehmen kann.

  • Beim Initialisieren wäre es gut zu sagen, dass die Taschen nur BigInteger aufnehmen soll. Damit lassen sich erstens nur BigInteger-Objekte in die Tasche setzen (1) und zweitens gut sichtbar machen, dass die rechte und linke Tasche beide BigInteger aufnehmen und nicht die linke Tasche BigInteger und die rechte Integer. Beide Taschen könnten ja grundsätzlich verschiedene Typen speichern, aber wenn wir später die Inhalte auf ihre Ordnung vergleichen wollen, müssen die gespeicherten Typen gleich sein.
  • Beim Entnehmen (2) des Tascheninhalts mit get() müssen wir uns daran erinnern, dass wir ein BigInteger reingesetzt haben. Das alleine werden wir wissen, aber wenn der Compiler wüsste, dass in der Tasche auf jeden Fall ein BigInteger ist, dann könnte die Typanpassung wegfallen und der Programmcode ist kürzer. Unser Wissen möchten wir gerne auf den Compiler übertragen! Wenn in der Tasche ein einfaches Integer-Objekt und kein BigInteger war, wir es aber annehmen und eine explizite Typanpassung setzen, meldet der Compiler bei einer Typverletzung keinen Fehler, aber zur Laufzeit gibt es die böse ClassCastException.

Um es auf einen Punkt zu bringen: Die Typsicherheit wird vom Compiler in der Lösung nicht ausreichend berücksichtigt. Explizite Typanpassungen sind in der Regel unschön und sollten vermieden werden. Aber wie bekommen wir die Taschen typsicher? Eine Lösung wäre, eine neue Klasse pro in der Tasche zu speichernden Typ zu deklarieren, also einmal eine PocketBigInteger, dann vielleicht PocketInteger, PocketString, PocketGameObject, usw. Nun dürfte klar sein, dass dies keine Lösung ist; wir können nicht für jeden Datentyp eine neue Klasse schreiben und die Logik bleibt die gleiche. Wir wollen wenig schreiben, aber Typsicherheit beim Compilieren  und nicht erst die Typsicherheit zur Laufzeit, wo uns vielleicht eine ClassCastException überrascht. Es wäre gut, wenn wir den Typ bei der Deklaration frei, also generisch halten können, aber sobald wir die Tasche benutzen, den Compiler dazu bringen könnten, auf diesen dann angegeben Typ zu achten und die Korrektheit der Nutzung sicherzustellen.

Die Lösung für diese Frage sind Generics .[2] Die Möglichkeit wurde in Java 5 eingeführt und gibt Entwicklern ganz neue Möglichkeiten, Datenstrukturen und Algorithmen zu programmieren, die von einem Datentyp unabhängig programmiert, also generisch sind. Es gibt eine ganze Reihe von Beispielen, in denen Speicherstrukturen wie unsere Tasche nicht nur für einen Datentyp BigInteger sinnvoll sind, sondern grundsätzlich für alle, aber die Implementierung relativ unabhängig vom Typ der Elemente ist. Das gilt zum Beispiel für einen Sortieralgorithmus, der auf der Ordnung der Elemente arbeitet. Wenn zwei Elemente größer oder kleiner sein können, muss ein Algorithmus lediglich diese Eigenschaft nutzen können, aber es wäre egal, ob es Zahlen vom Typ BigInteger, Dobule oder auch Strings oder Kunden sind – der Algorithmus selbst ist davon nicht betroffen. Der häufigste Einsatz von Generics sind aber tatsächlich Container, die typsicher gestaltet werden sollen.

Hinweis   Die Idee, Generics in Java einzuführen, ist schon älter und geht auf das Projekt Pizza bzw. dem Teilprojekt GJ  (A Generic Java Language Extension) von Martin Odersky (auch Schöpfer der Programmiersprache Scala), Gilad Bracha, David Stoutamire und Philip Wadler zurück. GJ wurde dann die Basis vom JSR 14: Add Generic Types To The Java Programming Language.

1.1.2 Generische Typen deklarieren

Wollen wir Pocket in einen generischen Typ umbauen, so müssen wir an den Stellen, an denen Object vorkam, einen Typstellvertreter, einen so genannten formalen Typparameter mit einer Typvariable, einsetzen. Der Name der Typvariablen muss in der Klassendeklaration angegeben werden, da es durchaus mehr als einen Stellvertreter geben kann.

Die Syntax für den generischen Typ unserer Tasche ist folgende:

public class Pocket<T>

{

  private T value;

  public Pocket() {}

  public Pocket( T value ) { this.value = value; }

  public void set( T value ) { this.value = value; }

  public T get() { return value; }

  public boolean isEmpty() { return value != null; }

  public void empty() { value = null; }

}

Anstelle des Typs Object steht unser formaler Typparameter, in unserem Fall ist das T – die Abkürzung ist oft T, das für Typ steht. Bei generischen Typen steht die Angabe der Typvariable nur einmal zu Beginn der Klassendeklaration in spitzen Klammern hinter dem Klassennamen. Der Typparameter kann nun fast überall dort genutzt werden, wo auch ein herkömmlicher Typ stand. (So einfach ist das nicht immer. T t = new T(); wäre zum Beispiel nicht möglich – auf die Beschränkungen kommen wir noch zurück.) In unserem Beispiel ersetzen wir direkt Object durch T und fertig ist die generische Klasse.

Namenskonvention  Formale Typparameter sind in der Regel einzelne Großbuchstaben wie T (Typ), E (Element), K (Key/Schlüssel), V (Value/Wert). Sie sind nur Platzhalter und keine wirklichen Typen. Möglich wäre etwa auch folgendes, doch ist davon absolut abzuraten, da Dwarf viel zu sehr nach einem echten Klassentyp, als nach einem formalen Typparameter aussieht.

public class Pocket<Dwarf>

{

  private Dwarf value;

  public void set( Dwarf value ) { this.value = value; }

  public Dwarf get() { return value; }

}

1.1.3 Generics nutzen

Um die Tasche nutzen zu können, müssen wir die Klasse zusammen mit einem Typparameter angeben; es entsteht der parametrisierte Typ . Er ist eine Instanziierung eines generischen Typs mit konkreten Typargumenten (etwa String oder Integer). Der konkrete Typ steht hinter dem Klassen-/Schnittstellennamen in spitzen Klammern[3].

Pocket<Integer>  intPocket     = new Pocket<Integer>();

Pocket<String>   stringPocket  = new Pocket<String>();

intPocket.set( 1 );

int x = intPocket.get();

String s = stringPocket.get();

Der Entwickler macht so im Programmcode sehr deutlich, dass die Taschen einen Integer enthalten und nichts anderes. Zwar leidet die Lesbarkeit etwas, da insbesondere der Typ rechts und links angegeben wird und das durch geschachtelte Generics lang werden kann, doch die Code-Verständlichkeit ist höher. Das Schöne für die Typsicherheit ist, dass nun alle Eigenschaften mit dem angegebenen Typ geprüft werden. Wenn wir etwa aus intBox mit get() auf das Element zugreifen, ist es vom Typ Integer (und durch Unboxing gleich int) und set() erlaubt auch nur ein Integer. Das macht den Programmcode robuster und durch den Wegfall der Typanpassungen kürzer und lesbarer.

Keine Primitiven  Typparameter können in Java nur Objekte sein, aber keine primitiven Datentypen. Das schränkt die Möglichkeiten zwar ein, doch da es Autoboxing gibt, lässt sich damit leben.

Zusammenfassung der bisherigen Generics-Begriffe

Begriff

Beispiel

Generischer Typ (engl. generic type)

Pocket<T>

Typvariable oder formaler Typparameter (engl. formal type parameter)

T

Parametrisierter Typ (engl. parameterized type)

Pocket<BigInteger>

Typparameter (engl. actual type parameter)

BigInteger

Originaltyp (engl. raw type)

Pocket

Ist ein generischer Typ wie Pocket<T> gegeben, gibt es erst einmal keine Einschränkung für T. So beschränkt sich T nicht auf einfache Klassen- oder Schnittstellentypen, sondern kann auch wieder ein generischer Typ, etwa selbst wieder Pocket<T> oder andere generische Typen wir List<E>. Das ist logisch, denn jeder generische Typ ist ja ein eigenständiger Typ, der wie jeder andere Typ genutzt werden kann. Damit schachtelt sich die Deklaration:

Pocket<Pocket<String>> pocketOfPockets = new Pocket<Pocket<String>>();

pocketOfPockets.set( new Pocket<String>() );

pocketOfPockets.get().set( "Inner Pocket<String>" );

System.out.println( pocketOfPockets.get().get() ); // Inner Pocket<String>

Hier enthält die Tasche eine Tasche, die eine Zeichenkette „Inner Pocket<String>“ speichert. (Man stelle sich dieses Programmsegment mit den Typanpassungen ohne Generics vor …)

Bei Dingen wie diesen ist aber offensichtlich, wie hilfreich für den Compiler (und uns) Generics sind. Ohne Generics sehen eben alle Taschen gleich aus.

Mit Generics

Nach der Typlöschung

public class Pocket<T>

{

private T value;

public void set( T value ) { this.value = value; }

public T get() { return value; }

}

public class Pocket

{

private Object value;

public void set( Object value ) { this.value = value; }

public Object get() { return value; }

}

Nur ein gut gewählter Name und eine gute Dokumentation können beim nicht-generisch verwendeten Variablen helfen. Vor Java 5 haben sich Entwickler damit geholfen, in ein Blockkommentar Generics zu  dokumentieren, etwa in Pocket/*<String>*/ stringPocket.

1.1.4 Generische Schnittstellen

Eine Schnittstelle kann genauso als generischer Typ deklariert werden wie eine einfache Klasse. Werfen wir einen Blick auf die Schnittstellen java.lang.Comparable und java.util.Set (Abstraktion einer Datenstruktur Menge), die mit einer Typvariablen ausgestattet ist:

public interface Comparable<T>

{

public int compareTo(T o);

}

public interface Set<E> extends Collection<E>

{

int size();

boolean isEmpty();

boolean contains(Object o);

Iterator<E> iterator();

Object[] toArray();

<T> T[] toArray(T[] a);

boolean add(E e);

boolean remove(Object o);

boolean containsAll(Collection<?> c);

boolean addAll(Collection<? extends E> c);

boolean retainAll(Collection<?> c);

boolean removeAll(Collection<?> c);

void clear();

boolean equals(Object o);

int hashCode();

}

Wie bekannt, greift der Rumpf der Schnittstelle auf die Typvariable T und E zurück. Bei Set ist weiterhin zu erkennen, dass sie selbst eine generische deklarierte Schnittstelle erweitert.

Werden generische Schnittstellen eingesetzt, lassen sich zwei Benutzungsmuster ableiten.

Nicht-generischer Klassentyp löst Generics bei der Implementierung auf

Im ersten Fall implementiert eine Klasse die generische deklarierte Schnittstelle und gibt einen konkreten Typ an. Die numerischen Wrapper-Klassen implementieren zum Beispiel alle Comparable und füllen den Typparameter genau mit dem Typ der Klasse.

public final class Integer extends Number implements Comparable<Integer>

{

  public int compareTo( Integer anotherInteger ) { … }

Durch diese Nutzung wird für den Anwender Integer Generics-frei.

Generischer Klassentyp implementiert generisches Interface und gibt die Parametervariable weiter

Die Schnittstelle Set schreibt Operationen für Mengen vor. Eine implementierende Klasse ist zum Beispiel HashSet. Der Kopf der Typdeklaration ist:

public class HashSet<E>

    extends AbstractSet<E>

implements Set<E>, Cloneable, java.io.Serializable

Es ist abzulesen, dass Set eine Typvariable E deklariert, die HashSet nicht konkretisiert. Der Grund ist, dass die Datenstruktur Set vom Anwender als parametrisierter Typ verwendet werden und nicht aufgelöst werden soll.

1.1.5 Generische Methoden/Konstrukturen und Typ Inference

Eine Klasse kann auch ohne Generics deklariert werden, aber generische Methoden besitzen. Das gilt für Konstruktoren, Objektmethoden und Klassenmethoden. Interessant ist dies für Utility-Klassen, die nur statische Funktionen anbieten, aber selbst nicht als Objekt vorliegen.

public class GenericMethods

{

  public static <T> T random( T m, T n )

{

    return Math.random() > 0.5 ? m : n;

  }

  public static void main( String[] args )

{

    String s = random( "Analogkäse", "Gel-Schinken" );

    System.out.println( s );

  }

}

Hier wird die Funktion random() auf einem beliebigen Typ deklariert; die Angabe von <T> beim Klassennamen entfällt und verschiebt sich auf die Deklaration an der Methode.

Den Typ (der wichtig für die Rückgabe ist) leitet der Compiler also automatisch aus dem Kontext, also aus den Argumenten, ab. Diese Eigenschaft nennt sich Typ Inference.

Mit den Typen geht der Compiler so weit hoch, bis es „passt“. Ersetzen wir einen der beiden Strings durch Integer, so wird der Compiler Schnittmengen der Obertypen von Integer und String als gültigen Rückgabetyp von random() erlauben. Gültig wären demnach:

Object       s1 = random( "Essen", 1 );

Serializable s2 = random( "Essen", 1 );

Comparable   s3 = random( "Essen", 1 );

Hinweis  Natürlich kann eine Klasse als generischer Typ und eine darin enthaltende Methode als generische Methode mit unterschiedlichen Typ deklariert werden. In diesem Fall sollten die Typvariable unterschiedlich sein, um den Leser nicht zu verwirren. So bezieht sich im Folgenden T bei sit() eben nicht auf die Parametervariabe der Klasse Lupilu, sondern auf die der Methode.

class Lupilu<T> {

<T> void sit( T val );

}       

Knappe Fabrikfunktionen

Typ Inference ist eine sehr interessante Technik, mit der sich zum Beispiel elegant das Problem lösen lässt, dass beim Deklarieren und Initialisieren einer Referenzvariablen der Typparameter zweimal angegeben werden muss.

Pocket<String> p = new Pocket<String>();

Dass die Angabe <String> zweimal folgen muss ist kein Wunder, denn der Klassentyp ist nur mit dem tatsächlichen Typparameter vollständig. Doch Typ Inference liefert eine alternative Lösung. Besitzt die Klasse Pocket die Fabrikfunktion

public static <T> Pocket<T> newInstance() { return new Pocket<T>(); }

so ist die Alternative:

Pocket<String> p = Pocket.newInstance();

Aus dem Ergebnistyp Pocket<String> leitet der Compiler den tatsächlichen Typparameter String für die Tasche ab. Während bei einem einfachen Typ wie String die Schreibersparnis noch gering ist, wird es kürzer bei verschachtelten Datenstrukturen.

Pocket<Map<String, List<Integer>>> p = Pocket.newInstance();

Hier soll die Tasche einen Assoziativspeicher speichern, der eine Zeichenkette mit einer Liste von Zahlen assoziiert.

Generische Methoden mit expliziten Typparameter

Es gibt Situationen, in denen der Compiler nicht aus dem Kontext über Typ Inference den richtigen Typ ableiten kann. Folgendes ist nicht möglich:

boolean hasPocket = true;

Pocket<String> pocket = hasPocket ? Pocket.newInstance() : null;

Der Compiler meldet „Type mismatch: cannot convert from Pocket<Object> to Pocket<String>”.

Die Lösung: Wir müssen bei Pocket.newInstance() den Typparameter String explizit angeben:

Pocket<String> pocket = hasPocket ? Pocket.<String>newInstance() : null;

Die Syntax ist (mal wieder) gewöhnungsbedürftig.


[1] Das ist unproblematischer als Diprivan und Demerol …

[2]      In C(++) werden diese Typen von Klassen parametrisierte Klassen oder Templates (Schablonen) genannt. Java Generics gehen aber weit über das hinaus, was C++ bietet. 

[3]      Das auch XML in geschweiften Klammern daherkommt und XML als groß und aufgebläht gilt wollen wir nicht als Parallele zu Java sehen.

Labels:

Gründlich überarbeitetes Generics-Kapitel in der Insel

Im Großen und Ganzen bin ich mit meinem Java-Buch zufrieden. Doch eine Stelle gibt es, die mich seit Jahren quält: Das Generics-Kapitel. Das Wesentlich ist drin, und die Informationen sind weitestgehend korrekt (bis auf die Tatsache, das es <T super Typ> nicht gibt und es <? super Typ> heißen muss und dass es <T extends Comparable<T>>  statt <T extends Comparable> heißen muss), aber irgendwie fehlte die Präzision und Tiefe. Daher stand für die kommende 9. Auflage eine Überarbeitung ganz oben auf der TODO-Liste. Nun ist das Kapitel überarbeitet ist die 5 Generics-Unterkapitel sollen als Preview im Blog folgen.

  • “Einführung in Java Generics”, Generics-Tutorial Teil 1
  • “Umsetzen der Generics, Typlöschung und Raw-Types”, Generics-Tutorial Teil 2
  • “Einschränken der Typen über Bounds”, Generics-Tutorial Teil 3
  • “Generics und Vererbung, Invarianz”, Generics-Tutorial Teil 4
  • “Konsequenzen der Typlöschung: Super-Token und Brücken”, Generics-Tutorial Teil 5

Ich hoffe damit, eines der umfassendsten deutschsprachigen Generics-Tutorial online anbieten zu können. Über Anmerkungen wäre ich sehr dankbar.

Labels:

MouseInfo und PointerInfo und Bildschirmlupe

Mit der Klasse Robot lassen sich zwar Tastatur- und Maus-Ereignisse produzieren und Screenshots nehmen, doch Informationen über die aktuelle absolute Mausposition liefert die Klasse nicht. Die Klasse MouseInfo liefert diese Information. Die statische Funktion MouseInfo.getPointerInfo() liefert ein PointerInfo-Objekt, das Aussagen über die anzeigende Einheit und über die Position des Mauszeigers macht. MouseInfo.getNumberOfButtons() liefert die Anzahl der Knöpfe.

Die Anzahl der Anwendungsfälle ist vielfältig. So ermöglicht dies zum Beispiel eine Farbpipette, mit der sich die Farbe eines Punktes nehmen lässt, der unter dem Mauszeiger steht.

Point location = MouseInfo.getPointerInfo().getLocation();

Color pixelColor = new Robot().getPixelColor( location.x, location.y );

Ein anderes Beispiel ist eine Bildschirmlupe.

com/tutego/insel/ui/image/Magnifier.java, main()

final ImageIcon icon = new ImageIcon();

final JLabel label = new JLabel( icon );

new Timer( 100, new ActionListener() {

@Override public void actionPerformed( ActionEvent e )

{

try

{

Rectangle location = new Rectangle( MouseInfo.getPointerInfo().getLocation(), new Dimension( 40, 40 ) );

location.translate( -20, -20 );

BufferedImage image = new Robot().createScreenCapture( location );

icon.setImage( image.getScaledInstance( image.getWidth()*8, image.getHeight()*8, Image.SCALE_FAST ) );

label.repaint();

}

catch ( AWTException ae ) { }

}

} ).start();

JOptionPane.showMessageDialog( null, label );

 

Magnifier

Labels:

Initialisierung von Schnittstellenkonstanten

Eine Schnittstelle kann Attribute deklarieren, aber das sind dann immer initialisierte public final static Konstanten.

import java.util.Properties;

public interface PropertyReader

{

Properties DEFAULT_PROPERTIES = new Properties();

Properties getProperties();

}

Würden wir DEFAULT_PROPERTIES nicht mit new Properties() initialisieren, gäbe es einen Compilerfehler.

Nun stellt sich das Problem, wenn die statischen Attribute nicht einfach mit einem Objekt initialisiert werden können, sondern wenn zusätzlicher Programmcode zur Initialisierung gewünscht ist. Für unser Beispiel soll das Properties-Objekt unter dem Schlüssel date die Zeit speichern, an der die Klasse initialisiert wurde. Über statische Initialisierer ist dies jedenfalls nicht möglich.

import java.util.*;

public interface PropertyReader

{

  Properties DEFAULT_PROPERTIES = new Properties();

static

  ^

// Hier gibt's den Compilerfehler

// "Interfaces can't have static initializers."

  {

    DEFAULT_PROPERTIES.setProperty( "date", new Date().toString() );

}

  Properties getProperties();

}

Zwar sind statische Initialisierungsblöcke nicht möglich, aber mit zwei Tricks kann die Initialisierung erreicht werden. Eine innere anonyme Kasse ist eine Lösung:

import java.util.*;

public interface PropertyReader

{

  Properties DEFAULT_PROPERTIES = new Properties() { {

      setProperty( "date", new Date().toString() );

} };

  Properties getProperties();

}

Diese Lösung funktioniert allerdings nur, wenn keinen primitiven Werte über extra Programmcode initialisiert werden, und wenn es nicht-finale Klassen sind, von den überhaupt Unterklassen erlaubt sind.

Mit einem anderen Trick lassen sich auch diese Hürden nehmen. Die Idee liegt in der Einführung einer inneren statischen Klasse, die wir $$ nennen wollen. Innerhalb dieser Klasse platzieren einen static-Block. Anschließend muss nur noch eine Dummy-Variable gesetzt werden, damit der Initialisierungsblock der inneren Klasse auch ausgeführt wird, wenn die Klasse geladen wird. Dazu definieren wir die Variable $. Sie ist static und final, also somit eine echte Konstante. Da leider innere Klassen und Konstanten von Schnittstellen nicht privat sein können, und so unglücklicherweise von außen zugänglich sind, geben wir ihnen die schönen kryptischen Namen $ und $$, sodass sie nicht so attraktiv erscheinen.

import java.util.*;

public interface PropertyReader

{

  Properties DEFAULT_PROPERTIES = new Properties();

  PropertyReaderInit $ = new $$();

  static final class $$

  {

    static

    {

      DEFAULT_PROPERTIES.setProperty( "date", new Date().toString() );

}

  }

  Properties getProperties();

}

Innerhalb von static-Block lässt sich auf das Properties-Objekt zugreifen und somit auch die Werte eintragen. Ohne die Erzeugung des Objekts $ geht es nicht, denn andernfalls würde die Klasse $$ nicht initialisiert werden. Das Programm kann nun alle Elemente wie folgt nutzen:

import java.util.Properties;

public class SystemPropertyReaderDemo implements PropertyReader

{

  @Override public Properties getProperties()

  {

    return System.getProperties();

  }

  public static void main( String[] args )

  {

    System.out.println( PropertyReader.DEFAULT_PROPERTIES ); // {date=Thu …

}

}

Hinweis:   Aufzählugen über enum können einfacher initialisiert werden.

Labels:

Spielerei mit Javas dynamischer Bindung und überschatteten Attributen

Werfen wir ein Blick auf folgendes Java-Programm:

class SuperBoaster

{

  int nr = 1;

void boast()

  {

System.out.println( "Ich bin die Nummber " + nr );

}

}

public class SubBoaster extends SuperBoaster

{

int nr = 2;

  @Override void boast()

{

super.boast();                   // Ich bin die Nummber 1

System.out.println( super.nr );  // 1

System.out.println( nr );        // 2

  }

  public static void main( String[] args )

{

    new SubBoaster().boast();

  }

}

Die Methode boast() aus SubBoaster ruft mit super.boast() die Methode der Oberklasse auf. Ein einfacher Aufruf von boast() in der Unterklasse würde in eine Rekursion führen. Die Unterklasse hat mit super.nr Zugriff auf die überschattete Objektvariable nr aus der Oberklasse. Es ist super wie this eine spezielle Referenz und kann auch genauso eingesetzt werden, nur das super in den Namensraum der Oberklasse geht.

Eine Aneinanderreihung von super-Schlüsselwörtern bei einer tieferen Vererbungshierarchie ist nicht möglich. Hinter einem super muss eine Objekteigenschaft stehen und Anweisungen wie super.super.nr sind somit immer ungültig.

Für Variablen gibt es eine Möglichkeit, die sich durch einen Cast in die Oberklasse ergibt. Setzen wir in boast() der Unterklasse folgende Anweisung

System.out.println( ((SuperBoaster) this).nr );      // 1

Die Ausgabe 1 ist also identisch mit System.out.println( super.nr ).

Die this-Referenz entspricht einem Objekt vom Typ SubBoaster. Wenn wir dies aber in den Typ SuperBoaster konvertieren, bekommen wir genau das nr aus der Basisklasse unserer Hierarchie. Wir erkennen hier eine sehr wichtige Eigenschaft von Java, nämlich, dass Variablen nicht dynamisch gebunden werden. Anders wäre es, wenn wir folgendes in die Methode boast()der Unterklasse SubBoaster setzen:

((SuperBoaster)this).boast();

Hier ruft die Laufzeitumgebung nicht boast() aus SuperBoaster auf, sondern die aktuelle Funktion boast(), aus SubBoaster sodass wir in einer Rekursion landen. Der Grund dafür liegt in der dynamischen Bindung zur Laufzeit, die ein Compiler-Typecast nicht ändert.

Labels:

Bildschirmabzüge (Screenshots) und die Frage, ob der Benutzer etwas vor dem Bildschirm tut

Eine sehr interessante Funktion der Robot-Klasse ist createScreenCapture(). Mit ihr lässt sich ein Bildschirmabzug (engl. screenshot) ohne Mauszeiger (zumindest unter Windows, Linux, Solaris und Mac OS X) machen. Die Funktion erwartet als Argument ein Rectangle-Objekt (speichert x, y, height und width), das den zu »fotografierenden« Bereich spezifiziert. Das Ergebnis von createScreenCapture() ist ein BufferedImage-Objekt, das direkt mit ImageIO in eine Datei kommen kann.

BufferedImage bi = new Robot().createScreenCapture(

    new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()) );   

ImageIO.write( bi, "jpg", new File("c:/screenshot.jpg") );

Mit createScreenCapture() lassen sich sehr interessante Lösungen realisieren. So lässt sich zum Beispiel überwachen, ob sich Bildschirminhalte ändern. Mit einer Bildanalyse ließe sich auch herausfinden, was der Anwender sich gerade anschaut.

In einem kleinen Beispiel wollen wir feststellen, ob der Benutzer aktiv vom dem Bildschirm ist oder nicht. Wir machen dies an der Anzahl Pixel fest, die sich jede Sekunden verändern. Ein Programm soll dazu jede Sekunde ein Bildschirmabzug nehmen und ausgeben, um wie viel Prozent sich die Pixel gegenüber dem Vorgängerbild verändert haben.

package com.tutego.insel.ui.image;

import java.awt.*;

import java.awt.image.*;

public class ImageDiffs

{

  public static void main( String args[] ) throws Exception

  {

    Rectangle screenSize = new Rectangle( Toolkit.getDefaultToolkit().getScreenSize() );

    BufferedImage image1 = new Robot().createScreenCapture( screenSize );

    while ( true )

    {

      Thread.sleep( 1000 );   

      BufferedImage image2 = new Robot().createScreenCapture( screenSize );

      DataBuffer dataBuffer1 = image1.getData().getDataBuffer();

      DataBuffer dataBuffer2 = image2.getData().getDataBuffer();

      int total = dataBuffer1.getSize(), diff = 0;

      for ( int i = 0; i < total; i++ )

        if ( dataBuffer1.getElem( i ) != dataBuffer2.getElem( i ) )

          diff++;

      System.out.printf( "Pixel total=%d, unterschiedliche Pixel=%d, Unterschied=%.2f%%%n",

                         total, diff, (double) 100 * diff / total );

      image1 = image2;

    }

  }

}

Nach dem der Bildschirmabzug gemacht wurde, gilt es auf die Pixel zuzugreifen und die beiden Bilder zu vergleichen. Um das Bild abzulaufen, könnten wir entweder vom BufferedImage mit den Methode getRGB(int x, int y) arbeiten, oder uns – anders macht es getRGB() auch nicht – das Datenmodel der Grafik holen und es einfach komplett auflaufen; die Größe und Breite der beiden Bildschirmabzüge ist ja immer gleich, und so spielt die Höhe und Breite keine Rolle. Von den beiden DataBuffer-Objekten, die die Rohdaten der Grafik repräsentieren, erfragt getElem() den Farbwert und wenn es Unterschiede gibt, inkrementiert das Programm einen Zähler. Nach dem Ablaufen aller Werte gibt es eine Statistik, die etwa so beginnen kann:

Pixel total=2304000, unterschiedliche Pixel=297, Unterschied=0,01%

Pixel total=2304000, unterschiedliche Pixel=1708, Unterschied=0,07%

Pixel total=2304000, unterschiedliche Pixel=1689, Unterschied=0,07%

Pixel total=2304000, unterschiedliche Pixel=1000568, Unterschied=43,43%

Pixel total=2304000, unterschiedliche Pixel=1002525, Unterschied=43,51%

Pixel total=2304000, unterschiedliche Pixel=2087, Unterschied=0,09%

Labels:

Mit VisualVM durch den Speicher wühlen: Heap Dump

VisualVM ist eine grafische Oberfläche mit einfachen Profiling-Möglichkeiten und einer grafischen Oberfläche etwa für das JDK-Tool jstack. VisualVM ist Teil von Java 6 (dort heißt es Java VisualVM), aber eine aktuelle Version findet sich immer unter https://visualvm.dev.java.net/; diese Version heißt dann einfach VisualVM. Nutzen wir die Version von der Webseite im Folgenden.

VisualVM wird als Zip-Archiv (etwa visualvm_111.zip) angeboten. Nach dem Auspacken befindet sich unter dem bin-Ordner das ausführbare Programm visualvm.exe, das wir starten können. (VisualVM lässt sich über ein Plugin auch über Eclipse, NetBeans oder IntelliJ einbinden.) Beim ersten Mal müssen wir noch die Lizenzen abnicken und eine Kalibrierung starten, doch dann öffnet sich schon die grafische Oberfläche. Wählen wir links im Baum Local > VisualVM aus, so schauen wir uns die Zustände, etwa den Speicherbedarf und Thread-Auslastung des Programms VisualVM selbst an.

visualvm

Durch den Speicher wühlen: Heap Dump

Eine großartige Möglichkeit von VisualVM ist, sich während der Laufzeit zu einem Programm zu verbinden, und über die Objektverweise zu navigieren. Beispiel soll ein kleines Programm HeapUser sein, von dem wir später die vier Objektvariablen untersuchen wollen.

HeapUser.java

import java.util.*;

public class HeapUser

{

String string = "Hallo Welt";

Date date = new Date();

ArrayList<String> list = new ArrayList<String>( Arrays.asList( string, date.toString() ) );

HeapUser heapUser;

public static void main( String[] args )

{

HeapUser h = new HeapUser();

h.heapUser = h;

new Scanner( System.in ).next();

System.out.println( h.string );

}

}

Starten wir das Programm und VisualVM läuft noch im Hintergrund, so erkennt VisualVM automatisch das gestartete Program und aktualisiert die Baumansicht unter local.

Im Kontextmenü auf HeapUser lässt sich der HeapDump erfragen.

visualvmHeapUserHeapDump

Nach dem Aktivieren des Schalters Classes sind alle geladenen Klassen aufgeführt, und wie viele Exemplare es von den Klassen gibt.

visualvmHeapUserHeapClasses

Unten gibt es ein Suchfeld, in dem wir HeapUser eintragen. Es bleibt eine Klasse in der Liste.

visualvmHeapUserHeapClasses2

Im Kontextmenü lässt sich nun Show in Instances View aufrufen.

visualvmHeapUserHeapShowInstances

Die folgende Ansicht bildet den Ausgangspunkt für exploratives Arbeiten.

visualvmHeapUserHeapShowInstancesHeapUser

Links ist abgebildet die Instanz, die wir untersuchen. Das ist HeapUser, von dem es genau ein Exemplar gibt (#1). Rechts gibt es zwei Einteilungen. In der oberen Einteilung können wir die Objekteigenschaften vom links ausgewählten Objekt sehen und durch die Baumansicht tiefer reinzoomen. So enthält this, also das ausgewählte Objekt, die Variablen heapUser, list, date und string. An den auf sich selbst verweisenden Pfeilen an heapUser lässt sich – die Symbolen werden in einer Art Statusleiste kurz erklärt – erkennen, dass die Variable heapUser das eigene Objekt referenziert. Falten wir list auf, so sehen wie die Objektvariablen der ArrayList-Instanz im Baum, und unter anderem lässt sich die size ablesen, also die Anzahl Elemente in der Liste. elementData wiederum ist ein Knoten, der sich auffalten lässt, der er repräsentiert das interne Feld – die eckigen Klammern deuten den Typ „Feld“ an – der ArrayList. Wird er ausgefaltet, gelangen wir zu den beiden Strings. Im unteren Bereich der Einteilung, bei References, ist abzulesen, wer das selektierte Objekt referenziert. Es gibt zwei Stellen, an denen das untersuchte HeapUser-Objekt referenziert wird: Einmal über die lokale Variable in der main-Methode, und einmal über die Objektvariable.

Profiling von Java-Applikationen

Ein Profiler zeigt an, an welchen Stellen ein Programm Prozessorzeit verbraucht. Auf der Webseite https://visualvm.dev.java.net/profiler.html stellt Sun Dokumentation bereit, wie VisualVM als Profiler genutzt wird.

Labels:

Pluggable Annotation Processing API

Annotationen  lassen sich in zwei Gruppen einteilen (andere Einteilungen sind möglich):

· Compiler-Annotationen: @Override, @Deprecated, @SuppressWarnings

· Programm-Annotationen, die zur Laufzeit ausgewertet werden: @Resource, @WebService, @WebMethod, …

Zwischen dem, was Compiler sieht und zwischen dem, was ein Programm zur Laufzeit auswertet, gibt es noch eine weitere Ebene, die der Tools.[1] Wir können den Compiler schlecht erweiterten, wenn er auf Grund von eigener Annotationen gewisse Prüfungen vornehmen soll (etwa ob eine mit @Entity annotierte Klasse einen Standardkonstruktor besitzt), oder Artefakte, also etwa andere Java-Klassen oder XML-Dokumente generieren soll. Zur Laufzeit wäre das zu spät.

Aus diesem Grund können sogenannte Custom Annotation Processors eingesetzt werden. Sie werden vom Java-Compiler berücksichtigt (der mit einem Schalter auch kein Bytecode generieren muss) und haben Zugriff auf die Strukturdaten der Java-Typen, also Klassennamen, Methodennamen, usw. Das ist ein bisschen wie statisches Reflection, nur dass eben Reflection zur Laufzeit und mit Hilfe der JVM arbeitet und die Annotations-Prozessoren mit Hilfe des Compilers. Die Infrastruktur hat Sun in Java 5 vorbereitet – und damals das „Annotation Processing Tool“ (apt) in Umlauf gebracht – und noch einmal in Java 6 nachgebessert und über die JSR 269 (http://jcp.org/en/jsr/detail?id=269) genauer spezifiziert.

Der erste Annotations-Prozessor

Um einen Annotations-Prozessor in den Compiler einzuhängen sind zwei Schritte nötig:

· Zunächst wird mit Hilfe der Pluggable Annotation Processing API eine Klasse, der eigentliche Annotations-Prozessor geschrieben. Dieser kann auf die annotierten Elemente zurückgreifen und sogar in den Compilerprozess eingreifen.

· Der Annotations-Prozessor muss dem Compiler bekannt gemacht werden. Das geschieht entweder über einen Eintrag in einer besonderen Textdatei oder über den Compilerschalter -processor bei javac.

1.1.1 Einen Annotations-Prozessor schreiben

Jeder Annotations-Prozessor implementiert die Schnittstelle javax.annotation.processing.Processor. Es ist allerdings nicht nötig, diese direkt zu implementieren; Java bietet mit AbstractProcessor eine Klasse, in der nur noch eine Methode realisiert werden muss: process(). Genau diese Methode enthält den Kern der Anwendung, in dem auf die die Annotationen zurückgegriffen werden kann. Der process()-Methode übergibt der Java-Compiler alle Informationen. Geben wir diese Informationen in unserem ersten Beispiel aus:

com.tutego.insel.annotation.apt.FirstProcessor.java

package com.tutego.insel.annotation.apt;

import java.util.Set;

import javax.annotation.processing.*;

import javax.lang.model.SourceVersion;

import javax.lang.model.element.TypeElement;

import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes( "*" )

@SupportedSourceVersion( SourceVersion.RELEASE_6 )

public class FirstProcessor extends AbstractProcessor

{

  @Override

public boolean process( Set<? extends TypeElement> annotations,

                          RoundEnvironment roundEnv )

  {

    String msg = String.format( "%n %s%n %s",

                                roundEnv.toString(), annotations.toString() );

    processingEnv.getMessager().printMessage( Kind.NOTE, msg );        

return false;

  }

}

Der Annotations-Prozessor ist selbst annotiert mit @SupportedAnnotationTypes – was bestimmt, auf welche Annotationen der Annotations-Prozessor hört (mit * reagiert er auf alle Annotationen) – und mit @SupportedSourceVersion.

Beginnt der Java-Compiler den Annotations-Prozessor zu initialisieren, gibt es einen vordefinierten Lebenszyklus. Es beginnt mit dem Aufbau über den Standardkonstruktor. Dann ruft er die Methode  void init(ProcessingEnvironment) auf. Praktischerweise implementiert AbstractProcessor diese Methode schon und merkt sich die übergebene ProcessingEnvironment-Referenz in der protected Variablen processingEnv. Anschließend folgt ein Aufruf von process(). Die Bedeutung der Argumente lässt sich am Besten an der Ausgabe ablesen, wenn der Annotations-Prozessor eingebunden ist und arbeitet.

1.1.2 Einen Annotations-Prozessor einbinden

Entweder wird der Annotations-Prozessor über den Schalter -processor bei javac angegeben oder in einer Datei javax.annotation.processing.Processor vermerkt, die sich im Verzeichnis META-INF/services befindet. Wählen wir diesen Weg, denn damit kann die Angabe bei jedem Compileraufruf unterbleiben.

META-INF/services/javax.annotation.processing.Processor

com.tutego.insel.annotation.apt.FirstProcessor

Da unser FirstProcessor auch etwas zu verarbeiten haben muss, setzten wir drei Typen in einer Compilation-Unit in das Default-Paket.

FullOfAnnotations.java

import javax.annotation.Resource;

import javax.jws.WebService;

import javax.xml.bind.annotation.*;

@XmlRootElement public class FullOfAnnotations

{

@Resource String string;

@XmlElement String blab;

}

@XmlRootElement @WebService interface FullOfAnnotations2

{

@Deprecated void foo( int i );

}

enum NotFullOfAnnotations { }

Unser Annotations-Prozessor wird also später drei Typen finden (auch wenn NotFullOfAnnotations keine Annotation besitzt!) und insgesamt fünf Annotationen.

Wechseln wir für einen Test auf die Kommandozeile, so verdeutlicht ein dir die Verzeichnisstruktur:

$ dir /B

com

META-INF

FullOfAnnotations.java

Im Unterverzeichnis com liegen nun die anderen Klassen und auch der Annotations-Prozessor, in META-INF gib es das Unterverzeichnis services, in dem wiederum die javax.annotation.processing.Processor liegt. Dann gibt es unsere Testklasse FullOfAnnotations, die nun kompiliert werden soll. Dabei wird sich der Annotations-Prozessor einschalten und Meldungen ausgeben:

$ javac *.java

Note:

[errorRaised=false, rootElements=[FullOfAnnotations, FullOfAnnotations2, NotFullOfAnnotations], processingOver=false]

[javax.annotation.Resource, java.lang.Deprecated, javax.jws.WebService, javax.xml.bind.annotation.XmlRootElement, javax.xml.bind.annotation.XmlElement]

Note:

[errorRaised=false, rootElements=[], processingOver=true]

[]

Drei Dinge lassen sich an der Ausgabe auslesen:

1. roundEnv.toString() in process() zeigt die rootElements, also genau die Typen, die der Annotations-Prozessor verarbeitet hat. Darunter ist auch NotFullOfAnnotations, was überhaupt keine Annotationen hat, doch der Annotations-Prozessor hat dennoch diesen Typ gesehen.

2. annotations.toString() zeigt alle Annotationen an, die während der gesamten Durchlaufs aller Klassen gefunden wurde.

3. Der Annotations-Prozessor läuft in zwei Runden. Der Grund ist einfach: In jeder Runde könnten sich Programminformationen ändern, sodass ein neuer Lauf nötig ist, auch diese Änderungen einzusammeln. Wir sehen in der Ausgabe, dass beim zweiten Lauf keine Neuerungen hinzugekommen sind, und damit keine weitere Iteration nötig ist. Die letzte Anzeige zeigt das mit „processingOver=true” an.

1.1.3 Weitere Methoden der Pluggable Annotation Processing API

Im Folgenden sollen ein paar Code-Idiome vorgestellt und weitere Methoden der Pluggable Annotation Processing API vorstellt werden.

Wenn immer eine Runde gedreht wird, gibt es etwas zu tun. Ein Annotations-Prozessor enthält in der Regel die Fallunterscheidung

if ( ! roundEnv.processingOver() )

  ...

um nur dann etwas zu tun, wenn die Verarbeitung noch nicht gelaufen ist.

Zugriff auf die Wurzelelemente, also alle vom Annotations-Prozessor gefundenen Typen, liefert roundEnv.getRootElements(). Das Ergebnis ist eine Menge von javax.lang.model.element.Element-Objekten.

Set<? extends Element> elements = roundEnv.getRootElements();

Element selbst ist eine Schnittstelle und von ihr gibt es diverse Unterschnittstellen. Die Methode getKind() zeigt, mit welchem konkreten Element wir es wirklich zu tun haben. Lassen wir eine Schleife über unsere Wurzelelemente laufen:

for ( Element element : roundEnv.getRootElements() )

  processingEnv.getMessager().printMessage( Kind.NOTE, element.getKind().toString() );     

Die Ausgabe wäre:

Note: CLASS

Note: INTERFACE

Note: ENUM

Von den Typen aus lässt es sich weiter in die Tiefe laufen. Dafür bietet die Element-Klasse die Methode getEnclosedElements(). Das Ergebnis ist vom Typ List<? extends Element>. Um alle Attribute der vom Annotations-Prozessor gefunden Klassen auszugeben, schreiben wir:

for ( Element element : roundEnv.getRootElements() )

{

  processingEnv.getMessager().printMessage( Kind.NOTE, element.getSimpleName() );

  for ( Element enclosedElement : element.getEnclosedElements() )

    if ( enclosedElement.getKind() == ElementKind.FIELD )

      processingEnv.getMessager().printMessage( Kind.NOTE,

                                                " " + enclosedElement.getSimpleName() );

}

Die Augabe ist:

Note: FullOfAnnotations

Note:  string

Note:  blab

Note: FullOfAnnotations2

Note: NotFullOfAnnotations

Die Elemente könnten nun weiter untersucht werden, oder auf den konkreten Typ gecastet werden, was zu zusätzliche Methoden führt.

Interessieren uns für alle Methoden (und Konstruktoren), schreiben wir:

for ( Element enclosedElement : element.getEnclosedElements() )

if ( enclosedElement instanceof ExecutableElement )

{

    ExecutableElement e = (ExecutableElement) enclosedElement;

    String msg = e.getModifiers() +  " " + e.getReturnType() + " " +

                 e.getSimpleName() + "( " + e.getParameters() + " ) " +

                 e.getThrownTypes();

    processingEnv.getMessager().printMessage( Kind.NOTE, " " + msg );

}

Wenn sowieso nur eine beschränkte Anzahl von Annotationen in Betracht kommen, sollte @SupportedAnnotationTypes präzisiert werden. Wir wollen das in einem letzten Beispiel nutzen, um mit @Entity deklarierte Klassen zu testen, ob sie einen nicht-parametrisierten Konstruktor besitzen.

Zunächst die Annotation:

com.tutego.insel.annotation.apt.javax.persistence.Entity.java

package com.tutego.insel.annotation.apt.javax.persistence;

public @interface Entity { }

Unser Annotationstyp Entity ist eine sehr einfache Kopie vom Annotationstyp aus javax.persistence.

Eine Beispielkasse RealEntity soll korrekterweise einen Standardkonstruktor haben:

com.tutego.insel.annotation.apt.RealEntity.java

package com.tutego.insel.annotation.apt;

import com.tutego.insel.annotation.apt.javax.persistence.Entity;

@Entity

public class RealEntity

{

  public RealEntity() { }

  public RealEntity( String name ) { }

}

Aber die Klasse NoRealEntity nicht:

com.tutego.insel.annotation.apt.RealEntity.java, RealEntity

@Entity public class NoRealEntity

{

    public NoRealEntity( String name ) { }

}

Der Annotations-Prozessor ist nicht schwierig. Bei SupportedAnnotationTypes geben wir Entity an und dann laufen wir alle Methoden ab, die <init> heißen und testen, ob die Parameterliste leer ist. (Sichtbarkeiten, Exceptions, Modifizierer betrachten wir nicht.)

package com.tutego.insel.annotation.apt;

import java.util.Set;

import javax.annotation.processing.*;

import javax.lang.model.SourceVersion;

import javax.lang.model.element.*;

import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes( "com.tutego.insel.annotation.apt.javax.persistence.Entity" )

@SupportedSourceVersion( RELEASE_6 )

public class CheckEntityBeansForDefaultConstructorProcessor extends AbstractProcessor

{

  @Override

  public boolean process( Set< ? extends TypeElement> annotations,

                          RoundEnvironment roundEnv )

  {

    if ( ! roundEnv.processingOver() )

      for ( Element element : roundEnv.getRootElements() )

      {

        boolean foundDefaultContructor = false;

        for ( Element ee : element.getEnclosedElements() )

          if ( ee instanceof ExecutableElement )

          {

if (    "<init>".equals( ((ExecutableElement) ee).getSimpleName().toString() )

                 && ((ExecutableElement) ee).getParameters().isEmpty() )

              foundDefaultContructor = true;

          }

        if ( ! foundDefaultContructor )

          processingEnv.getMessager().printMessage( Kind.ERROR,

            element.getSimpleName() + " hat keinen Standardkonstruktor!" );

      }

    return false;

  }

}

Ein kleiner Eintrag in die javax.annotation.processing.Processor-Datei und damit ist die Vorbereitung erledigt.

META-INF/services/javax.annotation.processing.Processor

# com.tutego.insel.annotation.apt.FirstProcessor

com.tutego.insel.annotation.apt.CheckEntityBeansForDefaultConstructorProcessor

Auf der Kommandozeile kann das nun getestet werden:

$ javac com/tutego/insel/annotation/apt/*.java

error: NoRealEntity hat keinen Standardkonstruktor!

1 error

Nehmen wir in RealEntity zum Beispiel den Standard-Konstruktur auch heraus, folgt:

$ javac com/tutego/insel/annotation/apt/*.java

error: NoRealEntity hat keinen Standardkonstruktor!

error: RealEntity hat keinen Standardkonstruktor!

2 errors

Um den Java-Compiler nur als Annotations-Prozessor-verarbeitende Maschine einzusetzen, wenden wir den Schalter -proc:only an.

$ javac -proc:only com/tutego/insel/annotation/apt/*.java


[1]      Das wird auch klar, wenn wir uns vor Augen führen, dass  die Annotation @Retention steuert, wer die Annotation sehen kann. Und dass es in RetentionPolicy drei Konstanten gibt: SOURCE, CLASS, RUNTIME.

Labels:

Beans aus XML Schema-Datei generieren, xjc, JAXB Plugins

Da es für existierende XML-Dateien mühselig ist, die annotierten JavaBeans von Hand aufzubauen, gibt es einen Generator. Der Java Architecture for XML Binding Binding Compiler, kurz xjc XE "xjc" , ist Teil vom JDK 6 und kann von der Kommandozeile, Ant-Skript oder auch von Entwicklungsumgebungen[1] aufgerufen werden. Er nimmt eine XML Schema-Datei und generiert die Java-Klassen und eine ObjectFactory, die als – wie der Name schon sagt – Fabrik für die gemappten Objekte aus der XML-Elemente ist.
Die Geocoding API von Yahoo!

Für ein Beispiel wählen wir die „Yahoo! Maps Web Services - Geocoding API“. Mit ihr lassen sich zu einer Adresse in den USA die Latitude (geographische Breite) und Longitude (geographische Länge) ermitteln. (Im nächsten Schritt könnte dann ein Kartendienst die Adresse mit Hilfe der Geodaten anzeigen.)

Der Online-Dienst arbeitet über den REST-Stil, das heißt, eine URL enthält die Parameter der Anfrage. Die Webseite von Yahoo! gibt in einem Beispiel für eine URL vor:

http://local.yahooapis.com/MapsService/V1/geocode?appid=YD-9G7bey8_JXxQP6rxl.fBFGgCdNjoDMACQA--&street=701+First+Ave&city=Sunnyvale&state=CA

Der Kartenserver antwortet mit einer XML-Datei (zum Verdeutlichung hübsch formatiert):

<?xml version="1.0"?>

<ResultSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

           xmlns="urn:yahoo:maps"

           xsi:schemaLocation="urn:yahoo:maps

             http://api.local.yahoo.com/MapsService/V1/GeocodeResponse.xsd">

<Result precision="address">

    <Latitude>37.416397</Latitude>

    <Longitude>-122.025055</Longitude>

    <Address>701 1st Ave</Address>

    <City>Sunnyvale</City>

    <State>CA</State>

    <Zip>94089-1019</Zip>

    <Country>US</Country>

</Result>

</ResultSet>

Für unser Beispiel wollen wir das XML-Dokument, das der Yahoo! Maps Web Service liefert, nicht von Hand auseinanderpflücken, sondern JAXB soll uns eine gefüllte JavaBean mit allen Informationen liefern.

xjc aufrufen

Im ersten Schritt wechseln wir auf die Kommandozeile und testen, ob entweder das bin-Verzeichnis vom JDK im Suchpfad ist, oder wir wechseln direkt in das bin-Verzeichnis, sodass wir xjc direkt aufrufen können, und folgende Ausgabe erscheint.

$ xjc

grammar is not specified

Usage: xjc [-options ...] <schema file/URL/dir/jar> ... [-b <bindinfo>] ...

If dir is specified, all schema files in it will be compiled.

If jar is specified, /META-INF/sun-jaxb.episode binding file will be compiled.

Options:

  -nv                :  do not perform strict validation of the input schema(s)

  -extension         :  allow vendor extensions - do not strictly follow the

                        Compatibility Rules and App E.2 from the JAXB Spec

  -b <file/dir>      :  specify external bindings files (each <file> must have its own -b)

                        If a directory is given, **/*.xjb is searched

  -d <dir>           :  generated files will go into this directory

  -p <pkg>           :  specifies the target package

  -httpproxy <proxy> :  set HTTP/HTTPS proxy. Format is [user[:password]@]proxyHost:proxyPort

  -httpproxyfile <f> :  Works like -httpproxy but takes the argument in a file to protect password

  -classpath <arg>   :  specify where to find user class files

  -catalog <file>    :  specify catalog files to resolve external entity references

                        support TR9401, XCatalog, and OASIS XML Catalog format.

  -readOnly          :  generated files will be in read-only mode

  -npa               :  suppress generation of package level annotations (**/package-info.java)

  -no-header         :  suppress generation of a file header with timestamp

  -target 2.0        :  behave like XJC 2.0 and generate code that doesnt use any 2.1 features.

  -xmlschema         :  treat input as W3C XML Schema (default)

  -relaxng           :  treat input as RELAX NG (experimental,unsupported)

  -relaxng-compact   :  treat input as RELAX NG compact syntax (experimental,unsupported)

  -dtd               :  treat input as XML DTD (experimental,unsupported)

  -wsdl              :  treat input as WSDL and compile schemas inside it (experimental,unsupported)

  -verbose           :  be extra verbose

  -quiet             :  suppress compiler output

  -help              :  display this help message

  -version           :  display version information

Extensions:

  -Xlocator          :  enable source location support for generated code

  -Xsync-methods     :  generate accessor methods with the 'synchronized' keyword

  -mark-generated    :  mark the generated code as @javax.annotation.Generated

  -episode <FILE>    :  generate the episode file for separate compilation

Eigentlich ist bis auf die Angabe der Schema-Quelle (aus einer Datei oder die URL) keine weitere Angabe nötig. Es ist aber praktisch, zwei Optionen zu setzen: -p bestimmt das Java-Paket für die generierten Klassen und -d das Ausgabeverzeichnis, wo der Generator die erzeugten Dateien ablegt. Yahoo! bietet die Schema-Datei unter http://local.yahooapis.com/MapsService/V1/GeocodeResponse.xsd an, die sich direkt bei xjc angeben lässt:

$ xjc -d "c:/" -p com.tutego.insel.xml.jaxb.yahoo.geocoding http://local.yahooapis.com/MapsService/V1/GeocodeResponse.xsd

parsing a schema...

compiling a schema...

com\tutego\insel\xml\jaxb\yahoo\geocoding\ObjectFactory.java

com\tutego\insel\xml\jaxb\yahoo\geocoding\ResultSet.java

com\tutego\insel\xml\jaxb\yahoo\geocoding\ResultType.java

com\tutego\insel\xml\jaxb\yahoo\geocoding\package-info.java

Das Tool generiert die Klasse ResultSet und ResultType für den komplexen Typ aus dem XML Schema, sowie package-info.java, um eine Paket-Annotation festmachen zu können und ObjectFactory, die zwei einfache Fabrikfunktionen enthält, um ein ResultSet- und ResultType-Objetk aufbauen zu können.

Die von xjc nach c:/ geschriebenen Java-Klassen müssen nun in das Java-Projekt geschoben werden. Dann kann ein Java-Programm den Service mit einer URL ansprechen, einen Unmarshaller aufbauen und sich das Ergebnis-XML in eine JavaBean konvertieren lassen.

JAXBContext context = JAXBContext.newInstance( ObjectFactory.class );

Unmarshaller unmarshaller = context.createUnmarshaller();

// http://developer.yahoo.com/maps/rest/V1/geocode.html

String url = "http://local.yahooapis.com/MapsService/V1/geocode?" +

                      "appid=YD-9G7bey8_JXxQP6rxl.fBFGgCdNjoDMACQA--&" +

                      "street=701+First+Ave&city=Sunnyvale&state=CA";

ResultSet results = (ResultSet) unmarshaller.unmarshal( new URL(url) );

ResultType result = results.getResult().get( 0 );

System.out.printf( "Longitude = %s, Latitude= %s%n", result.getLongitude(), result.getLatitude() );

Die Bildschirmausgabe zeigt die geographische Länge und Breite der Yahoo! Konzernzentrale:

Longitude = -122.025055, Latitude= 37.416397

Konflikte in der Schema-Datei

Der Yahoo! Service ist zwar nett, aber gerne hätte ich andere OX-Mapping von anderen XML-Dokumenten gezeigt.[2] Leider haben viele XML Schemata ein Problem, sodass sie nicht direkt vom Schema-Compiler verarbeitet werden können. Ein Beispiel zeigt das Dilemma.

<container>

<head><content title="Titel"/></head>

<body><content doc="doc.txt"/></body>

</container>

In der hierarchischen Struktur heißt das in <head> und <body> vorkommende XML-Element gleich, nämlich content. Die Schema-Datei kann widerspruchslos definieren, dass die beiden XML-Elemente gleich heißen, aber unterschiedliche Attribute erlauben, sozusagen dass Head-Content und das Body-Content. Allein durch ihre Hierarchie, also dadurch, dass sie einmal unter head, und einmal unter body liegen, sind sie eindeutig bestimmt. Der Schema-Compiler von Java bekommt aber Probleme, da er diese hierarchische Information in eine flache bringt. Er kann einfach eine Klasse Head und Body aufbauen, aber bei <content> steht er vor einem Problem. Da die Schema-Definitionen unterschiedlich sind, müssten zwei verschiedene Java-Klassen unter dem gleichen Namen Content generiert werden. Das geht nicht, und xjc und bringt mit Fehlern ab.

Fehler diese Art gibt es leider häufig, und sind der Grund, warum aus vielen Schemata nicht einfach JavaBeans generiert werden können. Erfolglos ohne weitere Einstellungen sind beispielsweise DocBook, Office Open XML, SVG, MathML und weitere. Doch wie sieht die Lösung aus? jxc sieht Konfigurationsdateien vor, die das Mapping anpassen können. In diesen Mapping-Dokumenten identifiziert ein XPath-Ausdruck die problematische Stelle und gibt einen Substitutionstyp an. Die Spezifikation unter https://jaxb.dev.java.net/spec-download.html weist Interessierte in die richtige Richtung.

JAXB Plugins

Auf der Webseite https://jaxb2-commons.dev.java.net/ gibt es eine Reihe nützlicher zusätzlicher Plugins für JAXB. Darunter:

· Camelcase Always Plugin. Sind die Elementnamen großgeschrieben, so wird JAXB automatisch großgeschriebene Properties umsetzen, sodass etwa aus NAME der Setter/Getter setNAME() und getNAME() entsteht. Das Plugin verhindert dies und nennt die Setter/Getter wie gewohnt setName() und getName().

· Value-Constructor Plugin. Jede JavaBean bekommt von xjc nur einen Standard-Konstruktur. Dieses Plugin gibt einen weiteren Konstruktor hinzu, der alle Attribute direkt initialisiert.

· Default Value Plugin. Ein XML Schema kann mit defaultValue vordefinierte Initialbelegungen für Attribute angeben. xjc ignoriert diese. Das Plugin wertet diese Vorbelegungen aus und initialisiert die Attribute der JavaBean gemäß den Werten.

· Property Change Listener Injector Plugin. Eine über xjc generierte JavaBean schreibt ein bei setXXX() übergebenen Wert direkt in das private Attribut durch. Mit dem Plugin wird ein VetoableChangeListener eingeführt, der gegen Wertänderungen votieren kann.


[1]      Für Eclipse gibt es dazu das Zusatz-Plugin https://jaxb-workshop.dev.java.net/plugins/eclipse/xjc-plugin.html – NetBeans integriert xjc direkt.

[2]      Die Google Keyhole Markup Language (KML) Version 2.1 funktioniert auch. Die Schema-Datei  http://code.google.com/intl/de/apis/kml/schema/kml21.xsd macht keine Probleme. Sei dem KML aber beim Open Geospatial Consortium liegt, gibt es für der Version 2.2 und dem Schema http://schemas.opengis.net/kml/2.2.0/ogckml22.xsd Übersetzungsprobleme.

Labels:

Fail-Fast Iterator und die ConcurrentModificationException

Beackern zwei Programmstellen gleichzeitig eine Datenstruktur, kann es zu Schwierigkeiten kommen, wenn sich die Datenstruktur strukturell verändert, also neue Elemente hinzukommen oder wegfallen. Probleme treten leicht auf, wenn ein Verweis auf die Datenstruktur im Programm an verschiedenen Stellen weitergegeben wird. Führt nun eine Stelle strukturellen Änderungen mit Methoden wie remove() oder add() durch, und die Größe der Liste verändert sich damit, kann das zu Zugriffsfehlern führen, wenn die andere Stelle von der Änderung nichts mitbekommt.

Nehmen wir an, zwei Programmteile greifen auf eine gemeinsame Liste zurück. Ein Programmteil merkt sich eine Suchstelle und will anschießend auf das gefundene Element zurückgreifen. Zwischen diesen beiden Operationen verändert jedoch ein anderes Programmteil die Liste und die Position des Elements verschiebt sich:

List<String> list = new ArrayList<String>( Arrays.asList( "Trullo", "Zippus" ) );
int posOfZippus = list.indexOf( "Zippus" );
System.out.println( list.get( posOfZippus ) ); // Zippus
list.add( 0, "Apulien" );
System.out.println( list.get( posOfZippus ) ); // Trullo

Nach dem Einfügen von “Apulien” ist posOfZippus also veraltet. Es wäre eine Hilfe, wenn get() die strukturelle Veränderung bemerken würde.


Beim Erfragen über Listen-Iteratoren ist das anders. Die Entwickler der Java-Bibliothek haben einen Entwurf gewählt, bei dem konfliktträchtigen Änderungen über die Listen-Iteratoren auffallen und zu einer ConcurrentModificationException führen. Das funktioniert so, dass sich die Liste die Anzahl struktureller Änderungen merkt – beim JDK von Sun intern in der Variablen modCount.

List<String> list = new ArrayList<String>( Arrays.asList( "Stunden", "der" ) );
Iterator<String> iterator = list.iterator(); // modCount = 0, expectedModCount = 0
list.get( 0 ); // Anfragen modCount nicht
list.add( "Entspannung" ); // modCount = 1, expectedModCount = 0
iterator.next(); // modCount != expectedModCount => CME

Der Iterator registriert die Änderungen, da sich der modCount in der Zwischenzeit verändert hat. Wird der Iterator initialisiert, erfragt er den modCount der Liste und speichert den Wert in expectedModCount. Jede strukturelle Änderung in der Liste führt zum Inkrement vom modCount. Wird später eine Operation über den Iterator durchgeführt, testet er, ob der gemerkte expectedModCount mit dem aktuellen modCount übereinstimmt. In unserem Fall tut er das nicht, denn add() ist eine strukturelle Änderung und modCount wird 1. Beim nachfolgendem next() kommt es zu einer ConcurrentModificationException.

  

Labels:

Ausblick auf die Logging-Zukunft

Immer wieder gibt es Versuche, die Logging-Bibliothek von Sun und log4j unter einen Hut zu bringen. Das kann so aussehen, dass die Client-API eine andere ist als die darunterliegende Bibliothek, die das Logging wirklich durchführt. Der populärste Versuch ist eine leichtgewichtige Bibliothek Simple Logging Facade[1] for Java (http://www.slf4j.org/), abgekürzt SLF4J. Die API fühlt sich etwa so an:

import org.slf4j.*;

public class HelloMyLogger

{

private final static Logger log = LoggerFactory.getLogger( HelloMyLogger.class );

public static void main( String[] args )

{

log.info( "Hallo {}", "Welt" );

}

}

SLF4J hat sich mittlerweile als Logging-Fassade etabliert und schiebt eine alternative API, die Apache Commons Logging (http://jakarta.apache.org/commons/logging/), abgekürzt JCL, in den Hintergrund. JCL führte in der Praxis zu gewaltigen Problemen mit Klassenladern (http://www.qos.ch/logging/thinkAgain.jsp) und wurde von einigen Kritikern als größter Irrsinn beschimpft (http://www.jroller.com/page/fate/?anchor=the_apache_syndrome). Vielleicht kein Wunder, dass die letzte JCL-Version 1.1.1 von Ende 2007 ist. SLF4J hat keine Probleme mit Klassenladern. Zudem bietet SLF4J hübsche parametrisierbare Log-Nachrichten, also das, was in den geschweiften Klammern zu sehen ist und einen Platzhalter definiert.

Während SLF4J eine Fassade ist, die kein eigenes Logging implementiert, gibt es auch Veränderungen unter der Haube. Dabei ist es nicht so, dass sich Suns Logging verändert hätte, sondern Logback (http://logback.qos.ch/) tritt als Nachfolger für log4j an. Logback implementiert auch die API von SLF4J direkt. Der Autor von log4j und Logback ist in beiden Fällen Ceki Gülcü, sodass Logback von der langen Logging-Erfahrung des Autors profitiert. Zwar hat log4j immer noch mehr Nutzer als Logback und ist besser bekannt, doch es kann gut sein, das in der nächsten Auflage der Insel nicht mehr Logging am Beispiel von log4j, sondern an Logback gezeigt wird.


[1] Das Prinzip, eine einfache API vor eine komplexe Implementierung zu setzten, ist unter dem Entwurfsmuster Fassade bekannt.

Labels:

Inselraus: getContextClassLoader() vom Thread

Entwickler von Java-Enterprise-Applikationen haben oft damit zu kämpfen, dass immer der falsche Klassenlader eine Klasse bezieht und die Typen dann nicht zusammenpassen. In unserem Beispiel mit dem statischen Initialisierungsblock ist gut zu erkennen, dass durch das zweimalige Laden die Laufzeitumgebung auch zweimal die Anweisungen ausführt. Hätten wir Singletons definiert, würden ihre statische Anfragemethoden unterschiedliche, nicht kompatible Objekte liefern, obwohl es laut Defini-tion eines Singletons der Fall sein müsste. Allgemein gesprochen: Besonders Fabrikfunktionen liefern bei mehreren Versionen der Klasse unterschiedliche Objekte, die nicht zusammenpassen. Zwei Lösungen gibt es hier: Zum einen bekommen die Fabrikfunktionen einen Klassenlader, in dessen Kontext sie die Klassen erzeugen können, oder sie nutzen den Klassenlader, der mit einem Thread ver-bunden ist.

Jeder Thread ist mit einem Klassenlader assoziiert, der standardmäßig mit dem Standardklassenlader identisch ist. getContextClassLoader() auf dem Thread-Objekt bezieht diesen Klassenlader:

Listing 8.13    com/tutego/insel/lang/ThreadClassLoader.java, main()
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println( loader );  // sun.misc.Launcher$AppClassLoader@a12a00
loader = ThreadClassloader.class.getClassLoader();
System.out.println( loader );  // sun.misc.Launcher$AppClassLoader@a12a00

Soll der assoziierte Klassenlader geändert werden, bewerkstelligt dies setContextClassLoader().

Labels:

Inselupdate: enum mit eigenen Konstruktoren und Methoden

Da eine enum-Klasse mit der Klassendeklaration verwandt ist, kann sie ebenso Attribute und Metho-den deklarieren. Geben wir einer Aufzählung Country eine Methode, die den ISO 3166-2 Landescode des jeweiligen Aufzählungselements liefert:

public enum Country
{
  GERMANY, UK, CHINA;

  public String getISO3Country()
  {
    if ( this == GERMANY )
      return Locale.GERMANY.getISO3Country();
    else if ( this == UK )
      return Locale.UK.getISO3Country();
    return Locale.CHINA.getISO3Country();
  }
}   

Die Methode getISO3Country() kann nun auf den Enum-Objekten aufgerufen werden.

System.out.println( Country.CHINA.getISO3Country() ); // CHN
Da switch auf enum erlaubt ist, können wir schreiben:

Country c = Country.GERMANY;

switch ( c )
{
  case GERMANY:
    System.out.println( "Aha. Ein Krauti" );         // Aha. Ein Krauti
    System.out.println( meinLand.getISO3Country() ); // DEU
    break;
  default:      System.out.println( "Anderes Land" );
}

Enum mit Konstruktoren

Neben dieser Variante wollen wir eine zweite Implementierung nutzen, und nun Konstruktoren hinzu-ziehen, um das gleiche Problem auf andere Weise zu lösen:

public enum Country
{
  GERMANY( Locale.GERMANY ), UK( Locale.UK ), CHINA( Locale.CHINA );

  private Locale country;

  private Country( Locale country )
  {
    this.country = country;
  }

  public String getISO3Country()
  {
    return country.getISO3Country();
  }
}

Bei der Deklaration der Konstanten wird in runden Klammern ein Argument für den Konstruktor übergeben. Der Konstruktor speichert das zugehörige Locale-Objekt in der internen Variable country, auf die dann getISO3Country() Bezug nimmt.

Enum mit überschriebenen Methoden   

In dem Enum-Typen lassen sich nicht nur Methoden hinzufügen, sondern auch Methoden überschrei-ben. Beginnen wir mit einer lokalisierten und überladenen Methode toString().

public enum WeekdayInternational
{
  SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;

  @Override
  public String toString()
  {
    return new SimpleDateFormat().getDateFormatSymbols().getWeekdays()[ ordinal() + 1 ]; 
  }

  public String toString( Locale l )
  {
    return new SimpleDateFormat("", l).getDateFormatSymbols().getWeekdays()[ ordinal() + 1 ]; 
  }
}

Die erste Methode ist aus unserer Oberklasse Object überschrieben, die zweite als überladene Funk-tion hinzugefügt. Ein Beispiel macht den Aufruf und die Funktionsweise klar:

System.out.println( WeekdayInternational.SATURDAY );                         // Samstag
System.out.println( WeekdayInternational.SATURDAY.toString() );              // Samstag
System.out.println( WeekdayInternational.SATURDAY.toString(Locale.FRANCE) ); // samedi
System.out.println( WeekdayInternational.SATURDAY.toString(Locale.ITALY) );  // sabato

An dieser Stelle hören die Möglichkeiten der Enum-Syntax aber noch nicht auf. Ähnlich wie die Syn-tax von inneren anonymen Klassen, die es erlauben, Methoden zu überschreiben, bieten Aufzählungs-typen eine ähnliche Syntax, um gezielt Methoden für eine spezielle Konstante zu überschreiben.

Nehmen wir an, in einem Spiel gibt es eine eigene Währung, denn Ponro Dollar. Nun soll dieser aber mit einer Referenzwährung, dem Euro, in Beziehung gesetzt werden: Der Wechselkurs ist einfach 1:2.

public enum GameCurrency
{
  EURO() {
    @Override double convertTo( GameCurrency targetCurrency, double value )
    {
      return targetCurrency == EURO ? value : value / 2;
    }
  },
  PONRODOLLAR() {
    @Override double convertTo( GameCurrency targetCurrency, double value )
    {
      return targetCurrency == PONRODOLLAR ? value : value * 2;
    }
  }; 
  abstract double convertTo( GameCurrency targetCurrency, double value );
}

Der interessante Teil ist die Deklaration der abstrakten convertTo() Methode und der Implementie-rung lokal bei den einzelnen Konstanten. (Natürlich müssen wir nicht jede Methoden im Enum abs-trakt machen, sondern sie kann auch konkret sein. Dann muss nicht jedes Enum-Element die abstrakte Methode implementieren.)

Mit einem statischen Import für die Aufzählung lässt sich die Nutzung und Funktionalität schnell zeigen:

System.out.println( EURO.convertTo( EURO, 12 ) );               // 12.0
System.out.println( EURO.convertTo( PONRODOLLAR, 12 ) );        // 6.0
System.out.println( PONRODOLLAR.convertTo( EURO, 12 ) );        // 24.0
System.out.println( PONRODOLLAR.convertTo( PONRODOLLAR, 12 ) ); // 12.0

Labels:

Insel: Kurzer Abriss der Java Open-Source-Werdung

Schon seit Java 1.0 gibt es den Quellcode der Standard-Bibliotheken (beim JDK im im Wurzelverzeichnis unter dem Namen src.zip) und jeder Interessierte konnte einen Blick auf die Implementierung werfen. Zwar verschloss Sun nicht den Blick auf die Implementierungen, aber weder die Laufzeitumgebung, noch der Compiler oder die Bibliotheken standen unter einer akzeptierten Open-Source-Lizenz. Sehr über 10 Jahren gab es in der Software-Welt Forderungen an Sun, die gesamte Java-Plattform unter einer bekanntere Lizenzform wie der GNU General Public License (GPL) oder BSD-Lizenz zu stellen. Dabei hat Jonathan Schwartz in San Francisco bei der JavaOne Konferenz 2006 schon angedeutet: ››It's not a question of whether we'll open source Java, now the question is how‹‹. War die Frage also statt des „Ob“ ein „Wie“, kündigte bei der Eröffnungsrede der JavaOne Konferenz im Mai 2007 Rich Green die endgültige Freigabe Java als OpenJDK  unter der Open Source-Lizenz GPL 2 an. Dem ging Ende 2006 die Freigabe von Compiler und der virtuellen Maschine voraus.

Die Geschichte ist allerdings noch ein wenig komplizierter. Obwohl OpenJDK nun unter der GPL steht, enthielt es doch Teile wie den Font-Renderer, Sound-Unterstützung, Farbmanagement, SNMP-Code, die als binäre Pakete beigelegt wurden, weil etwa die Rechte zur Veröffentlichung fehlten. Sun nennt diese Teile, die etwa 4 % vom JDK 6 ausmachen, „belasteter Code“ (engl. „encumbered code“) . Das hindere puristische Linux-Distributoren daran, OpenJDK auszuliefern. RedHat veröffentlichte daraufhin im Juni 2007 das Projekt IcedTea, um diese binären Teile auf der Basis vom OpenJDK durch GPL-Software zu ersetzen. So basiert der Font-Renderer zum Beispiel auf FreeType  und das Farbmanagement auf little CMS . Mit diesen Ersetzungen schaffte dann OpenJDK + IcedTea im Juni 2008 das Technology Compatibility Kit (TCK) von Sun und heißt nun OpenJDK 6. Darauf hin floss das OpenJDK 6 unter der GPLv2 in Linux-Distributionen wie Fedora und Debian ein.

Labels:

Inselupdate: instanceof mit Class-Objekten, isInstance() und isAssignableFrom

Der binären Operator instanceof testet, ob ein Objekt Exemplar einer Klasse oder der Oberklasse ist. Wenn das Ergebnis wahr ist, lässt sich das Objekt unter dem gegeben Typen ansprechen, ist also zu-weisungskompatibel. Der rechte Operator bei instanceof, der Typname, muss jedoch immer zur Übersetzungszeit bekannt sein und kann nicht dynamisch, etwa durch eine String, festgelegt werden.

Ist der Typname zur Compilerzeit vielleicht unbekannt, kann das Class-Objekt helfen. Die Methode isInstance(Object) ist sozusagen ein dynamisches instanceof. Gilt mit dem Operator

object instanceof ReferenceType

So heißt das mit der Methode

ReferenceType-Class-Objekt.isInstance( object )

Gewöhungsbedürftig ist sicherlich die Tatsache, dass bei der Methode isInstance() die beiden Operanden umgedreht sind. Dazu ein paar Beispiele:

Component b = new JLabel();
out.println( b instanceof JLabel );                                                  // true
out.println( JLabel.class.isInstance( b ) );                                         // true
out.println( Object.class.isInstance( b ) );                                         // true
out.println( Class.forName("java.awt.Component").isInstance( b ) );                  // true
out.println( String.class.isInstance( b ) );                                         // false

Die Methode isInstance(object) ist natürlich ein wenig eingeschränkt dadurch, dass es immer ein Test-Objekt geben muss. Die Frage etwa, ob das Class-Objekt der Schnittstelle PublicKey eine ist-eine-Art von Serializable ist, kann isInstance(object) nicht beantworten, denn dann müsste es vorher ein Objekt geben. Für diesen Fall bietet das Class-Objekt noch eine zweite Funktion: isAs-signableFrom(Class).

Class<?> clazz = Serializable.class;
out.println( clazz.isAssignableFrom( String.class ) );                  // true
out.println( clazz.isAssignableFrom( Thread.class ) );                  // false
out.println( clazz.isAssignableFrom( PublicKey.class ) );               // true

Solange der Typname zur Übersetzungszeit bekannt ist, ist instanceof immer noch die beste Lösung. Doch wenn die Klasse nur durch ein Class-Objekt gegeben ist, bleibt immer noch isAssignableFrom(). Die Methode clazz.isInstance(obj) ist sozusagen eine Kurzform von clazz.isAssignableFrom(obj.getClass()).

Labels:

Insel: Service-Factory, IoC, Lookup, Generics, Services und alles zusammen

Je größer eine Java-Anwendung wird, desto größer werden die Abhängigkeiten zwischen Klassen und Typen. Um die Abhängigkeiten zu reduzieren, ist zunächst gewünscht, sich nicht so sehr an Implementieren zu binden, sondern an Schnittstellen. (Das gelobte „Programmieren geben Schnittstellen und nicht gegen eine Implementierung.“) Eine Schnittstelle beschreibt dann Dienste, so genannte Services, auf die an anderer Stelle zurückgegriffen werden kann. Die nächste Frage ist, wie ein Service mit Geschäftslogik zu der Stelle kommt, an denen er benötigt wird, etwa auf der grafischen Oberfläche als Aktion hinter einer Schaltfläche. Hier haben sich zwei Wege herausgestellt:

  • Service-Fabriken. Eine Service-Fabrik ist eine Zentrale, an die sich Interessenten wenden, wenn sie einen Service nutzen wollen. Die Fabrik liefert eine passende Implementierung, die immer eine Service-Schnittstelle implementiert. Welche Realisierung – also konkrete Klasse – die Fabrik liefert, soll den Nutzer nicht interessieren; eben Programmieren gegen Schnittstellen.
  • Dependency Injection/Inversion of Control (IoC). Nach diesem Prinzip fragen die Interessenten nicht aktiv über eine zentrale Service-Fabrik nach den Diensten, sondern den Interessenten wird der Service über eine übergeordnete Einheit gegeben (injiziert). Die magische Einheit nennt sich IoC-Container. In der Vergangenheit hat sich das Spring-Framework als De-facto-Standard eines IoC-Containers herauskristallisiert.
Arbeiten mit dem ServiceLoader

Java SE bietet bisher keine Bibliothek für Dependency Injection aber mit der Klasse java.util.ServiceLoader eine einfache Realisierung für Service-Fabriken. Ein eigenes Programm soll auf einen Grüß-Dienst zurückgreifen, aber welche Implementierung das sein wird, soll an anderer Stelle entschieden werden.

ServiceLoader<Greeter> greeterServices = ServiceLoader.load( Greeter.class );

for ( Greeter greeter : greeterServices )

  System.out.println( greeter.getClass() + " : " + greeter.greet( "Chris" ) );

ServiceLoader erfragt mit load() eine Realisierung, die die Schnittstelle Greeter implementieren soll. Die Realisierung ist der Service-Provider. Greeter deklariert eine greet()-Operation:

package com.tutego.insel.services;

public interface Greeter

{

  String greet( String name );

}

Der Service liefert aber eine konkrete Klasse. Demnach muss es irgendwo eine Zuordnung geben, die einen Typnamen (Greeter) mit einer konkreten Klasse, der Service-Implementierung, verbindet. Dazu ist im Wurzelverzeichnis des Klassenpfades ein Order META-INF mit einem Unterordner services anzulegen. In diesem Unterordner ist eine Textdatei (provider-configuration file) zu setzen, die den gleichen Namen wie die Service-Schnittstelle besitzt:

META-INF/

  services/

    com.tutego.insel.services.Greeter

Diese Textdatei, die keine Dateiendung aufweist, enthält Zeilen mit voll qualifizierten Klassenamen (binary name genannt) für die Implementierung, die später hinter diesem Service stehen. Es kann eine Zeile oder durchaus mehrere Zeilen für unterschiedliche Implementierungen angegeben sein. In der Datei META-INF/services/com.tutego.insel.services.Greeter steht:

com.tutego.insel.services.FrisianGreeter

FrisianGreeter ist demnach unsere letzte Klasse und eine tatsächliche Implementierung des Services:

package com.tutego.insel.services;

public class FrisianGreeter implements Greeter

{

public String greet( String name ) {

  return "Moin " + name + "!";

}

}

Utility-Klasse Lookup als ServiceLoader-Fassade

So nett der ServiceLoader auch ist, die API könnte ein wenig kürzer sein. Denn oftmals gibt es nur eine Service-Implementierung und nicht gleich mehrere. Daher soll eine Fassade eine knackigere API anbieten. Eine kurze Methode lookup() liefern genau den ersten Service (oder null) und lookupAll() gibt alle Service-Klassen in einer Sammlung zurück. (Das Listing nutzt mehrere Dinge, die die Insel bisher nicht vorgestellt hat! Dazu zählen Generics, Datenstrukturen, Iterator, Meta-Objekte.)

public class Lookup

{

public static <T> T lookup( Class<T> clazz )

{

  Iterator<T> iterator = ServiceLoader.load( clazz ).iterator();

  return iterator.hasNext() ? iterator.next() : null;

}

public static <T> Collection<? extends T> lookupAll( Class<T> clazz )

{

  Collection<T> result = new ArrayList<T>();

  for ( T e : ServiceLoader.load( clazz ) )

   result.add( e );

  return result;

}

}

Die Nutzung vereinfacht sich damit:

System.out.println( Lookup.lookup( Greeter.class ).greet( "Chris" ) ); // Moin Chris!

System.out.println( Lookup.lookupAll( Greeter.class ).size() ); // 1

Unverkennbar ist natürlich der Einfluss der NetBeans-Klasse http://bits.netbeans.org/dev/javadoc/org-openide-util/org/openide/util/Lookup.html.

Labels:

Java Closures : Inselupdate für Java 7

Mit den Closures nach Gilad Bracha, Neal Gafter, James Gosling, Peter von der Ahé.

Innere Klassen, insbesondere anonyme innere Klassen, sind bisher die einzige Möglichkeit, um Programmteile an Methoden zu übergeben. Nehmen wir einen Timer als Beispiel. Dem eigentlichen Zeitgeber muss ein Stücken Code übergeben werden, sodass der Timer weiß, was er zu tun hat. Mit dem Zeitgeber, der Klasse Timer, und der abstrakten Basisklasse TimerTask zur Beschreibung der Aufgaben, ist schnell ein Beispiel programmiert, das wie eine Uhr in jeder Sekunde die Zeit auf dem Bildschirm ausgibt:


public class TimerExample
{
public static void main( String[] args )
{
class MyTimer extends java.util.TimerTask {
@Override public void run() {
System.out.println( new java.util.Date() );
}
}

new java.util.Timer().scheduleAtFixedRate( new MyTimer(), 0, 1000 );
}
}

Für die Beschreibung des Programmcodes ist extra eine eigene Klasse erforderlich. Über eine innere anonyme Klasse lässt sich der Programmcode jedoch noch etwas weiter verkürzen:

 public class TimerExample
{
public static void main( String[] args )
{
new java.util.Timer().scheduleAtFixedRate( new java.util.TimerTask() {
@Override public void run() {
System.out.println( new java.util.Date() );
}
}, 0, 1000 );
}
}

Aus dem Programm ist deutlich abzulesen, dass zum "Transport" der println()-Anweisung einiges an Schreibarbeit nötig ist. Wünschenswert ist es aber, wenn der Programmcode leichter an die Funktion scheduleAtFixedRate() zu übergeben wäre. Da die bereitgestellte Funktion bisher nicht so programmiert ist, ist das Ziel, dieses nachzuprogrammieren.

Deklaration eines Closures

Ein Closure repräsentiert einen Block Java-Code. Die allgemeine Schreibweise ist

{ formal parameters => statements expression }

Die Schreibweise definiert eine anonyme Funktion. Vergleichen wir einer bekannten Funktionsdeklaration

void out()
{
System.out.printn("Hallo Welt");
}

mit der Deklaration eines Closures:

{ => System.out.printn("Hallo Welt"); }

Während normale Funktionen mit ihrem Namen aufgerufen werden, steht eine Closure für ein Objekt, welches eine invoke()-Methode anbietet.

public class Closures
{
public static void main( String[] args )
{
{ => System.out.println("Hallo Welt"); }.invoke();
}
}

Aus der allgemeinen Schreibweise

{ formal parameters => statements expression }

lässt sich absehen, dass formale Parameter wie bei normalen Funktionsdeklarationen möglich sind. So lassen sich in den Closure-Block Daten einführen. Vergleichen wir wieder eine Funktions- mit einer Closure-Deklaration:

void quote( String s )
{
System.out.println("'" + s + "'");
}

Der Closure mit Beispiel:

public class Closures
{
public static void main( String[] args )
{
{ String s => System.out.println("'" + s + "'"); }.invoke( "tutego" );
}
}

Zwei Dinge fallen an dem Beispiel auf:
• Formale Parameter haben einen Typ (links vom Pfeil steht String s ).
• Die invoke()-Funktion nimmt immer so viele Argumente an, wie es formale Parameter in der Closure-Deklaration gibt.

Die Beispiele bisher zeigen Closures, die in ihrem Rumpf eine Anweisung tragen. Closures können aber auch Ausdrücke enthalten.

public class Closures
{
public static void main( String[] args )
{
System.out.println( { int a, int b => (a + b) / 2 }.invoke( 10, 20) ); // 15
}
}

Im Rumpf endet die Ausdruck nicht mit einem Semikolon, denn es wäre ja auch verboten,

System.out.println( (a + b) / 2; );

zu schreiben.

Ein auffälliger Unterschied zur Funktion ist die fehlende return-Anweisung. Closures selbst sind quasi an die Aufrufstelle eingesetzte Programmteile und ein return würde die Funktion verlassen!

Funktions-Typ

Closures, wie { int a, int b => (a + b) / 2 }, besitzen einen so genannten Funktions-Typ, der durch die Typen der Parameter und der Rückgabe bestimmt ist. Die allgemeine Notation ist

{ formal parameters => return type }

Gibt eine Funktion nicht zurück, so steht rechts vom Pfeil void. Für unsere drei bisherigen Closures sind die Typen:

{ => void }
{ String => void }
{ int, int => int }

Da in einer Funktionsdeklaration ohne Parameter ja auch kein void steht – void out(void) ist falsch – steht auch im ersten Fall links vom Pfeil nichts.
Mit diesem Funktions-Typ lassen sich die Closures ausgezeichnet referenzieren:

{ => void } printHelloWorld = { => System.out.println("Hallo Welt"); }; 

{ String => void } printQuoted = { String s => System.out.println("'" + s + "'"); };

{ int, int => int } avg = { int a, int b => (a + b) / 2 };

printHelloWorld.invoke(); // Hallo Welt

printQuoted.invoke( "tutego" ); // 'tutego'

System.out.println( avg.invoke( 10, 20 ) ); // 15

Mit diesem Wissen lassen sich Funktionen schreiben, die ein Closure entgegennehmen. Eine Funktion repeat() soll dabei einen Programmcode so oft wie gewünscht aufrufen:

public class Repeater
{
public static void repeat( int times, { => void } block )
{
for ( int i = 0; i < times; i++ )
block.invoke();
}

public static void main( String[] args )
{
repeat( 2, { => System.out.println("Hallo"); } );
}
}

Das Hallo kommt also zweimal auf den Bildschirm.

In der Java-Bibliothek sind nicht alle Funktionen so parametrisiert, so dass Closures als Parameter erlaubt sind. Schreiben wir für den Timer eine eigene Methode scheduleAtFixedRate(), die einen Codeblock entgegennimmt und ausführt:

public class TimerExample
{
public static void scheduleAtFixedRate( { => void } task, long delay, long period )
{
new java.util.Timer().scheduleAtFixedRate( new java.util.TimerTask() {
@Override public void run() {
task.invoke();
}
}, delay, period );
}
public static void main( String[] args )
{
scheduleAtFixedRate(
{ => System.out.println( new java.util.Date() ); }
, 0, 1000 );
}
}

Mit Closures sind auch Funktionszeiger leicht zu realisieren:

public class FunctionPointerWithClosures
{
static void invoker( { => void } block )
{
block.invoke();
}

public static void main( String[] args )
{
{ => void } method1 = { => System.out.println("Hello www.tutego.com"); };
{ => void } method2 = { => System.out.println("Hallo www.tutego.com"); };

invoker( Math.random() > 0.5 ? method1 : method2 );
}
}

Natürlich lassen sich jetzt die Methoden auch in eine Datenstruktur setzen:

Map<String, { => void }> methods = new HashMap<String, { => void }>();
methods.put( "German", { => System.out.println("Hallo www.tutego.com"); } );
methods.put( "English", { => System.out.println("Hello www.tutego.com"); } );

methods.get( "German" ).invoke();

for ( { => void } method : methods.values() )
invoker( method );

In ein Feld können Closures nicht gesetzt werden! Der Grund liegt an der internen Generics-Umsetzung.

Nehmen wir an, wir wollen eine Funktion each() schreiben, die einen String mit einem gegebenen Delimiter zerlegt und dann eine Operation auf den einzelnen Token ausführt. Soll weiterhin die Operation einen String zurückgeben, so kann das in herkömmlichem Java etwa so implementiert werden:

class StringUtils
{
public static interface StringInStringOutBlock
{
String execute( String in );
}

public static String each( String source, String delimiter, StringInStringOutBlock block )
{
StringBuilder result = new StringBuilder();

for ( String token : source.split( delimiter ) )
result.append( block.execute( token ) );

return result.toString();
}
public static void main( String[] args )
{
class QuoterBlock implements StringInStringOutBlock {
@Override public String execute( String in ) {
return "'" + in + "'";
}
}

String s = each( "Hallo Welt!", " ", new QuoterBlock() );
System.out.println( s );
}
}

Closures machen das viel kompakter:

class StringUtils
{
public static String each( String source, String delimiter, { String => String } block )
{
StringBuilder result = new StringBuilder();

for ( String token : source.split( delimiter ) )
result.append( block.invoke( token ) );

return result.toString();
}

public static void main( String[] args )
{
String s = each( "Hallo Welt!", " ", { String in => "'" + in + "'" } );
System.out.println( s );
}
}

Closure mit Variablenzugriff

Closures haben einige Eigenschaften, die innere anonyme Klassen so erst einmal nicht haben. Eine davon ist, dass ein Closure-Block auf auch nicht-finale Werte lesend und schreiben zugreifen kann.

int i = 0;
repeat( 3, { => System.out.println(i); } ); // 0 0 0

@Shared int j = 0;
repeat( 3, { => System.out.println(j++); } ); // 0 1 2
System.out.println( j ); // 3

Auffällig ist die Annotation @Shared. Annotiert sie nicht die Variablen, auf die der Closure schreibend zugreift, gibt es eine Compiler-Warnung (kein Fehler!): "warning: [shared] captured variable j not annotated @Shared".

Closure Conversion

Damit automatisch Java-Entwicklung ohne Anpassung der Bibliotheken von den Closures profitieren, haben die Sprachentwickler einen speziellen Mechanismus eingebaut: Ein Closure kann einem Interface mit einer Operation zugewiesen werden, wenn die Rückgabe- und Parametertypen übereinstimmen. Die Schnittstelle Runnable und ActionListener sind wie folgt deklariert:

public interface Runnable {
void run();
}
public interface ActionListener extends EventListener {
void actionPerformed(ActionEvent e);
}

Kompatible Closures sind:

Runnable run = { => System.out.println("Nebenläufig!"); };
ActionListener listener = { ActionEvent l => System.out.println("Gedrückt!"); };


Ohne Zuweisung an Variablen ist ein nebenläufiges Programm schnell gestartet und ein Ereignisbehandler ohne viel Programmcode an einer Schaltfläche festgemacht:



new Thread( { => System.out.println("Nebenläufig!"); } ).start();
JButton b = new JButton();
b.addActionListener( { ActionEvent l => System.out.println("Gedrückt!"); } );

Diese automatische Konvertierung ist wirklich sehr praktisch, denn viele wichtige Java-Schnittstellen schreiben nur eine Operation vor.

Nach einem Vormittag mit Java-Closures kann ich sagen, dass ich gut mit der Syntax leben kann. Ich mag's! Schauen wir mal, ob sich noch groß etwas ändert.

Labels: ,

Inselupdate: Applets und HTML-Converter

Applets in der Wiege von Java

Applets sind kleine Java-Programme, die in einem Web-Browser ablaufen. Sie gehören zu den Java-Programmen der ersten Stunde. Obwohl Applets Java an die Spitze der Programmiersprachen brachte, sind die heute nur noch selten zu sehen. Es gibt zwar Ausnahmen, wie den Routenplaner http://map24.de/, doch im Allgemeinen sind in öffentlichen Webseiten Applets weitestgehend verschwunden. Der Grund, warum Java-Applets weniger attraktiv für den Konsumenten sind liegt nicht daran, dass die Client-seitige Darstellung und Logik unwichtig geworden ist, doch vielmehr an ande-ren Gründen:

  • Mit HTML, CSS sowie JavaScript lassen sich heutzutage viele Aufgaben lösen, die 1995 unlösbar waren. Dagegen wirken kompilierte Java-Programme wie Raketentechnik. Während bei Java-Applets erst eine JVM gestartet werden muss, was natürlich eine gewisse Zeit kostet, ist JavaSc-ript und HTML sofort bereit. Starke JavaScript-Bibliotheken ermöglichen tolle Effekte und schnelle Verarbeitung.
  • Ist Java installiert, steht auf den Rechnern eine moderne und schnelle Java-Laufzeitumgebung für Applets über ein Brower-Plugin zur Verfügung. Möchte der Anwender Applets nutzen, aber kein JVM ist installiert, ist der Bezug langwierig und viele Megabyte Daten müssen vom Sun-Server geladen werden. Sun arbeitet aktuell an einem System, mit dem nur relevante Bibliotheken bezogen werden, doch die Umsetzung ist noch Zukunftsmusik. (Microsoft lieferte für Windows XP immerhin noch eine eigene JVM aus, obwohl sie auf dem Stand von Java 1.1 stehen bleib. Wegen immer wieder aufkehrenden Sicherheitsproblemen sollten Anwender die Microsofts Java VM jedoch deinstallieren. Microsoft liefert für Vista kein Java mit aus und empfiehlt daher, Suns JVM zu installieren. )
  • Java ist als allgemeine Programmiersprache entworfen worden, aber nicht als einfache Programmiersprache für grafische Effekte. Hier liegt der Vorteil von Adobe Flash. Mit starken Tools kön-nen Designer großartige Oberflächen entwerfen und die Verbreitung des Flash-Players ist phäno-menal . Zudem erweitert Adobe die Multimedia-Techologie Flash, die Programmiersprache ActionScript sowie die Produktpalette zur Entwicklung kontinuierlich. Möglicherweise ändert sich dass, wenn Java Laufzeitumgebungen stark verbreitet sind und sich Suns neue Programmierspra-che JavaFX (http://java.sun.com/javafx/) verbreitet hat.

Hinweis   Wir wollen im Folgenden davon ausgehen, dass nicht die Java-Laufzeitumgebung 1.1 von Microsoft installiert ist, sondern ein vollwärtiges Java von Sun. Ist Suns Version von Java installiert, ersetzt es die JVM von Microsoft, und aktuelle Java-Programme lassen sich ausführen.

HTML Converter

Läuft der Browser auf ein <applet>-Tag, so startet er die Java Laufzeitumgebung, die das Java-Programm ausführt. Problematisch ist nur, wenn für den Browser kein Java (oder die falsche Version) installiert ist, und der Browser keine Idee hat, war er mit dem <applet>-Tag anfangen soll. Die Lö-sung ist, auf das <applet>-Tag zu verzichten, und eine Browser-spezifische Alternative zu verwen-den, um dem Benutzer zur Installation einer richten JVM zu verhelfen. Der HTML-Code dafür ist recht kryptisch, sodass Sun ein Tool vorsieht, welches das <applet>-Tag ersetzt. Das Hilfsprogramm heißt HTML Converter und befindet sich im bin-Verzeichnis vom JDK. Nach dem Start öffnet sich eine grafische Oberfläche, mit der die HTML-Datei oder ein Ordner mit Applet-referenzierenden HTML-Dateien angegeben wird.
<neupic: htmlconverter.png, „Der HTML Converter“>
Aus dem bedächtigen

<html><body>
<applet code="HelloWorldApplet.class" width="200" height="100"></applet>
</body></html>

erzeugt der Generator anschließend (wenn die Schablonendatei „Erweitert“ angegeben ist):

<html><body>
<!--"CONVERTED_APPLET"-->
<!-- HTML CONVERTER -->
<script language="JavaScript" type="text/javascript"><!--
    var _info = navigator.userAgent;
    var _ns = false;
    var _ns6 = false;
    var _ie = (_info.indexOf("MSIE") > 0 && _info.indexOf("Win") > 0 && _info.indexOf("Windows 3.1") < 0);
//--></script>
    <comment>
        <script language="JavaScript" type="text/javascript"><!--
        var _ns = (navigator.appName.indexOf("Netscape") >= 0 && ((_info.indexOf("Win") > 0 && _info.indexOf("Win16") < 0 && java.lang.System.getProperty("os.version").indexOf("3.5") < 0) || (_info.indexOf("Sun") > 0) || (_info.indexOf("Linux") > 0) || (_info.indexOf("AIX") > 0) || (_info.indexOf("OS/2") > 0) || (_info.indexOf("IRIX") > 0)));
        var _ns6 = ((_ns == true) && (_info.indexOf("Mozilla/5") >= 0));
//--></script>
    </comment>

<script language="JavaScript" type="text/javascript"><!--
    if (_ie == true) document.writeln('<object classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" WIDTH = "200" HEIGHT = "100"  codebase="http://java.sun.com/update/1.6.0/jinstall-6u30-windows-i586.cab#Version=6,0,0,5"><noembed><xmp>');
    else if (_ns == true && _ns6 == false) document.writeln('<embed ' +
        'type="application/x-java-applet;version=1.6" \
            CODE = "HelloWorldApplet.class" \
            WIDTH = "200" \
            HEIGHT = "100" ' +
        'scriptable=false ' +
        'pluginspage="http://java.sun.com/products/plugin/index.html#download"><noembed><xmp>');
//--></script>
<applet  CODE = "HelloWorldApplet.class" WIDTH = "200" HEIGHT = "100"></xmp>
    <PARAM NAME = CODE VALUE = "HelloWorldApplet.class" >
    <param name="type" value="application/x-java-applet;version=1.6">
    <param name="scriptable" value="false">

</applet>
</noembed>
</embed>
</object>

<!--
<APPLET CODE = "HelloWorldApplet.class" WIDTH = "200" HEIGHT = "100">

</APPLET>
-->
<!--"END_CONVERTED_APPLET"-->

</body></html>

Labels:

Use JavaMail API to reveive all Google mails

package com.tutego.insel.mail;

import java.util.*;
import javax.mail.*;
import javax.mail.internet.ContentType;
import javax.swing.JOptionPane;

public class GetEMails
{
public static void getMail( final Properties props ) throws Exception
{
Session session = Session.getInstance( props, new javax.mail.Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication( props.getProperty( "mail.pop3.user" ),
props.getProperty( "mail.pop3.password" ) );
}
} );
session.setDebug( true );

Store store = session.getStore( "pop3" );
store.connect();

Folder folder = store.getFolder( "INBOX" );
folder.open( Folder.READ_ONLY );

Message message[] = folder.getMessages();

for ( int i = 0; i < message.length; i++ )
{
Message m = message[i];

System.out.println( "-------------------------\nNachricht: " + i );
System.out.println( "Von: " + Arrays.toString(m.getFrom()) );
System.out.println( "Betreff: " + m.getSubject() );
System.out.println( "Gesendet am: " + m.getSentDate() );
System.out.println( "Content-Type: " + new ContentType(m.getContentType()) );

if ( m.isMimeType("text/plain") )
System.out.println( m.getContent() );
}
folder.close( false );
store.close();
}

public static void main( String[] args ) throws Exception
{
Properties props = new Properties();
props.setProperty( "mail.pop3.host", "pop.gmail.com" );
props.setProperty( "mail.pop3.user", JOptionPane.showInputDialog( "user" ) );
props.setProperty( "mail.pop3.password", JOptionPane.showInputDialog( "pass" ) );
props.setProperty( "mail.pop3.port", "995" );
props.setProperty( "mail.pop3.auth", "true" );
props.setProperty( "mail.pop3.socketFactory.class", "javax.net.ssl.SSLSocketFactory" );

getMail( props );
}
}

Labels: