Como simplificar uma implementação compareTo () com segurança nula?

157

Estou implementando o compareTo()método para uma classe simples como esta (para poder usar Collections.sort()e outros brindes oferecidos pela plataforma Java):

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

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

Quero que a ordem natural desses objetos seja: 1) classificada por nome e 2) classificada por valor, se o nome for o mesmo; ambas as comparações não diferenciam maiúsculas de minúsculas. Para ambos os campos, os valores nulos são perfeitamente aceitáveis, portanto, compareTonão devem ser interrompidos nesses casos.

A solução que vem à mente é a seguinte: (estou usando "cláusulas de guarda" aqui, enquanto outros podem preferir um único ponto de retorno, mas isso não vem ao 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);
}

Isso faz o trabalho, mas não estou perfeitamente feliz com esse código. É certo que não é muito complexo, mas é bastante detalhado e tedioso.

A questão é: como você tornaria isso menos detalhado (mantendo a funcionalidade)? Sinta-se à vontade para consultar as bibliotecas padrão Java ou o Apache Commons, se elas ajudarem. A única opção para tornar isso (um pouco) mais simples seria implementar meu próprio "NullSafeStringComparator" e aplicá-lo para comparar os dois campos?

Edições 1-3 : Eddie está certo; Corrigido o caso "ambos os nomes são nulos" acima

Sobre a resposta aceita

Eu fiz essa pergunta em 2009, no Java 1.6, é claro, e na época a solução JDK pura de Eddie era minha resposta preferida aceita. Eu nunca mudei para mudar isso até agora (2017).

Também existem soluções de bibliotecas de terceiros - uma das coleções Apache Commons 2009 e uma da Guava 2013, ambas postadas por mim - que eu preferia em algum momento.

Agora fiz a solução Java 8 limpa por Lukasz Wiktor a resposta aceita. Definitivamente, isso deve ser preferido no Java 8, e atualmente o Java 8 deve estar disponível para quase todos os projetos.

Jonik
fonte

Respostas:

173

Usando o 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);
}
Lukasz Wiktor
fonte
7
Eu apoio o uso de coisas internas do Java 8 em favor do Apache Commons Lang, mas esse código do Java 8 é bastante feio e ainda detalhado. Por enquanto, continuarei com org.apache.commons.lang3.builder.CompareToBuilder.
jschreiner
1
Isso não funciona para Collections.sort (Arrays.asList (null, val1, null, val2, null)), pois tentará chamar compareTo () em um objeto nulo. Para ser sincero, parece um problema com a estrutura de coleções, tentando descobrir como resolver isso.
Pedro Borges
3
@PedroBorges O autor perguntou sobre a classificação de objetos de contêiner que possuam campos classificáveis ​​(onde esses campos podem ser nulos), não sobre a classificação de referências de contêineres nulos. Portanto, enquanto seu comentário estiver correto, Collections.sort(List)não funcionará quando a Lista contiver nulos, o comentário não é relevante para a pergunta.
Scrubbie 27/01
211

Você pode simplesmente usar o Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)
Dag
fonte
4
(@Kong: Este cuida de null-segurança, mas não diferencia maiúsculas de insensibilidade que foi outro aspecto da questão inicial assim não alterar a resposta aceita..)
Jonik
3
Além disso, em minha opinião, o Apache Commons não deve ser a resposta aceita em 2013. (Mesmo que alguns subprojetos sejam melhor mantidos que outros.) A goiaba pode ser usada para obter a mesma coisa ; veja nullsFirst()/ nullsLast().
Jonik
8
@ Jonik Por que você acha que o Apache Commons não deve ser a resposta aceita em 2013?
reallynice
1
Grandes partes do Apache Commons são coisas herdadas / mal mantidas / de baixa qualidade. Existem alternativas melhores para a maioria das coisas que ele fornece, por exemplo, no Guava, que é uma lib de alta qualidade por toda parte, e cada vez mais no próprio JDK. Por volta de 2005, sim, o Apache Commons era a merda, mas atualmente não há necessidade disso na maioria dos projetos. (Claro, há exceções, por exemplo, se eu precisava de um cliente de FTP, por alguma razão, eu provavelmente usaria aquele em Apache Commons Net, e assim por diante.)
Jonik
6
@ Jonik, como você responderia à pergunta usando o Goiaba? Sua afirmação de que o Apache Commons Lang (pacote org.apache.commons.lang3) é "herdado / mal mantido / de baixa qualidade" é falsa ou, na melhor das hipóteses, infundada. O Commons Lang3 é fácil de entender e usar, e é mantido ativamente. É provavelmente a minha biblioteca mais usada (além do Spring Framework e Spring Security) - a classe StringUtils com seus métodos de segurança nula torna a normalização de entrada trivial, por exemplo.
Paul
93

Eu implementaria um comparador seguro nulo. Pode haver uma implementação por aí, mas isso é tão simples de implementar que eu sempre desenvolvi o meu.

Nota: Seu comparador acima, se os dois nomes forem nulos, nem mesmo comparará os campos de valor. Eu não acho que é isso que você quer.

Eu implementaria isso com algo como o seguinte:

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

EDIT: erros corrigidos no exemplo de código. É isso que recebo por não testá-lo primeiro!

Edição: nullSafeStringComparator promovido para estático.

Eddie
fonte
2
Em relação ao aninhado "se" ... acho que o aninhado é menos legível para esse caso, portanto evito-o. Sim, às vezes haverá uma comparação desnecessária devido a isso. final para parâmetros não é necessário, mas é uma boa ideia.
Eddie
31
Uso doce do XOR
James McMahon
9
@phihag - Eu sei que já faz mais de 3 anos, MAS ... a finalpalavra-chave não é realmente necessária (o código Java já é detalhado como é.) No entanto, evita a reutilização de parâmetros como vars locais (uma péssima prática de codificação). Como nosso entendimento coletivo de software melhora com o tempo, sabemos que as coisas devem ser finais / const / imutáveis ​​por padrão. Portanto, prefiro um pouco de verbosidade extra ao usar finaldeclarações de parâmetro (por mais trivial que seja a função) obter inmutability-by-quasi-default.) A sobrecarga de compreensibilidade / manutenção é insignificante no grande esquema das coisas.
Luis.espinal
24
@ James McMahon Eu tenho que discordar. Xor (^) poderia simplesmente ser substituído por diferente de (! =). Ele até compila no mesmo código de bytes. O uso de! = Vs ^ é apenas uma questão de gosto e legibilidade. Então, a julgar pelo fato de você estar surpreso, eu diria que não pertence aqui. Use xor quando estiver tentando calcular uma soma de verificação. Na maioria dos outros casos (como este), vamos nos ater a! =.
Bdb 27/07/2014
5
É fácil estender esta resposta, substituindo cordas com T, T declarado como <T estende Comparable <T >> ... e então podemos comparar com segurança quaisquer objetos comparáveis anuláveis
Thierry
20

Consulte a parte inferior desta resposta para obter a solução atualizada (2013) usando o Goiaba.


Foi com isso que eu finalmente fui. Acontece que já tínhamos um método utilitário para comparação de cadeias com segurança nula, portanto a solução mais simples era fazer uso disso. (É uma grande base de código; é fácil perder esse tipo de coisa :)

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

É assim que o auxiliar é definido (é sobrecarregado para que você também possa definir se os nulos vêm primeiro ou por último, se você quiser):

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

Portanto, isso é essencialmente o mesmo que a resposta de Eddie (embora eu não chamaria um método auxiliar estático de comparador ) e o de uzhin também.

De qualquer forma, em geral, eu teria favorecido fortemente a solução de Patrick , pois acho que é uma boa prática usar bibliotecas estabelecidas sempre que possível. ( Conheça e use as bibliotecas como Josh Bloch diz.) Mas, neste caso, isso não produziria o código mais limpo e simples.

Edit (2009): Versão do Apache Commons Collections

Na verdade, aqui está uma maneira de tornar a solução baseada no Apache Commons NullComparatormais simples. Combine-o com a distinção entre maiúsculas e minúsculasComparator fornecida na Stringclasse:

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

Agora isso é bem elegante, eu acho. (Apenas um pequeno problema permanece: o Commons NullComparatornão suporta genéricos, então há uma tarefa não verificada.)

Atualização (2013): versão do Goiaba

Quase cinco anos depois, eis como eu abordaria minha pergunta original. Se codificasse em Java, eu (é claro) estaria usando o Guava . (E certamente não o Apache Commons.)

Coloque essa constante em algum lugar, por exemplo, na classe "StringUtils":

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

Então, em 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);
}    

Obviamente, isso é quase idêntico à versão do Apache Commons (ambos usam o CASE_INSENSITIVE_ORDER do JDK ), o uso de nullsLast()ser a única coisa específica do Goiaba. Esta versão é preferível simplesmente porque o Goiaba é preferível, como uma dependência, ao Commons Collections. (Como todos concordam .)

Se você estava pensando Ordering, observe que ele é implementado Comparator. É bastante útil, especialmente para necessidades de classificação mais complexas, permitindo, por exemplo, encadear vários pedidos usando compound(). Leia Ordenação explicada para mais!

Jonik
fonte
2
String.CASE_INSENSITIVE_ORDER realmente torna a solução muito mais limpa. Boa atualização.
224 Patrick Patrick
2
Se você usa o Apache Commons de qualquer maneira, existe um método para ComparatorChainnão precisar de um compareTométodo próprio .
Amebe 23/09/12
13

Eu sempre recomendo usar o Apache commons, pois provavelmente será melhor do que aquele que você pode escrever sozinho. Além disso, você pode fazer um trabalho "real" em vez de reinventar.

A classe em que você está interessado é o Comparador Nulo . Permite que você faça nulos altos ou baixos. Você também fornece seu próprio comparador para usar quando os dois valores não são nulos.

No seu caso, você pode ter uma variável de membro estática que faz a comparação e, em seguida, seu compareTométodo apenas faz referência a isso.

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

}

Mesmo se você decidir criar sua própria, lembre-se dessa classe, pois ela é muito útil ao solicitar listas que contenham elementos nulos.

Patrick
fonte
Obrigado, eu meio que esperava que houvesse algo assim no Commons! Neste caso, no entanto, eu não acabar de usá-lo: stackoverflow.com/questions/481813/...
Jonik
Acabei de perceber que sua abordagem pode ser simplificada usando String.CASE_INSENSITIVE_ORDER; veja minha resposta de edição editada.
317 Jonik
Isso é bom, mas a verificação "if (other == null) {" não deve estar lá. O Javadoc para Comparable diz que compareTo deve lançar um NullPointerException se outro for nulo.
22412 Daniel Danieliuc #
7

Eu sei que pode não ser uma resposta direta à sua pergunta, porque você disse que valores nulos precisam ser suportados.

Mas quero apenas observar que o suporte a nulos no compareTo não está alinhado com o contrato compareTo descrito nos javadocs oficiais do Comparable :

Observe que null não é uma instância de nenhuma classe e e.compareTo (null) deve gerar uma NullPointerException mesmo que e.equals (null) retorne false.

Portanto, eu lançaria NullPointerException explicitamente ou deixaria que fosse lançada pela primeira vez quando o argumento nulo estiver sendo desreferenciado.

Piotr Sobczyk
fonte
4

Você pode extrair o 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); 

}

Yoni Roit
fonte
2
O "0: 1" não deve ser "0: -1"?
Rolf Kristensen
3

Você pode projetar sua classe para ser imutável (o Java 2º Ed. Efetivo tem uma ótima seção sobre isso, o Item 15: Minimizar a mutabilidade) e garantir na construção que nenhum nulo é possível (e usar o padrão de objeto nulo, se necessário). Em seguida, você pode pular todas essas verificações e assumir com segurança que os valores não são nulos.

Fabian Steeg
fonte
Sim, essa é geralmente uma boa solução, e simplifica muitas coisas - mas aqui eu estava mais interessado no caso em que são permitidos valores nulos, por uma razão ou outra, e deve ser levado em conta :)
Jonik
2

Eu estava procurando por algo semelhante e isso pareceu um pouco complicado, então eu fiz isso. Eu acho que é um pouco mais fácil de entender. Você pode usá-lo como um comparador ou como um liner. Para esta pergunta, você mudaria para compareToIgnoreCase (). Como é, os nulos flutuam. Você pode virar o 1, -1 se quiser que eles afundem.

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);
    }
}
Dustin
fonte
2

podemos usar o java 8 para fazer uma comparação amigável entre nulos entre objetos. suponho que eu tenho uma classe Boy com 2 campos: String name e Integer age e eu quero primeiro comparar nomes e depois envelhecer se ambos forem iguais.

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

e o 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
Leo Ng
fonte
1

Para o caso específico em que você sabe que os dados não terão nulos (sempre é uma boa ideia para cadeias) e os dados são realmente grandes, você ainda está fazendo três comparações antes de comparar os valores, se tiver certeza de que esse é o seu caso , você pode otimizar um pouco. YMMV como código legível supera otimização 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);
kisna
fonte
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);
        }


    }

}

saída é

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]
Nikhil Kumar K
fonte
1

Uma das maneiras simples de usar o NullSafe Comparator é usar a implementação do Spring, abaixo é um exemplo simples de referência:

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;
    }
Amandeep Singh
fonte
0

Outro exemplo do Apache ObjectUtils. Capaz de classificar outros 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());
}
snp0k
fonte
0

Esta é a minha implementação que eu uso para classificar meu ArrayList. as classes nulas são classificadas até a última.

para o meu caso, o EntityPhone estende o EntityAbstract e meu contêiner é List <EntityAbstract>.

o método "compareIfNull ()" é usado para classificação segura nula. Os outros métodos são de integridade, mostrando como o compareIfNull pode ser usado.

@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;
}
Angel Koh
fonte
0

Se você quer um simples hack:

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

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

Se você quiser colocar nulos no final da lista, basta alterar isso no método acima

return o2.getName().compareTo(o1.getName());
MartePessoas
fonte