Heiner Kücker

verbesserter Comparator

Home

Java-Seite

   ASM Improved

   heterogene
   Map, HMap

   Constraint
   Code Generator

   JSP WorkFlow
   PageFlow FlowControl
   Page Flow Engine
   Web Flow Engine
   Control_and_Command

   JSP_Spreadsheet

   Code-Generator
   für Option-Either-Stil
   in Java

   verbesserter
   Comparator

   Fluent-Interface
   Code-Generator
   auf Basis
   einer Grammatik

   Visitor mit Multidispatch

   for-Schleife mit
   yield-return

   Kognitions-Maschine
   semantisches Netz

   Domain Parser

   Codegenerator_für
   hierarchische
   Datenstrukturen

   Expression_Engine
   Formula_Parser

   Thread Preprocessor

   State Transition Engine

   AspectJ

   Java_Explorer

   DBF_Library

   Kalender_Applet

   SetGetGen

   BeanSetGet

   CheckPackage

   LineNumbers

   GradDms

   Excel-Export

   StringTokenizer

   JspDoc

   JspCheck

   JSP-Schulung
   Java Server Pages
   Struts

   Ascii-Tabellen-
   Layouter

   Ascii-Baum-
   Layouter

   Ascii-Art-Fluss-
   Diagramm-
   Parser

   AsciiArt
   AssignmentMatrix
   Layouter

   StringSerial

   Silbentrennung

   JDBC_Schlüssel-
   Generierung

   bidirektional/
   unidirektional
   gelinkte Liste

   Java_Sitemap
   Generator

   XmlBuilder

   RangeMap

   StringFormatter

   VersionSafe
   XCopy

   JTextField

   CommandLine-
   ParamReader

   Bitmap-Grafik

   MultiMarkable-
   Buffered-
   InputStream

   JavaCache

   JdomUtil

   CollectionUtil

   XML Really
   Pull Parser

   Log-Filter

   Remote-Protokoll

   Sudoku-Generator

   Delegation statt
   Mehrfachvererbung

   Disjunct
   Interval Set

   WebCam_Demo

   Weiterentwicklung_Java

Alaska-XBase++-Seite

Projekte

Philosophien
Techniken


Konzepte

Sudoku

Kontakt /
Impressum


Links

SiteMap





Letzte Aktualisierung:
21.09.2014

verbesserter Comparator

Über diesen Comparator gibt es einen Artikel in Java aktuell 2014-04.

Den Artikel und die entsprechenden Beispiel-Code habe ich bereits im Februar 2014 geschrieben. Dabei habe ich die neuen Möglichkeiten von Java Version 8 nicht eingearbeitet. Entstanden ist die Idee zu dieser Lösung 2013 in einem Projekt, welches noch Java Version 6 verwendete. Der Beispiel-Code ist für Java Version 6 ausgelegt. Ich nehme an, dass noch nicht alle Projekte Java Version 8 verwenden, für diese ist die hier beschriebene Lösung sinnvoll anwendbar.

Die Verwendung eines reflection-freien Getters (Property-Accessor) kann der Copy-Paste-Anteil für einen Comparator verringert werden.

Durch die Verwendung von Generics(Type-Parameter) wird Flüchtigkeitsfehlern vorgebeugt.

Anleitung

Sicher hat jeder Java-Programmierer schon mal so einen Comparator geschrieben:

    Comparator<Customer> comparator =
            new Comparator<>() {
                @Override
                public int compare(
                        final Customer c1 ,
                        final Customer c2 ) {
                    return c1.getSurName().compareTo(
                            c2.getSurName() );
                }
            };
Wenn nach einer anderen Property(Member) unserer Bean sortiert werden soll, muss der gesamte Code kopiert und die abzufragenden Properties angepasst werden.

Gibt es mehrere Beans, müssen ausserdem die beiden Parameter-Typen der compare-Methode geändert werden, also bereits vier Code-Stellen.

Eine langweilige und fehleranfällige Angelegenheit.

Getter ohne Reflektion

Basis meines Alternativ-Vorschlages ist ein Property-Accessor, der ohne Reflektion auskommt, in diesem Fall nur der lesende Anteil, der Getter:
    interface Getter<B, P> {
        P get( B bean );
    }
Die beiden Typ-Parameter(Generics) B(Bean) und P(Property) bestimmen die jeweils beteiligten Typen. Der Sinn der get-Methode sollte klar sein.

Dieses Interface implementieren wir für alle benötigten Properties(Member):

    public static final Getter<Customer, String> PRE_NAME_GETTER =
            new Getter<Customer, String>()
            {
                @Override
                public String get( Customer bean )
                {
                    return bean.getPreName();
                }
            };
Die Getter-Objekte sind statisch, existieren also nur einmal je JVM.

Durch die Typ-Parameter sind Namensverwechslungen und Typ-Fehler wie bei der Benutzung von Reflection ausgeschlossen.

Die Laufzeit ist sicher auch ok, wahrscheinlich besser als Reflektion.

Unser Getter-Objekt übergeben wir nun dem verbesserten wiederverwendbaren Comparator(Code der Java-Klasse mit Member und Initialisierung im Konstruktor hier weggelassen):

    new AccessorComparator( Customer.SUR_NAME_GETTER );
Jetzt müssen wir nur noch einen Wert ändern. Ok, die Getter-Objekte müssen auch vorhanden sein, aber die kann man noch an anderen Codestellen verwenden.

So ungefähr sieht die compare-Methode des verbesserten Comparators aus:

    @Override
    public int compare(
            final T o1 ,
            final T o2 ) {
        return this.getter.get( o1 ).compareTo(
                this.getter.get( o2 ) );
    }
Ein weiterer Wunsch ist die null-Sicherheit, null-Werte sind nicht vergleichbar, sie verstossen gegen die totale Ordnung, in realen Applikationen tauchen sie aber oft auf.

Hier wird davon ausgegangen, dass null kleiner als nicht-null ist:

    P p1 = getter.get( o1 );
    P p2 = getter.get( o2 );
    if ( p1 == null  p2 == null ) {
        return 0;
    } else if ( p1 == null ) {
        return -1;
    } else if ( p2 == null ) {
        return 1;
    } else
        ...
Kaum ist dieses Problem gelöst, müssen wir nach mehreren Properties in einer definierten Rangfolge sortieren.

Wir übergeben die Getter-Objekte als Array mit einem vararg-Parameter oder dynamisch als Liste:

    for ( Getter g : getters ) {
        int compareResult =
            getter.get( o1 ).compareTo(
                getter.get( o2 );
        if ( compareResult != 0 ) {
            return compareResult;
        }
    }
    return 0;
Diese Lösung ist effektiv, der Aufruf der compareTo-Methode und damit der Zugriff auf die Properties erfolgt nur wenn unbedingt nötig, dies könnte aufwändig sein, im Hibernate-/JPA-Umfeld könnte das Abfragen der Properties eine Datenbank-Abfrage auslösen.

Nun kommt noch jemand daher und möchte nach bestimmten Properties absteigend sortieren.
Ein Problem dabei ist, dass wir nicht wie mit Collections.reverseOrder(Comparator) die Sortier-Richtung für eine einzelne Property umkehren können.
Aber das bekommen wir auch noch hin:

    Comparator<Customer> comparator =
            new AccessorComparator<>(
                    invert(
                       Customer.SUR_NAME_GETTER ) );
Die Methode invert ist eine statische Methode des AccessorComparator, in obigen Code-Schnipsel per statischem Import ohne Klassen-Prefix notierbar.

Sie liefert ein spezielles magisches Objekt mit dem originalen Getter als Member:

    public static <B, P extends Comparable<P>> Getter<B, P> invert(
            final Getter<B, P> getterToInvert )
    {
        return new InvertGetter<>( getterToInvert );
    }
Die Klasse InvertGetter ist private im AccessorComparator versteckt.

Beim Sortieren wird per instanceOf geprüft, ob der magische Getter auftaucht:

        if ( getter instanceof InvertGetter )
            // absteigende Sortierung
        {
            final InvertGetter<B, ?> invertGetter = (InvertGetter<B, ?>) getter;
            p1 = (P) invertGetter.getterToInvert.get( o1 );
            p2 = (P) invertGetter.getterToInvert.get( o2 );
        }
Das ist nicht besonders schön, bleibt aber unter dem API unseres AccessorComparator verborgen.

Dynamisch kann man die Sortier-Properties festlegen, indem man diese in einer Liste sammelt und die Liste dann per toArray-Methode in ein Array für den vararg-Parameter umwandelt.

Für die Benutzung gibt es keine Einschränkungen, jeder kann mit dem Code alles tun, zum Beispiel diesen in sein Projekt kopieren und das Package entsprechend anpassen.

In einem weiteren Package ist ein Beispiel für einen PropertyComparator abgelegt, von dem es auch eine NullIsLesserPropertyComparator-Implementierung zum Behandeln von null-Werten gibt und welcher wie beim klassischen Comparator üblich durch die Methode Collections.reverseOrder() umgedreht werden kann. Schliesslich ist eine andere Lösung immer nur ein paar Refactoring-Schritte entfernt.

Warum ist dies nur der 'fast' perfekte Comparator

Der verbesserte Comparator vermeidet Copy-Paste und arbeitet effektiv bezüglich des Zugriffs auf die benötigten Properties.

Die Anforderungen:

  • typsicher
  • effizient
  • multi-Property/Field-fähig
  • null-sicher
  • Property-individuell aufsteigend-absteigend sortierbar
wurden erfüllt.

Aber die Lösung zum Umkehren der Sortier-Richtung mit instanceOf hat den Geschmack der Unsauberkeit im Sinne der OOP, mir ist aber keine bessere Lösung eingefallen. Wer eine bessere Idee hat, kann sich gern bei mir melden.

Das Erstellen der Property-Accessoren ist erst mal ein höherer Aufwand, der sich aber bei entsprechend häufiger Benutzung auszahlt.

Durch die Verwendung von Typ-Parametern(Generics) in der Getter-Klasse müssen primitive Properties in die jeweiligen Wrapper-Objekte verpackt werden.

Bei der Arbeit mit varag-Parametern wird jedesmal ein Array erzeugt, Konstruktoren mit unterschiedlichen Parameter-Listen könnten effektiver sein.

Mit den Java8-Lambdas ergeben sich eventuell neue Möglichkeiten, die ich hier nicht berücksichtigt habe.

Fazit

Dieser Comparator kann sicher kein Projekt retten, aber die zugrundeliegende Philosophie, eine ausdrucksstarke und der Problematik angemessene Lösung zu suchen, tut sicher jedem Projekt gut.

Ich beobachte oft, dass etliche Frameworks oder Tools alle möglichen Probleme lösen sollen.

Das ist wie mit den Dampfmaschinen in der Anfangszeit der Industrialisierung, je grösser, je besser, meine verbraucht x-mal soviel Kohle wie deine.

Das einfache Programmierhandwerk wird nicht geschätzt und wenn, dann werden oft Prinzipien wie Clean-Code oder TDD pauschal bis zur Umkehrung ihres Sinns betrieben.

Quellen/Links

[1] http://www.nosid.org/java-compound-comparator.html



Download der Quelldateien COMPARATOR_WITH_PROPERTY_ACCESSOR.zip

Installation:

Entpacken in Verzeichnis Ihrer Wahl und Import als Eclipse-Projekt

Start mit den enthaltenen JUnit4-Tests.

Achtung: Erweiterungen und Fixes stelle ich ohne Historie und ohne Ankündigung hier bereit.
Deshalb am besten immer die letzte Version runterladen.

Lizenzbedingungen:

Die Programme, Quelltexte und Dokumentationen können ohne irgendwelche Bedingungen kostenlos verwendet werden.
Sie sind Freeware und Open Source. Für Fehler und Folgen wird keinerlei Haftung übernommen.

Hinweise zur Fehlerbeseitigung und Verbesserung sind mir willkommen.

Ich freue mich auch über Feedback bezüglich der erfolgreichen Verwendung meiner Sourcen.

Bei Fragen helfe ich gern mit Hinweisen oder zusätzlicher Dokumentation, falls ich dafür Zeit habe.