How do I use multiple comparators in a binarySearch in java...
I'm trying to sort a list of contestants which are sorted by name and their starting number.
The problem is if two contestants have the same name I get an IndexOutOfBoundsException so I want to do a secondary binarySearch using the starting number (which is unique) but still keeping them in the right order with names.
This is what I've got right now:
static void add(Contestant c){
int pos = Collections.binarySearch(byName, c, new ConNameCmp());
if (pos >= 0){
pos = Collections.binarySearch(byName, c, new ConStartCmp());
}
byName.add(-pos-1, c);
One Comparator only
Don't use two Comparators, use a single Comparator that compares both values:
public int compare(Foo a, Foo b){
// compare bar() values first
int result = a.bar().compareTo(b.bar());
// compare baz() values only if bar() values are different
if(result==0){
result = a.baz().compareTo(b.baz());
}
return result;
}
(In your case bar() is the name and baz() is the number).
Use Libraries
Creating Comparators this way is a lot easier if you use either Guava or Commons / Lang
Guava Versions:
#Override
public int compare(final Foo a, final Foo b){
return ComparisonChain
.start()
.compare(a.bar(), b.bar())
.compare(a.baz(), b.baz())
.result();
}
Commons / Lang Version:
#Override
public int compare(final Foo a, final Foo b){
return new CompareToBuilder()
.append(a.bar(), b.bar())
.append(a.baz(), b.baz())
.toComparison();
}
(Both of these versions won't fail if any of the values are null, my quick and dirty code above will)
Solve the Problem
I don't think you should do a Binary search in the first place, this seems very complicated.
Why don't you use a TreeSet with a custom comparator? Or Collections.sort(list, comparator)? (For both of these options you can use the comparators I showed earlier).
Also, you should think about letting your Contestant implement Comparable<Contestant>. That way you won't need to use an external Comparator. You can use the same logic as above in the compareTo() method, just replace one of the objects with this.
You might have already tried this, and this solution might not be available to you, but if you can change your "Contestant" class, you can make it extend the "java.lang.Comparable" interface and override Comparable#compareTo(Contestant) method so that it takes both the name and starting number into account. Afterwards, you'll be able to use the Collections.binarySearch(Collection<Contestant>, Contestant) method for your need.
Related
Why we use new keyword before comparator while using it as a constructor in sorting as comparator is an interface so we cannot instantiate it?
Collections.sort(persons, new Comparator<Person>() {
#Override
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
});
That's because this code does not instantiate Comparator. As you said, that's not possible.
Instead, it is syntax sugar. It's short for:
// Yes, you can define a class inside a method.
class $AutoGeneratedName implements Comparator<Person> {
#Override public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
}
Collections.sort(persons, new $AutoGeneratedName());
In other words, short for: Define a new class, which implements Comparator. Then, instantiate this class once right away. Resolve this entire expression as a reference to this newly created instance. This construct is called an anonymous inner class.
CAREFUL - this code is bad.
20 years ago, that code was mostly fine, except for one detail: using a - b in comparisons is dangerous for very large numbers, but presumably, given that this is about 'age', not going to be an issue. Still, bad form; return Integer.comparing(p1.getAge(), p2.getAge()) would be much better.
But since then, this is no longer needed. You can write the concept much shorter like so:
Collections.sort(persons, (a, b) -> Integer.compare(a.getAge(), b.getAge());
We can do even that much, much simpler, and more readably by using List#sort with Comparator.comparingInt.
persons.sort(Comparator.comparingInt(Person::getAge));
which does exactly what you think it does when you just read it like its english: It sorts the collection 'persons' by comparing a specific int - which int? The one you get when you invoke getAge() on the person method.
This last snippet is what you should be using instead.
It is generally said that comparator is used to have multiple sorting sequences of collection of objects while comparable is used to have single sorting sequence.
What is the use of comparator interface in java when it is possible to have multiple sorting sequences using comparable interface?
import java.util.*;
enum CompareValue {RollNo, Marks;}
class Student implements Comparable<Student> {
public int marks;
public int rollNo;
public static CompareValue comparator = CompareValue.RollNo;
Student (int marks, int rollNo) {
this.marks = marks;
this.rollNo = rollNo;
}
public int compareTo(Student s) {
switch (comparator) {
case RollNo:
return this.rollNo - s.rollNo;
case Marks:
return this.marks - s.marks;
}
return 0;
}
}
public class Test
{
public static void main (String[] args)
{
Student s1 = new Student(59, 103);
Student s2 = new Student(87, 102);
Student s3 = new Student(78, 101);
Student students[] = {s1, s2, s3};
Arrays.sort(students);
System.out.println("Student list sorted by rollno");
for (Student s:students) {
System.out.println(s.rollNo + " - " + s.marks);
}
Student.comparator = CompareValue.Marks;
System.out.println("Student list sorted by marks");
Arrays.sort(students);
for (Student s:students) {
System.out.println(s.rollNo + " - " + s.marks);
}
}
}
When your compareTo method has different behaviors based on the value of some static variable, you are basically introducing a global setting that controls the natural ordering of the Student class.
This could be confusing and counter intuitive to users of your class.
Besides, it makes the implementation of compareTo awkward, especially if you have more than two implementations, and each implementation depends on multiple instance variables.
Comparator is a much more suitable interface to supply multiple different comparisons for instances of the same class, each implementation having its own compare() logic.
When you have objects that do not implement comparable, but you would like to sort a collection consisting them, you would either have to extend them just to sort your collection or provide a comparator that compares them even though they are not comparable.
Or you might want to compare sort those objects in a different manner then their natural sort.
Imagine such an example.
String is an object that is comparable. Imagine you want to sort a collection of strings based on their hashCode instead of the string natural order. How would you do it without creating a comparator?
What you have shown there is indeed multiple sort orders using Comparable, but don't you think it's too much boiler plate code? Let's say if you have added a new field to the class called name, and now you want to sort by name. You'd have to:
add a new case to the enum
add a new case to the compareTo.
Another disadvantage of using the approach you showed is that it is not necessarily clear what this means:
Arrays.sort(student);
You would have to look through your code and check what value you have set the comparator.
Also, if I were using your class and I want to sort by something else, I would have to create a Comparator anyway, because I can't edit your class.
But if you use Comparator, you solve all of these problems:
Arrays.sort(students, Comparator.comparing(Student::getName));
Therefore, Comparable is only useful when there is one natural order, like dates and times for example.
If we look at the Comparable and Comparator interfaces and what they mean, everything will be clear.
Comparable:
This is an internal property of a JAVA class i.e. it assumes that whenever one uses the internal compareTo() method, one is using it for the specified object.
public int compareTo(T o);
Therefore, in implementation of this method we use this which is the current object and compare it to some other object of same type. These can be treated as defaults or use for natural ordering.
Like 1 comes before 2 and so on. This is the natural ordering.
Comparator:
This is property which actually is not tightly bound to the Java class itself. Comparators are used to actually provide a method to be used by some other services (like Collections.sort()) for achieving a particular goal.
int compare(T o1, T o2);
By this we mean, You can have multiple Comparators, providing different ways of achieving different goals wherein the actual service can pick any two objects and compare them.
This can be used to provide custom ordering, like using some equation we can come up with an ordering where f(1) actually comes after f(2) and so on. This equation will likely be achieving some order which solves a use-case.
I currently have a code snippet which returns strings of a list in ascending order:
Collections.sort(myList, new Comparator<MyClass>() {
#Override
public int compare(MyClass o1, MyClass o2) {
return o1.aString.compareTo(o2.aString);
}
});
While it works, I would like to add some custom "rules" to the order to put certain strings to the front. For instance:
if(aString.equals("Hi")){
// put string first in the order
}
if(aString begins with a null character, e.g. " ") {
// put string after Hi, but before the other strings
}
// So the order could be: Hi, _string, a_string, b_string, c_string
Is it possible to customize the sorting of a list with a Comparator like this?
The answer from MC Emperor is quite nice (+1) in that it fulfills the OP's requirement of not using Java 8 APIs. It also uses a neat internal function technique (the getOrder method) of mapping conditions to small integer values in order to effect a first-level comparison.
Here's an alternative that uses Java 8 constructs. It assumes that MyClass has a getString method that does the obvious thing.
Collections.sort(myList,
Comparator.comparing((MyClass mc) -> ! mc.getString().equals("Hi"))
.thenComparing(mc -> ! mc.getString().startsWith(" "))
.thenComparing(MyClass::getString));
This is pretty opaque until you get used to this style. The key insight is that the "extractor" function that's supplied to Comparator.comparing and Comparator.thenComparing often simply extracts a field, but it can be a general mapping to any other value. If that value is Comparable then an additional Comparator for it needn't be provided. In this case the extractor function is a boolean expression. This gets boxed to a Boolean which as it turns out is Comparable. Since false orders before true we need to negate the boolean expression.
Also note that I had to provide an explicit type declaration for the lambda parameter, as type inference often doesn't work for chained comparator cases such as this one.
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;
}
else 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 Integer.compare(getOrder(o1), getOrder(o2))1 determines the comparison. Otherwise, if both are the same, then the lexiographic order is used for comparison.
Here is some code in action.
1 Always use Integer::compare rather than subtracting one from the other, because of the risk of erroneous results due to integer overflow. See here.
Yes, that is possible, you have complete control over the compareTo() method. Two things:
Use String#equals instead of == to compare strings
Make sure you check both arguments to compareTo for your exceptional cases.
A concrete way of implementing something where some words are always first and some words are always last, with ordering defined among the exceptions:
Map<String, Integer> exceptionMap = new HashMap<>();
exceptionMap.put("lowest", -2);
exceptionMap.put("second_lowest", -1);
exceptionMap.put("second_highest", 1);
exceptionMap.put("highest", 2);
public int compareToWithExceptionMap(String s1, String s2) {
int firstExceptional = exceptionMap.getOrDefault(s1, 0);
int secondExceptional = exceptionMap.getOrDefault(s2, 0);
if (firstExceptional == 0 && secondExceptional == 0) {
return s1.compareTo(s2);
}
return firstExceptional - secondExceptional;
}
If I have the following List:
List<String> list = Arrays.asList("hello", "world", "hello");
And I apply the following (Java8):
list.stream().distinct().collect(Collectors.toString());
Then I would get a list with "hello" and "world".
However, in my case, I have a list of a type (from an external api) where I want to "bypass" the equals Method, ideally with a comparator, as it doesn't cover what I need.
Assume this class looks like this:
public class Point {
float x;
float y;
//getters and setters omitted
}
In this case, I would like two points that cover a certain criteria to be defined as equal, for instance (30, 20) and (30.0001, 19.999).
A custom comparator could do the trick, but I have found no API that does what the distinct() in Java8 Stream does, but with a comparator (or similar pattern).
Any thoughts? I know I could write such a function, but I would rather like the elegant way of using existing apis... I have no restriction with external libraries (guava, apache-commons, etc. are welcome if they have a comfortable way of doing what I need).
HashingStrategy is the concept you're looking for. It's a strategy interface that allows you to define custom implementations of equals and hashcode.
public interface HashingStrategy<E>
{
int computeHashCode(E object);
boolean equals(E object1, E object2);
}
Streams don't support hashing strategies but Eclipse Collections does. It has sets and maps that support hashing strategies as well as overloads of methods like distinct() that take hashing strategies.
This would work well for Strings. For example, here's how we could get all distinct Strings ignoring case.
MutableList<String> strings = Lists.mutable.with("Hello", "world", "HELLO", "World");
assertThat(
strings.distinct(HashingStrategies.fromFunction(String::toLowerCase)),
is(equalTo(Lists.immutable.with("Hello", "world"))));
Or you can write the hashing strategy by hand to avoid garbage creation.
HashingStrategy<String> caseInsensitive = new HashingStrategy<String>()
{
#Override
public int computeHashCode(String string)
{
int hashCode = 0;
for (int i = 0; i < string.length(); i++)
{
hashCode = 31 * hashCode + Character.toLowerCase(string.charAt(i));
}
return hashCode;
}
#Override
public boolean equals(String string1, String string2)
{
return string1.equalsIgnoreCase(string2);
}
};
assertThat(
strings.distinct(caseInsensitive),
is(equalTo(Lists.immutable.with("Hello", "world"))));
This could work for Points too, but only if you can group all points within non-overlapping regions to have the same hashcode. If you're using a Comparator defined to return 0 when two Points are close enough, then you can run into transitivity problems. For example, Points A, B, and C can fall along a line with A and C both close to B but far from each other. Still, if this is a useful concept to you, we'd welcome a pull request adding ListIterable.distinct(Comparator) to the API.
Note: I am a committer for Eclipse Collections.
I want to pass in an arraylist and sort it no matter whats in it
Error I am getting: Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Character
ArrayList myArray = new ArrayList();
myArray.add("wayne");
myArray.add("bob");
myArray.add('h');
myArray.add(4);
myArray.add(5.50);
ArrayList sortedArray = Validate.Sort(myArray);
for(Object x: sortedArray)
{
System.out.println(x);
}
public static ArrayList Sort(ArrayList value)
{
Collections.sort(value);
return value;
}
Your ArrayList has a mix of String and char values. So when the two try to get compared, the String's compareTo method is being called. It has a signature of compareTo(String,String), so it's trying to cast the char to a String. Either put all Strings in, or write a custom comparator that handles mixed types like this.
Take a look at Comparator:
http://docs.oracle.com/javase/6/docs/api/java/util/Comparator.html
In your case the compareTo() may look like:
public int compareTo(Object a, Object b){
if(a == b)
return 0;
if(a==null)
return 1;
if(b==null)
return -1;
return a.toString().compareTo(b.toString);
}
But I think this would be kind of useless.
I want to pass in an arraylist and sort it no matter whats in it
What is this supposed to mean? A total ordering is a binary relation which satisfies some properties (antisimmetry, transitivity, totality). More easily, given two elements, you must identify a criterion to determine which is smaller/bigger than the other.
You cannot do this, unless you know what these elements actually are. If you want to sort an array of whatever, given object A and B, whose types you don't even know, how can you tell which is bigger than the other?
In Java, you might use the hashCode, which is indeed a criterion, but it's hardly useful. If you want to avoid duplicates, you can use a Set rather than a List. Anyway, if you really want to sort elements based on their hashCode, you can do it like this:
private static final Comparator<Object> hashComparator
= new Comparator<Object>() {
#Override
public int compare(Object a, Object b) {
return System.identityHashCode(a) - System.identityHashCode(b);
}
#Override
public boolean equals(Object obj) {
//this is a static singleton, nothing can be equal to it
return false;
}
};
public static ArrayList Sort(ArrayList value)
{
Collections.sort(value, hashComparator);
return value;
}
EDIT: if you are interested in sorting a list of just Strings and Characters (or if you want to sort items based on their string representation) the right thing to do is probably convert everything to strings, or use a SortedMap<String, Object>. Otherwise, you can do the same I just showed you before, but using a more specific compare function, such as:
public int compare(Object a, Object b) {
return a.toString().compareTo(b.toString());
}