Echte typsichere Container

Verwenden Entwickler die Sammlungen untypisiert, also mit dem Raw-Typ, so lässt sich nicht verhindern, dass ein ungewünschter Typ die Sammlung betritt und beim Entnehmen zu einer Ausnahme führen kann. Der Grund liegt in der internen Umsetzung, dass nur der Compiler selbst die Typsicherheit sicherstellt, aber nicht die Laufzeitumgebung. Um die Typsicherheit zu erhöhen, bietet die Collections-Klasse ein paar Wrapper-Methoden, die eine Sammlung nehmen und Operationen nur eines gewissen Typs durchlassen – verstoßt ein Aufrufer dagegen, gibt es eine ClassCastException.

class java.util.Collections

§ static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)

§ static <E> List<E> checkedList(List<E> list, Class<E> type)

§ static <K,V> Map<K,V> checkedMap(Map<K,V> m, Class<K> keyType, Class<V> valueType)

§ static <E> Queue<E> checkedQueue(Queue<E> queue, Class<E> type)

§ static <E> Set<E> checkedSet(Set<E> s, Class<E> type)

§ static <K,V> SortedMap<K,V> checkedSortedMap(SortedMap<K,V> m, Class<K> keyType, Class<V> valueType)

§ static <E> SortedSet<E> checkedSortedSet(SortedSet<E> s, Class<E> type)

Beispiel: Lasse in eine Menge nur Strings, aber nichts anderes.

Set<String> set = Collections.checkedSet( new HashSet<String>(),

String.class );

set.add( "xyz" ); // Compiler OK

Set rawset = set;

rawset.add( "abc" ); // Compiler OK

rawset.add( new Object() ); // Compiler OK,  N Laufzeitfehler

Die Ausnahme ist: “Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.Object element into collection with element type class java.lang.String”

Auch wenn kein Programmierer freiwillig falsche Typen in eine Sammlung platziert, ist es doch besser, eine absolute Sicherheit zu bekommen, die nicht auf dem Compiler beruht. Wenn etwa ein Programm einer Skriptsprache die Möglichkeit eröffnet Elemente in eine Datenstruktur zu setzen, so lässt sich für die Skriptsprache eine geprüfte Sammlung zur Ablage nach außen geben. Achtet das Skript nicht auf den richtigen Typ, so knallt es im Skript. Das ist gewünscht, denn andernfalls würde viel später erst der falsche Typ bei der Bearbeitung auffallen und dann knallt es an der ganz falschen Stelle.

JDK-Interna: Removes the use of shared character array buffers by String along with the two fields needed to support the use of shared buffers.

Eine recht fette Änderung, die großen Einfluss auf die Performace von substring-Operationen hat. Denn die ist nun nicht mehr so schnell wir früher O(1), sondern O(length()). Dafür spart man Speicher bei jedem String-Objekt, und davon gibt es zur Laufzeit einer normalen Anwendung wirklich viele. Reduktion: zwei ints, also 2 x 4 Byte pro String-Objekt.

Implementierung: http://hg.openjdk.java.net/jdk8/tl/jdk/rev/2c773daa825d

File-Klassen auf NIO.2 umstellen

Um sich von der Klasse java.io.File zu lösen und in Richtung Path zu migrieren, ist Handarbeit angesagt. Zunächst gilt es, alle Stellen zu finden, die im Workspace oder Projekt die Klasse File referenzieren. Am Einfachsten ist es, eine Anweisung wie File f; in den Code zu setzen, dann zum Beispiel in Eclipse Strg+Shift+G zu aktivieren. Es folgt eine Liste aller Vorkommen. Diese Liste muss nun abgearbeitet werden und der Code auf NIO.2 konvertiert werden. Nicht immer ist es so einfach

Scanner s = new Scanner( new File(dateiname) );

in

Scanner s = new Scanner( Paths.get(dateiname) );

umzusetzen oder

new File( dateiname ).delete();

in

Files.delete( Paths.get( dateiname ) );

Eine andere Stelle, an der Konvertierungen möglich sind, betreffen FileReader und FileWriter. Diese Klassen sind gefährlich, weil sie standardmäßig die im System voreingestellte Kodierung verwenden. Eine Kodierung wie UTF-8 im Konstruktor explizit vorzugeben ist jedoch nicht möglich. NIO.2 bietet eine bessere Methode, um an einen Reader/Writer aus einer Datei zu kommen und Entwickler werden gezwungen, die Kodierung immer sichtbar anzugeben.

Deklaration

Klassisch

Mit NIO.2

Reader r =

new FileReader(f);

Files.newBufferedReader(Paths.get(f),

StandardCharsets.ISO_8859_1);

Writer w =

new FileWriter(f);

Files.newBufferedWriter(Paths.get(f),

StandardCharsets.ISO_8859_1);

Beziehen eines Dateistroms klassisch und mit NIO.2

Der Quellcode wird erst einmal länger, doch der Gewinn ist, dass die Kodierung nicht verschwindet und auch die Ein-/Ausgabe gleich gepuffert ist. Freunde der statischen Import-Anweisung komprimieren weiterhin zu Anweisungen wie

Reader r = newBufferedReader( get( f ), ISO_8859_1 );

Es ist auf den Fall Handarbeit angesagt. Eigene Blöcke, etwa zum Schreiben in Dateien können komplett zusammengestrichen werden auf einfache Methodenaufrufe wie

Files.write( Paths.get( f ), bytes );

List<String> lines = Files.readAllLines( Paths.get( f ), StandardCharsets.ISO_8859_1 );

Hilfe bei der Migration auf Java 7 mit NetBeans und Eclipse

Wer eine große Codebasis auf Java 7 migrieren möchte, steht vor dem Programm, wie das ohne großen manuellen Aufwand funktionieren kann. Als erstes steht die Umsetzung auf die neuen Syntax-Möglichkeiten an. Das ist ein echter Mehrwert, bei den Bibliotheken gibt es in dem Sinne keine Umsetzung von alt nach neu, die File-Klasse ist nicht als veraltet markiert, wobei es durchaus interessant ist, auch File-Operationen durch die NIO.2-Operationen zu ersetzen. Schauen wir uns an, welche Möglichkeiten die modernen IDEs bieten.

Migration mit NetBeans

Die aktuelle NetBeans IDE bringt einen Code-Transformer mit, der elegant die Code-Basis durchsucht und Transformationen automatisch durchführt. Das Menü Refactor bietet den Unterpunkt Inspect and Transform…, der zu einem Dialog führt.

Hier lässt sich bei Configuration auswählen, ob imports organisiert, oder in Java 7 transformiert werden soll. Unter Manage… öffnet sich ein weiter Dialog, der im Baum JDK 1.5 and later die erkannten Muster anzeigt. Für Java 7 ist dies Can use Diamond, Convert to try-with-resources, Join catch sections using mulitcatch, Use specific catch und Use switch over Strings when possible.

Wer neu Programmcode mit NetBeans schreibt wird gleich auf die neue Syntax hingewiesen. Vervollständigungen nutzen ganz natürlich zum Beispiel den Diamond-Operator.

Migration mit Eclipse

Eclipse integriert kein Werkzeug, um die Code-Basis automatisch in die Syntax von Java 7 zu konvertieren. Seit der Version Eclipse 3.7 sind jedoch Transformationen über die Quick-Fix/Quick-Assist möglich, was es erlaubt, Generics zu mit dem Diamanten zu verkürzen, Ausnahme-Anweisungen mit einem Multi-Catch zu umrahmen, gleiche Catch-Blöcke mit einem Multi-Catch zusammenzufassen und AutoCloseable-Resourcen mit einem try-mit-Resourcen zu schließen. Allerdings setzt Eclipse bis auf eine mögliche Diamanten-Umsetzung keine Warnung ein. Und diese Warnung ist standardmäßig abgeschaltet und muss aktiviert werden. Dazu wählte Window > Preferences im Baum Java > Compiler > Errors/Warning rechts in der Rubrik Generic Type bei Redundant type arguments statt Ignore dann Warning.

Jobangebot Java-Entwickler

Für den Einsatz bei einem Kunden wird benötigt ein versierter Java-Entwickler (m/w) (bereits mit ausreichender Projekterfahrung, Erfahrungen in Anwendungsdesign und mit Architektur-Verständnis) mit folgenden techn. Kenntnissen:

  • Java (logisch)
  • Hibernate
  • Subversion (Erfahrungen wären gut, nicht zwingend)
  • Ant
  • Swing
  • Framework: Wings (Erfahrungen wären gut, nicht zwingend)
  • Framework: Vaadin (Erfahrungen wären gut, nicht zwingend)
  • Datenbank MySQL
  • Einsatz: süddeutscher Raum
  • ab sofort, ½ Jahr bis länger

Bewerbungen bitte direkt an elke.roetzer<ÄT>md-consulting<PUNKT>de schicken.

Inselraus: Swing/Auswahlmenü mit Separator

Standardmäßig unterstützt die JList und auch JComboBox keine Separatoren, doch die Unterteilung in Segmente ist in der Praxis sehr nützlich. Microsoft Word und PowerPoint benutzen sie zum Beispiel, um die zuletzt vom Benutzer ausgewählten Zeichensätze prominent oben in der Liste zu haben (Excel dagegen tut dies nicht).

Abbildung

Abbildung 9.42: Separator in Words Zeichensatzauswahlliste

Wir wollen diese Möglichkeit nachbilden und dabei noch einiges über Modelle und Renderer lernen.

Abbildung

Abbildung 9.43: Eine eigene Variante mit JSeparator in JComboBox

Bei der Umsetzung gibt es unterschiedliche Varianten, die sich außer in der technischen Implementierung darin unterscheiden, ob das Modell eine Markierung für den Separator enthält oder nicht. Wir stellen beide Ansätze vor und beginnen mit der ersten Variante, also damit, einem Zellen-Renderer Positionen mitzugeben, die sagen, wo ein Trennstrich zu zeichnen ist.

package com.tutego.insel.ui.list;

import javax.swing.*;

public class JComboBoxWithSeparator1 {
  public static void main( String[] args ) throws Exception {
    UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    String[] items = { "Cambria", "Arial", "Verdana", "Times" };
    JComboBox<String> comboBox = new JComboBox<>( items );
    ListCellRenderer<String> renderer = new SeparatorAwareListCellRenderer1<>(
                                          comboBox.getRenderer(), 0 );
    comboBox.setRenderer( renderer );
    frame.add( comboBox );
    frame.pack();
    frame.setVisible( true );
  }
}

Die eigene Klasse SeparatorAwareListCellRenderer1 ist ein ListCellRenderer, den die JComboBox zur Darstellung der Komponenten nutzt. Im Konstruktor des Renderers geben wir den Original-Renderer mit – es kann ein bestimmter Renderer schon vorinstalliert sein, den wollen wir dekorieren – sowie eine variable Argumentliste von Positionen. Das Beispiel übergibt nur 0, da nach dem ersten Element (Index = 0) ein Trennzeichen zu setzen sein soll, sodass Cambria und Arial abgetrennt sind.

package com.tutego.insel.ui.list;

import java.awt.*;
import java.util.Arrays;

import javax.swing.*;

public class SeparatorAwareListCellRenderer1<E> implements ListCellRenderer<E>
{
  private final ListCellRenderer<? super E> delegate;
  private final int[] indexes;
  private final JPanel panel = new JPanel( new BorderLayout() );

  public SeparatorAwareListCellRenderer1( ListCellRenderer<? super E> delegate,
                                          int... indexes )
  {
    Arrays.sort( indexes );
    this.delegate = delegate;
    this.indexes  = indexes;
  }

  @Override
  public Component getListCellRendererComponent( JList<? extends E> list, E value,
                                                 int index, boolean isSelected,
                                                 boolean cellHasFocus )
  {
    panel.removeAll();
    panel.add( delegate.getListCellRendererComponent( list, value, index,
                                                      isSelected, cellHasFocus ) );
    if ( Arrays.binarySearch( indexes, index ) >= 0 )
      panel.add( new JSeparator(), BorderLayout.PAGE_END );

    return panel;
  }
}

Die Implementierung basiert auf der Idee, jede Komponente in einen Container (JPanel) zu setzen und in diesem Container dann je nach Bedarf ein JSeparatorunten an diesem Container anzufügen. Statt JPanel mit einem JSeparator auszustatten, kann ebenfalls auch ein Border unten gezeichnet werden. Die AnweisungArrays.binarySearch(indexes, index) >= 0 ist als »contains« zu verstehen, also als ein Test, ob der Index im Feld ist – leider gibt es so eine Methode nicht in der Java API. Wenn der Index im Feld ist, soll der Separator unter der Komponente erscheinen. Dass sich eine Trennlinie auch am Anfang befinden kann, berücksichtigt die Lösung nicht; diese Variante bleibt als Übungsaufgabe für die Leser.

Diese Lösung ist einfach und funktioniert gut, denn vorhandene Renderer werden weiterverwendet, was sehr wichtig ist, denn größere Swing-Anwendungen nutzen viele eigene Renderer, etwa um Icons und Text zusammenzufassen. Ein Nachteil ist, dass der Separator zu einen Element gehört, und wenn das Element etwa in der Liste ausgewählt wird, steht der Separator mit in der Selektion und ist nicht abgetrennt (das ist aber bei Word genauso).

Während die vorgestellte Variante in der Praxis gut funktioniert, wollen wir uns noch mit einer alternativen Umsetzung beschäftigen. Sie ist deutlich komplexer und auch nicht so flexibel. Die Lösung basiert auf der Idee, dass die Modelldaten eine Markierung für den Separator enthalten – die folgende Lösung nutzt null dafür. DaJList oder JComboBox eine null problemlos verträgt, ist die Basis schnell umgesetzt:

JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
String[] items = { "Cambria", null, "Arial", "Cambria", "Verdana", "Times" };
JComboBox<String> comboBox = new JComboBox<String>( items );
frame.add( comboBox );
frame.pack();
frame.setVisible( true );

Wenn wir das Programm starten, zeigt es dort, wo das Model eine null liefert, einfach nichts an. Die Selektion dieses null-Elements ist auch möglich und führt zu keiner Ausnahme.

Damit Swing das null-Element nicht als Leereintrag anzeigt, verpassen wir unserer JComboBox einen Zellen-Renderer. Der soll immer dann, wenn das Element nullist, ein Exemplar von JSeparator zeichnen. Vom Design ist es das beste, wenn der Zell-Renderer selbst die null-Elemente darstellt, aber das Zeichnen der Nicht-null-Elemente an einen anderen Zell-Renderer abgibt, anstatt diese etwa durch einen BasicComboBoxRenderer (ein JLabel, das ListCellRendererimplementiert) selbst zu renderen – das würde die Flexibilität der Lösung massiv einschränken.

package com.tutego.insel.ui.list;

import java.awt.Component;
import javax.swing.*;

public class SeparatorAwareListCellRenderer2<E> implements ListCellRenderer<E>
{
  private final ListCellRenderer<? super E> delegate;

  public SeparatorAwareListCellRenderer2( ListCellRenderer<? super E> delegate )
  {
    this.delegate = delegate;
  }

  @Override
  public Component getListCellRendererComponent( JList<? extends E> list, E value,
                                                 int index, boolean isSelected,
                                                 boolean cellHasFocus )
  {
    if ( value == null )
      return new JSeparator();

    return delegate.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus );
  }
}

Die eigene Klasse SeparatorAwareListCellRenderer2 bekommt einen anderen ListCellRenderer übergeben und liefert die Komponenten dieses Delegate-Renderers immer dann, wenn das Element ungleich null ist. Ist es dagegen gleich null, liefert getListCellRendererComponent() ein Exemplar des JSeparators.

Im Beispielprogramm muss der Renderer nun angemeldet werden, und dazu ist Folgendes zu ergänzen:

comboBox.setRenderer(
  new SeparatorAwareListCellRenderer2<String>(comboBox.getRenderer()) );

Nach dem Start des Programms ist das Ergebnis schon viel besser: Dort, wo vorher die JComboBox eine leere Zeile darstellte, ist nun ein Strich.

Die Lösung ist jedoch nur ein Etappensieg, denn die Navigation mit der Tastatur durch die Liste zeigt eine Schwachstelle: Das null-Element lässt sich auswählen und erscheint auch als Linie im Editor/Textfeld. Beheben lässt sich das Problem nicht mit dem Renderer, denn der ist nur mit der Darstellung in der Liste beauftragt. Hier muss in die Interna der Swing-Komponente eingegriffen werden. Jede Swing-Komponente hat ein korrespondierendes UI-Delegate, das für das Verhalten und die Darstellung der Komponente verantwortlich ist. Für die JComboBox sind das Unterklassen von ComboBoxUI, und zwei Methoden sind besonders interessant:selectNextPossibleValue() und selectPreviousPossibleValue().

Da jede UI-Implementierung ihr eigenes Look and Feel (LAF) mitbringt, müssen wir hier eigentlich einen Dekorator bauen und jede Methode bis auf die beiden genannten an das Original weiterleiten, doch das ist jetzt zu viel Arbeit, und so nehmen wir die Basisklasse WindowsComboBoxUI als Basisklasse, denn unser Beispiel nutzt das Windows-LAF. In der Unterklasse implementieren wir eigene Versionen der Methoden selectNextPossibleValue() undselectPreviousPossibleValue(), die so lange die Liste nach oben/unten laufen müssen, bis sie ein Element ungleich null finden.

package com.tutego.insel.ui.list;

import com.sun.java.swing.plaf.windows.WindowsComboBoxUI;

public class SeparatorAwareComboBoxUI extends WindowsComboBoxUI
{
  @Override
  protected void selectNextPossibleValue()
  {
    for ( int index = comboBox.getSelectedIndex() + 1;
          index < comboBox.getItemCount();
          index++ )
      if ( comboBox.getItemAt( index ) != null )
      {
        comboBox.setSelectedIndex( index );
        break;
      }
  }

  @Override
  protected void selectPreviousPossibleValue()
  {
    for ( int index = comboBox.getSelectedIndex() - 1;
          index >= 0;
          index-- )
      if ( comboBox.getItemAt( index ) != null )
      {
        comboBox.setSelectedIndex( index );
        break;
      }
  }
}

Das UI-Objekt muss ebenfalls im main()-Programm angemeldet werden:

comboBox.setUI( new SeparatorAwareComboBoxUI() );

Enthält die Liste null-Elemente, überspringt die Tastennavigation über die Cursor-Taste diese. Doch auch mit diesem Teilstück fehlt ein weiteres Detail: Mit einem Klick lässt sich die Linie doch noch auswählen. Das ist keine Frage des Renderers und auch keine Frage der Tastaturnavigation – es muss untersagt werden, dass bei der Aktivierung ein null-Element zum Editor kommen kann. Hier ist die Methode setSelectedItem() von JComboBox entscheidend. Denn jedes Element, das selektiert wird – und dadurch auch in das Textfeld kommt –, geht durch die Methode hindurch. Wenn wir die Methode überschreiben und bei null-Elementen einfach nichts tun, wird auch das null-Element nicht selektiert, und im Textfeld bleibt das letzte Element stehen.

Damit auch spezielle Implementierungen von JComboBox von diesem Verhalten profitieren können, müssen wir wieder einen Dekorator schreiben, doch das kostet zu viel Mühe, und so überschreibt eine einfache Unterklasse die setSelectedItem()-Methode. (Im Prinzip wäre auch eine überschriebene Methode vonsetSelectedIndex() sinnvoll, denn das könnte eine programmierte Aktivierung von null vermeiden.)

package com.tutego.insel.ui.list;

import javax.swing.*;

public class SeparatorAwareJComboBox<E> extends JComboBox<E>
{
  @SafeVarargs
  public SeparatorAwareJComboBox( E... items )
  {
    super( items );
  }

  @Override
  public void setSelectedItem( Object anObject )
  {
    if ( anObject != null )
      super.setSelectedItem( anObject );
  }
}

Im Hauptprogramm muss nur diese spezielle Klasse zum Einsatz kommen und so folgt:

package com.tutego.insel.ui.list;

import javax.swing.*;

public class JComboBoxWithSeparator2
{
  public static void main( String[] args ) throws Exception
  {
    UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
//    UIManager.setLookAndFeel( new NimbusLookAndFeel() );

    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    String[] items = { "Cambria", null, "Arial", "Verdana", "Times" };
    JComboBox<String> comboBox = new SeparatorAwareJComboBox<>( items );
    comboBox.setRenderer( new SeparatorAwareListCellRenderer2<>(comboBox.getRenderer()) );
    comboBox.setUI( new SeparatorAwareComboBoxUI() );
    comboBox.setSelectedIndex( 1 );
    frame.add( comboBox );
    frame.pack();
    frame.setVisible( true );
  }
}

Inselraus: Variablen mit Xor vertauschen

Eine besonders trickreiche Idee für das Vertauschen von Variableninhalten arbeitet mit dem Xor-Operator und benötigt keine temporäre Zwischenvariable. Die Zeilen zum Vertauschen von x und y lauten wie folgt:

int x = 12, 
  
    y = 49;
x ^= y; // x = x ^ y = 001100bin ^ 110001bin = 111101bin
y ^= x; // y = y ^ x = 110001bin ^ 111101bin = 001100bin
x ^= y; // x = x ^ y = 111101bin ^ 001100bin = 110001bin
System.out.println( x + " " + y );  // Ausgabe ist: 49 12

Der Trick funktioniert, da wir mit Xor etwas »hinein- und herausrechnen« können. Zuerst rechnet die erste Zeile das y in das x. Wenn wir anschließend die Zuweisung an das y machen, dann ist das der letzte schreibende Zugriff auf y, also muss hier schon das vertauschte Ergebnis stehen. Das stimmt auch, denn expandieren wir die zweite Zeile, steht dort: »y ^ x wird zugewiesen an y«, und dies ist y ^ (x ^ y). Der letzte Ausdruck verkürzt sich zu y = x, da aus der Definition des Xor-Operators für einen Wert a hervorgeht: a ^ a = 0. Die Zuweisung hätten wir zwar gleich so schreiben können, aber dann wäre der Wert von y verloren gegangen. Der steckt aber noch in x aus der ersten Zuweisung. Betrachten wir daher die letzte Zeile x ^ y: y hat den Startwert von x, doch in x steckt ein Xor-y. Daher ergibt x ^ y den Wert x ^ x ^ y, und der verkürzt sich zu y. Demnach haben wir den Inhalt der Variablen vertauscht. Im Übrigen können wir für die drei Xor-Zeilen alternativ schreiben:

y ^= x ^= y;   // Auswertung automatisch y ^= (x ^= y) 
  
x ^= y;

Da liegt es doch nahe, die Ausdrücke weiter abzukürzen zu x ^= y ^= x ^= y. Doch leider ist das falsch (es kommt für x immer null heraus). Motivierten Lesern bleibt dies als Denksportaufgabe überlassen.

GMail-URLs generieren

private static String createGMailUrl( String to, String cc, String subject, String body )
{
  StringBuilder result = new StringBuilder( "https://mail.google.com/mail/?view=cm&tf=0" );
  
  if ( to != null && ! to.isEmpty() )
    result.append( "&to=" ).append( to );
  if ( cc != null && ! cc.isEmpty() )
    result.append( "&cc=" ).append( cc );
  if ( subject != null && ! subject.isEmpty() ) {
    try {
      result.append( "&su=" ).append( encode( subject ) );
    }
    catch ( UnsupportedEncodingException e ) {
      e.printStackTrace();
    }
  }
  if ( body != null && ! body.isEmpty() ) {
    try {
      result.append( "&body=" ).append( encode( body ) );
    }
    catch ( UnsupportedEncodingException e ) {
      e.printStackTrace();
    }
  }
  return result.toString();
}
private static String encode( String param ) throws UnsupportedEncodingException
{    
  byte[] utf8 = param.getBytes( "utf-8" );
  StringBuilder result = new StringBuilder( param.length() * 3 );
  for ( int i = 0; i < utf8.length; i++ )
  {
    String c = Integer.toString( utf8[ i ] & 0xFF, 16 );
    result.append( '%' ).append( c.length() == 1 ? "0" + c : c );
  }
  return result.toString();
}

Wanted: Native English speaker for proofreadig my ebook about Google Guava I/O

Seit etwa einem Jahr schreibe ich unregelmäßig an Tutorials zu Google Guava. Das Paket com.google.common.io ist nahezu komplett dokumentiert und Schritt für Schritt taste ich mich an die fast 300 Typen der Google Bibliothek ran. Der I/O Teil fasst etwa 50 Seiten und ich möchte dieses Bündel als ebook veröffentlichen. Da mir mein englisch Geschriebenes holprig und nicht flüssig vorkommt, suche ich einen Java-affinen englische Muttersprachler, der/die etwas über Google Guava IO lernen möchte bzw. im Idealfall selbst Erfahrung mit der Lib gesammelt hat und Details einstreuen kann. Interessenten sollten sich bitte unter ullenboom<at>gmail<dot>com melden. Danke.

Nach meiner jetzigen Planung werden es drei ebooks:

  • I/O and Networks
  • Basic Utilities and Java SE Extensions
  • Common Collection

Die Dokumentation zu com.google.common.base und com.google.common.primitives ist zu 80 % abgeschlossen, sodass ich denke, das zweite ebook auch noch in diesem Jahr fertigstellen zu können.

JDOM 2.0.1 freigegeben

http://www.jdom.org/news/index.html. Von der Webseite:

04.28.2012: JDOM 2.0.1 Released!

JDOM 2.0.1 is here!

JDOM 2.0.1 Introduces official support for Android! See the JDOM and Android page. JDOM 2.0.1 also fixes a bug in the ‚Compact‘ output of XML.

Get JDOM 2.0.0 Here! or from the maven-central repository here: Group: org.jdom, Articact: jdom

04.08.2012: JDOM 2.0.0 Released!

JDOM 2.0.0 is here!

JDOM 2.0.0 brings JDOM in to the world of Generics and other Java language items introduced with Java 5. As a result, JDOM 2.0.0 requires Java 5 or later, but is only fully supported on Java 6 and later.

Get JDOM 2.0.0 Here! or from the maven-central repository here: Group: org.jdom, Articact: jdom

In der Insel habe ich meine Programme nun auf JDOM 2 gebracht – ohne große Probleme. Nur die Generics muss ich in der Doku dokumentieren. Das einzige, was ich umschreiben musste, was das Kapitel über XPath, dazu gleich ein eigener Beitrag.

Inselraus: identityHashCode() ist für eine System-eindeutige Objekt-IDs nicht geeignet

Stellen wir uns vor, wir hätten eine Objekthierarchie im Speicher, die zum Beispiel jeder Socke einen Besitzer zuspricht. Wenn wir im Speicher Assoziationen abbilden, dann sollen diese Verweise auch noch nach dem Tod des Programms überleben. Eine Lösung ist die Serialisierung, eine andere eine Objekt-Datenbank oder aber auch eine XML-Datei. Doch überlegen wir selbst, wo bei der Abbildung auf eine Datenbank oder eine Datei das Problem besteht. Zunächst stehen ganz unterschiedliche Objekte mit ihren Eigenschaften im Speicher. Das Speichern der Zustände ist kein Problem, denn nur die Attribute müssten abgespeichert werden. Doch wenn ein Objekt auf ein anderes verweist, muss dieser Verweis gesichert werden. Aber in Java ist ein Verweis durch eine Referenz gegeben, und was sollte es da zu speichern geben? Eine Lösung für das Problem ist, jedem Objekt im Speicher einen Zähler zu geben und beim Speichen etwa zu sagen: »Der Besitzer 2 kennt Socke 5«.

Der Identifizierer für die Objekte muss eindeutig sein, und wir können überlegen, System.identityHashCode() zu nutzen. In der Implementierung der virtuellen Maschine von Oracle geht in den Wert von identityHashCode() die Information über den wahren Ort des Objekts im Speicher ein. Bei einer 64-Bit-Implementierung würden auch 32 Bit abgeschnitten, und die Eindeutigkeit wäre somit automatisch nicht mehr gewährleistet. Ein weiteres Problem besteht darin, dass zwar die Implementierung von Oracles identityHashCode() auf die eindeutige Objektspeicheradresse abbildet, aber dass das nicht jeder Hersteller so machen muss. Damit ist identityHashCode() nicht überall gesichert unterschiedlich. Zudem ist es prinzipiell denkbar, dass die Speicherverwaltung die Objekte verschiebt. Was sollte identityHashCode() dann machen? Wenn die neue Speicheradresse dahinter steckt, würde sich der Hashcode ändern, und das darf nicht sein. Es käme ebenfalls zu einem Problem, wenn mehr als Integer.MAX_INTEGER viele Objekte im Speicher stünden. (Doch wenn wir uns die große Zahl 2^32 = 4.294.967.296 vor Augen halten, dann es ist unwahrscheinlich, dass sich mehr als 4 Milliarden Objekte im Speicher tummeln. Zudem bräuchten wir 4 Gigabyte Speicher, wenn jedes Objekt auch nur 1 Byte kosten würde.)

Es ist gar nicht so schwierig, zwei unterschiedliche Objekte mit gleichen identityHashCode()-Resultat zu bekommen. Wir erzeugen ein paar String-Objekte und testen, jedes mit jedem, ob identityHashCode() den gleichen Wert ergibt:

String[] strings = new String[5000];
for ( int i = 0; i < strings.length; i++ )
  strings[i] = Integer.toString( i );
int cnt = 0;
for ( int i = 0; i < strings.length; i++ ) {
  for ( int j = i + 1; j < strings.length; j++ ) {
    int id1 = System.identityHashCode( strings[i] );
    int id2 = System.identityHashCode( strings[j] );
    if ( id1 == id2 ) {
      out.println( "Zwei Objekte mit identityHashCode() = " + id1 );
      out.println( " Objekt 1: \"" + strings[i] + "\"" );
      out.println( " Objekt 2: \"" + strings[j] + "\"" );
      out.println( " Object1.hashCode(): " + strings[i].hashCode() );
      out.println( " Object2.hashCode(): " + strings[j].hashCode() );
      out.println( " Object1.equals(Object2): " + strings[i].equals( strings[j] ) );
      cnt++;
    }
  }
}
System.out.println( cnt + " Objekte mit gleichem identityHashCode() gefunden." );

Ein Durchlauf bringt schnell Ergebnisse wie:

Zwei Objekte mit identityHashCode() = 9578500
Objekt 1: "541"
Objekt 2: "2066"
Object1.hashCode(): 52594
Object2.hashCode(): 1537406
Object1.equals(Object2): false
Zwei Objekte mit identityHashCode() = 14850080
Objekt 1: "2085"
Objekt 2: "2365"
Object1.hashCode(): 1537467
Object2.hashCode(): 1540288
Object1.equals(Object2): false
2 Objekte mit gleichem identityHashCode() gefunden.

Das Ergebnis ist also, dass identityHashCode() nicht sicher bei der Vergabe von Identifizierern ist. Um wirklich allen Problemen aus dem Weg zu gehen, ist ein Zählerobjekt oder eine ID über zum Beispiel die Klasse java.util.UUID nötig.

JavaFX 2.1 ist fertig

Download unter http://www.oracle.com/technetwork/java/javafx/downloads/index.html. Aus den http://docs.oracle.com/javafx/release-documentation.html:

  • Media H.264 and AAC support

  • Mac OS X support

    Applications must be packaged for the desktop, Web and Web Start applications are not yet supported.

  • LCD text

  • UI enhancements, including controls for Combo Box, Stacked Chart, and application-wide menu bar

  • Webview to support JavaScript to Java method calls