Input/Output Serialisierung und Object.hashCode()

Alan47

Mitglied
Hallo zusammen,

ich habe kürzlich etwas entdeckt, das mich sehr verwundert hat. Dafür habe ich euch ein minimales, funktionierendes Beispiel geschrieben:

Java:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashSet;

public class SerializationTest {

	@SuppressWarnings("unchecked")
	public static void main(String[] args) throws Exception {
		HashSet<MyObject> hashSet = new HashSet<MyObject>();
		MyObject objectA = new MyObject();
		hashSet.add(objectA);
		// serialize to file
		File file = new File("test");
		FileOutputStream fos = new FileOutputStream(file);
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(hashSet);
		oos.writeObject(objectA);
		oos.close();
		fos.close();
		// set all references to null
		hashSet = null;
		objectA = null;
		// reload from file
		FileInputStream fis = new FileInputStream(file);
		ObjectInputStream ois = new ObjectInputStream(fis);
		hashSet = (HashSet<MyObject>) ois.readObject();
		objectA = (MyObject) ois.readObject();
		// close the streams
		ois.close();
		fis.close();
		file.delete();
		// the critical call:
		if (hashSet.contains(objectA)) {
			System.out.println("This works");
			// WHY does this work???
		} else {
			System.out.println("This does not work");
		}

	}
}

class MyObject implements Serializable {

	public static final long serialVersionUID = 1L;
}


Was passiert:
- ein HashSet wird angelegt
- ein Objekt ("objektA") wird angelegt
- dem HashSet wird objektA hinzugefügt
- das HashSet und objektA werden in eine Datei serialisiert
- die Referenzen auf die Objekte werden aus dem Programm gelöscht
- das HashSet und objektA werden erneut geladen
- das HashSet bestätigt die Existenz von objektA in seinem Inhalt.


Eines vorweg: ich finde es gut, dass das funktioniert, es kommt meiner Anwendung entgegen. Aber: warum funktioniert das eigentlich? HashSets funktionieren ja auf Basis der "hashCode()"-Methode der Objekte, die sie speichern. Wenn ein Objekt seinen hashCode() ändert, so würde das HashSet das Objekt nicht mehr finden und ein Aufruf von "contains(...)" würde false zurückliefern.
Da der Aufruf von "contains(...)" aber true zurückliefert, lässt das nur einen Schluss zu: die Standard-Implementierung von "hashCode()" liefert über die Lebensdauer eines Objektes hinweg konstante Werte. Wenn man sich jetzt aber die API zu Object#hashCode() ansieht, so wird dort behauptet, dass die hashCode()-Implementierung auf der internen Objekt-ID basiert, die für jedes erzeugte Objekt einzigartig ist.

Und jetzt kommt die Stelle, wo ich mich frage, wieso der obige Code funktionieren kann: wenn ein Objekt serialisiert und erneut geladen wird, dann muss es zwangsläufig eine neue interne Objekt-ID bekommen (es könnte passieren, dass die ursprüngliche Objekt-ID schon vergeben ist). Daher müsste sich der hashCode() des Objektes ändern, da er ja auf eben jener ID passiert. Offensichtlich tut er das aber nicht, sonst würde das Objekt im HashSet nicht mehr gefunden werden. Die Ausgabe des Programms auf der Kommandozeile ist immer "This works".


Ein verzwicktes Rätsel - ich bin zwar froh, dass es funktioniert, aber ich wüsste gerne, warum es funktioniert. Kann jemand Licht auf diese Sache werfen?



Gruß,


Alan
 
Zuletzt bearbeitet:
mhm verstehe nicht so was dich ueberrascht ?!

du serialisierst eine HashSet das ein Objekt beinhaltet und wunderst dich danach dass das Objekt (das du auch deserialisiert) noch im Set ist ?

Holy moly, was waere das, wenn das nicht gelten wuerde ?! jegliche Containerklassen waeren unsinnig zu serialisieren ?!
 
@Alan47
du ziehst ohne Not falsche Schlüsse, z.B. könntest du dir die Hashcodes vorher und nachher auch anschauen!

das HashSet wird bei der Serialisierung nicht exakt gleich bleiben, nicht zwingend die alten HashCodes weiterverwenden,
sondern wird beim Laden neu aufgebaut mit den dann neuen Hashcodes,
das alte Objekt ist im neuen auch nicht mehr zu finden, ein weiterer Test

Code aus HashSet zur Serialisierung:
Java:
    /**
     * Save the state of this <tt>HashSet</tt> instance to a stream (that is,
     * serialize this set).
     *
     * @serialData The capacity of the backing <tt>HashMap</tt> instance
     *		   (int), and its load factor (float) are emitted, followed by
     *		   the size of the set (the number of elements it contains)
     *		   (int), followed by all of its elements (each an Object) in
     *             no particular order.
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
	// Write out any hidden serialization magic
	s.defaultWriteObject();

        // Write out HashMap capacity and load factor
        s.writeInt(map.capacity());
        s.writeFloat(map.loadFactor());

        // Write out size
        s.writeInt(map.size());

	// Write out all elements in the proper order.
	for (Iterator i=map.keySet().iterator(); i.hasNext(); )
            s.writeObject(i.next());
    }

    /**
     * Reconstitute the <tt>HashSet</tt> instance from a stream (that is,
     * deserialize it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
	// Read in any hidden serialization magic
	s.defaultReadObject();

        // Read in HashMap capacity and load factor and create backing HashMap
        int capacity = s.readInt();
        float loadFactor = s.readFloat();
        map = (((HashSet)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        // Read in size
        int size = s.readInt();

	// Read in all elements in the proper order.
	for (int i=0; i<size; i++) {
            E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }

gut ist allgemein bei Serialisierung, dass das eine MyObject nur genau einmal im Stream steht, nicht zweimal und dann getrennt wieder geladen,
da wird aufgepasst so dass beim Laden das gelesene Objekt exakt das aus der geladenen Map ist
 
das HashSet wird bei der Serialisierung nicht exakt gleich bleiben, nicht zwingend die alten HashCodes weiterverwenden,
sondern wird beim Laden neu aufgebaut mit den dann neuen Hashcodes,
das alte Objekt ist im neuen auch nicht mehr zu finden, ein weiterer Test

Ha, genau das ist der Punkt 🙂 Ich war irgendwie total verbohrt und dachte dass das HashSet eben einfach rausgeschrieben und wieder reingelesen wird - darauf, dass es beim Reinlesen mit den neuen Hashcodes neu aufgebaut werden könnte, habe ich komplett vergessen. Darum war ich auch so verwirrt, jetzt ist es klar.

Vielen Dank!




Alan
 

Zurück
Oben