it-swarm.it

È una buona idea avere una logica nel metodo uguale che non corrisponde esattamente?

Mentre assistevamo uno studente con un progetto universitario, abbiamo lavorato su un esercizio Java fornito dall'università che ha definito una classe per un indirizzo con i campi:

number
street
city
zipcode

E ha specificato che la logica uguale deve restituire vero se il numero e il codice postale corrispondono.

Una volta mi è stato insegnato che il metodo uguale dovrebbe solo fare un confronto esatto tra gli oggetti (dopo aver controllato il puntatore), il che ha un senso per me, ma contraddice con il compito che gli è stato assegnato.

Posso capire perché vorresti sovrascrivere la logica in modo da poter usare cose come list.contains() con la tua corrispondenza parziale, ma mi chiedo se questo sia considerato kosher, e se no perché no?

35
William Dunne

Definire l'uguaglianza per due oggetti

L'uguaglianza può essere definita arbitrariamente per due oggetti qualsiasi. Non esiste una regola rigida che vieti a qualcuno di definire in qualsiasi modo desideri. Tuttavia, l'uguaglianza è spesso definita quando è significativa per le regole di dominio di ciò che viene implementato.

Si prevede che segua il contratto di relazione di equivalenza :

  • È riflessivo : per qualsiasi valore di riferimento non nullo x, x.equals (x) dovrebbe restituire vero.
  • È simmetrico : per qualsiasi valore di riferimento non nullo x e y, x.equals (y) dovrebbe restituire vero se e solo se y.equals ( x) restituisce vero.
  • È transitivo : per qualsiasi valore di riferimento non nullo x, ye z, se x.equals (y) restituisce true e y.equals ( z) restituisce true, quindi x.equals (z) dovrebbe restituire true.
  • È coerente : per qualsiasi valore di riferimento non nullo xey, più invocazioni di x.equals (y) restituiscono costantemente true o restituiscono costantemente false, a condizione che non vengano modificate informazioni utilizzate in uguali confronti sugli oggetti.
  • Per qualsiasi valore di riferimento non nullo x, x.equals (null) dovrebbe restituire false.

Nel tuo esempio, forse non è necessario distinguere due indirizzi che hanno lo stesso codice postale e lo stesso numero di diversi. Ci sono domini che sono perfettamente ragionevoli aspettarsi che funzioni il seguente codice:

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

Questo può essere utile, come hai detto, perché quando non ti importa che siano oggetti diversi, ti importa solo dei valori che detengono. Forse il codice postale + il numero civico sono sufficienti per identificare l'indirizzo corretto e le informazioni rimanenti sono "extra" e non si desidera che tali informazioni aggiuntive influiscano sulla logica della parità.

Questo potrebbe essere un modello perfettamente valido per un software. Assicurati solo che ci sia della documentazione o dei test unitari per garantire questo comportamento e che l'API pubblica rifletta questo uso.


Non dimenticare hashCode()

Un ulteriore dettaglio rilevante per l'implementazione è il fatto che molte lingue utilizzano fortemente il concetto di codice hash . Queste lingue, Java compreso, di solito assumono la seguente proposta:

Se x.equals (y) quindi x.hashCode () e y.hashCode () sono uguali.

Dallo stesso link di prima:

Si noti che è generalmente necessario sovrascrivere il metodo hashCode ogni volta che questo metodo (uguale a) viene ignorato, in modo da mantenere il contratto generale per il metodo hashCode, che afferma che oggetti uguali devono avere codici hash uguali.

Notare che avere lo stesso hashCode non significa che due oggetti siano uguali !

In tal senso, quando si implementa l'uguaglianza, si dovrebbe anche implementare una hashCode() che segue la proprietà sopra menzionata. Questo hashCode() viene utilizzato dalle strutture di dati per l'efficienza e garantire limiti superiori alla complessità delle loro operazioni.

Venire con una buona funzione di codice hash è difficile e un intero argomento su se stesso. Idealmente, il codice hash di due oggetti diversi dovrebbe essere diverso o avere una distribuzione uniforme tra le occorrenze di istanze.

Ma tieni presente che la seguente semplice implementazione soddisfa ancora la proprietà di uguaglianza, anche se non è una "buona" funzione hash:

public int hashCode() {
    return 0;
}

Un modo più comune di implementare il codice hash è usare i codici hash dei campi che definiscono l'uguaglianza e fare un'operazione binaria su di essi. Nel tuo esempio, codice postale e numero civico. Spesso è fatto come:

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

Se ambiguo, selezionare Chiarezza

Qui è dove faccio una distinzione su cosa ci si dovrebbe aspettare riguardo all'uguaglianza. Diverse persone hanno aspettative diverse riguardo all'uguaglianza e se stai cercando di seguire Principle of Least Stonishment puoi prendere in considerazione altre opzioni per descrivere meglio il tuo design.

Quale di quelli dovrebbe essere considerato uguale?

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?

Un caso potrebbe essere fatto per ognuno di quelli che sono veri o falsi. In caso di dubbio, si può definire una relazione diversa più chiara nel contesto del dominio.

Ad esempio, è possibile definire 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;

O nel caso di vettori, 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;

In questo modo, descrivi meglio le tue intenzioni progettuali per l'uguaglianza ed eviti di infrangere le aspettative dei futuri lettori riguardo a ciò che il tuo codice effettivamente fa. (Puoi dare un'occhiata a tutte le risposte leggermente diverse per vedere come le aspettative delle persone variano riguardo alla relazione di uguaglianza del tuo esempio)

89
Albuquerque

È nel contesto del compito universitario in cui lo scopo dell'attività è quello di esplorare e comprendere l'override dell'operatore. Questo sembra un compito esemplificativo che ha abbastanza scopo implicito da farlo apparire come un esercizio utile in quel momento.

Tuttavia, se questa fosse una recensione del mio codice, la contrassegnerei come un difetto di progettazione significativo.

Il problema è questo Abilita il codice sintatticamente pulito che sembra ovviamente corretto:

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

E sulla base dei commenti di altri utenti, questo codice produrrebbe esiti corretti in Brasile, dove i codici postali sono unici per una strada. Tuttavia, se poi hai provato a utilizzare questo software negli Stati Uniti, dove questo presupposto non è più valido, questo codice sembra ancora corretto.

se questo fosse stato implementato come:

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

poi qualche anno dopo, quando a un diverso sviluppatore brasiliano viene data la base di codice e viene detto che il software consegna i pacchi agli indirizzi errati per il loro nuovo cliente in California, l'assunto ora rotto è ovvio nel codice ed è visibile al punto di decisione su se consegnare o meno - che è probabilmente il primo posto che il programmatore di manutenzione guarda per vedere perché il pacco viene consegnato all'indirizzo sbagliato.

Avere la logica non ovvia nascosta in un sovraccarico dell'operatore renderà la correzione del codice più lunga. Per rilevare questo problema in questo codice, probabilmente richiederebbe una sessione con un debugger che lo attraversa.

42
Michael Shaw

L'uguaglianza è una questione di contesto. Il fatto che due oggetti siano considerati uguali o meno è una questione tanto di contesto quanto di due oggetti coinvolti.

Quindi, if nel tuo contesto ha senso ignorare la città e la strada, quindi non c'è problema ad attuare l'uguaglianza basata esclusivamente sul codice postale e sul numero. (Come è stato sottolineato in uno dei commenti, codice postale e numero sono sufficienti per identificare in modo univoco un indirizzo in Brasile.)

Ovviamente, dovresti assicurarti di seguire le regole appropriate per sovraccaricare l'uguaglianza, ad esempio assicurandoti di sovraccaricare hashCode di conseguenza.

25
Jörg W Mittag

Un operatore di uguaglianza affermerà che due oggetti sono uguali se e solo se devono essere considerati uguali, a causa di qualsiasi considerazione che ritieni utile.

Lo ripeterò: per qualsiasi considerazione che ritieni utile.

Lo sviluppatore del software è al posto di guida qui. Oltre ad essere coerente con ovvi requisiti (a = a, a = b implica b + a, a = b e b = c implica a = c) e coerenza con la funzione hash) l'operatore di uguaglianza può essere quello che ti piace.

3
gnasher729

Sebbene siano state fornite molte risposte, la mia opinione non è ancora presente.

Una volta mi hanno insegnato che il metodo uguale dovrebbe fare solo un confronto esatto tra gli oggetti

A parte ciò che dicono le regole, questa definizione è ciò che le persone assumono dalla loro introduzione quando parlano di guaglianza. Alcune risposte affermano che l'uguaglianza dipende dal contesto. Hanno ragione in un certo senso che gli oggetti possono essere uguali anche se non tutti i loro campi corrispondono. Ma la comprensione comune di "è uguale" non dovrebbe essere ridefinita troppo.

Tornando all'argomento, per me un indirizzo uguale a un altro se punta nella stessa posizione.

In Germania ci possono essere diverse specifiche di una città, ad esempio se un sobborgo è chiamato. Quindi la città di un indirizzo nel sobborgo SUB può essere indicata solo come "Città principale" o "Città principale, SUB" o anche solo "SUB". Perché dare il nome della città principale è ok, tutti i nomi delle strade in una città e tutti i sobborghi assegnati devono essere univoci.

Qui il codice postale è sufficiente per dire alla città, anche se il nome della città varia.
Ma lasciare la strada NON è unico, a meno che il codice postale non indichi anche una strada ben nota, che di solito non lo è.
Quindi non è intuitivo considerare due indirizzi uguali se possono puntare a posizioni diverse la cui differenza consiste nei campi ignorati.

Se esiste un caso d'uso che richiede solo alcuni ma tutti i campi, il metodo di confronto che lo fa dovrebbe essere nominato in modo appropriato. Esiste un solo metodo "uguale" che non deve essere segretamente trasformato in "uguale per un solo caso d'uso speciale, ma nessuno può vederlo".

Ciò significa che, per le ragioni spiegate, direi ...

ma mi chiedo se questo è considerato kosher

Senza sapere se ti trovi accidentalmente in un luogo in cui i nomi delle strade non contano: no, non lo è.
Se vuoi programmare qualcosa che non viene utilizzato solo in una tale posizione: no, non lo è.
Se vuoi dare agli studenti la sensazione di fare le cose nel modo giusto e di mantenere il codice comprensibile e logico: no, non lo è.

2
puck

Sebbene il requisito dato contraddica senso umano è OK lasciare che solo un sottoinsieme delle proprietà degli oggetti definisca il significato di "unico".

Il problema qui è che esiste una relazione tecnica tra equals() e hashcode() in modo che per due oggetti a e b di quel tipo sia considerato:
if a.equals(b) then a.hashcode()==b.hashcode()
Se disponi di un sottoinsieme delle proprietà che definiscono le tue condizioni di unicità, devi utilizzare lo stesso sottoinsieme per calcolare il valore di ritorno di hashcode().

Dopo tutto l'approccio molto più appropriato per il requisito potrebbe essere stato quello di implementare Comparable o persino un metodo personalizzato isSame().

1
Timothy Truckle

Dipende.

È una buona idea ...? Dipende. Può essere una buona idea, se stai sviluppando un'applicazione che verrà utilizzata una sola volta , ad esempio, in un compito univercity (se stai andando per eliminare il codice dopo la revisione dell'assegnazione) o alcune utilità di migrazione (migrare i dati legacy una volta e non è più necessaria l'utilità).

Ma nel settore IT in molti casi sarebbe una cattiva idea. Perché? @ Jörg W Mittag ha detto L'uguaglianza è una questione di contesto ... se nel tuo contesto ha senso ... . Ma spesso lo stesso oggetto viene utilizzato in molti contesti diversi che hanno diversi vista sull'uguaglianza. Alcuni esempi di come in modo diverso può essere definita l'uguaglianza della stessa entità:

  • Come uguaglianza di tutti gli attributi di due entità
  • Come uguaglianza di chiavi primarie di due entità
  • Come uguaglianza di chiavi primarie e versioni di due entità
  • Come uguaglianza di tutti gli attributi "business" tranne la chiave primaria e la versione

Se si implementa in equals () la logica per un particolare contesto, sarà difficile in seguito utilizzare questo oggetto in altri contesti, perché molti sviluppatori nei team del progetto non conosceranno esattamente la logica per quale contesto è esattamente implementato lì e in quali casi possono fare affidamento su di esso. In alcuni casi lo useranno in modo errato (come descritto da @Michael Shaw), in altri casi ignoreranno la logica e implementeranno i propri metodi per lo stesso scopo (che potrebbe funzionare in modo diverso da quello che ti aspettavi).

Se l'applicazione verrà utilizzata per un periodo più lungo come 2-3 anni, normalmente ci saranno più nuovi requisiti, più cambiamenti e più contesti. E molto probabilmente ci saranno aspettative multiple sull'uguaglianza. Ecco perché suggerirei:

  • Implementare equals () formalmente, senza connessione al contesto aziendale, significa senza alcuna logica aziendale, proprio come l'uguaglianza di tutti gli attributi degli oggetti (ovviamente hashCode/equals il contratto deve essere seguito)
  • Per ogni contesto fornire un metodo separato che implementa l'uguaglianza nel senso di questo contesto, come isPrimaryKeyAndVersionEqual () , areBusinessAttributesEqual () .

Quindi per trovare un oggetto in un contesto particolare basta usare il metodo corrispondente, come segue:

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

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

Quindi ci saranno meno bug nel codice, l'analisi del codice sarà più facile, il cambio dell'applicazione per i nuovi requisiti sarà più facile.

1
mentallurg

Come altri menzionati, l'uguaglianza da un lato è solo un concetto matematico che soddisfa alcune proprietà (vedi ad esempio di Albuquerque risposta). D'altra parte, la sua semantica e la sua attuazione sono determinate dal contesto.

Indipendentemente dai dettagli di implementazione, prendi ad esempio una classe che rappresenta espressioni aritmetiche (come (1 + 3) * 5). Se si implementa un interprete per tali espressioni utilizzando le regole di valutazione standard per le espressioni aritmetiche, ha senso considerare le rispettive istanze per (1 + 3) * 5 e 10 + 10 deve essere equal. Tuttavia, se si implementa una stampante carina per tali espressioni sopra le istanze non verrebbero considerate equal, mentre (1 + 3) * 5 e (1+3)*5 voluto.

0
michid

Come altri hanno già detto, la semantica esatta dell'uguaglianza degli oggetti fa parte della definizione del dominio aziendale. In questo caso, non credo sia ragionevole avere un oggetto "generale" come Address (contenente number, street, city, zipcode) avere una definizione molto stretta di uguaglianza (che, come altri hanno già detto, funziona in Brasile ma non negli Stati Uniti, per esempio).

Invece, vorrei che Address avesse una semantica simile al valore per l'uguaglianza (definita dall'uguaglianza di tutti i membri). Vorrei quindi:

  1. Crea una classe StreeNumberAndZip (# TODO: bad name), Che contiene solo un street e un zipCode e definisce equals su quelli. Ogni volta che vuoi confrontare due Address oggetti in quel particolare modo, puoi fare addressA.streetNumberAndZip().equals(addressB.streetNumberAndZip()), o ...
  2. Crea una classe AddressUtils con un metodo bool equalStreeNumberAndZipCode(Address a, Address b), che definisce la stretta uguaglianza lì.

In entrambi i casi, hai ancora accesso all'uso di addressA.equals(addressB) per il controllo completo dell'uguaglianza.

Per n campi di un oggetto, esistono 2^n Diverse definizioni di uguaglianza (ogni campo può essere incluso o escluso dal controllo). Se ti trovi a dover controllare l'uguaglianza in molti modi diversi, potrebbe anche essere utile avere qualcosa come un enum AddressComponent. Potresti quindi avere un bool addressComponentsAreEqual(EnumSet<AddressComponent> equatedComponents, Address a, Address b), quindi puoi chiamare qualcosa del genere

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

Questo ovviamente è molto più digitazione, ma può salvarti dall'esplosione esponenziale dei metodi di controllo dell'uguaglianza.

L'uguaglianza è sottile da ottenere e la sua importanza è ingannevolmente di vasta portata. Soprattutto nelle lingue in cui l'implementazione di un operatore di uguaglianza significa improvvisamente che il tuo oggetto dovrebbe giocare a Nizza con set e mappe.

Nella stragrande maggioranza dei casi, l'uguaglianza dovrebbe essere identità, nel senso che un oggetto è uguale a un altro se e solo se è lo stesso pezzo di memoria con il stesso indirizzo. La relazione di identità rispetta sempre tutte le condizioni per una corretta relazione di uguaglianza: riflessività, transitività ecc. L'identità è anche il modo più veloce per confrontare due cose, poiché si confrontano semplicemente i due puntatori. Il rispetto dei contratti di relazione di equivalenza è la cosa più importante di qualsiasi implementazione di uguaglianza poiché la mancata osservanza si traduce in bug notoriamente difficili da diagnosticare.

Il secondo modo di implementare uguale è confrontare se i tipi corrispondono quindi confrontare ogni campo "posseduto" dell'oggetto. Questo spesso finisce per ricorrere molto lontano nei dettagli di ogni oggetto. Se il tuo oggetto entra in strutture dati che chiamano uguale, probabilmente sarà ciò che la struttura dati impiega la maggior parte del suo tempo a fare se usi questo approccio. Ci sono altri problemi:

  • se l'oggetto cambia, cambia anche il risultato del suo confronto con altri oggetti, il che rompe ogni sorta di ipotesi che le classi standard fanno sull'uguaglianza;
  • se il tuo oggetto si trova in una gerarchia di classi/interfacce, l'unico modo sano per confrontare due oggetti in quella gerarchia è se i loro tipi concreti corrispondono esattamente (vedi l'eccellente Joshua Bloch Java efficace prenota per maggiori dettagli al riguardo);
  • se provi a rendere molto severa la relazione sull'uguaglianza includendo il maggior numero di campi possibile, alla fine finirai in una situazione in cui la tua uguaglianza non corrisponde a una logica aziendale di "identità".

Il terzo modo sarebbe selezionare solo i campi rilevanti per la logica aziendale e ignorare il resto. La probabilità che questo approccio venga infranto è arbitrariamente vicina a 1. La prima ragione, come menzionato da altri, è che un confronto che ha senso in uno contesto non lo fa ' ha necessariamente senso in tutti i contesti . La lingua ti chiede di definire una forma uguaglianza, quindi funziona meglio in tutti i contesti. Per gli indirizzi, una tale logica di confronto semplicemente non esiste. Puoi avere "quei due indirizzi specializzati specializzati identici", ma non dovresti rischiare che tale metodo sia il solo vero metodo supportato dal linguaggio per confrontare ciò confonderà inevitabilmente i lettori.

Consiglierei anche di dare un'occhiata ai falsi che i programmatori credono sugli indirizzi: https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/ è una lettura divertente e potrebbe aiutarti ad evitare alcune insidie.

0
Kafein