Suponha que você tenha alguns objetos com vários campos que podem ser comparados por:
public class Person {
private String firstName;
private String lastName;
private String age;
/* Constructors */
/* Methods */
}
Portanto, neste exemplo, quando você pergunta se:
a.compareTo(b) > 0
você pode estar se perguntando se o sobrenome de a vem antes de b, ou se a é mais antigo que b, etc ...
Qual é a maneira mais limpa de permitir a comparação múltipla entre esses tipos de objetos sem adicionar sobrecarga desnecessária ou sobrecarga?
java.lang.Comparable
interface permite a comparação apenas por um campo- Adicionando vários métodos comparar (ou seja
compareByFirstName()
,compareByAge()
, etc ...) está cheio na minha opinião.
Então, qual é a melhor maneira de fazer isso?
Respostas:
Você pode implementar um
Comparator
que compara doisPerson
objetos e pode examinar quantos campos desejar. Você pode inserir uma variável em seu comparador que diga a qual campo comparar, embora provavelmente seja mais simples escrever vários comparadores.fonte
Com o Java 8:
Se você tiver métodos de acessador:
Se uma classe implementar Comparable, esse comparador poderá ser usado no método compareTo:
fonte
(Person p)
é importante para comparadores encadeados.Comparator
instâncias em todas as chamadas?.thenComparing(Person::getLastName, Comparator.nullsFirst(Comparator.naturalOrder()))
- primeiro selector de campo, então o comparadorcompareTo
como mostrado acima,Comparator
é criado sempre que o método é chamado. Você pode evitar isso armazenando o comparador em um campo final estático privado.Você deve implementar
Comparable <Person>
. Supondo que todos os campos não serão nulos (por uma questão de simplicidade), que a idade é um int e a classificação da comparação é a primeira, a última, a idade, ocompareTo
método é bastante simples:fonte
(de maneiras de classificar listas de objetos em Java com base em vários campos )
Código de trabalho nesta essência
Usando Java 8 lambda's (adicionado em 10 de abril de 2019)
O Java 8 resolve isso muito bem com o lambda (embora Guava e Apache Commons ainda possam oferecer mais flexibilidade):
Graças à resposta de @ gaoagong abaixo .
Confuso e complicado: Classificação manual
Isso requer muita digitação, manutenção e é propenso a erros.
A maneira reflexiva: Classificando com BeanComparator
Obviamente, isso é mais conciso, mas ainda mais suscetível a erros, pois você perde sua referência direta aos campos usando Strings (sem segurança tipográfica, auto-refatorações). Agora, se um campo for renomeado, o compilador nem relatará um problema. Além disso, como essa solução usa reflexão, a classificação é muito mais lenta.
Como chegar: Classificando com o ComparisonChain do Google Guava
Isso é muito melhor, mas requer algum código da placa da caldeira para o caso de uso mais comum: valores nulos devem ter menos valor por padrão. Para campos nulos, você deve fornecer ao Guava uma diretiva extra o que fazer nesse caso. Esse é um mecanismo flexível se você deseja fazer algo específico, mas geralmente deseja o caso padrão (por exemplo, 1, a, b, z, null).
Classificação com o Apache Commons CompareToBuilder
Como o ComparisonChain do Guava, essa classe de biblioteca classifica facilmente em vários campos, mas também define o comportamento padrão para valores nulos (ou seja, 1, a, b, z, null). No entanto, você também não pode especificar mais nada, a menos que você forneça seu próprio Comparador.
portanto
Em última análise, tudo se resume ao sabor e à necessidade de flexibilidade (ComparavaChain da Guava) versus código conciso (CompareToBuilder do Apache).
Método bônus
Encontrei uma boa solução que combina vários comparadores em ordem de prioridade no CodeReview em
MultiComparator
:Claro que o Apache Commons Collections já tem um utilitário para isso:
ComparatorUtils.chainedComparator (comparatorCollection)
fonte
@Patrick Para classificar mais de um campo consecutivamente, tente ComparatorChain
fonte
Outra opção que você sempre pode considerar é o Apache Commons. Ele oferece muitas opções.
Ex:
fonte
Você também pode dar uma olhada no Enum que implementa o Comparator.
http://tobega.blogspot.com/2008/05/beautiful-enums.html
por exemplo
fonte
fonte
Para aqueles capazes de usar a API de streaming Java 8, existe uma abordagem mais limpa que está bem documentada aqui: Lambdas e classificação
Eu estava procurando o equivalente do C # LINQ:
Encontrei o mecanismo no Java 8 no comparador:
Então, aqui está o trecho que demonstra o algoritmo.
Confira o link acima para obter uma maneira mais clara e uma explicação sobre como a inferência de tipo do Java torna um pouco mais difícil de definir em comparação com o LINQ.
Aqui está o teste de unidade completo para referência:
fonte
Escrever um
Comparator
manual para esse caso de uso é uma solução IMO terrível. Tais abordagens ad hoc têm muitas desvantagens:Então, qual é a solução?
Primeiro alguma teoria.
Vamos denotar a proposição "tipo
A
suporta comparação" porOrd A
. (Da perspectiva do programa, você pode pensarOrd A
em um objeto que contém lógica para comparar doisA
s. Sim, assim comoComparator
.)Agora, se
Ord A
eOrd B
, então, seu composto(A, B)
também deve suportar comparação. ieOrd (A, B)
. SeOrd A
,,Ord B
eOrd C
, entãoOrd (A, B, C)
.Podemos estender esse argumento à aridade arbitrária e dizer:
Ord A, Ord B, Ord C, ..., Ord Z
⇒Ord (A, B, C, .., Z)
Vamos chamar esta afirmação 1.
A comparação dos compostos funcionará exatamente como você descreveu na sua pergunta: a primeira comparação será tentada primeiro, depois a próxima, depois a próxima e assim por diante.
Essa é a primeira parte da nossa solução. Agora a segunda parte.
Se você sabe disso
Ord A
e sabe como se transformarB
emA
(chame essa função de transformaçãof
), também pode terOrd B
. Quão? Bem, quando as duasB
instâncias devem ser comparadas, primeiro você as transforma emA
usof
e depois aplicaOrd A
.Aqui, estamos mapeando a transformação
B → A
paraOrd A → Ord B
. Isso é conhecido como mapeamento contravariante (oucomap
abreviado).Ord A, (B → A)
⇒ comapOrd B
Vamos chamar esta afirmação 2.
Agora vamos aplicar isso ao seu exemplo.
Você tem um tipo de dados nomeado
Person
que compreende três campos do tipoString
.Nós sabemos disso
Ord String
. Pela declaração 1Ord (String, String, String)
,.Podemos escrever facilmente uma função de
Person
para(String, String, String)
. (Apenas retorne os três campos.) Como sabemosOrd (String, String, String)
ePerson → (String, String, String)
, pela declaração 2, podemos usar ocomap
getOrd Person
.QED.
Como faço para implementar todos esses conceitos?
A boa notícia é que você não precisa. Já existe uma biblioteca que implementa todas as idéias descritas neste post. (Se você está curioso para saber como eles são implementados, pode ver por baixo do capô .)
É assim que o código ficará com ele:
Explicação:
stringOrd
é um objecto do tipoOrd<String>
. Isso corresponde à nossa proposta original de "comparação de suportes".p3Ord
é um método que levaOrd<A>
,Ord<B>
,Ord<C>
, e retornaOrd<P3<A, B, C>>
. Isso corresponde à afirmação 1. (P3
significa produto com três elementos. Produto é um termo algébrico para compósitos.)comap
corresponde a bemcomap
,.F<A, B>
representa uma função de transformaçãoA → B
.p
é um método de fábrica para criar produtos.Espero que ajude.
fonte
Em vez de métodos de comparação, você pode definir apenas vários tipos de subclasses "Comparator" dentro da classe Person. Dessa forma, você pode passá-los para os métodos de classificação de coleções padrão.
fonte
Eu acho que seria mais confuso se o seu algoritmo de comparação fosse "inteligente". Eu iria com os numerosos métodos de comparação que você sugeriu.
A única exceção para mim seria a igualdade. Para testes de unidade, foi útil substituir os .Equals (em .net) para determinar se vários campos são iguais entre dois objetos (e não que as referências sejam iguais).
fonte
Se houver várias maneiras de um usuário solicitar uma pessoa, você também poderá ter várias configurações do Comparator como constantes em algum lugar. A maioria das operações de classificação e coleções classificadas usa um comparador como parâmetro.
fonte
fonte
A implementação de código do mesmo está aqui se precisarmos classificar o objeto Person com base em vários campos.
fonte
fonte
Se você implementar a interface Comparável , escolha uma propriedade simples para ordenar. Isso é conhecido como ordenamento natural. Pense nisso como o padrão. É sempre usado quando nenhum comparador específico é fornecido. Geralmente esse é o nome, mas seu caso de uso pode exigir algo diferente. Você pode usar qualquer número de outros comparadores que possa fornecer a várias APIs de coleções para substituir a ordem natural.
Observe também que normalmente se a.compareTo (b) == 0, então a.equals (b) == true. Tudo bem se não, mas existem efeitos colaterais a serem observados. Veja os excelentes javadocs na interface Comparable e você encontrará muitas informações excelentes sobre isso.
fonte
Blog a seguir, com um bom exemplo de Comparador encadeado
http://www.codejava.net/java-core/collections/sorting-a-list-by-multiple-attributes-example
Comparador de chamadas:
fonte
A partir da resposta de Steve, o operador ternário pode ser usado:
fonte
É fácil comparar dois objetos com o método hashcode em java`
fonte
Normalmente, substituo meu
compareTo()
método dessa maneira sempre que tenho que fazer uma classificação multinível.Aqui, a primeira preferência é dada ao nome do filme, depois ao artista e, finalmente, ao songLength. Você só precisa garantir que esses multiplicadores estejam distantes o suficiente para não cruzar os limites um do outro.
fonte
É fácil fazer isso usando a biblioteca Guava do Google .
por exemplo
Objects.equal(name, name2) && Objects.equal(age, age2) && ...
Mais exemplos:
fonte