Speichern und Laden von Objekten (Seriealisierung vs. Reflection)

blauerninja

Mitglied
Speichern und Laden von Objekten

Wer schon bisschen länger programmiert, kommt sicher bald an ein Problem: ein Objekt soll gespeichert werden, um irgendwann wieder geladen und in den gespeicherten Zustand versetzt zu werden. Am ehesten taucht diese Aufgabe bei der Spieleprogrammierung auf. Schließlich will kein Gamer jedes Mal vom Neuen anfangen oder das Spiel und somit den PC ständig laufen lassen.

Java begegnet diesem Problem mit zwei mächtigen Mechanismen - die da wären:
Seriealisierung
Reflektion

In diesem Tutorial möchte ich euch beide Mechanismen darstellen und mit einem Vergleich enden.


Speichermedium

Generell gibt es 4 verschiedene Medien, worin man ein Objekt speichern kann.
  • Datenbank
  • XML-File
  • Text-Dokument
  • Binärdatei

Bei den ersten 3 werden die zu speichernden Daten aus dem Objekt ausgelesen und entsprechend in eine Datenbank oder eine Datei gespeichert. Beim Laden werden die Daten über einen entsprechenden Konstuktor oder setter-Methoden wieder in das Objekt geladen.
Bei der 4. Möglichkeit speichert man den Speicherinhalt des Objekts aus dem Arbeitsspeicher auf die Festplatte. Beim Laden werden die Daten wieder ins RAM geladen und man könnte meinen, das Objekt war niemals abwesend. Man kennt es zum Beispiel vom Ruhezustand bei Windows.

Welchen Weg man auch wählt, eins muss sicher gestellt werden: die Daten müssen vor Manipulation geschützt werden (etwa durch Verschlüsselung oder Checksummen) und vor dem Laden auf Gültigkeit geprüft werden.
Werden bspw. die Binärdaten verändert, so kann es zu einem gravierenden Fehler oder Programmabsturz führen, wenn man daraus wieder ein Objekt erzeugt und damit weiterarbeitet.

Im folgenden werde ich der Einfachheit halber die Daten in eine Text- bzw. bei der Serialisierung in eine Binärdatei schreiben. Auf den Datenschutz verzichte ich. Ein Tutorial zum Verschlüsseln einer Datei lässt sich sicherlich finden. Ansonsten lege ich jedem Java - Mehr als eine Insel als weiterführende Literatur ans Herz. Damit sind wir auch schon beim ersten Mechanismus.
 

blauerninja

Mitglied
Serialisisierung

Der wohl einfachere Mechanismus. Zuerst ein Beispiel:

Java:
package serialisationTest;

import java.io.Serializable;

public class Data implements Serializable {
  private int value;

  public Data(int value) {
    this.value = value;
  }

  public void change() {
    value++;
  }

  public void print() {
    System.out.println("var: " + value);
    System.out.println();
  }
}

Eine simple Klasse mit einer Objektvariable und einer Methode zum Verändern und für die Ausgabe auf dem Monitor. Der Dreh- und Angelpunkt stellt die Klasse
Code:
Serializable
aus dem Paket
Code:
java.io
dar. Nur Objekte, die dieses Interface implementieren können mittels der Serialisierung gespeichert werden. Erzeugen wir also eine Klasse, die mit Objekten unserer Datenklasse arbeitet und sie speichert.

Java:
package serialisationTest;

import java.io.*;

public class Save {
  public static void main(String[] args) {
    long time = System.nanoTime();
    Data[] data = new Data[1000000];

    for (int i = 0; i < data.length; i++) {
      data[i] = new Data(i);
    }

    save(data);
    System.out.println("Time: " + (System.nanoTime() - time)/(1000*1000*1000.0) + " s");
  }
}

Wir erzeugen in der
Code:
main()
1 Mio. Objekte und wollen sie nun in einer Datei speichern. Es soll Ausgegen werden wie lange das gedauert hat (für den Vergleich zu Reflektion). Implementieren wir also die Methode
Code:
save()
:

Java:
package serialisationTest;

import java.io.*;

public class Save {
  public static void main(String[] args) {
    long time = System.nanoTime();
    Data[] data = new Data[1000000];

    for (int i = 0; i < data.length; i++) {
      data[i] = new Data(i);
    }

    save(data);
    System.out.println("Time: " + (System.nanoTime() - time)/(1000*1000*1000.0) + " s");
  }

  public static void save(Data[] data) {
    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.bin"))) {
      for (Data i : data) {
        out.writeObject(i);
      }

      System.out.println("Serialization succeeded");
      System.out.println();
    } catch (Exception e) {
      System.out.println("Serialization failed");
      System.out.println();
    }
  }
}

Wir erzeugen ein
Code:
ObjectOutputStream
-Objekt und schreiben mittels
Code:
writeObject()
die Elemente des Array in die Datei (Man hätte auch den ganzen Array mittels
Code:
out.writeObject(data)
direkt in die Datei schreiben können). Das definieren der Objekte, die Exceptions werfen können, direkt im try ist übrigens erst seit Java 7 möglich. Wer noch mit Java 6 arbeitet, muss die Anweisung in den try-Block schreiben und den Strom zum Schluss noch schließen.

Lassen wir den Code kompilieren und führen wir ihn aus, so wird nach bei mir ca. 4.5s die Meldung ausgegeben, dass wir erfolgreich waren und die Binärdatei erstellt wurde. Natürlich steht da nur unsinniges Zeugs drin, wenn man sie mit einem Editor öffnet.

Versuchen wir nun die Objekte wieder zu laden:

Java:
package serialisationTest;

import java.io.*;

public class Load {
  public static void main(String[] args) {
    long time = System.nanoTime();
    Data[] data = load();
    System.out.println("Time: " + (System.nanoTime() - time)/(1000*1000*1000.0) + " s");

    data[0].print();
    data[1000000-1].change();
  }

  private static Data[] load() {
    Data[] data = new Data[1000000];

    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.bin"))) {
      for (int i = 0; i < data.length; i++) {
        data[i] = (Data) in.readObject();
      }

      System.out.println("Deserialization succeeded");
      System.out.println();
    } catch (Exception e) {
      System.out.println("Deserialization failed");
      System.out.println();
    }

    return data;
  }
}

Die Klasse arbeitet analog zu oben. Wir laden die Daten, geben die Zeit und führen Methoden für das erste und das letzte Objekt des Arrays aus, um zu prüfen, ob wirklich Objekte vom Typ
Code:
Data
erzeugt wurden. Nach ca. 4,1s wird die Meldung ausgegeben und die Methodenaufrufe führen zu keinem Laufzeitfehler.

Noch einige Hinweise zu Serialsierung:

Benutzen wir in der Klasse
Code:
Data
keinen primitiven Typ für unsere Variable, sondern Objekte, so müssen diese die Schnittstelle
Code:
Serializable
ebenfalls implementieren, damit sie gespeichert werden. Unterklassen von
Code:
Data
sind wegen der Vererbung natürlich auch serialisierbar.
Mit dem Schlüsselwort
Code:
[B]transient[/B]
können Variablen markiert werden, die nicht gespeichert werden sollen. Allerdings ist hier zu beachten, dass die Schlüsselwörter
Code:
static
oder
Code:
final
die Wirkung von
Code:
transient
aufheben. Sprich: nur bei nichtfinalen Instanzvariablen ist dieses Schlüsselwort sinnvoll.
Eine als
Code:
transient
definierte Variable hat nach dem Laden den Standartwert, gemäß:
  • boolean -> false
  • byte, short, int, long -> 0
  • float, double -> 0.0
  • Arrays, Objekte -> null

Es muss also vor allem bei Arrays und Objekten als Variablentyp darauf geachtet werden, dass im weiteren Verlauf keine
Code:
NullPointerException
aufkommt.
 
Zuletzt bearbeitet:

blauerninja

Mitglied
Reflection

Der Reflectionmechanismus ist ein sehr mächtiges Tool von Java, mit dem sich sämtliche Klassen, deren Methoden und Variablen erzeugen lassen. Mehr zu dem Thema: Java ist eine Insel.

Wir haben wieder unsere Datenklasse von vorhin, diesmal ein wenig modifiziert, schließlich müssen wir den Werte der Variablen herauslesen.

Java:
package serialisationTest;
 
public class Data {
  private int value;
 
  public Data(int value) {
    this.value = value;
  }

  public int getValue() {
    return this.value;
  }
 
  public void change() {
    value++;
  }
 
  public void print() {
    System.out.println("var: " + value);
    System.out.println();
  }
}

Auch die Klasse zum Speichern bleibt fast gleich.

Java:
package serialisationTest;

import java.io.*;
 
public class Save {
  public static void main(String[] args) {
    long time = System.nanoTime();
    Data[] data = new Data[1000000];
 
    for (int i = 0; i < data.length; i++) {
      data[i] = new Data(i);
    }
 
    save(data);
    System.out.println("Time: " + (System.nanoTime() - time)/(1000*1000*1000.0) + " s");
  }
 
  public static void save(Data[] data) {
    try (BufferedWriter out = new BufferedWriter(new FileWriter("test.txt"))) {
      for (Data i : data) {
        String saveData = i.getClass().getName() + "=" + i.getValue() + System.getProperty("line.separator");
        out.write(saveData);
      }
 
      System.out.println("Serialization succeeded");
      System.out.println();
    } catch (Exception e) {
      System.out.println("Serialization failed");
      System.out.println();
    }
  }
}

Wir speichern den Klassennamen samt Pakethierarchie und den Variablenwert in jeweils einer Zeile, getrennt durch ein Trennzeichen. Den vollständigen Klassennamen brauchen wir später bei der Erzeugung des Objekts. Hier im Beispiel können wir den sogar weglassen, da wir wissen, welchen Typ unser Objekt hat.
Man denke aber, dass es zum Beispiel verschiedene Unterklassen von
Code:
Data
geben kann und die Arrayewerte zufällig als Objekte dieser Unterklassen erzeugt werden. Dann ist es wohl wichtig, welcher Klasse das gespeicherte Objekt angehört hatte, sonst wissen wir nicht, was wir beim Laden erzeugen müssen. Somit sind wir schon beim Laden:

Java:
package serialisationTest;

import java.io.*;
import java.lang.reflect.*;

public class Load {
  public static void main(String[] args) {
    long time = System.nanoTime();
    Data[] data = load();
    System.out.println("Time: " + (System.nanoTime() - time)/(1000*1000*1000.0) + " s");

    System.out.println(data[0].getName());
    System.out.println(data[1000000-1].getDescription());
  }

  private static Data[] load() {
    Data[] data = new Data[1000000];

    try (BufferedReader in = new BufferedReader(new FileReader("test.txt"))) {
      for (int i = 0; i < data.length; i++) {
        String savedData = in.readLine();
        data[i] = load(savedData);
      }

      System.out.println("Deserialization succeeded");
      System.out.println();
    } catch (Exception e) {
      System.out.println("Deserialization failed");
      System.out.println();
    }

    return data;
  }

  private static Data load(String savedData) throws Exception {
    String[] args = savedData.split("=");
    String classPath = args[0];
    String var = args[1];

    Class<Data> dataClass = (Class<Data>) Class.forName(classPath);
    Constructor<Data> constructor = dataClass.getDeclaredConstructor(Integer.TYPE);
    return (Data) constructor.newInstance(var);
  }
}

Wir lesen die Zeilen aus der Datei und Trennen die Argumente bzgl. des Trennzeichens. Wir brauchen nun den vollständigen Klassennamen (mit Pakethierarchie). Die Klasse
Code:
Class
besitzt viele Methoden, darunter
Code:
forClass(Sring)
, die ein Klassenobjekt vom Typ
Code:
Class
erstellt. Durch dieses Objekt können wir nun auf alle in der Klasse definierten Methoden und Variablen zugreifen. Wir holen uns eine Instanz des in der Klasse definierten Konstruktors mit dem
Code:
int
Parameter und erzeugen mit
Code:
newInstance(args...)
ein neues Objekt der Klasse.
Code:
args
sind dabei die Parameter, die wir sonst beim Konstruktoraufruf übergeben würden.
Alles kein Hexenwerk, wenn man sich den Ablauf verinnerlicht hat.
 

blauerninja

Mitglied
Weiterführung des Reflectionkapitels:

Wichtig bei der Reflectionmethode ist, dass die Daten geschützt werden. Sie können schließlich manipuliert werden. Beim Seriealisieren gibt es dieses Problem nicht, da kümmert sich Java schon darum mit einer Prüfsumme und wirft einen Laufzeitfehler, wenn auch nur ein Bit in der Binärdatei verändert wurde. Beim Reflection bekommen wir nur eine Exception, wenn eine Klasse nicht existiert oder wenn Variablen nicht erzeugt werden können, bspw: String statt int.


Vor- und Nachteile

Kommen wir zur Gegenüberstellung beider Mechanismen. Erstmal der Zeitvergleich. Wir haben 1 Mio. Objekte gespeichert und geladen. Bei einem Test mit deutlich größeren Objekten kam auf meinem PC folgendes raus:

Reflection:
  • Speichern und Verschlüsseln: 3.0s, 25.7 MB
  • Laden und Entschlüsseln: 3.7s

+ schneller
+ kleinere Datei
+ minimalistischer (nur benötigte Dinge werden gespeichert)
o nach dem Laden ein komplett neues Objekt
- für Schutz muss selbst gesorgt werden
- ein wenig komplizierterer Algorithmus


Seriealisierung:
  • Speichern: 8.7s, 35.2 MB
  • Laden: 9.3s

+ serialisiertes Objekt kann in jedem Strom versendet werden (auch Netzwerk)
+ gespeicherte Daten müssen nicht mehr zusätzlich geschützt werden
- langsamer
- größere Datei
- Komponenten eines Objekts müssen auch Serializable implementieren
- transient Variablen können Fehler verursachen


Fazit
Damit sind wir auch schon am Ende angekommen. Ich persönlich werde Reflection benutzen. Letztendlich hat beides Vor- und Nachteile. 1 Mio. Objekte werden selten gespeichert werden müssen, sodass der Zeit- und Größenunterschied nicht so viel wiegt (und selbst bei so einer Million ist der Zeitfaktor noch akzeptabel auf heutigen PCs). Arbeitet man mit Strömen oder soll gar über ein Netzwerk ein Objekt verschicken, eignet sich hierfür natürlich die Serialisierung besser. Mit Reflection hat man eine größere Auswahl an Speichermedien. Meines Wissens können aber auch serialisierte Objekte in einer Datenbank gespeichert werden.

Ich hoffe, ich konnte euch weiterhelfen. Kommentare, Kritik, Verbesserungen sind jederzeit willkommen.
LG blauerninja
 
Zuletzt bearbeitet:

The_S

Top Contributor
Hm, das Reflection-Beispiel ist aber extrem vereinfacht und auch nicht allgemein gehalten. Serialisierung ist da viel mächtiger und unkomplizierter, deshalb kann man die Beiden imho nicht so einfach vergleichen.
 
7

78dhj38d8i34k

Gast
Bei funktioniert der Quelltext nicht...
Der Compiler nimmt die try-Bedingung nicht an, bzw. er sagt mit direkt hinterm try fehlt "{". Also das ObjectOutputStream out nimmt er nicht an.
Das Fehler existiert in beiden Methoden, in Save und in Load..
 

blauerninja

Mitglied
Hi,
sag bitte in welcher Zeile der Fehler steckt. Welche Java Version hast du. In der Konsole kann man mit java -version dies ermitteln. Ich benutze das neue try, wo ein Strom gleich übergeben wird. Dieser wird dann automatisch geschlossen. Dies macht das frühere manuelle Schließen einer Ressource überflüssig. Dazu braucht man aber Java 1.7. Vllt liegt es daran, denn bei mir war der Code compilier- und ausführbar. Und mit Copy-Paste kann man wenig falsch machen ;)

LG
blauerninja
 
7

78dhj38d8i34k

Gast
Hallo,
ich habe Java-Version 1.7.0_05.
Der Fehler ist in Serialisierung im 3. Code in Z.19 und um 4.Code in Z.18. ;)
 

blauerninja

Mitglied
Tut mir leid, kann ich nicht bestätigen. Bei mir (Copy & Paste) ist sowohl Save als auch Load von Abschnitt Serialization kompilierbar und wird ohne Fehler ausgeführt.

Vllt liegt es wirklich am neuen try-catch. Schreib ihn mal in die normale try-Version um. Evtl verschwindet der Fehler dann.
 
Zuletzt bearbeitet:

wef34fewrg

Aktives Mitglied
Ist jetzt schon etwas älter, aber dennoch interessant.

Kommen wir zur Gegenüberstellung beider Mechanismen. Erstmal der Zeitvergleich. Wir haben 1 Mio. Objekte gespeichert und geladen. Bei einem Test mit deutlich größeren Objekten kam auf meinem PC folgendes raus:

1. Zeitunterschied

Reflection:
Java:
 public static void save(Data[] data) {
    try (BufferedWriter out = new BufferedWriter(new FileWriter("test.txt"))) {
      for (Data i : data) {
        String saveData = i.getClass().getName() + "=" + i.getValue() + System.getProperty("line.separator");
        out.write(saveData);
      }

Serialisierung:
Java:
public static void save(Data[] data) {
    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.bin"))) {
      for (Data i : data) {
        out.writeObject(i);
      }

Die enormen Zeitunterschiede kommen dadurch zu Stande, dass in der Reflection Methode ein BufferedWriter genutzt, in der Serialisierung der BufferedOutputStream aber weggelassen wird. Wenn man den BOS mit einbaut wird man sehen, dass die Unterschiede relativ gering sind. Verbessert bei mir das Laden und Speichern um den Faktor 10.
Die Grundaussage, dass die Reflection schneller ist, ist allerdings nicht angezweifelt. Schon zwei mal nicht bei der performance Leistung der built in Variante.

2. Speichergröße.

Da war ich jetzt schon etwas erstaunt. Auch hier gilt grundsätzlich, dass die built in Serialisierung leider zu viel Platz braucht und anderen Speichermethoden unterlegen ist:(, allerdings dürfte in deinem Beispiel die Reflection einen größeren Output erzeugen als die Serialisierung. Hab jetzt leider gesehen dass der Threadersteller hier nicht mehr aktiv ist, aber würde mich dennoch interessieren, wie er/sie auf die Werte gekommen ist.

Bei den Vor und Nachteilen ließe sich bestimmt streiten. :)

Fazit
.
.
.
Meines Wissens können aber auch serialisierte Objekte in einer Datenbank gespeichert werden.

Ist eigentlich sogar noch der häufigste Anwendungsfall. Ich glaube aber, dass die Serialisierung von den meisten gemieden wird und die Objekte auch mit Hilfe performanter Frameworks serialisiert werden. So zumindest meine, wenn auch bescheidene, Erfahrung.
 
Zuletzt bearbeitet:

Neue Themen


Oben