#1 Dlaczego YouTube zepsuł licznik, banki boją się double, a String nie lubi zmian?

Kiedy zaczynamy przygodę z programowaniem, kod wydaje się po prostu tekstem: piszemy zmienną, przypisujemy jej wartość, coś się wyświetla na ekranie. Z czasem jednak zauważamy, że komputer nie myśli tak jak człowiek. Dla procesora i pamięci RAM nasz kod to twarda fizyka: elektrony, rejestry maszynowe i adresy w krzemie.

1. Zegary binarne i katastrofa Gangnam Style (Typy całkowite w kodzie U2)

W Javie mamy 4 podstawowe typy całkowitoliczbowe:

Co się stanie, gdy przekroczysz limit?

Spójrz na ten niewinnie wyglądający kod:

int licznik = Integer.MAX_VALUE; // 2 147 483 647
licznik = licznik + 1;
System.out.println(licznik);

Intuicja podpowiada: program zgłosi błąd albo rzuci wyjątek. Otóż nie. Program radośnie wypisze: -2147483648. W ułamku sekundy dodatnia liczba stała się skrajnie ujemna.

Dlaczego tak się dzieje? Koło liczb w kodzie U2

Liczby całkowite są w pamięci komputera zorganizowane w tzw. kodzie uzupełnień do dwóch (U2). Możesz to sobie wyobrazić jako tarczę zegara:

                     0000 (0)
         1111 (-1)      ▲      0001 (+1)
                        │
    1000 (-MIN) ◄───────┼───────► 0111 (+MAX)
      (bit znaku: 1)    │         (bit znaku: 0)
                        ▼
                ⚡ PRZEPEŁNIENIE ⚡

Prawdziwa historia: To nie jest teoria! Dokładnie to spotkało inżynierów YouTube w 2014 roku, kiedy teledysk Gangnam Style wyświetlono ponad 2,14 miliarda razy. Licznik wyświetleń był zapisany jako 32-bitowy int, co doprowadziło do przepełnienia (overflow). Google musiało w pośpiechu przepisać bazę na 64-bitowy typ long!

2. Gdzie podziały się grosze? Dlaczego double to wróg bankiera

Kolejny eksperyment, który szokuje każdego początkującego programistę:

double a = 0.1;
double b = 0.2;
System.out.println(a + b);
System.out.println(a + b == 0.3);

Konsola:

0.30000000000000004
false

Dlaczego 0.1 + 0.2 != 0.3?

Typy zmiennoprzecinkowe (float - 4 bajty, double - 8 bajtów) działają w oparciu o standard binarny IEEE 754. Komputer operuje na potęgach dwójki (1/2, 1/4, 1/8...). W tym systemie ułamek dziesiętny 0.1 (1/10) jest ułamkiem nieskończonym okresowym - dokładnie tak samo jak 1/3=0.3333... w naszym systemie dziesiętnym. Komputer musi w którymś momencie uciąć liczbę, tracąc mikro-precyzję.

Złota zasada: W systemach finansowych, sklepach internetowych i bankowości NIGDY nie używamy typów float ani double! Do obliczeń pieniężnych stosujemy obiektową klasę BigDecimal (zapisując liczby w postaci łańcuchów tekstu, np. new BigDecimal("0.1")) lub operujemy na liczbach całkowitych, przeliczając kwoty na grosze/centy.

3. Prawda czy fałsz: Jak Wirtualna Maszyna Javy widzi boolean?

Teoretycznie typ logiczny boolean potrzebuje zaledwie 1 bitu informacji: 0 (fałsz) lub 1 (prawda).

Jak jest w praktyce?

4. Znak a Obiekt: char to w przebraniu liczba, a String to nienaruszalna twierdza

Częstym błędem na egzaminie zawodowym jest mylenie pojedynczego cudzysłowu ' ' z podwójnym " ". To dwa zupełnie różne światy:

char (Prymityw - 2 bajty)

Reprezentuje pojedynczy znak zakodowany w standardzie Unicode (UTF-16). W rzeczywistości jest to 16-bitowa liczba całkowita bez znaku (od 0 do 65 535). Dlatego możesz zrobić tak:

char litera = 'A';
litera++; 
System.out.println(litera); // Wypisze 'B'!

System.out.println('A' + 'B'); // Wypisze 131 (czyli 65 + 66), a NIE "AB"!

String (Pełnoprawna klasa obiektowa)

String to nie prymityw. Zmienna przechowuje jedynie referencję (adres w pamięci), która wskazuje na obiekt leżący na stercie (Heap).

Najważniejsze pojęcie: Niemutowalność (Immutability)

Obiekt klasy String jest niemutowalny - oznacza to, że raz utworzony w pamięci, nigdy nie może zmienić swojej zawartości.

Zobaczmy klasyczny błąd:

String login = "adam";
login.toUpperCase();

System.out.println(login); // Wypisze "adam", a NIE "ADAM"!

Metoda .toUpperCase() nie zmieniła tekstu w obiekcie "adam". Zamiast tego po cichu utworzyła w pamięci zupełnie nowy obiekt "ADAM", a ponieważ nigdzie go nie przypisaliśmy, natychmiast zniknął w próżni. Aby zmiana odniosła skutek, musimy nadpisać referencję: login = login.toUpperCase();.

Dlaczego twórcy Javy tak to zaprojektowali?

  1. String Pool (Pula łańcuchów): Jeśli w kodzie 50 razy wpiszesz "Polska", JVM utworzy tylko jeden obiekt w pamięci, a wszystkie zmienne będą do niego wspólnie linkować. Gdyby ktoś mógł zmienić literę w jednym miejscu, zepsułby tekst we wszystkich 50 miejscach programu!
  2. Bezpieczeństwo: Niemutowalne obiekty gwarantują, że parametry sieciowe, hasła czy nazwy baz danych nie zostaną podmienione w trakcie działania aplikacji.
  3. Wielowątkowość: Dwa wątki mogą bez obaw czytać ten sam String bez ryzyka kolizji.

5. Dwie pułapki na egzaminie INF.04 i rozmowach kwalifikacyjnych

Pułapka 1: Porównywanie przez == zamiast .equals()

String s1 = "kot";
String s2 = "kot";
String s3 = new String("kot");

System.out.println(s1 == s2);      // true (ten sam adres ze String Pool!)
System.out.println(s1 == s3);      // false (różne adresy w pamięci!)
System.out.println(s1.equals(s3)); // true (ta sama zawartość znakowa)

Pułapka 2: Łączenie tekstu w pętli

Niemutowalność ma swoją cenę. Taki kod to zbrodnia na pamięci komputera:

String wynik = "";
for (int i = 0; i < 50_000; i++) {
    wynik += "a"; // KATASTROFA! Tworzy 50 000 nowych obiektów w pamięci!
}

Pętla ta potrafi zająć kilka sekund i zarżnąć Garbage Collector.

Rozwiązanie? Użyj mutowalnego bufora:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 50_000; i++) {
    sb.append("a"); // Modyfikuje JEDEN obiekt w miejscu - czas: ok. 2 milisekundy!
}
String wynik = sb.toString();

Podsumowanie w pigułce

  1. int to domyślny typ całkowity, ale jeśli twoje dane mogą przekroczyć 2,14 miliarda - od razu wybierz long.
  2. Przepełnienie (overflow) w kodzie U2 powoduje cichy przeskok z wartości maksymalnej do minimalnej ujemnej.
  3. Do pieniędzy i walut zawsze stosuj BigDecimal, nigdy double/float.
  4. boolean w tablicy ma 11 bajt, ale jako pojedyncza zmienna na stosie procesora rozrasta się do 44 bajtów.
  5. char zapisujemy w apostrofach ' ' i można na nim liczyć jak na liczbie.
  6. String jest niemutowalny - żadna metoda nie zmienia istniejącego napisu, każda zwraca nowy obiekt.
  7. Zawsze porównuj teksty za pomocą .equals(), a do intensywnego sklejania napisów w pętlach używaj StringBuilder.