Smart Comparator

Geschrieben von Michael Biesmann & Fabian Markert

1. Einleitung
2. Was ist unsere Idee?
2.1. Wie sind wir auf die Idee gekommen?
2.2. Warum machen wir das?
3. Wie haben wir diesen Comparator umgesetzt und wofür ist er gedacht?
3.1. Wie funktioniert unser SmartComparator?
3.1.1. Die Instanziierung
3.1.2. Die Vorverarbeitung
3.1.3. Der Vergleich
3.1.4. Änderung der Sortierung
4. Wie kann unser SmartComparator eingesetzt werden?
4.1. Erzeugung per String-Array
4.2. Erzeugung per MethodNameRecord-Array
4.3. Erzeugung per MethodNameGenerator
4.4. Erzeugung per @CompareCriteria-Annotation
4.5. Erzeugung per @NamedSorts und @NamedSort Annotations
5. Haben wir getestet?
5.1. Was haben wir getestet?
5.2. Wie haben wir getestet?
5.3. Was war das Testergebnis?
5.3.1. Auswertung System 1 Linux (Oracle 1.7.0_45):
5.3.2. Auswertung System 1 Linux ( icedtea 7.2.4.3):
5.3.3. Auswertung System 1 Windows ( 1.7.0_45):
5.3.4. Auswertung System 2 Linux ( icedtea 7.2.4.3):
5.3.5. Auswertung System 2 Windows ( 1.7.0_45):
5.3.6. Auswertung System 3 ( Oracle 1.7.0_07 ):
6. Quellcode
7. Fazit und Ausblick


1. Einleitung

Wir wollen hier unser Hauptthema erörtern, auf das wir so lange hin gearbeitet haben. Wir werden uns hier folgenden Fragen stellen:

  • Was ist unsere Idee
  • Wie sind wir auf die Idee gekommen
  • Warum machen wir das
  • Wie haben wir diesen Comparator umgesetzt und wofür ist er gedacht
  • Wie funktioniert unser SmartComparator
  • Wie kann unser SmartComparator eingesetzt werden
  • Haben wir getestet
  • Was haben wir getestet
  • Wie haben wir getestet


2. Was ist unsere Idee?

Die Idee die wir hatten, besteht darin einen Comparator für alles anzubieten. Ein Comparator soll für die Sortierung aller eigenen Datenobjekte dienen. Dabei muss die Möglichkeit bestehen, die Objekte nach einem oder mehreren Attributen zu sortieren. Der Comparator sollte dabei einfach und flexibel zu benutzen sein.


2.1. Wie sind wir auf die Idee gekommen?

Nachdem wir viel programmieren und programmiert haben in der Vergangenheit, sind wir immer auf ähnlich geartete Probleme gestoßen. Eines, was sehr häufig auftritt, ist eben das Sortieren von eigenen Datenobjekten nach verschiedenen Kriterien. Zum einen traten diese Dinge zum Vorschein bei der Verarbeitung der Daten, aber auch bei der Ausgabe der Daten in Tabellen. Hier benötigt man die Funktion zum Sortieren der Tabelleninhalte recht häufig. Vor allem immer dann, wenn man dem Benutzer eine gut bedienbare GUI anbieten will.


2.2. Warum machen wir das?

Wir wollen allen anderen Entwicklern und auch uns selbst in Zukunft die Möglichkeit an die Hand geben, Software schneller und auch einfacher zu entwickeln. Außerdem kann die Umsetzung von mehreren Comparatoren für verschiedene Datenobjekte ziemlich eintönig auf Dauer werden.


3. Wie haben wir diesen Comparator umgesetzt und wofür ist er gedacht?

Die Umsetzung und das Testen gestalteten sich schwierig und umfangreich. Wir haben dafür zusammen volle 4h benötigt für unsere erste Version. ;-)

Wir fingen damit an, uns zu überlegen, welche Objekte oder Werte wir sortieren können - angefangen von den Wrapper Klassen von Java, wie Integer, Float, Boolean usw., aber auch Objekte wie Strings oder primitive Datentypen. Hier wird der erfahrene Programmierer wissen, dass die zurück gelieferten Datentypen eines Datenobjektes meist primitive Datentypen sind oder die Wrapper-Klassen davon. Natürlich besteht die Möglichkeit, eine Liste oder ein Array als Rückgabe zu erhalten. Diese Objekte sind eben das Ziel wofür ein Comparator benötigt wird und diente uns somit nicht als Ausgangspunkt für die Sortierung.


3.1. Wie funktioniert unser SmartComparator?

Der Kern besteht aus der public int compare(T t, T t1) Methode und den drei dazugehörigen private Methoden private int compare(T t, T t1, MethodNameRecord VAL), private void analyzeClassReturnTypes() und private Class castObjects(Class clazz).

3.1.1. Die Instanziierung

Man erzeugt eine SmartComparator-Instanz und übergibt ihm im Konstruktor die Klasse des zu sortierenden Objekttyps und eine Liste von Methodennamen. Bei der Liste der Methodennamen handelt es sich um Methoden, die in dem Objekttyp implementiert sind. Dieser Liste kann man eine Sortierrichtung für jede Methode übergeben.

3.1.2. Die Vorverarbeitung

Während der Instanziierung wird eine Vorverarbeitung der übergebenen Methoden und deren Sortierrichtung vorgenommen. Die Analyse startet mit der Methode analyzeClassReturnTypes. Diese Methode überprüft, ob die angegebenen Methoden existieren oder nicht. Wenn die angegebene Methode existiert, wird das Methodenobjekt, der Datentyp für die Rückgabe und die Sortierrichtung in den MethodNameRecord-Parameter übergeben. Wenn die Methode nicht existiert oder der Rückgabetyp der Methode Void ist, dann wird eine entsprechende Runtime Exception geworfen.

3.1.3. Der Vergleich

Nachdem die Instanz des SmartComparators erzeugt und die Vorverarbeitung beendet wurde, übergibt man der public compare Methode die zu vergleichenden/sortierenden Instanzen des angegebenen Objekttyps. Hier wird der Vergleich gestartet, indem die privat compare Methode gerufen wird. Dabei werden die vorher übergebenen und analysierten Methoden mit deren Datentypen für die Rückgabewerte an die Methode übergeben. Dies geschieht mit der Übergabe des MethodNameRecord-Parameters. In der private compare Methode wird dann der Vergleich für die eine übergebene Methode durchgeführt. Hierfür werden die Daten per Reflection von den Objekten geholt und über die compareTo Methode der Klassen miteinander verglichen. Der Rückgabewert der compareTo Methode wird an die public compare Methode zurückgegeben. In dieser Methode wird geprüft, ob beide Werte gleich sind oder nicht. Wenn beide Werte gleich sind, dann wird der Vergleich mit der nächsten übergebenen Methode fortgeführt. Sind die Werte unterschiedlich, wird überprüft welche Sortierrichtung angegeben wurde. Ist die Sortierrichtung Absteigend/ Descendent, wird der Rückgabewert des Vergleichs mit -1 multipliziert. Im Anschluss wird der Wert des Vergleichs zurückgegeben.

Die Reihenfolge, in der die Vergleiche durchgeführt werden, entspricht der Reihenfolge der Übergabe der Methodennamen.

3.1.4. Änderung der Sortierung

Für ein und denselben Objekttyp, der bei der Instanziierung angegeben wurde, kann die Sortierung geändert werden ohne das der SmartComparator neu erzeugt werden muss. Hierfür benutzt man eine der changeSorting Methoden. Nach dem Aufruf einer solchen Methode wird abermals die Methode analyzeClassReturnTypes aufgerufen, um die übergebenen Daten zu validieren. Wenn der Aufruf keine Runtime Exception erzeugt, kann danach mit der neuen Sortierung weiter gearbeitet werden.


4. Wie kann unser SmartComparator eingesetzt werden?

Der SmartComparator ist vielseitig einsetzbar. Man kann den SmartComparator zum Beispiel erzeugen lassen mit einer Liste von Methodennamen. Ein anderer Ansatz, den wir bevorzugen, ist der Einsatz von Annotations in der Datenklasse. Hier besteht die Möglichkeit, eine Sortierung als Standard zu definieren, mit der @CompareCriteria Annotation. Ein anderer Weg ist der, viele verschiedene Sortiermöglichkeiten anzubieten. Das kann man mit den Annotations @NamedSorts und @NamedSort realisieren. Vergleichbar sind diese beiden Annotations mit dem Ansatz der NamedQueries in Hibernate oder anderen JPA Providern. Weiter unten sind alle Möglichkeiten aufgelistet, über die man einen SmartComparator instanziieren kann.

4.1. Erzeugung per String-Array

In dieser Variante übergeben sie dem Konstruktor ein Array von Methodennamen. Das reicht aus, um den SmartComparator erzeugen zu lassen. Hierbei ist zu beachten, dass die Sortierung für jede angegebene Methode Aufsteigend/Ascendent erfolgt.

4.2. Erzeugung per MethodNameRecord-Array

Mit dieser Möglichkeit muss der Programmierer im Quellcode die MethodNameRecord Instanzen selbst erzeugen und in einem Array zusammenführen. Die Priorisierung findet hier über die Array Indizes statt. Der Index mit der geringeren Nummer hat die höhere Priorität.

4.3. Erzeugung per MethodNameGenerator

Mit dem MethodNameGenerator besteht eine einfache Alternative, ein MethodNameRecord-Array generieren zu lassen. Dabei wird dem Generator eine Liste von Methodennamen und optional auch die Sortierrichtung übergeben. Der MethodNameGenerator erzeugt daraus ein MethodNameRecord-Array, das dann nach der vorher beschriebenen Methode an den SmartComparator übergeben werden kann.

4.4. Erzeugung per @CompareCriteria-Annotation

Die @CompareCriteria Annotation kann an den Methodennamen oder den Feldern definiert werden. Weiterhin erlaubt diese Annotation die Priorisierung der Methoden oder Felder für die Sortierung. Optional ist wie immer die Angabe der Sortierrichtung in der Annotation möglich. Nach der Übergabe der Klasse an den Konstruktor wird die Klasse auf die angegebene Annotation geparst und ausgewertet. Danach ist der SmartComparator auch schon einsatzbereit.

4.5. Erzeugung per @NamedSorts und @NamedSort Annotations

Wie oben bereits erwähnt, sind diese beiden Annotations von der Deklaration und der Anwendung her mit den NamedQuery-Annotations der JPA-Provider zu vergleichen. Um eine Liste von Sortiermöglichkeiten zu definieren, wird als erstes die @NamedSorts-Annotation verwendet. In dieser Annotation kann man eine durch Komma getrennte Liste von @NamedSort-Annotations angeben. In der @NamedSort-Annotation wird als erstes der Name der Sortierung angegeben. Danach folgt eine Liste von @MethodName-Annotations. Diese Annotation erwartet als Parameter den Namen der Methode und optional die Sortierrichtung. Die Priorität, die bei der Sortierung den Methodennamen zukommt, entspricht der Reihenfolge der Aufzählung in der @NamedSort-Annotation.


5. Haben wir getestet?

Ja


5.1. Was haben wir getestet?

System 1 ist definiert als:

  • AMD A4-3300M APU mit Radeon HD Graphics AuthenticAMD
  • 8GB DDR 3 RAM

Betriebssysteme:

  • Sabayon Linux mit Kernel 3.12.0
  • Windows 8.1 Professional

JavaVM:

  • Linux: Oracle 1.7.0_45
  • Linux: icedtea-7.2.4.3
  • Windows: 1.7.0_45


System 2 ist definiert durch:

  • Intel(R) Core(TM)2 Duo CPU E8400 @ 3.00GHz
  • 8GB DDR 3 RAM

Betriebssysteme:

  • Genttoo Linux mit Kernel 3.12.0
  • Windows 8.1 Professional

JavaVM:

  • Linux: icedtea-7.2.4.3
  • Windows: 1.7.0_45


System 3 ist definiert als:

  • Intel Core i7-2630QM 2GHz 64 Bit
  • 8 GB DDR 3 RAM
  • Windows 7 64-Bit
  • JVM: 1.7.0_07

Unser Ziel ist es, unseren SmartComparator gegen einen Nativ implementierten Comparator antreten zu lassen. Wir wollen zeigen, inwieweit unser Comparator mithalten kann und für welche Datenmengen er eine hinreichende Alternative darstellt.


5.2. Wie haben wir getestet?

Wir haben hierfür eine Datenklasse und mehrere Testfälle geschrieben. An den SmartComparator wurde die Deklarierung der Methodennamen und der Sortierrichtung übergeben. Hier haben wir nicht nur den Nativen dem SmartCompararor gegenüber gestellt, wir haben auch den Vergleich primitive Variable und Wrapper-Klasse nicht gescheut. Damit wollen wir die Performanz dieses Ansatzes aufzuzeigen. Die Tests sind hierbei mehrfach durchlaufen worden mit einer Datenmenge von 10.000 durch Zufall generierten Datensätzen. Aus den gesammelten Werten der einzelnen Durchläufe wurde ein Durchschnittswert ermittelt. Auch diese Tests haben wir für ein und das gleiche Beispiel mehrfach wiederholt, weil die Zeiten von Test zu Test schwankend waren. Daher werden die Vergleiche nicht durch exakte Zeiten dargestellt, sondern durch Ratios. Durch diese Darstellung lässt sich das Laufzeitverhalten auch besser beurteilen. Abgerundet werden die Tests durch den Vergleich von Datenobjekten mit mehreren Variablen und mit einer Mischung aus Wrapper-Klassen und primitiven Datentypen.


5.3. Was war das Testergebnis?


5.3.1. Auswertung System 1 Linux (Oracle 1.7.0_45):

Typ Nativ [ms] SmartComparator [ms] Ratio [SC/Nativ]
native bool 3,45 5,29 1,53
wrapped bool 3,36 7,95 2,37
native char 6,39 17,18 2,69
wrapped char 6,58 16,58 2,52
native short 6,55 17,87 2,73
wrapped short 6,98 17,36 2,49
native int 5,59 16,19 2,90
wrapped int 7,18 16,71 2,33
native Long 6,11 17,29 2,83
wrapped Long 7,08 16,55 2,34
native Float 6,00 17,63 2,94
wrapped Float 7,34 17,44 2,38
native Double 5,84 18,33 3,14
wrapped Double 7,62 17,43 2,29
total 6,15 15,70 2,55
Typ Nativ [ms] Wrapped [ms] Ratio [Nativ/Wrapped]
bool 5,29 7,95 1,50
char 17,18 16,58 0,97
short 17,87 17,36 0,97
int 16,19 16,71 1,03
long 17,29 16,55 0,96
float 17,63 17,44 0,99
double 18,33 17,43 0,95
total 15,68 15,72 0,78


5.3.2. Auswertung System 1 Linux ( icedtea 7.2.4.3):

Typ Nativ [ms] SmartComparator [ms] Ratio [SC/Nativ]
native bool 3,31 5,41 1,63
wrapped bool 3,56 7,68 2,16
native char 6,89 16,59 2,41
wrapped char 6,81 15,38 2,26
native short 6,76 18,02 2,67
wrapped short 7,35 16,25 2,21
native int 5,61 15,53 2,77
wrapped int 7,47 16,19 2,17
native Long 6,48 16,52 2,55
wrapped Long 7,39 16,06 2,17
native Float 6,08 16,42 2,70
wrapped Float 8,23 17,31 2,10
native Double 5,90 17,07 2,89
wrapped Double 7,67 16,31 2,13
total 6,39 15,05 2,35
Typ Nativ [ms] Wrapped [ms] Ratio [Nativ/Wrapped]
bool 5,41 7,68 1,42
char 16,59 15,38 0,93
short 18,02 16,25 0,90
int 15,53 16,19 1,04
long 16,52 16,06 0,97
float 16,42 17,31 1,05
double 17,07 16,31 0,96
total 15,08 15,03 0,85


5.3.3. Auswertung System 1 Windows ( 1.7.0_45)

Typ Nativ [ms] SmartComparator [ms] Ratio [SC/Nativ]
native bool 4,78 5,92 1,24
wrapped bool 3,31 9,59 2,90
native char 10,35 24,58 2,37
wrapped char 9,13 19,42 2,13
native short 7,78 21,94 2,82
wrapped short 10,91 20,32 1,86
native int 9,06 19,48 2,15
wrapped int 10,85 19,45 1,79
native Long 8,55 16,91 1,98
wrapped Long 9,53 18,74 1,97
native Float 9,41 26,33 2,80
wrapped Float 10,92 20,83 1,91
native Double 8,45 20,29 2,40
wrapped Double 8,81 18,03 2,05
total 8,70 18,70 2,15
Typ Nativ [ms] Wrapped [ms] Ratio [Nativ/Wrapped]
bool 5,92 9,59 1,62
char 24,58 19,42 0,79
short 21,94 20,32 0,93
int 19,48 19,45 1,00
long 16,91 18,74 1,11
float 26,33 20,83 0,79
double 20,29 18,03 0,89
total 19,35 18,05 0,96


5.3.4. Auswertung System 2 Linux ( icedtea 7.2.4.3):

Typ Nativ [ms] SmartComparator [ms] Ratio [SC/Nativ]
native bool 2,58 4,55 1,76
wrapped bool 2,67 6,44 2,41
native char 4,97 14,74 2,97
wrapped char 5,52 17,26 3,13
native short 5,07 14,32 2,82
wrapped short 5,25 12,71 2,42
native int 6,53 14,86 2,28
wrapped int 5,53 12,90 2,33
native Long 5,40 12,65 2,34
wrapped Long 3,33 09,05 2,72
native Float 3,20 10,47 3,27
wrapped Float 4,10 10,40 2,54
native Double 4,35 13,72 3,15
wrapped Double 3,57 9,74 2,73
total 4,43 11,70 2,64
Typ Nativ [ms] Wrapped [ms] Ratio [Nativ/Wrapped]
bool 4,55 6,44 1,42
char 14,74 17,26 1,17
short 14,32 12,71 0,89
int 14,86 12,90 0,87
long 12,65 9,05 0,72
float 10,47 10,40 0,99
double 13,72 9,74 0,71
total 12,19 11,21 0,79


5.3.5. Auswertung System 2 Windows ( 1.7.0_45):

Typ Nativ [ms] SmartComparator [ms] Ratio [SC/Nativ]
native bool 1,09 2,52 2,31
wrapped bool 2,03 3,95 1,95
native char 3,93 9,81 2,50
wrapped char 2,95 8,45 2,86
native short 3,48 9,66 2,78
wrapped short 2,49 9,36 3,76
native int 3,96 9,04 2,28
wrapped int 3,40 8,78 2,58
native Long 4,39 10,27 2,34
wrapped Long 3,75 8,88 2,37
native Float 3,32 9,99 3,01
wrapped Float 3,58 9,05 2,53
native Double 2,99 9,99 3,34
wrapped Double 3,46 9,23 2,67
total 3,20 8,50 2,65
Typ Nativ [ms] Wrapped [ms] Ratio [Nativ/Wrapped]
bool 2,52 3,95 1,57
char 9,81 8,45 0,86
short 9,66 9,36 0,97
int 9,04 8,78 0,97
long 10,27 8,88 0,86
float 9,99 9,05 0,91
double 9,99 9,23 0,92
total 8,75 8,24 0,78


5.3.6. Auswertung System 3 ( Oracle 1.7.0_07 ):

Typ Nativ [ms] SmartComparator [ms] Ratio [SC/Nativ]
nativ bool 1,29 19,94 15,46
nativ char 5,08 37,05 7,29
nativ double 5,14 38,64 7,52
nativ float 4,98 38,82 7,80
nativ int 5,26 37,09 7,05
nativ long 6,10 38,45 6,30
nativ short 5,87 38,80 6,61
wrapped bool 1,41 16,38 11,62
wrapped char 3,59 37,71 10,50
wrapped double 4,05 37,84 9,34
wrapped float 4,16 39,14 9,41
wrapped int 3,85 38,32 9,95
wrapped long 3,94 38,50 9,77
wrapped short 3,48 36,71 10,55
total 58,2 493,39 8,48
Typ SC-Wrapped [ms] SC-Nativ [ms] Ratio [Nativ/Wrapped]
bool 16,38 19,94 1,22
char 37,71 37,05 0,98
double 37,84 38,64 1,02
float 39,14 38,82 0,99
int 38,32 37,09 0,97
long 38,50 38,45 1,00
short 36,71 38,80 1,06
total 244,60 248,79 1,01713


6. Quellcode

https://github.com/P1tt187/SmartComparator


7. Fazit und Ausblick

Der SmartComparator ermöglicht das Sortieren von Werten ohne die Programmierung eigener Comparatoren. Hier ist es nur erforderlich, zu deklarieren, nach welchen Kriterien der SmartComparator die Vergleiche vollziehen soll. Zusammengefasst gesagt, ist der SmartComparator eine Alternative für alle, denen es nicht auf jede Millisekunde ankommt.

Wir durchdenken schon die nächste Stufe des SmartComparators, um noch mehr Performance- Gewinn zu erzielen. Bis dahin wünschen wir euch erst einmal mit dieser Version viel Spaß und freuen uns über Hinweise oder Anregungen.

Wir möchten alle Leser dazu anhalten, den SmartComparator selber zu testen und uns ihre Systembeschreibung mit ihren Messdaten zukommen zu lassen. Wir werden auf einer weiterführenden Seite die Testergebnisse, Systembeschreibung und falls gewünscht - den Namen der testenden Person - veröffentlichen.


Stand: 5. November 2013