Pregunta

estoy implementando compareTo() método para una clase simple como esta (para poder usar Collections.sort() y otras ventajas 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 el orden natural para que estos objetos sean:1) ordenado por nombre y 2) ordenado por valor si el nombre es el mismo;ambas comparaciones no deben distinguir entre mayúsculas y minúsculas.Para ambos campos los valores nulos son perfectamente aceptables, por lo que compareTo En estos casos no debe romperse.

La solución que me viene a la mente es la siguiente (estoy usando "cláusulas de protección" aquí, mientras que otros podrían preferir 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 funciona, pero no estoy del todo satisfecho con este código.Es cierto que no lo es muy complejo, pero bastante detallado 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 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;Se corrigió 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 JDK pura de Eddie Fue mi respuesta aceptada preferida.Nunca logré cambiar eso hasta ahora (2017).

También hay Soluciones de biblioteca de terceros—uno de Apache Commons Collections de 2009 y uno de Guava de 2013, ambos publicados por mí— que prefiero en algún momento.

ahora hice la limpieza Solución Java 8 de Lukasz Wiktor la respuesta aceptada.Definitivamente debería preferirse esto si está en Java 8, y en estos días Java 8 debería estar disponible para casi todos los proyectos.

¿Fue útil?

Solución

Uso de 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);
}

Otros consejos

Simplemente puede usar Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)

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 anterior, si los 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: Se corrigieron errores tipográficos en el ejemplo de código. ¡Eso es lo que obtengo por no probarlo primero!

EDITAR: Promovió nullSafeStringComparator a estático.

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 valores nulos son primeros o últimos, 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 la de uzhin también.

De todos modos, en general, habría preferido a 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 habría producido el código más simple y limpio.

Editar (2009): versión de Colecciones de Apache Commons

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

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: Commons public class Metadata implements Comparable<Metadata> no admite genéricos, por lo que hay una asignación no verificada).

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

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

Ponga esta constante en alguna parte, p. en " StringUtils " clase:

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

Luego, en nullsLast():

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

Si se preguntaba sobre compound() , tenga en cuenta que implementa <=>. Es bastante útil, especialmente para necesidades de clasificación más complejas, lo que le permite, por ejemplo, encadenar varios pedidos con <=>. ¡Lea Pedidos explicados para obtener más información

Siempre recomiendo usar los 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 en la que está interesado es 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 método compareTo solo hace referencia a eso.

Algo así

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.

Sé que puede que no se 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 el oficial javadocs para Comparable :

  

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

Por lo tanto, lanzaría NullPointerException explícitamente o simplemente dejaría que se lanzara la primera vez cuando se desreferenciara el argumento nulo.

Puede 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, Artículo 15: Minimiza la mutabilidad) y asegúrate, durante la construcción, de que no son posibles valores nulos (y usa el patrón de objeto nulo si es necesario). Luego puede omitir todas esas comprobaciones y asumir con seguridad 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 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);
    }
}

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

En caso de que alguien use Spring, hay una clase org.springframework.util.comparator.NullSafeComparator que también lo hace por usted. Simplemente decora tu propio comparable con este así

new NullSafeComparator<YourObject>(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<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]

Una de las formas simples de usando 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;
    }

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

Esta es mi implementación que uso para ordenar mi ArrayList. las clases nulas se ordenan hasta el último.

para mi caso, EntityPhone extiende EntityAbstract y mi contenedor es List < EntityAbstract & Gt ;.

el " compareIfNull () " El método se utiliza para la clasificació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;
}

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 seguro que este es tu 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);
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top