Sovrascrivere il metodo equals () in Java

2

Breve domanda: perché Java consente di sovrascrivere equals() , perché non è definitivo?

Sto leggendo la seconda edizione di Java efficace di Joshua Bloch. Sono un po 'sconcertato dalla conclusione che

There is no way to extend an instantiable class and add a value component while preserving the equals contract

Non sta dicendo lo stesso che equals() dovrebbe essere un metodo final ?

Il libro fornisce un esempio di una classe A con equals() metodo e poi una classe AX estendendo A con il proprio metodo equals() che è diverso dal metodo equals() in A .

Non entrerò nei dettagli di equals() in A e equals() in AX , ma è sufficiente dire che sono diversi . Pertanto abbiamo un problema di interoperabilità che garantisce la violazione della transitività e / o della simmetria (forse anche qualcos'altro) di equals() quando mischiamo diverse implementazioni di A in alcuni contesti (specialmente HashSet , HashMap type).

Pensando ulteriormente, non credo di essere d'accordo con la conclusione che avere qualcosa di simile

public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
      return false;
    ...
}

è sbagliato. Penso che questo sia esattamente il modo corretto per gestire l'override di equals() .

Java lo rende possibile, quindi Java consente di sovrascrivere equals() per un motivo. Se avesse preso in considerazione il principio di sostituzione di Liskov in senso stretto, allora non avrebbe permesso di sovrascrivere equals() e implicitamente rende qualsiasi implementazione di equals() finale a livello di compilatore. Quali sono i tuoi pensieri?

Posso pensare a un caso in cui la composizione non è semplicemente adatta e sovrascrivere equals() è l'opzione migliore. Questo è il caso in cui la classe A deve essere resa persistente in un database e il contesto implica che non esiste alcuna raccolta con un'implementazione di A e sottoclassi di A come AX .

    
posta InformedA 04.02.2015 - 07:14
fonte

3 risposte

2

equals() è un sottoprodotto di un tentativo di migliorare C ++ quando è stato creato. C ++ ha un overloading dell'operatore che ti permette di eseguire operazioni personalizzate quando chiamate con operatori validi in altri modi, come & lt ;, & gt ;,! =, ==, even =.

Il team ha preso la decisione (saggiamente) di rendere l'uguaglianza un metodo di classe piuttosto che avere metodi statici esterni come è stato fatto in C ++. Tuttavia, ciò significava anche che equals() abbinato a hashCode() definiva come tali classi venivano gestite nelle classi di raccolta.

Dato che qualsiasi classe potrebbe teoricamente sovrascrivere equals() o hashCode() , significa che solo perché hai una collezione di un certo tipo non garantisci un comportamento uniforme.

Per esempio, supponiamo che la classe A abbia due membri xey usati per determinare l'uguaglianza. Arriva la classe B che ha x, yez. Se un'istanza di B avesse gli stessi valori xey, come andresti ad inserire questa istanza in Set ? Se chiami equals di un'istanza di A, determinerà che i due siano uguali e se chiami equals di un'istanza di B, restituirà false poiché non è un'istanza di B.

Per essere perfettamente corretto, la classe B dovrebbe trattare l'elemento z come condizione aggiuntiva solo nel caso in cui sia un'istanza di B, altrimenti si presta al metodo equals() di classe A e, se una cosa del genere non è possibile, la classe B non dovrebbe permettersi di sovrascrivere equals() o hashCode() . Ciò crea una situazione appiccicosa in quanto in teoria non dovresti preoccuparti di come la classe genitore lavori da un punto di vista dell'implementazione (se fatto giusto comunque), ma eccoci qui.

Potresti fare una finale di classe A per evitare che succedano cose del genere, ma ovviamente non puoi mai estendere la classe A. Java punta a rendere certe classi standard come String final per prevenire complicazioni di questo tipo (decisione molto intelligente su la loro parte). Penso che alla fine della giornata, ciò che conta è che tu stia molto attento nell'utilizzo di equals() e hashCode() . Cerco di usarlo con parsimonia, e sono sempre consapevole di quali classi intendo essere disponibili in una libreria e quali classi sono per uso interno in modo da non creare condizioni in cui le cose potrebbero andare terribilmente storte.

Il principio di sostituzione di Liskov va bene in teoria, ma in pratica non si riesce mai a gestirlo. Prendi Collection come esempio. Collection è implementato da ArrayList , Set o LinkedList tra gli altri. Se è vero che potresti raggiungere lo stesso obiettivo finale sostituendo un Collection con say a HashSet , non è un'implementazione ideale per eseguire operazioni su tutti gli oggetti contenuti all'interno (meglio LinkedHashSet a quel punto). Non romperà il codice esistente, ma potresti renderlo in modo grossolanamente inefficiente a seconda di come viene utilizzato Collection . Considera anche questo un esempio abbastanza pulito.

Se sei fortunato, cambiano solo i dettagli di implementazione, ma molti si comportano in modo radicalmente diverso, con alcuni metodi che lanciano un NotImplementedException .

Quindi richiedere che le classi che implementano equals() debbano rispettare il principio di sostituzione di Liskov è molto chiedere, e sospetto che non volessero alienare la maggior parte dei programmatori C ++ che hanno familiarità con Java.

    
risposta data 04.02.2015 - 10:09
fonte
3

Un semplice argomento contro la tua logica è, a che cosa serve il metodo equals se è modificato per essere un finale. Quindi eseguirà la stessa logica contro qualsiasi dato oggetto. Se vuoi confrontare A e AX nella classe dell'oggetto quale sarebbe la logica?

L'override del metodo finale può essere in grado di verificare se due oggetti diversi (che vivono in 2 diverse locazioni di memoria) sono effettivamente uguali (== il metodo è lì per verificare se due riferimenti sono dello stesso oggetto) in base alla logica definita . Ad esempio se hai un oggetto utente come questo,

User
{
    string name;
    string email;
    string phone;
}

Per un sistema, sarebbe l'email che potresti interessare a identificare univocamente un utente. Quindi puoi confrontare in base al valore della posta elettronica. Ma alcuni altri sistemi potrebbero utilizzare il telefono come identificativo univoco. Quindi possono decidere di implementare diversi metodi di uguaglianza.

Quando si tratta della logica del principio di sostituzione di A, AX e Liksov che è,

Le funzioni che utilizzano puntatori o riferimenti alle classi base devono essere in grado di utilizzare oggetti di classi derivate senza saperlo.

Quindi, se lo fai,

AX ax1 = new AX();
AX ax2 = new AX();
ax1.equals(ax2);

e

A a = new AX();
AX ax = new AX();
a.equals(ax);

dovrebbe darti lo stesso risultato, e lo sarà.

    
risposta data 04.02.2015 - 11:48
fonte
1

Per dare una risposta breve alla tua domanda breve - è perché la funzione equals () definita per java.lang.Object è in gran parte inutile. È solo equivalente a "==" in quanto restituisce true se i due riferimenti si riferiscono allo stesso oggetto, e in realtà non verifica se sono "uguali" nel senso normale della parola.

Quindi, se in realtà vuoi confrontare due oggetti per vedere se contengono dati uguali, devi sovrascrivere l'implementazione predefinita di equals (). Normalmente dovresti sovrascrivere hashCode ().

    
risposta data 04.02.2015 - 13:13
fonte