1. Pojęcie kodowania znaków (encoding)
Zacznijmy od najważniejszego pytania: czym jest kodowanie znaków?
Wyobraźmy sobie, że jedziesz na międzynarodową konferencję. Każdy mówi w swoim języku, ale wszyscy chcą się zrozumieć. Potrzebny jest tłumacz, który wie, że słowo "hello" po rosyjsku to "privet", a po hiszpańsku "hola". W świecie komputerów kodowanie znaków jest właśnie takim tłumaczem.
Kodowanie znaków — to sposób reprezentacji znaków jako bajtów
Komputer to proste urządzenie: rozumie tylko zera i jedynki, czyli bity i bajty. Ludzie chcą jednak widzieć litery, cyfry, emoji i nawet — o, zgrozo! — chińskie ideogramy. Aby komputer mógł „zapisać” znak, trzeba uzgodnić, jakiemu ciągowi bajtów będzie odpowiadał każdy znak.
Kodowanie znaków (encoding) — to zestaw reguł, według których znaki (litery, cyfry, znaki interpunkcyjne, emoji itd.) zamieniają się na bajty do przechowywania i transmisji, i odwrotnie: bajty zamieniają się na znaki do wyświetlania.
Przykład: litera 'A' cyrylicka (U+0410) w różnych kodowaniach
- W kodowaniu UTF-8 litera 'A' cyrylicka (U+0410) jest kodowana dwoma bajtami: 0xD0 0x90.
- W kodowaniu Windows-1251 ta sama litera — jednym bajtem: 0xC0.
- Natomiast łacińska 'A' w niemal wszystkich popularnych kodowaniach to 0x41.
Jeśli odczytasz plik w niewłaściwym kodowaniu, znaki zamienią się w „krzaczki” (niezrozumiałe symbole lub znaki zapytania).
2. Po co są potrzebne kodowania znaków
Dlaczego nie można po prostu przechowywać liter tak, jak są?
Bo komputer rozumie tylko liczby (zera i jedynki). To, jaka liczba odpowiada której literze — to właśnie istota kodowania.
Przykład: „Hello” na dysku
Gdy zapisujesz do pliku słowo "Hello", dla komputera to tylko sekwencja bajtów. To, jak te bajty interpretować, zależy od kodowania.
- Jeśli plik zapisano w UTF-8, każda cyrylicka litera zajmuje dwa bajty i tak dalej.
- Jeśli w Windows-1251, każda litera — jeden bajt, ale wartości bajtów są inne.
Gdzie potrzebne jest kodowanie znaków?
- Przy zapisie tekstu do pliku: aby bajty można było później poprawnie odczytać.
- Przy odczycie tekstu z pliku: aby bajty zostały z powrotem zamienione na litery.
- Przy wysyłaniu tekstu przez sieć (np. HTTP, e‑mail).
- Przy pracy z bazami danych: tam także trzeba wiedzieć, w jakim kodowaniu jest przechowywany tekst.
Jeśli nie określisz kodowania...
To jak otworzyć tekst w nieznanym języku i próbować go czytać. Szanse rosną, jeśli wiesz, jaki to język. Jeśli nie — w najlepszym razie nic nie zrozumiesz, w najgorszym dostaniesz bełkot.
3. Problemy bez właściwego kodowania
„Krzaczki” i utrata danych
Najczęstsza skarga początkujących (i nie tylko) programistów: „Dlaczego zamiast "Hello" widzę "Привет" albo nawet same znaki zapytania?”
Dzieje się tak, gdy plik został zapisany w jednym kodowaniu, a odczytywany jest w innym. Na przykład plik powstał na starym Windowsie w Windows-1251, a otwierasz go w Linuksie, gdzie domyślnie jest UTF-8. Albo odwrotnie.
Przykład
- Plik zapisano w Windows-1251: bajt dla cyrylickiej litery (np. U+041F) — 0xCF.
- Otworzono w UTF-8: program oczekuje, że cyrylica to dwa bajty, a dostaje jeden. Wszystko się sypie.
Utrata danych
Jeśli przy zapisie znak nie jest obsługiwany przez wybrane kodowanie (na przykład próbujesz zapisać emoji w ASCII), to zniknie lub zostanie zastąpiony znakiem zapytania. Wszystko, co nie „mieści się” w kodowaniu, przepada.
Problemy przy wymianie plików
Pliki zapisane w jednym kodowaniu mogą wyświetlać się niepoprawnie na innych komputerach, jeśli tam domyślne kodowanie jest inne. Często dzieje się tak przy wymianie plików między Windows i Linux lub przy otwieraniu starych plików.
4. Kodowanie znaków w Javie: reprezentacja wewnętrzna i zewnętrzna
Wewnątrz JVM: zawsze Unicode (UTF-16)
W Javie łańcuchy (String) wewnątrz programu są zawsze przechowywane w Unicode (ściślej, w UTF-16). To znaczy, że możesz spokojnie przypisywać zmiennym teksty w dowolnym języku świata, a Java to wszystko „strawi”.
String hello = "Witaj, świecie! 😀";
W pamięci JVM ten tekst jest przechowywany jako zestaw 16‑bitowych liczb (char), gdzie każdemu znakowi odpowiada jego kod w tablicy Unicode.
Ciekawostka
W Javie symbol typu char ma 16 bitów (2 bajty). Jednak niektóre znaki (np. rzadkie ideogramy lub emoji) wymagają już dwóch char — to „pary surogatowe”.
Przy wejściu/wyjściu: kodowanie ma znaczenie!
Kiedy odczytujesz lub zapisujesz łańcuchy do świata zewnętrznego (pliki, sieć), Java musi przekształcić wewnętrzną reprezentację (UTF-16) w sekwencję bajtów. Właśnie wtedy potrzebne jest kodowanie.
- Jeśli nie wskażesz kodowania wprost, Java użyje domyślnego systemowego (w rosyjskim Windows może to być Windows-1251, w Linuksie — UTF-8).
- To ryzykowne: na innym komputerze rezultat może być inny.
Przykład: odczyt i zapis pliku bez określenia kodowania
// Zła praktyka! Kodowanie znaków nie zostało podane.
FileReader reader = new FileReader("data.txt");
FileWriter writer = new FileWriter("data.txt");
W tym przypadku Java użyje kodowania systemowego. Jeśli plik został zapisany w innym systemie — zobaczysz „krzaczki”.
Dobra praktyka: zawsze podawaj kodowanie
// Dobrze! Kodowanie zostało jawnie wskazane.
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("data.txt"), StandardCharsets.UTF_8));
5. Krótka ilustracja: co dzieje się podczas pracy z kodowaniami
Schemat: droga łańcucha znaków od pliku do programu i z powrotem
[Plik na dysku (bajty, kodowanie X)]
|
V
[Java czyta bajty i za pomocą kodowania X zamienia je na String (UTF-16)]
|
V
[Pracujesz z łańcuchem znaków w programie]
|
V
[Java zapisuje String do bajtów, używając kodowania Y]
|
V
[Plik na dysku (bajty, kodowanie Y)]
Jeśli X i Y są takie same — wszystko działa. Jeśli różne — możliwe problemy.
6. Krótka historia kodowań (dla ciekawych)
ASCII
ASCII — jedno z najstarszych kodowań: jeden bajt na znak, tylko alfabet angielski, cyfry i podstawowe znaki. Inne alfabety — odpadają.
Windows-1251, ISO-8859-1 i inne „staruszki”
To jednobajtowe kodowania dla różnych zestawów znaków: cyrylica, łacina, greka itd. Każdy wybierał swoje i zaczęło się zamieszanie.
Unicode i rodzina UTF
- Unicode — globalna tablica dla znaków całego świata.
- UTF-8, UTF-16, UTF-32 — różne sposoby reprezentacji znaków Unicode w bajtach.
- UTF-8 stał się standardem dla sieci Web, plików i wymiany między systemami.
7. Praktyka: jak kodowanie wpływa na pracę z plikami
Zobaczmy krótki przykład zapisu i odczytu łańcuchów w różnych kodowaniach.
Przykład: zapis i odczyt w różnych kodowaniach
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class EncodingDemo {
public static void main(String[] args) throws IOException {
String text = "Witaj, świecie! 😀";
// Zapiszmy plik w UTF-8
try (Writer writer = new OutputStreamWriter(
new FileOutputStream("utf8.txt"), StandardCharsets.UTF_8)) {
writer.write(text);
}
// Spróbujmy teraz odczytać go w nieprawidłowym kodowaniu
try (Reader reader = new InputStreamReader(
new FileInputStream("utf8.txt"), Charset.forName("Windows-1251"))) {
int c;
while ((c = reader.read()) != -1) {
System.out.print((char) c);
}
}
// Na ekranie będą krzaczki!
}
}
Wniosek: Jeśli kodowania się nie zgadzają — tekst zostanie zniekształcony.
8. Kodowanie znaków a integracja z innymi systemami
W rzeczywistych projektach plikami często wymieniają się różne programy, napisane w różnych językach i działające na różnych systemach operacyjnych. Każdy może oczekiwać innego kodowania. Jeśli nie uzgodnić tego wcześniej — dostaniesz „krzaczki” i trudne do wyłapania błędy. Typowy przypadek: baza danych przechowuje tekst w UTF-8, a program czyta plik źródłowy jako Windows-1251 i ładuje do DB — zniekształcone znaki gwarantowane.
9. Typowe błędy przy pracy z kodowaniem znaków
Błąd nr 1: Brak określonego kodowania przy odczycie/zapisie pliku.
W efekcie program działa „u mnie”, a u kolegi — „krzaczki”.
Błąd nr 2: Używanie przestarzałych konstruktorów (FileReader, FileWriter).
Zawsze używają kodowania systemowego — pułapka dla początkujących.
Błąd nr 3: Nieprawidłowe kodowanie pliku źródłowego.
Jeśli plik zapisano w jednym kodowaniu, a czyta się go w innym, część znaków będzie zniekształcona lub zastąpiona znakami zapytania.
Błąd nr 4: Utrata znaków przy konwersji między kodowaniami.
Jeśli docelowe kodowanie nie obsługuje wszystkich znaków (na przykład ASCII zamiast UTF-8), część tekstu po prostu zniknie.
GO TO FULL VERSION