System.identityHashCode()

...und das Problem der nicht-eindeutigen Objektverweise für Objekt-IDs

Die Gleichheit von Objekten wird mit der Methode equals() neu definiert. Wenn equals() neu implementiert wird, dann gilt das in der Regel auch für die Methode hashCode(), die ebenfalls überschrieben werden soll. So wird hashCode() bei unterschiedlichen Objektzuständen unterschiedliche Werte zurückgeben und gleiche Objekteinhalte müssen den gleichen Hashwert liefern. Die Standardimplementierung von Object sieht nun so aus, dass auch bei Objekten, die gleiche Werte annehmen, unterschiedliche Hashwerte herauskommen — das ist auch der Grund, warum wir hashCode() überschreiben sollten. Doch was liefert denn hashCode() von Object eigentlich? Es sieht so aus, als ob dies eine Objekt-ID wäre, die das Objekt eindeutig kennzeichnet. Die Ur-ID geht verloren, wenn hashCode() neu implementiert wird. Doch interessiert der ursprüngliche hashCode()-Wert, so bietet sich System.identityHashCode() an.

Beispiel: Obwohl die Hashwerte zu zwei Objekten gleich sind, liefert identityHashCode() in der Regel unterschiedliche Werte.

Point p = new Point( 0, 0 );
Point q = new Point( 0, 0 );
System.out.println( System.identityHashCode(p) ); // z.B 16032330
System.out.println( System.identityHashCode(q) ); // z.B. 13288040

System.out.println( p.hashCode() ); // 0
System.out.println( q.hashCode() ); // 0

Beispiel: Wenn hashCode() nicht überschrieben wird, dann stimmt der Hashwert mit dem identityHashCode() überein. Ein Beispiel dafür ist die Klasse StringBuffer, sie überschreibt hashCode() nicht.

StringBuffer sb1 = new StringBuffer();
StringBuffer sb2 = new StringBuffer();
System.out.println( System.identityHashCode(sb1) + " " + sb1.hashCode() ); // z.B. 7439041 7439041
System.out.println( System.identityHashCode(sb2) + " " + sb2.hashCode() ); // z.B. 4152583 4152583

Diese statische Funktion identityHashCode() liefert den Original-Identifizierer der Objekte. Auf den ersten Blick sieht sie nach einer eindeutig ID aus, das stimmt aber nicht immer. Es kann durchaus zwei unterschiedliche Objekte im Speicher geben, für die System.identityHashCode() gleich ist. Wir werden gleich ein Beispiel sehen.

Abbilden von Objektverweisen in Datenbanken oder Dateien

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 andere 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 Sun geht in den Wert von identityHashCode() die Information über den wahren Ort des Objekts im Speicher ein. Bei einer 64-Bit-Implementierung würde auch 32 Bit abgeschnitten und die Eindeutig ist somit automatisch nicht mehr gewährleistet. Ein weiteres Problem besteht darin, dass zwar die Implementierung von Sun identityHashCode() auf die eindeutige Objektspeicheradresse abbildet, aber das 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. Ebenfalls ein Problem ist, 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 Objekten im Speicher tummeln. Zudem bräuchten wie 4 Gigabyte Speicher, wenn jedes Objekt auch nur 1 Byte kosten würde.)

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

public class IdentityHashCode
{
  public static void main( String args[] )
  {
    Object o[] = new Object[5000];

    for ( int i = 0; i < o.length; i++ )
      o[i] = Integer.toString( i );

    int cnt = 0;

    for ( int i = 0; i < o.length; i++ )
    {
      for (int j = i + 1; j < o.length; j++ )
      {
        int id1 = System.identityHashCode( o[i] );
        int id2 = System.identityHashCode( o[j] );

        java.io.PrintStream p = System.out;

        if ( id1 == id2 )
        {
          p.println( "Zwei Objekte mit identityHashCode() = " + id1 );
          p.println( " Objekt 1: \"" + o[i] + "\"");
          p.println( " Objekt 2: \"" + o[j] + "\"");
          p.println( " Object1.hashCode(): " + o[i].hashCode() );
          p.println( " Object2.hashCode(): " + o[j].hashCode() );
          p.println( " Object1.equals(Object2): " + o[i].equals(o[j]) );

          cnt++;
        }
      }
    }
    System.out.println( cnt +
                        " Objekte mit gleichem identityHashCode() gefunden" );
  }
}

Ein Durchlauf bringt schnell Ergebnisse wie:

Zwei Objekte mit identityHashCode() = 9578500 
Objekt 1: "272" 
Objekt 2: "1797" 
Object1.hashCode(): 49805 
Object2.hashCode(): 1514436 
Object1.equals(Object2): false 
[…]
4 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 die sicherste Lösung. Diese kann etwa so aussehen:

class Socke
{
  final int id = UniqueGenerator.next();
}

public class UniqueGenerator
{
  private static int id;

  public static synchronized int next()
  {
    return ++id;
  }
}

Jetzt verbindet sich zum Beispiel eine Socke mit dem Identifizierer 1 mit dem Besitzer 2. Beim Laden dienen diese Werte nur zur Identifizierung der Objekte, keinesfalls dazu, beim Laden wieder den gleichen Objektspeicher zu bekommen. Das heißt während der Rekonstruktion im Speicher haben sie schon ganz andere Werte für identityHashCode() und die alten Werten müssen wir vergessen. Beim erneuten Anspeichern dürfen die Werte ganz anders aussehen.

Eine kleine Anmerkung zur Implementierung: Unter Java 5 können die IDs gut mit einem AtomicInteger realisiert werden.

Beispiel: Eine Socke besitzt ein Attribut für Farbe und ein Besitzer hat einen Namen und einen Verweis auf die Socke. Dann könnte ein Dateiformat zum Speichern der Assoziationen so aussehen:

<objects>
  <object typ="Socke" id="1">
   <farbe> Standard-Blau </farbe>
  </object>
  <object typ="Besitzer" id="2">
   <socke ref="1"/>
  </object>
</objects>