Un'istanza potrebbe essere uguale a qualche 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 essere uguali ma possono avere due diversi codici hash. Non è un po 'sbagliato?

Credo che questo sarebbe meglio se si eseguisse invece il cast degli operandi.


1
L'esempio con punti colorati come indicato nel link ha più senso per me. Vorrei considerare che un punto 2D (x, y) può essere visto come un punto 3D con un componente Z zero (x, y, 0) e vorrei che l'uguaglianza restituisse false nel tuo caso. In effetti, nell'articolo, si dice esplicitamente che un ColoredPoint è diverso da un Point e restituisce sempre false.
coredump,

10
Niente di peggio dei tutorial che rompono le convenzioni comuni ... Ci vogliono anni per spezzare quel tipo di abitudini dai programmatori.
corsiKa,

3
@coredump Trattare un punto 2D come avere una zcoordinata zero potrebbe essere una convenzione utile per alcune applicazioni (vengono in mente i primi sistemi CAD che gestiscono i dati legacy). Ma è una convenzione arbitraria. I piani in spazi con 3 o più dimensioni possono avere orientamenti arbitrari ... è ciò che rende interessanti problemi interessanti.
Ben Ruders,

Risposte:


71

Questo non dovrebbe essere l'uguaglianza perché rompe la transitività . 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 lanciare il tipo di riferimento. Quello che vuoi davvero qui è un metodo di conversione che creerà un nuovo Point2Doggetto da un Point3Doggetto. Ciò renderebbe anche l'espressione più significativa:

twoD.equals(threeD.projectXY())

1
L'articolo descrive le implementazioni che interrompono la transitività e offre una serie di soluzioni alternative. In un dominio in cui consentiamo punti 2D, abbiamo già deciso che la terza dimensione non ha importanza. e quindi (10, 20, 50)uguale (10, 20, 60)va bene. Ci preoccupiamo solo di 10e 20.
Ben Ruders,

1
Dovrebbe Point2Davere un projectXYZ()metodo per fornire una Point3Drappresentazione di se stesso? In altre parole, le implementazioni dovrebbero conoscersi?
hjk

4
@hjk Sbarazzarsi Point2Dsembra più semplice poiché la proiezione di punti 2D richiede di definire prima il loro piano nello spazio 3D. Se il punto 2D sa che è piano, allora è già un punto 3D. In caso contrario, non può proiettare. Mi viene in mente la pianura di Abbott .
Ben Ruders,

@benrudgers Puoi, tuttavia, definire un Plane3Doggetto, che definirà un piano nello spazio 3D, quel piano può avere un liftmetodo (2D-> 3D sta sollevando, non proiettando) che accetterà un Point2De un numero per il "terzo asse "- distanza dal piano lungo il piano normale. Per facilità d'uso, puoi definire i piani comuni come costanti statiche, in modo da poter fare cose comePlane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye,

@IdanArye Stavo commentando il suggerimento che i punti 2D dovrebbero avere un metodo di proiezione. Per quanto riguarda i piani con metodi di sollevamento, penso che avrebbe bisogno di due argomenti per avere un senso: un punto 2D e il piano su cui si presume si trovino, cioè deve davvero essere una proiezione se non possiede il punto ... e se possiede il punto, perché non possedere solo un punto 3D e eliminare un tipo di dati problematico e l'odore di un metodo kludged? YMMV.
ben rudgers,

10

Mi allontano dalla lettura dell'articolo pensando alla saggezza di Alan J. Perlis:

Epigramma 9. È meglio disporre di 100 funzioni su una struttura di dati rispetto a 10 funzioni su 10 strutture di dati.

Il fatto che ottenere "l'uguaglianza" sia il tipo di problema che tiene sveglio l' inventore di Scala di Martin Ordersky dovrebbe fare una pausa sul fatto che scavalcare equalsun albero ereditario sia una buona idea.

Quello che succede quando siamo sfortunati a ottenere un ColoredPointerrore è che la nostra geometria fallisce perché abbiamo usato l'ereditarietà per proliferare i tipi di dati anziché crearne uno buono. Questo nonostante sia necessario tornare indietro e modificare il nodo radice dell'albero ereditario per far equalsfunzionare. Perché non aggiungere solo a ze colora Point?

La buona ragione sarebbe che PointeColoredPoint operare in domini diversi ... almeno se quei domini non si mescolavano mai. Tuttavia, in tal caso, non è necessario eseguire l'override equals. Il confronto ColoredPointe Pointper l'uguaglianza ha senso solo in un terzo dominio in cui è permesso mescolarsi. E in quel caso, probabilmente è meglio avere l '"uguaglianza" su misura per quel terzo dominio piuttosto che cercare di applicare la semantica di uguaglianza dall'uno o dall'altro o entrambi i domini non mescolati. In altre parole, "uguaglianza" dovrebbe essere definita locale nel luogo in cui abbiamo fango che scorre da entrambe le parti perché potremmo non voler ColoredPoint.equals(pt)fallire contro i casi Pointanche se l'autore ha ColoredPointpensato che fosse una buona idea sei mesi fa alle 2 del mattino .


6

Quando i vecchi dei di programmazione stavano inventando una programmazione orientata agli oggetti con le classi, decisero quando si trattava di composizione ed eredità di avere due relazioni per un oggetto: "è un" e "ha un".
Ciò ha parzialmente risolto il problema delle sottoclassi rispetto alle classi principali, ma le ha rese utilizzabili senza interrompere il codice. Poiché un'istanza della sottoclasse "è un" oggetto superclasse e può essere sostituita direttamente per esso, anche se la sottoclasse ha più funzioni membro o membri dati, la "ha una" garantisce che eseguirà tutte le funzioni del genitore e avrà tutte le sue membri. Quindi si potrebbe dire che un Point3D "è un" Punto e un Point2D "è un" Punto se entrambi ereditano da Point. Inoltre un Point3D potrebbe essere una sottoclasse di Point2D.

La parità tra le classi è specifica del dominio del problema, tuttavia, e l'esempio sopra è ambiguo su ciò di cui il programmatore ha bisogno affinché il programma funzioni correttamente. Generalmente, vengono seguite le regole del dominio matematico e i valori dei dati generano uguaglianza se si limita l'ambito del confronto a solo in questo caso due dimensioni, ma non se si confrontano tutti i membri dei dati.

Quindi ottieni una tabella di restringimento delle uguaglianze:

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. 

In genere scegli le regole più rigide che puoi che eseguiranno ancora tutte le funzioni necessarie nel tuo dominio problematico. I test di uguaglianza integrati per i numeri sono progettati per essere tanto restrittivi quanto possono essere a fini matematici, ma il programmatore ha molti modi per aggirare questo se non fosse questo l'obiettivo, inclusi arrotondamento su / giù, troncamento, gt, lt, ecc. . Gli oggetti con timestamp vengono spesso confrontati in base al tempo di generazione e quindi ogni istanza deve essere unica in modo che i confronti siano 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 dei dati degli oggetti è ciò che devi fare e che può diventare molto costoso se hai molti e molti oggetti con molti membri dei dati. Le alternative sono solo di confrontare i valori di dati rilevanti o fare in modo che l'oggetto generi un valore di hash dei suoi membri di dati interessati per un rapido confronto con altri oggetti simili, mantenere le raccolte ordinate e potate per rendere i confronti più veloci e meno intensivi della CPU e forse consentire oggetti che sono identici nei dati da abbattere e al suo posto viene inserito un puntatore duplicato a un singolo oggetto.


2

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


1
Può essere OK sovrascrivere hashCode senza sovrascrivere equals. Ad esempio, si farebbe questo per testare un algoritmo di hashing diverso per la stessa condizione di uguaglianza.
Patricia Shanahan,

1

È spesso utile per le classi non pubbliche avere un metodo di test di equivalenza che consenta a oggetti di tipi diversi di considerarsi "uguali" se rappresentano le stesse informazioni, ma poiché Java non consente in che modo le classi possano impersonare ciascuna altro è spesso utile avere un singolo tipo di wrapper rivolto al pubblico nei casi in cui potrebbe essere possibile avere oggetti equivalenti con rappresentazioni diverse.

Ad esempio, considera una classe che incapsula una matrice di doublevalori 2D immutabile . Se un metodo esterno richiede una matrice di identità di dimensioni 1000, un secondo richiede una matrice diagonale e passa una matrice contenente 1000, e un terzo chiede una matrice 2D e passa una matrice 1000x1000 in cui tutti 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 utilizzare internamente diversi backing store [il primo con un singolo campo per dimensione, il secondo con un array di mille elementi e il terzo con un array di 1000 elementi] ma dovrebbero riferirsi reciprocamente come equivalenti [poiché tutti e tre incapsulano una matrice immutabile 1000x1000 con quelli sulla diagonale e zero ovunque].

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

Tieni presente 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 lo so"), quindi il normale metodo "uguale" non sarebbe adatto. Mentre qualsiasi oggetto potrebbe semplicemente rispondere "Non lo so" quando viene chiesto ad altri, aggiungere la logica ad esempio a una matrice diagonale che non si preoccuperebbe di chiedere a qualsiasi matrice di identità o matrice diagonale su qualsiasi elemento al di fuori della diagonale principale accelererebbe notevolmente i confronti tra tali tipi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.