Casi legittimi di avere .equals () che si comporta in modo incoerente con .compareTo ()?

5

La documentazione Java dice che " è strongmente raccomandato " per far sì che si comportino in modo coerente.

Ma ci sono casi legittimi di metodo java / c # / python / etc Object.equals() che si comporta in modo incoerente con il metodo Comparable.compareTo() ?

    
posta Wes 06.05.2015 - 00:51
fonte

4 risposte

15

Il motivo per cui hai due metodi diversi è che fanno due cose diverse.

Il metodo .equals restituisce un valore booleano che indica se l'oggetto su cui si chiama il metodo è uguale a l'oggetto passato come parametro (per alcune definizioni di "è uguale a" che è coerente con la natura dell'oggetto che viene confrontato).

Il metodo .compareTo restituisce un numero intero negativo, zero o un numero intero positivo poiché questo oggetto è minore, uguale o maggiore dell'oggetto specificato. Questo lo rende un metodo utile per l'ordinamento; ti permette di confrontare un'istanza con un'altra per scopi di ordinare.

Quando la documentazione Java dice che questi due metodi devono comportarsi in modo coerente, il loro significato è che il metodo .equals deve restituire true esattamente nelle stesse situazioni in cui il metodo .compareTo restituisce zero e deve restituire false esattamente nelle stesse situazioni in cui il metodo .compareTo restituisce un numero diverso da zero.

C'è qualche buona ragione per violare queste regole? Generalmente no, per gli stessi motivi che

#define TRUE FALSE

è una pessima idea. L'unica ragione legittima per comportamento inconsistente è suggerita nella documentazione Java stessa: "[se la] classe ha un ordinamento naturale che è incoerente con gli uguali."

Per riportare a casa il punto, puoi effettivamente definire .equals() in termini di compareTo() , garantendo così un comportamento coerente. Considera questo metodo .equals() da una classe Rational che, dopo alcuni controlli di integrità, definisce semplicemente .equals come compareTo() == 0 :

public boolean equals(Object y) {
    if (y == null) return false;
    if (y.getClass() != this.getClass()) return false;
    Rational b = (Rational) y;
    return compareTo(b) == 0;
}
    
risposta data 06.05.2015 - 01:19
fonte
7

Questa confusione si verificherebbe in situazioni in cui ci sono intese conflittuali di equals .

compareTo è "facile" in quanto pone una semplice domanda "quale è più grande?" Se ti è stata data una serie di Cose , come li ordinerai?

equals d'altra parte vuole chiedere "sono la stessa cosa?"

BigDecimal in Java è uno di questi posti dove ci sono intese conflittuali su cosa significa avere due cose uguali.

BigDecimal foo = new BigDecimal("1.00");
BigDecimal bar = new BigDecimal("1.000");

Queste due entrate avranno lo stesso significato per l'ordinamento. Tuttavia, essi sono non uguali per quanto riguarda l'uguaglianza. Hanno uno stato sottostante diverso (la precisione del numero) e ciò significa che non sono uguali ma foo.compareTo(bar) restituirà 0.

Considera questo, se per qualche motivo avevi un Map qux = HashMap<BigDecimal, Object>() , vuoi qux.put(foo,foo) per prendere lo stesso punto di qux.put(bar,bar) e quindi sfrattare l'inserimento precedente?

Quindi, mentre sono matematici uguali (che è il modo in cui CompareTo li ordina), non sono lo stato interno uguale, e quindi la necessità dell'incoerenza qui.

Sì, questa incoerenza arriva al prezzo di un carico cognitivo più alto per trattare con BigDecimal. Significa che le mappe potrebbero non comportarsi come vuoi tu ... e la domanda è "quale mappa vuoi comportarti correttamente?"

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.TreeMap;

class Main {
    public static void main (String[] args) {
        BigDecimal foo = new BigDecimal("1.00");
        BigDecimal bar = new BigDecimal("1.000");

        HashMap<BigDecimal, String> hash = new HashMap();
        TreeMap<BigDecimal, String> tree = new TreeMap();

        hash.put(foo, "foo");
        hash.put(bar, "bar");

        tree.put(foo, "foo");
        tree.put(bar, "bar");

        System.out.println("hash foo: " + hash.get(foo));
        System.out.println("hash bar: " + hash.get(bar));

        System.out.println("tree foo: " + tree.get(foo));
        System.out.println("tree bar: " + tree.get(bar));
    }
}

ideone

Output:

hash foo: foo
hash bar: bar
tree foo: bar
tree bar: bar

Poiché compareTo ha restituito 0, nella TreeMap, bar rimosso foo quando è stata inserita la barra. Tuttavia, poiché si tratta di oggetti diversi con diversi stati interni e quindi diversi codici hash, entrambi erano in grado di esistere all'interno di una HashMap.

Dai documenti :

Note: care should be exercised if BigDecimal objects are used as keys in a SortedMap or elements in a SortedSet since BigDecimal's natural ordering is inconsistent with equals. See Comparable, SortedMap or SortedSet for more information.

E così, questo è il problema, l'incoerenza e il "non c'è una buona risposta a questo" dilemma.

Si potrebbe anche immaginare questo con una classe Rational in cui si vuole mantenere 2/4 con lo stato interno di 2/4 in modo che:

Rational twoFourths = new Rational(2,4);
Rational oneHalf = new Rational(1,2);

System.out.println(twoFourths); // prints 2/4
System.out.println(oneHalf);    // prints 1/2
System.out.println(twoFourths.compareTo(oneHalf)); // prints 0
System.out.println(twoFourths.equals(oneHalf));    // ???

E tu sei di nuovo contro la stessa domanda. Sono uguali al senso matematico dell'uguaglianza (il che significa che l'hashCode deve anche restituire lo stesso valore?) O non sono uguali usando lo stato orientato agli oggetti del senso di uguaglianza dell'oggetto nonostante sia lo 'stesso'.

    
risposta data 07.05.2015 - 03:22
fonte
2

Il "motivo legittimo" per avere equals() incoerente con compareTo() è se servono a diversi scopi del mondo reale.

Iniziamo con BigDecimal . Se stai semplicemente ordinando un elenco di valori, probabilmente non ti interessa la scala. Tuttavia, se stai controllando l'uguaglianza, quello che stai veramente controllando è che la serie di operazioni che ha portato a questo risultato sono le stesse . Per essere onesti, sono sorpreso che gli sviluppatori di BigDecimal non abbiano scelto di includere la modalità di arrotondamento nei test per l'uguaglianza.

Come altro esempio, considera le entità di indirizzo del cliente memorizzate in un database. Puoi ottenere tariffe postali più economiche se indirizzi gli indirizzi di massa, quindi potresti scegliere di basare l'ordinamento naturale sul codice postale e sull'indirizzo. Tuttavia, anche se due indirizzi sono identici in tutti gli aspetti esteriori, potrebbero non essere uguali , poiché un particolare indirizzo appartiene a un particolare cliente (e ti troverai in tutti i tipi di problemi se provi a condividili).

Questi sono due casi in cui l'uguaglianza richiede una garanzia più strong rispetto all'ordine. Il contrario è possibile anche se improbabile: una volta ho implementato un sistema in cui dovevo garantire un ordinamento stabile di diverse istanze, quindi ho aggiunto un test aggiuntivo a confronto, in modo tale che le diverse istanze non sarebbero mai comparabili come 0. Ma non ricordo i dettagli di quel sistema, e non può tirar fuori un buon esempio sul momento.

Come altre persone hanno detto, devi stare attento quando usi oggetti in cui l'uguaglianza e l'ordine sono incoerenti. Personalmente, preferisco implementare un Comparator piuttosto che avere le classi come Comparable .

    
risposta data 07.05.2015 - 13:42
fonte
2

But are there legitimate cases of java/c#/python/etc Object.equals() method behaving inconsistently with the method Comparable.compareTo()?

Sì, ad esempio, ecco un caso legittimo in Python 2.x. Il valore float per NaN non confronta uguale ( == ) a se stesso, tuttavia cmp() (equivalente a compareTo ) restituirà 0 (uguale), che è incoerente con il risultato di == :

>>> x = float('nan')
>>> x == x
False
>>> cmp(x, x)
0

(Nota: cmp() è stato rimosso in Python 3, ecco perché ho specificato solo Python 2.x.)

NaN è un tipo di caso speciale nella maggior parte dei linguaggi, perché IEEE 754 definisce NaN come non uguale a se stesso, qualcosa che non è vero per praticamente qualsiasi altro valore incontrato nella programmazione.

È interessante notare che Java e C # non hanno questa incoerenza. Anche se NaN nel tipo primitivo double non è uguale a se stesso usando l'operatore di uguaglianza primitiva, il metodo equals degli oggetti Double testerà NaN uguale a se stesso, per preservare la coerenza con compareTo e per consentire l'uso come chiavi nei dizionari. Quindi hanno optato per rendere l'uguaglianza primitiva e equals incoerenti, piuttosto che rendere equals e compareTo inconssitent.

Anche Ruby non ha questa incoerenza. NaN rispetto a se stesso con l'operatore <=> restituisce nil (incomparabile). Così hanno evitato l'incoerenza limitando la comparabilità a un sottoinsieme di numeri.

    
risposta data 07.05.2015 - 22:26
fonte

Leggi altre domande sui tag