Gedanken zur Verwendung von Collections

Gestern bin ich über Code gestolpert wie den folgenden:

if (coll != null) {
   for (Item item : coll) {
      // do something
   }
}

Dieses Pattern hat sich sehr oft wiederholt. Ich fragte mich: “Warum kann diese Collection überhaupt null sein?” – Die lapidare Antwort steht in der Felddefinition:

private Collection<Item> coll;

Das Feld wird auch später im Zuge der Instantiierung nirgendwo initialisiert.
Klar, das ist per se erst mal nicht schlimm. Dann teste ich eben vor jedem Zugriff auf null und alles funktioniert wunderbar.

STOP -> Falsche (implizite) Frage! (“Was ist schlimm daran?”)

Diese Frage kann man natürlich auch beantworten, aber besser ist folgende Frage: “Was bringt das Nicht-Initialisieren?” Antwort: “Weniger Speicherverbrauch.”

STOP -> Ich frage mich, ob ich ein Hörgerät brauche. Hat mein Gegenüber tatsächlich mit dem Speicherplatz argumentiert? In einer Software, die via SAX XML parst und Daten in eine Datenbank schreibt, von dort wieder ausliest und einen DOM baut, der dann als File geschrieben wird?

Was also bringt das Nicht-Initialisieren einer Collection? – Nichts außer unübersichtlichem Code und Fehlermöglichkeiten. Daher sollten Collections immer mit einer geeigneten leeren Collection vorbelegt werden.

Für mich gibt es keinen semantischen Unterschied zwischen einer leeren Collection und null.
Wenn nichts drin ist, ist nichts drin. Ich brauche keine unterschiedlichen “Nichtse”.

Gibt’s da noch mehr zu sagen? -> “JA!”

Wieso ist die Collection eigentlich als Collection deklariert und nicht als geeigneter Subtyp? – Weiß der Entwickler nicht, was er will?
Eine erste Durchsicht des Codes zeigt, dass die Collection immer als ArrayList instanziiert wird. Dann ist es ratsam, das Feld als List<Item> zu deklarieren. Immerhin kann List mehr als Collection.
Ich brauche wohl nicht zu sagen, dass schon beim weiteren Durchforsten des Codes Stellen zu Tage gefördert wurden, in denen das Feld nicht auf null geprüft wurde…

Wir haben also dann die Initialisierung vorgenommen und alle null-Prüfungen entfernt. Auf die Umwandlung von Collection nach List haben wir zunächst verzichtet:

private Collection<Item>; coll = new ArrayList<Item>();

Außerdem waren wir in der glücklichen Situation, dass wir einige UnitTests hatten. Und siehe da: NullPointerException beim Zugriff auf coll! – Ich war leicht geschockt – oder besser: tat so.

Die Ursache lag in folgender Methode:

public void setItems(List<Item> items) {
   coll = items;
}

Sieht harmlos aus, aber hierdurch kann coll auf null gesetzt werden. Also flugs alle Sender auf diese Methode gesucht und dort dann folgendes gefunden (list ist hier eine lokale Variable; fragt bitte nicht nach dem Rest der Methode…):

if (list == null || list.isEmpty()) {
   thing.setItems(null);
}

Hier kommt uns also das null wieder durch die Hintertür rein. Als ersten Verbesserungsschritt sorgen wir also dafür, dass list immer initialisiert ist. Das ist bei einer lokalen Variablen trivial. Und der Code wird kleiner:

thing.setItems(list);

Andere Aufrufer haben wir ähnlich geändert.

Ich frage mich immer wieder: “Wer zum Teufel braucht einen setter auf Collection-Felder?”
Was ist die Alternative?
Die Alternative ist einleuchtend:
Das Collection-Feld ist ein Implementierungsdetail, das eine gewisse Anzahl von Elementen verwaltet. Man möchte ggf. Elemente hinzufügen oder entfernen. Über das tatsächliche Durchführen muss aber der Besitzer dieser Elemente die Kontrolle behalten. Daher:

public void addItem(Item item) {
   coll.add(item);
}

public void removeItem(Item item) {
   coll.remove(item);
}

Die Methoden braucht man natürlich nur, wenn man die Operationen auch benötigt.

Zum Schluss: ZUGRIFF

Damit die beiden oben genannten Methoden (add/remove) auch nicht durch die Hintertür ausgetrickst werden können, darf natürlich nicht die Collection zur Veränderung nach außen gegeben werden. Dies kann (nicht prüfbar) durch Konvention erreicht werden, oder aber (durch den Compiler prüfbar) durch Rückgabe als unveränderbare Collection:

public List<Item> getColl() {
   return Collections.unmodifiableList(coll);
}

Fazit:

Collections in Objekten brauchen Schutz und Zuneigung:

  • Sie dürfen nie null sein oder werden
  • Das umgebende Objekt ist für sie verantwortlich, daher dürfen ändernde Zugriffe ausschließlich nur durch dieses Objekt vorgenommen werden

Dadurch erreicht man:

  • Klaren Code, da zahlreiche Fehlersituationen nicht mehr möglich sind (Reflection-Aufrufe ausgenommen…)
  • Bessere Nachvollziehbarkeit der Modifikationsstellen

Ich hoffe, diese Ausführungen waren verständlich, einleuchtend und unterhaltsam.

P.S.: Ich suche immer noch jemanden, der mich von meiner hier kundgetanen Meinung abbringen will.

4 thoughts on “Gedanken zur Verwendung von Collections

  1. Von dieser Meinung werde ich dich nicht abbringen wollen, ich teile sie nämlich.

    Das einzige Problem, was ich noch sehe, ist das Herausgeben der Collection als “unmodifiable” List. Der Klient des Objektes sieht nur eine List, kann entsprechend nicht wissen, dass sie unmodifiable ist.
    Wenn er nun “doof” genug ist, die Liste modifizieren zu wollen, wird er unwissentlich in eine Exception laufen. Er müsste also dieses Implementierungsdetail kennen, was auch wieder die Kapselung verletzt.

    Die Lösung, einen Iterator nach Außen zu geben, ist aber auch nicht sonderlich schön, weil dann ggf. die Elemente wieder einzeln in eine neue Liste eingefügt werden müssen, die dann weiterverarbeitet wird.

  2. Hallo Florian,
    Du bringst das schön auf den Punkt. Allerdings halte ich die Tatsache, dass eine herausgegebene Collection unveränderbar ist nicht für ein Implementierungsdetail, sondern eine Selbstverständlichkeit. Ist also eher als Workaround um die technischen Details von Java zu verstehen. Die Exception per se fliegt dann natürlich zur Laufzeit. Aber wenn der Entwickler das ganze testet, wird es es vor Produktivgang herausfinden, da bin ich zuversichtlich.

  3. Selbstverständlich gibt es einen Unterschied zwischen einer leeren Collection, und einer Collection, die nicht da ist. Beispielsweise ist es durchaus gängig, beim Einsatz von lazy-loading-Techniken irgendwelche Objekte — darunter eben auch Collections — mit null zu initialisieren und sie ggf. dann zu laden, wenn sie benötigt werden. Und da ist es eben wichtig, zwischen null und einer leeren Collection zu unterscheiden.

    Weiter geht es dann mit der Behauptung, dass man das betreffende Feld als List deklarieren solle, weil eine List ja schließlich mehr könne als eine Collection. Ach… und wieso dann nicht gleich als ArrayList deklarieren? Die kann schließlich noch mehr als eine List… Nun, ganz einfach: weil schon eine Deklaration als List dämlich ist. Die zusätzliche Funktionalität einer List gegenüber einer Collection wurde ja bisher offenbar gar nicht gebraucht, und mit einer Deklaration als List schränkt man sich unnötig für die Zukunft ein. Was, wenn sich eine ArrayList als suboptimal herausstellt und man stattdessen ein HashSet verwenden will? Mit einer Collection kein Problem, mit List dagegen hat man ein Problem.

    Erstaunlich auch, dass der Autor offenbar nicht weiß, wofür genau der Compiler zuständig ist und wofür nicht. Denn es ist keineswegs der Compiler, der den Versuch verhindert, eine “unmodifiableList” zu modifizieren, sondern die entsprechende Wrapperklasse (was allerdings nicht am Compiler, sondern am schlechten Design der Java-Collection-Klassen liegt), und das auch erst zur Laufzeit.
    Umso interessanter ist es, dass man einige Modifikationen tatsächlich durch den Compiler unterbinden lassen kann, wofür man allerdings das Java-Typsystem verstanden haben muss: man gibt eine Collection statt einer Collection zurück. Das unterbindet zur Compilezeit zumindest die Operationen, die neue Elemente einfügen (add, addAll, und für List auch set).

  4. Ah, die Blogsoftware ist offenbar nicht in der Lage, < und > korrekt zu escapen. Gemeint war: man soll eine Collection<? extends Item> statt einer Collection<Item>.

Leave a comment