Java Language
Comparabile e comparatore
Ricerca…
Sintassi
- La classe pubblica MyClass implementa
<MyClass
> comparabile - MyComparator di classe pubblica implementa il comparatore
<SomeOtherClass
> - public int compareTo (MyClass altro)
- int pubblico (SomeOtherClass o1, SomeOtherClass o2)
Osservazioni
Quando si implementa un compareTo(..)
che dipende da un double
, non effettuare le seguenti operazioni:
public int comareTo(MyClass other) {
return (int)(doubleField - other.doubleField); //THIS IS BAD
}
Il troncamento causato dal cast (int)
farà sì che il metodo restituisca a volte in modo errato 0
anziché un numero positivo o negativo e possa quindi portare a confronti e errori di ordinamento.
Invece, l'implementazione corretta più semplice è utilizzare Double.compare , in quanto tale:
public int comareTo(MyClass other) {
return Double.compare(doubleField,other.doubleField); //THIS IS GOOD
}
Una versione non generica di Comparable<T>
, semplicemente Comparable
, esiste da Java 1.2 . A parte l'interfaccia con il codice legacy, è sempre meglio implementare la versione generica Comparable<T>
, poiché non richiede il casting al confronto.
È molto normale che una classe sia paragonabile a se stessa, come in:
public class A implements Comparable<A>
Mentre è possibile uscire da questo paradigma, sii cauto quando lo fai.
Un Comparator<T>
può ancora essere utilizzato su istanze di una classe se tale classe implementa Comparable<T>
. In questo caso, verrà utilizzata la logica del Comparator
; l'ordine naturale specificato dall'implementazione Comparable
sarà ignorato.
Ordinamento di una lista usando Paragonabile o un comparatore
Diciamo che stiamo lavorando su una classe che rappresenta una persona con il loro nome e cognome. Abbiamo creato una classe base per farlo e implementato i metodi equals
e hashCode
appropriati.
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);
}
}
Ora vorremmo ordinare una lista di oggetti Person
per nome, come nel seguente scenario:
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.
}
Sfortunatamente, come indicato, quanto sopra non verrà compilato. Collections.sort(..)
sa solo come ordinare un elenco se gli elementi in quell'elenco sono comparabili o se viene fornito un metodo di confronto personalizzato.
Se ti è stato chiesto di ordinare il seguente elenco: 1,3,5,4,2
, non avresti problemi a dire che la risposta è 1,2,3,4,5
. Questo perché gli Integer (sia in Java che matematicamente) hanno un ordinamento naturale , un ordinamento di base di confronto standard di default. Per dare alla nostra classe Person un ordinamento naturale, implementiamo Comparable<Person>
, che richiede l'implementazione del metodo 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);
}
}
}
Ora, il metodo principale fornito funzionerà correttamente
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
}
Se, tuttavia, non si desidera o non è possibile modificare la classe Person
, è possibile fornire un Comparator<T>
personalizzato Comparator<T>
che gestisce il confronto di due oggetti Person
. Se ti è stato chiesto di ordinare il seguente elenco: circle, square, rectangle, triangle, hexagon
non puoi, ma se ti venisse chiesto di ordinare quell'elenco in base al numero di angoli , potresti farlo. Proprio così, fornire un comparatore indica a Java come confrontare due oggetti normalmente non confrontabili.
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
}
I comparatori possono anche essere creati / usati come una classe interiore anonima
//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...
}
});
}
Comparatori basati sull'espressione Lambda
A partire da Java 8, i comparatori possono anche essere espressi come espressioni lambda
//Lambda
Collections.sort(people, (p1, p2) -> { //Legal
//Method code....
});
Metodi predefiniti del comparatore
Inoltre, esistono interessanti metodi predefiniti nell'interfaccia di Comparatore per la costruzione di comparatori: il seguente costruisce un comparatore confrontandolo con lastName
e quindi firstName
.
Collections.sort(people, Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName));
Inversione dell'ordine di un comparatore
Qualsiasi comparatore può anche essere facilmente invertito usando il reversedMethod
che cambierà l'ordine crescente in discendente.
Il confronto e confrontare i metodi
L'interfaccia Comparable<T>
richiede un metodo:
public interface Comparable<T> {
public int compareTo(T other);
}
E l'interfaccia Comparator<T>
richiede un metodo:
public interface Comparator<T> {
public int compare(T t1, T t2);
}
Questi due metodi fanno essenzialmente la stessa cosa, con una differenza minore: compareTo
confronta this
con gli other
, mentre compare
confronta t1
con t2
, senza preoccuparsi affatto di this
.
A parte questa differenza, i due metodi hanno requisiti simili. Specificamente (per compareTo), confronta questo oggetto con l'oggetto specificato per l'ordine. Restituisce un numero intero negativo, zero o un numero intero positivo poiché questo oggetto è minore, uguale o maggiore dell'oggetto specificato. Così, per il confronto di a
e b
:
- Se
a < b
,a.compareTo(b)
ecompare(a,b)
dovrebbe restituire un intero negativo, eb.compareTo(a)
ecompare(b,a)
dovrebbe restituire un intero positivo - Se
a > b
,a.compareTo(b)
ecompare(a,b)
devono restituire un intero positivo eb.compareTo(a)
ecompare(b,a)
devono restituire un numero intero negativo - Se
a
ugualeb
per il confronto, tutti i confronti dovrebbero restituire0
.
Ordinamento naturale (comparabile) vs esplicito (comparatore)
Esistono due metodi Collections.sort()
:
- Uno che accetta un
List<T>
come parametro in cuiT
deve implementare Comparable e sovrascrive il metodocompareTo()
che determina l'ordinamento. - Uno che accetta un elenco e un comparatore come argomenti, in cui il comparatore determina l'ordinamento.
Innanzitutto, ecco una classe Person che implementa Comparable:
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();
}
}
Ecco come compareTo()
la classe precedente per ordinare una lista nell'ordinamento naturale dei suoi elementi, definito dal metodo di confronto 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);
Ecco come utilizzare un comparatore in linea anonimo per ordinare un elenco che non implementa Comparable o, in questo caso, per ordinare un elenco in un ordine diverso dall'ordinamento naturale:
//-- 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);
Ordinamento delle voci della mappa
A partire da Java 8, ci sono metodi predefiniti sull'interfaccia Map.Entry
per consentire l'ordinamento delle iterazioni della mappa.
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
Ovviamente, questi possono anche essere usati al di fuori della stream stream:
List<Map.Entry<String, Integer>> entries = new ArrayList<>(numberOfEmployees.entrySet());
Collections.sort(entries, Map.Entry.comparingByValue());
Creazione di un comparatore utilizzando il metodo di confronto
Comparator.comparing(Person::getName)
Questo crea un comparatore per la classe Person
che usa questo nome di persona come origine di confronto. Inoltre è possibile utilizzare la versione del metodo per confrontare long, int e double. Per esempio:
Comparator.comparingInt(Person::getAge)
Ordine inverso
Per creare un comparatore che impone l'ordine inverso usa il metodo reversed()
:
Comparator.comparing(Person::getName).reversed()
Catena di comparatori
Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName)
Questo creerà un comparatore che raffigura il cognome con il cognome e poi lo confronta con il nome. Puoi concatenare tutti i comparatori che vuoi.