Dlaczego należy uważać z klasami kopertowymi?

Już kilka miesięcy temu, podjąłem decyzję, że swój pierwszy wpis na blogu poświęcę głupiemu błędowi, który popełniłem w pracy – kosztował mnie dwa dni debugowania. Jest to bardzo dobry moment, żeby opowiedzieć o klasach kopertowych w Javie w ogólności i przestrzec innych przed podobnymi błędami.

Zacznijmy od początku. Wszyscy wiemy, że Java jest językiem obiektowym. Oczywiście wspiera też inne paradygmaty programowania, ale to jest temat na inną okazję. Java nie podchodzi do tematu obiektowości tak radykalnie jak Smalltalk, ponieważ w Javie mamy do dyspozycji typy prymitywne. Typy prymitywne też są tematem na zupełnie inny artykuł. Typy prymitywne służą do przechowywania wartości, które są prymitywne – czyli proste. Są to głównie typy liczbowe (stało- i zmiennoprzecinkowe). Dodatkowo mamy typ znakowy i logiczny. W związku z tym, że ustalone są rozmiary poszczególnych typów, pamięć na poszczególne wartości można zaalokować w czasie kompilacji – na stosie. W przeciwieństwie do typów obiektowych, których rozmiarów nie znamy w czasie kompilacji i dla których alokujemy miejsce na stercie wołając operator new. Stos i sterta to są opowieści na inną okazję.

Nawet jeżeli nie masz zbyt wiele doświadczenia w Javie to powinieneś być świadom, że pojedyncze wartości typów prymitywnych są niewystarczające do budowania rozbudowanych aplikacji. Często wartości prymitywne agreguje się w tablicach. Jeżeli zmienną potraktujemy jako szufladę na coś, to tablicę możemy zobrazować jako komodę, w której wszystkie szuflady mają taki sam rozmiar. Okazuje się, że tablice są bardzo wydajne, ale są też bardzo niewygodne w używaniu. Dlatego stworzono kolekcje, czyli pewnego rodzaju uogólnienie na temat tablic. Jest wiele rodzajów kolekcji, a te zasługują na zupełnie osobny cykl artykułów. W tym momencie możemy założyć, że to są obiekty, w których możemy przechowywać inne obiekty. No właśnie, nie możemy w kolekcjach przechowywać wartości typów prymitywnych. Ale na to mamy rozwiązanie. W pakiecie java.lang są zdefiniowane klasy kopertowe dla typów prymitywnych. Czyli klasy, których zadaniem jest opakować wartość typu prymitywnego. Dzięki autoboxingu i outboxingu możemy konwertować typy prymitywne na obiekty i vice-versa.

int pI1 = 123;
Integer wI1 = pI1;
int pI2 = wI1;
Integer wI2 = pI2;

Teraz zagadka: Co dla powyższego kodu wypisze instrukcja:

System.out.println(pI1 == pI2);

Odpowiedź brzmi true. Ale tutaj chyba nikt nie ma żadnych wątpliwości. Mamy dwie zmienne prymitywne, które mają taką samą wartość. Więc wartość  true jest jak najbardziej uzasadniona. Ale co z instrukcją:

System.out.println(wI1 == wI2);

Odpowiedź również brzmi true. Ale czy to ma sens? Operator == sprawdza, czy obie referencje wskazują na ten sam obiekt na stercie. To by oznaczało, że Java w instrukcji:

Integer wI1 = pI1;

stworzyła jeden obiekt a potem w linijce:

Integer wI2 = pI2;

Nie stworzyła nowego obiektu, tylko zauważyła, że obiekt dla wartości 123 już jest stworzony na stercie i referencja wI2 wskazuje na ten obiekt. Rozumowanie brzmi sensownie, więc teraz kolejna zagadka. Co wypisze poniższy program?

int pI1 = 321;
Integer wI1 = pI1;
int pI2 = wI1;
Integer wI2 = pI2;
System.out.println(pI1 == pI2);
System.out.println(wI1 == wI2);

Uważny Czytelniku, ten program różni się tylko wartością początkową. I to wystarczy, aby program na wyjście wypisał dwie linijki:

true
false

Zgodnie z angielskojęzyczną Wikipedią typ int może przyjmować wartości z przedziału od -2,147,483,648 do +2,147,483,647. Czyli raczej nie jest specjalnie rozsądnym sprawdzać dla każdej konwersji, czy dla danej liczby obiekt już był stworzony czy nie. Weź pod uwagę, że są jeszcze inne typy prymitywne i opakowujące je klasy kopertowe. Okazuje się, że specjalne traktowanie, które pokazał przykład pierwszy działa tylko dla małych liczb. Można to sprawdzić tym prostym programem:

for (int i = -200; i <= 200; i++) {
   int pI1 = i;
   Integer wI1 = pI1;
   int pI2 = wI1;
   Integer wI2 = pI2;
   System.out.println(i + " => " + (wI1 == wI2));
}

Podsumowując: należy uważać z operatorem == w stosunku do obiektów. Podobnie ma się sprawa z klasą String. Ale może o tym kiedy indziej. Ten błąd jest bardzo nieoczywisty i trudny do debugowania.

Ale cały wpis chciałem poświęcić zupełnie innemu problemowi. Mam plik flag.properties (o plikach properties i o tym, jak je czytać powstanie osobny wpis). Plik ten nieprzypadkowo znajduje się w katalog o nazwie resources. Plik flag.properties ma w sobie zawartość:

flag=true

Pytanie dlaczego poniższy program wypisuje na wyjście napis:

Flag is set to false
Properties p = new Properties();
p.load(new FileReader("src/main/resources/flag.properties"));
boolean flag = Boolean.getBoolean(p.getProperty("flag"));
if (flag) {
   System.out.println("Flag is set to true");
} else {
   System.out.println("Flag is set to false");
}

Pobieżna analiza tego kodu mówi:

  1. Przeczytaj plik properties
  2. Znajdź w nim wartość dla klucza flag
  3. Przetłumacz napis na wartość logiczną
  4. W zależności od wartości logicznej wypisz odpowiedni komunikat

Z tym, że nie do końca. Cały problem polega w tym, co zwraca metoda statyczne

Boolean.getBoolean()

Returns true if and only if the system property named by the argument exists and is equal to, ignoring case, the string „true”.

Czyli tak naprawdę w punkcie trzecim, program szuka zmiennej systemowej o nazwie true. W związku z tym, że zmiennej o takiej nazwie nie ma, metoda zwraca false. Stąd nieoczekiwane zachowanie programu.

Przyznaję się, że taki babol zdarzył mi się w pracy, po wielu latach pisania różnych programów w Javie. Znalezienie tego błędu zajęło mi dwa dni.

Do wykonanie prawidłowego kroku 3 należy użyć metody parseBoolean:

Properties p = new Properties();
p.load(new FileReader("src/main/resources/flag.properties"));
boolean flag = Boolean.parseBoolean(p.getProperty("flag"));
if (flag) {
   System.out.println("Flag is set to true");
} else {
   System.out.println("Flag is set to false");
}

Kod prezentowany w artykule możesz znaleźć na githubie.

Aspirujący twórca internetowy, który zna się na programowaniu i chce się dzielić wiedzą

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Scroll to top