Java Language
Comparable y comparador
Buscar..
Sintaxis
- clase pública implementa MyClass Comparable
<MyClass
> - la clase pública MyComparator implementa Comparator
<SomeOtherClass
> - public int compareTo (MyClass other)
- public int compare (SomeOtherClass o1, SomeOtherClass o2)
Observaciones
Al implementar un compareTo(..)
que depende de un double
, no haga lo siguiente:
public int comareTo(MyClass other) {
return (int)(doubleField - other.doubleField); //THIS IS BAD
}
El truncamiento causado por (int)
cast causará que el método a veces devuelva incorrectamente 0
lugar de un número positivo o negativo, y por lo tanto puede llevar a errores de comparación y clasificación.
En su lugar, la implementación correcta más simple es usar Double.compare , como tal:
public int comareTo(MyClass other) {
return Double.compare(doubleField,other.doubleField); //THIS IS GOOD
}
Una versión no genérica de Comparable<T>
, simplemente Comparable
, ha existido desde Java 1.2 . Además de interactuar con el código heredado, siempre es mejor implementar la versión genérica Comparable<T>
, ya que no requiere conversión en la comparación.
Es muy estándar que una clase sea comparable a sí misma, como en:
public class A implements Comparable<A>
Si bien es posible romper con este paradigma, tenga cuidado al hacerlo.
Un Comparator<T>
todavía se puede usar en instancias de una clase si esa clase implementa Comparable<T>
. En este caso, se utilizará la lógica del Comparator
; El orden natural especificado por la implementación Comparable
será ignorado.
Ordenar una lista usando Comparable o un comparador
Digamos que estamos trabajando en una clase que representa a una persona por su nombre y apellido. Hemos creado una clase básica para hacer esto e implementado los métodos equals
y hashCode
adecuados.
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);
}
}
Ahora nos gustaría ordenar una lista de objetos Person
por su nombre, como en el siguiente escenario:
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.
}
Desafortunadamente, como está marcado, lo anterior actualmente no compilará. Collections.sort(..)
solo sabe cómo ordenar una lista si los elementos de esa lista son comparables o si se proporciona un método personalizado de comparación.
Si le pidieran que clasificara la siguiente lista: 1,3,5,4,2
, no tendría ningún problema en decir que la respuesta es 1,2,3,4,5
. Esto se debe a que los enteros (tanto en Java como matemáticamente) tienen un orden natural , un orden de comparación estándar predeterminado. Para dar a nuestra clase Person un orden natural, implementamos la Comparable<Person>
, que requiere la implementación del método 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);
}
}
}
Ahora, el método principal dado funcionará correctamente.
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
}
Sin embargo, si no desea o no puede modificar la clase Person
, puede proporcionar un Comparator<T>
personalizado Comparator<T>
que maneje la comparación de cualquiera de los dos objetos Person
. Si le pidieran que clasificara la siguiente lista: circle, square, rectangle, triangle, hexagon
no podría, pero si le pidieran que clasificara esa lista según el número de esquinas , podría hacerlo. De este modo, al proporcionar un comparador se le indica a Java cómo comparar dos objetos que normalmente no son comparables.
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
}
Los comparadores también se pueden crear / usar como una clase interna anónima
//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...
}
});
}
Comparadores basados en expresiones Lambda
A partir de Java 8, los comparadores también se pueden expresar como expresiones lambda
//Lambda
Collections.sort(people, (p1, p2) -> { //Legal
//Method code....
});
Métodos predeterminados del comparador
Por otra parte, existen métodos predeterminados interesantes en la interfaz de comparación para la construcción de comparadores: la siguiente construye un comparador compara por lastName
y luego firstName
.
Collections.sort(people, Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName));
Invertir el orden de un comparador
Cualquier comparador también puede revertirse fácilmente usando el método reversedMethod
que cambiará el orden ascendente a descendente.
Los métodos de comparar y comparar
La interfaz Comparable<T>
requiere un método:
public interface Comparable<T> {
public int compareTo(T other);
}
Y la interfaz del Comparator<T>
requiere un método:
public interface Comparator<T> {
public int compare(T t1, T t2);
}
Estos dos métodos hacen esencialmente lo mismo, con una pequeña diferencia: compareTo
compara this
con other
, mientras que compare
compara t1
con t2
, sin preocuparse en absoluto por this
.
Aparte de esa diferencia, los dos métodos tienen requisitos similares. Específicamente (para compareTo), compara este objeto con el objeto especificado para orden. Devuelve un entero negativo, cero o un entero positivo, ya que este objeto es menor, igual o mayor que el objeto especificado. Así, para la comparación de a
y b
:
- Si
a < b
,a.compareTo(b)
ycompare(a,b)
debe devolver un entero negativo, yb.compareTo(a)
ycompare(b,a)
deben devolver un entero positivo - Si
a > b
,a.compareTo(b)
ycompare(a,b)
debe devolver un entero positivo, yb.compareTo(a)
ycompare(b,a)
deben devolver un entero negativo - Si
a
es igual ab
para la comparación, todas las comparaciones deben devolver0
.
Clasificación natural (comparable) vs explícita (comparativa)
Hay dos métodos Collections.sort()
:
- Uno que toma una
List<T>
como parámetro dondeT
debe implementar Comparable y anular elcompareTo()
que determina el orden de clasificación. - Uno que toma una Lista y un Comparador como los argumentos, donde el Comparador determina el orden de clasificación.
Primero, aquí hay una clase de persona que 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();
}
}
Aquí es cómo usaría la clase anterior para ordenar una Lista en el orden natural de sus elementos, definido por la anulación del método 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);
Aquí es cómo usaría un Comparador en línea anónimo para ordenar una Lista que no implementa Comparable, o en este caso, para ordenar una Lista en un orden diferente al orden natural:
//-- 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);
Ordenar entradas de mapa
A partir de Java 8, hay métodos predeterminados en la interfaz Map.Entry
para permitir la clasificación de iteraciones de mapas.
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
Por supuesto, estos también se pueden utilizar fuera de la api de flujo:
List<Map.Entry<String, Integer>> entries = new ArrayList<>(numberOfEmployees.entrySet());
Collections.sort(entries, Map.Entry.comparingByValue());
Creando un comparador usando el método de comparación
Comparator.comparing(Person::getName)
Esto crea un comparador para la clase Person
que usa este nombre de persona como fuente de comparación. También es posible usar la versión del método para comparar long, int y double. Por ejemplo:
Comparator.comparingInt(Person::getAge)
Orden invertida
Para crear un comparador que imponga el orden inverso, use el método reversed()
:
Comparator.comparing(Person::getName).reversed()
Cadena de comparadores
Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName)
Esto creará un comparador que se compara con el apellido y luego con el nombre. Puedes encadenar tantos comparadores como quieras.