One of the common requirements in Java is to compare objects by multiple fields. For example, if you have a list of CreditCard objects and you want to compare them by their provider and credit limit I mean, first compare them by their providers like VISA, Master, or American Express and if they are from the same provider then compare them by their credit limit, also known as breaking ties. Before Java 8 this wasn't easy because you have to write nested codes to compare multiple fields of an object as there was no easy way to chain multiple Comparators in Java, but things have changed a lot from Java 8.
The java.util.Comparator interface has been enhanced to take advantage of new Java 8 features which allow concrete methods on interfaces. Earlier, you can only declare methods on the interface, but from Java 8 you can put static and default methods on interfaces to actually perform some task.
Java API designer has taken great advantage of these features to enhance existing popular interfaces like Collection, Iterable, Map, List, Comparators, and others, if you are interested you can further see Java SE 8 for Really Impatient for a full list of useful methods on existing interfaces. It has a section called Miscellaneous goodies which highlights several useful enhancements on existing API in Java 8.
Before Java 8, if you want to compare objects by multiple fields, you can write the following implementation of compare() method from java.util.Comparator class
public int compare(CreditCard first, CreditCard second) { int i = first.provider.compareTo(second.provider); if (i != 0) return i; i = first.creditLimit - second.creditLimit; if (i != 0) return i; return Integer.compare(first.fee, second.fee); }
This method first compares two CreditCards by their provider, but if the provider is the same then compareTo() return zero and it moves on to compare the credit limit, and if the credit limit is also the same then it finally compares the fee for each credit card and returns ordering.
In general, you compare fields in the order you want. You use Comparable's compareTo() for comparing String as we did for the provider.
For integral fields like int and long you can either just use subtraction if you are sure that both values are positive or better you can use the Integer.compare() method which was added on Java SE 7 to facilitate the comparison of int primitive and Integer objects.
The code is not bad at all and many of us have similar examples everywhere, but this is not really chaining of Comparator. If you want to change the order like if you want to compare one field before another like comparing fees before comparing credit limits then you need to change the code here or write a separate comparator. It's not flexible enough to allow you to compose comparison logic.
The Java 8 release changed this totally by providing comparing() and thenComparing() methods. These methods are added as default and static methods on java.util.Comparator interface and they allow you to create Comparator to compare multiple fields on the fly using lambda expression as shown below:
Comparator.comparing((CreditCard c)->c.provider)
.thenComparing(c->c.creditLimit)
.thenComparingInt(c->c.fee);
This code returns a Comparator which first compares CreditCards by their provider and if the provider is the same then it compares them by their CreditLimit and if that is also the same then it compares them by their fee. Both comparing() and thenComparing() method returns Comparator.
In fact, comparing takes a Accepts a function that extracts a sort key from a type T, and returns a Comparator<T> that compares by that sort key using the specified Comparator. While thenComparing() only compares if this comparator thinks that two values are equals.
You can even simplify the above code in Java 8 by using method reference as shown below:
Comparator.comparing(CreditCard::getProvider)
.thenComparingInt(CreditCard::getCreditLimit)
.thenComparingInt(CreditCard::getFee);
You can see it is very simple and flexible. If you want to change the order of fields you can easily do it by changing the order of the thenComparing() method. You can even pass a dynamic, on the fly comparator and you can store it for further usage.
The java.util.Comparator class also provides comparingInt() and thenComparingInt() method to compare primitive values and avoiding boxing and unboxing. There are similar methods for long and double as well like coparingLong() and thenComparing() and comparingDouble() and thenComparingDouble().
How to sort the List of objects by multiple fields in Java?Comparator - Example
Here is our complete Java program to sort a list of objects by multiple fields. Even though you can implement this logic using Comparable, it's better to use Comparator because Comparable is used to define natural ordering. If your natural ordering requires multiple fields comparing then go for it, otherwise use a Comparator.In general, the code for sorting which defines natural order is implemented using Comparable and resides in the same class but Comparator gives you the flexibility to compare objects in the order you want but without modifying any sorting code in the class itself.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/*
* Java Program to sort an ArrayList
*/
public class Main {
public static void main(String[] args) throws Exception {
//before Java 8 e.g. Java SE 6 or Java SE 7
Comparator<CreditCard> compareByProviderThenCreditLimit = new Comparator<CreditCard>(){
public int compare(CreditCard first, CreditCard second) {
int i = first.provider.compareTo(second.provider);
if (i != 0) return i;
i = first.creditLimit - second.creditLimit;
if (i != 0) return i;
return Integer.compare(first.fee, second.fee);
}
};
// in Java 8 using Comparator.comparing and Comparator.thenComparing
// and lambda expression
Comparator<CreditCard> compareByProviderThenCreditLimitInJDK8
= Comparator.comparing((CreditCard c)->c.provider)
.thenComparing(c->c.creditLimit)
.thenComparingInt(c->c.fee);
// you can make it even better by using method reference if you have accessor method
Comparator<CreditCard> compareByProviderThenCreditLimitInJDK8UsingMethodReference
= Comparator.comparing(CreditCard::getProvider)
.thenComparing(CreditCard::getCreditLimit)
.thenComparingInt(CreditCard::getFee);
List<CreditCard> listOfCreditCards = new ArrayList<>();
listOfCreditCards.add(new CreditCard("Master", 10000, 230));
listOfCreditCards.add(new CreditCard("Master", 20000, 230));
listOfCreditCards.add(new CreditCard("Master", 20000, 330));
listOfCreditCards.add(new CreditCard("VISA", 10000, 230));
listOfCreditCards.add(new CreditCard("American Express", 10000, 230));
System.out.println("list before sorting: " + listOfCreditCards);
// comparing by provider, creditlimit and then fee using Java 7 way
listOfCreditCards.sort(compareByProviderThenCreditLimit);
System.out.println("list after sorting (prior to Java 8): " + listOfCreditCards);
//comparing by multiple fields and compartor in Java 8
listOfCreditCards.sort(compareByProviderThenCreditLimitInJDK8);
System.out.println("list after sorting (in Java 8): " + listOfCreditCards);
// Java 8 multiple field comparator example
listOfCreditCards.sort(compareByProviderThenCreditLimitInJDK8UsingMethodReference);
System.out.println("list after sorting (in Java 8 using method refernece): "
+ listOfCreditCards);
}
}
class CreditCard{
String provider;
int creditLimit;
int fee;
public CreditCard(String provider, int creditLimit, int fee){
this.provider = provider;
this.creditLimit = creditLimit;
this.fee = fee;
}
public String getProvider() {
return provider;
}
public int getCreditLimit() {
return creditLimit;
}
public void setProvider(String provider) {
this.provider = provider;
}
public void setCreditLimit(int creditLimit) {
this.creditLimit = creditLimit;
}
public int getFee() {
return fee;
}
public void setFee(int fee) {
this.fee = fee;
}
@Override
public String toString() {
return String.format(provider + ": " + creditLimit + ":" + fee);
}
}
Output:
list before sorting:
[Master: 10000:230, Master: 20000:230, Master: 20000:330,
VISA: 10000:230, American Express: 10000:230]
list after sorting (prior to Java 8):
[American Express: 10000:230, Master: 10000:230, Master: 20000:230,
Master: 20000:330, VISA: 10000:230]
list after sorting (in Java 8): [American Express: 10000:230,
Master: 10000:230, Master: 20000:230, Master: 20000:330, VISA: 10000:230]
list after sorting (in Java 8 using method reference):
[American Express: 10000:230, Master: 10000:230, Master: 20000:230,
Master: 20000:330, VISA: 10000:230]
You can see that how easy it is now to compare Java objects on multiple fields and this also makes sorting easier. Now, let's see all other comparator methods which were added in Java 8 to the API to make better use of this API.
Useful Comparator Methods from Java API
Here is a quick list of useful methods added on java.util.Comparator interface in JDK 8:
1. reversed()
This method returns a comparator that imposes the reverse ordering of this comparator.
2. thenComparing(Comparator<? super T> other)
Returns a lexicographic-order comparator with another comparator. If this Comparator considers two elements equal, i.e. compare(a, b) == 0, other is used to determine the order.
thenComparing(Function<? super T,? extends U> keyExtractor,
Comparator<? super U> keyComparator)
Returns a lexicographic-order comparator with a function that extracts a key to be compared with the given Comparator.
3. thenComparingInt()
Returns a lexicographic-order comparator with a function that extracts an int sort key. You can use this to avoid boxing and unboxing while dealing with primitive values e.g. int or short. There are also methods like thenComparingLong() and thenComparingDouble() to compare primitive long and double values.
4. naturalOrder()
Returns a comparator that compares Comparable objects in the natural order. The returned comparator is serializable and throws NullPointerException when comparing null.
5. nullsFirst
Returns a null-friendly comparator that considers null to be less than non-null. When both are null, they are considered equal. If both are non-null, the specified Comparator is used to determine the order. If the specified comparator is null, then the returned comparator considers all non-null values to be equal.
6. nullsLast
Returns a null-friendly comparator that considers null to be greater than non-null. When both are null, they are considered equal. If both are non-null, the specified Comparator is used to determine the order. If the specified comparator is null, then the returned comparator considers all non-null values to be equal.
7. comparing
Accepts a function that extracts a sort key from a type T, and returns a Comparator<T> that compares by that sort key using the specified Comparator.
8. comparingInt()
This method Accepts a function that extracts an int
sort key from a type T and returns a Comparator<T> that compares by that
sort key. This method is used to compare primitive int and short values to
avoid boxing and unboxing. There are also similar methods that exist like
comparingLong() and
comparingDouble() to compare primitive long and double values.
That's all about how to compare objects by multiple fields in Java 8. The comparing by multiple attributes was tricky on Java Se 6 and Java 7 because there was no cleaner way to chain multiple Comparators but Java 8 solved that problem by introducing comparing() and thenComparing methods.
You can use these two methods to easily compare a list or array of objects on multiple fields like you can compare a list of persons into firstName and if two persons have the same firstName you can compare their lastName to break ties.
Other Core Java Articles you may like:
- How to write Comparator in Java 8 using lambda
- Difference between Comparator and Comparable in Java
- How to sort ArrayList in reverse order in java?
- How to sort a HashMap by keys and values in Java
- 3 Ways to sort a List in Java 8 and 11
Thanks for reading this article so far. If you have any questions or feedback then please drop a comment. If you like this article then please share it with your friends and colleagues.
P.S. - If you want to learn more about Core Java and Functional programming concepts like Lambda, Stream, and functional like map, flat map, and filter then please see these best Java Functional Programming courses. It explains key concepts like lambda expressions, streams, functional interface, Optionals, etc.
No comments:
Post a Comment
Feel free to comment, ask questions if you have any doubt.