Como simplificar uma implementação compareTo () isenta de erros?

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

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

Eu quero que a ordenação natural para esses objects seja: 1) classificada por nome e 2) classificada por valor se o nome for o mesmo; ambas as comparações devem ser insensíveis a maiúsculas e minúsculas. Para ambos os campos, os valores nulos são perfeitamente aceitáveis, portanto compareTo não deve quebrar nesses casos.

A solução que vem à mente é semelhante à seguinte (estou usando “cláusulas de guarda” aqui, enquanto outras podem preferir um único ponto de retorno, mas isso é irrelevante):

 // 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 muito feliz com esse código. Evidentemente não é muito complexo, mas é bastante verboso e tedioso.

A questão é, como você faria 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 o 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 a minha resposta preferida. Eu nunca consegui mudar isso até agora (2017).

Há também soluções de bibliotecas de terceiros – um Apache Commons Collections 2009 e um Goiaba 2013, ambos postados por mim – que eu preferia em algum momento.

Eu agora fiz a solução limpa do Java 8 por Lukasz Wiktor a resposta aceita. Isso definitivamente deve ser preferido no Java 8, e atualmente o Java 8 deve estar disponível para quase todos os projetos.

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

Você pode simplesmente usar o Apache Commons Lang :

 result = ObjectUtils.compare(firstComparable, secondComparable) 

Eu implementaria um comparador seguro nulo. Pode haver uma implementação lá fora, mas isso é tão simples de implementar que eu sempre criei a minha.

Nota: Seu comparador acima, se ambos os nomes forem nulos, nem sequer comparará os campos de valor. Eu não acho que isso é o 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: Corrigidos erros de digitação no exemplo de código. Isso é o que eu recebo por não testá-lo primeiro!

EDIT: Promovido nullSafeStringComparator para estático.

Veja a parte inferior desta resposta para uma solução atualizada (2013) usando o Guava.


Isto é o que eu finalmente fui com. Descobrimos que já tínhamos um método utilitário para comparação de string nula, então 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 (ele é sobrecarregado para que você também possa definir se os nulos vêm em primeiro ou último lugar, se você quiser):

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

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

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

Editar (2009): versão do Apache Commons Collections

Na verdade, aqui está uma maneira de tornar a solução baseada no Apache Commons NullComparator mais simples. Combine-o com o Comparator sem distinção entre maiúsculas e minúsculas fornecido na class 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); } 

Isso é bem elegante, eu acho. (Apenas um pequeno problema permanece: o Commons NullComparator não suporta genéricos, então há uma atribuição desmarcada.)

Atualização (2013): versão goiaba

Quase 5 anos depois, aqui está como eu resolvo minha pergunta original. Se codificando em Java, eu (obviamente) estaria usando o Guava . (E certamente não é o Apache Commons.)

Coloque esta constante em algum lugar, por exemplo, na class “StringUtils”:

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

Em seguida, na 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); } 

Claro, isso é quase idêntico à versão do Apache Commons (ambos usam CASE_INSENSITIVE_ORDER do JDK), o uso de nullsLast() é a única coisa específica do Guava. Esta versão é preferível simplesmente porque a Goiaba é preferível, como dependência, às Coleções Commons. (Como todo mundo concorda .)

Se você estava se perguntando sobre o Ordering , observe que ele implementa o Comparator . É muito útil especialmente para necessidades de sorting mais complexas, permitindo, por exemplo, encadear vários Pedidos usando compound() . Leia pedidos explicados para mais!

Eu sempre recomendo o uso do Apache, já que ele 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 turma na qual você está interessado é o Null Comparator . Ele permite que você faça nulos altos ou baixos. Você também dá 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ático que faz a comparação e, em seguida, seu método compareTo apenas faz referência a isso.

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

}

Mesmo que você decida criar seu próprio, lembre-se desta class, pois ela é muito útil ao ordenar listas que contenham elementos nulos.

Eu sei que pode não ser diretamente responder à sua pergunta, porque você disse que os valores nulos devem ser suportados.

Mas eu só quero notar que o suporte a nulls em compareTo não está de acordo com o contrato compareTo descrito em javadocs oficiais para Comparable :

Note que null não é uma instância de qualquer class, e e.compareTo (null) deve lançar um NullPointerException mesmo que e.equals (null) retorne false.

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

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

}

Você poderia projetar sua class para ser imutável (o Effective Java 2nd Ed. Tem uma grande seção sobre isso, Item 15: Minimizar mutabilidade) e certificar-se de que não são possíveis nulos (e usar o padrão de object nulo se necessário). Então você pode pular todas as verificações e assumir com segurança que os valores não são nulos.

Eu estava procurando por algo semelhante e isso parecia 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 forro. Para esta questão, você mudaria para compareToIgnoreCase (). Como é, os nulos flutuam. Você pode virar o 1, -1 se quiser que ele afunde.

 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 o java 8 para fazer uma comparação amigável entre objects. Supostamente eu tenho uma class Boy com 2 campos: String name e Integer age e quero primeiro comparar nomes e depois envelhece se ambos forem iguais.

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

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 

No caso de alguém usar o Spring, há uma class org.springframework.util.comparator.NullSafeComparator que faz isso para você também. Basta decorar o seu próprio comparável com isso assim

new NullSafeComparator(new YourComparable(), true)

http://www.google.com/adwords

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

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] 

Outro exemplo do Apache ObjectUtils. Capaz de ordenar outros tipos de objects.

 @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 o 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 é a minha implementação que eu uso para classificar minha ArrayList. as classs nulas são classificadas como últimas.

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

o método “compareIfNull ()” é usado para sorting segura nula. Os outros methods são para completude, mostrando como 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 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 o caso específico em que você sabe que os dados não terão nulos (sempre é uma boa ideia para strings) e os dados são muito 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 pouquinho. O YMMV como código legível supera a otimização secundária:

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