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


157

Estoy implementando un compareTo()método para una clase simple como esta (para poder usar Collections.sort()y otras cosas que ofrece la plataforma Java):

public class Metadata implements Comparable<Metadata> {
    private String name;
    private String value;

// Imagine basic constructor and accessors here
// Irrelevant parts omitted
}

Quiero que el orden natural de 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 compareToque no deben romperse en estos casos.

La solución que me viene a la mente es la siguiente: (estoy usando "cláusulas de protección" aquí, mientras que otros prefieren un único punto de retorno, pero eso no viene al caso):

// 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 perfectamente satisfecho con este código. Es cierto que no es muy complejo, pero es bastante detallado y tedioso.

La pregunta es, ¿cómo haría esto menos detallado (al tiempo que conserva la funcionalidad)? No dude en consultar las bibliotecas estándar de Java o Apache Commons si le ayudan. ¿La única opción para simplificar esto (un poco) sería implementar mi propio "NullSafeStringComparator" y aplicarlo para comparar ambos campos?

Ediciones 1-3 : Eddie tiene razón; arreglado el caso anterior de "ambos nombres son nulos"

Sobre la respuesta aceptada

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

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

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


Respuestas:


173

Usando Java 8 :

private static Comparator<String> nullSafeStringComparator = Comparator
        .nullsFirst(String::compareToIgnoreCase); 

private static Comparator<Metadata> metadataComparator = Comparator
        .comparing(Metadata::getName, nullSafeStringComparator)
        .thenComparing(Metadata::getValue, nullSafeStringComparator);

public int compareTo(Metadata that) {
    return metadataComparator.compare(this, that);
}

77
Apoyo el uso de cosas incorporadas de Java 8 a favor de Apache Commons Lang, pero ese código de Java 8 es bastante feo y aún detallado. Me quedaré con org.apache.commons.lang3.builder.CompareToBuilder por el momento.
jschreiner

1
Esto no funciona para Collections.sort (Arrays.asList (null, val1, null, val2, null)) ya que intentará llamar a compareTo () en un objeto nulo. Para ser honesto, parece un problema con el marco de colecciones, tratando de descubrir cómo arrinconar esto.
Pedro Borges

3
@PedroBorges El autor preguntó sobre la clasificación de objetos contenedores que poseen campos ordenables (donde esos campos pueden ser nulos), no sobre la clasificación de referencias de contenedores nulos. Entonces, si bien su comentario es correcto, ya que Collections.sort(List)no funciona cuando la Lista contiene valores nulos, el comentario no es relevante para la pregunta.
Scrubbie

211

Simplemente puede usar Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)

44
(@Kong: Esto se encarga de la seguridad nula pero no de la insensibilidad a mayúsculas y minúsculas, que era otro aspecto de la pregunta original. Por lo tanto, no cambia la respuesta aceptada).
Jonik

3
Además, en mi opinión, Apache Commons no debería ser la respuesta aceptada en 2013. (Incluso si algunos subproyectos se mantienen mejor que otros). La guayaba se puede usar para lograr lo mismo ; ver nullsFirst()/ nullsLast().
Jonik

8
@ Jonik ¿Por qué crees que Apache Commons no debería ser la respuesta aceptada en 2013?
realmente agradable

1
Grandes partes de Apache Commons son material heredado / mal mantenido / de baja calidad. Hay mejores alternativas para la mayoría de las cosas que ofrece, por ejemplo, en Guava, que es una lib de muy alta calidad, y cada vez más en JDK. Alrededor de 2005 sí, Apache Commons era la mierda, pero en estos días no es necesario en la mayoría de los proyectos. (Claro, hay excepciones; por ejemplo, si necesito un cliente FTP por alguna razón, probablemente
usaré

66
@ Jonik, ¿cómo responderías la pregunta usando Guava? Su afirmación de que Apache Commons Lang (paquete org.apache.commons.lang3) es "heredado / mal mantenido / de baja calidad" es falsa o, en el mejor de los casos, carece de fundamento. Commons Lang3 es fácil de entender y usar, y se mantiene activamente. Probablemente sea mi biblioteca más utilizada (aparte de Spring Framework y Spring Security): la clase StringUtils con sus métodos de seguridad nula hace que la normalización de entrada sea trivial, por ejemplo.
Paul

93

Implementaría un comparador seguro nulo. Puede haber una implementación por ahí, pero es tan fácil de implementar que siempre he implementado la mía.

Nota: Su comparador arriba, 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: Corregidos errores tipográficos en el código de muestra. ¡Eso es lo que obtengo por no probarlo primero!

EDITAR: Promocionado nullSafeStringComparator a estático.


2
Con respecto al "if" anidado ... creo que el if anidado es menos legible para este caso, así que lo evito. Sí, a veces habrá una comparación innecesaria debido a esto. El final para los parámetros no es necesario, pero es una buena idea.
Eddie el

31
Dulce uso de XOR
James McMahon

9
@phihag: sé que han pasado más de 3 años, PERO ... la finalpalabra clave no es realmente necesaria (el código Java ya es detallado). Sin embargo, evita la reutilización de parámetros como variables locales (una práctica de codificación terrible). nuestra comprensión colectiva del software mejora con el tiempo, sabemos que las cosas deben ser finales / constantes / inmutables por defecto. Por lo tanto, prefiero un poco más de verbosidad en el uso finalde declaraciones de parámetros (por muy trivial que sea la función) para obtener inmutability-by-quasi-default). La sobrecarga de comprensión / mantenibilidad es insignificante en el gran esquema de las cosas.
luis.espinal

24
@ James McMahon Tengo que estar en desacuerdo. Xor (^) podría simplemente ser reemplazado por no igual (! =). Incluso se compila con el mismo código de bytes. El uso de! = Vs ^ es solo una cuestión de gusto y legibilidad. Entonces, a juzgar por el hecho de que te sorprendiste, diría que no pertenece aquí. Use xor cuando intente calcular una suma de verificación. En la mayoría de los otros casos (como este), sigamos! =.
bvdb

55
Es fácil extender esta respuesta reemplazando String por T, T declarada como <T extend Comparable <T>> ... y luego podemos comparar de forma segura cualquier objeto comparable anulable
Thierry

20

Consulte la parte inferior de esta respuesta para obtener una solución actualizada (2013) con Guava.


Esto es con lo que finalmente fui. Resultó que ya teníamos un método de utilidad para la comparación de cadenas de seguridad 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 pasar por alto 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 ayudante (está sobrecargado para que también pueda definir si los nulos son lo primero o lo último, si lo desea):

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

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

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

Editar (2009): versión de las colecciones de Apache Commons

En realidad, aquí hay una manera de simplificar la solución basada en Apache Commons NullComparator. Combínelo con las mayúsculas y minúsculasComparator proporcionadas en la Stringclase:

public static final Comparator<String> 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: los Comunes NullComparatorno admiten genéricos, por lo que hay una asignación sin control)

Actualización (2013): versión de guayaba

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

Ponga esta constante en alguna parte, por ejemplo, en la clase "StringUtils":

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

Luego, en public class Metadata implements Comparable<Metadata>:

@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()ser la única cosa específica de Guava. Esta versión es preferible simplemente porque Guava es preferible, como una dependencia, a Commons Collections. (Como todos están de acuerdo )

Si te estabas preguntando Ordering, ten en cuenta que se implementa Comparator. Es bastante útil especialmente para necesidades de clasificación más complejas, lo que le permite, por ejemplo, encadenar varios pedidos usando compound(). Leer pedidos explicados para más!


2
String.CASE_INSENSITIVE_ORDER realmente hace que la solución sea mucho más limpia. Buena actualización
Patrick

2
Si usa Apache Commons de todos modos, hay ComparatorChainun compareTométodo para que no necesite un método propio .
Amoebe

13

Siempre recomiendo usar los recursos comunes de Apache, 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 que le interesa es el comparador nulo . Le permite hacer nulos altos o bajos. También le da su propio comparador para usar cuando los dos valores no son nulos.

En su caso, puede tener una variable miembro estática que haga la comparación y luego su compareTométodo solo hace referencia a eso.

Algo como

class Metadata implements Comparable<Metadata> {
private String name;
private String value;

static NullComparator nullAndCaseInsensitveComparator = new NullComparator(
        new Comparator<String>() {

            @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 la suya, tenga en cuenta esta clase, ya que es muy útil al ordenar listas que contienen elementos nulos.


¡Gracias, esperaba que hubiera algo como esto en Commons! En este caso, sin embargo, no
terminé

Acabo de darme cuenta de que su enfoque puede simplificarse mediante el uso de String.CASE_INSENSITIVE_ORDER; vea mi respuesta de seguimiento editada.
Jonik 01 de

Esto es bueno, pero la comprobación "if (other == null) {" no debería estar allí. El Javadoc para Comparable dice que compareTo debería lanzar una NullPointerException si otra es nula.
Daniel Alexiuc

7

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

Pero solo quiero señalar que admitir nulos en compareTo no está en línea con el contrato compareTo 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) devuelva false.

Por lo tanto, arrojaría NullPointerException explícitamente o simplemente dejaría que se lance la primera vez cuando se desreferencia el argumento nulo.


4

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); 

}


2
¿Debería el "0: 1" no ser "0: -1"?
Rolf Kristensen

3

Puede diseñar su clase para que sea inmutable (Effective Java 2nd Ed. Tiene una gran sección sobre esto, Elemento 15: Minimizar la mutabilidad) y asegurarse de que no haya valores nulos posibles (y use el patrón de objetos nulos si es necesario). Luego puede omitir todas esas comprobaciones y asumir con seguridad que los valores no son nulos.


Sí, generalmente es una buena solución y simplifica muchas cosas, pero aquí estaba más interesado en el caso en que los valores nulos están permitidos, por una razón u otra, y deben tenerse en cuenta :)
Jonik

2

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 revestimiento. Para esta pregunta, cambiaría a compareToIgnoreCase (). Como es, 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<String> NULL_SAFE_COMPARATOR = new Comparator<String>() {

        @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<String> list = new ArrayList<String>(Arrays.asList(new String[]{"qad", "bad", "sad", null, "had"}));
        Collections.sort(list, NULL_SAFE_COMPARATOR);

        System.out.println(list);
    }
}

2

Podemos usar Java 8 para hacer una comparación nula entre objetos. supuse que tenía 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<Boy> 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<Boy> 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


1

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 sabe con certeza 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);

1
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<Student> list = new ArrayList<Student>();
        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);
        }


    }

}

la 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]

1

Una de las formas simples de usar NullSafe Comparator es usar la implementación de Spring de la misma, a continuación se muestra uno de los ejemplos simples para referirse:

public int compare(Object o1, Object o2) {
        ValidationMessage m1 = (ValidationMessage) o1;
        ValidationMessage m2 = (ValidationMessage) o2;
        int c;
        if (m1.getTimestamp() == m2.getTimestamp()) {
            c = NullSafeComparator.NULLS_HIGH.compare(m1.getProperty(), m2.getProperty());
            if (c == 0) {
                c = m1.getSeverity().compareTo(m2.getSeverity());
                if (c == 0) {
                    c = m1.getMessage().compareTo(m2.getMessage());
                }
            }
        }
        else {
            c = (m1.getTimestamp() > m2.getTimestamp()) ? -1 : 1;
        }
        return c;
    }

0

Otro ejemplo de Apache ObjectUtils. Capaz de ordenar 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());
}

0

Esta es mi implementación que uso para ordenar mi ArrayList. Las clases nulas se ordenan hasta el final.

para mi caso, EntityPhone extiende EntityAbstract y mi contenedor es List <EntityAbstract>.

El método "compareIfNull ()" se utiliza para la ordenación segura nula. Los otros métodos son para completar, mostrando cómo se puede comparar 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<EntityAbstract> AbsComparatorByName = = new Comparator<EntityAbstract>() {
    @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;
}

0

Si quieres un Hack simple:

arrlist.sort((o1, o2) -> {
    if (o1.getName() == null) o1.setName("");
    if (o2.getName() == null) o2.setName("");

    return o1.getName().compareTo(o2.getName());
})

si desea poner nulos al final de la lista, simplemente cambie esto en el método anterior

return o2.getName().compareTo(o1.getName());
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.