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:
byte(1 bajt / 8 bitów): zakres od -128 do 127short(2 bajty / 16 bitów): zakres od -32 768 do 32 767int(4 bajty / 32 bity): zakres od ok. -2,14 mld do +2,14 mld (domyślny typ całkowity)long(8 bajtów / 64 bity): zakres astronomiczny (wymaga literałuL, np.5000L)
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 ⚡
- Prawa połowa koła to liczby dodatnie. Ich pierwszy bit (MSB) wynosi
0. - Lewa połowa koła to liczby ujemne. Ich pierwszy bit (MSB) wynosi
1(jest flagą minusa). - Gdy dodajesz jedynkę do maksymalnej liczby dodatniej (
0111...111), procesor po prostu wykonuje dodawanie binarne. Wynikiem jest1000...000. Ponieważ na pierwszej pozycji pojawiła się jedynka, komputer odczytuje tę liczbę jako minimalną liczbę ujemną.
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 typlong!
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
floatanidouble! 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?
- Architektura pamięci RAM i szyny danych procesora nie potrafi zaadresować pojedynczego bitu - najmniejszą porcją danych w pamięci jest 1 bajt (8 bitów).
- Co więcej, nowoczesne procesory najszybciej wykonują operacje na słowach 32 lub 64-bitowych. Dlatego pojedyncza zmienna
booleanna stosie w JVM jest najczęściej rozszerzana i traktowana jak... 4-bajtowyint! - Dopiero gdy utworzymy tablicę
boolean[], maszyna wirtualna upakowuje elementy, przydzielając 1 bajt na element (podobnie jak w tablicybyte[]).
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?
- 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! - Bezpieczeństwo: Niemutowalne obiekty gwarantują, że parametry sieciowe, hasła czy nazwy baz danych nie zostaną podmienione w trakcie działania aplikacji.
- Wielowątkowość: Dwa wątki mogą bez obaw czytać ten sam
Stringbez 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)
- Operator
==porównuje tożsamość pamięciową (czy dwie zmienne wskazują na ten sam adres w RAM). - Metoda
.equals()porównuje treść obiektów (czy napisy składają się z tych samych liter). Napisy ZAWSZE porównujemy przez.equals()!
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
intto domyślny typ całkowity, ale jeśli twoje dane mogą przekroczyć 2,14 miliarda - od razu wybierzlong.- Przepełnienie (overflow) w kodzie U2 powoduje cichy przeskok z wartości maksymalnej do minimalnej ujemnej.
- Do pieniędzy i walut zawsze stosuj
BigDecimal, nigdydouble/float. booleanw tablicy ma 11 bajt, ale jako pojedyncza zmienna na stosie procesora rozrasta się do 44 bajtów.charzapisujemy w apostrofach' 'i można na nim liczyć jak na liczbie.Stringjest niemutowalny - żadna metoda nie zmienia istniejącego napisu, każda zwraca nowy obiekt.- Zawsze porównuj teksty za pomocą
.equals(), a do intensywnego sklejania napisów w pętlach używajStringBuilder.