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:
char(typ prymitywny): Przechowuje dokładnie jeden znak. Zajmuje w pamięci 16 bitów (2 bajty). W kodzie zawsze zamykamy go w pojedynczych apostrofach:'X','5','\n'.String(typ obiektowy / referencyjny): To pełnoprawny obiekt reprezentujący ciąg znaków. W kodzie definiujemy go w podwójnych cudzysłowach:"Witaj świecie!". Jest niemutowalny (immutable) - raz stworzony tekst w Javie nie może zostać bezpośrednio zmodyfikowany (każda operacja typu.toUpperCase()tworzy w pamięci zupełnie nowy obiekt).
🧠 Warto zapamiętać: Zmienna typu
chartrzyma swoją wartość bezpośrednio na stosie (Stack). Z kolei zmienna typuStringtrzyma 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).
- Wykorzystywało 7 bitów, co dawało 27=128 unikalnych znaków (o numerach od 0 do 127).
- Wystarczało to z zapasem na angielski alfabet, cyfry, znaki interpunkcyjne oraz znaki sterujące (np. kod 10 to nowa linia
\n).
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:
- Microsoft stworzył stronę kodową Windows-1250.
- Standard międzynarodowy promował ISO-8859-2.
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:
- Unicode: To ogólnoświatowy katalog (baza danych). Przypisuje każdemu znakowi na Ziemi unikalny identyfikator (Code Point). Znak
'A'ma numerU+0041, a polskie'Ł'ma numerU+0141. Unicode obejmuje ponad 140 tysięcy znaków (w tym alfabety azjatyckie, starożytne hieroglify oraz emoji). - 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ę:
- Zaleta: Elementy leżą w pamięci RAM w jednym, spójnym, ciągłym bloku. Dostęp do dowolnego indeksu
oceny[42]jest natychmiastowy - ma złożoność czasowąO(1). - Ograniczenie 1 (Homogeniczność): Tablica jest jednorodna - przechowuje wyłącznie elementy tego samego typu (np. same liczby
int[]). - Ograniczenie 2 (Sztywny rozmiar): Rozmiar tablicy podajesz raz przy jej tworzeniu. Nie da się jej „powiększyć” w locie bez alokacji nowej tablicy i przepisania danych!
💡 A czym pod maską jest
String? Tak naprawdę jest to obiekt owijający wewnętrzną tablicębyte[]lubchar[].
Krok 2: Klasy i Rekordy (class / record) - Struktury Niejednorodne
Wracamy do naszego sklepu z grami. Gra to nie tylko tytuł. Pojedynczy produkt to:
- Tytuł:
String - Cena:
double - Rok wydania:
int
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:
- Listy (
ArrayList,LinkedList): Uporządkowane sekwencje o elastycznym rozmiarze. Dodajesz elementy za pomocą metody.add(), a lista sama dba o alokację pamięci. - Zbiory (
HashSet): Dbają o to, by elementy się nie powtarzały (kolekcja unikalnych wartości). - Mapy / Słowniki (
HashMap): Przechowują pary Klucz → Wartość (np.PESEL -> Obiekt UczeńlubID_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ść.