← Kursa Dön
📄 Text · 15 min

String Derinlemesine

String, Java'da en çok kullandığın veri tipi. Kullanıcı adı, e-posta, dosya yolu, hata mesajı — her yerde String var. Ama String'in bir sürü inceliği var ve bunları bilmezsen sinsi bug'larla tanışırsın.

Bu derste String'in iç yapısını, neden "özel" olduğunu ve doğru kullanım yollarını öğreneceğiz.


String Nedir?

String, karakter dizisi tutan bir sınıftır (class). Primitive tip değildir — bir nesnedir. Ama Java onu o kadar özel tutar ki, neredeyse primitive gibi kullanırsın.

String isim = "Ahmet";           // Literal ile oluşturma
String soyisim = new String("Yılmaz"); // new ile oluşturma (önerilmez)

Çift tırnak arasına yazdığın her şey bir String literal'dir. Java bunu arka planda bir String nesnesi olarak oluşturur.


String Immutable'dır — Değiştirilemez

Bu, String hakkında bilmen gereken en önemli şey: bir String nesnesi oluşturulduktan sonra içeriği asla değişmez.

Bunu şöyle düşün: String bir taş tablete kazınmış yazı gibi. Yazıyı değiştiremezsin — ama yeni bir tablet yapabilirsin.

String mesaj = "Merhaba";
mesaj = mesaj + " Dünya";

System.out.println(mesaj); // "Merhaba Dünya"

Burada "Merhaba" nesnesi değişmedi. Java yeni bir "Merhaba Dünya" nesnesi oluşturdu ve mesaj değişkeni artık bu yeni nesneye işaret ediyor. Eski "Merhaba" nesnesi bellekte öylece duruyor (garbage collector temizleyene kadar).

String s = "Java";
String s2 = s.toUpperCase(); // Yeni nesne oluşur

System.out.println(s);  // "Java" — orijinal değişmedi!
System.out.println(s2); // "JAVA" — yeni nesne

💡 String üzerinde çağırdığın her metot (toUpperCase, trim, replace vb.) orijinal String'i değiştirmez, yeni bir String döner. Bu çok önemli — unutma.


String Pool — Java'nın Akıllı Bellek Yönetimi

Java, String literal'lerini özel bir bellek alanında saklar: String Pool. Aynı içeriğe sahip iki literal aynı nesneyi paylaşır.

String a = "Merhaba";
String b = "Merhaba";

System.out.println(a == b); // true — aynı nesne!

İkisi de pool'daki aynı "Merhaba" nesnesine işaret ediyor. Bu, bellek tasarrufu sağlar.

Ama new ile oluşturduğunda farklı:

String c = new String("Merhaba");
String d = new String("Merhaba");

System.out.println(c == d);      // false — farklı nesneler!
System.out.println(c.equals(d)); // true — içerik aynı

new her zaman heap'te yeni bir nesne oluşturur, pool'u kullanmaz.


equals() vs == — Kritik Fark

Bu, Java'da en çok yapılan hatalardan biri. İki String'in içeriğini karşılaştırmak istiyorsan `equals()` kullan, == değil.

  • == : İki referansın aynı nesneyi gösterip göstermediğini kontrol eder

  • equals() : İki String'in içeriğinin aynı olup olmadığını kontrol eder

String s1 = "Java";
String s2 = "Java";
String s3 = new String("Java");

System.out.println(s1 == s2);      // true (ikisi de pool'da)
System.out.println(s1 == s3);      // false (s3 heap'te)
System.out.println(s1.equals(s3)); // true (içerik aynı)

⚠️ Altın Kural: String karşılaştırmasında her zaman `equals()` kullan. == sadece pool'daki literal'ler arasında doğru çalışır — bu güvenilir değil.

// Doğru yol
if (isim.equals("Ahmet")) {
    System.out.println("Hoş geldin Ahmet!");
}

// Daha güvenli yol — NullPointerException'dan korunur
if ("Ahmet".equals(isim)) {
    System.out.println("Hoş geldin Ahmet!");
}

İkinci yöntemde isim null olsa bile hata almassın, çünkü "Ahmet" literal'i null olamaz.

equalsIgnoreCase — Büyük/Küçük Harf Duyarsız

String girdi = "java";
System.out.println(girdi.equals("Java"));           // false
System.out.println(girdi.equalsIgnoreCase("Java"));  // true

Kullanıcı girdisi alırken çok işe yarar. Kullanıcı "JAVA", "Java" veya "java" yazabilir — hepsini yakala.


Sık Kullanılan String Metotları

length() — Uzunluk

String s = "Merhaba";
System.out.println(s.length()); // 7

Boş String ile null karıştırma:

String bos = "";
String nul = null;

System.out.println(bos.length()); // 0
// System.out.println(nul.length()); // NullPointerException!

charAt(index) — Belirli Karakter

String s = "Java";
System.out.println(s.charAt(0)); // 'J'
System.out.println(s.charAt(3)); // 'a'
// s.charAt(4) → StringIndexOutOfBoundsException!

İndeks 0'dan başlar, length() - 1'e kadar gider.

substring() — Alt Dize

String s = "Merhaba Dünya";
System.out.println(s.substring(8));     // "Dünya"
System.out.println(s.substring(0, 7));  // "Merhaba"

substring(baslangic, bitis) — başlangıç dahil, bitiş hariç. Bu "dahil-hariç" kuralı Java'nın her yerinde geçerli.

indexOf() ve contains() — Arama

String s = "Java programlama dili";

System.out.println(s.indexOf("program"));  // 5
System.out.println(s.indexOf("Python"));   // -1 (bulunamadı)
System.out.println(s.contains("Java"));    // true
System.out.println(s.contains("java"));    // false (büyük/küçük harf duyarlı)

split() — Bölme

String csv = "Ali,Veli,Ayşe,Fatma";
String[] isimler = csv.split(",");

for (String isim : isimler) {
    System.out.println(isim);
}
// Ali
// Veli
// Ayşe
// Fatma
String metin = "Merhaba   Dünya";
String[] kelimeler = metin.split("\\s+"); // Bir veya daha fazla boşluk

System.out.println(kelimeler.length); // 2

split() parametre olarak regex alır. Bu yüzden nokta ile bölmek istersen "\\." yazmalısın — çünkü regex'te . "herhangi bir karakter" demek.

trim() ve strip() — Boşluk Temizleme

String s = "   Merhaba   ";
System.out.println("[" + s.trim() + "]");  // [Merhaba]
System.out.println("[" + s.strip() + "]"); // [Merhaba] (Java 11+)

// Sadece baş veya son
System.out.println("[" + s.stripLeading() + "]");  // [Merhaba   ]
System.out.println("[" + s.stripTrailing() + "]");  // [   Merhaba]

trim() sadece ASCII boşluklarını temizler. strip() (Java 11+) Unicode boşluk karakterlerini de temizler. Modern Java'da strip() tercih et.

replace() — Değiştirme

String s = "Java çok zor";
String yeni = s.replace("zor", "kolay");
System.out.println(yeni); // "Java çok kolay"
System.out.println(s);    // "Java çok zor" — orijinal değişmedi!
// Tüm geçenleri değiştirir
String telefon = "0532-123-45-67";
String temiz = telefon.replace("-", "");
System.out.println(temiz); // "0532123456 7"

toUpperCase() ve toLowerCase()

String s = "Java";
System.out.println(s.toUpperCase()); // "JAVA"
System.out.println(s.toLowerCase()); // "java"

⚠️ Türkçe tuzağı: "title".toUpperCase()"TITLE", ama Türkçe locale'de "ı".toUpperCase()"I" değil, "I" olmalı. Locale belirtmen gerekebilir:

String s = "sığır";
System.out.println(s.toUpperCase(new java.util.Locale("tr", "TR")));
// "SIĞIR" — Türkçe İ/I kuralına uygun

String.format() — Biçimlendirme

String ad = "Ahmet";
int yas = 25;
double not_ = 3.75;

String mesaj = String.format("Ad: %s, Yaş: %d, Not: %.2f", ad, yas, not_);
System.out.println(mesaj);
// "Ad: Ahmet, Yaş: 25, Not: 3.75"
FormatAçıklamaÖrnek
%sString"Ahmet"
%dTam sayı42
%fOndalıklı3.140000
%.2f2 basamak ondalık3.14
%nYeni satır(satır sonu)
%bBooleantrue

Java 15+ ile text block ve formatted() metodu:

String html = """
    <html>
        <body>
            <p>Merhaba %s!</p>
        </body>
    </html>
    """.formatted(ad);

String Birleştirme (Concatenation)

+ Operatörü

En basit yol:

String ad = "Ali";
String soyad = "Demir";
String tamAd = ad + " " + soyad;
System.out.println(tamAd); // "Ali Demir"

Bir String ile başka bir tip birleştirildiğinde, diğer tip otomatik String'e çevrilir:

int yas = 30;
String mesaj = "Yaş: " + yas; // "Yaş: 30"
System.out.println("Sonuç: " + (10 + 20)); // "Sonuç: 30"
System.out.println("Sonuç: " + 10 + 20);   // "Sonuç: 1020" — Dikkat!

Son satırda soldan sağa işleniyor: önce "Sonuç: " + 10"Sonuç: 10", sonra "Sonuç: 10" + 20"Sonuç: 1020". Parantez kullanarak bu tuzaktan kaçın.

concat() Metodu

String s = "Merhaba".concat(" ").concat("Dünya");
System.out.println(s); // "Merhaba Dünya"

Pratikte + operatörü daha okunabilir, concat() nadiren kullanılır.


StringBuilder — Performanslı String Birleştirme

String immutable olduğu için döngüde string birleştirmek çok verimsiz. Her + işlemi yeni bir nesne oluşturur.

// KÖTÜ — her adımda yeni String nesnesi
String sonuc = "";
for (int i = 0; i < 10000; i++) {
    sonuc += i + ", "; // 10000 yeni String nesnesi!
}

Bunu şöyle düşün: Her satırda yeni bir kağıt alıp tüm yazıyı baştan yazıyorsun. 10.000 kere. Çılgınlık.

StringBuilder bunu çözer:

// İYİ — tek nesne üzerinde çalışır
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i).append(", ");
}
String sonuc = sb.toString();

StringBuilder mutable (değiştirilebilir) bir karakter dizisi tutar. append() çağırdığında mevcut diziyi genişletir, yeni nesne oluşturmaz.

StringBuilder Metodları

StringBuilder sb = new StringBuilder("Merhaba");

sb.append(" Dünya");       // Sonuna ekle
sb.insert(7, " Güzel");   // Belirli indekse ekle
sb.replace(0, 7, "Selam"); // Aralığı değiştir
sb.delete(5, 11);          // Aralığı sil
sb.reverse();              // Ters çevir

System.out.println(sb.toString());
// Zincirleme (method chaining) kullanımı
String sonuc = new StringBuilder()
    .append("Ad: ")
    .append("Ali")
    .append(", Yaş: ")
    .append(25)
    .toString();

System.out.println(sonuc); // "Ad: Ali, Yaş: 25"

StringBuilder vs StringBuffer

İkisi neredeyse aynı. Tek fark:

  • StringBuilder: Thread-safe değil, daha hızlı. Bunu kullan.

  • StringBuffer: Thread-safe (synchronized), daha yavaş.

Çoğu durumda StringBuilder yeterli. Çoklu thread ortamında çalışıyorsan ve aynı buffer'a farklı thread'ler yazıyorsa StringBuffer kullan — ama bu çok nadir bir senaryo.


String Karşılaştırma — Özet Tablo

YöntemNe yaparNe zaman kullan
==Referans karşılaştırmaKullanma (String için)
equals()İçerik karşılaştırmaHer zaman
equalsIgnoreCase()Büyük/küçük harf yok sayarakKullanıcı girdisi
compareTo()Alfabetik sıralamaSıralama işlemleri
String a = "elma";
String b = "armut";

int sonuc = a.compareTo(b);
// sonuc > 0 → a, b'den sonra gelir (alfabetik)
// sonuc < 0 → a, b'den önce gelir
// sonuc == 0 → eşitler

System.out.println("elma".compareTo("armut")); // pozitif (e > a)

Null ve Empty Kontrolü

String s = null;
String bos = "";
String bosluk = "   ";

// Null kontrolü
if (s != null && !s.isEmpty()) {
    // güvenli kullanım
}

// Java 11+ ile isBlank()
System.out.println("".isEmpty());     // true
System.out.println("".isBlank());     // true
System.out.println("   ".isEmpty());  // false
System.out.println("   ".isBlank()); // true (sadece boşluk)

isEmpty() uzunluğun 0 olup olmadığını kontrol eder. isBlank() (Java 11+) sadece boşluk karakterleri içerip içermediğini kontrol eder.

💡 Pratik öneri: null kontrolünü her zaman önce yap. s.isEmpty() çağrısı s null ise NullPointerException fırlatır.


Pratik Örnek: Kullanıcı Girdisi Temizleme

public class GirdiTemizleyici {
    public static String temizle(String girdi) {
        if (girdi == null) {
            return "";
        }
        
        return girdi
            .strip()                    // Baş/son boşluk
            .replaceAll("\\s+", " ")    // Çoklu boşluk → tek boşluk
            .trim();
    }
    
    public static void main(String[] args) {
        String kirli = "   Merhaba    Dünya   ";
        String temiz = temizle(kirli);
        System.out.println("[" + temiz + "]"); // [Merhaba Dünya]
    }
}

Java 13+: Text Blocks (Çok Satırlı String)

// Eski yol
String json = "{\n" +
    "  \"ad\": \"Ali\",\n" +
    "  \"yas\": 25\n" +
    "}";

// Text block (Java 13+)
String jsonYeni = """
    {
      "ad": "Ali",
      "yas": 25
    }
    """;

System.out.println(jsonYeni);

Text block'lar üç çift tırnakla (""") başlar ve biter. Escape karakterlerine gerek kalmaz, çok satırlı metinleri çok daha okunabilir yapar.


String.join() — Birleştirme

split() ile böldüysen, join() ile birleştirirsin:

String[] kelimeler = {"Java", "çok", "güzel"};
String cumle = String.join(" ", kelimeler);
System.out.println(cumle); // "Java çok güzel"

// Virgülle birleştirme
String csv = String.join(",", "Ali", "Veli", "Ayşe");
System.out.println(csv); // "Ali,Veli,Ayşe"

// List ile de çalışır
List<String> isimler = List.of("Java", "Python", "Go");
String diller = String.join(", ", isimler);
System.out.println(diller); // "Java, Python, Go"

String repeat() ve diğer Java 11+ Metotları

Java 11 ve sonrası String'e güzel metotlar ekledi:

// repeat() — Java 11+
String cizgi = "-".repeat(30);
System.out.println(cizgi); // "------------------------------"

String tab = "  ".repeat(3);
System.out.println(tab + "Girintili metin");

// isBlank() — Java 11+
System.out.println("".isBlank());      // true
System.out.println("   ".isBlank());   // true
System.out.println(" a ".isBlank());   // false

// lines() — Java 11+ (satırlara böler)
String metin = "Satır 1\nSatır 2\nSatır 3";
metin.lines().forEach(System.out::println);

// indent() — Java 12+
String kod = "if (true) {\n    ok();\n}";
System.out.println(kod.indent(4)); // Her satıra 4 boşluk ekler

String Performans Karşılaştırması

Döngüde string birleştirmenin ne kadar yavaş olduğunu somut olarak görelim:

public class StringPerformans {
    public static void main(String[] args) {
        int tekrar = 100_000;
        
        // Yöntem 1: + operatörü (KÖTÜ)
        long baslangic1 = System.currentTimeMillis();
        String s1 = "";
        for (int i = 0; i < tekrar; i++) {
            s1 += "a";
        }
        long sure1 = System.currentTimeMillis() - baslangic1;
        
        // Yöntem 2: StringBuilder (İYİ)
        long baslangic2 = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < tekrar; i++) {
            sb.append("a");
        }
        String s2 = sb.toString();
        long sure2 = System.currentTimeMillis() - baslangic2;
        
        System.out.println("+ operatörü: " + sure1 + " ms");
        System.out.println("StringBuilder: " + sure2 + " ms");
        // Tipik sonuç: ~3000ms vs ~2ms — 1000x fark!
    }
}

💡 Kural: 3-4 string birleştirme? + kullan, sorun yok. Döngüde veya çok sayıda birleştirme? StringBuilder kullan.

Modern Java derleyicisi tek satırdaki + zincirlerini otomatik olarak optimize eder. Ama döngüdeki birleştirmeleri optimize edemez — orada sen StringBuilder kullanmalısın.


intern() Metodu

intern() bir String'i String Pool'a ekler (veya pool'daki mevcut kopyasını döner):

String s1 = new String("test"); // Heap'te yeni nesne
String s2 = s1.intern();         // Pool'daki "test"i döner
String s3 = "test";              // Pool'daki aynı "test"

System.out.println(s1 == s3);  // false (s1 heap'te)
System.out.println(s2 == s3);  // true (ikisi de pool'da)

intern() genellikle çok fazla tekrarlanan string'lerde bellek tasarrufu için kullanılır (örneğin XML parser'lar). Günlük kodda nadiren ihtiyaç duyarsın.


Pratik Örnek: Metin İstatistikleri

public class MetinIstatistik {
    public static void main(String[] args) {
        String metin = "Java programlama dili 1995 yılında Sun Microsystems "
            + "tarafından geliştirildi. Java platform bağımsız bir dildir.";
        
        // Kelime sayısı
        String[] kelimeler = metin.split("\\s+");
        System.out.println("Kelime sayısı: " + kelimeler.length);
        
        // Karakter sayısı (boşluk hariç)
        long karakterSayisi = metin.chars()
            .filter(c -> c != ' ')
            .count();
        System.out.println("Karakter (boşluksuz): " + karakterSayisi);
        
        // Cümle sayısı
        String[] cumleler = metin.split("[.!?]+");
        System.out.println("Cümle sayısı: " + cumleler.length);
        
        // En uzun kelime
        String enUzun = "";
        for (String kelime : kelimeler) {
            if (kelime.length() > enUzun.length()) {
                enUzun = kelime;
            }
        }
        System.out.println("En uzun kelime: " + enUzun);
        
        // Belirli kelime kaç kez geçiyor
        String aranan = "Java";
        int index = 0;
        int sayac = 0;
        while ((index = metin.indexOf(aranan, index)) != -1) {
            sayac++;
            index += aranan.length();
        }
        System.out.println("'" + aranan + "' kaç kez: " + sayac);
    }
}

Pratik Örnek: E-posta Doğrulama (Basit)

public class EmailDogrulama {
    public static boolean gecerliMi(String email) {
        if (email == null || email.isBlank()) {
            return false;
        }
        
        email = email.strip();
        
        // @ işareti var mı?
        int atIndex = email.indexOf('@');
        if (atIndex <= 0) return false; // @ yok veya başta
        
        // @ sonrasında . var mı?
        int dotIndex = email.lastIndexOf('.');
        if (dotIndex <= atIndex + 1) return false;
        
        // Sonda . olmamalı
        if (dotIndex == email.length() - 1) return false;
        
        // Birden fazla @ olmamalı
        if (email.indexOf('@') != email.lastIndexOf('@')) return false;
        
        return true;
    }
    
    public static void main(String[] args) {
        System.out.println(gecerliMi("ali@example.com"));  // true
        System.out.println(gecerliMi("ali@example"));       // false
        System.out.println(gecerliMi("@example.com"));      // false
        System.out.println(gecerliMi("ali@@example.com"));  // false
        System.out.println(gecerliMi(null));                 // false
    }
}

Sık Yapılan Hatalar

1. == ile karşılaştırma:

String s1 = new String("test");
String s2 = new String("test");
if (s1 == s2) { } // false — BUG!
if (s1.equals(s2)) { } // true — doğru

2. Null üzerinde metot çağırma:

String s = null;
s.length(); // NullPointerException!

// Güvenli yol
if (s != null && s.length() > 0) { ... }
// Veya
"sabit".equals(s); // s null olsa bile hata vermez

3. Döngüde + ile birleştirme:

// Yavaş — StringBuilder kullan
String s = "";
for (int i = 0; i < 1000; i++) {
    s += i; // Her adımda yeni String nesnesi!
}

4. split() ile regex tuzağı:

"dosya.txt".split(".");   // Boş dizi! (. regex'te her karakter)
"dosya.txt".split("\\.");  // ["dosya", "txt"] — doğru
"a|b|c".split("|");       // Her karakter ayrı! (| regex'te OR)
"a|b|c".split("\\|");     // ["a", "b", "c"] — doğru

5. String'in immutable olduğunu unutmak:

String s = "  merhaba  ";
s.trim(); // Orijinal s DEĞİŞMEZ!
System.out.println("[" + s + "]"); // [  merhaba  ]

s = s.trim(); // Sonucu geri ata!
System.out.println("[" + s + "]"); // [merhaba]

Mülakat Soruları

S: String neden immutable? C: Güvenlik (String Pool paylaşımı), thread safety, hashCode caching. Değiştirilebilseydi pool'daki bir string'i değiştirmek onu kullanan herkesi etkilerdi.

S: String Pool nedir? C: JVM'in string literal'leri sakladığı özel bellek alanı. Aynı içerikli literal'ler aynı nesneyi paylaşır, bellek tasarrufu sağlar.

S: StringBuilder vs StringBuffer farkı? C: StringBuilder thread-safe değil (daha hızlı), StringBuffer thread-safe (synchronized, daha yavaş). Çoğu durumda StringBuilder yeterli.

S: String s = "abc" ile String s = new String("abc") farkı? C: İlki pool'dan alır (varsa mevcut nesneyi kullanır). İkincisi her zaman heap'te yeni nesne oluşturur.


Özet

  • String immutable'dır — üzerindeki her işlem yeni bir nesne döner, orijinali değiştirmez

  • String karşılaştırmasında `equals()` kullan, == kullanma

  • String Pool sayesinde aynı literal'ler bellekte tek nesne olarak tutulur

  • Döngüde string birleştirme gerekiyorsa `StringBuilder` kullan

  • trim()/strip(), split(), substring(), format() en sık kullanılan metotlardır

  • Null kontrolünü her zaman equals()'tan önce yap veya literal'i sola koy: "sabit".equals(degisken)