Add Additional Rules to the Compare Method of a Comparator

Add additional rules to the compare method of a Comparator

That's possible.

Using Java 8 features

You could pass a function to the Comparator.comparing method to define your rules. Note that we simply return integers, the lowest integer for the elements which should come first.

Comparator<MyClass> myRules = Comparator.comparing(t -> {
if (t.aString.equals("Hi")) {
return 0;
}
if (t.aString.startsWith(" ")) {
return 1;
}
else {
return 2;
}
});

If you want the remaining elements to be sorted alphabetically, you could use thenComparing(Comparator.naturalOrder()), if your class implements Comparable. Otherwise, you should extract the sort key first:

Collections.sort(myList, myRules.thenComparing(Comparator.comparing(t -> t.aString)));

Note that the actual specific numbers returned don't matter, what matters is that lower numbers come before higher numbers when sorting, so if one would always put the string "Hi" first, then the corresponding number should be the lowest returned (in my case 0).

Using Java <= 7 features (Android API level 21 compatible)

If Java 8 features are not available to you, then you could implement it like this:

Comparator<MyClass> myRules = new Comparator<MyClass>() {

@Override
public int compare(MyClass o1, MyClass o2) {
int order = Integer.compare(getOrder(o1), getOrder(o2));
return (order != 0 ? order : o1.aString.compareTo(o2.aString));
}

private int getOrder(MyClass m) {
if (m.aString.equals("Hi")) {
return 0;
}
else if (m.aString.startsWith(" ")) {
return 1;
}
else {
return 2;
}
}
};

And call it like this:

Collections.sort(list, myRules);

This works as follows: first, both received strings are mapped to your custom ruleset and subtracted from eachother. If the two differ, then the operation getOrder(o1) - getOrder(o2) determines the comparison. Otherwise, if both are the same, then the lexiographic order is used for comparison.

Here is some code in action.

How to create a comparator with dynamic rules?

I believe the following should push you in the right direction in chaining the comparators for the item given variable filter comparisons. It uses reflection to call the getters and assumes that the comparisons are with doubles.

If they are not guaranteed to be doubles, then adapt the reflection casting to cast to something that works for all your use cases.

PropertyUtils.getProperty() is a part of Apache Commons BeanUtils and can be replaced by your choice of getting the values, whether it be through reflection or static comparators.

public class Filter {

// properties, constructors, getters, setters ...

public Comparator<Item> itemComparator() {
return (item1, item2) -> {
Double val1 = (Double) PropertyUtils.getProperty(item1, fieldName);
Double val2 = (Double) PropertyUtils.getProperty(item2, fieldName);
return (order.equals("asc") ? val1.compareTo(val2) : val2.compareTo(val1);
};
}

public static Comparator<Item> chainedItemComparators(List<Filter> filters) {
return filters.stream()
.map(Filter::itemComparator)
.reduce((item1, item2) -> 0, (f1, f2) -> f1.thenComparing(f2));
}
}

To then use the chained comparator:

public static void main(String[] args) {
List<Filter> filters = new ArrayList<>(Arrays.asList(
new Filter("price", "desc"),
new Filter("size", "asc")
));
List<Item> items = new ArrayList<>(Arrays.asList(
new Item("bar", 6.0, 15.0),
new Item("baz", 7.0, 5.0),
new Item("foo", 10.0, 5.0)
));
items.sort(Filter.chainedItemComparators(filters));
}

Java Using Comparators in combination with custom Comparators

You can do this something like that:

coll.sort(Comparator
.comparingInt((String s) -> s.equals("just") ? 0 : 1) // Words "just" first
.thenComparing(Comparator.naturalOrder())); // Then others

Create Comparator using map of getter method references

You are not storing the changes. thenCompare doesn't modify the original comparator, but returns a new one.

for (var s : fields) {
comparator = comparator.thenComparing(fieldToFieldExtractor.get(s)));
}

Java Comparator based on extern (third) value

Your question boils down to this: does the comparator induce an ordering that is total (in the precise mathematical sense) or not?

I believe it does. You first map all values to a range between 0 and 3. This is your most important attribute to sort on, so you test it first. Now if they are different, you use integer difference to indicate the ordering which is "totally" fine. If they are the same you start ordering lexicographically first by first name then by last name. The lexicographic ordering is of course total. So you are fine again.

As said in other answers nothing else matters. You do not have to worry about the actual size of the int returned by the comparator.

What does very much matter, but you do not show here, is that your equals method on Person should return true if and only if compareTo returns 0. Your compareTo method can only return 0 if both Persons have the same first name and last name. So if that is true, then equals should also do that. Check that. Ok. Then the other direction. Check there are no more other occasions in which your equals returns 0. Done.

Finally, if you do not trust your reasoning there exists a reasonably good way of testing. Create a random person generator, generate pairs and triples of persons and test the rules for a total ordering for millions of combinations. I.e. if a < b then !(b < a), etc. If we did miss something, chances are a few runs of this setup will point out the flaws in our reasoning.

How to compare objects by multiple fields

You can implement a Comparator which compares two Person objects, and you can examine as many of the fields as you like. You can put in a variable in your comparator that tells it which field to compare to, although it would probably be simpler to just write multiple comparators.

Aggregating similar objects using comparator without consistency with equals()

Considering that "similarity" is not a transitive operation: if a is similar to b, and b is similar to c, then a may not be similar to c, I would suggest not to use Comparable/Comparator here, because its contract implies transitivity.

A custom interface that suits your needs should be a good option:

interface SimilarityComparable<T, D> {
// D - object that represents similarity level
D getSimilarityLevel(T other);
}

However with this approach you won't be able to define multiple groups of similarity for the same type, because of Java generics:

class Location implements SimilarityComparable<Location, Distance>, SimilarityComparable<Location, NameDifference> {
// won't compile - can't use two generic interfaces of the same type simultaneously
}

In this case you can fall-back to comparators:

interface SimilarityComparator<T, D> {
D getSimilarityLevel(T a, T b);
}

class LocationDistanceSimilarityComparator implements SimilarityComparator<Location, Distance> {
...
}

class LocationNameSimilarityComparator implements SimilarityComparator<Location, NameDifference> {
...
}

How to implement a Comparator to compare names?

Looking at the documentation, the compare() method should be used to determine the order of the two students (here, you would want to do it alphabetically). If you wanted to check for equality, you could override the equals() method of the Comparator. For strings, you can just use existing methods to compare the students' name fields.

public static Comparator<Student> getCompByName()
{

Comparator comp = new Comparator<Student>(){

@Override
public int compare(Student s1, Student s2)
{
s1.name.compareTo(s2.name)
}

};

return comp;
}


Related Topics



Leave a reply



Submit