ti-enxame.com

É uma boa idéia ter lógica no método equals que não faça a correspondência exata?

Enquanto assistíamos um aluno em um projeto universitário, trabalhamos em um exercício Java fornecido pela universidade que definia uma classe para um endereço com os campos:

number
street
city
zipcode

E especificou que a lógica igual deve retornar true se o número e o CEP corresponderem.

Uma vez fui ensinado que o método equals deveria fazer apenas uma comparação exata entre os objetos (depois de verificar o ponteiro), o que faz algum sentido para mim, mas contradiz a tarefa que eles receberam.

Eu posso ver por que você deseja substituir a lógica para poder usar coisas como list.contains() com sua correspondência parcial, mas estou pensando se isso é considerado kosher e, se não, por que não?

35
William Dunne

Definindo a igualdade para dois objetos

A igualdade pode ser arbitrariamente definida para dois objetos. Não existe uma regra estrita que proíba alguém de definir como quiser. No entanto, a igualdade geralmente é definida quando é significativa para as regras de domínio do que está sendo implementado.

Espera-se seguir o contrato de relação de equivalência :

  • É reflexivo : para qualquer valor de referência não nulo x, x.equals (x) deve retornar true.
  • É simétrico : para quaisquer valores de referência não nulos xey, x.equals (y) deve retornar true se e somente se y.equals ( x) retorna verdadeiro.
  • É transitivo : para qualquer valor de referência não nulo x, ye z, se x.equals (y) retornar true e y.equals ( z) retorna verdadeiro, então x.equals (z) deve retornar verdadeiro.
  • É consistente : para quaisquer valores de referência não nulos xey, várias invocações de x.equals (y) retornam consistentemente true ou retornam false consistentemente, desde que nenhuma informação usada em comparações iguais nos objetos seja modificada.
  • Para qualquer valor de referência não nulo x, x.equals (nulo) deve retornar false.

No seu exemplo, talvez não seja necessário distinguir dois endereços que tenham o mesmo código postal e número como sendo diferentes. Existem domínios perfeitamente razoáveis ​​para esperar que o seguinte código funcione:

Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2);

Isso pode ser útil, como você mencionou, para quando você não se importa que eles sejam objetos diferentes - você só se importa com os valores que eles possuem. Talvez o CEP + o número da rua sejam suficientes para você identificar o endereço correto e as informações restantes serem "extras", e você não deseja que essas informações extras afetem sua lógica de igualdade.

Essa poderia ser uma modelagem perfeitamente boa para um software. Apenas verifique se há alguma documentação ou testes de unidade para garantir esse comportamento e se a API pública reflete esse uso.


Não se esqueça de hashCode()

Um detalhe adicional relevante para a implementação é o fato de muitas linguagens usarem fortemente o conceito de código de hash . Essas linguagens, Java, geralmente assumem a seguinte proposição:

Se x.equals (y), x.hashCode () e y.hashCode () são os mesmos.

No mesmo link de antes:

Observe que geralmente é necessário substituir o método hashCode sempre que esse método (igual) for substituído, para manter o contrato geral do método hashCode, que afirma que objetos iguais devem ter códigos de hash iguais.

Observe que ter o mesmo hashCode não significa que dois objetos sejam iguais !

Nesse sentido, quando se implementa a igualdade, também se deve implementar uma hashCode() que segue a propriedade mencionada acima. Essa hashCode() é usada pelas estruturas de dados para garantir eficiência e garantir limites superiores à complexidade de suas operações.

É difícil criar uma boa função de código hash e um tópico inteiro por si só. Idealmente, o hashCode de dois objetos diferentes deve ser diferente ou ter uma distribuição uniforme entre as ocorrências da instância.

Mas lembre-se de que a implementação simples a seguir ainda cumpre a propriedade de igualdade, mesmo que não seja uma função de hash "boa":

public int hashCode() {
    return 0;
}

Uma maneira mais comum de implementar o código de hash é usar os códigos de hash dos campos que definem sua igualdade e fazer uma operação binária neles. No seu exemplo, código postal e número da rua. Geralmente é feito como:

public int hashCode() {
    return this.zipCode.hashCode() ^ this.streetNumber.hashCode();
}

Quando ambíguo, escolha clareza

Aqui é onde eu faço uma distinção sobre o que se deve esperar em relação à igualdade. Pessoas diferentes têm expectativas diferentes em relação à igualdade e, se você deseja seguir o Princípio do mínimo de espanto , pode considerar outras opções para descrever melhor seu design.

Qual desses deve ser considerado igual?

Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2); // Are typos the same address?
Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");
assert a1.equals(a2); // Are abbreviations the same address?
Vector3 v1 = new Vector3(1.0f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // Should two vectors that have the same values be the same?
Vector3 v1 = new Vector3(1.00000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // What is the error tolerance?

Pode-se argumentar que cada um deles seja verdadeiro ou falso. Em caso de dúvida, pode-se definir uma relação diferente que seja mais clara no contexto do domínio.

Por exemplo, você pode definir isSameLocation(Address a):

Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");

System.out.print(a1.equals(a2)); // false;
System.out.print(a1.isSameLocation(a2)); // true;

Ou no caso de Vectors, isInRangeOf(Vector v, float range):

Vector3 v1 = new Vector3(1.000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);

System.out.print(v1.equals(v2)); // false;
System.out.print(v1.isInRangeOf(v2, 0.01f)); // true;

Dessa forma, você descreve melhor sua intenção de design para a igualdade e evita quebrar as expectativas de futuros leitores em relação ao que seu código realmente faz. (Você pode dar uma olhada em todas as respostas ligeiramente diferentes para ver como as expectativas das pessoas variam em relação à relação de igualdade do seu exemplo)

89
Albuquerque

É no contexto da atribuição da universidade que o objetivo da tarefa é explorar e entender a substituição do operador. Parece um exemplo de tarefa que tem propósito implícito suficiente para fazê-la parecer um exercício que vale a pena na época.

No entanto, se isso fosse uma revisão de código por mim, eu marcaria isso como uma falha de design significativa.

O problema é esse. Permite código sintaticamente limpo que parece obviamente correto:

if (driverLocation.equals(parcel.deliveryAddress)) { parcel.deliver(); }

E com base nos comentários de outros usuários, esse código produziria resultados corretos no Brasil, onde os códigos postais são exclusivos de uma rua. No entanto, se você tentou usar esse software nos EUA, onde essa suposição não é mais válida, esse código ainda parece correto.

se isso tivesse sido implementado como:

if (Address.isMatchNumberAndZipcode(driverLocation, parcel.deliveryAddress)) {
  parcel.deliver();
}

alguns anos depois, quando um desenvolvedor brasileiro diferente recebe a base de código e informa que o software entrega pacotes nos endereços errados para seu novo cliente na Califórnia, a suposição agora quebrada é óbvia no código e é visível no ponto de decisão em entrega ou não - o que provavelmente é o primeiro local em que o programador de manutenção examina para ver por que a encomenda é entregue no endereço errado.

Ter uma lógica não óbvia oculta em uma sobrecarga do operador fará com que a correção do código demore mais. Para capturar esse problema nesse código, provavelmente seria necessária uma sessão com um depurador percorrendo o código.

42
Michael Shaw

A igualdade é uma questão de contexto. Se dois objetos são considerados iguais ou não, é tanto uma questão de contexto quanto um dos dois objetos envolvidos.

Portanto, se no seu contexto, faz sentido ignorar cidade e rua, então não há problema em implementar a igualdade apenas com base no código postal e no número. (Como apontado em um dos comentários, CEP e número são o suficiente para identificar exclusivamente um endereço no Brasil.)

Obviamente, você deve seguir as regras apropriadas para sobrecarregar a igualdade, como também sobrecarregar hashCode de acordo.

25
Jörg W Mittag

Um operador de igualdade alegará que dois objetos são iguais se, e somente se, forem considerados iguais, devido a quaisquer considerações que julgar úteis.

Repito: devido às considerações que você achar úteis.

O desenvolvedor do software está no banco do motorista aqui. Além de ser consistente com os requisitos óbvios (a = a, a = b implica b + a, a = b = c implica a = c) e consistência com a função hash), o operador de igualdade pode ser o que você quiser.

3
gnasher729

Embora tenham sido dadas muitas respostas, minha opinião ainda não está presente.

Uma vez me ensinaram que o método dos iguais deveria fazer apenas uma comparação exata entre os objetos

Além do que as regras dizem, essa definição é o que as pessoas assumem por sua intuição quando falam sobre igualdade. Algumas respostas dizem que a igualdade depende do contexto. Eles estão certos no sentido de que os objetos podem ser iguais, mesmo que nem todos os seus campos correspondam. Mas o entendimento comum de "é igual" não deve ser redefinido demais.

De volta ao tópico, para mim um endereço igual a outro se ele apontar para o mesmo local.

Na Alemanha, pode haver especificações diferentes de uma cidade, por exemplo, se um subúrbio for nomeado. Em seguida, a cidade de um endereço no subúrbio SUB pode ser especificada apenas como "Cidade principal" ou "Cidade principal, SUB" ou até apenas "SUB". Como fornecer o nome principal da cidade é aceitável, todos os nomes de ruas de uma cidade e todos os subúrbios atribuídos devem ser exclusivos.

Aqui, o código postal é suficiente para informar a cidade, mesmo que o nome da cidade varie.
Mas sair da rua NÃO é único, a menos que o CEP também aponte para uma rua conhecida, o que geralmente não acontece.
Portanto, não é intuitivo considerar dois endereços iguais se eles puderem apontar para locais diferentes cuja diferença consiste nos campos ignorados.

Se houver um caso de uso que exija apenas alguns, mas todos os campos, o método compare deve ser nomeado adequadamente. Existe apenas um método "é igual" que não deve ser secretamente transformado em "é igual para apenas um caso de uso especial - mas ninguém pode ver isso".

Isso significa que, pelas razões explicadas, eu diria ...

mas eu estou querendo saber se isso é considerado kosher

Sem o conhecimento, se você estiver acidentalmente em um local onde os nomes das ruas não importam: não, não é.
Se você deseja programar algo não apenas usado em um local como esse: não, não é.
Se você deseja dar aos alunos a sensação de fazer as coisas certas e manter o código compreensível e lógico: não, não é.

2
puck

Embora o requisito fornecido contradiga o senso humano, não há problema em deixar apenas um subconjunto das propriedades dos objetos definir o significado de "único".

O problema aqui é que existe uma relação técnica entre equals() e hashcode() para que, para dois objetos a e b desse tipo seja considerado :
if a.equals(b) then a.hashcode()==b.hashcode()
Se você tiver um subconjunto das propriedades que definem suas condições de exclusividade, deverá usar o mesmo subconjunto para calcular o valor de retorno de hashcode().

Afinal, a abordagem muito mais apropriada para o requisito pode ter sido implementar Comparable ou mesmo um método personalizado isSame().

1
Timothy Truckle

Depende.

É uma boa ideia ...? Depende. Pode ser uma boa ideia, se você estiver desenvolvendo um aplicativo que será usado apenas uma vez , por exemplo, em uma atribuição de universidade (se você estiver indo para jogar fora o código após a atribuição revisada) ou algum utilitário de migração (você migra os dados herdados uma vez e não precisa mais do utilitário).

Mas, na indústria de TI, em muitos casos, isso seria uma má idéia. Por quê? @ Jörg W Mittag disse A igualdade é uma questão de contexto ... se no seu contexto faz sentido ... . Mas muitas vezes o mesmo objeto é usado em muitos contextos diferentes que possuem diferentes visão sobre igualdade. Apenas alguns exemplos de quão diferente pode ser definida a igualdade da mesma entidade:

  • Como igualdade de todos os atributos de duas entidades
  • Como igualdade de chaves primárias de duas entidades
  • Como igualdade de chaves primárias e versões de duas entidades
  • Como igualdade de todos os atributos "comerciais", exceto da chave primária e versão

Se você implementar na igual a () a lógica de um contexto em particular, será difícil mais tarde usar esse objeto em outros contextos, porque muitos desenvolvedores nas equipes do seu projeto, você não saberá exatamente a lógica para qual contexto exatamente é implementado lá e em quais casos eles poderão confiar nele. Em alguns casos, eles o usarão incorretamente (como @Michael Shaw descreveu); em outros casos, eles ignorarão a lógica e implementarão seus próprios métodos para a mesma finalidade (o que pode funcionar de maneira diferente do esperado).

Se seu aplicativo for usado por mais tempo por 2 a 3 anos, normalmente haverá vários novos requisitos, várias alterações e vários contextos. E muito provavelmente haverá múltiplas expectativas diferentes sobre igualdade. É por isso que eu sugiro:

  • Implementar é igual a () formalmente, sem conexão com o contexto de negócios, significa sem lógica de negócios, assim como a igualdade de todos os atributos do objeto (é claro, hashCode/igual a contrato deve ser seguido)
  • Para cada contexto, forneça um método separado que implemente igualdade no sentido desse contexto, como isPrimaryKeyAndVersionEqual () , areBusinessAttributesEqual () .

Para encontrar um objeto em um contexto específico, basta usar o método correspondente, da seguinte maneira:

if (list.sream.anyMatch(e -> e.isPrimaryKeyAndVersionEqual(myElement))) ...

if (list.sream.anyMatch(e -> e.areBusinessAttributesEqual(myElement))) ...

Assim, haverá menos erros no código, a análise do código será mais fácil, a alteração do aplicativo para novos requisitos será mais fácil.

1
mentallurg

Como outros mencionados, por um lado a igualdade é apenas um conceito matemático que satisfaz algumas propriedades (ver, por exemplo, Albuquerque resposta). Por outro lado, sua semântica e implementação são determinadas pelo contexto.

Independentemente dos detalhes da implementação, considere, por exemplo, uma classe que representa expressões aritméticas (como (1 + 3) * 5). Se você implementar um intérprete para essas expressões usando as regras de avaliação padrão para expressões aritméticas, faz sentido considerar as instâncias respectivas para (1 + 3) * 5 e 10 + 10 para ser equal. No entanto, se você implementar uma impressora bonita para essas expressões, as instâncias acima não serão consideradas equal, enquanto (1 + 3) * 5 e (1+3)*5 seria.

0
michid

Como outros já mencionaram, a semântica exata da igualdade de objetos faz parte da definição do domínio de negócios. Nesse caso, não acho razoável ter um objeto "geral" como Address (contendo number, street, city, zipcode) para ter uma definição muito estreita de igualdade (que, como outros mencionaram, funciona no Brasil, mas não nos EUA, por exemplo).

Em vez disso, eu teria Address semântica de valor para igualdade (definida pela igualdade de todos os membros). Gostaria então:

  1. Crie uma classe StreeNumberAndZip (# TODO: bad name), Que contém apenas um street e um zipCode, e defina equals sobre eles. Sempre que você quiser comparar dois objetos Address dessa maneira específica, poderá executar addressA.streetNumberAndZip().equals(addressB.streetNumberAndZip()) ou ...
  2. Crie uma classe AddressUtils com um método bool equalStreeNumberAndZipCode(Address a, Address b), que define a igualdade estreita nesse local.

Nos dois casos, você ainda tem acesso para usar addressA.equals(addressB) para verificação completa da igualdade.

Para n campos de um objeto, existem 2^n Diferentes definições de igualdade (cada campo pode ser incluído ou excluído da verificação). Se você precisar verificar a igualdade de várias maneiras diferentes, também pode ser útil ter algo como um enum AddressComponent. Você poderia então ter uma bool addressComponentsAreEqual(EnumSet<AddressComponent> equatedComponents, Address a, Address b), para poder chamar algo como

bool addressAreKindOfEqual = AddressUtils.addressComponentsAreEqual(
    new EnumSet.of(
        AddressComponent.streetNumber, 
        AddressComponent.zipCode,
    ),
    addressA, addressB
);

Isso obviamente é muito mais digitado, mas pode evitar que você tenha uma explosão exponencial de métodos de verificação de igualdade.

0

A igualdade é sutil para acertar e sua importância é enganosamente abrangente. Especialmente nas linguagens em que a implementação de um operador de igualdade de repente significa que seu objeto deve ser agradável com conjuntos e mapas.

Na esmagadora maioria dos casos, a igualdade deve ser identidade, o que significa que um objeto é igual a outro se e somente se for o mesmo pedaço de memória com o mesmo endereço. A relação de identidade sempre respeita todas as condições para uma relação de igualdade adequada: reflexividade, transitividade etc. A identidade também é a maneira mais rápida de comparar duas coisas, pois você apenas compara os dois indicadores. Respeitar os contratos de relação de equivalência é a coisa mais importante sobre qualquer implementação de igualdade, pois a falha em fazer isso se traduz em bugs que são notoriamente difíceis de diagnosticar.

A segunda maneira de implementar iguais é comparar se os tipos correspondem e comparar todos os campos "de propriedade" do objeto. Isso geralmente acaba se repetindo nos detalhes de cada objeto. Se o seu objeto entrar em estruturas de dados que chamam iguais, provavelmente será igual ao que a estrutura de dados passa a maior parte do tempo se você usar essa abordagem. Existem outros problemas:

  • se o objeto muda, o resultado de sua comparação com outros objetos também muda, o que quebra todos os tipos de suposições que as classes padrão fazem sobre igualdade;
  • se seu objeto estiver em uma hierarquia de classe/interface, a única maneira sensata de comparar dois objetos nessa hierarquia é se seus tipos concretos corresponderem exatamente (consulte Java efetivo excelente de Joshua Bloch livro para mais detalhes sobre isso);
  • se você tentar tornar o relacionamento de igualdade muito rigoroso, incluindo o maior número possível de campos, acabará em uma situação em que sua igualdade não corresponde a uma lógica comercial de "igualdade".

A terceira maneira seria selecionar apenas os campos relevantes para a lógica de negócios e ignorar o restante. A probabilidade dessa abordagem ser quebrada é arbitrariamente próxima de 1. A primeira razão mencionada por outros é que uma comparação que faz sentido em um contexto não ' necessariamente faz sentido em todos os contextos . A linguagem solicita que você defina uma igualdade de forma, portanto é melhor funcionar em todos os contextos. Para endereços, essa lógica de comparação simplesmente não existe. Você pode ter especializado "esses dois endereços aparência idênticos" métodos, mas não deve arriscar que esse método seja o Only True Way To Compare como isso inevitavelmente confundirá os leitores.

Também recomendo que os programadores da Falsehoods acreditem nos endereços: https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/ é uma leitura divertida e pode ajudar a evitar algumas armadilhas.

0
Kafein