Bilgisayar ve Programlamaya Giriş · Ders Notu

Dizgiler

Programların işlediği verinin büyük bölümü metindir: öğrenci adları, e-posta adresleri, kullanıcının klavyeden yazdığı cümleler, ekrana basılan raporlar. Java'da metin String sınıfının nesneleriyle temsil edilir. Bu not dizgilerin bellekte nasıl tutulduğunu, neden değiştirilemediğini ve en sık kullanılan metotlarını anlatır. İndislerle karakterlere erişim, alt dizgi alma, arama, bölme ve değiştirme işlemleri tek tek ele alınır. Ardından dizgilerin karşılaştırılması gelir: == ile equals arasındaki fark, compareTo ile sözlüksel sıra ve Türkçe harflerin yol açtığı tuzaklar. Klavyeden metin okuma, StringBuilder ile verimli birleştirme ve printf ile biçimli çıktı da notun konuları arasındadır. Son kesimdeki uygulamalar (ters çevirme, palindrom, sesli harf ve kelime sayma, harf frekansı, Sezar şifresi) bütün bu araçları bir araya getirir. Her program JDK 21 ile derlenip çalıştırılmıştır; verilen çıktılar programların gerçek çıktılarıdır.

Öğrenme çıktıları

Temsil
String ile char arasındaki farkı, dizgi değişkeninin bir referans tuttuğunu ve dizgilerin değiştirilemez olduğunu açıklamak; boş dizgi ile null'ı ayırt etmek.
İşleme
İndis ve charAt ile karakterlere erişmek; length, indexOf, substring, split, replace gibi metotları doğru sınırlarla kullanmak; döngüyle dizgi taramak.
Karşılaştırma
İçerik karşılaştırmasını (equals) referans karşılaştırmasından (==) ayırmak; compareTo sonucunu hesaplamak; Türkçe harflerde yerel ayarın etkisini açıklamak.
Girdi ve çıktı
Scanner ile kelime ve satır okumak, tampon davranışını izlemek; StringBuilder ile verimli birleştirme yapmak; printf ile biçimli metin üretmek.

11.1Dizgi nedir?

Şimdiye kadar yazılan programların çoğu sayılarla çalıştı. Bir sayı int ya da double değişkeninde tutulur ve aritmetik işlemlere girer. Metin ise başka bir yapıdır: harflerin, rakamların, boşlukların ve noktalama işaretlerinin sıralı bir dizisidir. Bu kesim metnin Java'daki iki yapı taşını, char ve String tiplerini tanıtır.

11.1.1Karakter ve dizgi

Tanım 11.1

Karakter (character), metnin tek bir harfi, rakamı, boşluğu ya da işaretidir. Java'da bir karakter char tipindeki bir değişkende tutulur ve tek tırnakla yazılır: 'T', '7', ' '. Dizgi (string), sıfır ya da daha fazla karakterin sıralı dizisidir. Java'da dizgiler String sınıfının nesneleridir ve çift tırnakla yazılır: "Tarkan", "7", "".

Tırnak türü önemlidir. 'T' tek bir karakterdir ve tipi char'dır. "T" ise uzunluğu 1 olan bir dizgidir ve tipi String'dir. İkisi ekranda aynı görünür ama Java için farklı tiplerdir. Bu yüzden bir dizgi bir char değişkenine atanamaz:

public class IlkelDegil {
    public static void main(String[] args) {
        char c = "T";
        System.out.println(c);
    }
}
IlkelDegil.java:3: error: incompatible types: String cannot be converted to char
        char c = "T";
                 ^
1 error

İleti, String tipinin char tipine dönüştürülemeyeceğini söyler. Doğrusu char c = 'T'; yazmaktır.

String sınıfı java.lang paketindedir. Bu paket her Java programına kendiliğinden eklenir; Scanner için gereken import satırı String için gerekmez. Programın içine doğrudan yazılan "Tarkan" gibi bir metne dizgi sabiti (string literal) denir.

11.1.2Dizgi bir nesnedir

char, int ve double gibi ilkel tipler (primitive type) değerlerini doğrudan değişkenin içinde tutar. String ise ilkel bir tip değildir; bir sınıftır. Bir String değişkeni metnin kendisini değil, metni taşıyan nesnenin bellekteki yerini tutar. Bu yer bilgisine referans (reference) denir. Şekil 11.1 iki tipin bellekteki görünümünü karşılaştırır.

YığınHeapad0T1a2r3k4a5nString nesnesi: karakter dizisic'T'char: değer doğrudan yığında
Şekil 11.1: String ad = "Tarkan"; ve char c = 'T'; bildirimlerinin bellekteki görünümü. ad değişkeni heap'teki altı karakterlik nesneyi gösteren bir ok (referans) tutar. c değişkeni ise 'T' değerini doğrudan içinde taşır.

Şekildeki iki bellek bölgesinin adı ilerideki bütün çizimlerde geçer. Yığın (stack), metotların yerel değişkenlerinin tutulduğu bölgedir. Heap ise new ile oluşturulan nesnelerin ve dizgilerin bulunduğu bölgedir. Yığındaki bir String değişkeni heap'teki nesneyi bir okla gösterir. Aynı nesneyi birden çok değişken gösterebilir. Dizgilerin karşılaştırılmasında bu paylaşım belirleyicidir.

Tanım 11.2

Bir nesne (object), bir sınıftan üretilmiş, kendi verisini ve metotlarını taşıyan bellek parçasıdır. Referans, bir nesnenin bellekteki yerini gösteren değerdir. String s = "Ada"; bildiriminde s değişkeni bir referanstır; "Ada" ise heap'teki nesnedir.

Nesne olmanın bir sonucu da metotlardır. Bir dizgiye ait işlemler nokta işaretiyle çağrılır: ad.length() dizginin uzunluğunu, ad.charAt(0) ilk karakterini verir. İlkel tiplerin metodu yoktur; c.length() gibi bir yazım derlenmez.

11.1.3Dizgi oluşturma, boş dizgi ve null

Bir dizgi değişkenine dört farklı biçimde ilk değer verilebilir:

String ad = "Tarkan";
String bos = "";
String kopya = new String(ad);
String yok = null;

İlk satır en yaygın biçimdir: ad, "Tarkan" sabitini gösterir. İkinci satırdaki "", hiç karakter içermeyen boş dizgidir. Üçüncü satır new ile yeni bir nesne oluşturur ve ad'ın içeriğini ona kopyalar. Dördüncü satırdaki null, değişkenin hiçbir nesneyi göstermediği anlamına gelir. Şekil 11.2 dört değişkenin bellekteki durumunu gösterir.

YığınHeapDizgi havuzu (sabitler)Tarkan""uzunluk 0Tarkannew: ayrıadboskopyayoknull
Şekil 11.2: Dört dizgi değişkeni. ad ve bos dizgi havuzundaki sabitleri gösterir. kopya, new ile açılan ayrı bir nesneyi gösterir; içeriği aynı olsa da nesne farklıdır. yok hiçbir nesneyi göstermez.

Şekildeki dizgi havuzu, heap içinde aynı sabitlerin tek bir kopyasının tutulduğu özel bir bölgedir. Buradaki kural kısadır: sabitle oluşturulan dizgi havuzdan gelir, new ise her zaman yeni bir nesne açar. Uygulamada new String(...) yazmaya neredeyse hiç gerek yoktur; dizgi sabiti tercih edilir.

Boş dizgi ile null sık karıştırılır. Boş dizgi gerçek bir nesnedir; uzunluğu 0'dır ve bütün metotları çağrılabilir. null ise bir nesne değildir; bir değişkenin “henüz hiçbir şeyi göstermiyorum” durumudur. null bir değişken üzerinden metot çağrılırsa program NullPointerException istisnasıyla durur. Aşağıdaki program dört değişkeni yazdırır, sonra yok.length()'i çağırır:

public class Olusturma {
    public static void main(String[] args) {
        String ad = "Tarkan";
        String bos = "";
        String kopya = new String(ad);
        String yok = null;
        char c = 'T';
        System.out.println(ad + " " + ad.length());
        System.out.println("[" + bos + "] " + bos.length());
        System.out.println(kopya + " " + kopya.length());
        System.out.println(yok);
        System.out.println(c);
        System.out.println(yok.length());
    }
}
Tarkan 6
[] 0
Tarkan 6
null
T
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "yok" is null

Çıktıda boş dizginin köşeli parantezler arasında hiçbir şey göstermediği ve uzunluğunun 0 olduğu görülür. System.out.println(yok) hata vermez, null yazar. Hata ancak null üzerinden bir metot çağrıldığında oluşur. JDK 21'in iletisi sorunun nedenini de söyler: yok null olduğu için String.length() çağrılamaz.

Sık yapılan hata

Değeri henüz belli olmayan bir dizgi değişkenine null verip sonra metot çağırmak. Değişken bir döngüde ya da koşulda doldurulacaksa ilk değeri "" yapmak çoğu zaman daha güvenlidir. null gerçekten “değer yok” anlamı taşıyorsa metot çağrısından önce s != null denetimi yapılır.

11.1.4+ ile birleştirme

İki dizgiyi uç uca eklemeye dizgi birleştirme (string concatenation) denir. Java'da bunun için + operatörü kullanılır. Aynı operatör sayılarda toplama yapar. Hangisinin yapılacağına işlenenlerin tipi karar verir: iki işlenenden en az biri String ise + birleştirmedir, ikisi de sayıysa toplamadır.

public class Birlestirme {
    public static void main(String[] args) {
        String sayi1 = "12";
        String sayi2 = "34";
        String toplam = sayi1 + sayi2;
        System.out.println(toplam);
        int a = 12, b = 34;
        System.out.println(a + b);
        System.out.println(sayi1 + 3 + 4);
        System.out.println(3 + 4 + sayi1);
        System.out.println(sayi1 + (3 + 4));
        System.out.println("Toplam: " + a + b);
        System.out.println("Toplam: " + (a + b));
        System.out.println("x" + null + 'c' + 2.5 + true);
        System.out.println("Merhaba".concat(" dünya"));
    }
}
1234
46
1234
712
127
Toplam: 1234
Toplam: 46
xnullc2.5true
Merhaba dünya

Program satır satır şöyle çalışır. sayi1 + sayi2 iki dizgiyi birleştirir; değişkenin adı “toplam” olsa da sonuç "1234"'tür. a + b iki int'i toplar: 46. Sonraki iki satır işlem sırasının etkisini gösterir. + soldan sağa işlenir:

  • sayi1 + 3 + 4: önce "12" + 3 hesaplanır. Bir işlenen dizgi olduğu için 3 dizgiye çevrilir: "123". Sonra "123" + 4 = "1234".
  • 3 + 4 + sayi1: önce 3 + 4 hesaplanır. İkisi de sayıdır: 7. Sonra 7 + "12" = "712".
  • sayi1 + (3 + 4): parantez önce hesaplanır: 7. Sonra "12" + 7 = "127".

"Toplam: " + a + b satırı da aynı tuzağa düşer: soldaki dizgi yüzünden iki sayı da birer birer metne eklenir ve “Toplam: 1234” yazılır. Toplamın yazılması için parantez gerekir: "Toplam: " + (a + b). Bir dizgiye null, char, double ve boolean değerleri de eklenebilir; her biri metin karşılığına çevrilir. concat metodu da iki dizgiyi birleştirir, ancak yalnızca dizgi kabul eder.

Örnek 11.1

int x = 5, y = 2; iken "x+y=" + x + y ve x + y + "=x+y" ifadelerinin değerleri nedir?

Çözüm. İlk ifadede en soldaki işlenen dizgidir. "x+y=" + 5 = "x+y=5", ardından "x+y=5" + 2 = "x+y=52". İkinci ifadede ilk iki işlenen sayıdır: 5 + 2 = 7. Sonra 7 + "=x+y" = "7=x+y". Aynı üç işlenen, sıraları değişince farklı sonuç verir.

11.1.5char bir sayıdır

Bilgisayar karakterleri sayıyla saklar. Her karakterin Unicode standardında bir kod numarası vardır: 'a' 97, 'b' 98, 'A' 65, '0' 48'dir. Java'da char aslında 16 bitlik işaretsiz bir tam sayı tipidir. Bu yüzden bir char aritmetik işlemlere girebilir. Sonucun tipi kurala bağlıdır: char ile bir tam sayı ya da iki char toplanınca sonuç int olur.

public class CharAritmetik {
    public static void main(String[] args) {
        char k = 'a';
        System.out.println(k + 1);
        System.out.println((char) (k + 1));
        System.out.println("" + k + 1);
        System.out.println('a' + 'b');
        System.out.println("" + 'a' + 'b');
        k++;
        System.out.println(k);
    }
}
98
b
a1
195
ab
b

k + 1 bir int'tir: 97 + 1 = 98. (char) (k + 1) bu sayıyı yeniden karaktere çevirir: b. "" + k + 1 ifadesinde en soldaki boş dizgi bütün ifadeyi birleştirmeye çevirir: önce "a", sonra "a1". 'a' + 'b' ise iki sayının toplamıdır: 97 + 98 = 195. İki karakteri yan yana yazdırmak için başa yine "" konur. k++ ise char değişkenini olduğu yerde bir artırır ve tipi değişmez: b.

Karakterlerin sayı olması dizgi işlemlerinin çoğunda işe yarar. İki küçük harfin farkı alfabedeki uzaklıklarını verir: 'k' − 'a' = 107 − 97 = 10. Bir rakam karakterinden '0' çıkarılınca rakamın sayı değeri bulunur: '7' − '0' = 55 − 48 = 7. Harf frekansı ve Sezar şifresi uygulamaları bu iki fikre dayanır.

public class CharSayi {
    public static void main(String[] args) {
        System.out.println((int) 'a' + " " + (int) 'k' + " " + (int) 'v' + " " + (int) 'z' + " " + (int) 'A' + " " + (int) '0');
        System.out.println('k' - 'a');
        System.out.println((char) ('a' + 10));
        System.out.println((int) 'ç' + " " + ('ç' - 'a') + " " + (int) 'ı' + " " + (int) 'İ' + " " + (int) 'ş');
        System.out.println('7' - '0');
    }
}
97 107 118 122 65 48
10
k
231 134 305 304 351
7

Son satırlar Türkçe harflerin kodlarını da gösterir: 'ç' 231, 'ı' 305, 'İ' 304, 'ş' 351'dir. Bu kodlar İngiliz alfabesinin 97–122 aralığının dışındadır. 'ç' − 'a' = 134 olduğu için 'a''dan 'z''ye 26 hücrelik bir harf tablosunda 'ç''ye yer yoktur.

11.2Değiştirilemezlik

Dizgilerin en önemli özelliği, oluşturulduktan sonra içeriklerinin değişmemesidir. Bu özellik ilk bakışta garip gelir: s = s + "!" yazınca s değişmiş gibi görünür. Gerçekte değişen nesne değil, değişkenin gösterdiği yerdir.

11.2.1Yeni nesne, aynı eski nesne

Tanım 11.3

Oluşturulduktan sonra içeriği değiştirilemeyen nesneye değiştirilemez (immutable) nesne denir. String değiştirilemez bir sınıftır: hiçbir metodu nesnenin karakterlerini değiştirmez. Dizgiyi “değiştiren” her işlem (birleştirme, büyük harfe çevirme, alt dizgi alma, değiştirme) yeni bir String nesnesi üretir ve onu döndürür. Bu özelliğin adı değiştirilemezliktir (immutability).

Şekil 11.3 en basit örneği gösterir. String s = "Merhaba"; satırından sonra s, yedi karakterlik nesneyi gösterir. s = s + "!"; satırı bu nesneye ünlem eklemez. Önce s + "!" hesaplanır ve sekiz karakterlik yeni bir nesne oluşur. Sonra atama, s'nin okunu bu yeni nesneye taşır. Eski "Merhaba" nesnesi olduğu gibi kalır.

YığınHeapMerhabas YığınHeapMerhabadeğişmediMerhaba!yenis
Şekil 11.3: s = s + "!"; ataması. Üstte atamadan önceki durum: s, “Merhaba” nesnesini gösterir. Altta atamadan sonraki durum: “Merhaba!” adlı yeni bir nesne oluşmuş ve ok ona taşınmıştır; eski nesne değişmemiştir.

Bunu bir programla da görmek mümkündür. Atamadan önce eski nesneyi gösteren ikinci bir değişken (eski) tutulursa, atamadan sonra iki değişkenin farklı nesneleri gösterdiği anlaşılır:

public class Unlem {
    public static void main(String[] args) {
        String s = "Merhaba";
        String eski = s;
        s = s + "!";
        System.out.println(s);
        System.out.println(eski);
        System.out.println(s == eski);
    }
}
Merhaba!
Merhaba
false

eski hâlâ “Merhaba”yı gösterir. s == eski karşılaştırması, iki değişkenin aynı nesneyi gösterip göstermediğini sorar ve false verir: artık iki ayrı nesne vardır. Hiçbir değişkenin göstermediği nesneler zamanla gereksiz hâle gelir. Java'da bu nesneleri çöp toplayıcı (garbage collector) kendiliğinden temizler; programcının belleği elle geri vermesi gerekmez.

11.2.2toUpperCase örneğini adım adım izlemek

Değiştirilemezliğin pratikteki sonucu şudur: bir dizgi metodunun sonucu kullanılmak isteniyorsa bir değişkene atanmalıdır. Aşağıdaki beş satır bu noktayı gösterir.

String dil = "java";
dil.toUpperCase();
System.out.println(dil); // java
dil = dil.toUpperCase();
System.out.println(dil); // JAVA
java
JAVA

İkinci satır dil.toUpperCase()'i çağırır ama dönen değeri hiçbir yere koymaz. Metot görevini yapar ve yeni bir "JAVA" nesnesi üretir. Ne var ki bu nesneyi gösteren bir değişken yoktur; sonuç kaybolur. dil eski nesneyi göstermeye devam ettiği için üçüncü satır java yazar. Dördüncü satırda aynı çağrının sonucu dil'e atanır; ok yeni nesneye taşınır ve beşinci satır JAVA yazar. Tablo 11.1 beş satırın izleme tablosudur; Şekil 11.4 ise kritik iki anı gösterir.

Tablo 11.1: toUpperCase örneğinin izleme tablosu. “Heap'teki nesneler” sütunu o satırdan sonra bellekte bulunan dizgi nesnelerini verir.
Satırdil neyi gösterir?Heap'teki nesnelerÇıktı
1"java""java"
2"java""java", "JAVA" (kimse göstermiyor)
3"java""java", "JAVA" (çöp)java
4"JAVA" (yeni nesne)"java", "JAVA"
5"JAVA""java", "JAVA"JAVA
YığınHeapDizgi havuzu (sabitler)javaJAVAkimse göstermiyordil YığınHeapDizgi havuzu (sabitler)javaJAVAdil
Şekil 11.4: Üstte 2. satırdan sonraki durum: toUpperCase() yeni bir “JAVA” nesnesi üretmiş, ama onu gösteren değişken yoktur. Altta 4. satırdan sonraki durum: dönen nesne dil'e atanmış, ok ona taşınmıştır. “java” nesnesi iki durumda da aynıdır.
Sık yapılan hata

ad.trim();, ad.toUpperCase(); ya da ad.replace('a', 'e'); gibi satırları tek başına yazıp ad'ın değiştiğini sanmak. Bu satırlar derlenir ve çalışır, ama sonuç atılır. Doğrusu ad = ad.trim(); biçiminde sonucu yeniden atamaktır. Bazı geliştirme ortamları dönen değeri kullanılmayan çağrılar için uyarı gösterir.

Bir dizginin tek bir karakterini olduğu yerde değiştirmenin yolu da yoktur. Dizilerde a[0] = 5; yazılabilir, ama dizgilerde buna karşılık gelen bir işlem yoktur. charAt yalnızca okumak içindir; ona değer atanamaz:

public class SetCharAt {
    public static void main(String[] args) {
        String s = "java";
        s.charAt(0) = 'J';
        System.out.println(s);
    }
}
SetCharAt.java:4: error: unexpected type
        s.charAt(0) = 'J';
                ^
  required: variable
  found:    value
1 error

İleti, eşittirin solunda bir değişken beklendiğini ama bir değer bulunduğunu söyler. s.charAt(0) bir değer üretir, bir bellek yeri değildir. İlk harfi büyütmek için yeni bir dizgi kurulur: s = Character.toUpperCase(s.charAt(0)) + s.substring(1); Bu satır "Java" üretir.

11.2.3Değiştirilemezlik neden yararlıdır?

Her değişiklikte yeni nesne üretmek israf gibi görünebilir. Java'nın tasarımcıları bu bedeli üç kazanç için ödemeyi seçmiştir:

  • Güvenli paylaşım. Aynı dizgi nesnesini birden çok değişken gösterebilir. Hiçbiri nesneyi değiştiremeyeceği için bir değişken üzerinden yapılan işlem ötekileri etkilemez. Dizgi havuzu bu sayede mümkündür: programdaki bütün "Giresun" sabitleri tek bir nesneyi paylaşır.
  • Öngörülebilirlik. Bir dizgi bir metoda gönderildiğinde metot onu bozamaz. Çağıran taraf, metottan dönünce dizginin aynı kaldığından emindir.
  • Güvenlik. Dosya adları, ağ adresleri ve parolalar dizgilerde taşınır. Denetimden geçmiş bir dosya adının sonradan değiştirilememesi önemli bir güvencedir.

Bedel ise çok sayıda birleştirmenin yavaşlamasıdır. Bir döngüde binlerce kez s = s + x yazılırsa her turda yeni nesne üretilir ve eski içerik kopyalanır. Bu durum için Java değiştirilebilir bir yardımcı sınıf sunar: StringBuilder.

11.3İndisleme

Bir dizginin karakterleri sıralıdır ve her birinin bir sıra numarası vardır. Bu numaraya indis (index) denir. Diziler gibi dizgilerde de sayma 0'dan başlar.

Tanım 11.4

Uzunluğu n olan bir dizginin karakterleri 0, 1, …, n − 1 indislerindedir. İlk karakter 0, son karakter n − 1 indisindedir. Bir karaktere indisiyle erişmeye indisleme (indexing) denir. Java'da s.length() dizginin uzunluğunu, s.charAt(i) i indisindeki karakteri bir char değeri biçiminde döndürür.

0G1i2r3e4s5u6nss.length() = 7s.charAt(0) → 'G's.charAt(6) → 'n'7✗
Şekil 11.5: s = "Giresun" dizgisinin indisleri. Yedi karakter 0'dan 6'ya kadar numaralanır. s.charAt(0) 'G', s.charAt(6) 'n' döndürür. 7 numaralı bir hücre yoktur; s.charAt(7) geçersizdir.

Şekil 11.5'teki dizgi için aşağıdaki program uzunluğu, ilk ve son karakteri yazar. Son karakterin indisi her dizgi için s.length() - 1 ifadesiyle bulunur; böylece kod uzunluk ne olursa olsun çalışır.

public class Indis {
    public static void main(String[] args) {
        String s = "Giresun";
        System.out.println(s.length());
        System.out.println(s.charAt(0));
        System.out.println(s.charAt(s.length() - 1));
        int[] dizi = {4, 8, 15};
        System.out.println(dizi.length + " " + s.length());
        System.out.println(s.charAt(7));
    }
}
7
G
n
3 7
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7

Son satır, var olmayan 7 numaralı karaktere erişmeye çalışır. Derleyici bu hatayı yakalamaz, çünkü indis genellikle program çalışırken hesaplanır. Hata çalışma zamanında StringIndexOutOfBoundsException istisnasıyla ortaya çıkar. İleti, 7 indisinin 7 uzunluklu bir dizginin sınırları dışında kaldığını söyler. Bu tür bir indise sınır dışı (out of bounds) indis denir.

Programdaki dizi.length ile s.length() arasındaki fark da önemlidir. Dizilerde length bir alandır ve parantezsiz yazılır. Dizgilerde length() bir metottur ve parantezle çağrılır. Parantez unutulursa program derlenmez:

public class StringMetotDegil {
    public static void main(String[] args) {
        String s = "Giresun";
        System.out.println(s.length);
    }
}
StringMetotDegil.java:4: error: cannot find symbol
        System.out.println(s.length);
                            ^
  symbol:   variable length
  location: variable s of type String
1 error

Derleyici, String tipinde length adlı bir değişken bulamadığını söyler. İletideki “symbol” sözcüğü değişken, metot ya da sınıf gibi adlandırılmış bir program öğesi anlamındadır.

Sık yapılan hata

Son karaktere s.charAt(s.length()) ile erişmeye çalışmak. Uzunluk n ise son indis n − 1'dir; s.charAt(s.length()) her zaman sınır dışıdır. Aynı hata döngülerde de görülür: for (int i = 0; i <= s.length(); i++) döngüsünün son turu charAt'te istisna fırlatır. Doğru koşul i < s.length()'tir.

11.4Dizgi metotları

String sınıfının altmışı aşkın metodu vardır. Bu kesim gündelik programlarda en sık gerekenleri ele alır. Hepsinde ortak bir kural geçerlidir: hiçbir metot asıl dizgiyi değiştirmez. Bir sonuç gerekiyorsa metot onu yeni bir değerle döndürür.

11.4.1length, indexOf, substring ve contains

Dört temel metot bir arada şu programda kullanılır:

public class TemelMetotlar {
    public static void main(String[] args) {
        String s = "Java Programlama";
        int uzunluk = s.length();
        int konum = s.indexOf("Programlama");
        String alt = s.substring(5, 16);
        boolean icerir = s.contains("Java");
        System.out.println(alt + " " + konum);
        System.out.println(uzunluk + " " + icerir + " " + s.indexOf("java") + " " + s.indexOf('a') + " " + s.lastIndexOf('a'));
    }
}
Programlama 5
16 true -1 1 15

Programın son println satırı, ilk altı satıra eklenmiş bir denetim satırıdır; çıktısı aşağıda ayrıca açıklanır. İlk altı satır şöyle çalışır:

  1. s, 16 karakterlik "Java Programlama" dizgisini gösterir. Aradaki boşluk da bir karakterdir ve 4 indisindedir.
  2. s.length() 16 döndürür. İndisler 0'dan 15'e kadardır.
  3. s.indexOf("Programlama"), aranan metnin ilk karakterinin indisini döndürür. “P” 5 indisindedir; konum 5 olur. Aranan metin hiç geçmeseydi sonuç −1 olurdu.
  4. s.substring(5, 16), 5 indisinden başlayıp 16 indisinden önce biten parçayı verir. Başlangıç dahil, bitiş hariçtir. Parçanın uzunluğu 16 − 5 = 11'dir: "Programlama".
  5. s.contains("Java"), aranan metin dizginin herhangi bir yerinde geçiyorsa true döndürür.
  6. alt + " " + konum birleştirilip yazılır: Programlama 5.
Tablo 11.2: Temel metotlar programının izleme tablosu. Boş hücre, değişkenin henüz bildirilmediğini gösterir.
SatıruzunlukkonumalticerirÇıktı
1
216
3165
4165"Programlama"
5165"Programlama"true
6165"Programlama"trueProgramlama 5
0J1a2v3a4␣5P6r7o8g9r10a11m12l13a14m15a▲ konum
Şekil 11.6: "Java Programlama" dizgisinin on altı hücresi. indexOf("Programlama"), aranan metnin ilk karakteri olan “P”nin indisini, yani 5'i verir; ok bu hücreyi gösterir.

Programa eklenen denetim satırı üç ayrıntıyı daha gösterir. s.indexOf("java") −1 verir, çünkü arama büyük-küçük harfe duyarlıdır: dizgide “java” değil “Java” vardır. s.indexOf('a') 1 verir; indexOf tek bir karakteri de arayabilir ve ilk geçtiği yeri bulur. s.lastIndexOf('a') ise aramayı sondan yapar ve son geçtiği yeri, 15'i bulur.

Örnek 11.2

String t = "bilgisayar"; için t.indexOf('a'), t.lastIndexOf('a'), t.indexOf("say") ve t.substring(2, 5) değerleri nedir?

Çözüm. Önce indisler yazılır: b=0, i=1, l=2, g=3, i=4, s=5, a=6, y=7, a=8, r=9. İlk 'a' 6 indisinde, son 'a' 8 indisindedir. "say" metni 5 indisindeki 's' ile başlar; sonuç 5'tir. substring(2, 5) 2, 3 ve 4 indislerindeki karakterleri alır: "lgi". Uzunluğu 5 − 2 = 3'tür.

11.4.2Sık kullanılan metotlar

Tablo 11.3 en sık kullanılan metotları özetler. Ardından gelen program, hepsini kısa bir dizgi üzerinde dener.

Tablo 11.3: Sık kullanılan String metotları. Hepsi asıl dizgiyi değiştirmeden bir sonuç döndürür.
MetotSonuçDönüş tipi
length()Karakter (kod birimi) sayısıint
charAt(i)i konumundaki karakterchar
substring(a, b)a dahil, b hariç alt dizgiString
indexOf(x)x'in ilk konumu; yoksa −1int
toLowerCase(), toUpperCase()Küçük ya da büyük harfli yeni dizgiString
trim(), strip()Baştaki ve sondaki boşlukları atılmış yeni dizgiString
isEmpty(), isBlank()Uzunluk 0 mı; yalnızca boşluktan mı oluşuyor?boolean
s0M1e2r3h4a5b6asubstring(1, 4) → "erh"indexOf('a') → 4length() → 7indexOf('z') → -1toLowerCase() → "merhaba"
Şekil 11.7: s = "Merhaba" üzerinde beş metot. substring(1, 4) 1, 2 ve 3 indislerini alır: “erh”. indexOf('a') ilk 'a'nın indisini, 4'ü verir; 'z' hiç geçmediği için indexOf('z') −1'dir.
public class SikMetotlar {
    public static void main(String[] args) {
        String s = "Merhaba";
        System.out.println(s.length());
        System.out.println(s.charAt(1));
        System.out.println(s.substring(1, 4));
        System.out.println(s.indexOf('a'));
        System.out.println(s.indexOf('z'));
        System.out.println(s.toLowerCase());
        System.out.println(s.toUpperCase());
        String k = "  Ada Can \t";
        System.out.println("[" + k.trim() + "] [" + k.strip() + "]");
        String g = " Ada ";
        System.out.println(g.trim().length() + " " + g.strip().length());
        System.out.println("".isEmpty() + " " + "   ".isEmpty() + " " + "   ".isBlank());
        System.out.println("ab".repeat(3));
    }
}
7
e
erh
4
-1
merhaba
MERHABA
[Ada Can] [Ada Can]
5 3
true false true
ababab

İlk yedi satır Şekil 11.7'daki değerleri doğrular. Sonraki satırlar boşluk temizleme metotlarını dener. k dizgisinin başında iki boşluk, sonunda bir boşluk ve bir sekme karakteri (\t) vardır. trim() ve strip() ikisi de bunları siler ve aradaki boşluğa dokunmaz: [Ada Can].

İki metodun farkı daha seyrek karakterlerde ortaya çıkar. trim() yalnızca kodu 32 ve altında olan karakterleri, yani ASCII boşluk ve kontrol karakterlerini siler. strip() ise Unicode'un bütün boşluk karakterlerini tanır. g dizgisinin iki ucunda “em boşluğu” (U+2003) adlı geniş bir boşluk vardır. trim() bu karakteri tanımaz ve uzunluk 5 kalır; strip() siler ve uzunluk 3'e iner. Kullanıcı girdisinde ve kopyalanıp yapıştırılmış metinde strip() daha güvenlidir.

isEmpty() yalnızca uzunluğu 0 olan dizgide true verir. isBlank() ise boş dizgide ve yalnızca boşluktan oluşan dizgide true verir. Bu yüzden üç boşluklu " " için ilki false, ikincisi true'dur. Son satırdaki repeat(3) dizgiyi üç kez yan yana yazar: ababab.

11.4.3split ile bölme

Virgülle ayrılmış bir liste, boşluklarla ayrılmış bir cümle ya da noktalı bir tarih, parçalara ayrılarak işlenir. Parçaları ayıran karaktere ayırıcı (delimiter) denir. split metodu dizgiyi ayırıcıların bulunduğu yerlerden keser ve parçaları bir String dizisinde döndürür.

import java.util.Arrays;

public class SplitReplace {
    public static void main(String[] args) {
        String[] p = "a,b,,c".split(",");
        System.out.println(Arrays.toString(p) + " " + p.length); // [a, b, , c] 4
        String s = "ali veli";
        String t = s.replace('l', 'L');
        System.out.println(t); // aLi veLi
        System.out.println(s); // ali veli
        String[] q = "a,b,,".split(",");
        System.out.println(Arrays.toString(q) + " " + q.length);
        String[] r = ",a,b".split(",");
        System.out.println(Arrays.toString(r) + " " + r.length);
        String[] u = "3.14".split(".");
        System.out.println(u.length);
        String[] v = "3.14".split("\\.");
        System.out.println(Arrays.toString(v));
    }
}
[a, b, , c] 4
aLi veLi
ali veli
[a, b] 2
[, a, b] 3
0
[3, 14]

Programın ilk altı satırı sunumdaki örnektir; sonraki satırlar split'in öteki kurallarını dener. "a,b,,c".split(",") metni üç virgülden keser ve dört parça üretir: "a", "b", "" ve "c". Ardışık iki virgülün arası boştur; bu boş dizgi de diziye girer. Arrays.toString(p) diziyi okunabilir biçimde yazar: [a, b, , c]. İki virgül arasındaki boşluk, boş parçanın yeridir. Şekil 11.8 bu bölmeyi gösterir.

0a1,2b3,4,5cmetin0a1b2""3cpvirgüllerden 4 parça
Şekil 11.8: "a,b,,c".split(","). Üstteki altı karakterlik metin 1, 3 ve 4 indislerindeki virgüllerden kesilir. Alttaki p dizisinin dört hücresi vardır; 2 indisindeki hücre boş dizgidir.

Ek satırlar split'in üç kuralını gösterir:

  • Sondaki boş parçalar atılır. "a,b,,".split(",") dört parça değil iki parça verir: [a, b]. Sondaki iki boş parça diziye girmez.
  • Baştaki boş parça kalır. ",a,b".split(",") üç parça verir: [, a, b]. İlk virgülden önceki boş metin bir parça sayılır.
  • Ayırıcı bir düzenli ifadedir. split'e verilen metin düz bir karakter değil, bir kalıptır. Kalıp dilinde nokta “herhangi bir karakter” anlamına gelir. Bu yüzden "3.14".split(".") her karakterden keser, bütün parçalar boş kalır ve sondaki boş parçalar atılınca dizi uzunluğu 0 olur. Noktanın kendisiyle bölmek için split("\\.") yazılır ve sonuç [3, 14] olur.
Tanım 11.5

Düzenli ifade (regular expression), bir metin kalıbını tarif eden küçük bir dildir. Bazı karakterlerin bu dilde özel anlamı vardır: . herhangi bir karakter, + “bir ya da daha fazla”, [0-9] herhangi bir rakam, \s herhangi bir boşluk karakteri anlamına gelir. Özel bir karakteri kendi anlamıyla kullanmak için önüne ters bölü konur. Java dizgi sabitinde ters bölünün kendisi \\ diye yazıldığı için kalıp "\\." ya da "\\s+" biçimini alır.

Örnek 11.3

"23.10.2026".split("\\.") ve "elma, armut,kiraz".split(",") kaç parça verir, parçalar nelerdir?

Çözüm. İlk dizgide iki nokta vardır; üç parça çıkar: "23", "10", "2026". İkinci dizgide iki virgül vardır; yine üç parça çıkar: "elma", " armut", "kiraz". İkinci parçanın başındaki boşluk korunur, çünkü ayırıcı yalnızca virgüldür. Boşluğun da atılması için ya her parçaya strip() uygulanır ya da ayırıcı ",\\s*" (virgül ve ardından sıfır ya da daha fazla boşluk) biçiminde verilir.

11.4.4replace ve düzenli ifadeler

replace bir dizgideki bütün eşleşmeleri başka bir metinle değiştirilmiş yeni bir dizgi döndürür. Aynı programın 4–7. satırları bunu gösterir: s.replace('l', 'L'), "ali veli" içindeki iki 'l' harfini (1 ve 6 indisleri) büyük harfe çevirir ve "aLi veLi" döndürür. s aynı kalır; son satır yine ali veli yazar. Şekil 11.9 iki nesneyi yan yana gösterir.

0a1l2i34v5e6l7is0a1L2i34v5e6L7itreplace('l','L'): yeni nesne
Şekil 11.9: t = s.replace('l', 'L'). Üstte s'nin gösterdiği “ali veli” nesnesi değişmeden durur. Altta t, 1 ve 6 indislerinde 'L' bulunan yeni nesneyi gösterir.

Değiştirme ailesinin dört üyesi vardır ve aralarındaki fark aranan şeyin türündedir. Tablo 11.4 sunumdaki örnekleri, JDK 21 çıktılarıyla birlikte verir.

Tablo 11.4: Değiştirme metotları. Sonuç sütunu programın gerçek çıktısıdır.
ÇağrıSonuçAçıklama
"banana".replace('a', 'o')bononoher 'a' karakteri 'o' olur
"banana".replace("an", "AN")bANANadüz metin araması; soldan sağa, çakışmadan
"a1b22c333".replaceAll("[0-9]+", "#")a#b#c#her rakam grubu tek bir # olur
"a1b22c333".replaceFirst("[0-9]+", "#")a#b22c333yalnızca ilk eşleşme değişir
"Ada Can Ece".replaceAll(" +", " ")Ada Can Eceart arda boşluklar teke iner

replace düz metin arar; replaceAll ve replaceFirst ise düzenli ifade arar. [0-9]+ kalıbı “bir ya da daha fazla rakam” demektir. "a1b22c333" içinde üç rakam grubu vardır: 1, 22 ve 333. Her grup bütünüyle eşleşir ve tek bir # ile değişir. " +" kalıbı da “bir ya da daha fazla boşluk” demektir; iki ve üç boşluklu aralıklar tek boşluğa iner.

Sık yapılan hata

Düz bir metni değiştirmek için replaceAll kullanmak. Aranan metinde nokta, artı, yıldız gibi özel karakterler varsa sonuç beklenmedik olur. "1.5".replace(".", ",") beklenen 1,5'i verir. "1.5".replaceAll(".", ",") ise her karakteri değiştirir ve ,,, üretir; nokta “herhangi bir karakter” anlamına gelir. Düz metin için replace yeterlidir ve o da bütün eşleşmeleri değiştirir.

11.4.5join, valueOf ve format

Üç static metot, bir nesne üzerinden değil sınıf adıyla çağrılır: String.join, String.valueOf ve String.format.

  • String.join(", ", "Ada", "Can", "Ece"), parçaları aralarına ayırıcı koyarak birleştirir: Ada, Can, Ece. split'in tersi gibi düşünülebilir.
  • String.valueOf(3.5) bir değeri dizgiye çevirir: "3.5". Karakter dizisi verilirse karakterleri birleştirir: String.valueOf(new char[]{'h', 'i'}) "hi" döndürür.
  • String.format, printf ile aynı biçim dizgisini kullanır ama sonucu ekrana yazmaz, dizgi biçiminde döndürür.

Tablo 11.5 format'ın üç örneğini iki farklı yerel ayarla verir. Programın tamamı ReplaceTablosu sınıfıdır; bir kez İngilizce (ABD), bir kez Türkçe yerel ayarlı bir Java sanal makinesinde çalıştırılmıştır.

Tablo 11.5: String.format örnekleri. Son iki satırda yerel ayar verilmemiştir; sonuç makinenin ayarına bağlıdır.
Çağrıen_US makinedetr_TR makinede
String.format(Locale.US, "%.2f", 3.14159)3.143.14
String.format(Locale.US, "%5d|%-5d|%05d", 42, 42, 42)␣␣␣42|42␣␣␣|00042␣␣␣42|42␣␣␣|00042
String.format(Locale.US, "%,d", 1234567)1,234,5671,234,567
String.format("%.2f", 3.14159)3.143,14
String.format("%,d", 1234567)1,234,5671.234.567

Tablodaki ␣ işareti, sonuçtaki boşlukları görünür kılmak içindir. %5d sayıyı 5 karakterlik alana sağa yaslar; 42 iki karakter olduğu için önüne üç boşluk gelir. %-5d sola yaslar ve boşlukları sona koyar. %05d boşluk yerine sıfırla doldurur. %,d binlik ayırıcı ekler. %.2f sayıyı virgülden sonra iki basamağa yuvarlar.

Locale.US verilen satırlar her makinede aynı sonucu verir. Yerel ayar verilmeyen son iki satır ise makinenin diline uyar: Türkçe ayarlı bilgisayarda ondalık ayırıcı virgül, binlik ayırıcı noktadır. Bir dosyaya ya da başka bir programa gidecek sayılarda yerel ayar açıkça verilir; ekranda kullanıcıya gösterilecek sayılarda makinenin ayarı uygun olabilir.

11.4.6Alt dizgi sınırları

substring en sık hata yapılan metottur. Kuralı akılda tutmanın kolay bir yolu, indisleri karakterlerin üzerine değil aralarındaki çizgilere yazmaktır. Şekil 11.10'de "PROGRAM" dizgisinin yedi karakteri arasında 0'dan 7'ye sekiz çizgi vardır. substring(a, b), a çizgisiyle b çizgisi arasında kalan karakterleri alır.

PROGRAM01234567s.substring(0, 3) → "PRO" PROGRAM01234567s.substring(3, 7) → "GRAM" PROGRAM01234567s.substring(2, 2) → "" PROGRAM01234567✗ 9 yoks.substring(5, 9) → StringIndexOutOfBoundsException
Şekil 11.10: s = "PROGRAM" üzerinde dört çağrı. 0 ile 3 çizgileri arası “PRO”dur. Bitiş length() = 7 olabilir: 3 ile 7 arası “GRAM”dır. Başlangıç bitişe eşitse arada karakter yoktur: “”. 9 numaralı çizgi olmadığı için substring(5, 9) hata verir.
Tanım 11.6

Alt dizgi (substring), bir dizginin ardışık karakterlerinden oluşan parçasıdır. s.substring(a, b), a indisindeki karakterden başlayıp b indisindeki karakterden önce biten alt dizgiyi döndürür; uzunluğu b − a'dır. Geçerli aralık 0 ≤ a ≤ b ≤ s.length()'tir. s.substring(a) ise a indisinden dizginin sonuna kadar olan parçayı verir ve s.substring(a, s.length()) ile aynıdır.

public class AltDizgi {
    public static void main(String[] args) {
        String s = "PROGRAM";
        System.out.println(s.substring(0, 3));
        System.out.println(s.substring(3, 7));
        System.out.println(s.substring(3));
        System.out.println("[" + s.substring(2, 2) + "]");
        System.out.println("[" + s.substring(7) + "]");
        System.out.println(s.substring(0, 3).length() + " " + s.substring(2, 5));
        System.out.println(s.substring(5, 9));
    }
}
PRO
GRAM
GRAM
[]
[]
3 OGR
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [5, 9) out of bounds for length 7

Çıktı her kuralı doğrular. substring(0, 3) üç karakter alır: 3 − 0 = 3. substring(3, 7) ve substring(3) aynı sonucu verir, çünkü dizginin uzunluğu 7'dir. substring(2, 2) boş dizgidir; bu bir hata değildir, “sıfır karakter al” demektir. substring(7) da boş dizgidir: 7 son çizgidir ve ondan sonra karakter yoktur. Son satırdaki substring(5, 9) ise bitiş uzunluğu aştığı için istisna fırlatır. İleti, [5, 9) aralığının 7 uzunluklu dizgi için geçersiz olduğunu söyler. Köşeli parantez başlangıcın dahil, yuvarlak parantez bitişin hariç olduğunu gösterir; matematikteki yarı açık aralık gösterimidir.

Başlangıç bitişten büyükse de istisna fırlatılır:

public class AltDizgiTers {
    public static void main(String[] args) {
        String s = "PROGRAM";
        System.out.println(s.substring(5, 3));
    }
}
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [5, 3) out of bounds for length 7
Tablo 11.6: s = "PROGRAM" (uzunluk 7) için substring çağrıları.
ÇağrıKoşul 0 ≤ a ≤ b ≤ 7SonuçUzunluk
s.substring(0, 3)0 ≤ 0 ≤ 3 ≤ 7 sağlanır"PRO"3
s.substring(2, 5)sağlanır"OGR"3
s.substring(3, 7)sağlanır"GRAM"4
s.substring(2, 2)sağlanır (a = b)""0
s.substring(7)sağlanır (a = 7)""0
s.substring(5, 9)9 ≤ 7 yanlışistisna–
s.substring(5, 3)5 ≤ 3 yanlışistisna–
Sık yapılan hata

substring'in ikinci parametresini “kaç karakter alınacağı” sanmak. "PROGRAM".substring(2, 3), 2 indisinden başlayan üç karakteri değil, yalnızca bir karakteri (“O”) verir. İkinci parametre uzunluk değil bitiş konumudur. k karakter almak için substring(a, a + k) yazılır.

11.4.7Uygulama: e-postayı parçalamak

indexOf ile substring birlikte kullanıldığında bir metin bir ayırıcının iki yanına ayrılabilir. Bir e-posta adresi @ işaretiyle kullanıcı adına ve alan adına ayrılır:

String eposta = "ali@gru.edu";
int at = eposta.indexOf('@');
String kullanici = eposta.substring(0, at);
String alan = eposta.substring(at + 1);
char ilk = eposta.charAt(0);
System.out.println(kullanici + " / " + alan);
System.out.println(Character.toUpperCase(ilk));
ali / gru.edu
A

Program yedi satırda şunu yapar: @'in konumunu bulur, ondan öncesini ve sonrasını ayrı ayrı alır, ilk karakteri okur ve büyük harfe çevirir. Tablo 11.7 izleme tablosudur; Şekil 11.11 iki kesme noktasını gösterir.

Tablo 11.7: E-posta programının izleme tablosu. eposta = "ali@gru.edu" (11 karakter, indisler 0–10).
SatırİşlemDeğişkenÇıktı
2indexOf('@'): @ 3 indisindeat = 3
3substring(0, 3): 0, 1, 2 indislerikullanici = "ali"
4substring(4): 4'ten sona kadaralan = "gru.edu"
5charAt(0)ilk = 'a'
6birleştirmeali / gru.edu
7Character.toUpperCase('a')A
0a1l2i3@4g5r6u7.8e9d10u▲ ateposta 0a1l2i3@4g5r6u7.8e9d10u▲ ateposta
Şekil 11.11: “ali@gru.edu” dizgisinin parçalanması. Üstte substring(0, at): @'in solundaki 0–2 indisleri, “ali”. Altta substring(at + 1): @'den sonraki 4 indisinden sona kadar, “gru.edu”. @ karakterinin kendisi iki parçaya da girmez.

İki ayrıntı dikkat ister. Birincisi, at + 1 yazılmasının nedenidir: substring(at) yazılsaydı alan adı "@gru.edu" olurdu. İkincisi, charAt'in dönüş tipidir. ilk bir char'dır ve büyük harfe çevirmek için String'in değil Character sınıfının toUpperCase metodu kullanılır. Bu metot char alır ve char döndürür.

Programın zayıf noktası, adreste @ bulunmadığı durumdur. O zaman indexOf −1 döndürür ve substring(0, -1) çağrısı istisna fırlatır:

Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [0, -1) out of bounds for length 10

Sağlam bir program önce aramanın sonucunu denetler:

public class EpostaGuvenli {
    public static void main(String[] args) {
        String[] adresler = {"ali@gru.edu", "aligru.edu"};
        for (String eposta : adresler) {
            int at = eposta.indexOf('@');
            if (at < 0) {
                System.out.println(eposta + ": @ yok");
            } else {
                System.out.println(eposta.substring(0, at) + " / " + eposta.substring(at + 1));
            }
        }
    }
}
ali / gru.edu
aligru.edu: @ yok
Örnek 11.4

s = "ece.kaya@uni.tr" adresinden “KAYA, ece” çıktısını üretmek için hangi indisler kullanılır?

Çözüm. Adres 15 karakterdir; indisler 0–14'tür. s.indexOf('@') 8'dir; s.substring(0, 8) "ece.kaya"yı verir. Bu yeni dizgide nokta 3 indisindedir. substring(0, 3) "ece", substring(4) "kaya" verir. "kaya".toUpperCase() yeni bir "KAYA" dizgisi döndürür. Program çalıştırıldığında çıktı şudur:

KAYA, ece

11.5Karşılaştırma

İki dizginin aynı olup olmadığını sormak, programlamanın en sık işlerinden biridir: girilen parola doğru mu, kullanıcı “evet” mi yazdı, iki öğrencinin adı aynı mı? Sayılarda bu soru == ile sorulur. Dizgilerde ise == beklenen soruyu sormaz.

11.5.1== ile equals

Bir String değişkeni bir referans tuttuğu için a == b iki referansı karşılaştırır. Soru “iki değişken aynı nesneyi mi gösteriyor?” olur. İki ayrı nesne aynı karakterleri taşısa bile == false verir. İçeriği karşılaştırmak için equals metodu kullanılır.

Tanım 11.7

Referans karşılaştırması (a == b), iki değişkenin aynı nesneyi gösterip göstermediğini sınar. İçerik karşılaştırması (a.equals(b)), iki dizginin aynı uzunlukta olup aynı konumlarda aynı karakterleri taşıyıp taşımadığını sınar. Dizgilerin eşitliği her zaman equals ile sorulur.

String a = "Giresun";
String b = new String("Giresun");
String c = "Giresun";
System.out.println(a == b);      // false
System.out.println(a == c);      // true
System.out.println(a.equals(b)); // true
false
true
true

Program üç değişken oluşturur ve üç karşılaştırma yapar. Şekil 11.12 belleğin son durumunu, Tablo 11.8 satır satır izlemeyi verir.

YığınHeapDizgi havuzu (sabitler)GiresunGiresunnew ile ayrı nesneabc
Şekil 11.12: Üç satırdan sonra bellek. a ve c, dizgi havuzundaki tek “Giresun” nesnesini gösterir. b, new ile heap'te açılan ayrı bir “Giresun” nesnesini gösterir. İki nesnenin içeriği aynı, yeri farklıdır.
Tablo 11.8: == ile equals programının izleme tablosu.
SatırİşlemSonuçGerekçe
1a = "Giresun"a → havuzsabit havuza konur
2b = new String("Giresun")b → yeni nesnenew her zaman ayrı nesne açar
3c = "Giresun"c → havuzaynı sabit; havuzdaki nesne paylaşılır
4a == bfalseiki ok farklı nesneye gider
5a == ctrueiki ok aynı nesnededir
6a.equals(b)truekarakterler tek tek eşittir: G=G, i=i, …, n=n

5. satırdaki true yanıltıcıdır. a == c'nin doğru çıkması, içeriklerin eşit olmasından değil, iki sabitin havuzda aynı nesneyi paylaşmasındandır. Dizgilerden biri klavyeden okunmuş, bir dosyadan gelmiş ya da bir işlemle üretilmiş olsaydı ayrı bir nesne olurdu ve == false verirdi. Bir dizginin nereden geldiği koda bakınca her zaman anlaşılmaz. Bu yüzden kural basittir: içerik için equals.

Sık yapılan hata

Kullanıcının yazdığı cevabı if (cevap == "evet") ile denetlemek. Scanner'ın okuduğu dizgi her zaman yeni bir nesnedir; kullanıcı “evet” yazsa bile koşul yanlış çıkar. Program derlenir ve hata vermez, yalnızca yanlış çalışır. Doğrusu if (cevap.equals("evet"))'tir.

11.5.2Karşılaştırma metotları

equals dışında dört karşılaştırma metodu daha sık kullanılır. Tablo 11.9 bunları örnekleriyle verir; sonuçlar programın gerçek çıktısıdır.

Tablo 11.9: Karşılaştırma metotları.
MetotAmaçÖrnekSonuç
equalsiçerik eşitliği"abc".equals("abc")true
equalsIgnoreCaseharf büyüklüğünü yok sayan eşitlik"Java".equalsIgnoreCase("JAVA")true
startsWithbelirli metinle başlama"Giresun".startsWith("Gir")true
endsWithbelirli metinle bitme"Giresun".endsWith("sun")true
compareTosözlüksel sıra: negatif, 0 ya da pozitif"elma".compareTo("armut")4
GiresunstartsWith("Gir") → trueendsWith("sun") → trueequals("giresun") → falseequalsIgnoreCase("giresun") → true
Şekil 11.13: “Giresun” dizgisi “Gir” ile başlar ve “sun” ile biter. equals("giresun") false verir, çünkü 'G' ile 'g' farklı karakterlerdir. equalsIgnoreCase("giresun") harf büyüklüğünü yok saydığı için true verir.
public class KarsilastirmaMetotlari {
    public static void main(String[] args) {
        System.out.println("abc".equals("abc"));
        System.out.println("Java".equalsIgnoreCase("JAVA"));
        System.out.println("Giresun".startsWith("Gir"));
        System.out.println("Giresun".endsWith("sun"));
        System.out.println("elma".compareTo("armut"));
        System.out.println("Giresun".equals("giresun"));
        System.out.println("Giresun".equalsIgnoreCase("giresun"));
        System.out.println("Giresun".startsWith("gir"));
    }
}
true
true
true
true
4
false
true
false

Son üç satır büyük-küçük harf duyarlılığını gösterir. equals, startsWith ve endsWith karakterleri olduğu gibi karşılaştırır: "Giresun".startsWith("gir") false'tur. Harf büyüklüğünün önemsiz olduğu yerlerde (bir komutun “EVET”, “Evet” ya da “evet” yazılması gibi) equalsIgnoreCase kullanılır.

11.5.3compareTo ve sözlüksel sıra

Eşitlik dışında iki dizginin sırası da sorulabilir: hangisi sözlükte önce gelir? Bu sıraya sözlüksel sıra (lexicographic order) denir. a.compareTo(b) bir tam sayı döndürür. Sonuç negatifse a önce, sıfırsa ikisi eşit, pozitifse a sonra gelir.

Tanım 11.8

a.compareTo(b) iki dizgiyi baştan başlayarak karakter karakter karşılaştırır. İki durum vardır:

  1. İlk farklı karakter k konumundaysa sonuç a.charAt(k) − b.charAt(k)'dır; yani iki karakterin kod farkıdır.
  2. Kısa olan dizginin bütün karakterleri uzun olanın başıyla aynıysa sonuç a.length() − b.length()'dir; yani uzunluk farkıdır.

İki dizgi tamamen aynıysa ikinci kural 0 verir.

String x = "elma";
String y = "armut";
System.out.println(x.compareTo(y)); // 4
String k = "kedi";
System.out.println(k.compareTo("kediler")); // -3

Sunumdaki iki örnek, iki kuralı ayrı ayrı işletir. Şekil 11.14 her ikisinin kritik adımını gösterir.

  • "elma".compareTo("armut"): 0 konumunda 'e' ile 'a' karşılaşır ve farklıdır. Sonuç 'e' − 'a' = 101 − 97 = 4'tür. Pozitif olduğu için “elma”, “armut”tan sonra gelir. Dizgilerin uzunlukları (4 ve 5) sonuca hiç girmez; ilk farklı karakter kararı verir.
  • "kedi".compareTo("kediler"): ilk dört konumda k=k, e=e, d=d, i=i'dir. Ardından “kedi” biter; farklı karakter bulunamamıştır. Sonuç uzunluk farkıdır: 4 − 7 = −3. Negatif olduğu için kısa olan “kedi” önce gelir.
0e1l2m3ax0a1r2m3u4ty'e' (101) - 'a' (97) = 4 0k1e2d3ik0k1e2d3i4l5e6rarg4 - 7 = -3 (uzunluk farkı)
Şekil 11.14: İki compareTo kuralı. Üstte “elma” ile “armut”: ilk karakterler farklıdır ve sonuç kod farkıdır, 101 − 97 = 4. Altta “kedi” ile “kediler”: ilk dört karakter eşittir ve sonuç uzunluk farkıdır, 4 − 7 = −3.

Aşağıdaki program sunumdaki iki satırın yanında sekiz karşılaştırma daha yapar:

public class CompareTo {
    public static void main(String[] args) {
        String x = "elma";
        String y = "armut";
        System.out.println(x.compareTo(y)); // 4
        String k = "kedi";
        System.out.println(k.compareTo("kediler")); // -3
        System.out.println(y.compareTo(x));
        System.out.println("Ali".compareTo("ali"));
        System.out.println("abc".compareTo("abc"));
        System.out.println("Zeytin".compareTo("armut"));
        System.out.println("çilek".compareTo("dut"));
        System.out.println("ılgın".compareTo("incir"));
        System.out.println("Ali".compareToIgnoreCase("ali"));
        System.out.println("10".compareTo("9"));
    }
}
4
-3
-4
-32
0
-7
131
200
0
-8

Tablo 11.10 her sonucu kurala göre açıklar.

Tablo 11.10: compareTo sonuçlarının hesabı. Kod sütunu ilk farklı karakterlerin Unicode kodlarıdır.
Çağrıİlk farkHesapSonuç
"elma".compareTo("armut")konum 0: e / a101 − 974
"kedi".compareTo("kediler")yok4 − 7−3
"armut".compareTo("elma")konum 0: a / e97 − 101−4
"Ali".compareTo("ali")konum 0: A / a65 − 97−32
"abc".compareTo("abc")yok3 − 30
"Zeytin".compareTo("armut")konum 0: Z / a90 − 97−7
"çilek".compareTo("dut")konum 0: ç / d231 − 100131
"ılgın".compareTo("incir")konum 0: ı / i305 − 105200
"Ali".compareToIgnoreCase("ali")harf büyüklüğü yok sayılır–0
"10".compareTo("9")konum 0: 1 / 949 − 57−8

Tablonun alt yarısı compareTo'nun gerçek sözlük sırasından nerede ayrıldığını gösterir:

  • Büyük harfler küçüklerden önce gelir. Büyük harflerin kodları 65–90, küçük harflerinki 97–122 aralığındadır. Bu yüzden “Zeytin”, “armut”tan önce gelir.
  • Türkçe harfler sona düşer. 'ç' (231), 'ı' (305), 'ş' (351) gibi harflerin kodları bütün İngilizce harflerden büyüktür. “çilek”, “dut”tan sonra; “ılgın”, “incir”den sonra gelir. Türk alfabesinde ise ç, d'den önce; ı, i'den önce gelir.
  • Sayılar metin gibi karşılaştırılır. “10”, “9”dan önce gelir, çünkü ilk karakterler '1' ve '9'dur. Sayıların değerini karşılaştırmak için önce Integer.parseInt ile sayıya çevirmek gerekir.

Türkçe alfabeye uygun sıralama için Java'nın java.text.Collator sınıfı kullanılır. Collator, bir dilin sıralama kurallarını bilir. Aşağıdaki program aynı meyve listesini önce compareTo'nun sırasıyla, sonra Türkçe Collator ile sıralar:

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

public class Siralama {
    public static void main(String[] args) {
        String[] meyveler = {"zeytin", "çilek", "armut", "Elma", "ıhlamur", "incir", "üzüm", "dut"};
        String[] a = meyveler.clone();
        Arrays.sort(a);
        System.out.println(Arrays.toString(a));
        String[] b = meyveler.clone();
        Arrays.sort(b, Collator.getInstance(Locale.forLanguageTag("tr")));
        System.out.println(Arrays.toString(b));
    }
}
[Elma, armut, dut, incir, zeytin, çilek, üzüm, ıhlamur]
[armut, çilek, dut, Elma, ıhlamur, incir, üzüm, zeytin]

İlk satırda büyük harfle başlayan “Elma” en başa geçmiş, Türkçe harfle başlayan “çilek”, “üzüm” ve “ıhlamur” en sona düşmüştür. İkinci satır Türk alfabesinin sırasıdır: a, ç, d, e, ı, i, ü, z. Collator ayrıca büyük-küçük harf farkını ikinci planda tutar; “Elma”, e harfinin yerine yerleşir.

Örnek 11.5

"kalem".compareTo("kale"), "kale".compareTo("kalp") ve "Kale".compareTo("kale") değerleri nedir?

Çözüm. İlk çağrıda “kale”, “kalem”in başıdır; farklı karakter yoktur. Sonuç uzunluk farkıdır: 5 − 4 = 1. İkinci çağrıda ilk üç karakter eşittir; 3 konumunda 'e' ile 'p' karşılaşır. Sonuç 101 − 112 = −11'dir. Üçüncü çağrıda ilk karakterler 'K' (75) ve 'k'dir (107). Sonuç 75 − 107 = −32'dir. Aynı harfin büyüğü ile küçüğü arasındaki fark İngilizce harflerin hepsinde 32'dir.

11.5.4Türkçe karakterler ve yerel ayar

Büyük-küçük harf dönüşümü çoğu dilde basittir: a ↔ A, b ↔ B. Türkçede ise noktalı ve noktasız iki ayrı i harfi vardır: i ↔ İ ve ı ↔ I. İngilizcede bu çiftler i ↔ I biçimindedir. Aynı “I” harfinin küçüğü İngilizcede “i”, Türkçede “ı”dır. Java bu farkı yerel ayar (locale) ile çözer. Yerel ayar, bir dilin ve bölgenin yazım kurallarını taşıyan java.util.Locale nesnesidir.

Tanım 11.9

toLowerCase(Locale) ve toUpperCase(Locale), harf dönüşümünü verilen dilin kurallarına göre yapar. Türkçe için yerel ayar Locale.forLanguageTag("tr"), İngilizce için Locale.ENGLISH ile elde edilir. Parametresiz toLowerCase() ve toUpperCase(), programın çalıştığı makinenin varsayılan yerel ayarını kullanır.

import java.util.Locale;

public class TurkceLocale {
    public static void main(String[] args) {
        Locale tr = Locale.forLanguageTag("tr");
        System.out.println("TITLE".toLowerCase(Locale.ENGLISH));
        System.out.println("TITLE".toLowerCase(Locale.forLanguageTag("tr")));
        System.out.println("i".toUpperCase(tr));
        System.out.println("i".toUpperCase(Locale.ENGLISH));
        System.out.println("I".toLowerCase(tr));
        System.out.println("ISPARTA".toLowerCase(tr));
        System.out.println("ısparta".toUpperCase(tr));
        System.out.println("TITLE".toLowerCase(tr).equals("title"));
        System.out.println("title".equalsIgnoreCase("TITLE"));
        System.out.println("istanbul".toUpperCase(tr) + " " + "istanbul".toUpperCase(Locale.ENGLISH));
        System.out.println("İZMİR".toLowerCase(tr) + " " + "İZMİR".toLowerCase(tr).length());
        System.out.println("İZMİR".toLowerCase(Locale.ENGLISH).length() + " " + "İZMİR".length());
        System.out.println("ISPARTA".equalsIgnoreCase("ısparta") + " " + "i".equalsIgnoreCase("İ"));
    }
}
title
tıtle
İ
I
ı
ısparta
ISPARTA
false
true
İSTANBUL ISTANBUL
izmir 5
7 5
true true

Tablo 11.11 ilk yedi çağrının sonuçlarını iki dilin kurallarıyla açıklar.

Tablo 11.11: Türkçe ve İngilizce harf dönüşümü. tr = Locale.forLanguageTag("tr").
ÇağrıSonuçKural
"TITLE".toLowerCase(Locale.ENGLISH)titleİngilizce: I → i
"TITLE".toLowerCase(Locale.forLanguageTag("tr"))tıtleTürkçe: I → ı
"i".toUpperCase(tr)İTürkçe: i → İ
"i".toUpperCase(Locale.ENGLISH)Iİngilizce: i → I
"I".toLowerCase(tr)ıTürkçe: I → ı
"ISPARTA".toLowerCase(tr)ıspartaTürkçe: I → ı
"ısparta".toUpperCase(tr)ISPARTATürkçe: ı → I

Sonraki satırlar dönüşümün eşitlik sınamalarına etkisini gösterir. "TITLE".toLowerCase(tr) “tıtle” verdiği için "title"'a eşit değildir. equalsIgnoreCase ise karakterleri tek tek, dilden bağımsız bir kuralla karşılaştırır ve true verir. “istanbul” Türkçe kurallarla “İSTANBUL”, İngilizce kurallarla “ISTANBUL” olur. “İZMİR”in Türkçe küçüğü beş harfli “izmir”dir. İngilizce kurallarla küçültüldüğünde ise her “İ”, bir “i” ve onun üstüne eklenen ayrı bir nokta işaretine dönüşür. Sonuç ekranda “izmir”e benzer ama uzunluğu 7'dir. Bu metin “izmir” ile equals sınamasından geçmez.

En sinsi sorun parametresiz çağrılardadır. Aşağıdaki program aynı kodu iki farklı makinede çalıştırır: biri İngilizce (ABD), öteki Türkçe yerel ayarlıdır.

import java.util.Locale;

public class VarsayilanDil {
    public static void main(String[] args) {
        System.out.println(Locale.getDefault());
        String dosya = "ARSIV.ZIP".toLowerCase();
        System.out.println(dosya);
        System.out.println(dosya.endsWith(".zip"));
        System.out.println("title".toUpperCase());
        System.out.println("ARSIV.ZIP".toLowerCase(Locale.ENGLISH).endsWith(".zip"));
        System.out.printf("%.2f%n", 17.5);
    }
}

İngilizce ayarlı makinede çıktı şudur:

en_US
arsiv.zip
true
TITLE
true
17.50

Türkçe ayarlı makinede aynı program şunu yazar:

tr_TR
arsıv.zıp
false
TİTLE
true
17,50

Program bir dosya adının “.zip” ile bitip bitmediğini denetliyor. İngilizce makinede “ARSIV.ZIP” küçültülünce “arsiv.zip” olur ve denetim geçer. Türkçe makinede aynı satır “arsıv.zıp” üretir ve denetim başarısız olur. Kodda hiçbir değişiklik yoktur; yalnızca bilgisayarın dili farklıdır. "title".toUpperCase() da Türkçe makinede “TİTLE” verir. Yerel ayarı açıkça veren beşinci satır ise iki makinede de true yazar. Son satırdaki printf da yerel ayardan etkilenir: ondalık ayırıcı bir makinede nokta, ötekinde virgüldür.

Sık yapılan hata

Anahtar kelime, komut adı, dosya uzantısı, e-posta adresi gibi teknik metinleri parametresiz toLowerCase() ya da toUpperCase() ile dönüştürmek. Program geliştiricinin bilgisayarında doğru çalışır, Türkçe ayarlı bir bilgisayarda bozulur. Bu hataya yazılım dünyasında “Türk i'si sorunu” adı verilir. Teknik metinde Locale.ENGLISH (ya da dilden bağımsız Locale.ROOT), Türkçe metinde Locale.forLanguageTag("tr") açıkça verilir.

Eski kaynaklarda Türkçe yerel ayar new Locale("tr") biçiminde de oluşturulur. Bu yapıcı JDK 19'dan beri kullanımdan kaldırılmıştır (deprecated); JDK 21 derleyicisi -Xlint seçeneğiyle bu satır için uyarı verir. Yerine Locale.forLanguageTag("tr") ya da Locale.of("tr") kullanılır.

Örnek 11.6

Bir program kullanıcının yazdığı il adını büyük harfe çevirip "IĞDIR" ile karşılaştırıyor. Kullanıcı “ığdır” yazıyor. toUpperCase(Locale.ENGLISH) ve toUpperCase(tr) ile sonuç ne olur?

Çözüm. Noktasız “ı”nın büyüğü iki dilde de “I”dır; İngilizce kurallar da bu Türkçe harfi tanır. Her iki çağrı “IĞDIR” verir ve karşılaştırma geçer. Kullanıcı “iğdir” yazsaydı durum değişirdi: Türkçe kurallarla “İĞDİR”, İngilizce kurallarla “IĞDIR” çıkardı. Türkçe bir metinde doğru olan Türkçe kurallardır; bu durumda “iğdir”in yanlış yazıldığı ortaya çıkar.

11.6Bellek: dizgi havuzu

Bir programda aynı dizgi sabiti pek çok yerde geçebilir: “Giresun”, “evet”, boş dizgi. Her biri için ayrı nesne açmak belleği boşa harcar. Java bu yüzden sabitleri ortak bir havuzda toplar.

11.6.1Havuz ve new

Tanım 11.10

Dizgi havuzu (string pool), Java sanal makinesinin dizgi sabitlerini sakladığı bellek bölgesidir. Aynı içerikli bütün sabitler havuzdaki tek bir nesneyi paylaşır. new String(...) ise havuza bakmaz ve her çağrıda heap'te yeni bir nesne oluşturur.

Paylaşım ancak dizgiler değiştirilemez olduğu için güvenlidir. Değiştirilebilseydi, bir değişken üzerinden yapılan değişiklik aynı sabiti kullanan bütün değişkenleri etkilerdi. Şekil 11.15 üç değişkenin durumunu gösterir.

YığınHeapDizgi havuzu (sabitler)GiresunpaylaşılırGiresunnewacb
Şekil 11.15: Dizgi havuzu. a ve c aynı sabitle oluşturulduğu için havuzdaki tek “Giresun” nesnesini paylaşır. b, new ile oluşturulduğu için heap'te ayrı bir nesneyi gösterir.
public class Havuz {
    public static void main(String[] args) {
        String a = "Giresun";
        String c = "Giresun";
        String b = new String("Giresun");
        String d = new String("Giresun");
        System.out.println((a == c) + " " + (a == b) + " " + (b == d));
        System.out.println(a.equals(b) + " " + b.equals(d));
        System.out.println((b.intern() == a) + " " + (b.intern() == b));
    }
}
true false false
true true
true false

İlk satır üç referans karşılaştırması yapar. a == c doğrudur: ikisi de havuzdaki nesneyi gösterir. a == b yanlıştır: b heap'te ayrı bir nesnedir. b == d de yanlıştır: iki new çağrısı iki ayrı nesne üretir. İkinci satırdaki equals sınamaları ise hepsi doğrudur, çünkü içerikler aynıdır.

11.6.2Sabit birleştirme ve intern

Bir dizginin havuzdan gelip gelmediği her zaman koddan kolayca okunmaz. Aşağıdaki program bunun iki ince durumunu gösterir:

String a = "a", b = "b";
String r = a + b;
final String f = "a";
System.out.println(("a" + "b") == "ab");  // true
System.out.println((f + "b") == "ab");    // true
System.out.println(r == "ab");            // false
System.out.println(r.equals("ab"));       // true
System.out.println(r.intern() == "ab");   // true
true
true
false
true
true

Beş sonuç şöyle açıklanır:

  1. ("a" + "b") == "ab" doğrudur. İki işlenen de sabit olduğu için birleştirmeyi program çalışmadan önce derleyici yapar. Derlenmiş kodda "a" + "b" yerine doğrudan "ab" sabiti bulunur ve bu sabit havuzdan gelir.
  2. (f + "b") == "ab" de doğrudur. f, final ile bildirilmiş ve bir sabitle başlatılmıştır. Değeri hiç değişemeyeceği için derleyici onu da bir sabit gibi işler.
  3. r == "ab" yanlıştır. r = a + b satırındaki a ve b sıradan değişkenlerdir. Birleştirme çalışma zamanında yapılır ve sonuç heap'te yeni bir nesnedir.
  4. r.equals("ab") doğrudur; içerikler aynıdır.
  5. r.intern() == "ab" doğrudur. intern() metodu havuza bakar: aynı içerikte bir dizgi varsa onu, yoksa dizgiyi havuza ekleyip onu döndürür. Dönen referans havuzdaki "ab"'dir.

Derleme sırasında değeri belli olan ifadelere sabit ifade (constant expression) denir: dizgi sabitleri, final sabitler ve bunların birleşimleri. Sabit ifadelerin sonucu havuza konur. Değişken içeren her birleştirme ise yeni bir nesne üretir.

intern'ün etkisi Havuz programının son satırında da görülür. b.intern() == a doğrudur: b'nin içeriği havuzda vardır ve intern havuzdaki nesneyi döndürür. b.intern() == b ise yanlıştır: b'nin kendisi havuz dışındaki nesnedir. Uygulamada intern'e nadiren gerek duyulur; burada havuzun işleyişini görünür kılmak için kullanılmıştır.

Sık yapılan hata

Birkaç denemede =='nin doğru sonuç verdiğini görüp dizgileri == ile karşılaştırmaya devam etmek. Denemelerde kullanılan sabitler havuzda paylaşıldığı için == tesadüfen doğru çıkar. Aynı kod, metin klavyeden ya da bir dosyadan geldiğinde yanlış sonuç verir. =='nin doğru çıkması içeriklerin eşit olduğunu değil, nesnenin aynı olduğunu gösterir.

11.6.3null güvenli karşılaştırma

equals bir metottur ve bir nesne üzerinden çağrılır. Değişken null ise çağrı yapılamaz ve NullPointerException oluşur. Aynı karşılaştırmayı ters yönde yazmak bu sorunu ortadan kaldırır:

public class NullGuvenli {
    public static void main(String[] args) {
        String x = null;
        System.out.println("sabit".equals(x));
        System.out.println(x.equals("sabit"));
    }
}
false
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "x" is null

"sabit".equals(x) satırında metot gerçek bir nesne olan "sabit" üzerinden çağrılır. Parametre null olduğunda equals hata vermez, yalnızca false döndürür. x.equals("sabit") satırında ise metot null üzerinden çağrılmaya çalışılır ve program durur. Bu yüzden sabit değer sola yazılır: "evet".equals(cevap), cevap null olsa bile güvenlidir.

11.7Scanner ile metin girdisi

Klavyeden metin okumak için java.util.Scanner sınıfı kullanılır. Sınıfın iki metin okuma metodu vardır: next() bir kelime, nextLine() bir satır okur. İki metodun davranışını anlamanın anahtarı, klavyeden gelen karakterlerin beklediği tampondur (buffer).

11.7.1next ve nextLine

Kullanıcı bir şey yazıp Enter tuşuna bastığında yazdığı karakterler ve satır sonu karakteri tampona girer. Bu notta satır sonu karakteri ⏎ ile gösterilir. Scanner'ın her okuma metodu tamponun başından karakter alır ve okuma konumunu öne kaydırır. Okunan karakterler bir daha okunmaz.

Tanım 11.11

next(), baştaki boşlukları ve satır sonlarını atlar, ardından boşluk olmayan karakterleri ilk boşluğa ya da satır sonuna kadar okur. Bu parçaya kelime (token) denir. Kelimeden sonraki boşluk ya da ⏎ tamponda kalır. nextLine() ise bulunduğu konumdan satır sonuna kadar her şeyi, boşluklar dahil okur. ⏎'ı tüketir ama sonuca eklemez.

import java.util.Scanner;
// ...
Scanner klavye = new Scanner(System.in);
String kelime = klavye.next();
klavye.nextLine();
String satir = klavye.nextLine();

Şekil 11.16 bu kodun, tamponda “Ada⏎Merhaba dünya⏎” varken nasıl çalıştığını özetler. next() “Ada”yı okur ve ⏎'ı bırakır. Aradaki nextLine() yalnızca o ⏎'ı tüketir ve boş dizgi döndürür. Sonraki nextLine() ikinci satırın tamamını okur.

Ada⏎Merhaba dünya⏎① next() → "Ada"③ nextLine() → "Merhaba dünya"② nextLine() → "" (yalnız ⏎ tüketilir)tampon: Ada⏎Merhaba dünya⏎⏎ = satır sonu (Enter)
Şekil 11.16: Tampon “Ada⏎Merhaba dünya⏎” iken üç okuma. ① next() “Ada”yı okur. ② nextLine() yalnızca ⏎'ı tüketir ve "" döndürür. ③ nextLine() “Merhaba dünya”yı okur.

next()'in boşlukta durması bir satırdaki kelimeleri tek tek okumayı kolaylaştırır. Aşağıdaki programa tek satırda “␣␣Ada␣␣␣Can␣Ece␣Su” girilir (␣ boşluğu gösterir):

import java.util.Scanner;

public class NextKelimeler {
    public static void main(String[] args) {
        Scanner klavye = new Scanner(System.in);
        String a = klavye.next();
        String b = klavye.next();
        String c = klavye.nextLine();
        System.out.println("[" + a + "] [" + b + "] [" + c + "]");
    }
}
[Ada] [Can] [ Ece Su]

İlk next() baştaki iki boşluğu atlar ve “Ada”yı okur. İkinci next() aradaki üç boşluğu atlar ve “Can”ı okur. nextLine() ise satırın geri kalanını olduğu gibi alır: başındaki boşluk da sonuca girer ve “␣Ece␣Su” okunur. nextLine() boşluk atlamaz.

11.7.2Tamponu adım adım izlemek

Sunumdaki izleme örneği beş satırlıktır:

Scanner klavye = new Scanner(System.in);
String kelime = klavye.next();
klavye.nextLine();
String satir = klavye.nextLine();
System.out.println(kelime + " | " + satir);

Program bir kez çalıştırılır. Kullanıcı önce “Ada” yazıp Enter'a, sonra “Giresun Merkez” yazıp Enter'a basar. Çıktı şudur:

Ada | Giresun Merkez
Tablo 11.12: Scanner tamponunun izlenmesi. Tampon sütununda okuma konumundan sonraki, henüz okunmamış karakterler gösterilir.
AdımSatırOlayOkunmamış tamponSonuç
11Scanner klavyeye bağlanır(boş)
22next() okuyacak bir şey bulamaz ve bekler; kullanıcı “Ada⏎” girerAda⏎
32next() boşluk ya da ⏎'a kadar okur⏎kelime = "Ada"
43nextLine() ⏎'a kadar okur, ⏎'ı tüketir(boş)"" (atılır)
54nextLine() bekler; kullanıcı “Giresun Merkez⏎” girerGiresun␣Merkez⏎
64nextLine() satırın tamamını okur, ⏎'ı tüketir(boş)satir = "Giresun Merkez"
75yazdırma(boş)Ada | Giresun Merkez
Klavye tamponu (System.in)Ada⏎▲ okuma konumu Klavye tamponu (System.in)Ada⏎▲ okuma konumu Klavye tamponu (System.in)Ada⏎Giresun␣Merkez⏎okuma konumu ▲
Şekil 11.17: Tablodaki 3., 4. ve 6. adımlarda klavye tamponu. Okunmuş karakterler soluk, okuma konumu ▲ ile gösterilir. 3. adımda next() “Ada”yı okumuş, ⏎ tamponda kalmıştır. 4. adımda nextLine() ⏎'ı tüketmiştir. 6. adımda ikinci satır da tamamen okunmuştur.

3. satırdaki klavye.nextLine(); çağrısının sonucu hiçbir değişkene atanmaz. Bu satırın tek görevi tamponda kalan ⏎'ı temizlemektir. Satır silinirse ne olur? 4. satırdaki nextLine() kalan ⏎'ı bulur, hemen boş dizgi döndürür ve kullanıcıya ikinci satırı yazma fırsatı vermez. Aşağıdaki program bu durumu gösterir; girdi yine aynıdır:

import java.util.Scanner;

public class TamponHatasi {
    public static void main(String[] args) {
        Scanner k = new Scanner(System.in);
        String kelime = k.next();
        String satir = k.nextLine();
        System.out.println(kelime + "|" + satir + "|");
    }
}
Ada||

satir boş dizgidir; iki dikey çizginin arasında hiçbir şey yoktur. Kullanıcının yazdığı “Giresun Merkez” hiç okunmamış, tamponda kalmıştır.

11.7.3nextInt sonrası nextLine

Aynı sorun nextInt(), nextDouble() gibi sayı okuyan metotlardan sonra da ortaya çıkar. Bu metotlar da next() gibi davranır: sayıyı okur, satır sonunu tamponda bırakır. Aşağıdaki program önce yaşı, sonra ad soyadı okumak ister. Kullanıcı “19⏎” ve “Ada Yılmaz⏎” girer:

import java.util.Scanner;

public class YasAd {
    public static void main(String[] args) {
        Scanner klavye = new Scanner(System.in);
        System.out.print("Yaş: ");
        int yas = klavye.nextInt();
        System.out.print("Ad soyad: ");
        String ad = klavye.nextLine();
        System.out.println();
        System.out.println("[" + ad + "] " + yas);
    }
}
Yaş: Ad soyad: 
[] 19

“Ad soyad:” sorusu ekrana gelir ama program cevabı beklemez. nextLine(), 19'dan sonra kalan ⏎'ı okur ve ad boş kalır. Çözüm, sayıdan sonra artığı temizleyen bir nextLine() eklemektir:

import java.util.Scanner;

public class YasAdDuzeltme {
    public static void main(String[] args) {
        Scanner klavye = new Scanner(System.in);
        System.out.print("Yaş: ");
        int yas = klavye.nextInt();
        klavye.nextLine();
        System.out.print("Ad soyad: ");
        String ad = klavye.nextLine();
        System.out.println();
        System.out.println("[" + ad + "] " + yas);
    }
}
Yaş: Ad soyad: 
[Ada Yılmaz] 19

Çıktıda “Ad soyad:” yazısından sonra satırın boş göründüğü dikkat çekebilir. Bunun nedeni girdinin bir dosyadan verilmiş olmasıdır: klavyeden çalıştırıldığında kullanıcının yazdığı metin o satırda görünür. System.out.println() bu yüzden programa eklenmiştir.

Sık yapılan hata

nextInt() ile nextLine()'ı art arda kullanıp ikinci okumanın neden “atlandığını” anlayamamak. Okuma atlanmamıştır: nextLine() tamponda kalan boş satırı okumuştur. Kural şudur: next(), nextInt() ya da nextDouble()'dan sonra bir satır okunacaksa araya ⏎'ı tüketen bir nextLine() konur. Başka bir yol, bütün girdiyi nextLine() ile satır satır okuyup sayıları Integer.parseInt ile dönüştürmektir.

11.8Birleştirme ve StringBuilder

+ operatörü birkaç dizgiyi birleştirmek için okunaklı ve yeterlidir. Ancak birleştirme bir döngünün içinde binlerce kez yapılırsa değiştirilemezliğin bedeli belirginleşir.

11.8.1Döngüde + kullanmanın maliyeti

s = s + x satırı s'yi uzatmaz. Her çalıştığında s'nin bütün karakterlerini ve x'i taşıyan yeni bir nesne üretir. Eski nesne çöpe gider. Şekil 11.18 bunu üç turluk bir döngü için gösterir.

String + (döngüde)1␣çöp1␣2␣çöp1␣2␣3␣yeni nesneStringBuilder1␣2␣3␣tek nesne
Şekil 11.18: Üstte + ile birleştirme: her tur “1␣”, “1␣2␣”, “1␣2␣3␣” adlı yeni bir nesne üretir; öncekiler çöp olur. Altta StringBuilder: tek bir nesne büyür.

Kopyalanan karakter sayısı hesaplanabilir. Her turda tek karakter eklenen bir döngü düşünelim. 1. turda 1 karakterlik, 2. turda 2 karakterlik, n. turda n karakterlik yeni bir nesne kurulur. Toplam kopyalanan karakter sayısı şudur:

1 + 2 + 3 + … + n = n(n + 1) / 2

Bu toplam kabaca n²/2'dir. n = 1.000 için 500.500, n = 100.000 için yaklaşık 5 milyar karakter eder. Aşağıdaki program bu sayıları döngünün içinde sayarak doğrular:

public class Maliyet {
    public static void main(String[] args) {
        for (int n : new int[]{1000, 10000, 100000}) {
            long kopya = 0;
            String s = "";
            for (int i = 0; i < n; i++) {
                kopya += s.length() + 1;
                s = s + "x";
            }
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < n; i++) {
                sb.append('x');
            }
            System.out.println(n + " " + kopya + " " + (s.length() == sb.length()));
        }
    }
}
1000 500500 true
10000 50005000 true
100000 5000050000 true

n on katına çıktığında kopyalanan karakter sayısı yaklaşık yüz katına çıkar: 500.500'den 50.005.000'e, oradan 5.000.050.000'e. Bu büyüme hızına karesel büyüme denir ve büyük n değerlerinde program belirgin biçimde yavaşlar. StringBuilder ile yapılan ikinci döngüde ise her karakter tampona bir kez yazılır; toplam iş n ile orantılıdır. Satırın sonundaki true, iki yöntemin aynı uzunlukta dizgi ürettiğini gösterir.

11.8.2StringBuilder ile birleştirme

Tanım 11.12

StringBuilder, karakterleri değiştirilebilir (mutable) bir tamponda tutan sınıftır. append metodu tamponun sonuna ekleme yapar ve yeni nesne üretmez. Tamponun içeriği bittiğinde toString() ile bir String'e dönüştürülür. StringBuilder de java.lang paketindedir.

StringBuilder metin = new StringBuilder();
for (int i = 1; i <= 3; i++) {
    metin.append(i).append(' ');
}
String sonuc = metin.toString();
System.out.println(sonuc + "|");
1 2 3 |

Tablo 11.13 programın on adımını verir. Döngü değişkeni i 1'den 3'e kadar döner ve her turda iki ekleme yapılır: sayı ve ardından bir boşluk.

Tablo 11.13: StringBuilder programının izleme tablosu. ␣ boşluk karakteridir.
AdımSatıriKoşul / işlemmetin (tampon)sonuc
11boş tampon oluşturulur""
2211 <= 3 → true""
331append(1).append(' ')"1␣"
422i++; 2 <= 3 → true"1␣"
532append(2).append(' ')"1␣2␣"
623i++; 3 <= 3 → true"1␣2␣"
733append(3).append(' ')"1␣2␣3␣"
824i++; 4 <= 3 → false, döngü biter"1␣2␣3␣"
95toString()"1␣2␣3␣""1␣2␣3␣"
106yazdırma: sonuc + "|""1␣2␣3␣""1␣2␣3␣"
YığınHeap1␣2␣3␣StringBuilder (değişebilir)metin YığınHeap1␣2␣3␣StringBuilder (değişebilir)1␣2␣3␣Stringmetinsonuc
Şekil 11.19: Üstte 7. adımdan sonra: tek StringBuilder nesnesi “1␣2␣3␣” karakterlerini taşır; döngü boyunca yeni nesne açılmamıştır. Altta 9. adımdan sonra: toString() tamponun o anki içeriğinden ayrı bir String nesnesi üretmiş, sonuc onu gösterir.

3. satırdaki metin.append(i).append(' ') yazımı dikkat çekicidir. append, ekleme yaptığı nesnenin kendisini döndürür. Bu yüzden ikinci append aynı nesne üzerinde çağrılabilir. Bu tür ardışık çağrılara zincirleme denir. Sondaki "|" yalnızca çıktının sonundaki boşluğu görünür kılmak içindir.

append her tipten değer alır: int, char, double, boolean, String. Sayılar metin karşılıklarıyla eklenir. append(1) tampona 1 karakterini ekler; bir toplama yapmaz.

11.8.3StringBuilder'ın öteki metotları

StringBuilder değiştirilebilir olduğu için String'de bulunmayan metotlar da taşır. Bunlar tamponu yerinde değiştirir:

public class SbMetotlar {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Java");
        sb.append(21);
        System.out.println(sb + " " + sb.length());
        sb.insert(0, "JDK ");
        System.out.println(sb);
        sb.setCharAt(4, 'j');
        System.out.println(sb);
        sb.deleteCharAt(sb.length() - 1);
        System.out.println(sb);
        sb.reverse();
        System.out.println(sb);
        StringBuilder t = sb;
        t.append('!');
        System.out.println(sb + " " + (t == sb));
        System.out.println(new StringBuilder("ab").equals(new StringBuilder("ab")) + " "
                + new StringBuilder("ab").toString().equals("ab"));
    }
}
Java21 6
JDK Java21
JDK java21
JDK java2
2avaj KDJ
2avaj KDJ! true
false true
Tablo 11.14: StringBuilder metotları ve programdaki etkileri.
ÇağrıEtkiTampon
new StringBuilder("Java")başlangıç içeriğiJava
append(21)sona eklerJava21 (uzunluk 6)
insert(0, "JDK ")0 konumuna eklerJDK Java21
setCharAt(4, 'j')4 konumundaki karakteri değiştirirJDK java21
deleteCharAt(length() - 1)son karakteri silerJDK java2
reverse()tamponu ters çevirir2avaj KDJ

Programın son iki satırı iki önemli farkı gösterir. StringBuilder t = sb; yeni bir tampon açmaz; t aynı nesneyi gösterir. t.append('!') bu yüzden sb'yi de değiştirir ve t == sb doğrudur. Değiştirilebilir nesnelerde paylaşım bu tür yan etkilere yol açar; String'de böyle bir durum olamaz. İkinci fark karşılaştırmadadır: StringBuilder'ın equals metodu içeriğe değil referansa bakar. Aynı “ab” içerikli iki tampon için equals false verir. İçerik karşılaştırması için önce toString() çağrılır.

Örnek 11.7

StringBuilder sb = new StringBuilder("ab"); ile başlayan ve 1'den 3'e kadar her turda sb.append(i).append('-') yapan bir döngüden sonra String s = sb.toString(); sb.reverse(); satırları çalışıyor. s + " " + sb ne yazar?

Çözüm. Döngü tamponu “ab” → “ab1-” → “ab1-2-” → “ab1-2-3-” yapar. toString() o anki içerikten ayrı bir String üretir: s = “ab1-2-3-”. reverse() yalnızca tamponu ters çevirir: “-3-2-1ba”. s etkilenmez, çünkü ayrı ve değiştirilemez bir nesnedir. Birleştirmede sb için toString() kendiliğinden çağrılır. Program çıktısı:

ab1-2-3- -3-2-1ba

Hangi durumda hangisi seçilir? Birkaç parçayı bir kez birleştirmek için + okunaklıdır ve yeterlidir; Java derleyicisi bu tür tek satırlık birleştirmeleri zaten verimli koda çevirir. Bir döngü içinde ya da sayısı önceden bilinmeyen çok sayıda parçayla metin kurulurken StringBuilder kullanılır.

11.9Uygulamalar

Bu kesimdeki programların hepsi aynı iskeleti paylaşır. Bir döngü dizginin karakterlerini sırayla ya da iki uçtan gezer. Her karakter charAt ile okunur ve bir karar verilir. Programları birbirinden ayıran şey, her adımda yapılan iştir.

11.9.1Dizgiyi ters çevirme

Bir dizgiyi ters çevirmek için karakterler sondan başa doğru okunur ve yeni bir dizgiye eklenir. Sunumdaki program bunu iki yolla yapar: önce elle yazılmış bir döngüyle, sonra StringBuilder'ın hazır metoduyla.

String s = "Java";
String ters = "";
for (int i = s.length() - 1; i >= 0; i--)
  ters += s.charAt(i);
System.out.println(ters);
String t2 = new StringBuilder(s).reverse()
                                .toString();
System.out.println(t2);
avaJ
avaJ

Döngü i = s.length() - 1, yani son indisle başlar ve i >= 0 olduğu sürece i'yi bir azaltır. Her turda s.charAt(i), ters'in sonuna eklenir. Tablo 11.15 dört turu verir.

Tablo 11.15: Ters çevirme döngüsünün izleme tablosu. s = "Java".
ii >= 0s.charAt(i)ters (turdan sonra)
3true'a'"a"
2true'v'"av"
1true'a'"ava"
0true'J'"avaJ"
−1false–döngü biter
0J1a2v3a▲ isters"av" 0J1a2v3a▲ isters"avaJ"
Şekil 11.20: Ters çevirme döngüsü. Üstte i = 2 turu: 'v' eklenmiş, ters “av” olmuştur. Altta i = 0 turu: 'J' de eklenmiş, sonuç “avaJ”dır. s hiç değişmez.

Döngüdeki her ters += s.charAt(i) yeni bir String üretir; dört karakterlik bir dizgide bu önemsizdir. İkinci yol olan new StringBuilder(s).reverse().toString() ise tek satırdır. s'nin içeriğiyle bir tampon açar, tamponu ters çevirir ve sonucu String'e dönüştürür. İki yol aynı çıktıyı verir ve ikisinde de s değişmez.

Bu döngünün iki sınır durumu da doğru çalışır. Boş dizgide i −1 ile başlar, koşul ilk denetimde yanlıştır ve ters boş kalır. Tek karakterli dizgide döngü bir kez döner ve dizginin kendisi elde edilir.

11.9.2Palindrom cümle

Tersten okunuşu düzden okunuşuyla aynı olan kelime ya da cümleye palindrom denir: “kazak”, “ana”, “Ey Edip Adanada pide ye”. Cümlede büyük harfler ve boşluklar bulunduğu için doğrudan karşılaştırma yanlış sonuç verir: 'E' ile 'e' farklı karakterlerdir. Önce metin temizlenir, sonra iki uçtan içeriye doğru karşılaştırılır.

String cumle = "Ey Edip Adanada pide ye";
String t = cumle.toLowerCase().replace(" ", "");
int sol = 0, sag = t.length() - 1;
boolean palindrom = true;
while (sol < sag && palindrom) {
  if (t.charAt(sol) != t.charAt(sag))
    palindrom = false;
  sol++; sag--;
}
System.out.println(palindrom);
true
eyedipadanadapideye 19 9 9

Program şu adımlarla çalışır:

  1. toLowerCase() bütün harfleri küçültür, replace(" ", "") boşlukları siler. 23 karakterlik cümle 19 karakterlik "eyedipadanadapideye" olur.
  2. İki işaretçi değişken kurulur: sol 0'dan, sag son indis olan 18'den başlar. Aksi kanıtlanana kadar palindrom = true kabul edilir.
  3. Her turda iki uçtaki karakter karşılaştırılır. Eşit değillerse palindrom false yapılır ve döngü koşulu bir sonraki denetimde döngüyü bitirir. Eşitlerse iki işaretçi birer adım içeri kayar.
  4. sol < sag koşulu yanlış olunca bütün çiftler denetlenmiş demektir.
Tablo 11.16: Palindrom döngüsünün izleme tablosu. t = "eyedipadanadapideye" (19 karakter).
Tursolsagt.charAt(sol)t.charAt(sag)Eşit mi?
1018eeevet
2117yyevet
3216eeevet
4315ddevet
5414iievet
6513ppevet
7612aaevet
8711ddevet
9810aaevet
–99sol < sag yanlış: döngü biter, palindrom = true
0e1y2e3d4i5p6a7d8a9n10a11d12a13p14i15d16e17y18e▲ sol▲ sagt 0e1y2e3d4i5p6a7d8a9n10a11d12a13p14i15d16e17y18e▲ solsagt
Şekil 11.21: Palindrom denetiminde iki işaretçi. Üstte başlangıç: sol 0'da, sag 18'de. Altta son durum: işaretçiler ortadaki 9 indisinde buluşmuştur. Ortadaki 'n' kendisiyle karşılaştırılmaz.

Programın çıktısındaki ikinci satır, döngüden sonraki değişkenleri gösteren bir denetim satırıdır: temizlenmiş metin, uzunluğu 19 ve işaretçilerin son değerleri 9 ve 9. Uzunluk tek olduğu için ortada eşi olmayan bir karakter kalır. Dokuz tur yeter: ⌊19 / 2⌋ = 9. Genel kural şudur: n karakterlik bir metin için en çok n/2 tur gerekir.

Aynı mantık bir metot içine alınıp birkaç cümleyle denenebilir. Bu sürümde küçültme Türkçe kurallarla yapılır; böylece “I” ve “İ” içeren cümleler de doğru işlenir:

import java.util.Locale;

public class PalindromDeneme {
    static boolean palindromMu(String cumle) {
        String t = cumle.toLowerCase(Locale.forLanguageTag("tr")).replace(" ", "");
        int sol = 0, sag = t.length() - 1;
        boolean palindrom = true;
        while (sol < sag && palindrom) {
            if (t.charAt(sol) != t.charAt(sag))
                palindrom = false;
            sol++; sag--;
        }
        return palindrom;
    }

    public static void main(String[] args) {
        String[] cumleler = {"Ey Edip Adanada pide ye", "Kaç kişi", "Ana", "Kazak", "Ali"};
        for (String c : cumleler) {
            System.out.println(c + " -> " + palindromMu(c));
        }
    }
}
Ey Edip Adanada pide ye -> true
Kaç kişi -> false
Ana -> true
Kazak -> true
Ali -> false

“Kaç kişi” palindrom değildir: temizlenmiş hâli “kaçkişi”, tersi “işikçak”tır. İlk turda 'k' ile 'i' karşılaşır ve döngü orada biter. “Ali” de ilk turda elenir: 'a' ≠ 'i'.

11.9.3Sesli harf sayma

Bir karakterin belirli bir kümeye ait olup olmadığını anlamanın kısa bir yolu, kümeyi bir dizgide toplayıp indexOf ile aramaktır. Karakter kümede varsa indexOf 0 ya da daha büyük bir değer, yoksa −1 döndürür.

String s = "Merhaba Dünya".toLowerCase();
String sesliler = "aeıioöuü";
int sayac = 0;
for (int i = 0; i < s.length(); i++) {
  char c = s.charAt(i);
  if (sesliler.indexOf(c) >= 0) sayac++;
}
System.out.println(sayac);
5

“Merhaba Dünya” önce küçültülür: “merhaba dünya”. sesliler dizgisi Türkçenin sekiz sesli harfini tutar: a, e, ı, i, o, ö, u, ü. Döngü on üç karakteri sırayla dener. Tablo 11.17 her karakter için indexOf'un sonucunu verir.

Tablo 11.17: Sesli harf sayımının izleme tablosu. sesliler = "aeıioöuü"; ␣ boşluktur.
icsesliler.indexOf(c)Sesli mi?sayac
0m−1hayır0
1e1evet1
2r−1hayır1
3h−1hayır1
4a0evet2
5b−1hayır2
6a0evet3
7␣−1hayır3
8d−1hayır3
9ü7evet4
10n−1hayır4
11y−1hayır4
12a0evet5
0m1e2r3h4a5b6a7␣8d9ü10n11y12as✓✓✓✓sayac4 0m1e2r3h4a5b6a7␣8d9ü10n11y12as✓✓✓✓✓sayac5
Şekil 11.22: Sesli harf taraması. Üstte i = 9: 'ü', sesliler dizgisinin 7 indisinde bulunur ve sayaç 4 olur. Altta tarama bitmiştir: işaretli beş hücre (e, a, a, ü, a) sesli harftir.

Programın başındaki toLowerCase() çağrısı burada önemlidir. Küçültme yapılmasaydı büyük “A”, “E” gibi harfler sesliler dizgisinde bulunamazdı. Ancak parametresiz küçültme makinenin diline bağlıdır. Metinde büyük “I” ya da “İ” varsa Türkçe olmayan bir makinede yanlış küçük harfe dönüşebilir. Türkçe metin için toLowerCase(Locale.forLanguageTag("tr")) güvenli yoldur.

Örnek 11.8

Aynı program “Giresun Üniversitesi” için kaç sayar? Sayılan harfler hangileridir?

Çözüm. Küçültülmüş metin “giresun üniversitesi”dir. Sesliler sırayla i, e, u (giresun: üç), ü, i, e, i, e, i (üniversitesi: altı) olur. Toplam 3 + 6 = 9'dur. Boşluk ve ünsüzler için indexOf −1 döndürür ve sayaç artmaz.

11.9.4Kelime sayma

Bir cümledeki kelime sayısı, boşlukları sayıp bir eklemekle bulunamaz. Cümlenin başında, sonunda ya da kelimeler arasında birden çok boşluk olabilir. Daha sağlam bir yol, “kelime dışından kelime içine” geçişleri saymaktır.

Tanım 11.13

Bu programda kelime, boşluk olmayan karakterlerin kesintisiz dizisidir. Bir metindeki kelime sayısı, boşluk olmayan bir karakterden hemen önce boşluk ya da metnin başı bulunan konumların sayısına eşittir.

String metin = "Ali  ata bak";
int kelime = 0;
boolean icinde = false;
for (int i = 0; i < metin.length(); i++) {
  char c = metin.charAt(i);
  if (c != ' ' && !icinde) {
    kelime++;
    icinde = true;
  } else if (c == ' ') icinde = false;
}
System.out.println(kelime);
3

Program bir boolean bayrak tutar: icinde, o anda bir kelimenin içinde olunup olunmadığını gösterir. Boşluk olmayan bir karakter gelir ve bayrak false ise yeni bir kelime başlamıştır: sayaç artar ve bayrak true yapılır. Boşluk gelirse bayrak false yapılır. Boşluk olmayan karakter geldiğinde bayrak zaten true ise hiçbir şey yapılmaz; aynı kelime sürer.

Tablo 11.18: Kelime sayma döngüsünün izleme tablosu. metin = "Ali␣␣ata␣bak" (12 karakter).
icicinde (önce)Olaykelimeicinde (sonra)
0Afalseyeni kelime1true
1ltruedevam1true
2itruedevam1true
3␣truekelime biter1false
4␣falseboşluk, sayılmaz1false
5afalseyeni kelime2true
6ttruedevam2true
7atruedevam2true
8␣truekelime biter2false
9bfalseyeni kelime3true
10atruedevam3true
11ktruedevam3true
0A1l2i3␣4␣5a6t7a8␣9b10a11kmetin▲ yeni▲ yeni▲ yenikelime3
Şekil 11.23: “Ali␣␣ata␣bak” metninde üç kelime başlangıcı. ▲ işaretleri, boşluktan harfe geçilen 0, 5 ve 9 indislerini gösterir. Çift boşluk (3 ve 4 indisleri) ikinci bir kelime başlatmaz.

Aynı sonuç split ile tek satırda da bulunabilir: metin.trim().split("\\s+").length. trim() baştaki ve sondaki boşlukları atar; "\\s+" kalıbı “bir ya da daha fazla boşluk karakteri” demektir ve art arda boşlukları tek ayırıcı sayar. Bu kısa yolun bir tuzağı vardır: boş ya da yalnızca boşluktan oluşan metinde sonuç 0 değil 1'dir. "".split(...) bir parçalı dizi döndürür ve o parça boş dizgidir. Aşağıdaki program beş metin için üç yöntemi karşılaştırır:

public class KelimeKisaYol {
    public static void main(String[] args) {
        String[] metinler = {"Ali  ata bak", "  Ali ata  bak ", "", "   ", "tek"};
        for (String metin : metinler) {
            int k1 = metin.trim().split("\\s+").length;
            int k2 = metin.isBlank() ? 0 : metin.trim().split("\\s+").length;
            System.out.println("[" + metin + "] " + k1 + " " + k2 + " " + metin.split(" ").length);
        }
    }
}
[Ali  ata bak] 3 3 4
[  Ali ata  bak ] 3 3 6
[] 1 0 1
[   ] 1 0 0
[tek] 1 1 1

Her satırda sırasıyla kısa yolun sonucu, isBlank() denetimli sonucu ve split(" ")'nin sonucu yazılır. İlk iki sütun boş metinler dışında aynıdır. split(" ") ise her tek boşluğu ayırıcı saydığı için çift boşluklarda boş parçalar üretir: “Ali␣␣ata␣bak” için 3 yerine 4 verir. Bu yüzden kısa yol, önce isBlank() ile denetlenerek kullanılır.

11.9.5Karakter frekansı

Bir metinde her harfin kaç kez geçtiğini saymak için 26 hücreli bir int dizisi kullanılır. Hücre 0 'a' harfinin, hücre 1 'b' harfinin, hücre 25 'z' harfinin sayacıdır. Bir harfin hücresi, char'ların sayı olmasından yararlanılarak c - 'a' ile bulunur.

String s = "kavak";
int[] frekans = new int[26];
for (int i = 0; i < s.length(); i++) {
  char c = s.charAt(i);
  frekans[c - 'a']++;
}
for (int k = 0; k < 26; k++)
  if (frekans[k] > 0)
    System.out.println((char) ('a' + k)
                       + ": " + frekans[k]);
a: 2
k: 2
v: 1
Tablo 11.19: “kavak” için frekans dizisinin güncellenmesi. Yalnızca değişen hücreler gösterilmiştir.
icc − 'a'Güncellenen hücrefrekans[0]frekans[10]frekans[21]
0k107 − 97 = 10frekans[10]010
1a97 − 97 = 0frekans[0]110
2v118 − 97 = 21frekans[21]111
3a97 − 97 = 0frekans[0]211
4k107 − 97 = 10frekans[10]221
skavaka2b0c0d0e0f0g0h0i0j0k2l0m0n0o0p0q0r0s0t0u0v1w0x0y0z0frekans[0] = 'a' … frekans[25] = 'z'
Şekil 11.24: “kavak” tarandıktan sonra 26 hücreli frekans dizisi. Yalnızca 'a' (2), 'k' (2) ve 'v' (1) hücreleri sıfırdan farklıdır.

İkinci döngü 26 hücreyi sırayla gezer ve yalnızca sıfırdan büyük olanları yazar. Hücre numarasından harfe geri dönmek için (char) ('a' + k) yazılır: 'a' + 10 = 97 + 10 = 107, yani 'k'. Dönüşüm ((char)) gereklidir; yoksa 'a' + k bir int olur ve ekrana 107 yazılır. Çıktının alfabe sırasında olması da bu ikinci döngüden gelir: hücreler 0'dan 25'e gezilir.

Programın iki maliyeti ayrı ayrı hesaplanabilir. İlk döngü n karakterlik metni bir kez tarar; ikinci döngü her zaman 26 tur döner. Toplam iş n + 26 ile orantılıdır. Her harf için metni baştan tarayan bir çözüm ise 26 · n karşılaştırma yapardı.

Sık yapılan hata

Programı büyük harf, boşluk ya da Türkçe harf içeren bir metinle çalıştırmak. 26 hücreli tablo yalnızca küçük İngilizce harfler içindir. 'ç' için 'ç' − 'a' = 231 − 97 = 134 çıkar ve dizinin sınırı aşılır:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 134 out of bounds for length 26

Büyük harfler önce küçültülür, harf olmayan karakterler if (c >= 'a' && c <= 'z') ile atlanır. Türkçe harfler için ya 29 harflik bir alfabe dizgisi ve indexOf kullanılır ya da daha geniş bir tablo kurulur.

11.9.6Sezar şifresi

Sezar şifresi (Caesar cipher), her harfi alfabede sabit sayıda konum ileri kaydıran çok eski bir şifreleme yöntemidir. Kaydırma 3 ise 'a' → 'd', 'b' → 'e' olur. Alfabenin sonundaki harfler başa sarar: 'x' → 'a', 'y' → 'b', 'z' → 'c'.

Başa sarmanın aritmetiği mod işlemidir. Harfin alfabedeki yeri c - 'a' ile 0–25 arasında bir sayıya çevrilir. Kaydırma eklenir ve sonucun 26'ya bölümünden kalan alınır:

yer = (c − 'a' + kaydir) % 26

Kalan her zaman 0 ile 25 arasındadır; bu yüzden sonuç alfabenin dışına taşmaz. Ardından 'a' + yer ile yeniden harfe dönülür.

String metin = "yazilim";
int kaydir = 3;
StringBuilder sifreli = new StringBuilder();
for (int i = 0; i < metin.length(); i++) {
  char c = metin.charAt(i);
  int yer = (c - 'a' + kaydir) % 26;
  char yeni = (char) ('a' + yer);
  sifreli.append(yeni);
}
System.out.println(sifreli);
bdclolp
Tablo 11.20: “yazilim” kelimesinin 3 kaydırmayla şifrelenmesi.
icc − 'a'(c − 'a' + 3) % 26yenisifreli
0y2427 % 26 = 1bb
1a03 % 26 = 3dbd
2z2528 % 26 = 2cbdc
3i811 % 26 = 11lbdcl
4l1114 % 26 = 14obdclo
5i811 % 26 = 11lbdclol
6m1215 % 26 = 15pbdclolp
metinsifreli0y1a2z3i4l5i6mbdc+3a b c … x y zy + 3 → b (başa sarar)
Şekil 11.25: 'z' harfinin şifrelenmesi. 25 + 3 = 28 alfabenin dışındadır; 28 % 26 = 2 başa sarar ve 'c' elde edilir. sifreli tamponunda o ana kadar “bdc” birikmiştir.

Şifreyi çözmek için harfler aynı miktarda geri kaydırılır. Ancak (c - 'a' - 3) % 26 yazmak tehlikelidir: Java'da negatif bir sayının kalanı negatiftir ve (0 − 3) % 26 = −3 çıkar. Geri kaydırma yerine 26 − 3 = 23 ileri kaydırma yapılır; alfabede 23 ileri gitmek 3 geri gitmekle aynıdır ve sonuç hep pozitif kalır. Aşağıdaki program şifreleme işini bir metoda alır ve aynı metotla şifreyi çözer:

public class SezarCoz {
    static String kaydir(String metin, int kaydir) {
        StringBuilder sonuc = new StringBuilder();
        for (int i = 0; i < metin.length(); i++) {
            char c = metin.charAt(i);
            int yer = (c - 'a' + kaydir) % 26;
            sonuc.append((char) ('a' + yer));
        }
        return sonuc.toString();
    }

    public static void main(String[] args) {
        String sifreli = kaydir("yazilim", 3);
        System.out.println(sifreli);
        System.out.println(kaydir(sifreli, 26 - 3));
        System.out.println(kaydir("kaz", 2));
        System.out.println((-3) % 26);
    }
}
bdclolp
yazilim
mcb
-3

İkinci satır çözülmüş metindir: “yazilim”. Üçüncü satır “kaz” kelimesinin 2 kaydırmayla şifresidir: 'k' (10) → 12 → 'm', 'a' → 'c', 'z' (25) → 27 % 26 = 1 → 'b'. Son satır negatif kalanın Java'daki değerini doğrular. Sezar şifresinin yalnızca 25 anlamlı anahtarı vardır; bilgisayar hepsini bir anda dener. Bu yüzden yöntem güvenlik için değil, mod aritmetiğini ve karakter işlemeyi öğretmek için kullanılır.

11.9.7Alıştırmalardan çözümlü örnekler

Sunumun alıştırmalarından altısı aşağıda çözülmüştür. Her çözüm, notta anlatılan araçlardan birini ya da birkaçını kullanır.

Örnek 11.9

Bir cümledeki büyük harf, küçük harf, rakam ve boşluk sayılarını ayrı ayrı bulan program.

Çözüm. Her karakter bir else-if zinciriyle sınıflandırılır. Character sınıfının isUpperCase, isLowerCase ve isDigit metotları Türkçe harfleri de tanır; bu yüzden “Ö” büyük, “ı” küçük harf sayılır.

public class KarakterTurleri {
    public static void main(String[] args) {
        String cumle = "Giresun 2024 Yılı Öğrenci";
        int buyuk = 0, kucuk = 0, rakam = 0, bosluk = 0, diger = 0;
        for (int i = 0; i < cumle.length(); i++) {
            char c = cumle.charAt(i);
            if (Character.isUpperCase(c)) buyuk++;
            else if (Character.isLowerCase(c)) kucuk++;
            else if (Character.isDigit(c)) rakam++;
            else if (c == ' ') bosluk++;
            else diger++;
        }
        System.out.println("Büyük: " + buyuk + ", küçük: " + kucuk + ", rakam: " + rakam
                + ", boşluk: " + bosluk + ", diğer: " + diger);
    }
}
Büyük: 3, küçük: 15, rakam: 4, boşluk: 3, diğer: 0

“Giresun 2024 Yılı Öğrenci” cümlesinde G, Y ve Ö büyük harftir. Küçük harfler “iresun” (6), “ılı” (3) ve “ğrenci”dir (6): toplam 15. Dört rakam ve üç boşluk vardır. Toplam 3 + 15 + 4 + 3 = 25, cümlenin uzunluğuna eşittir.

Örnek 11.10

İki kelimenin anagram olup olmadığını (aynı harflerin yeniden dizilişi, ör. “elma” – “alem”) karakter frekanslarıyla denetleyen program.

Çözüm. Uzunluklar farklıysa kelimeler anagram olamaz. Uzunluklar aynıysa tek bir frekans dizisi yeter: ilk kelimenin harfleri sayacı artırır, ikincininkiler azaltır. Sonunda bütün hücreler sıfırsa iki kelime aynı harfleri aynı sayıda taşır.

public class Anagram {
    static boolean anagramMi(String a, String b) {
        if (a.length() != b.length()) return false;
        int[] frekans = new int[26];
        for (int i = 0; i < a.length(); i++) {
            frekans[a.charAt(i) - 'a']++;
            frekans[b.charAt(i) - 'a']--;
        }
        for (int k = 0; k < 26; k++)
            if (frekans[k] != 0) return false;
        return true;
    }

    public static void main(String[] args) {
        System.out.println(anagramMi("elma", "alem"));
        System.out.println(anagramMi("kavak", "kavka"));
        System.out.println(anagramMi("kalem", "kelam"));
        System.out.println(anagramMi("kitap", "katip"));
        System.out.println(anagramMi("ders", "dere"));
    }
}
true
true
true
true
false

“ders” ile “dere” aynı uzunluktadır ama 's' hücresi +1, 'e' hücresi −1 kalır; sonuç false'tur. Program yalnızca küçük İngilizce harfler için yazılmıştır; Türkçe harfler için frekans kesimindeki uyarı burada da geçerlidir.

Örnek 11.11

Kullanıcının girdiği ad-soyadı “SOYAD, Ad” biçiminde, soyadı büyük harfle yazdıran program.

Çözüm. Satır nextLine() ile okunur, çünkü ad soyad boşluk içerir. Birden çok adı olan kişiler için soyadı son kelime kabul edilir ve lastIndexOf(' ') ile ayrılır. Büyük harfe çevirme Türkçe kurallarla yapılır.

import java.util.Locale;
import java.util.Scanner;

public class AdSoyad {
    public static void main(String[] args) {
        Scanner klavye = new Scanner(System.in);
        String satir = klavye.nextLine().strip();
        int bosluk = satir.lastIndexOf(' ');
        String ad = satir.substring(0, bosluk).strip();
        String soyad = satir.substring(bosluk + 1);
        System.out.println(soyad.toUpperCase(Locale.forLanguageTag("tr")) + ", " + ad);
    }
}

“Ada Nur Yılmaz” girildiğinde çıktı:

YILMAZ, Ada Nur

“␣␣Mehmet Işık␣” girildiğinde strip() kenar boşluklarını atar ve çıktı şudur:

IŞIK, Mehmet

“Işık”ın büyük harfli hâli Türkçe kurallarla “IŞIK”tır. Program tek kelimelik bir girdide lastIndexOf'un −1 döndürmesi yüzünden hata verir; bu durum ayrıca denetlenebilir.

Örnek 11.12

Bir cümledeki en uzun kelimeyi split ile bulan program.

Çözüm. Cümle "\\s+" kalıbıyla kelimelere bölünür. İlk aday ilk kelimedir; her kelime adayla karşılaştırılır ve daha uzunsa yeni aday olur.

public class EnUzunKelime {
    public static void main(String[] args) {
        String cumle = "Bilgisayar mühendisliği  öğrencileri dizgileri seviyor";
        String[] kelimeler = cumle.trim().split("\\s+");
        String enUzun = kelimeler[0];
        for (String k : kelimeler) {
            if (k.length() > enUzun.length()) enUzun = k;
        }
        System.out.println(enUzun + " " + enUzun.length() + " " + kelimeler.length);
    }
}
mühendisliği 12 5

Cümlede çift boşluk olmasına karşın beş kelime bulunur. “mühendisliği” 12 karakterle en uzunudur. “öğrencileri” 11 karakterdir. Koşul > olduğu için eşit uzunlukta iki kelime varsa önce geleni kalır.

Örnek 11.13

Sezar şifresini büyük harfleri de (A–Z) destekleyecek biçimde genişleten, öteki karakterleri olduğu gibi bırakan program.

Çözüm. Her karakter üç gruba ayrılır. Küçük harfler 'a'ya, büyük harfler 'A'ya göre kaydırılır. Rakam, boşluk ve noktalama değişmeden eklenir.

public class SezarBuyuk {
    static String sifrele(String metin, int kaydir) {
        StringBuilder sonuc = new StringBuilder();
        for (int i = 0; i < metin.length(); i++) {
            char c = metin.charAt(i);
            if (c >= 'a' && c <= 'z') {
                sonuc.append((char) ('a' + (c - 'a' + kaydir) % 26));
            } else if (c >= 'A' && c <= 'Z') {
                sonuc.append((char) ('A' + (c - 'A' + kaydir) % 26));
            } else {
                sonuc.append(c);
            }
        }
        return sonuc.toString();
    }

    public static void main(String[] args) {
        String s = sifrele("Merhaba, Dunya XYZ!", 3);
        System.out.println(s);
        System.out.println(sifrele(s, 26 - 3));
    }
}
Phukded, Gxqbd ABC!
Merhaba, Dunya XYZ!

'M' → 'P', 'X' → 'A', 'Y' → 'B', 'Z' → 'C' olur; virgül, boşluk ve ünlem aynen kalır. İkinci satır, 23 kaydırmanın metni geri çevirdiğini gösterir. Türkçe harfler bu programda “öteki karakterler” grubuna girer ve şifrelenmez; bu yüzden örnek metinde “Dünya” yerine “Dunya” kullanılmıştır.

Örnek 11.14

replace kullanmadan bir dizgideki bütün boşlukları StringBuilder ile silen program.

Çözüm. Dizgi taranır ve boşluk olmayan her karakter tampona eklenir. Boşluklar atlanır.

public class BoslukSil {
    public static void main(String[] args) {
        String s = " Ey Edip  Adanada pide ye ";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c != ' ') sb.append(c);
        }
        System.out.println("[" + sb + "] " + sb.length() + " " + s.length());
    }
}
[EyEdipAdanadapideye] 19 26

26 karakterlik metinde yedi boşluk vardır: baştaki, sondaki, “Edip”ten sonraki çift boşluk ve üç tek boşluk. Geriye 19 karakter kalır. Bu sayı palindrom örneğindeki temizlenmiş metnin uzunluğuyla aynıdır; harf büyüklüğü dışında sonuç da aynıdır.

11.10printf ile biçimleme

Bir rapor ya da tablo yazdırırken değerlerin hizalı durması, ondalıklı sayıların belirli basamakta kesilmesi istenir. + ile birleştirme bunu kolayca sağlamaz. System.out.printf metodu ise metni bir kalıba göre biçimler (formatting).

Tanım 11.14

printf'in ilk parametresi biçim dizgisidir (format string). Biçim dizgisindeki düz metin olduğu gibi yazılır. % ile başlayan her biçim belirteci (format specifier) ise sıradaki argümanın yerini tutar ve onun nasıl yazılacağını söyler. Belirtecin genel yapısı %[-][genişlik][.duyarlık]tür biçimindedir. Başlıca türler s (metin), d (tam sayı), f (kayan noktalı sayı) ve n'dir (satır sonu).

String ad = "Java"; int adet = 3; double fiyat = 17.5;
System.out.printf("%-8s %3d %7.2f%n", ad, adet, fiyat);
Java       3   17.50
[Java    ][  3][  17.50]
[    Java][3  ][17.5][1.750000e+01]
[Giresun][12345][3][4]
true c 50%

Programın ilk printf satırı sunumdaki örnektir. Üç belirteç üç argümanla sırayla eşleşir. Şekil 11.26 çıktının hücre hücre nasıl oluştuğunu gösterir.

%-8s␣%3d␣%7.2f%nbiçim dizgisiargümanlarad"Java"adet3fiyat17.5çıktı:Java␣␣␣␣␣␣␣3␣␣␣17.50⏎satır sonu
Şekil 11.26: "%-8s %3d %7.2f%n" biçim dizgisi. “Java” 8 hücreye sola yaslanır ve dört boşlukla tamamlanır. Ardından biçim dizgisindeki düz boşluk gelir. 3 sayısı 3 hücreye sağa yaslanır. 17.5, iki ondalıkla “17.50” olur ve 7 hücreye sağa yaslanır. %n satırı bitirir.
  • %-8s: metin en az 8 karakterlik alana yazılır. Eksi işareti sola yaslar; “Java” dört karakter olduğu için sağına dört boşluk eklenir.
  • %3d: tam sayı 3 karakterlik alana sağa yaslanır; 3'ün önüne iki boşluk gelir.
  • %7.2f: sayı virgülden sonra iki basamakla yazılır (“17.50”, beş karakter) ve 7 karakterlik alana sağa yaslanır; önüne iki boşluk gelir.
  • %n: işletim sistemine uygun satır sonu ekler.

Programın sonraki satırları alanların sınırını köşeli parantezle görünür kılar ve başka biçimleri dener. %8s eksi işareti olmadığı için sağa yaslar. %-3d sola yaslar. %.1f genişlik vermeden bir ondalık yazar. %e bilimsel gösterimdir: 1,75 × 10¹. Dördüncü satır iki önemli kuralı gösterir. Birincisi, genişlik bir alt sınırdır: “Giresun” 7 karakter olduğu için %3s onu kesmez, tamamını yazar. İkincisi, %.0f yarımları yukarı yuvarlar: 2,5 → 3 ve 3,5 → 4. Son satırda %s'nin boolean ve char değerlerini de yazdığı görülür. %% ise yüzde işaretinin kendisidir.

Biçim belirteçleri tablolar için idealdir. Aynı genişlikteki alanlar alt alta yazıldığında sütunlar kendiliğinden hizalanır:

public class Tablo {
    public static void main(String[] args) {
        String[] urun = {"Kalem", "Defter", "Hesap makinesi"};
        int[] adet = {12, 3, 1};
        double[] fiyat = {7.5, 42.0, 349.9};
        System.out.printf("%-15s %5s %9s%n", "Ürün", "Adet", "Tutar");
        for (int i = 0; i < urun.length; i++) {
            System.out.printf("%-15s %5d %9.2f%n", urun[i], adet[i], adet[i] * fiyat[i]);
        }
    }
}
Ürün             Adet     Tutar
Kalem              12     90.00
Defter              3    126.00
Hesap makinesi      1    349.90

Başlık satırı ile veri satırları aynı genişlikleri (15, 5, 9) kullandığı için sütunlar düzgün durur. Metin sütunu sola, sayı sütunları sağa yaslanmıştır; tablolarda alışılmış düzen budur. Tutar sütununda 12 × 7,5 = 90, 3 × 42 = 126 ve 1 × 349,9 = 349,9 iki ondalıkla yazılmıştır.

Ondalık ayırıcı yerel ayara bağlıdır. Aynı program Türkçe ayarlı bir makinede şunu yazar:

Ürün             Adet     Tutar
Kalem              12     90,00
Defter              3    126,00
Hesap makinesi      1    349,90

Sayının kendisi değişmez, yalnızca yazılışı değişir. Sunumdaki örnek de Türkçe makinede “17,50” yazar. Her makinede aynı çıktı isteniyorsa printf(Locale.US, …) biçiminde yerel ayar ilk parametrede verilir.

Biçim dizgisini derleyici denetlemez. Belirteç ile argümanın tipi uyuşmazsa ya da argüman eksikse hata program çalışırken ortaya çıkar:

public class BicimHata {
    public static void main(String[] args) {
        System.out.printf("%d%n", 3.5);
    }
}
Exception in thread "main" java.util.IllegalFormatConversionException: d != java.lang.Double

%d bir tam sayı bekler, 3.5 ise double'dır. İleti, d dönüşümünün Double ile kullanılamayacağını söyler. Java sayıyı yuvarlamaz ya da kesmez, istisna fırlatır. Eksik argümanda da benzer bir istisna oluşur; ilk belirteç yazıldıktan sonra program durur:

public class BicimEksik {
    public static void main(String[] args) {
        System.out.printf("%s ve %s%n", "Ada");
    }
}
Ada ve 
Exception in thread "main" java.util.MissingFormatArgumentException: Format specifier '%s'
Sık yapılan hata

Ondalıklı bir değeri %d ile ya da tam sayıyı %f ile yazdırmak. İkisi de IllegalFormatConversionException fırlatır. int için %d, double için %f kullanılır; her tip için güvenli seçenek %s'dir ama o da biçim denetimi sağlamaz. Satır sonunu unutmak da sık görülür: printf, println gibi kendiliğinden satır atlamaz; %n gerekir.

11.11Unicode ve metin uzunluğu

Java'nın char tipi 16 bittir ve 65.536 farklı değer alabilir. Türkçe harfler de dahil gündelik metinlerin hemen bütün karakterleri bu aralıktadır. Ancak Unicode standardı bugün 150 binden fazla karakter tanımlar: eski yazı sistemleri, matematik simgeleri, emojiler. Bunların bir kısmı tek bir char'a sığmaz.

Tanım 11.15

Unicode'da her karakterin bir numarası vardır; bu numaraya kod noktası (code point) denir. Java dizgileri bu numaraları UTF-16 kodlamasıyla, 16 bitlik kod birimleri (code unit) biçiminde saklar. Bir char bir kod birimidir. U+FFFF'ten büyük kod noktaları iki kod biriminden oluşan bir çiftle saklanır. length() kod birimi sayısını verir, karakter sayısını değil.

"çay".length() = 3çaybir emoji (ör. gülen yüz): length() = 2← 1 görünen karakterD83DDE00← 2 char (UTF-16 çifti)
Şekil 11.27: “çay” üç kod biriminden oluşur ve uzunluğu 3'tür; ç tek bir char'a sığar. Gülen yüz emojisi tek bir görünür karakterdir ama D83D ve DE00 adlı iki kod biriminden oluşan bir UTF-16 çiftiyle saklanır; uzunluğu 2'dir.
public class UnicodeOrnek {
    public static void main(String[] args) {
        String cay = "çay";
        String emoji = "😀";
        System.out.println(cay.length() + " " + emoji.length() + " " + emoji.codePointCount(0, emoji.length()));
        System.out.printf("%X %X %X%n", (int) emoji.charAt(0), (int) emoji.charAt(1), emoji.codePointAt(0));
        String birlesik = "çay";
        System.out.println(birlesik.length() + " " + birlesik.equals(cay));
        System.out.println(java.text.Normalizer.normalize(birlesik, java.text.Normalizer.Form.NFC).equals(cay));
        System.out.println("straße".toUpperCase(java.util.Locale.ROOT) + " " + "straße".length() + " " + "straße".toUpperCase(java.util.Locale.ROOT).length());
    }
}
3 2 1
D83D DE00 1F600
4 false
true
STRASSE 6 7

İlk satırda “çay”ın uzunluğu 3, emojinin uzunluğu 2'dir. codePointCount ise kod noktalarını sayar ve emoji için 1 verir. İkinci satır emojinin iki kod birimini ve gerçek kod noktasını onaltılık tabanda yazar: U+1F600. Bu sayı 65.535'ten büyük olduğu için tek bir char'a sığmaz.

Üçüncü satır daha ince bir durumu gösterir. “ç” harfi Unicode'da iki yolla yazılabilir: tek bir hazır karakter (U+00E7) ya da bir “c” ve onun altına eklenen ayrı bir çengel işareti (U+0327). İkinci yazım ekranda aynı görünür ama uzunluğu 4'tür ve equals ile ilk yazıma eşit değildir. java.text.Normalizer sınıfı iki yazımı aynı biçime getirir; dördüncü satır normalleştirmeden sonra eşitliğin sağlandığını gösterir. Son satırda Almanca “ß” harfi büyük harfe çevrilince iki harfe, “SS”ye dönüşür ve dizgi uzar: 6 karakterlik “straße”, 7 karakterlik “STRASSE” olur.

Bu ayrıntıların günlük programlardaki sonucu şudur: Türkçe metinlerde length() ve charAt beklendiği gibi çalışır. Emoji, birleşik işaretler ya da başka yazı sistemleri içeren metinlerde ise “kullanıcının gördüğü karakter sayısı” length()'ten farklı olabilir. Bu tür metinlerde ters çevirme gibi işlemler de bir çifti ortasından bölüp bozuk karakterler üretebilir.

11.12Özet

  • char tek bir karakteri tek tırnakla ('T'), String karakter dizisini çift tırnakla ("Tarkan") tutar. String ilkel tip değil, java.lang paketindeki bir sınıftır; değişken nesnenin referansını tutar. "" uzunluğu 0 olan gerçek bir nesnedir; null hiçbir nesneyi göstermez ve üzerinden metot çağrılırsa NullPointerException oluşur.
  • +, işlenenlerden biri dizgiyse birleştirme, ikisi de sayıysa toplamadır ve soldan sağa işlenir: "12" + 3 + 4 = "1234", 3 + 4 + "12" = "712". char bir sayıdır: 'a' + 1 = 98, 'k' − 'a' = 10.
  • String değiştirilemezdir. toUpperCase, replace, substring, + yeni bir nesne döndürür; sonuç atanmazsa kaybolur. Değişen nesne değil, değişkenin gösterdiği yerdir. Kimsenin göstermediği nesneleri çöp toplayıcı temizler.
  • İndisler 0'dan length() − 1'e kadardır; sınır dışı indis StringIndexOutOfBoundsException verir. length() dizgilerde metot, dizilerde length alandır.
  • substring(a, b): a dahil, b hariç; uzunluk b − a; geçerli aralık 0 ≤ a ≤ b ≤ length(). indexOf ilk konumu ya da −1 verir. split düzenli ifade alır; ardışık ayırıcılar boş parça üretir, sondaki boş parçalar atılır.
  • replace düz metin, replaceAll düzenli ifade arar. trim yalnızca ASCII boşlukları, strip bütün Unicode boşluklarını siler. format ve printf yerel ayara bağlıdır.
  • == referansı, equals içeriği karşılaştırır; dizgiler her zaman equals ile karşılaştırılır. compareTo ilk farklı karakterin kod farkını, fark yoksa uzunluk farkını verir. Büyük harfler küçüklerden, İngilizce harfler Türkçe harflerden önce gelir; Türkçe sıralama için Collator kullanılır.
  • Türkçede i ↔ İ ve ı ↔ I eşleşir. Parametresiz toLowerCase()/toUpperCase() makinenin diline bağlıdır; teknik metinde Locale.ENGLISH, Türkçe metinde Locale.forLanguageTag("tr") verilir.
  • Aynı sabitler dizgi havuzunda tek nesneyi paylaşır; new her zaman yeni nesne açar. Sabit ifadeler derleme sırasında birleştirilir ve havuza girer; değişkenli birleştirme yeni nesne üretir. intern() havuzdaki nesneyi verir. "sabit".equals(x) null'a karşı güvenlidir.
  • next() bir kelime okur ve ⏎'ı tamponda bırakır; nextLine() satırın kalanını okur ve ⏎'ı tüketir. next() ya da nextInt()'ten sonra satır okunacaksa araya bir nextLine() konur.
  • Döngüde s = s + x her turda yeni nesne üretir; n turda yaklaşık n²/2 karakter kopyalanır. StringBuilder değiştirilebilir bir tampondur: append, insert, reverse aynı nesneyi değiştirir; sonuç toString() ile alınır.
  • Uygulamaların ortak kalıbı karakter taramasıdır. Ters çevirme sondan başa, palindrom iki uçtan içeri doğru tarar. Kelime sayma bir bayrak kullanır. Frekans tablosu hücreyi c - 'a' ile seçer; Sezar şifresi (c - 'a' + k) % 26 ile kaydırır.
  • printf'te %-8s sola, %3d sağa yaslar; %7.2f iki ondalıkla 7 hücreye yazar; %n satır sonudur. Tip uyuşmazlığı çalışma zamanında istisna verir.
  • Dizgiler UTF-16 kod birimlerinden oluşur; Türkçe harfler tek char'a sığar, emoji gibi karakterler iki char tutar. Bu yüzden length() görünen karakter sayısıyla her zaman aynı değildir.

11.13Kavram listesi

Terimlerin İngilizce karşılıkları ve öteki sunumlardaki kullanımları terim sözlüğünde bulunur.

11.14Kendini Dene

  1. int p = 2, q = 5; iken System.out.println(p + q + "x" + p + q); satırı ne yazar?

    Cevap

    + soldan sağa işlenir. İlk iki işlenen sayıdır: 2 + 5 = 7. Sonra 7 + "x" = "7x" olur ve ifade birleştirmeye döner: "7x2", ardından "7x25". Çıktı 7x25'tir.

  2. String s = "dizgi"; s.toUpperCase(); s = s.replace('i', 'ı'); satırlarından sonra s neyi gösterir?

    Cevap

    toUpperCase()'in sonucu atanmadığı için kaybolur; s hâlâ "dizgi"dir. replace'in sonucu ise atanır ve iki 'i' 'ı' olur: s "dızgı"yı gösterir. "dizgi" nesnesi değişmemiştir; yalnızca s'nin oku yeni nesneye taşınmıştır.

  3. String t = "Karadeniz"; için t.length(), t.charAt(4), t.indexOf("de"), t.substring(4, 8) ve t.lastIndexOf('a') değerleri nedir?

    Cevap

    İndisler: K=0, a=1, r=2, a=3, d=4, e=5, n=6, i=7, z=8. Uzunluk 9'dur. charAt(4) 'd'dir. "de" 4 indisinde başlar. substring(4, 8) 4–7 indislerini alır: "deni" (uzunluk 8 − 4 = 4). Son 'a' 3 indisindedir.

  4. "x;;y;z;;".split(";").length kaçtır? Parçalar nelerdir?

    Cevap

    Metin her noktalı virgülden kesilir: "x", "", "y", "z", "", "". Ardışık iki noktalı virgülün arasındaki boş parça kalır, sondaki iki boş parça atılır. Sonuç [x, , y, z], uzunluk 4'tür.

  5. "Masa".compareTo("masa"), "kitap".compareTo("kitaplık") ve "ödev".compareTo("sınav") değerleri nedir? Hangileri gerçek sözlük sırasıyla çelişir?

    Cevap

    İlkinde ilk karakterler 'M' (77) ve 'm'dir (109): 77 − 109 = −32. İkincisinde "kitap", "kitaplık"ın başıdır: 5 − 8 = −3. Üçüncüsünde 'ö' (246) ile 's' (115) karşılaşır: 246 − 115 = 131, yani "ödev" "sınav"dan sonra gelir. Türk alfabesinde ö, s'den önce geldiği için üçüncü sonuç sözlük sırasıyla çelişir.

  6. Türkçe ayarlı bir bilgisayarda "MAIL".toLowerCase().equals("mail") ifadesinin değeri nedir? İfade nasıl düzeltilir?

    Cevap

    Parametresiz toLowerCase() Türkçe kuralları kullanır ve "I"yı "ı"ya çevirir: "maıl". "mail"e eşit olmadığı için değer false'tur. "MAIL" teknik bir sözcük olduğu için toLowerCase(Locale.ENGLISH) yazılır ya da doğrudan "MAIL".equalsIgnoreCase("mail") kullanılır; ikisi de true verir.

  7. String a = "ab"; String b = "a"; String c = b + "b"; final String d = "a"; String e = d + "b"; satırlarından sonra a == c, a == e ve a == c.intern() değerleri nedir?

    Cevap

    b final olmadığı için b + "b" çalışma zamanında yeni bir nesne üretir: a == c false'tur. d final bir sabit olduğu için d + "b" derleme sırasında "ab" sabitine dönüşür ve havuzdan gelir: a == e true'dur. c.intern() havuzdaki "ab"yi döndürür: true.

  8. Kullanıcı önce “42⏎”, sonra “Ali Can⏎” yazıyor. int n = k.nextInt(); String s = k.nextLine(); String t = k.next(); satırlarından sonra s ve t nedir?

    Cevap

    nextInt() 42'yi okur ve ⏎'ı bırakır. nextLine() o ⏎'a kadar okur: s boş dizgidir. next() ikinci satırdaki ilk kelimeyi okur: t "Ali"dir. " Can⏎" tamponda kalır.

  9. “ankara” kelimesi 4 kaydırmalı Sezar şifresiyle şifreleniyor. Sonuç nedir? Şifreyi çözmek için kaç kaydırma kullanılır?

    Cevap

    Harflerin yerleri: a=0, n=13, k=10, r=17. 4 eklenince 4, 17, 14, 21 olur; hiçbiri 26'yı aşmaz. Altı harf sırayla e, r, o, e, v, e'ye dönüşür: "eroeve". Çözmek için 26 − 4 = 22 kaydırma kullanılır.

  10. System.out.printf("[%6.1f][%-4s][%03d]%n", 2.75, "ab", 7); satırı ne yazar?

    Cevap

    %6.1f, 2,75'i bir ondalığa yuvarlar: "2.8" (üç karakter) ve 6 hücreye sağa yaslar; önüne üç boşluk gelir. %-4s, "ab"yi 4 hücreye sola yaslar; sonuna iki boşluk gelir. %03d, 7'yi 3 hücreye sıfırla doldurur: "007". Çıktı [ 2.8][ab ][007]'dir (İngilizce yerel ayarda; Türkçede "2,8").

11.15İleri okuma

  • Oracle, The Java Tutorials, “Strings” ve “The StringBuilder Class” sayfaları: dizgi oluşturma, birleştirme, metotlar ve StringBuilder resmî örneklerle anlatılır.
  • Oracle, Java SE 21 API Belgeleri, java.lang.String, java.util.Formatter ve java.util.Locale sınıfları: her metodun kesin davranışı, split'in boş parça kuralları ve biçim belirteçlerinin tam listesi.
  • R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach (Addison-Wesley, 2016), Kesim 3.1 “Using Data Types”: String bir veri tipi gözüyle ele alınır; genom dizgileri üzerinde arama örnekleri verilir.
  • Unicode Consortium, The Unicode Standard, “Case Mappings” bölümü ve SpecialCasing.txt dosyası: Türkçe ve Azerice i/İ/ı/I kurallarının ve “ß → SS” gibi uzunluk değiştiren dönüşümlerin kaynağı.

11.16Kaynaklar

  1. P. Deitel, H. Deitel, Java: How to Program, Early Objects, 11. baskı, Pearson, 2018.
  2. Y. D. Liang, Introduction to Java Programming and Data Structures, 13. baskı, Pearson, 2024.
  3. Oracle, The Java Tutorials: Strings; The StringBuilder Class.
  4. Oracle, Java SE 21 API: String, StringBuilder.
  5. R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach, Addison-Wesley, 2016: 3.1 Using Data Types.
  6. Unicode Consortium, The Unicode Standard.

İlgili sayfalar: Dizgiler sunumu · Çalışma soruları · Dizgi Laboratuvarı simülatörü · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.