Class GeneralComparator
- java.lang.Object
-
- com.gentlyweb.utils.GeneralComparator
-
- All Implemented Interfaces:
Serializable
,Comparator
public class GeneralComparator extends Object implements Comparator, Serializable
A general purpose comparator designed to compare any pair of heterogeneous objects.Skip to bottom of description.
Description Contents:
- Basic Operation
- Ascending/Descending Comparison
- Comparing values
- Multi field sorting
- Thread safety and reuse
- Efficiency
- Examples
Basic Operation
This class works by you specify a number of fields that are then used to access into each of the objects. The term field is a bit mis-leading since you can specify either a field name within a class or a zero argument method name.
When specifying a field you can use dot notation to acess into return types/ field types.
For instance if you have the following class structure:public class A { public B = new B (); } public class B { public C = new C (); } public class C { String d = ""; }
Then to perform comparison between between 2 objects of type A on the d field in Class C you would specify a field of: B.C.d. Notice that you don't specify the current class, all fields are taken relative to the current type.
Similarly, if class C actually looked like:public class C { private String d = ""; public String getD () { return d; } }
Then you would could use the same field since if a public field with the specified name cannot be found then it is converted to a method name instead, following JavaBeans conventions. So the d is converted to getD and a public method is looked for (with no arguments) with that name. Finally if a get* method cannot be found then a method with just the name, in this case d, is searched for that takes no arguments. (Note: this has been added because there are a number of cases where the standard java.*.* classes DO NOT follow the JavaBeans conventions but you would want to use a GeneralComparator).
You can override the method name conversion by using the actual method name (in case your method doesn't follow the JavaBeans convention), so if C looked like:public class C { private d = ""; public String getMyDField () { return d; } }
You would then use a field of: B.C.getMyDField in which case the method getMyDField would be looked for instead of looking for field.
Ascending/Descending Comparison
When you add a field you specify whether you want the comparison to occur in ascending or descending order. Setting to have a descending search just means that the result gained from comparing values is reversed.
Comparing values
If a field value equates to a public Java field in the object then we call:
java.lang.reflect.Field.get(Object)
on each of the objects passed in and then if the type of the field implementsComparable
then we call:Comparable.compareTo(Object,Object)
to get the appropriate value. If the type of the field does NOT implementComparable
then we call toString on the object returned and then callString.compareTo(Object,Object)
for the value to return instead.
Similarly we do the same when the field value equates to a public Java method of the object. We calljava.lang.reflect.Method.invoke(Object,Object[])
with a zero-length argument list and then if the return type of the method implementsComparable
we callComparable.compareTo(Object,Object)
with the result of the method calls to get our value. Otherwise we call toString on the returned values and then callString.compareTo(Object,Object)
to get the value.
The upshot of all this is that you can sort a list of objects on values returned from method calls or by fields that may be X levels deep within an Object WITHOUT having to implement a complex version of the Comparable interface yourself.Multi field sorting
You can specify as many fields as you want to compare on, when the comparison is made (via the {@ #compare(Object,Object)} method) we only compare on as many fields as we need to, so if the result of the first field comparison indicates that the values are different we just return since there is no point doing further comparing, other fields will have no effect. Only if the values are equal do we move onto "lower" fields.
Thread safety and reuse
This class is NOT Thread safe and never will be (or should be...) since you would not want other threads modifying the fields you compare on. Once configured however the comparator is perfectly reusable since the only data it holds relates to the fields/methods that are accessed. If you do plan to use across multiple Threads then you need some kind of external synchronization, for example:
public synchronized void sortObjects (List objs) { Collections.sort (objs, myGeneralComparator); }
Efficiency
To try and access into the Object structure we use a
Getter
which is basically a List of Field and Methods that should be accessed/invoked when trying to find the value we require. Since we are reliant on reflection for this the key factor is the cost of traversing down the accessor chain (so the size of it will be important). The accessor chain is gained when you call one of the add*** methods so the finding of the method/field is a one-off.Warning on Getters
The Getter class supports the [ ] notation for accessing Maps and Lists however in terms of comparisons and sorting this means little and should NOT be used!!! This is not checked because there may be situations where you would want to do it, however you would need to ensure that your Lists/Maps contain heterogeneous Objects.
JDOM Support
This class is capable of initing itself from a JDOM Element and also to save it's current state into a JDOM element. This is primarily to support the
ConfigList
andConfigMap
objects, however it can be used in other places when you may want to keep the state of the comparator.Format
When
#getAsJDOMElement()
is called it will return a JDOM Element in the form given below (conversely, the constructor#GeneralComparator(Element)
expects the passed in JDOM element to have the same format):lt;comparator class="[[NAME OF CLASS THAT IS BEING COMPARED]]"> lt;field id="[[ACCESSOR VALUE INTO THE OBJECT]]" type="either ASC or DESC" /> lt;!-- There can be X number of fields, the minimum is 1. --> lt;/>
Examples
Say we wanted to sort a number of
Property
objects.// Create a new GeneralComparator. GeneralComparator g = new GeneralComparator (
Property
.class); // Now we want to sort them on type first, then id then description, then // value. g.addField
("type", GeneralComparator.ASC); g.addField
("getID", GeneralComparator.ASC); g.addField
("description", GeneralComparator.ASC); g.addField
("value", GeneralComparator.ASC); // Then (by magic...) get our collection of Properties that can // be sorted. List properties = Helper.getProperties (); // Now sort them using our comparator. Collections.sort (properties, g);We could always set any of the fields to be a descending sort. Notice here that we use "getID" since that method name is not using the correct JavaBeans convention. Also,
Property
implements theComparable
interface but using the comparator will override it.
Or if we wanted to sort a slice of messages from aLogger
:GeneralComparator g = new GeneralComparator (Logger.Message.class); // Sort on the time, but this time sort on the day of the Date // this is a deprecated method but it's not a biggie. We want // them in reverse order. g.addField ("time.day", GeneralComparator.DESC); // Get our slice... long time = System.currentTimeMillis (); Date now = new Date (time); Date oneDayAgo = new Date (time - (24*60*60*1000)); int types = Logger.Message.INFORMATION || Logger.Message.ERROR; List messages = myLogger.getMessages (now, oneDayAgo, types); Collections.sort (messages, g); // We could perform another sort but this time, sorting first // on the type, this should be in ascending order, i.e. ERROR first... g.addFieldBefore ("type", GeneralComparator.ASC, "time.day"); Collections.sort (messages, g);
A quite common need is to sort the entries in a Map rather than the keys but still maintain the key/value relationship. To do so use the code below:
// Get all the entries in the Map as a List. List l = new ArrayList (); l.addAll (myMap.entrySet ()); // Create a GeneralComparator. It is also possible to use the specific // implementation of the Map.Entry interface for the specific Map but // this way keeps it generic. GeneralCompartor gc = new GeneralComparator (Map.Entry.class); // Specify the "value" (i.e. getValue method). gc.addField ("value", GeneralComparator.DESC); // Sort. Collections.sort (l, gc);
- See Also:
- Serialized Form
-
-
Nested Class Summary
Nested Classes Modifier and Type Class Description class
GeneralComparator.XMLConstants
-
Constructor Summary
Constructors Constructor Description GeneralComparator(Class c)
Create a new GeneralComparator using the data held in the JDOM element.
-
Method Summary
All Methods Instance Methods Concrete Methods Modifier and Type Method Description void
addField(Getter field, String type)
void
addField(String field, String type)
Add a field that we sort on, if you readd the same field then the type is just updated.void
addFieldAfter(String field, String type, String ref)
Add a new field in AFTER the named field, if we don't have the named field then we just calladdField(String,String)
which will add the field in after all the others.void
addFieldAtIndex(String field, String type, int index)
Add a new field in at the specified index.void
addFieldBefore(String field, String type, String ref)
Add a new field in BEFORE the named field, if we don't have the named field then we just calladdField(String,String)
which will add the field in after all the others.int
compare(Object obj1, Object obj2)
Implement the {@link Comparator.compare(Object,Object)} method.boolean
equals(Object obj)
Implement the {@link Comparator.equals(Object)} method.Class
getCompareClass()
int
getCount()
protected List
getFields()
Return a List of GeneralComparator.SortField objects, this is used in theequals(Object)
method.void
removeField(String field)
Remove a field that we sort on.-
Methods inherited from class java.lang.Object
clone, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
-
Methods inherited from interface java.util.Comparator
reversed, thenComparing, thenComparing, thenComparing, thenComparingDouble, thenComparingInt, thenComparingLong
-
-
-
-
Field Detail
-
count
public int count
-
ASC
public static final String ASC
Use to indicate that a field should be sorted in ascending order.- See Also:
- Constant Field Values
-
DESC
public static final String DESC
Use to indicate that a field should be sorted in descending order.- See Also:
- Constant Field Values
-
-
Constructor Detail
-
GeneralComparator
public GeneralComparator(Class c)
Create a new GeneralComparator using the data held in the JDOM element.- Parameters:
root
- The root JDOM element.- Throws:
org.jdom.JDOMException
- If the format is incorrect.ChainException
- If we can't load the class that we need.IllegalArgumentException
- If the field is invalid.
-
-
Method Detail
-
getCompareClass
public Class getCompareClass()
-
addField
public void addField(Getter field, String type) throws IllegalArgumentException
- Throws:
IllegalArgumentException
-
addFieldAtIndex
public void addFieldAtIndex(String field, String type, int index) throws IllegalArgumentException
Add a new field in at the specified index. Remember that indices start at 0 and proceed in asceding order. If the index specified is <0 then we add the field in at 0, moving everything else down by 1. If the index specified is >(fields.length - 1) then we just add to the end of the fields.- Parameters:
field
- The field to add.type
- The type, either GeneralComparator.ASC or GeneralComparator.DESC.index
- The index to add at.- Throws:
IllegalArgumentException
- If we can't find the field in the class/class chain passed into the constructor.
-
addFieldBefore
public void addFieldBefore(String field, String type, String ref) throws IllegalArgumentException
Add a new field in BEFORE the named field, if we don't have the named field then we just calladdField(String,String)
which will add the field in after all the others.- Parameters:
field
- The field to add.type
- Sort either ascending or descending, should be either GeneralComparator.ASC or GeneralComparator.DESC.ref
- The reference field.- Throws:
IllegalArgumentException
- If we can't find the field in the class/class chain passed into the constructor.
-
addFieldAfter
public void addFieldAfter(String field, String type, String ref) throws IllegalArgumentException
Add a new field in AFTER the named field, if we don't have the named field then we just calladdField(String,String)
which will add the field in after all the others.- Parameters:
field
- The field to add.type
- Sort either ascending or descending, should be either GeneralComparator.ASC or GeneralComparator.DESC.ref
- The reference field.- Throws:
IllegalArgumentException
- If we can't find the field in the class/class chain passed into the constructor.
-
removeField
public void removeField(String field)
Remove a field that we sort on. If we don't have the field then we do nothing.- Parameters:
field
- The field to remove.
-
addField
public void addField(String field, String type) throws IllegalArgumentException
Add a field that we sort on, if you readd the same field then the type is just updated. The order in which you add the fields provides the order in which the objects are sorted.- Parameters:
field
- The field to sort on.type
- The type either GeneralComparator.ASC or GeneralComparator.DESC.- Throws:
IllegalArgumentException
- If we can't find the field in the class/class chain passed into the constructor.
-
compare
public int compare(Object obj1, Object obj2)
Implement the {@link Comparator.compare(Object,Object)} method. Here we check each field in turn, we only check subsequent fields if the "higher" up fields are equal. So if fields 0 and 1 are both equal then we check field 2 and so on... Note: it is possible that we have an exception thrown here, however the compare method doesn't allow for exceptions to be thrown, so we just consume them and return 0, we only catch IllegalAccessException and InvocationTargetException.- Specified by:
compare
in interfaceComparator
- Parameters:
obj1
- The first object.obj2
- The second object.- Returns:
- A value according to the rules laid out in {@link Comparator.compare(Object,Object)},
if either object is
null
then we return 0 or we return 0 if either object returned from the accessor chain "get" call is null.
-
equals
public boolean equals(Object obj)
Implement the {@link Comparator.equals(Object)} method. We just look through our fields and then compare the fields.- Specified by:
equals
in interfaceComparator
- Overrides:
equals
in classObject
- Parameters:
obj
- Another GeneralComparator.- Returns:
true
if all our fields match those in the passed in GeneralComparator AND that they are in the same order AND that they have the same type,false
otherwise.
-
getFields
protected List getFields()
Return a List of GeneralComparator.SortField objects, this is used in theequals(Object)
method.- Returns:
- A List of GeneralComparator.SortField objects.
-
getCount
public int getCount()
-
-