Java Language
Porównywalny i Komparator
Szukaj…
Składnia
- klasa publiczna MyClass implementuje porównywalne
<MyClass
> - klasa publiczna MyComparator implementuje Komparator
<SomeOtherClass
> - public int compareTo (MyClass other)
- public int porównaj (SomeOtherClass o1, SomeOtherClass o2)
Uwagi
Wdrażając metodę compareTo(..)
, która zależy od double
, nie wykonuj następujących czynności:
public int comareTo(MyClass other) {
return (int)(doubleField - other.doubleField); //THIS IS BAD
}
Obcięcie spowodowane rzutowaniem (int)
spowoduje, że metoda czasami nieprawidłowo zwraca 0
zamiast liczby dodatniej lub ujemnej, a zatem może prowadzić do błędów w porównywaniu i sortowaniu.
Zamiast tego najprostszą poprawną implementacją jest użycie Double.compare jako takiego:
public int comareTo(MyClass other) {
return Double.compare(doubleField,other.doubleField); //THIS IS GOOD
}
Od wersji Java 1.2 istnieje ogólna wersja Comparable<T>
, po prostu Comparable
. Poza interfejsem ze starszym kodem zawsze lepiej jest zaimplementować ogólną wersję Comparable<T>
, ponieważ nie wymaga rzutowania po porównaniu.
Jest bardzo standardowe, aby klasa była porównywalna do siebie, jak w:
public class A implements Comparable<A>
Chociaż można oderwać się od tego paradygmatu, zachowaj ostrożność.
Comparator<T>
może być nadal używany w instancjach klasy, jeśli klasa ta implementuje Comparable<T>
. W takim przypadku zastosowana zostanie logika Comparator
; naturalne uporządkowanie określone w implementacji Comparable
zostanie zignorowane.
Sortowanie listy za pomocą porównywalnego lub komparator
Powiedzmy, że pracujemy nad klasą reprezentującą Osobę po imieniu i nazwisku. Stworzyliśmy do tego podstawową klasę i zaimplementowaliśmy odpowiednie metody equals
i hashCode
.
public class Person {
private final String lastName; //invariant - nonnull
private final String firstName; //invariant - nonnull
public Person(String firstName, String lastName){
this.firstName = firstName != null ? firstName : "";
this.lastName = lastName != null ? lastName : "";
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String toString() {
return lastName + ", " + firstName;
}
@Override
public boolean equals(Object o) {
if (! (o instanceof Person)) return false;
Person p = (Person)o;
return firstName.equals(p.firstName) && lastName.equals(p.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName);
}
}
Teraz chcielibyśmy posortować listę obiektów Person
według ich nazw, na przykład w następującym scenariuszu:
public static void main(String[] args) {
List<Person> people = Arrays.asList(new Person("John", "Doe"),
new Person("Bob", "Dole"),
new Person("Ronald", "McDonald"),
new Person("Alice", "McDonald"),
new Person("Jill", "Doe"));
Collections.sort(people); //This currently won't work.
}
Niestety, jak zaznaczono, powyższe obecnie się nie kompiluje. Collections.sort(..)
wie, jak posortować listę tylko wtedy, gdy elementy na tej liście są porównywalne lub podano niestandardową metodę porównania.
Gdybyś został poproszony o posortowanie następującej listy: 1,3,5,4,2
, nie miałbyś problemu z odpowiedzią na pytanie 1,2,3,4,5
. Wynika to z faktu, że liczby całkowite (zarówno w Javie, jak i matematycznie) mają naturalne uporządkowanie , standardowe, domyślne uporządkowanie bazowe porównania. Aby nadać naszej klasie Person naturalne uporządkowanie, wdrażamy Comparable<Person>
, co wymaga implementacji metody compareTo(Person p):
public class Person implements Comparable<Person> {
private final String lastName; //invariant - nonnull
private final String firstName; //invariant - nonnull
public Person(String firstName, String lastName) {
this.firstName = firstName != null ? firstName : "";
this.lastName = lastName != null ? lastName : "";
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String toString() {
return lastName + ", " + firstName;
}
@Override
public boolean equals(Object o) {
if (! (o instanceof Person)) return false;
Person p = (Person)o;
return firstName.equals(p.firstName) && lastName.equals(p.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName);
}
@Override
public int compareTo(Person other) {
// If this' lastName and other's lastName are not comparably equivalent,
// Compare this to other by comparing their last names.
// Otherwise, compare this to other by comparing their first names
int lastNameCompare = lastName.compareTo(other.lastName);
if (lastNameCompare != 0) {
return lastNameCompare;
} else {
return firstName.compareTo(other.firstName);
}
}
}
Teraz podana główna metoda będzie działać poprawnie
public static void main(String[] args) {
List<Person> people = Arrays.asList(new Person("John", "Doe"),
new Person("Bob", "Dole"),
new Person("Ronald", "McDonald"),
new Person("Alice", "McDonald"),
new Person("Jill", "Doe"));
Collections.sort(people); //Now functions correctly
//people is now sorted by last name, then first name:
// --> Jill Doe, John Doe, Bob Dole, Alice McDonald, Ronald McDonald
}
Jeśli jednak nie chcesz lub nie możesz zmodyfikować klasy Person
, możesz podać niestandardowy Comparator<T>
który obsługuje porównanie dowolnych dwóch obiektów Person
. Jeśli zostaniesz poproszony o posortowanie następującej listy: circle, square, rectangle, triangle, hexagon
, nie możesz, ale jeśli zostaniesz poproszony o posortowanie tej listy na podstawie liczby rogów , możesz. Właśnie dlatego zapewnienie komparatora instruuje Javę, jak porównać dwa normalnie nieporównywalne obiekty.
public class PersonComparator implements Comparator<Person> {
public int compare(Person p1, Person p2) {
// If p1's lastName and p2's lastName are not comparably equivalent,
// Compare p1 to p2 by comparing their last names.
// Otherwise, compare p1 to p2 by comparing their first names
if (p1.getLastName().compareTo(p2.getLastName()) != 0) {
return p1.getLastName().compareTo(p2.getLastName());
} else {
return p1.getFirstName().compareTo(p2.getFirstName());
}
}
}
//Assume the first version of Person (that does not implement Comparable) is used here
public static void main(String[] args) {
List<Person> people = Arrays.asList(new Person("John", "Doe"),
new Person("Bob", "Dole"),
new Person("Ronald", "McDonald"),
new Person("Alice", "McDonald"),
new Person("Jill", "Doe"));
Collections.sort(people); //Illegal, Person doesn't implement Comparable.
Collections.sort(people, new PersonComparator()); //Legal
//people is now sorted by last name, then first name:
// --> Jill Doe, John Doe, Bob Dole, Alice McDonald, Ronald McDonald
}
Komparatory mogą być również tworzone / używane jako anonimowa klasa wewnętrzna
//Assume the first version of Person (that does not implement Comparable) is used here
public static void main(String[] args) {
List<Person> people = Arrays.asList(new Person("John", "Doe"),
new Person("Bob", "Dole"),
new Person("Ronald", "McDonald"),
new Person("Alice", "McDonald"),
new Person("Jill", "Doe"));
Collections.sort(people); //Illegal, Person doesn't implement Comparable.
Collections.sort(people, new PersonComparator()); //Legal
//people is now sorted by last name, then first name:
// --> Jill Doe, John Doe, Bob Dole, Alice McDonald, Ronald McDonald
//Anonymous Class
Collections.sort(people, new Comparator<Person>() { //Legal
public int compare(Person p1, Person p2) {
//Method code...
}
});
}
Komparatory oparte na wyrażeniu lambda
Od wersji Java 8 komparatory mogą być również wyrażane jako wyrażenia lambda
//Lambda
Collections.sort(people, (p1, p2) -> { //Legal
//Method code....
});
Domyślne metody komparatora
Ponadto istnieją ciekawe domyślne metody interfejsu komparatora do budowania komparatorów: poniżej buduje się komparator porównując według lastName
a następnie firstName
.
Collections.sort(people, Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName));
Odwracanie kolejności komparatora
Dowolny komparator można również łatwo odwrócić za pomocą reversedMethod
który zmieni kolejność rosnącą na malejącą.
Metody porównywania i porównywania
Comparable<T>
interfejs Comparable<T>
wymaga jednej metody:
public interface Comparable<T> {
public int compareTo(T other);
}
Interfejs Comparator<T>
wymaga jednej metody:
public interface Comparator<T> {
public int compare(T t1, T t2);
}
Te dwie metody robią w zasadzie to samo, z jedną niewielką różnicą: compareTo
porównuje this
z other
, podczas gdy compare
porównuje t1
do t2
, nie przejmując się this
.
Oprócz tej różnicy obie metody mają podobne wymagania. W szczególności (dla CompareTo) porównuje ten obiekt z określonym obiektem w celu zamówienia. Zwraca ujemną liczbę całkowitą, zero lub dodatnią liczbę całkowitą, ponieważ ten obiekt jest mniejszy, równy lub większy niż określony obiekt. Tak więc, dla porównania i a
b
:
- Jeśli
a < b
,a.compareTo(b)
icompare(a,b)
powinny zwrócić ujemną liczbę całkowitą, ab.compareTo(a)
icompare(b,a)
powinny zwrócić dodatnią liczbę całkowitą - Jeśli
a > b
,a.compareTo(b)
icompare(a,b)
powinny zwrócić dodatnią liczbę całkowitą, ab.compareTo(a)
icompare(b,a)
powinny zwrócić ujemną liczbę całkowitą - Jeśli równa
a
b
dla porównania, wszystkie porównania powinien powrócić0
.
Naturalne (porównywalne) vs jawne (porównawcze) sortowanie
Istnieją dwie metody Collections.sort()
:
- Taki, który przyjmuje
List<T>
jako parametr, w którymT
musi implementować Porównywalny i przesłonić metodęcompareTo()
, która określa kolejność sortowania. - Taki, który jako argument przyjmuje List i Komparator, przy czym Komparator określa kolejność sortowania.
Po pierwsze, oto klasa Person, która implementuje porównywalne:
public class Person implements Comparable<Person> {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(Person o) {
return this.getAge() - o.getAge();
}
@Override
public String toString() {
return this.getAge()+"-"+this.getName();
}
}
Oto, w jaki sposób użyjesz powyższej klasy do sortowania listy w naturalnym porządku jej elementów, zdefiniowanej przez przesłonięcie metody compareTo()
:
//-- usage
List<Person> pList = new ArrayList<Person>();
Person p = new Person();
p.setName("A");
p.setAge(10);
pList.add(p);
p = new Person();
p.setName("Z");
p.setAge(20);
pList.add(p);
p = new Person();
p.setName("D");
p.setAge(30);
pList.add(p);
//-- natural sorting i.e comes with object implementation, by age
Collections.sort(pList);
System.out.println(pList);
Oto jak użyłbyś anonimowego wbudowanego Komparatora do sortowania Listy, która nie implementuje Porównywalnego, lub w tym przypadku do sortowania Listy w kolejności innej niż naturalna kolejność:
//-- explicit sorting, define sort on another property here goes with name
Collections.sort(pList, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getName().compareTo(o2.getName());
}
});
System.out.println(pList);
Sortowanie wpisów mapy
Począwszy od Java 8, w interfejsie Map.Entry
istnieją domyślne metody umożliwiające sortowanie iteracji map.
Map<String, Integer> numberOfEmployees = new HashMap<>();
numberOfEmployees.put("executives", 10);
numberOfEmployees.put("human ressources", 32);
numberOfEmployees.put("accounting", 12);
numberOfEmployees.put("IT", 100);
// Output the smallest departement in terms of number of employees
numberOfEmployees.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.limit(1)
.forEach(System.out::println); // outputs : executives=10
Oczywiście można ich również używać poza interfejsem API strumienia:
List<Map.Entry<String, Integer>> entries = new ArrayList<>(numberOfEmployees.entrySet());
Collections.sort(entries, Map.Entry.comparingByValue());
Tworzenie komparatora przy użyciu metody porównywania
Comparator.comparing(Person::getName)
Spowoduje to utworzenie komparatora dla klasy Person
która używa tej nazwy osoby jako źródła porównania. Możliwe jest również użycie wersji metody do porównania długich, całkowitych i podwójnych. Na przykład:
Comparator.comparingInt(Person::getAge)
Odwrócone zamówienie
Aby utworzyć komparator narzucający odwrotną kolejność, użyj metody reverse reversed()
:
Comparator.comparing(Person::getName).reversed()
Łańcuch komparatorów
Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName)
Spowoduje to utworzenie komparatora, który najpierw porównuje z nazwiskiem, a następnie porównuje z imieniem. Możesz połączyć tyle komparatorów, ile chcesz.