Potrebbe essere un'istanza uguale ad un'altra istanza di un tipo più specifico?

25

Ho letto questo articolo: Come scrivere un metodo di uguaglianza in Java .

Fondamentalmente, fornisce una soluzione per un metodo equals () che supporta l'ereditarietà:

Point2D twoD   = new Point2D(10, 20);
Point3D threeD = new Point3D(10, 20, 50);
twoD.equals(threeD); // true
threeD.equals(twoD); // true

Ma è una buona idea? queste due istanze sembrano uguali ma potrebbero avere due codici hash diversi. Non è un po 'sbagliato?

Credo che sarebbe meglio farlo con il cast degli operandi.

    
posta Wes 28.10.2015 - 14:53
fonte

5 risposte

71

Questo non dovrebbe essere l'uguaglianza perché rompe transitivity . Considera queste due espressioni:

new Point3D(10, 20, 50).equals(new Point2D(10, 20)) // true
new Point2D(10, 20).equals(new Point3D(10, 20, 60)) // true

Poiché l'uguaglianza è transitiva, ciò dovrebbe significare che anche la seguente espressione è vera:

new Point3D(10, 20, 50).equals(new Point3D(10, 20, 60))

Ma certo - non lo è.

Quindi, la tua idea di casting è corretta - aspettati che in Java, casting significhi semplicemente il casting del tipo di riferimento. Quello che vuoi veramente qui è un metodo di conversione che creerà un nuovo oggetto Point2D da un oggetto Point3D . Ciò renderebbe anche l'espressione più significativa:

twoD.equals(threeD.projectXY())
    
risposta data 28.10.2015 - 15:13
fonte
10

Mi allontano dal leggere l'articolo pensando al saggezza di Alan J. Perlis:

Epigram 9. It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.

Il fatto che ottenere il diritto di "uguaglianza" è il tipo di problema che mantiene Martin Ordersky l'inventore di Scala di notte dovrebbe dare una pausa sull'opportunità di sovrascrivere equals in un albero di ereditarietà.

Ciò che accade quando siamo sfortunati a ottenere un ColoredPoint è che la nostra geometria fallisce perché abbiamo usato l'ereditarietà per proliferare i tipi di dati piuttosto che crearne uno buono. Questo, nonostante debba tornare indietro e modificare il nodo radice dell'albero di ereditarietà per fare in modo che equals funzioni. Perché non aggiungere solo z e color a Point ?

La buona ragione è che Point e ColoredPoint operino in domini diversi ... almeno se quei domini non si sono mai mescolati. Tuttavia, se questo è il caso, non è necessario eseguire l'override di equals . Il confronto tra ColoredPoint e Point per l'uguaglianza ha senso solo in un terzo dominio in cui è consentito socializzare. E in tal caso, probabilmente è meglio avere l'uguaglianza su misura per quel terzo dominio piuttosto che provare ad applicare la semantica dell'uguaglianza da uno o l'altro o da entrambi i domini non protetti. In altre parole, "l'uguaglianza" dovrebbe essere definita localmente nel luogo in cui abbiamo il fango che scorre da entrambi i lati, perché non possiamo volere che ColoredPoint.equals(pt) fallisca contro istanze di Point anche se l'autore di ColoredPoint lo ha pensato è stata una buona idea sei mesi fa alle 2 del mattino.

    
risposta data 28.10.2015 - 17:06
fonte
6

Quando le vecchie divinità di programmazione inventavano la programmazione orientata agli oggetti con le classi, decisero quando si trattava di composizione e ereditarietà di avere due relazioni per un oggetto: "è un" e "ha un".
Questo ha in parte risolto il problema che le sottoclassi fossero diverse rispetto alle classi genitore, ma le rendeva utilizzabili senza la rottura del codice. Poiché un'istanza di sottoclasse "è un" oggetto superclasse e può essere sostituita direttamente per questo, anche se la sottoclasse ha più funzioni membro o membri dati, il "ha a" garantisce che eseguirà tutte le funzioni del genitore e avrà tutte le sue funzioni membri. Quindi potresti dire che Point3D "è un" Punto, e un Punto 2D "è un" Punto se entrambi ereditano da Punto. Inoltre, Point3D potrebbe essere una sottoclasse di Point2D.

L'uguaglianza tra le classi è specifica per il dominio del problema, tuttavia, e l'esempio sopra è ambiguo su ciò che il programmatore ha bisogno per il corretto funzionamento del programma. Generalmente, le regole del dominio matematico vengono seguite ei valori dei dati generano l'uguaglianza se si limita l'ambito della comparazione solo in questo caso due dimensioni, ma non se si confrontano tutti i membri dei dati.

Quindi ottieni una tabella di uguaglianze di restringimento:

Both objects have same values, limited to subset of shared members

Child classes can be equal to parent classes if parent and childs
data members are the same.

Both objects entire data members are the same.

Objects must have all same values and be similar classes. 

Objects must have all same values and be the same class type. 

Equality is determined by specific logical conditions in the domain.

Only Objects that both point to same instance are equal. 

Di solito scegli le regole più rigide che puoi ancora svolgere tutte le funzioni necessarie nel tuo dominio problematico. I test di uguaglianza incorporati per i numeri sono progettati per essere restrittivi come possono essere per scopi matematici, ma il programmatore ha molti modi per farlo, se questo non è l'obiettivo, compresi arrotondamenti su, giù, troncamento, gt, lt, ecc. . Gli oggetti con data e ora vengono spesso confrontati in base al tempo di generazione e quindi ogni istanza deve essere univoca, pertanto i confronti diventano molto specifici.

Il fattore di progettazione in questo caso è determinare modi efficienti per confrontare gli oggetti. A volte un confronto ricorsivo di tutti i membri di dati di oggetti è ciò che devi fare e questo può diventare molto costoso se hai un sacco di oggetti con molti membri di dati. Le alternative sono solo confrontare valori di dati rilevanti, o fare in modo che l'oggetto generi un valore hash dei suoi membri di dati interessati per un rapido confronto con altri oggetti simili, mantenere raccolte e sfoltire le raccolte per rendere i confronti più veloci e meno intensivi della CPU, e forse consentire oggetti che sono identici nei dati da eliminare e un puntatore duplicato a un singolo oggetto deve essere posizionato al suo posto.

    
risposta data 28.10.2015 - 19:23
fonte
2

La regola è che, ogni volta che si sostituisce hashcode() , si sostituisce equals() e viceversa. Se questa è una buona idea o meno dipende dall'uso previsto. Personalmente, andrei con un metodo diverso ( isLike() o simile) per ottenere lo stesso effetto.

    
risposta data 28.10.2015 - 15:11
fonte
1

Spesso è utile per le classi non di pubblico avere un metodo di test di equivalenza che consente a oggetti di diverso tipo di considerarsi a vicenda "uguale" se rappresentano la stessa informazione, ma poiché Java non consente alcun modo in cui le classi possono impersonarsi a vicenda, è spesso opportuno disporre di un singolo tipo di wrapper pubblico nei casi in cui sia possibile avere oggetti equivalenti con rappresentazioni differenti.

Ad esempio, considera una classe che incapsula una matrice 2D immutabile di valori double . Se un metodo esterno richiede una matrice di identità di dimensione 1000, un secondo richiede una matrice diagonale e passa una matrice contenente 1000 uni, e un terzo chiede una matrice 2D e passa un array 1000x1000 dove gli elementi sulla diagonale primaria sono tutti 1.0 e tutti gli altri sono zero, gli oggetti dati a tutte e tre le classi possono usare internamente diversi backing stores [il primo ha un singolo campo per le dimensioni, il secondo ha un array di mille elementi e il terzo ha migliaia di matrici di 1000 elementi] ma dovrebbe riferirsi l'un l'altro come equivalente [poiché tutti e tre incapsulano una matrice immutabile 1000x1000 con quelli sulla diagonale e zero ovunque altrove].

Oltre al fatto che nasconde l'esistenza di distinti tipi di backing-store, il wrapper sarà anche utile per facilitare i confronti, dal momento che il controllo degli elementi per l'equivalenza sarà generalmente un processo a più fasi. Chiedi il primo oggetto se sa se è uguale al secondo; se non lo sa, chiedi al secondo se sa se è uguale al primo. Se nessuno dei due sa, allora chiedi a ciascun array il contenuto dei suoi singoli elementi [uno potrebbe aggiungere altri controlli prima di decidere di fare il lungo-lento percorso di comparazione degli oggetti].

Si noti che il metodo di test di equivalenza per ciascun oggetto in questo scenario dovrebbe restituire un valore a tre stati ("Sì, sono equivalente", "No non sono equivalente" o "Non so "), quindi il normale metodo" uguale "non sarebbe adatto. Mentre qualsiasi oggetto potrebbe semplicemente rispondere "Non so" quando gli viene chiesto di qualsiasi altro, aggiungendo la logica ad es. una matrice diagonale che non si preoccuperebbe di chiedere una matrice di identità o una matrice diagonale su qualsiasi elemento fuori dalla diagonale principale accelererebbe notevolmente i confronti tra tali tipi.

    
risposta data 29.10.2015 - 16:05
fonte

Leggi altre domande sui tag