#2 Od zera do kolekcji: Jak komputer czyta litery i dlaczego jedna zmienna to za mało?

Zastanawiałeś się kiedyś, co tak naprawdę dzieje się w trzewiach Twojego komputera, gdy wciskasz na klawiaturze literę A albo wysyłasz znajomemu emotkę 🔥? Dla procesora świat jest czarno-biały - istnieje tylko prąd albo jego brak, czyli bity 0 i 1. Komputer nie ma „oczu”, by zobaczyć kształt litery.

W tym wpisie przejdziemy drogę od pojedynczego bajtu tekstu aż po zaawansowane struktury, które napędzają bazy danych i silniki gier

1. Przypomnienie z bazy: char vs String

Na poprzedniej lekcji rozróżniliśmy dwa podstawowe byty w Javie:

🧠 Warto zapamiętać: Zmienna typu char trzyma swoją wartość bezpośrednio na stosie (Stack). Z kolei zmienna typu String trzyma na stosie jedynie adres (referencję), który wskazuje na właściwy obiekt umieszczony na stercie (Heap)!

2. Jak komputer rozumie tekst? Od ASCII do UTF-8

Skoro komputer zna wyłącznie liczby, potrzebujemy „książki szyfrów”, która powie: „Gdy widzisz liczbę 65, narysuj na ekranie piksele składające się na literę A”. Taka książka to tabela kodowa.

A. Klasyk: Tabela ASCII (Lata 60.)

Pierwszym globalnym standardem było ASCII (American Standard Code for Information Interchange).

Spójrz na tę regularność - programiści z lat 60. byli geniuszami ergonomii:

Zakres znaków Kody dziesiętne (DEC) Przykład i zależność
Cyfry '0' - '9' 48 - 57 '0' to 48. Jeśli chcesz zamienić znak cyfry na prawdziwą liczbę, odejmij znak zera: int cyfra = '7' - '0'; (wynik: 7).
Wielkie litery 'A' - 'Z' 65 - 90 'A' to kod 65, 'B' to 66 itd.
Małe litery 'a' - 'z' 97 - 122 'a' to kod 97, 'b' to 98 itd.

Ciekawostka:
Odejmij kod 'A' (65) od 'a' (97). Różnica wynosi dokładnie 32!
W systemie binarnym 32=25 (0010 0000). Oznacza to, że zmiana wielkości litery z małej na dużą polega na przestawieniu zaledwie jednego, szóstego bitu.

char litera = 'A'; // kod 65
char malaLitera = (char)(litera + 32); // kod 97, czyli 'a'
System.out.println(malaLitera); // Wyświetli: a

B. Piekło stron kodowych (Dlaczego widziałeś „krzaczki”?)

Gdy komputery trafiły do Polski, pojawił się problem: gdzie podziać „ą”, „ę”, „ł”, „ź”? W 128 znakach ASCII ich nie było.

Rozszerzono pamięć do pełnego 1 bajta (8 bitów = 256 znaków). Górną połowę (128-255) każdy kraj zagospodarował po swojemu:

Efekt? Jeśli wysłałeś koledze plik zapisany w Windows-1250, a jego edytor czytał ISO-8859-2, na ekranie zamiast słowa „Zażółć gęślą jaźń” pojawiały się legendarne krzaczki (mojibake), np. Za¿ó³æ gêœl¹ jaŸñ.

C. Zbawienie: Unicode i format UTF-8

Świat potrzebował jednego standardu:

  1. Unicode: To ogólnoświatowy katalog (baza danych). Przypisuje każdemu znakowi na Ziemi unikalny identyfikator (Code Point). Znak 'A' ma numer U+0041, a polskie 'Ł' ma numer U+0141. Unicode obejmuje ponad 140 tysięcy znaków (w tym alfabety azjatyckie, starożytne hieroglify oraz emoji).
  2. UTF-8: To sposób zapisu numerów Unicode w pamięci/pliku:
    • Jest kodowaniem o zmiennej długości (od 1 do 4 bajtów na znak).
    • Geniusz kompatybilności: pierwsze 128 znaków (angielski alfabet i cyfry) zajmuje dokładnie 1 bajt i ma kody identyczne ze starym ASCII z 1963 roku! Plik tekstowy po angielsku w UTF-8 waży tyle samo, co w starym ASCII.
    • Polskie litery zajmują 2 bajty, a emotki (np. 🚀) - 4 bajty.

3. Most ku strukturom złożonym: Dlaczego typy proste to za mało?

Do tej pory operowaliśmy na typach prostych (prymitywach): int, double, boolean, char. Przechowują one pojedynczą wartość w jednej zmiennej:

int wiek = 18;
char ocena = 'A';

Wyobraź sobie teraz, że piszesz system dla magazynu w sklepie z grami. W magazynie masz 5 000 gier. Czy stworzysz 5 000 zmiennych: String tytul1, tytul2...? Oczywiście, że nie! Tutaj do gry wkracza ewolucja struktur danych.

Krok 1: Tablica (Array) - Struktura Jednorodna

Chcemy przechować 100 ocen z egzaminu. Zamiast stu zmiennych, tworzymy tablicę:

💡 A czym pod maską jest String? Tak naprawdę jest to obiekt owijający wewnętrzną tablicę byte[] lub char[].

Krok 2: Klasy i Rekordy (class / record) - Struktury Niejednorodne

Wracamy do naszego sklepu z grami. Gra to nie tylko tytuł. Pojedynczy produkt to:

Tablica nie pozwoli nam zmieszać tych typów. Musimy zdefiniować własny typ złożony (heterogeniczny):

// Nowoczesna Java: Niezmienny nośnik danych (Rekord)
public record Gra(String tytul, double cena, int rokWydania) {}

// Lub klasyczna Klasa (dane + zachowanie)
Gra wiedzmin = new Gra("Wiedźmin 3", 99.99, 2015);

Łączymy powiązane logicznie dane różnych typów pod jedną banderą.

Krok 3: Kolekcje i Wskaźniki/Referencje - Struktury Dynamiczne

Co w sytuacji, gdy asortyment sklepu stale się zmienia? Gry są kupowane, dodawane, wyprzedawane. Sztywna tablica Gra[] staje się uciążliwa.

Rozwiązaniem są kolekcje dynamiczne (w Javie zrzeszone w Java Collections Framework). Zamiast rezerwować sztywny blok pamięci, obiekty tworzone są na stercie, a łączą je referencje:

  1. Listy (ArrayList, LinkedList): Uporządkowane sekwencje o elastycznym rozmiarze. Dodajesz elementy za pomocą metody .add(), a lista sama dba o alokację pamięci.
  2. Zbiory (HashSet): Dbają o to, by elementy się nie powtarzały (kolekcja unikalnych wartości).
  3. Mapy / Słowniki (HashMap): Przechowują pary Klucz → Wartość (np. PESEL -> Obiekt Uczeń lub ID_Gry -> Obiekt Gra).

4. Ściąga do zeszytu: Architektura danych w pigułce

Poniższe drzewo podsumowuje podział, który musi znać każdy technik programista:

KLASYFIKACJA STRUKTUR DANYCH
│
├── 1. TYPY PROSTE (Prymitywy / Skalary)
│    └── Bezpośrednia wartość w pamięci (int, double, boolean, char).
│
└── 2. TYPY ZŁOŻONE (Strukturalne / Referencyjne)
     │
     ├── A. JEDNORODNE (Ten sam typ, ciągły blok pamięci)
     │    └── Tablice statyczne (np. int[], String[]) -> stały rozmiar, szybki indeks O(1).
     │
     ├── B. NIEJEDNORODNE (Różne typy zgrupowane w jedną całość logiczną)
     │    ├── Rekordy (record) -> niezmienne kontenery danych (DTO).
     │    └── Klasy (class) -> stan (pola) + logika (metody).
     │
     └── C. DYNAMICZNE KOLEKCJE (Zmienny rozmiar, oparte na referencjach)
          ├── Listy (ArrayList) -> zachowują kolejność wprowadzania.
          ├── Zbiory (HashSet) -> gwarantują brak duplikatów.
          └── Mapy (HashMap) -> błyskawiczny dostęp przez unikalny Klucz -> Wartość.