Cómo comparar objetos por múltiples campos

Supongamos que tiene algunos objetos que tienen varios campos que pueden ser comparados por:

public class Person { private String firstName; private String lastName; private String age; /* Constructors */ /* Methods */ } 

Entonces en este ejemplo, cuando preguntas si:

 a.compareTo(b) > 0 

usted podría estar preguntando si el apellido de a viene antes de b, o si a es más viejo que b, etc.

¿Cuál es la forma más limpia de permitir la comparación múltiple entre este tipo de objetos sin agregar desorden innecesario o sobrecarga?

  • java.lang.Comparable interfaz java.lang.Comparable permite la comparación solo por un campo
  • Agregar numerosos métodos de comparación (es decir, compareByFirstName() , compareByAge() , etc …) está desordenado en mi opinión.

Entonces, ¿cuál es la mejor manera de hacerlo?

Puede escribir una clase de comparación que compare dos objetos Persona, y puede examinar tantos campos como desee. Puede poner una variable en su comparador que le diga a qué campo comparar, aunque probablemente sería más simple escribir múltiples comparadores.

Con Java 8:

 Comparator.comparing((Person p)->p.firstName) .thenComparing(p->p.lastName) .thenComparingInt(p->p.age); 

Si tiene métodos de acceso:

 Comparator.comparing(Person::getFirstName) .thenComparing(Person::getLastName) .thenComparingInt(Person::getAge); 

Si una clase implementa Comparable, dicho comparador puede usarse en el método compareTo:

 @Override public int compareTo(Person o){ return Comparator.comparing(Person::getFirstName) .thenComparing(Person::getLastName) .thenComparingInt(Person::getAge) .compare(this, o); } 

Debería implementar Comparable . Suponiendo que todos los campos no serán nulos (por razones de simplicidad), esa edad es un int, y la clasificación de comparación es first, last, age, el método compareTo es bastante simple:

 public int compareTo(Person other) { int i = firstName.compareTo(other.firstName); if (i != 0) return i; i = lastName.compareTo(other.lastName); if (i != 0) return i; return Integer.compare(age, other.age); } 

(de House of Code )

Desordenado y enrevesado: clasificación a mano

 Collections.sort(pizzas, new Comparator() { @Override public int compare(Pizza p1, Pizza p2) { int sizeCmp = p1.size.compareTo(p2.size); if (sizeCmp != 0) { return sizeCmp; } int nrOfToppingsCmp = p1.nrOfToppings.compareTo(p2.nrOfToppings); if (nrOfToppingsCmp != 0) { return nrOfToppingsCmp; } return p1.name.compareTo(p2.name); } }); 

Esto requiere mucho tipeo, mantenimiento y es propenso a errores.

La forma reflexiva: Ordenando con BeanComparator

 ComparatorChain chain = new ComparatorChain(Arrays.asList( new BeanComparator("size"), new BeanComparator("nrOfToppings"), new BeanComparator("name"))); Collections.sort(pizzas, chain); 

Obviamente, esto es más conciso, pero aún más propenso a errores, ya que pierde su referencia directa a los campos mediante el uso de cadenas en su lugar. Ahora, si se cambia el nombre de un campo, el comstackdor ni siquiera informará un problema. Además, debido a que esta solución usa reflexión, la clasificación es mucho más lenta.

Cómo llegar: clasificación con Google Guava’s ComparisonChain

 Collections.sort(pizzas, new Comparator() { @Override public int compare(Pizza p1, Pizza p2) { return ComparisonChain.start().compare(p1.size, p2.size).compare(p1.nrOfToppings, p2.nrOfToppings).compare(p1.name, p2.name).result(); // or in case the fields can be null: /* return ComparisonChain.start() .compare(p1.size, p2.size, Ordering.natural().nullsLast()) .compare(p1.nrOfToppings, p2.nrOfToppings, Ordering.natural().nullsLast()) .compare(p1.name, p2.name, Ordering.natural().nullsLast()) .result(); */ } }); 

Esto es mucho mejor, pero requiere un código de placa de caldera para el caso de uso más común: los valores nulos deben valorarse menos de manera predeterminada. Para null-fields, debe proporcionar una directiva adicional a Guava qué hacer en ese caso. Este es un mecanismo flexible si desea hacer algo específico, pero a menudo desea el caso predeterminado (es decir, 1, a, b, z, nulo).

Ordenando con Apache Commons CompareToBuilder

 Collections.sort(pizzas, new Comparator() { @Override public int compare(Pizza p1, Pizza p2) { return new CompareToBuilder().append(p1.size, p2.size).append(p1.nrOfToppings, p2.nrOfToppings).append(p1.name, p2.name).toComparison(); } }); 

Como Guava’s ComparisonChain, esta clase de biblioteca se ordena fácilmente en múltiples campos, pero también define el comportamiento predeterminado para valores nulos (es decir, 1, a, b, z, nulo). Sin embargo, tampoco puede especificar nada más, a menos que proporcione su propio Comparador.

Así

En última instancia todo se reduce al sabor y la necesidad de flexibilidad (Guava’s ComparisonChain) versus código conciso (CompareToBuilder de Apache).

Método de bonificación

Encontré una buena solución que combina múltiples comparadores por orden de prioridad en CodeReview en un MultiComparator :

 class MultiComparator implements Comparator { private final List> comparators; public MultiComparator(List> comparators) { this.comparators = comparators; } public MultiComparator(Comparator... comparators) { this(Arrays.asList(comparators)); } public int compare(T o1, T o2) { for (Comparator c : comparators) { int result = c.compare(o1, o2); if (result != 0) { return result; } } return 0; } public static  void sort(List list, Comparator... comparators) { Collections.sort(list, new MultiComparator(comparators)); } } 

Por supuesto, Apache Commons Collections ya tiene una utilidad para esto:

ComparatorUtils.chainedComparator (comparatorCollection)

 Collections.sort(list, ComparatorUtils.chainedComparator(comparators)); 

@Patrick Para ordenar más de un campo consecutivamente, pruebe ComparatorChain

Un ComparatorChain es un Comparador que envuelve uno o más Comparadores en secuencia. ComparatorChain llama a cada Comparador en secuencia hasta que 1) cualquier Comparador individual devuelve un resultado distinto de cero (y luego se devuelve ese resultado), o 2) Se agota el ComparatorChain (y se devuelve cero). Este tipo de ordenamiento es muy similar a la clasificación de múltiples columnas en SQL, y esta clase permite que las clases Java emulen ese tipo de comportamiento al ordenar una Lista.

Para facilitar aún más la clasificación tipo SQL, el orden de cualquier Comparador individual en la lista puede ser revertido.

Llamar a un método que agrega nuevos Comparadores o cambia el orden ascendente / descendente después de comparar (Objeto, Objeto) ha sido llamado dará como resultado una excepción de operación no admitida. Sin embargo, tenga cuidado de no alterar la Lista de comparadores subyacente o el BitSet que define el orden de clasificación.

Las instancias de ComparatorChain no están sincronizadas. La clase no es segura para subprocesos en el momento de la construcción, pero es segura para la ejecución de subprocesos después de que se completan todas las operaciones de configuración.

Otra opción que siempre puedes considerar es Apache Commons. Proporciona muchas opciones.

 import org.apache.commons.lang3.builder.CompareToBuilder; 

Ex:

 public int compare(Person a, Person b){ return new CompareToBuilder() .append(a.getName(), b.getName()) .append(a.getAddress(), b.getAddress()) .toComparison(); } 

También puedes echar un vistazo a Enum que implementa Comparator.

http://tobega.blogspot.com/2008/05/beautiful-enums.html

p.ej

 Collections.sort(myChildren, Child.Order.ByAge.descending()); 

Para aquellos que pueden usar la API de streaming Java 8, hay un enfoque más ordenado que está bien documentado aquí: Lambdas y clasificación

Estaba buscando el equivalente de C # LINQ:

 .ThenBy(...) 

Encontré el mecanismo en Java 8 en el Comparador:

 .thenComparing(...) 

Así que aquí está el fragmento que demuestra el algoritmo.

  Comparator comparator = Comparator.comparing(person -> person.name); comparator = comparator.thenComparing(Comparator.comparing(person -> person.age)); 

Consulte el enlace anterior para obtener una explicación más clara sobre cómo la inferencia de tipos de Java hace que sea un poco más complicado de definir en comparación con LINQ.

Aquí está la prueba unitaria completa para referencia:

 @Test public void testChainedSorting() { // Create the collection of people: ArrayList people = new ArrayList<>(); people.add(new Person("Dan", 4)); people.add(new Person("Andi", 2)); people.add(new Person("Bob", 42)); people.add(new Person("Debby", 3)); people.add(new Person("Bob", 72)); people.add(new Person("Barry", 20)); people.add(new Person("Cathy", 40)); people.add(new Person("Bob", 40)); people.add(new Person("Barry", 50)); // Define chained comparators: // Great article explaining this and how to make it even neater: // http://blog.jooq.org/2014/01/31/java-8-friday-goodies-lambdas-and-sorting/ Comparator comparator = Comparator.comparing(person -> person.name); comparator = comparator.thenComparing(Comparator.comparing(person -> person.age)); // Sort the stream: Stream personStream = people.stream().sorted(comparator); // Make sure that the output is as expected: List sortedPeople = personStream.collect(Collectors.toList()); Assert.assertEquals("Andi", sortedPeople.get(0).name); Assert.assertEquals(2, sortedPeople.get(0).age); Assert.assertEquals("Barry", sortedPeople.get(1).name); Assert.assertEquals(20, sortedPeople.get(1).age); Assert.assertEquals("Barry", sortedPeople.get(2).name); Assert.assertEquals(50, sortedPeople.get(2).age); Assert.assertEquals("Bob", sortedPeople.get(3).name); Assert.assertEquals(40, sortedPeople.get(3).age); Assert.assertEquals("Bob", sortedPeople.get(4).name); Assert.assertEquals(42, sortedPeople.get(4).age); Assert.assertEquals("Bob", sortedPeople.get(5).name); Assert.assertEquals(72, sortedPeople.get(5).age); Assert.assertEquals("Cathy", sortedPeople.get(6).name); Assert.assertEquals(40, sortedPeople.get(6).age); Assert.assertEquals("Dan", sortedPeople.get(7).name); Assert.assertEquals(4, sortedPeople.get(7).age); Assert.assertEquals("Debby", sortedPeople.get(8).name); Assert.assertEquals(3, sortedPeople.get(8).age); // Andi : 2 // Barry : 20 // Barry : 50 // Bob : 40 // Bob : 42 // Bob : 72 // Cathy : 40 // Dan : 4 // Debby : 3 } /** * A person in our system. */ public static class Person { /** * Creates a new person. * @param name The name of the person. * @param age The age of the person. */ public Person(String name, int age) { this.age = age; this.name = name; } /** * The name of the person. */ public String name; /** * The age of the person. */ public int age; @Override public String toString() { if (name == null) return super.toString(); else return String.format("%s : %d", this.name, this.age); } } 

Escribir un Comparator manualmente para un caso de uso así es una solución terrible IMO. Tales enfoques ad hoc tienen muchos inconvenientes:

  • Sin código reutilizado Viola SECO.
  • Repetitivo.
  • Mayor posibilidad de errores.

Entonces, ¿cuál es la solución?

Primero algo de teoría.

Denotemos la proposición “tipo A apoya la comparación” por Ord A (Desde la perspectiva del progtwig, puede pensar en Ord A como un objeto que contiene lógica para comparar dos A s. Sí, al igual que Comparator ).

Ahora bien, si Ord A y Ord B , entonces su compuesto (A, B) también debería ser compatible con la comparación. es decir, Ord (A, B) . Si Ord A , Ord B y Ord C , entonces Ord (A, B, C) .

Podemos extender este argumento a arity arbitrario, y decir:

Ord A, Ord B, Ord C, ..., Ord ZOrd (A, B, C, .., Z)

Llamemos a esta statement 1.

La comparación de los compuestos funcionará tal como lo describió en su pregunta: la primera comparación se probará primero, luego la siguiente, luego la siguiente, y así sucesivamente.

Esa es la primera parte de nuestra solución. Ahora la segunda parte.

Si sabes que Ord A , y sabes cómo transformar B en A (llama a esa función de transformación f ), entonces también puedes tener Ord B ¿Cómo? Bueno, cuando se comparen las dos instancias B , primero las transformará en A usando f y luego aplicará Ord A

Aquí, estamos mapeando la transformación B → A a Ord A → Ord B Esto se conoce como mapeo contravariante (o comap para abreviar).

Ord A, (B → A)comap Ord B

Llamemos a esta statement 2.


Ahora apliquemos esto a tu ejemplo.

Tiene un tipo de datos llamado Person que comprende tres campos de tipo String .

  • Sabemos que Ord String . Por la instrucción 1, Ord (String, String, String) .

  • Podemos escribir fácilmente una función de Person a (String, String, String) . (Simplemente devuelva los tres campos). Como conocemos Ord (String, String, String) y Person → (String, String, String) , según el enunciado 2, podemos usar comap para obtener Ord Person .

QED.


¿Cómo implemento todos estos conceptos?

La buena noticia es que no es necesario. Ya existe una biblioteca que implementa todas las ideas descritas en esta publicación. (Si tiene curiosidad de cómo se implementan estos, puede mirar debajo del capó ).

Así es como se verá el código con él:

 Ord personOrd = p3Ord(stringOrd, stringOrd, stringOrd).comap( new F>() { public P3 f(Person x) { return p(x.getFirstName(), x.getLastname(), x.getAge()); } } ); 

Explicación:

  • stringOrd es un objeto de tipo Ord . Esto corresponde a nuestra propuesta original de “compatibilidad de comparación”.
  • p3Ord es un método que toma Ord , Ord , Ord , y devuelve Ord> . Esto corresponde al enunciado 1. ( P3 significa producto con tres elementos. Producto es un término algebraico para compuestos).
  • comap corresponde a bien, comap .
  • F representa una función de transformación A → B
  • p es un método de fábrica para crear productos.
  • La expresión completa corresponde a la statement 2.

Espero que ayude.

En lugar de métodos de comparación, es posible que desee definir varios tipos de subclases “Comparador” dentro de la clase Persona. De esta forma, puede pasarlos a los métodos de clasificación de colecciones estándar.

 import com.google.common.collect.ComparisonChain; /** * @author radler * Class Description ... */ public class Attribute implements Comparable { private String type; private String value; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @Override public String toString() { return "Attribute [type=" + type + ", value=" + value + "]"; } @Override public int compareTo(Attribute that) { return ComparisonChain.start() .compare(this.type, that.type) .compare(this.value, that.value) .result(); } } 

Creo que sería más confuso si tu algoritmo de comparación fuera “inteligente”. Me gustaría ir con los numerosos métodos de comparación que sugirió.

La única excepción para mí sería la igualdad. Para las pruebas unitarias, me ha sido útil anular los .Equals (en .net) para determinar si varios campos son iguales entre dos objetos (y no que las referencias sean iguales).

Si hay varias formas en que un usuario puede pedir una persona, también puede tener la configuración de múltiples Comparadores como constantes en alguna parte. La mayoría de las operaciones de ordenación y colecciones ordenadas toman un comparador como parámetro.

 //here threshold,buyRange,targetPercentage are three keys on that i have sorted my arraylist final Comparator sortOrder = new Comparator() { public int compare(BasicDBObject e1, BasicDBObject e2) { int threshold = new Double(e1.getDouble("threshold")) .compareTo(new Double(e2.getDouble("threshold"))); if (threshold != 0) return threshold; int buyRange = new Double(e1.getDouble("buyRange")) .compareTo(new Double(e2.getDouble("buyRange"))); if (buyRange != 0) return buyRange; return (new Double(e1.getDouble("targetPercentage")) < new Double( e2.getDouble("targetPercentage")) ? -1 : (new Double( e1.getDouble("targetPercentage")) == new Double( e2.getDouble("targetPercentage")) ? 0 : 1)); } }; Collections.sort(objectList, sortOrder); 

Es fácil comparar dos objetos con el método hashcode en java`

 public class Sample{ String a=null; String b=null; public Sample(){ a="s"; b="a"; } public Sample(String a,String b){ this.a=a; this.b=b; } public static void main(String args[]){ Sample f=new Sample("b","12"); Sample s=new Sample("b","12"); //will return true System.out.println((sahashCode()+sbhashCode())==(fahashCode()+fbhashCode())); //will return false Sample f=new Sample("b","12"); Sample s=new Sample("b","13"); System.out.println((sahashCode()+sbhashCode())==(fahashCode()+fbhashCode())); } 

Si implementa la interfaz Comparable , querrá elegir una propiedad simple para ordenar. Esto se conoce como ordenamiento natural. Piense en esto como el predeterminado. Siempre se usa cuando no se proporciona un comparador específico. Por lo general, este es el nombre, pero su caso de uso puede requerir algo diferente. Usted es libre de usar cualquier cantidad de otros Comparadores que pueda suministrar a varias API de colecciones para anular el orden natural.

También tenga en cuenta que, por lo general, si a.compareTo (b) == 0, entonces a.equals (b) == verdadero. Está bien si no, pero hay efectos secundarios a tener en cuenta. Vea los excelentes javadocs en la interfaz Comparable y encontrará mucha información excelente sobre esto.

El siguiente blog muestra un buen ejemplo comparativo encadenado

http://www.codejava.net/java-core/collections/sorting-a-list-by-multiple-attributes-example

 import java.util.Arrays; import java.util.Comparator; import java.util.List; /** * This is a chained comparator that is used to sort a list by multiple * attributes by chaining a sequence of comparators of individual fields * together. * */ public class EmployeeChainedComparator implements Comparator { private List> listComparators; @SafeVarargs public EmployeeChainedComparator(Comparator... comparators) { this.listComparators = Arrays.asList(comparators); } @Override public int compare(Employee emp1, Employee emp2) { for (Comparator comparator : listComparators) { int result = comparator.compare(emp1, emp2); if (result != 0) { return result; } } return 0; } } 

Comparador de llamadas:

 Collections.sort(listEmployees, new EmployeeChainedComparator( new EmployeeJobTitleComparator(), new EmployeeAgeComparator(), new EmployeeSalaryComparator()) ); 

A partir de la respuesta de Steve, el operador ternario puede usarse:

 public int compareTo(Person other) { int f = firstName.compareTo(other.firstName); int l = lastName.compareTo(other.lastName); return f != 0 ? f : l != 0 ? l : Integer.compare(age, other.age); } 
 //Following is the example in jdk 1.8 package com; import java.util.ArrayList; import java.util.Comparator; import java.util.List; class User { private String firstName; private String lastName; private Integer age; public Integer getAge() { return age; } public User setAge(Integer age) { this.age = age; return this; } public String getFirstName() { return firstName; } public User setFirstName(String firstName) { this.firstName = firstName; return this; } public String getLastName() { return lastName; } public User setLastName(String lastName) { this.lastName = lastName; return this; } } public class MultiFieldsComparision { public static void main(String[] args) { List users = new ArrayList(); User u1 = new User().setFirstName("Pawan").setLastName("Singh").setAge(38); User u2 = new User().setFirstName("Pawan").setLastName("Payal").setAge(37); User u3 = new User().setFirstName("Anuj").setLastName("Kumar").setAge(60); User u4 = new User().setFirstName("Anuj").setLastName("Kumar").setAge(43); User u5 = new User().setFirstName("Pawan").setLastName("Chamoli").setAge(44); User u6 = new User().setFirstName("Pawan").setLastName("Singh").setAge(5); users.add(u1); users.add(u2); users.add(u3); users.add(u4); users.add(u5); users.add(u6); System.out.println("****** Before Sorting ******"); users.forEach(user -> { System.out.println(user.getFirstName() + " , " + user.getLastName() + " , " + user.getAge()); }); System.out.println("****** Aftre Sorting ******"); users.sort( Comparator.comparing(User::getFirstName).thenComparing(User::getLastName).thenComparing(User::getAge)); users.forEach(user -> { System.out.println(user.getFirstName() + " , " + user.getLastName() + " , " + user.getAge()); }); } } 

Usualmente anulo mi compareTo() esta manera cada vez que tengo que hacer una clasificación multinivel.

 public int compareTo(Song o) { // TODO Auto-generated method stub int comp1 = 10000000*(movie.compareTo(o.movie))+1000*(artist.compareTo(o.artist))+songLength; int comp2 = 10000000*(o.movie.compareTo(movie))+1000*(o.artist.compareTo(artist))+o.songLength; return comp1-comp2; } 

Aquí primero se da preferencia al nombre de la película, luego al artista y finalmente a songLength. Solo tienes que asegurarte de que esos multiplicadores estén lo suficientemente distantes como para no cruzar los límites del otro.

Es fácil de hacer usando la biblioteca de guayaba de Google .

por ejemplo, Objects.equal(name, name2) && Objects.equal(age, age2) && ...

Más ejemplos: