Eclipse Google Plugin unterstützt nun Version 3.5

Thema der Woche: Repository-Pattern in DDD und DAO

Zur Abstraktion von Zugriffen auf konkreten Datastores (oft eine relationale Datenbank) haben sich DAOs (http://java.sun.com/blueprints/corej2eepatterns/Patterns/DataAccessObject.html) etabliert. Im Domain Driven Design (DDD) gibt es etwas, das so aussieht wie ein DAO, und zu großer Diskussion in der Community führt.

Lassen sich hier schon Unterschiede herausstellen?

Verfolge den “Streit” über den Unterschied zwischen Repos/DAO in den Blogs

Fragen:

  • Was sagt Eric Evans zu der Diskussion? Kann ein Repo als DAO implementiert sein?
  • Stehen Repo und DAO beide auf der Ebene vom Domain-Model?
  • Warum sagen Kritiker, dass Repos eine zusätzliche (unnötige) Zwischenschicht sind?
  • Einige EJB-Advocates injizieren gerne bei Java EE 5 Anwendungen den Entity-Manager in den DAO bzw. sehen den EM als DAO selbst, den man dann in Session-Beans injiziert. Ist das ein Design im Sinne von DDD?

Optional. Wie sieht Paginierung aus? http://tech.groups.yahoo.com/group/domaindrivendesign/message/5795

Labels:

Eclipse Groovy Tools – aber nur für Eclipse 3.4

SpringSource arbeitet an einem (alternativen) Plugin für Groovy. Der Compiler nutzt inkrementelle Compilation und die IDE visualisiert ordentlich Fehler und gibt Warnungen, etwa bei der Raw-Typ Nutzung von Generics.

Generics warnings for groovy code

Weitere Infos unter http://blog.springsource.com/2009/07/30/a-groovier-eclipse-experience/

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:

Java Datastore API für die Google App Engine

Um in der Google Cloud Daten zu speichern bietet Google drei API an: JPA, JDO und eine Low-Level-API. Infos dazu gibt liefert http://code.google.com/intl/de/appengine/docs/java/datastore/. JPA und JDO basieren im Kern auf der Low-Level API, die auf die http://en.wikipedia.org/wiki/BigTable zurückgreift.

Für JPA und JDO gibt es selbst von Google viele Beispiele, aber die Low-Level-API ist nicht so gut dokumentiert und selbst das Beispielprogramm in der JavaDoc enthält Fehler. Zeit daher, ein sehr einfaches Beispiel mit einer 1:n Relationen anzugehen.

Im Mittelpunkt der API steht der DatastoreService, der ein bisschen an den EntityManager von JPA erinnert. Er bietet Methoden für die CRUD-Operationen. Mein Beispiel geht schon ein bisschen “pseudo-ORM” an die Aufgabe ran, einer Person Nachrichten zuordnen zu können:

Die Personen-Klasse:

package com.tutego.server.entity;

import java.util.ArrayList;
import java.util.List;

import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;

public class Person
{
  private final static String ENTITY_NAME = "Person";
  Entity personEntity;

  public enum Gender
  {
    MALE, FEMALE
  }

  public Person()
  {
    personEntity = new Entity( ENTITY_NAME );
  }

  private Person( Key key )
  {
    try
    {
      personEntity = DatastoreServiceFactory.getDatastoreService().get( key );
    }
    catch ( EntityNotFoundException e )
    {
    }
  }

  private Person( Entity entity )
  {
    personEntity = entity;
  }

  public static Person get( Key key )
  {
    return new Person( key );
  }

  public void setUsername( String username )
  {
    personEntity.setProperty( "username", username );
  }

  public String getUsername()
  {
    return personEntity.getProperty( "username" ).toString();
  }

  public void setGender( Gender gender )
  {
    personEntity.setProperty( "gender", gender.toString() );
  }

  public Gender getGender()
  {
    return Gender.valueOf( personEntity.getProperty( "gender" ).toString() );
  }

  public Key put()
  {
    return DatastoreServiceFactory.getDatastoreService().put( personEntity );
  }

  public static void deleteAll()
  {
    Query deleteAllQuery = new Query( ENTITY_NAME );

    for ( Entity entity : DatastoreServiceFactory.getDatastoreService().prepare( deleteAllQuery ).asIterable() )
      DatastoreServiceFactory.getDatastoreService().delete( entity.getKey() );
  }

  private static List<Person> executeQuery( Query query )
  {
    List<Person> result = new ArrayList<Person>();
    for ( Entity entity : DatastoreServiceFactory.getDatastoreService().prepare( query ).asIterable() )
      result.add( new Person( entity ) );
    return result; 
  }

  public static List<Person> findAllPersons()
  {
    Query query = new Query( ENTITY_NAME );

    return executeQuery( query );
  }

  public static List<Person> findPersonByGender( Gender gender )
  {
    Query query = new Query( ENTITY_NAME );
    query.addFilter( "gender", Query.FilterOperator.EQUAL, gender.toString() );

    return executeQuery( query );
  }

  @Override
  public String toString()
  {
    return String.format( "Person[%s,%s]", getUsername(), getGender() );
  }
}

Die Nachrichten-Klasse:

package com.tutego.server.entity;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;

public class Message
{
  private final static String ENTITY_NAME = "Message";

  private Entity messageEntity;

  public Message()
  {
    messageEntity = new Entity( ENTITY_NAME );
  }

  private Message( Key key )
  {
    try
    {
      messageEntity = DatastoreServiceFactory.getDatastoreService().get( key );
    }
    catch ( EntityNotFoundException e )
    {
    }
  }

  private Message( Entity entity )
  {
    messageEntity = entity;
  }

  public static Message get( Key key )
  {
    return new Message( key );
  }

  public void setText( String text )
  {
    messageEntity.setProperty( "text", text );
  }

  public String getText()
  {
    return messageEntity.getProperty( "text" ).toString();
  }

  public void setCreationTime( Date d )
  {
    messageEntity.setProperty( "creationtime", "" + d.getTime() );
  }

  public Date getCreationTime()
  {
    return new Date( Long.parseLong( messageEntity.getProperty( "creationtime" ).toString() ) );
  }

  public void setReceiver( Person p )
  {
    messageEntity.setProperty( "person_fk", p.personEntity.getKey() );
  }

  public Key put()
  {
    return DatastoreServiceFactory.getDatastoreService().put( messageEntity );
  }

  private static List<Message> executeQuery( Query query )
  {
    List<Message> result = new ArrayList<Message>();

    for ( Entity entity : DatastoreServiceFactory.getDatastoreService().prepare( query ).asIterable() )
      result.add( new Message( entity ) );
    return result; 
  }

  public static List<Message> findMessagesForPerson( Person p )
  {
    Query query = new Query( ENTITY_NAME );
    query.addFilter( "person_fk", Query.FilterOperator.EQUAL, p.personEntity.getKey() );

    return executeQuery( query );
  }

  @Override
  public String toString()
  {
    return String.format( "Message[%s,%s]", getCreationTime(), getText() );
  }
}

Getestet werden soll das ganze in einer einfachen Server-Funktion:

StringWriter sw = new StringWriter();
PrintWriter out = new PrintWriter( sw );

// Insert new entity

Person p1 = new Person();
p1.setUsername( "chris" );
p1.setGender( Person.Gender.MALE );
Key key1 = p1.put();

out.println( "* Key für erste Person " + p1 );
out.println( KeyFactory.keyToString( key1 ) );

Person p2 = new Person();
p2.setUsername( "pallas" );
p2.setGender( Person.Gender.FEMALE );
p2.put();

Person p3 = new Person();
p3.setUsername( "tina" );
p3.setGender( Person.Gender.FEMALE );
p3.put();

// Search for entity with a given key

Person p = Person.get( key1 );
out.println( "* Suche mit Schlüssel " + key1 );
out.println( p.getUsername() );

// Query

List<Person> findAll = Person.findAllPersons();
out.println( "* Alle Personen" );
out.println( findAll.toString() );

// Query

List<Person> females = Person.findPersonByGender( Gender.FEMALE );
out.println( "* Alle Frauen:" );
out.println( females.toString() );

Message msg1 = new Message();
msg1.setText( "Hallo Maus" );
msg1.setCreationTime( new Date(1) );
msg1.setReceiver( p );
msg1.put();

Message msg2 = new Message();
msg2.setText( "Hallo Ratte" );
msg2.setCreationTime( new Date(2) );
msg2.setReceiver( p );
msg2.put();

out.println( "* Alle Nachrichten für " + p );
out.println( Message.findMessagesForPerson( p ) );
out.println( "\n" );

// Clean up

Person.deleteAll();

out.flush();

return sw.toString().replace( "\n", "<br/>" );

Als Ergebnis kommt HTML zurück, was der Client zum Testen anschauen kann.

Labels:

Thema der Woche: Stripes, ein einfaches Web Presentation Framework

Die Listen http://java-source.net/open-source/web-frameworks und http://de.wikipedia.org/wiki/Liste_von_Webframeworks geben eine fast unendliche Aufzählung von Web-Frameworks an. Selbst bin ich ein Freund (je nach Anwendungsfall) von GWT, JSF 2.0 und Stripes.

Stripes is a presentation framework for building web applications using the latest Java technologies. The main driver behind Stripes is that web application development in Java is just too much work! It seems like every existing framework requires gobs of configuration. Struts is pretty feature-light and has some serious architectural issues (see Stripes vs. Struts for details). Others, like WebWork 2 and Spring-MVC are much better, but still require a lot of configuration, and seem to require you to learn a whole new language just to get started.

Stripes fällt die Kategorie der Action-orientierten Frameworks, wie Struts oder Spring MVC. Ein Front-Controller nimmt den Request entgegen und delegiert auf eine ActionBean-Klasse, die den Seitenfluss auf eine andere Zielseite steuert.

Aufgaben:

Trivia: Frederic Daoud ist der Autor vom Standard-Stripes Buch “Stripes: ...and Java Web Development Is Fun Again”. Frederic und seine Frau Nadia haben ein zweites Kind bekommen und es Ruby genannt.

Labels:

OpenJDK7 / JDK7 M4 Release

Die Ankündigung wurde unter http://blogs.sun.com/xiomara/entry/openjdk7_jdk7_release_milestone_4 gemacht. Interessant ist "For now JDK 7 is finally in sync with the JDK 6u14 updates." Dann wird es Zeit, dass jetzt mal die *wirklichen* Dinge implementiert werden und man nicht nur die Änderungen von Java 6 alle in Java 7 nachzieht. Bis auf NIO.2 ist hier noch nicht wirklich viel passiert. Und beim Project Coin wird viel diskutiert, aber bisher ohne Ergebnis im Java-Compiler.

Labels:

GWT und sein HTML/DOM, alternativer GWT FlowPanel

Zum Layout von GWT-Komponenten ist es unerlässlich zu verstehen, was GWT für ein DOM erzeugt. Zum einen sind da Werkzeuge wie FireBug unerlässlich und zum Anderen kann man sich vorher schon bei http://javabyexample.wisdomplug.com/component/content/article/75.html informieren, was GWT für eine Struktur erzeugen wird.

Schnell entstehen bei GWT-Anwendungen eine Unzahl geschachtelter Tabellen. Sie sind für die Performance der Darstellung nicht unerheblich, denn wenn man das Fenster zum Beispiel in der Größe ändert, so müssen die ganzen Größeninformationen neu berechnet werden.

Um das HTML schlank zu halten, lässt sich auf alternative Container zurückgreifen. Wer zum Beispiel horizontal oder vertikal anordnen will, greift sofort zum HorizontalPanel bzw. VerticalPanel. Doch zur Umsetzung setzt eben GWT eine Tabelle ein. Wenn man nur zum Beispiel eine Zeile wie “blal@googlemail.com  | My favorites | Profile  | Sign out” aufbauen möchte, ist das HorizontalPanel unnötig und schwergewichtig. Es bietet sich an, eine neue Panel-Klasse zu nutzen, etwa wie sie etwa http://blog.sudhirj.com/2009/05/vertical-and-horizontal-flow-panels-in.html vorstellt. Etwas komprimiert:

import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Widget;

public class HorizontalFlowPanel extends FlowPanel
{
  @Override
  public void add( Widget w )
  {
    w.getElement().getStyle().setProperty( "display", "inline" );
    super.add( w );
  }
}

Diese Implementierung führt nur zu einem <div>-Block statt einer <table>.

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:

Update der Eclipse-Plugin-Seite

Unter http://www.tutego.de/java/eclipse/plugin/eclipse-plugins.html habe ich die Liste der interessanten/wichtigen/coolen/notwendigen/… Eclipse-Plugin aktualisiert. Habe ich was vergessen?

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:

GWT 1.7 mit kleinen Updates ist raus

Sie dazu auch die Ankündigung http://groups.google.com/group/Google-Web-Toolkit-Contributors/browse_thread/thread/a0d35938d940d32d?pli=1.

GWT 1.7 is a minor update that adds better support for Internet Explorer 8, Firefox 3.5, and Safari 4.

Labels:

Ausführlicher Artikel über invokedynamic

Den gibt es bei http://java.sun.com/developer/technicalArticles/DynTypeLang/index.html. Ed Od beschreibt ausführlich die einzelnen invokeXXX-Bytecode Operationen, und wie invokedynamic da hineinpasst.


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:

Trident 1.0

Trident ist eine neue und sehr flexible Animationslibrary, die die Version 1.0 erblickt. Grouchnikov’sBlog-Eintrag erklärt dazu mehr.

Labels:

EventBus Version 1.3

Seit einem Monat gibt es EventBus 1.3. Die Webseite spricht im Wesentlichen von zwei Neuigkeiten:

  • The ability to control the order in which subscribers are called with the Priority interface and priority annotation parameters.
  • Support for annotated inner classes and other non-public members.
  • Mein Blog-Eintrag und Folien stellen das nette Projekt vor:

    Labels:

    Vergleich von HTML-Dateien mit Daisy Diff

    In meiner Seminarverwaltungssoftware können Trainer die Seminarbeschreibungen ändern. Natürlich möchte ich mitbekommen, welche Stellen geändert wurden und das am Liebsten hübsches aufbereitet. Da bin ich auf http://code.google.com/p/daisydiff/ gestoßen. Man kann entweder über die Kommandozeile arbeiten oder mit einer Hilfsklasse, wobei zwei Klassen einen ersten Anhaltspunkt für die API geben:

    Ein kleines Beispiel soll das Diff in HTML-Form in den Temp-Ordner C:\Users\CHRIST~1\AppData\Local\Temp\ schreiben:

    package com.tutego.traida;

    import java.awt.Desktop;
    import java.io.File;
    import java.io.FileWriter;

    import org.outerj.daisy.diff.Main;

    public class DaisyDiffDemo
    {
      public static void main( String[] args ) throws Exception
      {
        String html1 = "Eine zwei Polizei";
        String html2 = "Eins zwei drei Polizei";

        File fileIn = File.createTempFile( "daisyin", ".html" );
        File fileOut = File.createTempFile( "daisyout", ".html" );
        File fileDiff = File.createTempFile( "daisydiff", ".html" );
        new FileWriter( fileIn ).append( html1 ).close();
        new FileWriter( fileOut ).append( html2 ).close();

        String[] daisyDiffArgs = {
          fileIn.getAbsolutePath(), fileOut.getAbsolutePath(), "--file="+fileDiff.getAbsolutePath()
        };
        Main.main( daisyDiffArgs );

        fileIn.deleteOnExit();
        fileOut.deleteOnExit();
        Desktop.getDesktop().open( fileDiff );
      }
    }

    Das generierte HTML greift auf allerlei Zeugs zurück. Damit die HTML-Datei gut angezeigt wird, einfach aus dem Zip die Order css, images und js in das temp-Verzeichnis C:\Users\CHRIST~1\AppData\Local\Temp kopieren. Dann sieht das Ergebnis so aus:

    sshot-1

    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:

    Mit GPL-Tools von COBOL nach Java migrieren

    Auf Jazoon09 (Präsentation: http://docs.google.com/Present?docid=dcc9m6z9_1524fzspccfp) wurde ein Projekt vorgestellt, wie 4 Millionen Zeilen COBOL erfolgreich nach Java konvertiert werden konnten. (infoQ http://www.infoq.com/news/2009/07/cobol-to-java hat die Links). Im Zentrum der Konvertierung steht der Konverter (83.000 Quellcodezeilen, fast 700 Klassen), das zusammen mit der Laufzeitbibliotheken (153k Zeilen, fast 900 Klassen) auf COBOL-Programme übersetzt und ausführt. Die COBOL-Masken werden in HTML übersetzt.

    Die GPL-Projekte sind unter http://code.google.com/p/naca/ zu finden (aber nicht im Source als SVN).

    Labels:

    Schönes Beispiel für Generics

    http://research.microsoft.com/en-us/um/people/akenn/generics/cupt.jpg

    Thema der Woche: Effective Java

    Das Buch Effective Java ist eines der beliebtesten Bücher für den fortgeschrittenen Java-Programmierer (http://java.sun.com/docs/books/effective/). Vor gar nicht allzulanger Zeit ist das Buch in die 2. Aufgabe gegangen (erste Auflage online).

    Gehe die verfügbaren Informationen durch:
    Beantworte folgende Fragen:
    • Warum ist es schwierig, eine perfekte equals()-Methode zu schreiben?
    • Welche Vorteile bieten immutale Objekte? Was hast das mit defensiven Kopien zu tun?
    • Wo hören die Wertebereiche von float und double auf?
    • Konstruiere ein Szenario, bei dem das wait() in einer if-Anweisung fehlschlägt und nur eine Schleife gültig ist.
    • Finde Informationen, ob über enum ein Singleton wirklich 100% korrekt ein Singleton ist und nur exakt ein einziges mal in der JVM ist.
    Die Aufgabe beschäftigt für 2 Wochen!

    Labels:

    Welches Projekt wird von Maven wie oft referenziert?

    Das sagt http://www.mvnbrowser.com/most-referenced.html. Die Liste ist interessant zu lesen. Vielleicht nicht ganz unerwartet ist ganz oben JUnit und log4j -- das wird von nahezu allen Programmen referenziert. Interessant in der Liste finde ich,
    • das HSQLDB doch noch so oft (für Tests) gebraucht wird, aber Derby noch auf die erste Seite kommt,
    • Spring (schon) so weit oben seht,
    • CXF vor Axis und dem alten XFire steht,
    • JAXB so groß im Einsatz ist,
    • Plexus von CodeHaus in der Liste auf der ersten Seite steht (unter anderem von Maven2 genutztes IoC-Framwork und daher wohl so weit vorne),
    • dom4j populärer als JDOM ist,
    • Struts ist (ein wenig) wichtiger als WebWork aber weit abgeschlagen von JSF.
    Eher unbekannte Projekte sind:
    • http://classworlds.codehaus.org/
    • http://qdox.codehaus.org/
    • http://www.janino.net/


    Labels:

    JavaDoc in Wiki-Notation statt HTML

    Das ist das Ziel vom http://code.google.com/p/markdown-doclet/. Mit der Syntax von http://daringfireball.net/projects/markdown/syntax und zusammen mit leicht optimiertem CSS und kleinen UML-Diagrammen für die Hierarchien, folgt:

    Labels:

    IDE-Woche: Eclipse 3.5, NetBeans 6.7, IntelliJ 9 M1

    So viel IDE gab es noch nie. In einer Woche drei wichtige Neuerungen.

    Labels: