5 Beispiele für Code-Duplizierung in der Java-Standardbibliothek

von Hubert Schmid vom 2013-03-24

Große Mengen duplizierten Codes in einem Projekt sind ein Indiz für Qualitätsmängel, die hohe Wartungskosten verursachen. Die Gründe für Code-Duplizierung sind vielfältig. Häufig wird sie durch Entwicklungsprozesse begünstigt, wenn kurzfristige Ziele im Vordergrund stehen. Aber auch Schwächen der eingesetzten Sprachen und Werkzeuge können ausschlaggebend sein. Um dafür ein besseres Verständnis zu entwickeln, habe ich mir angeschaut, wo in der Standardbibliothek von Java 7 Code in größerem Umfang dupliziert ist. Auf fünf Beispiele gehe ich an dieser Stelle ein.

AbstractQueuedSynchronizer und AbstractQueuedLongSynchronizer

Die Klasse java.util.concurrent.locks.AbstractQueuedLongSynchronizer ist seit Java 6 offizieller Bestandteil der Standardbibliothek. Dabei ist die rund 2 000 Zeilen umfassende Implementierung praktisch identisch mit der bereits früher eingeführten Klasse java.util.concurrent.locks.AbstractQueuedSynchronizer. Die wenigen Unterschiede verteilen sich über die gesamte Länge. In den meisten Fällen wurde lediglich int durch long ersetzt und der Klassenname angepasst.

Die Code-Duplizierung ist auf die mangelnde Unterstützung generischer Klassen zurückzuführen – beziehungsweise auf die fehlende Möglichkeit der Parametrisierung mit primitiven Typen. Deswegen wurde der Code bewusst exakt und mit allen Einrückungen und Kommentaren kopiert, so dass Änderungen vergleichsweise einfach in die jeweils andere Klasse übertragen werden können. Ein Kommentar am Anfang der Datei beschreibt dieses Vorgehen:

To keep sources in sync, the remainder of this source file is exactly cloned from AbstractQueuedSynchronizer, replacing class name and changing ints related with sync state to longs. Please keep it that way.

Dieses Vorgehen funktioniert natürlich nur solange wie sich Entwickler an solche Hinweise halten. Aktuell sind die Dateien zumindest noch bis auf die erwähnten Änderungen synchron – und das obwohl es bereits Änderungen seit ihrer Einführung gab.

TimSort und ComparableTimSort

Die beiden Klassen java.util.TimSort und java.util.ComparableTimSort sind jeweils ungefähr 900 Zeilen lang. Beide implementieren den gleichen Sortieralgorithmus für Arrays. Dabei vergleicht Erstere die zu sortierenden Objekte mit einem java.util.Comparator wohingegen Letztere erwartet, dass die Objekte das Interface java.lang.Comparable für den Vergleich implementieren.

Der Code beider Implementierungen ist weitgehend identisch, und ein Kommentar am Anfang der Klasse ComparableTimSort weist auf diesen Umstand hin: This is a near duplicate of TimSort, modified for use with arrays of objects that implement Comparable, instead of using explicit comparators. Ein weiterer Kommentar lässt Rückschlüsse auf die Gründe der Code-Duplizierung zu: If you are using an optimizing VM, you may find that ComparableTimSort offers no performance benefit over TimSort in conjunction with a comparator that simply returns ((Comparable)first).compareTo(Second).

Das ist in gewisser Weise absurd: ComparableTimSort ist eine Optimierung für den Fall, dass man eine nicht-optimierende Laufzeitumgebung einsetzt. Dieses Unding manifestiert sich in rund 850 Zeilen duplizierten Codes mit 50 verteilten Änderungen, von denen mindestens drei unbeabsichtigt sind.

AbstractMap: SimpleEntry und SimpleImmutableEntry

Die Klasse java.util.AbstractMap enthält zwei innere Klassen, die beide die Schnittstelle Map.Entry implementieren: SimpleEntry und SimpleImmutableEntry. Der wesentliche Unterschied ist, dass die Methode setValue in Letzterer eine UnsupportedOperationException wirft. Um das zu erreichen wurden 130 Zeilen einschließlich Kommentaren weitgehend unverändert kopiert.

Mit einer abstrakten Basisklasse könnte man diese Redundanz einfach auflösen. Dagegen spricht allerdings, dass diese Implementierungsentscheidung an der Schnittstelle sichtbar wird. Java fehlt an dieser Stelle leider ein Mechanismus, der eine bessere Trennung zwischen öffentlicher Schnittstelle und Implementierung (Kapselung) ermöglicht, sowie die Sprachunterstützung für unveränderliche Sichten auf Objekte, wie es sie beispielsweise in C++ gibt.

Set und CopyOnWriteArraySet

Die Schnittstelle java.util.Set ist 386 Zeilen lang und besteht vorwiegend aus Javadoc-Kommentaren zur Spezifikation. Die Klasse java.util.concurrent.CopyOnWriteArraySet implementiert diese Schnittstelle indirekt über AbstractSet und enthält ebenfalls die ursprünglichen Javadoc-Kommentare. Wie in einem solchen Fall nicht anders zu erwarten sind die Kommentare mittlerweile inkonsistent. Anscheinend wurden viele Verbesserungen und Ergänzungen der Dokumentation von Set bei der Weiterentwicklung nicht übernommen.

Diese Redundanz ist absolut unnötig: Die Javadoc-Tools sind hinreichend ausgereift und können automatisch die Dokumentation der Schnittstelle übernehmen. An den meisten Stellen der Standardbibliothek wird davon auch Gebrauch gemacht. Warum das an dieser Stelle nicht passierte, erschließt sich mir nicht.

GregorianCalendar und JapaneseImperialCalendar

Die Klasse java.util.GregorianCalendar existiert bereits seit Java 1.1 und konkretisiert die ebenfalls in diesem Zuge eingeführte Basisklasse java.util.Calendar. Seit Java 6 gibt es mit java.util.JapaneseImperialCalendar eine zweite Ableitung. Die beiden konkreten Klassen sind 3 200 beziehungsweise 2 400 Zeilen lang. Der Größenunterschied sollte jedoch nicht darüber hinweg täuschen, dass sich Letztere zu über 80 Prozent in Ersterer wiederfindet – allerdings unterbrochen durch mehr als 200 verteilte Unterschiede.

Im Gegensatz zu den vorherigen vier Beispielen ist in diesem Fall die Duplizierung tatsächlich fachlich begründet. Beide Klassen weisen aufgrund ihrer Anforderungen eine hohe Komplexität und eine starke Ähnlichkeit auf. Auch ein Blick in den Code offenbart keine einfache Lösung für die Auflösung der Redundanz. Sicherlich könnte man mit einem anderen Schnitt der Funktionalität einiges erreichen. Doch andererseits muss man in diesem Fall auch einfach akzeptieren, dass die erste Klasse bereits zehn Jahre länger existiert und Änderungen der Schnittstelle in der Regel unerwünscht sind.

Fazit

Die fünf Beispiele zeigen typische Fälle von Code-Duplizierung – von nachlässiger Entwicklung über Schwächen der Sprache bis zu fachlichen Herausforderungen. Dabei muss man der Standardbibliothek allerdings auch zugute halten, dass die Stellen explizit dokumentiert sind, und dass hohe Erwartungen bezüglich stabilen Schnittstellen an sie gestellt werden. Typische Anwendungen weisen im Unterschied dazu vermutlich einen deutlich höheren Anteil an Duplizierung auf, der auf den dahinter liegenden Projektdruck zurückzuführen ist.