JavaRush /Java Blog /Random-IT /Confronto di oggetti: pratica
articles
Livello 15

Confronto di oggetti: pratica

Pubblicato nel gruppo Random-IT
Questo è il secondo degli articoli dedicati al confronto degli oggetti. Il primo ha discusso le basi teoriche del confronto: come viene eseguito, perché e dove viene utilizzato. In questo articolo parleremo direttamente del confronto di numeri, oggetti, casi particolari, sottigliezze e punti non ovvi. Più precisamente, ecco di cosa parleremo:
Confronto di oggetti: pratica - 1
  • Confronto tra stringhe: ' ==' eequals
  • MetodoString.intern
  • Confronto tra primitive reali
  • +0.0E-0.0
  • SensoNaN
  • Giava 5.0. Metodi di generazione e confronto tramite ' =='
  • Giava 5.0. Autoboxing/Unboxing: ' ==', ' >=' e ' <=' per i wrapper degli oggetti.
  • Giava 5.0. confronto degli elementi enum (tipo enum)
Quindi iniziamo!

Confronto tra stringhe: ' ==' eequals

Ah, queste righe... Uno dei tipi più comunemente usati, che causa molti problemi. In linea di principio, c'è un articolo separato su di loro . E qui toccherò questioni di confronto. Naturalmente, le stringhe possono essere confrontate utilizzando equals. Inoltre, DEVONO essere confrontati tramite equals. Tuttavia, ci sono sottigliezze che vale la pena conoscere. Prima di tutto, stringhe identiche sono in realtà un singolo oggetto. Questo può essere facilmente verificato eseguendo il seguente codice:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
Il risultato sarà "lo stesso" . Ciò significa che i riferimenti alle stringhe sono uguali. Questo viene fatto a livello di compilatore, ovviamente per risparmiare memoria. Il compilatore crea UN'istanza della stringa e assegna str1un str2riferimento a questa istanza. Tuttavia, ciò si applica solo alle stringhe dichiarate come valori letterali nel codice. Se componi una stringa da pezzi, il collegamento ad essa sarà diverso. Conferma - questo esempio:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
Il risultato sarà "non lo stesso" . Puoi anche creare un nuovo oggetto usando il costruttore di copia:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
Anche il risultato sarà "non lo stesso" . Pertanto, a volte le stringhe possono essere confrontate tramite il confronto dei riferimenti. Ma è meglio non fare affidamento su questo. Vorrei soffermarmi su un metodo molto interessante che consente di ottenere la cosiddetta rappresentazione canonica di una stringa - String.intern. Parliamone più in dettaglio.

Metodo String.intern

Cominciamo dal fatto che la classe Stringsupporta uno string pool. Tutti i valori letterali stringa definiti nelle classi, e non solo, vengono aggiunti a questo pool. Quindi, il metodo internconsente di ottenere da questo pool una stringa che è uguale a quella esistente (quella su cui viene chiamato il metodo intern) dal punto di vista di equals. Se tale riga non esiste nel pool, quella esistente viene posizionata lì e viene restituito un collegamento ad essa. Pertanto, anche se i riferimenti a due stringhe uguali sono diversi (come nei due esempi sopra), le chiamate a queste stringhe internrestituiranno un riferimento allo stesso oggetto:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
Il risultato dell'esecuzione di questo pezzo di codice sarà "lo stesso" . Non posso dire esattamente perché è stato fatto in questo modo. Il metodo internè nativo e, a dire il vero, non voglio addentrarmi nel mondo selvaggio del codice C. Molto probabilmente questo viene fatto per ottimizzare il consumo di memoria e le prestazioni. In ogni caso, vale la pena conoscere questa funzionalità di implementazione. Passiamo alla parte successiva.

Confronto tra primitive reali

Per cominciare, voglio porre una domanda. Molto semplice. Qual è la seguente somma: 0,3f + 0,4f? Perché? 0,7f? Controlliamo:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
Di conseguenza? Come? Anche io. Per coloro che non hanno completato questo frammento, dirò che il risultato sarà...
f1==f2: false
Perché succede questo?... Eseguiamo un altro test:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1="+(double)f1);
System.out.println("f2="+(double)f2);
System.out.println("f3="+(double)f3);
System.out.println("f4="+(double)f4);
Da notare la conversione in double. Questo viene fatto per emettere più cifre decimali. Risultato:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
A rigor di termini, il risultato è prevedibile. La rappresentazione della parte frazionaria viene effettuata utilizzando una serie finita 2-n, e quindi non è necessario parlare della rappresentazione esatta di un numero scelto arbitrariamente. Come si vede dall'esempio la precisione di rappresentazione floatè di 7 cifre decimali. A rigor di termini, la rappresentazione float assegna 24 bit alla mantissa. Pertanto, il numero assoluto minimo che può essere rappresentato utilizzando float (senza tenere conto del grado, perché parliamo di precisione) è 2-24≈6*10-8. È con questo passaggio che i valori nella rappresentazione vanno effettivamente float. E poiché c'è la quantizzazione, c'è anche un errore. Da qui la conclusione: i numeri in una rappresentazione floatpossono essere confrontati solo con una certa precisione. Consiglierei di arrotondarli alla 6a cifra decimale (10-6), o, preferibilmente, di verificare il valore assoluto della differenza tra loro:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3-f4|<1e-6: "+( Math.abs(f3-f4) < 1e-6 ));
In questo caso il risultato è incoraggiante:
|f3-f4|<1e-6: true
Naturalmente, l'immagine è esattamente la stessa del tipo double. L'unica differenza è che per la mantissa sono allocati 53 bit, pertanto la precisione di rappresentazione è 2-53≈10-16. Sì, il valore di quantizzazione è molto più piccolo, ma c'è. E può fare uno scherzo crudele. A proposito, nella libreria di test JUnit , nei metodi per confrontare i numeri reali, la precisione è specificata esplicitamente. Quelli. il metodo di confronto contiene tre parametri: il numero, a cosa dovrebbe essere uguale e l'accuratezza del confronto. A proposito, vorrei menzionare le sottigliezze associate alla scrittura di numeri in formato scientifico, indicando il grado. Domanda. Come scrivere 10-6? La pratica dimostra che oltre l’80% risponde – 10e-6. Nel frattempo, la risposta corretta è 1e-6! E 10e-6 è 10-5! Abbiamo calpestato questo rastrello in uno dei progetti, in modo del tutto inaspettato. Hanno cercato l'errore per molto tempo, hanno guardato le costanti 20 volte e nessuno aveva ombra di dubbio sulla loro correttezza, finché un giorno, quasi per caso, è stata stampata la costante 10e-3 e ne hanno trovate due cifre dopo la virgola invece delle tre previste. Pertanto, fai attenzione! Andiamo avanti.

+0,0 e -0,0

Nella rappresentazione dei numeri reali il bit più significativo è con segno. Cosa succede se tutti gli altri bit sono 0? A differenza degli interi, dove in una situazione del genere il risultato è un numero negativo situato al limite inferiore dell'intervallo di rappresentazione, anche un numero reale con solo il bit più significativo impostato a 1 significa 0, solo con il segno meno. Pertanto, abbiamo due zeri: +0,0 e -0,0. Sorge una domanda logica: questi numeri dovrebbero essere considerati uguali? La macchina virtuale pensa esattamente in questo modo. Tuttavia, questi sono due numeri diversi , perché come risultato delle operazioni con essi si ottengono valori diversi:
float f1 = 0.0f/1.0f;
float f2 = 0.0f/-1.0f;
System.out.println("f1="+f1);
System.out.println("f2="+f2);
System.out.println("f1==f2: "+(f1==f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3="+f3);
System.out.println("f4="+f4);
...e il risultato:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
Quindi in alcuni casi ha senso trattare +0.0 e -0.0 come due numeri diversi. E se abbiamo due oggetti, in uno dei quali il campo è +0,0 e nell'altro -0,0, anche questi oggetti possono essere considerati disuguali. Sorge la domanda: come puoi capire che i numeri non sono uguali se il loro confronto diretto con una macchina virtuale dà true? La risposta è questa. Anche se la macchina virtuale considera questi numeri uguali, le loro rappresentazioni sono comunque diverse. Pertanto l’unica cosa che si può fare è confrontare le opinioni. E per ottenerlo esistono i metodi int Float.floatToIntBits(float)e long Double.doubleToLongBits(double), che restituiscono una rappresentazione di bit nella forma inte longrispettivamente (continuazione dell'esempio precedente):
int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0):"+ Integer.toBinaryString(i1));
System.out.println("i2 (-0.0):"+ Integer.toBinaryString(i2));
System.out.println("i1==i2: "+(i1 == i2));
Il risultato sarà
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
Pertanto, se +0.0 e -0.0 sono numeri diversi, dovresti confrontare le variabili reali attraverso la loro rappresentazione in bit. Sembra che abbiamo risolto +0.0 e -0.0. -0.0, tuttavia, non è l'unica sorpresa. Esiste anche una cosa come...

Valore NaN

NaNsta per Not-a-Number. Questo valore appare come risultato di operazioni matematiche errate, ad esempio la divisione 0,0 per 0,0, infinito per infinito, ecc. La particolarità di questo valore è che non è uguale a se stesso. Quelli.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...risulterà...
x=NaN
x==x: false
Come può risultare questo quando si confrontano gli oggetti? Se il campo dell'oggetto è uguale a NaN, il confronto darà false, cioè è garantito che gli oggetti siano considerati disuguali. Anche se, logicamente, potremmo volere esattamente il contrario. È possibile ottenere il risultato desiderato utilizzando il metodo Float.isNaN(float). Restituisce truese l'argomento è NaN. In questo caso, non farei affidamento sul confronto delle rappresentazioni di bit, perché non è standardizzato. Forse questo è abbastanza per i primitivi. Passiamo ora alle sottigliezze apparse in Java dalla versione 5.0. E il primo punto che vorrei toccare è

Giava 5.0. Metodi di generazione e confronto tramite ' =='

Esiste uno schema nel design chiamato metodo di produzione. A volte il suo utilizzo è molto più redditizio rispetto all'utilizzo di un costruttore. Lasciate che vi faccia un esempio. Penso di conoscere bene la shell dell'oggetto Boolean. Questa classe è immutabile e può contenere solo due valori. Cioè, infatti, per qualsiasi esigenza ne bastano solo due copie. E se li crei in anticipo e poi li restituisci semplicemente, sarà molto più veloce che usare un costruttore. Esiste un metodo del genere Boolean: valueOf(boolean). È apparso nella versione 1.4. Metodi di produzione simili sono stati introdotti nella versione 5.0 nelle classi Byte, Character, e . Quando queste classi vengono caricate, vengono creati array delle loro istanze corrispondenti a determinati intervalli di valori primitivi. Questi intervalli sono i seguenti: ShortIntegerLong
Confronto di oggetti: pratica - 2
Ciò significa che quando si utilizza il metodo, valueOf(...)se l'argomento rientra nell'intervallo specificato, verrà restituito sempre lo stesso oggetto. Forse questo dà un certo aumento di velocità. Ma allo stesso tempo sorgono problemi di natura tale che può essere abbastanza difficile andare fino in fondo. Leggi di più a riguardo. In teoria, il metodo di produzione valueOfè stato aggiunto sia alle classi Floatche a Double. La loro descrizione dice che se non hai bisogno di una nuova copia, è meglio usare questo metodo, perché può dare un aumento di velocità, ecc. e così via. Tuttavia, nell'implementazione attuale (Java 5.0), in questo metodo viene creata una nuova istanza, ad es. Non è garantito che il suo utilizzo dia un aumento della velocità. Inoltre, è difficile per me immaginare come questo metodo possa essere accelerato, perché a causa della continuità dei valori non è possibile organizzare una cache lì. Fatta eccezione per i numeri interi. Voglio dire, senza la parte frazionaria.

Giava 5.0. Autoboxing/Unboxing: ' ==', ' >=' e ' <=' per i wrapper degli oggetti.

Sospetto che i metodi di produzione e la cache dell'istanza siano stati aggiunti ai wrapper per le primitive intere per ottimizzare le operazioni autoboxing/unboxing. Lascia che ti ricordi di cosa si tratta. Se un oggetto deve essere coinvolto in un'operazione, ma è coinvolta una primitiva, questa primitiva viene automaticamente racchiusa in un wrapper di oggetto. Questo autoboxing. E viceversa: se in un'operazione deve essere coinvolta una primitiva, è possibile sostituirla con una shell dell'oggetto e il valore verrà automaticamente espanso da essa. Questo unboxing. Naturalmente, devi pagare per tale comodità. Le operazioni di conversione automatica rallentano leggermente le prestazioni dell'applicazione. Tuttavia, questo non è rilevante per l’argomento attuale, quindi lasciamo questa domanda. Tutto va bene finché abbiamo a che fare con operazioni chiaramente correlate a primitive o shell. Cosa accadrà all'operazione ' =='? Diciamo che abbiamo due oggetti Integercon lo stesso valore al suo interno. Come si confronteranno?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Risultato:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
Risultato:
i1==i2: true
Ora questo è più interessante! Se autoboxing-e vengono restituiti gli stessi oggetti! È qui che sta la trappola. Una volta scoperto che vengono restituiti gli stessi oggetti, inizieremo a sperimentare per vedere se è sempre così. E quanti valori controlleremo? Uno? Dieci? Cento? Molto probabilmente ci limiteremo a un centinaio in ciascuna direzione attorno allo zero. E otteniamo l’uguaglianza ovunque. Sembrerebbe che vada tutto bene. Tuttavia, guardiamo un po' indietro, qui . Hai indovinato qual è il problema?... Sì, le istanze delle shell degli oggetti durante l'autoboxing vengono create utilizzando metodi di produzione. Ciò è ben illustrato dal seguente test:
public class AutoboxingTest {

    private static final int numbers[] = new int[]{-129,-128,127,128};

    public static void main(String[] args) {
        for (int number : numbers) {
            Integer i1 = number;
            Integer i2 = number;
            System.out.println("number=" + number + ": " + (i1 == i2));
        }
    }
}
Il risultato sarà così:
number=-129: false
number=-128: true
number=127: true
number=128: false
Per i valori che rientrano nell'intervallo di caching vengono restituiti oggetti identici, per quelli al di fuori di esso vengono restituiti oggetti diversi. Pertanto, se da qualche parte nell'applicazione vengono confrontate le shell invece delle primitive, c'è la possibilità di ottenere l'errore più terribile: quello mobile. Perché molto probabilmente il codice verrà testato anche su un range limitato di valori in cui questo errore non comparirà. Ma nel lavoro reale apparirà o scomparirà, a seconda dei risultati di alcuni calcoli. È più facile impazzire che trovare un simile errore. Pertanto, ti consiglierei di evitare l'autoboxing ove possibile. E non è tutto. Ricordiamo la matematica, non oltre la 5a elementare. Lasciamo che le disuguaglianze A>=Be А<=B. Cosa si può dire della relazione Ae B? C'è solo una cosa: sono uguali. Sei d'accordo? Penso di si. Eseguiamo il test:
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
Risultato:
i1>=i2: true
i1<=i2: true
i1==i2: false
E questa è la cosa strana più grande per me. Non capisco affatto perché questa caratteristica sia stata introdotta nel linguaggio se introduce tali contraddizioni. In generale, lo ripeterò ancora una volta: se è possibile farne a meno autoboxing/unboxing, allora vale la pena sfruttare al meglio questa opportunità. L'ultimo argomento che vorrei toccare è... Java 5.0. confronto degli elementi di enumerazione (tipo enum) Come sapete, dalla versione 5.0 Java ha introdotto un tipo come enum - enumerazione. Le sue istanze per impostazione predefinita contengono il nome e il numero di sequenza nella dichiarazione di istanza nella classe. Di conseguenza, quando cambia l'ordine degli annunci, i numeri cambiano. Tuttavia, come ho detto nell'articolo 'Serializzazione così com'è' , ciò non causa problemi. Tutti gli elementi di enumerazione esistono in un'unica copia, questa è controllata a livello di macchina virtuale. Pertanto, possono essere confrontati direttamente, utilizzando i collegamenti. * * * Forse per oggi è tutto riguardo al lato pratico dell'implementazione del confronto tra oggetti. Forse mi sto perdendo qualcosa. Come sempre, aspetto con ansia i vostri commenti! Per ora, lasciami andare. Grazie a tutti per l'attenzione! Link alla fonte: Confronto di oggetti: pratica
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION