This content originally appeared on Level Up Coding - Medium and was authored by Ivan Polovyi
The Java language frequently operates on objects, and often, these objects need to be ordered in some way, such as by date. To order objects, we must compare them and determine which ones are greater, lesser, or equal to others. For this purpose, we can use the Comparable interface.
Comparable interface
The purpose of the Comparable<T> interface is to define the natural order of objects instantiated from a particular class. Therefore, the class whose objects must be ordered must implement this interface. In other words, the comparison logic is defined within the class itself. It provides a single sorting sequence, meaning that when a class contains multiple fields, the order of the objects is determined by only one field. For example, a Student class might be ordered by grade or a PurchaseTransaction class might be ordered by value.
The Comparable<T> interface contains a single method:
public int compareTo(T o);
This method defines the contract for the interface. Although it has only one abstract method and is technically a functional interface, it is not annotated with @FunctionalInterface and is not intended to be implemented with a lambda.
The compareTo method returns an integer with the following meanings:
- A negative integer (-1): the current object is less than the received object.
- Zero (0): the current object is equal to the received object.
- A positive integer (1): the current object is greater than the received object.
The method can throw the following exceptions:
- NullPointerException: when the argument is null.
- ClassCastException: when the argument cannot be compared to the current object.
An implementation of the compareTo method must obey the following rules:
- Anti-symmetry: For any two objects, if the first object is less than, equal to, or greater than the second, then the second must be greater than, equal to, or less than the first, respectively.
obj1.compareTo(obj2) == -1 => obj2.compareTo(obj1) == 1
obj1.compareTo(obj2) == 0 => obj2.compareTo(obj1) == 0
obj1.compareTo(obj2) == 1 => obj2.compareTo(obj1) == -1
- Transitivity: For any three objects, if the first object is greater than the second, and the second is greater than the third, then the first must be greater than the third, and so on.
obj1.compareTo(obj2) == -1; obj2.compareTo(obj3) == -1; => obj1.compareTo(obj3) == -1
obj1.compareTo(obj2) == 1; obj2.compareTo(obj3) == 1; => obj1.compareTo(obj3) == 1
- Congruence: If two objects are equal according to compareTo(), they should behave the same way when compared to a third object.
obj1.compareTo(obj2) == 0; obj1.compareTo(obj3) > 0; => obj2.compareTo(obj3) > 0
- Consistency with equals: It’s strongly recommended that compareTo() be consistent with equals() .
obj1.compareTo(obj2) == 0; => obj1.equals(obj2) == true;
Set up
Let me introduce an example class that I'm going to use to show how to implement Comparable<T> interface.
package com.polovyi.ivan.tutorials;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class PurchaseTransaction {
private String id;
private String paymentType;
private BigDecimal amount;
private LocalDate createdAt;
private int cashBack;
}
This is a simple POJO class that represents a purchase transaction.
Comparable<T> interface implementation
Imagine we need to sort the objects of this class by their cashback values in ascending order, from the lowest to the highest.
package com.polovyi.ivan.tutorials;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class A_PurchaseTransaction implements Comparable<A_PurchaseTransaction> {
private String id;
private String paymentType;
private BigDecimal amount;
private LocalDate createdAt;
private int cashBack;
@Override
public int compareTo(A_PurchaseTransaction object) {
int diff = this.getCashBack() - object.getCashBack();
return diff == 0 ?
0 :
diff > 0 ?
1 :
-1;
}
}
The class implements the Comparable<T> interface and overrides its compareTo method. In this method, the cashback of the given object, which is passed as a method parameter, is subtracted from the cashback of the current object. Based on the result, the method returns 0, 1, or -1.
The method can be simplified by using a compare method from the Integer class, like so:
package com.polovyi.ivan.tutorials;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class B_PurchaseTransaction implements Comparable<B_PurchaseTransaction> {
private String id;
private String paymentType;
private BigDecimal amount;
private LocalDate createdAt;
private int cashBack;
@Override
public int compareTo(B_PurchaseTransaction object) {
return Integer.compare(this.getCashBack(), object.getCashBack());
}
}
Now we can test it:
@Test
public void testSortByCashBack_A() {
A_PurchaseTransaction transaction1 = new A_PurchaseTransaction("#1", "VISA", BigDecimal.TEN, LocalDate.now(), 2);
A_PurchaseTransaction transaction2 = new A_PurchaseTransaction("#2", "MASTER", BigDecimal.TWO, LocalDate.now(), 3);
A_PurchaseTransaction transaction3 = new A_PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1);
List<A_PurchaseTransaction> transactions = new ArrayList<>();
transactions.add(transaction1);
transactions.add(transaction2);
transactions.add(transaction3);
System.out.println("Transactions BEFORE sort = " + transactions);
Collections.sort(transactions);
System.out.println("Transactions AFTER sort = " + transactions);
List<Integer> cashBackList = transactions.stream()
.map(A_PurchaseTransaction::getCashBack)
.collect(Collectors.toUnmodifiableList());
assertEquals(List.of(1, 2, 3), cashBackList);
}
In this test method, a list of objects is created and sorted using the Collection.sort() method. This results in a collection ordered by the cashback field from lower to higher values.
To obtain the same result, we can use Arrays.sort() method.
@Test
public void testSortByCashBack_A_Arrays_Sort() {
A_PurchaseTransaction transaction1 = new A_PurchaseTransaction("#1", "VISA", BigDecimal.TEN, LocalDate.now(), 2);
A_PurchaseTransaction transaction2 = new A_PurchaseTransaction("#2", "MASTER", BigDecimal.TWO, LocalDate.now(), 3);
A_PurchaseTransaction transaction3 = new A_PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1);
A_PurchaseTransaction[] transactions = {transaction1, transaction2, transaction3};
System.out.println("Transactions BEFORE sort = " + Arrays.toString(transactions));
Arrays.sort(transactions);
System.out.println("Transactions AFTER sort = " + transactions);
List<Integer> cashBackList = Arrays.stream(transactions).toList().stream()
.map(A_PurchaseTransaction::getCashBack)
.collect(Collectors.toUnmodifiableList());
assertEquals(List.of(1, 2, 3), cashBackList);
}
The stream of elements can be sorted as well like so:
@Test
public void testSortByCashBack_A_Stream() {
A_PurchaseTransaction transaction1 = new A_PurchaseTransaction("#1", "VISA", BigDecimal.TEN, LocalDate.now(), 2);
A_PurchaseTransaction transaction2 = new A_PurchaseTransaction("#2", "MASTER", BigDecimal.TWO, LocalDate.now(), 3);
A_PurchaseTransaction transaction3 = new A_PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1);
List<A_PurchaseTransaction> sortedList = Stream.of(transaction1, transaction2, transaction3)
.sorted()
.collect(Collectors.toUnmodifiableList());
System.out.println("Transactions AFTER sort = " + sortedList);
List<Integer> cashBackList = sortedList.stream()
.map(A_PurchaseTransaction::getCashBack)
.collect(Collectors.toUnmodifiableList());
assertEquals(List.of(1, 2, 3), cashBackList);
}
We can now create a TreeSet of transactions, and they will be sorted out of the box.
@Test
public void testSortByCashBack_A_TreeSet() {
A_PurchaseTransaction transaction1 = new A_PurchaseTransaction("#1", "VISA", BigDecimal.TEN, LocalDate.now(), 2);
A_PurchaseTransaction transaction2 = new A_PurchaseTransaction("#2", "MASTER", BigDecimal.TWO, LocalDate.now(), 3);
A_PurchaseTransaction transaction3 = new A_PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1);
Set<A_PurchaseTransaction> transactions = new TreeSet<>(Set.of(transaction1, transaction2, transaction3));
System.out.println("Transactions = " + transactions);
List<Integer> cashBackList = transactions.stream()
.map(A_PurchaseTransaction::getCashBack)
.collect(Collectors.toUnmodifiableList());
assertEquals(List.of(1, 2, 3), cashBackList);
}
By switching the sequence of objects, we control the order. Let's say the requirement is to be able to sort objects from the class by cashback field from higher to lower, aka descending sort.
package com.polovyi.ivan.tutorials;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class C_PurchaseTransaction implements Comparable<C_PurchaseTransaction> {
private String id;
private String paymentType;
private BigDecimal amount;
private LocalDate createdAt;
private int cashBack;
@Override
public int compareTo(C_PurchaseTransaction object) {
// int diff = this.getCashBack() - object.getCashBack();
int diff = object.getCashBack() - this.getCashBack();
return diff == 0 ?
0 :
diff > 0 ?
1 :
-1;
}
}
Now we can confirm it with a test:
@Test
public void testSortByCashBack_C() {
C_PurchaseTransaction transaction1 = new C_PurchaseTransaction("#1", "VISA", BigDecimal.TEN, LocalDate.now(), 2);
C_PurchaseTransaction transaction2 = new C_PurchaseTransaction("#2", "MASTER", BigDecimal.TWO, LocalDate.now(), 3);
C_PurchaseTransaction transaction3 = new C_PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1);
List<C_PurchaseTransaction> transactions = new ArrayList<>();
transactions.add(transaction1);
transactions.add(transaction2);
transactions.add(transaction3);
System.out.println("Transactions BEFORE sort = " + transactions);
Collections.sort(transactions);
System.out.println("Transactions AFTER sort = " + transactions);
List<Integer> cashBackList = transactions.stream()
.map(C_PurchaseTransaction::getCashBack)
.collect(Collectors.toUnmodifiableList());
assertEquals(List.of(3, 2, 1), cashBackList);
}
We can implement the compareTo method so it compares multiple fields of the object like so:
package com.polovyi.ivan.tutorials;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class D_PurchaseTransaction implements Comparable<D_PurchaseTransaction> {
private String id;
private String paymentType;
private BigDecimal amount;
private LocalDate createdAt;
private int cashBack;
@Override
public int compareTo(D_PurchaseTransaction object) {
if (this.getCashBack() != object.getCashBack())
return Integer.compare(this.getCashBack(), object.getCashBack());
if (!this.getCreatedAt().equals(object.getCreatedAt()))
return this.getCreatedAt().compareTo(object.getCreatedAt());
if (!this.getAmount().equals(object.getAmount()))
return this.getAmount().compareTo(object.getAmount());
return this.getPaymentType().compareTo(object.getPaymentType());
}
}
This compareTo method is used to compare two D_PurchaseTransaction objects for sorting purposes. It compares the objects based on a defined sequence of fields, with each field acting as a tiebreaker if the previous fields are equal. The comparison priority is as follows:
- Cashback: The method first compares the cashBack values of the two objects. If they are different, the method returns the result of comparing these values using Integer.compare(). This determines the primary ordering.
- Creation Date: If the cashBack values are equal, the method then compares the createdAt dates. If these dates are not equal, it returns the result of comparing the createdAt values using their compareTo method.
- Amount: If both the cashBack and createdAt values are equal, the method proceeds to compare the amount values. If these are not equal, it returns the result of comparing the amount values using their compareTo method.
- Payment Type: Finally, if the cashBack, createdAt, and amount values are all equal, the method compares the paymentType values using their compareTo method. This determines the final ordering if all previous fields are equal.
The method ensures a hierarchical comparison, giving the highest priority to the cashBack field and the lowest priority to the paymentType field. This approach sorts objects based on the most significant differences first. The hierarchy can be adjusted as needed to meet specific requirements.
Now we can test it:
@Test
public void testSortByCashBack_D_equal_cashback() {
D_PurchaseTransaction transaction1 = new D_PurchaseTransaction("#1", "VISA", BigDecimal.TEN, LocalDate.now().minus(1,
ChronoUnit.DAYS), 1);
D_PurchaseTransaction transaction2 = new D_PurchaseTransaction("#2", "MASTER", BigDecimal.TWO, LocalDate.now().minus(10,
ChronoUnit.DAYS), 1);
D_PurchaseTransaction transaction3 = new D_PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.now().minus(5,
ChronoUnit.DAYS), 1);
List<D_PurchaseTransaction> transactions = new ArrayList<>();
transactions.add(transaction1);
transactions.add(transaction2);
transactions.add(transaction3);
System.out.println("Transactions BEFORE sort = " + transactions);
Collections.sort(transactions);
System.out.println("Transactions AFTER sort = " + transactions);
List<String> idsList = transactions.stream()
.map(D_PurchaseTransaction::getId)
.collect(Collectors.toUnmodifiableList());
assertEquals(List.of("#2", "#3", "#1"), idsList);
}
Many classes in the Java API implement Comparable<T>, allowing us to utilize their comparison methods. For instance, in a previous example, I leveraged the compareTo methods in the LocalDate, BigDecimal, and String classes.
The same as classes records can implement Comparable<T> , like so:
package com.polovyi.ivan.tutorials;
import java.math.BigDecimal;
import java.time.LocalDate;
public record E_PurchaseTransaction(String id,
String paymentType,
BigDecimal amount,
LocalDate createdAt,
int cashBack
) implements Comparable<E_PurchaseTransaction> {
@Override
public int compareTo(E_PurchaseTransaction object) {
if (this.cashBack() != object.cashBack()) {
return Integer.compare(this.cashBack(), object.cashBack());
}
if (!this.createdAt().equals(object.createdAt())) {
return this.createdAt().compareTo(object.createdAt());
}
if (!this.amount().equals(object.amount())) {
return this.amount().compareTo(object.amount());
}
return this.paymentType().compareTo(object.paymentType());
}
}
The Enum can implement comparable too, and I've explained it in the following tutorial:
Spring REST API result sorting implementation using enum
The complete code with more test cases can be found here:
GitHub - polovyivan/java-comparable-interface
The Comparable<T> interface is a great choice for scenarios where objects of a particular class are naturally ordered by a single field. As you can see, using it is relatively simple. As a rule of thumb, you can use it for simple ordering. However, if the class contains more than one field that might need to be used for sorting, then the Comparator<T> interface might be a better choice.
Conclusion
As a programmer, you will frequently work with data, making it essential to know how to perform various operations on collections of data, such as sorting. Understanding these operations is often a topic of job interviews. This tutorial, complete with examples, will help you master these skills.
Thank you for reading! If you enjoyed this post, please like and follow it. If you have any questions or suggestions, feel free to leave a comment or connect with me on my LinkedIn account.
Java Comparable interface was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Ivan Polovyi
Ivan Polovyi | Sciencx (2024-07-12T13:15:00+00:00) Java Comparable interface. Retrieved from https://www.scien.cx/2024/07/12/java-comparable-interface/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.