← Kursa Dön
📄 Text · 12 min

Unicode ve Karakter Kodlama

Bilgisayarlar sadece sayıları anlar — 0 ve 1. Ama biz ekranda harfler, semboller, emojiler görüyoruz. Peki bilgisayar "A" harfini nasıl biliyor? İşte karakter kodlama tam olarak bu sorunu çözer: hangi sayı hangi karaktere karşılık gelecek?

Bu derste ASCII'den Unicode'a uzanan yolculuğu, Java'nın bu konudaki yaklaşımını ve Türkçe karakter sorunlarının neden yaşandığını öğreneceğiz.


ASCII — Her Şeyin Başlangıcı

1960'larda Amerikalılar bir standart oluşturdular: ASCII (American Standard Code for Information Interchange). 7 bit kullanır, toplam 128 karakter tanımlar.

Aralıkİçerik
0-31Kontrol karakterleri (satır sonu, tab vb.)
32-47, 58-64, 91-96, 123-127Noktalama, semboller
48-57Rakamlar (0-9)
65-90Büyük harfler (A-Z)
97-122Küçük harfler (a-z)
char a = 'A';
System.out.println((int) a); // 65

char sifir = '0';
System.out.println((int) sifir); // 48

// Karakter aritmetiği
char b = (char) ('A' + 1);
System.out.println(b); // 'B'

ASCII'nin sorunu açık: sadece İngilizce harfler var. Türkçe "ş", "ğ", "ü" yok. Çince, Japonca, Arapça hiç yok. 128 karakter dünya için yeterli değil.


Karmaşa Dönemi — Code Pages

ASCII'den sonra her ülke kendi "genişletilmiş" kodlamasını yaptı. ISO-8859-1 (Batı Avrupa), ISO-8859-9 (Türkçe — Latin-5), Windows-1254 (Türkçe Windows)...

Sorun şu: aynı sayı farklı kodlamalarda farklı karakterlere denk geliyordu. Bir Türk bilgisayarında doğru görünen metin, bir Japon bilgisayarında çöp karakterlere dönüyordu.

Bunu şöyle düşün: Herkes kendi telefon rehberini yapıyor. "135 numaralı kişi" birinin rehberinde Ahmet, diğerininkininde Yuki. Ortak bir rehber lazım — işte Unicode bu ortak rehber.


Unicode — Evrensel Standart

Unicode, dünyadaki tüm yazı sistemlerini kapsayan tek bir karakter seti. Her karaktere benzersiz bir numara (code point) verir.

GösterimAçıklamaÖrnek
U+0041Unicode code pointA
U+015EUnicode code pointŞ
U+1F600Unicode code point😀

Unicode 15.0 ile birlikte 149.000'den fazla karakter tanımlı. ASCII'nin 128 karakterinden geldiğimizi düşününce devasa bir gelişme.

Temel Unicode Blokları

Aralıkİçerik
U+0000 – U+007FTemel Latin (ASCII ile aynı)
U+0080 – U+00FFLatin-1 Eki
U+0100 – U+017FLatin Genişletilmiş-A (Türkçe harfler burada)
U+0400 – U+04FFKiril (Rusça vb.)
U+4E00 – U+9FFFCJK (Çince/Japonca/Korece)
U+1F600 – U+1F64FEmojiler

Türkçe'ye özel karakterler:

KarakterUnicodeAçıklama
çU+00E7Latin-1 Eki'nde
ÇU+00C7Latin-1 Eki'nde
ğU+011FLatin Genişletilmiş-A'da
ĞU+011ELatin Genişletilmiş-A'da
ıU+0131Latin Genişletilmiş-A'da
İU+0130Latin Genişletilmiş-A'da
öU+00F6Latin-1 Eki'nde
ÖU+00D6Latin-1 Eki'nde
şU+015FLatin Genişletilmiş-A'da
ŞU+015ELatin Genişletilmiş-A'da
üU+00FCLatin-1 Eki'nde
ÜU+00DCLatin-1 Eki'nde

UTF-8, UTF-16, UTF-32 — Kodlama Formatları

Unicode bir "tablo" — hangi numaranın hangi karakter olduğunu söylüyor. Ama bu numaraları bilgisayar belleğinde nasıl saklayacağız? İşte burada UTF devreye giriyor.

UTF-8 — Web'in Standardı

Değişken uzunluklu kodlama: bir karakter 1-4 byte kullanır.

AralıkByte SayısıÖrnek
U+0000 – U+007F1 byteA, B, 0-9
U+0080 – U+07FF2 byteş, ğ, ü, é
U+0800 – U+FFFF3 byte中, 日, ★
U+10000 – U+10FFFF4 byte😀, 🎉

UTF-8'in avantajı: ASCII metinler UTF-8'de de aynı kalır (1 byte). Bu yüzden geriye uyumlu ve verimli.

İnternetteki sayfaların %98'i UTF-8 kullanır. Yeni bir proje başlıyorsan soru sorma, UTF-8 kullan.

UTF-8 Encoding Detayı

UTF-8 nasıl çalışır? İlk byte'ın başındaki bit'ler kaç byte olduğunu söyler:

Byte sayısıİlk byte başlangıcıKullanılabilir bit
10xxxxxxx7 bit
2110xxxxx 10xxxxxx11 bit
31110xxxx 10xxxxxx 10xxxxxx16 bit
411110xxx 10xxxxxx 10xxxxxx 10xxxxxx21 bit
// 'A' (U+0041) → 1 byte: 01000001
// 'ş' (U+015F) → 2 byte: 11000101 10011111
// '中' (U+4E2D) → 3 byte: 11100100 10111000 10101101

String a = "A";
String s = "ş";
String c = "中";

System.out.println(a.getBytes("UTF-8").length); // 1
System.out.println(s.getBytes("UTF-8").length); // 2
System.out.println(c.getBytes("UTF-8").length); // 3

UTF-16 — Java'nın İç Formatı

Değişken uzunluklu: bir karakter 2 veya 4 byte kullanır.

AralıkBoyutAçıklama
U+0000 – U+FFFF2 byteBasic Multilingual Plane (BMP)
U+10000 – U+10FFFF4 byte (surrogate pair)Emojiler, nadir karakterler

Java `char` tipi UTF-16 kullanır — yani 2 byte, 0-65535 arası. Bu, BMP'deki tüm karakterleri kapsar ama emoji gibi karakterler için yetmez.

UTF-32

Sabit uzunluk: her karakter 4 byte. Basit ama israf. Pratikte nadiren kullanılır.


Java'da char ve Unicode

Java'nın char tipi 16 bit (2 byte) ve UTF-16 code unit tutar. Bu çoğu karakter için yeterli:

char turkce = 'ş';       // U+015F — 2 byte'a sığar
char cin = '中';          // U+4E2D — 2 byte'a sığar
char ascii = 'A';         // U+0041 — sorunsuz

System.out.println(turkce); // ş
System.out.println(cin);    // 中

Ama emojiler ve bazı nadir karakterler 2 byte'a sığmaz. Bunlar surrogate pair denen iki char ile temsil edilir:

String emoji = "😀";
System.out.println(emoji.length());      // 2! (iki char)
System.out.println(emoji.codePointCount(0, emoji.length())); // 1 (bir karakter)

// Surrogate pair
char high = emoji.charAt(0); // 0xD83D
char low = emoji.charAt(1);  // 0xDE00
System.out.println(Integer.toHexString(high)); // d83d
System.out.println(Integer.toHexString(low));  // de00

💡 Modern Java'da karakter bazlı işlem yapıyorsan codePointCount(), codePointAt() gibi metotları kullan. charAt() ve length() surrogate pair'leri yanlış sayar.


Unicode Escape Sequence'ler

Java'da Unicode karakterlerini doğrudan kaynak kodda \uXXXX formatıyla yazabilirsin:

char a = '\u0041';    // 'A'
char sHarf = '\u015F'; // 'ş'
char omega = '\u03A9'; // 'Ω'

System.out.println(a);      // A
System.out.println(sHarf);  // ş
System.out.println(omega);  // Ω

Yaygın escape sequence'ler:

EscapeKarakterAçıklama
\nYeni satırLine feed
\rSatır başıCarriage return
\tTabYatay tab
\\\Ters eğik çizgi
\""Çift tırnak
\''Tek tırnak
\uXXXXUnicode4 haneli hex code point
System.out.println("Sat\u0131r 1\nSat\u0131r 2");
// Satır 1
// Satır 2

System.out.println("Tab\tAras\u0131");
// Tab    Arası

System.out.println("\"T\u0131rnak i\u00E7inde\"");
// "Tırnak içinde"

⚠️ Dikkat: \u escape'leri derleyici tarafından çok erken aşamada (lexical analysis) işlenir. Bu yüzden yorum satırlarında bile sorun çıkarabilir:

// Bu bir dosya yolu: C:\users\name
// Yukarıdaki satır HATA VEREBİLİR! \u'dan sonraki "sers" 
// geçerli hex değil. Doğrusu:
// Bu bir dosya yolu: C:\\users\\name

Türkçe Karakter Sorunları

Türkçe, karakter kodlama açısından özel zorluklar taşır. En büyük sorun I/İ ve ı/i dönüşümü.

Büyük/Küçük Harf Tuzağı

İngilizcede:

  • iI (büyük)

  • Ii (küçük)

Türkçede:

  • iİ (büyük, noktalı)

  • ıI (büyük, noktasız)

  • İi (küçük, noktalı)

  • Iı (küçük, noktasız)

String s = "milk"; // İngilizce kelime

// Varsayılan locale Türkçe ise:
System.out.println(s.toUpperCase()); 
// "MILK" yerine "MİLK" olabilir! (i → İ)

Çözüm: Locale belirt.

import java.util.Locale;

String s = "istanbul";

// Türkçe kurallarla
String trUpper = s.toUpperCase(new Locale("tr", "TR"));
System.out.println(trUpper); // İSTANBUL (doğru!)

// İngilizce kurallarla
String enUpper = s.toUpperCase(Locale.ENGLISH);
System.out.println(enUpper); // ISTANBUL (I noktasız)
// Programlama amaçlı karşılaştırmalarda Locale.ROOT kullan
String protocol = "HTTP";
boolean esit = protocol.toLowerCase(Locale.ROOT).equals("http");
System.out.println(esit); // true — her locale'de doğru çalışır

💡 Altın kural: Kullanıcıya gösterilecek metinlerde Türkçe locale kullan. Programatik karşılaştırmalarda (protokol adı, dosya uzantısı vb.) Locale.ROOT veya Locale.ENGLISH kullan.

Dosya Kodlama Sorunları

Java kaynak dosyaları modern IDE'lerde genellikle UTF-8 olarak kaydedilir. Ama bazen eski dosyalar farklı kodlamada olabilir:

// Dosya okurken kodlama belirt
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;

// UTF-8 ile oku
String icerik = Files.readString(
    Path.of("dosya.txt"), 
    StandardCharsets.UTF_8
);

// ISO-8859-9 (Türkçe Latin-5) ile oku
byte[] bytes = Files.readAllBytes(Path.of("eski_dosya.txt"));
String turkce = new String(bytes, "ISO-8859-9");

Veritabanı ve Türkçe

Veritabanında Türkçe karakter sorunu yaşamamak için:

  • Veritabanı/tablo charset'ini utf8mb4 yap (MySQL)

  • Connection string'de characterEncoding=UTF-8 belirt

  • Collation olarak utf8mb4_turkish_ci kullan (Türkçe sıralama)


Karakter Sınıflandırma

Java'nın Character sınıfı Unicode'a uygun karakter sınıflandırma metotları sunar:

System.out.println(Character.isLetter('A'));     // true
System.out.println(Character.isLetter('5'));     // false
System.out.println(Character.isDigit('5'));      // true
System.out.println(Character.isLetterOrDigit('A')); // true
System.out.println(Character.isWhitespace(' ')); // true
System.out.println(Character.isUpperCase('A'));  // true
System.out.println(Character.isLowerCase('ş'));  // true
// Türkçe karakterler de doğru tanınır
System.out.println(Character.isLetter('ğ'));     // true
System.out.println(Character.isLetter('İ'));     // true
System.out.println(Character.toUpperCase('ş'));  // 'Ş'

Java'nın String Encoding Tarihi

Java'nın char tipinin neden 2 byte olduğunu anlamak için tarihsel bağlam önemli:

  • 1991: Java tasarlanırken Unicode 1.0 henüz 65.536 karakterle sınırlıydı

  • Java tasarımcıları: "16 bit tüm karakterleri kapsar" dediler → char = 2 byte

  • 1996: Unicode 2.0 ile supplementary characters eklendi (65.536'yı aştı)

  • Artık bazı karakterler 2 char'a sığmıyor → surrogate pair zorunluluğu

Bu geriye uyumluluk sorunu Java'da hâlâ yaşıyor. String.length() karakter sayısı değil, char (UTF-16 code unit) sayısını verir. Bu yüzden emoji içeren string'lerde codePointCount() kullanmalısın.

// "A" = 1 char, 1 code point
// "😀" = 2 char, 1 code point  
// "🇹🇷" = 4 char, 2 code point (iki regional indicator)

String test = "A😀🇹🇷";
System.out.println("length(): " + test.length());           // 7
System.out.println("codePointCount: " + 
    test.codePointCount(0, test.length()));                   // 4

Pratik Örnek: Türkçe Karakter Dönüştürücü

public class TurkceNormalizer {
    public static String turkceKaldir(String girdi) {
        if (girdi == null) return null;
        
        return girdi
            .replace('ç', 'c').replace('Ç', 'C')
            .replace('ğ', 'g').replace('Ğ', 'G')
            .replace('ı', 'i').replace('I', 'I')
            .replace('İ', 'I').replace('i', 'i')
            .replace('ö', 'o').replace('Ö', 'O')
            .replace('ş', 's').replace('Ş', 'S')
            .replace('ü', 'u').replace('Ü', 'U');
    }

    public static void main(String[] args) {
        String sehir = "İstanbul Güneşli";
        String ascii = turkceKaldir(sehir);
        System.out.println(ascii); // "Istanbul Gunesli"
    }
}

Bu tür dönüşümler URL slug oluşturma, dosya adı temizleme, arama normalizasyonu gibi işlerde sık kullanılır.


String ve Byte Dönüşümleri

import java.nio.charset.StandardCharsets;

String metin = "Merhaba Dünya";

// String → byte dizisi
byte[] utf8Bytes = metin.getBytes(StandardCharsets.UTF_8);
byte[] asciiBytes = metin.getBytes(StandardCharsets.US_ASCII);

System.out.println("UTF-8 byte sayısı: " + utf8Bytes.length);   // 15 (ü 2 byte)
System.out.println("ASCII byte sayısı: " + asciiBytes.length);  // 13 (ü kaybolur)

// byte dizisi → String
String geriDonen = new String(utf8Bytes, StandardCharsets.UTF_8);
System.out.println(geriDonen); // "Merhaba Dünya"

⚠️ Dikkat: Bir String'i yanlış kodlamayla byte'a çevirip geri dönüştürürsen veri kaybı olur. Her zaman aynı kodlamayı kullan.


Emoji ve Supplementary Characters

Unicode'un ilk 65.536 karakteri (BMP) char'a sığar. Ama emojiler ve bazı eski yazı sistemleri "supplementary" alanda — bunlar char'a sığmaz.

String bayrak = "🇹🇷"; // Türk bayrağı emojisi
System.out.println(bayrak.length());     // 4 (iki surrogate pair)
System.out.println(bayrak.codePointCount(0, bayrak.length())); // 2 (iki code point)

// Code point ile doğru iterasyon
String metin = "Merhaba 😀";
metin.codePoints().forEach(cp -> {
    System.out.println(Character.toString(cp) + " → U+" + 
        Integer.toHexString(cp).toUpperCase());
});
// M → U+4D
// e → U+65
// r → U+72
// h → U+68
// a → U+61
// b → U+62
// a → U+61
//   → U+20
// 😀 → U+1F600

Encoding Dönüşüm Tablosu

Farklı kodlamalar aynı byte'ları farklı yorumlar. İşte somut bir örnek:

import java.nio.charset.Charset;

public class EncodingFarki {
    public static void main(String[] args) throws Exception {
        String metin = "Şeker";
        
        // Farklı kodlamalarla byte'a çevir
        byte[] utf8 = metin.getBytes("UTF-8");
        byte[] latin5 = metin.getBytes("ISO-8859-9");
        byte[] latin1 = metin.getBytes("ISO-8859-1");
        
        System.out.println("UTF-8 byte sayısı: " + utf8.length);   // 6
        System.out.println("Latin-5 byte sayısı: " + latin5.length); // 5
        System.out.println("Latin-1 byte sayısı: " + latin1.length); // 5
        
        // Yanlış kodlamayla geri çevirince bozulur
        String bozuk = new String(utf8, "ISO-8859-9");
        System.out.println("Bozuk: " + bozuk); // Ş yerine garip karakterler
        
        String dogru = new String(utf8, "UTF-8");
        System.out.println("Doğru: " + dogru); // Şeker
    }
}

Bu "mojibake" denen bozuk karakter sorunu, web geliştirmede çok sık karşılaşılan bir problem. HTML'de <meta charset="UTF-8">, HTTP'de Content-Type: text/html; charset=UTF-8 header'ı bu yüzden kritik.


Java'da Charset Kullanımı

Java'nın Charset sınıfı desteklenen kodlamaları yönetir:

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

// Standart charset'ler (her JVM'de garanti)
Charset utf8 = StandardCharsets.UTF_8;
Charset ascii = StandardCharsets.US_ASCII;
Charset utf16 = StandardCharsets.UTF_16;
Charset iso8859 = StandardCharsets.ISO_8859_1;

// Varsayılan charset (platform bağımlı!)
Charset varsayilan = Charset.defaultCharset();
System.out.println("Varsayılan: " + varsayilan); // Genellikle UTF-8

// Kullanılabilir tüm charset'ler
System.out.println("Toplam charset: " + Charset.availableCharsets().size());

💡 Java 18+ ile varsayılan charset UTF-8 olarak sabitlendi. Eski Java sürümlerinde platform bağımlıydı (Windows'ta genellikle Windows-1252). Bu yüzden her zaman açıkça belirt.


Regex ve Unicode

Java regex'te Unicode karakter sınıfları kullanabilirsin:

String metin = "Ahmet 42 yaşında, İstanbul'da yaşıyor.";

// \p{L} — herhangi bir Unicode harf
String sadecHarfler = metin.replaceAll("[^\\p{L}\\s]", "");
System.out.println(sadecHarfler); // "Ahmet  yaşında İstanbulda yaşıyor"

// Türkçe karakterleri bul
boolean turkceVar = metin.matches(".*[çÇğĞıİöÖşŞüÜ].*");
System.out.println("Türkçe karakter var: " + turkceVar); // true

Pratik Örnek: Güvenli String Karşılaştırma

import java.text.Collator;
import java.util.Arrays;
import java.util.Locale;

public class TurkceSiralama {
    public static void main(String[] args) {
        String[] sehirler = {"Çanakkale", "Ankara", "İstanbul", 
                             "Bursa", "Şanlıurfa", "Diyarbakır", "Üsküdar"};
        
        // Yanlış sıralama — Unicode code point'e göre
        String[] yanlis = sehirler.clone();
        Arrays.sort(yanlis);
        System.out.println("Yanlış: " + Arrays.toString(yanlis));
        // Ç, İ, Ş, Ü sona gider — yanlış!
        
        // Doğru sıralama — Türkçe Collator ile
        Collator trCollator = Collator.getInstance(new Locale("tr", "TR"));
        String[] dogru = sehirler.clone();
        Arrays.sort(dogru, trCollator);
        System.out.println("Doğru: " + Arrays.toString(dogru));
        // Ankara, Bursa, Çanakkale, Diyarbakır, İstanbul, Şanlıurfa, Üsküdar
    }
}

Türkçe sıralama önemli bir konu. Normal String.compareTo() Unicode code point'e göre sıralar. Bu, Ç'yi C'den sonra, İ'yi I'dan sonra koymaz — tamamen farklı bir yere koyar. Collator kullanarak dil kurallarına uygun sıralama yapabilirsin.


BOM (Byte Order Mark) Sorunu

UTF-8 dosyaların başında bazen görünmez bir BOM karakteri (U+FEFF) olur. Windows Notepad bunu ekler. Bu, dosya okurken sorun çıkarabilir:

import java.nio.file.Files;
import java.nio.file.Path;

public class BomTemizle {
    public static String bomTemizle(String metin) {
        if (metin != null && metin.length() > 0 && metin.charAt(0) == '\uFEFF') {
            return metin.substring(1);
        }
        return metin;
    }
    
    public static void main(String[] args) throws Exception {
        String icerik = Files.readString(Path.of("dosya.txt"));
        icerik = bomTemizle(icerik);
        
        // Artık BOM olmadan işlem yapabilirsin
        if (icerik.startsWith("<?xml")) {
            System.out.println("XML dosyası");
        }
    }
}

Sık Yapılan Hatalar

1. Kodlama belirtmemek:

// Kötü — platform bağımlı
byte[] bytes = str.getBytes();
// İyi — kodlama açık
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);

2. Türkçe büyük/küçük harf dönüşümünde locale unutmak:

// Riskli
"istanbul".toUpperCase();
// Güvenli
"istanbul".toUpperCase(new Locale("tr", "TR"));

3. char ile emoji tutmaya çalışmak:

// Hata! Emoji char'a sığmaz
// char emoji = '😀'; // Derleme hatası
String emoji = "😀"; // Doğru — String kullan

4. String length() ile karakter sayısı sanmak:

String s = "Merhaba 😀";
System.out.println(s.length());     // 10 (char sayısı, karakter değil!)
System.out.println(s.codePointCount(0, s.length())); // 9 (gerçek karakter)

5. Farklı kodlamalarla okuma/yazma:

// Dosyayı UTF-8 ile yaz
Files.writeString(Path.of("test.txt"), "Türkçe", StandardCharsets.UTF_8);

// Ama Latin-5 ile okursan bozulur!
byte[] bytes = Files.readAllBytes(Path.of("test.txt"));
String bozuk = new String(bytes, Charset.forName("ISO-8859-9")); // Bozuk!

Özet

  • ASCII 128 karakter tanımlar (sadece İngilizce), Unicode 149.000+ karakter kapsar (tüm diller)

  • UTF-8 web standardı (değişken 1-4 byte), UTF-16 Java'nın iç formatı (2 veya 4 byte)

  • Java'da char 2 byte'tır ve BMP karakterlerini tutar; emojiler için surrogate pair kullanılır

  • Türkçe I/İ ve ı/i dönüşümü özel — toUpperCase()/toLowerCase() çağrırken Locale belirt

  • Dosya ve ağ işlemlerinde kodlamayı her zaman açıkça belirt (StandardCharsets.UTF_8)

  • Modern Java'da karakter bazlı doğru işlem için codePointCount() ve codePoints() kullan