Javas kaputte Datenkapselung

Meiner Erfahrung nach werden in Objekt-Orientierungs-Diskussionen folgende drei Punkte genannt, auch in dieser Priorität, die die Objektorientierung ausmachen sollen:

  1. Vererbung von Klassen
  2. Schnittstellen
  3. Kapselung

Ist Vererbung wirklich das was Objektorientierung ausmacht? Immerhin ist Vererbung nicht bei jeder Klasse notwendig, sondern nur von Fall zu Fall nötig. Streichen wir also den ersten Punkt von unserer Liste!

Die Schnittstellen, also Funktionen einer Klasse, ermöglichen die Kommunikation mit dem Objekt. Die Klasse kapselt ihre Eigenschaften (Daten) und ermöglicht nur über die Schnittstellen den Zugriff auf die Eigenschaften. Die Daten sollen vor direkter Manipulation geschützt werden, um eine Blackbox zu simulieren.

Ich sehe regelmäßig Java-Code, in dem die Blackbox-Regel gebrochen wird. Viele Programmierer meinen, wenn sie die Daten einer Klasse als Privat deklarieren, ist alles gut.

Gehen wir das klassische Beispiel Adresse und Person an. Die Klasse Adresse dient als Datenklasse:

class Adresse {
    private String m_strasse;
    private String m_ort;

    public String getStrasse() {
        return m_strasse;
    }

    public void setStrasse(String s) {
        m_strasse = s;
    }

    public String getOrt() {
        return m_ort;
    }

    public void setOrt(String s) {
        m_ort = s;
    }

    @Override
    public String toString() {
        return "Adresse: " + m_strasse + ", " + m_ort;
    }
}

Klasse Person hat einen Namen und eine Adresse:

class Person {
    private final String m_name;
    private Adresse m_adresse;

    public Person(String name) {
        m_name = name;
    }
    
    public String getName() {
        return m_name;
    }

    public Adresse getAdresse() {
        return m_adresse;
    }

    public void setAdresse(Adresse a) {
        if (a == null) {
            System.out.println("Sie müssen schon eine gültige Adresse übergeben!");
            return;
        }
        m_adresse = a;
        System.out.println("Ich (" + m_name + ") melde meine neue " + m_adresse);
    }
}

Nun, die Adresse ist in der Person gekapselt.  Wir können nur über die Set- und Get-Schnittstellen darauf zugreifen, siehe Zeile 13 und 17. Wir wollen das ein Personen-Objekt schließlich über seine eigene Wohnanschrift bescheid weiß und eine Änderung mitbekommt.

Geben wir unserer Person Fritz eine Wohnanschrift:

Person p = new Person("Fritz");

Adresse adr = new Adresse();
adr.setOrt("Entenhausen");
adr.setStrasse("Disneystrasse 1");
p.setAdresse(adr);

Durch Zeile 6 wird Fritz eine Adresse übergeben und Fritz wird uns seine neue Anschrift dann auch mitteilen.
Ausgabe:

Ich (Fritz) melde meine neue Adresse: Disneystrasse 1, Entenhausen

Der Sinn einer Schnittstelle und Kapselung wird nachfolgend noch deutlicher. Wir können so nämlich auch invalide Daten abfangen:

p.setAdresse(null);

Ausgabe:

Sie müssen schon eine gültige Adresse übergeben!

Das Person-Objekt hat also die Null-Adresse abgewiesen. Wunderbar! Das Objekt behält die Kontrolle.

Kapselung umgehen

So weit so gut. Jetzt werde ich Ihnen zeigen, wie kaputt die Kapselung von Java ist.

Adresse adr2 = p.getAdresse();
adr2.setOrt("Gotham");
adr2.setStrasse("Batman Höhle 1");

System.out.println( p.getAdresse() );

In Zeile 1 holen wir uns die Adresse von Fritz. Danach setzen wir die geholte Adresse auf neue Werte. Und wir benutzen dann keine Setter-Funktion auf das Personen-Objekt! Wir haben nur die Adresse gelesen und geändert, aber nicht an Fritz neu übergeben!

In Zeile 5 geben wir die Adresse von Fritz aus.
Ausgabe:

Adresse: Batman Höhle 1, Gotham

Was ist hier passiert? Obwohl wir lediglich lesend auf die Personen-Adresse zugegriffen haben, hat sich seine neue Anschrift geändert? Sollte das nicht erst passieren, wenn wir eine definierte Schnittstelle Person.setAdresse(Adresse) benutzen? Die Blackbox, also Kapselung, wurde hier ohne spezielle Tricks umgangen. Das ist eine Katastrophe und widerspricht allen Regeln der Objektorientierung!

Beispielprojekt

App-download-manager-iconIch stelle Ihnen das komplette Beispiel von oben als Sourcecode-Projekt (Netbeans) bereit: Kapselung1.zip

 

Analyse des Vorfalls

Jetzt kommt erstmal das Warum? Warum hat die Kapselung trotz Private-Variable und Getter nicht funktioniert?

public Adresse getAdresse() {
   return m_adresse;
}

Der Grund: Wir haben eine Referenz auf das gekapselte Objekt nach draußen gegeben und teilen diese mit jemand anderem. Sprich eine Shared Reference. Es sollte hier klar sein, dass der Aufrufer fröhlich die Daten ändern kann.
Es gibt aber genug Beispiele, wo dieses nicht möglich ist:

public String getName() {
   return m_name;
}

Hier wird zwar auch eine Referenz auf das gekapselte Objekt zurück gegeben, aber bekanntlich ist ein Java-String-Objekt unveränderbar.
Anderes Beispiel:

public int getAlter() {
   return m_alter;
}

Der int-Typ ist ein nativer oder auch primitiver Value-Type, d.h. es ist keine Referenz. Aber es wird eine Kopie des gekapselten Wertes zurück gegeben. Das passiert implizit. Der Anwender kann dann zwar den Wert ändern, es handelt sich dann aber um eine Kopie!

Zusätzliche Arbeit

In allen Java-Lehrbüchern wird munter mit Strings und nativen Value-Typen hantiert und die tolle Welt der Objekteorientierung gezeigt. Aber keines der Bücher geht an wahre Praxisbeispiele ran. Und wenn, dann wird der Kapselungsfehler nicht aufgezeigt.
Wir können und müssen uns aber eines klar sein: unsere eigenen Klassen müssen erst noch fit für eine Blackbox gemacht werden. Dieses kostet aber auch Arbeit!

Ich denke, der Kern des Fehlers liegt in den „tollen“ Java Beans und den Tools, die einem die Arbeit abnehmen per Knopfdruck in der IDE die ganzen Getter und Setter automatisch zu generieren. Das ist schnell gemacht und vergessen. Aber das ist eben nicht richtig!

Lösung 1

Also, was müssen Sie tun um unser Person-Adresse-Beispiel richtig zu stellen?

Das Adresse-Objekt könnte kopiert werden! Sprich eine Defensive Copying.

Eigentlich muss das auch für die Person-Klasse u.a. gelten, aber der Übersicht wegen behandeln wir nur Adresse.

public Adresse getAdresse() {
   return new Adresse( m_adresse );
}

Hier nutzen wir einen sogenannten Copy Constructor, kurz Copy Ctor. Den kann man selbst in jeder eigenen Klasse implementieren.
Wenn es eine fremde Klasse ist und diese keinen Copy Ctor anbietet, muss man die Eigenschaften per Hand kopieren:

public Adresse getAdresse() {
   Adresse copy = new Adresse();
   copy.setOrt( m_ort );
   copy.setStrasse( m_strasse );
   return copy;
}

(Hier wäre natürlich eine zentrale Hilfsfunktion angebrachter.)
Wichtig ist also, dass der Getter nicht das interne Objekt zurück gibt. Der Benutzer des Getter kann dann zwar das erhaltene Objekt auch ändern, aber ohne Auswirkung auf das gekapselte. Der Benutzer muss dann explizit den Setter benutzen!

Person p = new Person("Fritz");

Adresse adr = new Adresse();
adr.setOrt("Entenhausen");
adr.setStrasse("Disneystrasse 1");
p.setAdresse(adr);

// Im Gegensatz zum Bsp. Kapselung1, ändert das ausgelesene Objekt
// nicht die Personen-Daten. Die Kapselung funktioniert!
Adresse adr2 = p.getAdresse();
adr2.setOrt("Gotham");
adr2.setStrasse("Batman Höhle 1");

// Wir müssen explizit die setAdresse()-Schnittstelle benutzen:
p.setAdresse(adr2);

Wenn Sie adr2 ändern und von p die Adresse ausgeben, werden Sie sehen, das es noch die alte ist. Erst mit dem erneuten Setzen in Zeile 15 wird wirklich Fritz eine neue Adresse erhalten. Top!

Kopieren geht nicht immer

Mit der Defensive Copy sind Sie schon mal einen Schritt weiter gekommen um Shared References zu vermeiden. Das Kopieren kann man immer dann problemlos machen, wenn es eigene Klassen sind, die man kennt. Denn leider ist nicht immer eine Deep Copy (tiefe Kopie) möglich, sondern nur eine Shalow Copy (flache Kopie).
Wenn ein Objekt sehr viel Speicher verbraucht oder der Kopiervorgang viel Rechenleistung kostet, dann könnte die Lösung nicht ideal sein.

Für die anderen Fälle gibt es tatsächlich noch eine weitere Lösung: Immutable!

Beispielprojekt

App-download-manager-iconIch stelle Ihnen die komplette Lösung von oben als Sourcecode-Projekt (Netbeans) bereit: Kapselung2.zip

 

Ist Ihnen aber auch etwas anderes aufgefallen? Genau, es gibt noch den umgekehrten Fall in der Setter-Methode! Nämlich das der Aufrufer eine Referenz übergibt und diese ebenfalls übernommen wird, der Aufrufer aber die gleiche Referenz natürlich weiter behält.

class Person {
  private Adresse m_adr;
...
  public void setAdresse(Adresse a) {
    // Referenz wird übernommen und geteilt:
    m_adr = a;
  }
}

Was ist die Lösung? Richtig, wie beim Getter aus Lösung 1 muss auch hier eine defensive Kopie erzeugt werden!

class Person {
  private Adresse m_adr;
...
  public void setAdresse(Adresse a) {
    // Kopie erzeugen! Keine Referenz mit dem Aufrufer teilen!
    m_adr = new Adresse(a);
  }
}

Das gleiche gilt natürlich auch für Konstruktoren, die ja wie ein Setter arbeiten.

Defensive Copying

Dieses Idiom nennt man Defensive Copying. Es dient dazu sich vor Shared References zu schützen. Denn eine Klasse soll ihre Objekteigenschaften kapseln und nicht mit anderen teilen.

Lösung 2

Gibt es einen Weg Defensive Copying zu vermeiden? Jein! Im Gegensatz zu z.B. C++ gibt es in Java keine Const Correctness. Man kann aber in Java dafür sorgen, das der Aufrufer selbst ein neues Objekt erzeugen muss, bevor er eine Eigenschaft ändern will.

Immutable Object

Wenn Sie die Klassen-Eigenschaft durch Immutable Objects ersetzen, können Sie sich viel Code und somit potenzielle Fehlerquellen sparen! Im Java API gibt es ein paar immutable Klassen die Sie regelmäßig benutzen: String, Integer, Boolean u.a.

Diese Klassen sind nicht einfach Read-Only, sondern unveränderbar (immutable). Wer sie ändern will, muß nämlich erstmal neue Objekte erstellen:

// Gehen wir davon aus, das Adresse immutable ist.
Adresse adr = new Adresse("Entenhausen");
person.setAdresse(adr);

adr.setOrt("Gotham"); // Compile-ERROR, es gibt keine Setter!

// wir müssen ein neues Objekt erstellen:
Adresse adr2 = new Adresse("Gotham");
person.setAdresse(adr2);

Das Thema Immutable Objects werde ich noch mal in einem gesonderten Weblog bearbeiten.

Beispielprojekt

App-download-manager-iconIch stelle Ihnen die komplette Lösung von oben als Sourcecode-Projekt (Netbeans) bereit: Kapselung3.zip

 

Fazit

Sobald Sie eine Eigenschaft für ein Objekt kapseln müssen, versuchen Sie eigene Immutable Klassen zu designen und zu benutzen! Wenn dieses nicht möglich ist, müssen Sie Shared References vermeiden! Nutzen Sie dann Defensive Copying! So vermeiden Sie ungewollte Seiteneffekte.