JavaRush /Blog Java /Random-PL /Porównanie obiektów: praktyka
articles
Poziom 15

Porównanie obiektów: praktyka

Opublikowano w grupie Random-PL
To drugi z artykułów poświęconych porównywaniu obiektów. W pierwszym z nich omówiono teoretyczne podstawy porównania – jak się to robi, dlaczego i gdzie się je wykorzystuje. W tym artykule porozmawiamy bezpośrednio o porównywaniu liczb, obiektów, przypadków specjalnych, subtelności i nieoczywistych punktów. Dokładniej, oto o czym będziemy rozmawiać:
Porównanie obiektów: praktyka - 1
  • Porównanie ciągów: ' ==' iequals
  • metodaString.intern
  • Porównanie rzeczywistych prymitywów
  • +0.0I-0.0
  • OznaczającyNaN
  • Java 5.0. Generowanie metod i porównywanie poprzez „ ==
  • Java 5.0. Automatyczne pakowanie/rozpakowywanie: „ ==”, „ >=” i „ <=” dla opakowań obiektów.
  • Java 5.0. porównanie elementów wyliczeniowych (typ enum)
Więc zacznijmy!

Porównanie ciągów: ' ==' iequals

Ach te linie... Jeden z najczęściej używanych typów, który sprawia mnóstwo problemów. W zasadzie jest o nich osobny artykuł . I tutaj poruszę kwestie porównawcze. Oczywiście ciągi znaków można porównywać za pomocą equals. Co więcej, MUSZĄ być porównywane za pośrednictwem equals. Są jednak subtelności, o których warto wiedzieć. Po pierwsze, identyczne ciągi znaków są w rzeczywistości pojedynczym obiektem. Można to łatwo sprawdzić, uruchamiając następujący kod:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
Wynik będzie „taki sam” . Co oznacza, że ​​odniesienia do ciągów znaków są równe. Odbywa się to na poziomie kompilatora, oczywiście w celu zaoszczędzenia pamięci. Kompilator tworzy JEDNĄ instancję ciągu i przypisuje str1odwołanie str2do tej instancji. Dotyczy to jednak tylko ciągów zadeklarowanych w kodzie jako literały. Jeśli utworzysz ciąg znaków z kawałków, link do niego będzie inny. Potwierdzenie – ten przykład:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
Wynik będzie „nie taki sam” . Możesz także utworzyć nowy obiekt za pomocą konstruktora kopiującego:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
Wynik również będzie „nie taki sam” . Dlatego czasami ciągi znaków można porównywać poprzez porównanie referencji. Ale lepiej na tym nie polegać. Chciałbym poruszyć jedną bardzo interesującą metodę, która pozwala uzyskać tzw. reprezentację kanoniczną ciągu - String.intern. Porozmawiajmy o tym bardziej szczegółowo.

Metoda String.intern

Zacznijmy od tego, że klasa Stringobsługuje pulę stringów. Do tej puli dodawane są wszystkie literały łańcuchowe zdefiniowane w klasach i nie tylko. Zatem metoda internpozwala na pobranie z tej puli ciągu znaków równego istniejącemu (temu, na którym wywoływana jest metoda intern) z punktu widzenia equals. Jeśli takiego wiersza nie ma w puli, to istniejący jest tam umieszczany i zwracany jest link do niego. Zatem nawet jeśli odniesienia do dwóch równych ciągów znaków są różne (jak w dwóch powyższych przykładach), to wywołania tych ciągów internzwrócą odwołanie do tego samego obiektu:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
Wynik wykonania tego fragmentu kodu będzie „taki sam” . Nie potrafię dokładnie powiedzieć, dlaczego zostało to zrobione w ten sposób. Metoda internjest natywna i szczerze mówiąc, nie chcę zagłębiać się w świat kodu C. Najprawdopodobniej ma to na celu optymalizację zużycia pamięci i wydajności. W każdym razie warto wiedzieć o tej funkcji implementacji. Przejdźmy do kolejnej części.

Porównanie rzeczywistych prymitywów

Na początek chcę zadać pytanie. Bardzo prosta. Jaka jest następująca suma – 0,3f + 0,4f? Dlaczego? 0,7f? Sprawdźmy:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
W rezultacie? Tak jak? Ja też. Dla tych, którzy nie dokończyli tego fragmentu, powiem, że efektem będzie...
f1==f2: false
Dlaczego tak się dzieje?.. Wykonajmy kolejny 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);
Zwróć uwagę na konwersję do double. Odbywa się to w celu wygenerowania większej liczby miejsc po przecinku. Wynik:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
Ściśle mówiąc, wynik jest przewidywalny. Reprezentacja części ułamkowej odbywa się za pomocą skończonego szeregu 2-n, dlatego nie ma potrzeby mówić o dokładnej reprezentacji dowolnie wybranej liczby. Jak widać na przykładzie, dokładność reprezentacji floatwynosi 7 miejsc po przecinku. Ściśle mówiąc, reprezentacja float przydziela mantysie 24 bity. Zatem minimalna liczba bezwzględna, którą można przedstawić za pomocą float (bez uwzględnienia stopnia, bo mówimy o dokładności) to 2-24≈6*10-8. To właśnie w tym kroku wartości w reprezentacji faktycznie idą float. A ponieważ istnieje kwantyzacja, pojawia się również błąd. Stąd wniosek: liczby w przedstawieniu floatmożna porównywać tylko z pewną dokładnością. Radziłbym zaokrąglić je do 6 miejsca po przecinku (10-6) lub najlepiej sprawdzić wartość bezwzględną różnicy między nimi:
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 ));
W tym przypadku wynik jest zachęcający:
|f3-f4|<1e-6: true
Oczywiście zdjęcie jest dokładnie takie samo z typem double. Jedyna różnica polega na tym, że na mantysę przydzielono 53 bity, dlatego dokładność reprezentacji wynosi 2-53≈10-16. Tak, wartość kwantyzacji jest znacznie mniejsza, ale jest. I potrafi zrobić okrutny żart. Nawiasem mówiąc, w bibliotece testowej JUnit w metodach porównywania liczb rzeczywistych precyzja jest określona jawnie. Te. metoda porównania zawiera trzy parametry - liczbę, jaką powinna ona wynosić oraz dokładność porównania. Przy okazji chciałbym wspomnieć o subtelnościach związanych z pisaniem liczb w formacie naukowym, wskazując stopień. Pytanie. Jak napisać 10-6? Praktyka pokazuje, że ponad 80% odpowiada – 10e-6. Tymczasem prawidłowa odpowiedź to 1e-6! A 10e-6 to 10-5! Nadepnęliśmy na te grabie w jednym z projektów, całkiem niespodziewanie. Bardzo długo szukali błędu, sprawdzali stałe 20 razy i nikt nie miał cienia wątpliwości co do ich poprawności, aż pewnego dnia, w dużej mierze przez przypadek, wydrukowano stałą 10e-3 i znaleźli dwie cyfry po przecinku zamiast oczekiwanych trzech. Dlatego bądź ostrożny! Przejdźmy dalej.

+0,0 i -0,0

W reprezentacji liczb rzeczywistych najbardziej znaczący bit jest podpisany. Co się stanie, jeśli wszystkie pozostałe bity będą miały wartość 0? W przeciwieństwie do liczb całkowitych, gdzie w takiej sytuacji wynikiem jest liczba ujemna znajdująca się w dolnej granicy zakresu reprezentacji, liczba rzeczywista, w której tylko najbardziej znaczący bit jest ustawiony na 1, również oznacza 0, tylko ze znakiem minus. Mamy zatem dwa zera - +0,0 i -0,0. Powstaje logiczne pytanie: czy liczby te należy uznać za równe? Maszyna wirtualna myśli dokładnie w ten sposób. Są to jednak dwie różne liczby, ponieważ w wyniku operacji na nich uzyskuje się różne wartości:
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);
... i wynik:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
Dlatego w niektórych przypadkach sensowne jest traktowanie +0,0 i -0,0 jako dwóch różnych liczb. A jeśli mamy dwa obiekty, z czego w jednym pole wynosi +0,0, a w drugim -0,0, to te obiekty również można uznać za nierówne. Powstaje pytanie - jak można zrozumieć, że liczby są nierówne, jeśli daje to bezpośrednie porównanie z maszyną wirtualną true? Odpowiedź jest taka. Mimo że maszyna wirtualna uważa te liczby za równe, ich reprezentacje są nadal różne. Dlatego jedyne, co można zrobić, to porównać poglądy. A żeby to uzyskać służą metody int Float.floatToIntBits(float)i long Double.doubleToLongBits(double), które zwracają reprezentację bitową odpowiednio w postaci inti long(kontynuacja poprzedniego przykładu):
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));
Wynik będzie
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
Zatem, jeśli +0,0 i -0,0 to różne liczby, powinieneś porównać zmienne rzeczywiste poprzez ich reprezentację bitową. Wygląda na to, że uporządkowaliśmy +0,0 i -0,0. -0,0 to jednak nie jedyna niespodzianka. Jest też coś takiego jak...

Wartość NaN

NaNoznacza Not-a-Number. Wartość ta pojawia się w wyniku błędnych operacji matematycznych, powiedzmy dzielenia 0,0 przez 0,0, nieskończoności przez nieskończoność itp. Osobliwością tej wartości jest to, że nie jest ona równa samej sobie. Te.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...skutkuje...
x=NaN
x==x: false
Jak to może wyglądać przy porównywaniu obiektów? Jeśli pole obiektu jest równe NaN, to porównanie da false, tj. gwarantuje się, że obiekty zostaną uznane za nierówne. Chociaż logicznie rzecz biorąc, możemy chcieć czegoś zupełnie odwrotnego. Za pomocą metody możesz osiągnąć pożądany rezultat Float.isNaN(float). Zwraca, truejeśli argumentem jest NaN. W tym przypadku nie polegałbym na porównywaniu reprezentacji bitowych, ponieważ nie jest to ustandaryzowane. Być może wystarczy o prymitywach. Przejdźmy teraz do niuansów, które pojawiły się w Javie od wersji 5.0. I pierwsza kwestia, którą chciałbym poruszyć, to

Java 5.0. Generowanie metod i porównywanie poprzez „ ==

W projektowaniu istnieje wzorzec zwany metodą produkcji. Czasami jego użycie jest znacznie bardziej opłacalne niż użycie konstruktora. Dam ci przykład. Myślę, że dobrze znam powłokę obiektu Boolean. Ta klasa jest niezmienna i może zawierać tylko dwie wartości. Oznacza to, że na wszelkie potrzeby wystarczą tylko dwie kopie. A jeśli utworzysz je wcześniej, a następnie po prostu zwrócisz, będzie to znacznie szybsze niż użycie konstruktora. Istnieje taka metoda Boolean: valueOf(boolean). Pojawiło się w wersji 1.4. Podobne metody tworzenia zostały wprowadzone w wersji 5.0 w klasach Byte, Character, i . Po załadowaniu tych klas tworzone są tablice ich instancji odpowiadające określonym zakresom wartości pierwotnych. Zakresy te są następujące: ShortIntegerLong
Porównanie obiektów: praktyka - 2
Oznacza to, że w przypadku stosowania metody, valueOf(...)jeśli argument mieści się w podanym zakresie, zwrócony zostanie zawsze ten sam obiekt. Być może daje to pewien wzrost prędkości. Ale jednocześnie pojawiają się problemy tego rodzaju, że dotarcie do ich sedna może być dość trudne. Przeczytaj więcej na ten temat. Teoretycznie metoda wytwarzania valueOfzostała dodana zarówno do klas Float, jak i Double. Z ich opisu wynika, że ​​jeśli nie potrzebujesz nowej kopii, to lepiej zastosować tę metodę, bo może to spowodować zwiększenie prędkości itp. i tak dalej. Jednak w obecnej implementacji (Java 5.0) w tej metodzie tworzona jest nowa instancja, tj. Nie ma gwarancji, że jego użycie spowoduje wzrost prędkości. Co więcej, trudno mi sobie wyobrazić, jak można przyspieszyć tę metodę, ponieważ ze względu na ciągłość wartości nie można tam zorganizować pamięci podręcznej. Z wyjątkiem liczb całkowitych. To znaczy bez części ułamkowej.

Java 5.0. Automatyczne pakowanie/rozpakowywanie: „ ==”, „ >=” i „ <=” dla opakowań obiektów.

Podejrzewam, że metody produkcyjne i pamięć podręczna instancji zostały dodane do opakowań operacji pierwotnych na liczbach całkowitych w celu optymalizacji operacji autoboxing/unboxing. Przypomnę ci, co to jest. Jeśli obiekt musi być zaangażowany w operację, ale w grę wchodzi element pierwotny, wówczas ten element podstawowy jest automatycznie owijany w opakowanie obiektu. Ten autoboxing. I odwrotnie - jeśli w operacji musi być zaangażowany prymityw, możesz tam zastąpić powłokę obiektu, a wartość zostanie z niej automatycznie rozszerzona. Ten unboxing. Za taką wygodę trzeba oczywiście zapłacić. Automatyczne operacje konwersji spowalniają nieco aplikację. Nie ma to jednak związku z bieżącym tematem, więc zostawmy to pytanie. Wszystko jest w porządku, o ile mamy do czynienia z operacjami, które są wyraźnie powiązane z prymitywami lub powłokami. Co stanie się z ==operacją „”? Załóżmy, że mamy Integerw środku dwa obiekty o tej samej wartości. Jak będą porównywać?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Wynik:
i1==i2: false

Кто бы сомневался... Сравниваются они Jak obiektы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
Wynik:
i1==i2: true
Teraz jest ciekawiej! Jeśli autoboxing-e zwracane są te same obiekty! Tu właśnie kryje się pułapka. Gdy odkryjemy, że zwracane są te same obiekty, zaczniemy eksperymentować, aby sprawdzić, czy zawsze tak jest. A ile wartości sprawdzimy? Jeden? Dziesięć? Sto? Najprawdopodobniej ograniczymy się do stu w każdym kierunku wokół zera. I wszędzie mamy równość. Wydawać by się mogło, że wszystko jest w porządku. Jednak spójrz trochę wstecz, tutaj . Czy zgadłeś, na czym polega haczyk?.. Tak, instancje powłok obiektów podczas autoboxingu są tworzone przy użyciu metod produkcyjnych. Dobrze ilustruje to następujący 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));
        }
    }
}
Wynik będzie następujący:
number=-129: false
number=-128: true
number=127: true
number=128: false
Dla wartości mieszczących się w zakresie buforowania zwracane są identyczne obiekty, dla tych spoza niego zwracane są różne obiekty. Dlatego też, jeśli gdzieś w aplikacji zostaną porównane powłoki zamiast prymitywów, istnieje ryzyko otrzymania najstraszniejszego błędu: pływającego. Ponieważ kod najprawdopodobniej będzie również testowany na ograniczonym zakresie wartości, w którym ten błąd nie wystąpi. Ale w prawdziwej pracy albo się pojawi, albo zniknie, w zależności od wyników niektórych obliczeń. Łatwiej zwariować niż znaleźć taki błąd. Dlatego radziłbym, jeśli to możliwe, unikać automatycznego boksowania. I to nie wszystko. Pamiętajmy o matematyce, nie dalej niż od 5 klasy. Niech nierówności A>=Bi А<=B. Co można powiedzieć o związku Ai B? Jest tylko jedno – są równi. Czy sie zgadzasz? Myślę, że tak. Przeprowadźmy 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));
Wynik:
i1>=i2: true
i1<=i2: true
i1==i2: false
I to jest dla mnie największa dziwność. W ogóle nie rozumiem, po co wprowadzono do języka tę cechę, skoro wprowadza ona takie sprzeczności. Generalnie powtórzę jeszcze raz – jeśli można się bez tego obejść autoboxing/unboxing, to warto maksymalnie wykorzystać tę szansę. Ostatnim tematem jaki chciałbym poruszyć jest... Java 5.0. porównanie elementów wyliczeniowych (typ wyliczeniowy) Jak wiadomo, od wersji 5.0 Java wprowadziła taki typ jak wyliczenie – wyliczenie. Jego instancje domyślnie zawierają nazwę i numer sekwencyjny w deklaracji instancji w klasie. Odpowiednio, gdy zmienia się kolejność ogłoszeń, liczby ulegają zmianie. Jednakże, jak powiedziałem w artykule „Serializacja taka, jaka jest” , nie powoduje to problemów. Wszystkie elementy wyliczeniowe istnieją w jednym egzemplarzu, jest to kontrolowane na poziomie maszyny wirtualnej. Dlatego można je porównywać bezpośrednio, korzystając z linków. * * * Być może to wszystko na dziś, jeśli chodzi o praktyczną stronę implementacji porównywania obiektów. Być może coś przeoczyłem. Jak zawsze czekam na Wasze komentarze! Na razie pozwól mi odejść. Dziękuję wszystkim za uwagę! Link do źródła: Porównywanie obiektów: praktyka
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION