Heiner Kückerverbesserter Comparator |
|
Home Java-Seite Bit Packed Array 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. AnleitungSicher 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 ReflektionBasis 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. 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 ComparatorDer verbesserte Comparator vermeidet Copy-Paste und arbeitet effektiv bezüglich des Zugriffs auf die benötigten Properties.Die Anforderungen:
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.
FazitDieser 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
Achtung: Erweiterungen und Fixes stelle ich ohne Historie
und ohne Ankündigung hier bereit. Lizenzbedingungen:
Die Programme, Quelltexte und Dokumentationen können ohne
irgendwelche Bedingungen kostenlos verwendet werden. |