¿Cómo simplificar una implementación nula-segura compareTo ()?

Estoy implementando el compareTo() para una clase simple como esta (para poder usar Collections.sort() y otras ventajas ofrecidas por la plataforma Java):

 public class Metadata implements Comparable { private String name; private String value; // Imagine basic constructor and accessors here // Irrelevant parts omitted } 

Quiero que el orden natural para estos objetos sea: 1) ordenado por nombre y 2) ordenado por valor si el nombre es el mismo; ambas comparaciones deben ser insensibles a mayúsculas y minúsculas. Para ambos campos, los valores nulos son perfectamente aceptables, por lo que compareTo no debe romperse en estos casos.

La solución que me viene a la mente está en la línea de lo siguiente (estoy usando “cláusulas de guardia” aquí, mientras que otros preferirían un solo punto de retorno, pero eso está al lado del punto):

 // primarily by name, secondarily by value; null-safe; case-insensitive public int compareTo(Metadata other) { if (this.name == null && other.name != null){ return -1; } else if (this.name != null && other.name == null){ return 1; } else if (this.name != null && other.name != null) { int result = this.name.compareToIgnoreCase(other.name); if (result != 0){ return result; } } if (this.value == null) { return other.value == null ? 0 : -1; } if (other.value == null){ return 1; } return this.value.compareToIgnoreCase(other.value); } 

Esto hace el trabajo, pero no estoy muy contento con este código. Es cierto que no es muy complejo, pero es bastante prolijo y tedioso.

La pregunta es, ¿cómo harías esto menos detallado (conservando la funcionalidad)? No dude en consultar las bibliotecas estándar de Java o Apache Commons si lo ayudan. ¿La única opción para hacer esto (un poco) más simple sería implementar mi propio “NullSafeStringComparator” y aplicarlo para comparar ambos campos?

Ediciones 1-3 : Eddie tiene razón; arreglado el caso “ambos nombres son nulos” arriba

Sobre la respuesta aceptada

Hice esta pregunta en 2009, en Java 1.6 por supuesto, y en ese momento la solución pura de JDK por Eddie fue mi respuesta preferida. Nunca llegué a cambiar eso hasta ahora (2017).

También hay soluciones de biblioteca de terceros, una Apache Commons Collections one 2009 y una Guava one 2013, ambas publicadas por mí, que preferí en algún momento.

Ahora hice que la solución limpia de Java 8 de Lukasz Wiktor fuera la respuesta aceptada. Eso definitivamente debería ser preferido si está en Java 8, y en estos días Java 8 debería estar disponible para casi todos los proyectos.

Usando Java 8 :

 private static Comparator nullSafeStringComparator = Comparator .nullsFirst(String::compareToIgnoreCase); private static Comparator metadataComparator = Comparator .comparing(Metadata::getName, nullSafeStringComparator) .thenComparing(Metadata::getValue, nullSafeStringComparator); public int compareTo(Metadata that) { return metadataComparator.compare(this, that); } 

Simplemente puede usar Apache Commons Lang :

 result = ObjectUtils.compare(firstComparable, secondComparable) 

Implementaría un comparador nulo seguro. Puede haber una implementación, pero esto es tan sencillo de implementar que siempre he hecho mi parte.

Nota: Su comparador anterior, si ambos nombres son nulos, ni siquiera comparará los campos de valor. No creo que esto sea lo que quieres.

Implementaría esto con algo como lo siguiente:

 // primarily by name, secondarily by value; null-safe; case-insensitive public int compareTo(final Metadata other) { if (other == null) { throw new NullPointerException(); } int result = nullSafeStringComparator(this.name, other.name); if (result != 0) { return result; } return nullSafeStringComparator(this.value, other.value); } public static int nullSafeStringComparator(final String one, final String two) { if (one == null ^ two == null) { return (one == null) ? -1 : 1; } if (one == null && two == null) { return 0; } return one.compareToIgnoreCase(two); } 

EDITAR: corrigió los errores tipográficos en el ejemplo de código. ¡Eso es lo que obtengo por no probarlo primero!

EDITAR: Promocionó nullSafeStringComparator a static.

Vea la parte inferior de esta respuesta para una solución actualizada (2013) usando Guava.


Esto es a lo que finalmente fui. Resultó que ya teníamos un método de utilidad para la comparación String nula, por lo que la solución más simple era hacer uso de eso. (Es una gran base de código, es fácil perderse este tipo de cosas 🙂

 public int compareTo(Metadata other) { int result = StringUtils.compare(this.getName(), other.getName(), true); if (result != 0) { return result; } return StringUtils.compare(this.getValue(), other.getValue(), true); } 

Así es como se define el helper (está sobrecargado para que también pueda definir si los nulos son los primeros o los últimos, si lo desea):

 public static int compare(String s1, String s2, boolean ignoreCase) { ... } 

Así que esto es esencialmente lo mismo que la respuesta de Eddie (aunque yo no llamaría a un método auxiliar estático un comparador ) y la de uzhin también.

De todos modos, en general, hubiera favorecido fuertemente la solución de Patrick , ya que creo que es una buena práctica usar bibliotecas establecidas siempre que sea posible. ( Conozca y use las bibliotecas como dice Josh Bloch.) Pero en este caso, eso no habría arrojado el código más limpio y simple.

Edición (2009): versión de Apache Commons Collections

En realidad, aquí hay una manera de simplificar la solución basada en Apache Commons NullComparator . Combínelo con el Comparator insensible a mayúsculas proporcionado en String clase String :

 public static final Comparator NULL_SAFE_COMPARATOR = new NullComparator(String.CASE_INSENSITIVE_ORDER); @Override public int compareTo(Metadata other) { int result = NULL_SAFE_COMPARATOR.compare(this.name, other.name); if (result != 0) { return result; } return NULL_SAFE_COMPARATOR.compare(this.value, other.value); } 

Ahora esto es bastante elegante, creo. (Solo queda un pequeño problema: el Commons NullComparator no es compatible con los generics, por lo que hay una asignación sin NullComparator ).

Actualización (2013): versión Guava

Casi 5 años después, así es cómo abordaría mi pregunta original. Si codificara en Java, estaría (por supuesto) usando Guava . (Y ciertamente no Apache Commons.)

Pon esto constante en alguna parte, por ejemplo, en la clase “StringUtils”:

 public static final Ordering CASE_INSENSITIVE_NULL_SAFE_ORDER = Ordering.from(String.CASE_INSENSITIVE_ORDER).nullsLast(); // or nullsFirst() 

Luego, en public class Metadata implements Comparable :

 @Override public int compareTo(Metadata other) { int result = CASE_INSENSITIVE_NULL_SAFE_ORDER.compare(this.name, other.name); if (result != 0) { return result; } return CASE_INSENSITIVE_NULL_SAFE_ORDER.compare(this.value, other.value); } 

Por supuesto, esto es casi idéntico a la versión de Apache Commons (ambos usan CASE_INSENSITIVE_ORDER de JDK), el uso de nullsLast() es lo único específico de Guava. Esta versión es preferible simplemente porque Guava es preferible, como dependencia, a Colecciones de Commons. (Como todos están de acuerdo )

Si estaba pensando en Ordering , tenga en cuenta que implementa Comparator . Es bastante útil, especialmente para necesidades de clasificación más complejas, lo que le permite, por ejemplo, encadenar varias órdenes utilizando compound() . ¡Leer explicaciones explicadas para más!

Siempre recomiendo usar Apache commons ya que probablemente sea mejor que uno que pueda escribir por su cuenta. Además, puede hacer un trabajo “real” en lugar de reinventar.

La clase en la que estás interesado es el Comparador nulo . Le permite hacer nulls alto o bajo. También le da su propio comparador para usar cuando los dos valores no son nulos.

En su caso, puede tener una variable de miembro estático que haga la comparación y luego su método compareTo simplemente haga referencia a eso.

Algo así como

 class Metadata implements Comparable { private String name; private String value; static NullComparator nullAndCaseInsensitveComparator = new NullComparator( new Comparator() { @Override public int compare(String o1, String o2) { // inputs can't be null return o1.compareToIgnoreCase(o2); } }); @Override public int compareTo(Metadata other) { if (other == null) { return 1; } int res = nullAndCaseInsensitveComparator.compare(name, other.name); if (res != 0) return res; return nullAndCaseInsensitveComparator.compare(value, other.value); } 

}

Incluso si decide rodar el suyo, tenga en cuenta esta clase, ya que es muy útil cuando ordena listas que contienen elementos nulos.

Sé que puede que no responda directamente a su pregunta, porque dijo que los valores nulos deben ser compatibles.

Pero solo quiero señalar que admitir nulos en compareTo no está en línea con comparar un contrato descrito en javadocs oficiales para Comparable :

Tenga en cuenta que null no es una instancia de ninguna clase, y e.compareTo (null) debería arrojar una NullPointerException aunque e.equals (null) devuelve false.

Así que lanzaría NullPointerException explícitamente o simplemente dejaría que se lanzara la primera vez cuando se desreferencie el argumento nulo.

Puedes extraer el método:

 public int cmp(String txt, String otherTxt) { if ( txt == null ) return otjerTxt == null ? 0 : 1; if ( otherTxt == null ) return 1; return txt.compareToIgnoreCase(otherTxt); } public int compareTo(Metadata other) { int result = cmp( name, other.name); if ( result != 0 ) return result; return cmp( value, other.value); 

}

Podrías diseñar tu clase para que sea inmutable (Effective Java 2nd Ed. Tiene una gran sección sobre esto, Item 15: Minimize mutability) y asegúrate durante la construcción de que no hay nulos posibles (y usa el patrón de objeto nulo si es necesario). Luego puede omitir todos esos controles y asumir de manera segura que los valores no son nulos.

Estaba buscando algo similar y esto parecía un poco complicado, así que hice esto. Creo que es un poco más fácil de entender. Puede usarlo como un Comparador o como un trazador de líneas. Para esta pregunta, cambiaría a compareToIgnoreCase (). Como está, los nulos flotan. Puedes voltear el 1, -1 si quieres que se hundan.

 StringUtil.NULL_SAFE_COMPARATOR.compare(getName(), o.getName()); 

.

 public class StringUtil { public static final Comparator NULL_SAFE_COMPARATOR = new Comparator() { @Override public int compare(final String s1, final String s2) { if (s1 == s2) { //Nulls or exact equality return 0; } else if (s1 == null) { //s1 null and s2 not null, so s1 less return -1; } else if (s2 == null) { //s2 null and s1 not null, so s1 greater return 1; } else { return s1.compareTo(s2); } } }; public static void main(String args[]) { final ArrayList list = new ArrayList(Arrays.asList(new String[]{"qad", "bad", "sad", null, "had"})); Collections.sort(list, NULL_SAFE_COMPARATOR); System.out.println(list); } } 

podemos usar java 8 para hacer una comparación amistosa con el nulo entre objeto. Supongo que tengo una clase Boy con 2 campos: Nombre de cadena y Edad entera y quiero comparar primero los nombres y luego las edades si ambos son iguales.

 static void test2() { List list = new ArrayList<>(); list.add(new Boy("Peter", null)); list.add(new Boy("Tom", 24)); list.add(new Boy("Peter", 20)); list.add(new Boy("Peter", 23)); list.add(new Boy("Peter", 18)); list.add(new Boy(null, 19)); list.add(new Boy(null, 12)); list.add(new Boy(null, 24)); list.add(new Boy("Peter", null)); list.add(new Boy(null, 21)); list.add(new Boy("John", 30)); List list2 = list.stream() .sorted(comparing(Boy::getName, nullsLast(naturalOrder())) .thenComparing(Boy::getAge, nullsLast(naturalOrder()))) .collect(toList()); list2.stream().forEach(System.out::println); } private static class Boy { private String name; private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Boy(String name, Integer age) { this.name = name; this.age = age; } public String toString() { return "name: " + name + " age: " + age; } } 

y el resultado:

  name: John age: 30 name: Peter age: 18 name: Peter age: 20 name: Peter age: 23 name: Peter age: null name: Peter age: null name: Tom age: 24 name: null age: 12 name: null age: 19 name: null age: 21 name: null age: 24 

En caso de que alguien use Spring, existe una clase org.springframework.util.comparator.NullSafeComparator que hace esto por usted también. Simplemente decora tu propia comparable con ella así

new NullSafeComparator(new YourComparable(), true)

https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/util/comparator/NullSafeComparator.html

 import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Comparator; public class TestClass { public static void main(String[] args) { Student s1 = new Student("1","Nikhil"); Student s2 = new Student("1","*"); Student s3 = new Student("1",null); Student s11 = new Student("2","Nikhil"); Student s12 = new Student("2","*"); Student s13 = new Student("2",null); List list = new ArrayList(); list.add(s1); list.add(s2); list.add(s3); list.add(s11); list.add(s12); list.add(s13); list.sort(Comparator.comparing(Student::getName,Comparator.nullsLast(Comparator.naturalOrder()))); for (Iterator iterator = list.iterator(); iterator.hasNext();) { Student student = (Student) iterator.next(); System.out.println(student); } } } 

salida es

 Student [name=*, id=1] Student [name=*, id=2] Student [name=Nikhil, id=1] Student [name=Nikhil, id=2] Student [name=null, id=1] Student [name=null, id=2] 

Otro ejemplo de Apache ObjectUtils. Capaz de clasificar otros tipos de objetos.

 @Override public int compare(Object o1, Object o2) { String s1 = ObjectUtils.toString(o1); String s2 = ObjectUtils.toString(o2); return s1.toLowerCase().compareTo(s2.toLowerCase()); } 

Usando Java 7 :

 public int compareNullFirst(String str1, String str2) { if (Objects.equals(str1, str2)) { return 0; } else { return str1 == null ? -1 : (str2 == null ? 1 : str1.compareToIgnoreCase(str2)); } } 

Esta es mi implementación que uso para ordenar mi ArrayList. las clases nulas están ordenadas a la última.

en mi caso, EntityPhone amplía EntityAbstract y mi contenedor es List .

el método “compareIfNull ()” se usa para la clasificación segura nula. Los otros métodos son para completar, mostrando cómo se puede usar compareIfNull.

 @Nullable private static Integer compareIfNull(EntityPhone ep1, EntityPhone ep2) { if (ep1 == null || ep2 == null) { if (ep1 == ep2) { return 0; } return ep1 == null ? -1 : 1; } return null; } private static final Comparator AbsComparatorByName = = new Comparator() { @Override public int compare(EntityAbstract ea1, EntityAbstract ea2) { //sort type Phone first. EntityPhone ep1 = getEntityPhone(ea1); EntityPhone ep2 = getEntityPhone(ea2); //null compare Integer x = compareIfNull(ep1, ep2); if (x != null) return x; String name1 = ep1.getName().toUpperCase(); String name2 = ep2.getName().toUpperCase(); return name1.compareTo(name2); } } private static EntityPhone getEntityPhone(EntityAbstract ea) { return (ea != null && ea.getClass() == EntityPhone.class) ? (EntityPhone) ea : null; } 

Para el caso específico en el que sabe que los datos no tendrán valores nulos (siempre es una buena idea para las cadenas) y los datos son realmente grandes, todavía está haciendo tres comparaciones antes de comparar realmente los valores, si está seguro de que este es su caso , puedes optimizar un poco. YMMV como código legible supera la optimización menor:

  if(o1.name != null && o2.name != null){ return o1.name.compareToIgnoreCase(o2.name); } // at least one is null return (o1.name == o2.name) ? 0 : (o1.name != null ? 1 : -1);