Java ile Nesne Yönelimli Programlama · Ders Notu

Bölüm 3.4: Dizgi

Programların büyük kısmı metinle çalışır. Bir kullanıcı adı, bir e-posta adresi, bir dosya yolu ya da ekrana yazılan bir ileti hep karakterlerden oluşan bir metindir. Java'da bu tür metinler String sınıfının nesneleriyle saklanır. Bu not dizgilerin nasıl oluşturulduğunu, + ile birleştirmenin hangi sırayla yürüdüğünü ve karakterlere indisle nasıl erişildiğini anlatır. Ardından en sık kullanılan dizgi metotları gelir. Dizgilerin değiştirilemez olması, Türkçe harflerde yerel ayarın etkisi ve döngüde birleştirme için StringBuilder ayrı kesimlerde işlenir. Scanner ile klavyeden metin okumanın inceliklerinden sonra dizgilerin karşılaştırılması ele alınır: == ile equals() arasındaki fark, dizgi havuzu ve compareTo. Son kesimler printf ile biçimli çıktıyı, palindrom ve kelime frekansı uygulamalarını ve sık yapılan hataları kapsar. Sunumdaki bütün kodlar JDK 21 ile çalıştırılmış, çıktıları ve hata iletileri gerçek çalıştırmadan alınmıştır.

Öğrenme çıktıları

Dizgi oluşturmak
String nesnesini dizgi sabitiyle ve new ile oluşturmak; + birleştirmesinin soldan sağa nasıl işlendiğini açıklamak.
İndis ve metotlar
charAt, length, indexOf ve substring ile dizginin parçalarına erişmek; dizgiyi döngüyle karakter karakter dolaşmak.
Değiştirilemezlik
Dizgi metotlarının yeni nesne döndürdüğünü bellek çizimiyle göstermek; yerel ayarın büyük-küçük harf dönüşümüne etkisini bilmek; döngüde StringBuilder kullanmak.
Kullanıcı girişi
next(), nextLine() ve nextInt() arasındaki farkı açıklamak; satır sonu tuzağından kaçınmak.
Karşılaştırma
== ile equals() farkını dizgi havuzuyla açıklamak; compareTo ve öteki karşılaştırma metotlarını doğru kullanmak.
Biçimli çıktı
printf ve String.format ile genişlik, hizalama ve hassasiyeti ayarlamak.

3.4.1Dizgi kavramı

Bilgisayar her veriyi sayı olarak saklar. Bir harf de bellekte bir sayıdır; char tipi tek bir karakterin kodunu tutar. Gerçek programlar ise tek harfle değil, kelimelerle ve cümlelerle çalışır. Bir öğrencinin adı, bir ürünün açıklaması ya da bir sunucudan gelen yanıt, ardışık karakterlerden oluşan birer metindir. Java bu tür metinler için hazır bir sınıf sunar. Bu kesim o sınıfı tanıtır, dizginin nasıl oluşturulduğunu ve karakterlerine nasıl erişildiğini anlatır.

3.4.1.1Dizgi nedir?

Tanım 3.4.1

Dizgi (string), karakterlerden oluşan bir metni saklayan nesnedir. Java'da dizgilerin tipi java.lang.String sınıfıdır. Kaynak kodda çift tırnak arasına yazılan "Tarkan" gibi bir metin dizgi sabiti (string literal) adını alır.

Tanımdaki birkaç ayrıntı ilk bakışta gözden kaçar. Birincisi, String bir sınıftır, ilkel tip değildir. Bu yüzden adı büyük harfle başlar: int, double, char küçük harfle yazılır, String ise büyük S ile. Bir String değişkeni metnin kendisini değil, metni tutan nesnenin bellekteki yerini, yani bir referansı saklar. İkincisi, String sınıfı java.lang paketindedir. Bu paket her programa kendiliğinden eklenir, dolayısıyla import satırı gerekmez. Üçüncüsü, dizgi sabiti yazmak için new kullanılmaz. String ad = "Tarkan"; satırı nesneyi kendisi oluşturur.

Java karakterleri Unicode ile saklar. Unicode, dünyadaki yazı sistemlerinin karakterlerine birer numara veren standarttır. Bu sayede ç, ğ, ı, İ, ö, ş ve ü gibi Türkçe harfler dizgide sorunsuz durur. Dizgi aslında sıralı karakter kutularından oluşur. Her kutunun bir sıra numarası vardır ve numaralandırma 0'dan başlar (Şekil 3.4.1).

T0a1r2k3a4n5
Şekil 3.4.1: "Tarkan" dizgisi altı karakter kutusundan oluşur. Kutuların altındaki 0–5 sayıları indislerdir.

Tek karakter ile tek karakterlik dizgi farklı şeylerdir. 'T' tek tırnakla yazılır ve tipi char'dır; bellekte 84 sayısını tutan iki baytlık bir değerdir. "T" ise çift tırnakla yazılır ve uzunluğu 1 olan bir String nesnesidir. İkisi birbirinin yerine kullanılamaz. char c = "T"; satırı derlenmez, String s = 'T'; satırı da derlenmez.

Sunumdaki ilk örnek üç dizgi oluşturur ve yazdırır:

String ad = "Tarkan";
String adSoyad = "Mustafa Sandal";
String nokta = "(" + 3 + ", " + 5 + ")";
System.out.println(ad);
System.out.println(adSoyad);
System.out.println(nokta);
Tarkan
Mustafa Sandal
(3, 5)

İlk iki satırda değişkenlere dizgi sabitleri atanır. Boşluk da bir karakterdir: "Mustafa Sandal" 14 karakterlik bir dizgidir. Üçüncü satır sayılardan bir dizgi kurar. "(" bir dizgi olduğu için ardından gelen 3 metne çevrilir ve eklenir; aynı kural ", ", 5 ve ")" için de işler. Sonuçta nokta değişkeni "(3, 5)" metnini gösterir. Bu birleştirme kuralı biraz aşağıda ayrıntılı ele alınıyor, çünkü yeni başlayanların en sık şaşırdığı yer burasıdır.

3.4.1.2Dizgi oluşturmak

Bir dizgi birkaç yolla oluşturulur. En yaygını dizgi sabitidir. Bunun yanında String sınıfının yapıcıları, sayıyı dizgiye çeviren String.valueOf metodu ve iki dizgiyi uç uca ekleyen concat metodu da kullanılır. Sunumdaki kodun ilk yarısı bu yolları bir arada gösterir:

String s0 = "Giresun";            // deyim
char[] harfler = {'J', 'a', 'v', 'a'};
String s1 = new String(harfler);  // char[]
String s2 = String.valueOf(42);   // sayıdan
String s3 = "Giresun".concat(" Üni.");
System.out.println(s1 + " " + s2 + " " + s3);

System.out.println("Toplam: " + 3 + 5);
System.out.println("Toplam: " + (3 + 5));
System.out.println(3 + 5 + " elma");
System.out.println('a' + 'b' + "c");
System.out.println("" + 'a' + 'b');
Java 42 Giresun Üni.
Toplam: 35
Toplam: 8
8 elma
195c
ab

Kodun ilk altı satırı şöyle çalışır:

  • String s0 = "Giresun"; en sık görülen biçimdir: dizgi sabiti doğrudan değişkene atanır.
  • new String(harfler) bir char dizisindeki karakterlerden yeni bir dizgi kurar. Dizi {'J', 'a', 'v', 'a'} olduğu için s1 "Java" olur. Bu yapıcı, karakterleri tek tek hesaplanan bir metni dizgiye dönüştürmek için kullanışlıdır.
  • String.valueOf(42) 42 sayısının metin karşılığını, yani "42" dizgisini döndürür. valueOf bir sınıf metodudur: bir nesne üzerinden değil, sınıf adıyla çağrılır.
  • "Giresun".concat(" Üni.") iki dizgiyi birleştirir ve "Giresun Üni." döndürür. Burada metot doğrudan bir dizgi sabiti üzerinden çağrılır; sabit de bir nesne olduğu için bu geçerlidir.

Yazdırma satırı bu üç dizgiyi aralarına boşluk koyarak birleştirir: Java 42 Giresun Üni.. Kodun ikinci yarısı ise + operatörünün davranışını gösterir. Çıktının ikinci satırındaki Toplam: 35 çoğu öğrenciyi şaşırtır; nedeni bir sonraki alt kesimin konusudur.

new String("Giresun") biçimi de derlenir ve çalışır, ama gereksiz bir nesne üretir. Bu biçim karşılaştırma kesiminde dizgi havuzunu anlatmak için kullanılacak. Gündelik kodda bir sabitten dizgi oluşturmak için new yazmanın bir yararı yoktur.

3.4.1.3Birleştirme ve + operatörünün sırası

Tanım 3.4.2

Dizgi birleştirme (string concatenation), iki değeri uç uca ekleyerek yeni bir dizgi üretmektir. Java'da + operatörünün işlenenlerinden en az biri String ise öteki işlenen de dizgiye çevrilir ve iki metin birleştirilir. İki işlenen de sayıysa + sayısal toplama yapar.

Aynı + işareti iki ayrı iş yapar: toplama ve birleştirme. Hangisinin yapılacağına her işlemde ayrı ayrı karar verilir. Karar için yalnız o anki iki işlenenin tipine bakılır. Ayrıca + soldan sağa işlenir. Yani a + b + c ifadesi (a + b) + c diye hesaplanır. Bu iki kural birlikte düşünülünce sunumdaki çıktıların hepsi açıklanır:

  • "Toplam: " + 3 + 5: önce "Toplam: " + 3 hesaplanır. Sol yan dizgi olduğu için 3 metne çevrilir: "Toplam: 3". Ardından "Toplam: 3" + 5 gelir ve yine birleştirme yapılır. Sonuç Toplam: 35'tir.
  • "Toplam: " + (3 + 5): parantez önce hesaplanır. İçerideki iki işlenen de int olduğu için toplama yapılır, 8 çıkar. Sonra 8 dizgiye eklenir: Toplam: 8.
  • 3 + 5 + " elma": soldaki iki işlenen sayıdır, önce toplanır ve 8 olur. Ancak ondan sonra dizgiyle karşılaşılır: 8 elma.
  • 'a' + 'b' + "c": char bir sayı tipidir. İki char toplanınca kodları toplanır: 97 + 98 = 195. Sonra "c" eklenir: 195c.
  • "" + 'a' + 'b': baştaki boş dizgi her şeyi değiştirir. "" + 'a' zaten bir dizgidir, "a". Ardından 'b' metin olarak eklenir: ab.

Kural kısaca şudur: ifade soldan sağa okunur. Bir dizgiyle karşılaşılana kadar sayılar toplanır. Bir dizgi ortaya çıktığı andan sonraki her + birleştirmedir. Sayıların toplanması isteniyorsa parantez kullanılır.

Sunumdaki izleme slaytı aynı kuralı değişkenlerle gösterir:

int a = 4, b = 2;
String s1 = "Sonuç: " + a + b;
String s2 = "Sonuç: " + (a + b);
String s3 = a + b + " kalem";
String s4 = 'x' + 'y' + "z";
String s5 = "" + 'x' + 'y';
System.out.println(s1 + " | " + s2);
System.out.println(s3 + " | " + s4 + " | " + s5);
Sonuç: 42 | Sonuç: 6
6 kalem | 241z | xy

Tablo 3.4.1'deki her satır bir + işlemini gösterir. Sütunlar, işlenenlerin tipini ve o adımın sonucunu verir.

Tablo 3.4.1: a = 4, b = 2 iken her + işleminin türü ve sonucu. İşlemler soldan sağa yapılır.
DeğişkenİşlemSol işlenenSağ işlenenSonuç
s1"Sonuç: " + aStringintbirleştirme: "Sonuç: 4"
… + bStringintbirleştirme: "Sonuç: 42"
s2(a + b)intinttoplama: 6
"Sonuç: " + 6Stringintbirleştirme: "Sonuç: 6"
s3a + bintinttoplama: 6
6 + " kalem"intStringbirleştirme: "6 kalem"
s4'x' + 'y'charchartoplama: 120 + 121 = 241
241 + "z"intStringbirleştirme: "241z"
s5"" + 'x'Stringcharbirleştirme: "x"
"x" + 'y'Stringcharbirleştirme: "xy"

s1 ile s2 arasındaki fark yalnız bir çift paranteze dayanır, ama sonuç 42 ile 6 kadar farklıdır. Şekil 3.4.2 bu iki satırın ilk adımlarını sunumdaki çizimlerle gösterir. Üstteki kutuların altında işlenenlerin tipi, alttaki kutuda ise o adımın sonucu ve tipi yazılıdır.

"Sonuç: "String4int+"Sonuç: 4" (String) "Sonuç: 4"String2int+"Sonuç: 42" (String) 4int2int+6 (int) "Sonuç: "String6int+"Sonuç: 6" (String)
Şekil 3.4.2: s1 ve s2 satırlarının adımları. (a) Sol yan String olduğu için 4 metne eklenir. (b) Sonuç hâlâ String'dir; 2 de metin olarak eklenir ve "Sonuç: 42" çıkar. (c) Parantez içinde iki int toplanır, 6 olur. (d) 6 dizgiye eklenir.

s4 satırı ayrı bir dikkat ister. char değerleri metin gibi görünse de Java onları sayı olarak işler. 'x' karakterinin Unicode kodu 120, 'y''nin kodu 121'dir. İki char yan yana toplandığında sonuç bir int olur, 241. Bu sayı ancak "z" ile karşılaşınca metne çevrilir. s5'te ise baştaki "" ilk işlemi birleştirmeye çevirir ve iki harf metin olarak yan yana gelir. Karakterleri birleştirmek için başa boş dizgi koymak yaygın bir yoldur. Aynı iş String.valueOf('x') + 'y' ile de yapılır.

Sık yapılan hata

Ekrana bir toplam yazdırırken parantezi unutmak: System.out.println("Toplam: " + x + y); satırı x ile y'yi toplamaz, yan yana yazar. x = 10, y = 5 iken çıktı Toplam: 105 olur. Doğrusu "Toplam: " + (x + y)'dir. Hata derleyicinin gözünden kaçar, çünkü iki yazım da geçerli Java'dır.

Bir ayrıntı daha var: birleştirme her seferinde yeni bir String nesnesi üretir. "Sonuç: " + a + b ifadesinde mantıksal olarak iki ara dizgi oluşur. Az sayıda parça için bunun bir önemi yoktur. Bir döngüde binlerce kez birleştirme yapıldığında ise maliyet hızla büyür. Değiştirilemezlik kesiminde bu maliyet hesaplanıyor.

3.4.1.4İndisleme

Tanım 3.4.3

İndis (index), bir karakterin dizgideki sıra numarasıdır. İlk karakterin indisi 0, son karakterin indisi length() - 1'dir. Bir karaktere indisiyle erişmeye indisleme (indexing) denir.

İndislemenin iki temel metodu vardır. length() dizgideki karakter sayısını döndürür. charAt(i) ise i indisindeki karakteri bir char olarak verir. "Tarkan" altı karakterlidir; indisleri 0'dan 5'e kadar gider (Şekil 3.4.3). Uzunluk 6 olduğu hâlde 6 numaralı bir karakter yoktur. Numaralandırma 0'dan başladığı için son indis her zaman uzunluğun bir eksiğidir.

T0a1r2k3a4n5charAt(0)charAt(5)length() = 6
Şekil 3.4.3: İlk karakter charAt(0), son karakter charAt(length() - 1), yani charAt(5) ile alınır.

Sunumdaki örnek bu metotları "Tarkan" üzerinde dener:

String isim = "Tarkan";
System.out.println(isim.length());
System.out.println(isim.charAt(0));
System.out.println(isim.charAt(isim.length() - 1));
char c = isim.charAt(2);
System.out.println(c + 1);
System.out.println((char) (c + 1));
6
T
n
115
s

İlk satır uzunluğu, 6'yı yazar. charAt(0) ilk harfi, T'yi verir. Son harf için indis elle yazılmaz, uzunluktan hesaplanır: isim.length() - 1 = 5 ve charAt(5) = n. Bu yazım her uzunluktaki dizgide çalışır. Sonraki satırda c, indis 2'deki 'r' karakterini alır.

Son iki satır char'ın bir sayı olduğunu gösterir. 'r' karakterinin Unicode kodu 114'tür. c + 1 ifadesinde char ile int toplanır ve sonuç int olur: 115. Ekrana harf değil sayı çıkar. Bir sonraki harfi görmek için sonuç (char) ile geri çevrilir: 115 kodlu karakter s'dir. Harfleri kaydırmaya dayanan basit şifreleme örnekleri bu dönüşümü kullanır.

length() bir metottur, yani sonunda parantez vardır. Dizilerde ise uzunluk bir alandır ve parantezsiz yazılır: dizi.length. İki yazım sık karıştırılır ve ikisi de derleme hatası verir:

Uzun.java:4: error: cannot find symbol
        int n = s.length;
                 ^
  symbol:   variable length
  location: variable s of type String
DiziUzun.java:4: error: cannot find symbol
        int n = d.length();
                 ^
  symbol:   method length()
  location: variable d of type int[]

İletilerdeki symbol satırı farkı açıkça söyler. Birincide derleyici String içinde length adlı bir değişken arar ve bulamaz. İkincide int[] içinde length() adlı bir metot arar ve yine bulamaz. Bir başka karışıklık da dizgiye dizi gibi köşeli parantezle erişmeye çalışmaktır. s[0] yazımı C ve Python'da çalışır ama Java'da derlenmez:

Arr.java:4: error: array required, but String found
        char c = s[0];
                  ^

Geçersiz bir indis derleme sırasında yakalanmaz, çünkü indisin değeri çoğu zaman program çalışırken belli olur. Dizginin dışına taşan bir indis çalışma zamanında bir istisnaya yol açar:

String s = "Java";
System.out.println(s.charAt(4));
Exception in thread "main" java.lang.StringIndexOutOfBoundsException:
    Index 4 out of bounds for length 4

"Java" dört karakterlidir; geçerli indisler 0, 1, 2 ve 3'tür. charAt(4) çağrısı StringIndexOutOfBoundsException fırlatır ve program durur. İletideki “Index 4 out of bounds for length 4” sözü sorunun yerini kesin olarak verir: 4 numaralı indis istendi, uzunluk ise 4. Negatif indis de aynı istisnayı doğurur; "Java".charAt(-1) için ileti “Index -1 out of bounds for length 4” olur.

Sık yapılan hata

Son karakteri s.charAt(s.length()) ile almaya çalışmak. Uzunluk, son indisten bir fazladır ve bu çağrı her dizgide StringIndexOutOfBoundsException verir. Aynı hata döngüde i <= s.length() koşuluyla da yapılır. Doğru koşul i < s.length()'tir.

3.4.1.5Karakterleri döngüyle dolaşmak

Bir dizginin bütün karakterlerine tek tek bakmak için for döngüsü kullanılır. Döngü değişkeni 0'dan başlar ve uzunluğa ulaşmadan durur. Sunumdaki program kullanıcıdan bir kelime alır ve bu kelimenin tek sıralı harflerini yazar; döngü değişkeni her turda 2 artar:

Scanner scanner = new Scanner(System.in);
System.out.print("Bir kelime giriniz: ");
String kelime = scanner.nextLine();
for (int i = 0; i < kelime.length(); i += 2) {
  char harf = kelime.charAt(i);
  System.out.println((i + 1) + ". harf: " + harf);
}
scanner.close();

Program Scanner ile klavyeden bir satır okur. Scanner kullanmak için dosyanın başına import java.util.Scanner; satırı eklenir; okuma metotlarının ayrıntısı kullanıcı girişi kesimindedir. print, println'den farklı olarak alt satıra geçmez. Bu yüzden kullanıcının yazdığı kelime istemle aynı satırda görünür. Kullanıcı Giresun yazıp Enter'a bastığında çıktı şöyle olur:

Bir kelime giriniz: Giresun
1. harf: G
3. harf: r
5. harf: s
7. harf: n

Tablo 3.4.2 döngünün her turunu verir. "Giresun" yedi karakterlidir; indisleri 0'dan 6'ya kadar gider. Döngü değişkeni 0, 2, 4, 6 değerlerini alır. Ekrana yazılan sıra numarası insanların alıştığı biçimde 1'den başlasın diye i + 1 yazdırılır. Buradaki parantez gereklidir: (i + 1) + ". harf: " önce toplamayı yapar. Bu satırda parantez olmasa da sonuç aynı çıkardı, çünkü ifade bir sayıyla başlar. Yine de parantez, toplamanın birleştirmeden önce yapıldığını açıkça gösterir. "Harf " + i + 1 gibi dizgiyle başlayan bir ifadede ise parantezsiz yazım i = 0 iken Harf 1 yerine Harf 01 üretir.

Tablo 3.4.2: kelime = "Giresun" (uzunluk 7) için döngünün izlenmesi.
Turii < 7charAt(i)Yazılan satır
10doğru'G'1. harf: G
22doğru'r'3. harf: r
34doğru's'5. harf: s
46doğru'n'7. harf: n
—8yanlış—döngü biter

Son satır önemlidir. i 8 olduğunda koşul yanlış çıkar ve charAt(8) hiç çağrılmaz. Koşul i < kelime.length() biçiminde yazıldığı için döngü her uzunlukta güvenle durur. Kelime çift uzunlukta olsaydı, örneğin "Ordu", döngü 0 ve 2 indislerini okur ve i = 4'te biterdi. Boş bir satır girilirse uzunluk 0'dır ve gövde hiç çalışmaz.

Döngü sonunda scanner.close() çağrılır. Bu çağrı Scanner'ın kullandığı kaynağı serbest bırakır. Klavyeden okuyan bir Scanner kapatıldığında System.in da kapanır ve programın geri kalanında klavyeden yeniden okunamaz. Bu yüzden close() ancak bütün okumalar bittikten sonra çağrılır.

3.4.2Dizgi metotları

String sınıfı metinle ilgili gündelik işlerin çoğu için hazır metot sunar: bir kelimeyi aramak, metnin bir parçasını almak, harfleri büyütmek, boşlukları temizlemek, metni parçalara bölmek. Bu metotların hepsi bir dizgi nesnesi üzerinden, nokta ile çağrılır: s.length(), s.indexOf("a"). Ortak bir özellikleri daha var: hiçbiri üzerinde çağrıldığı dizgiyi değiştirmez. Sonuç her zaman yeni bir değer olarak döner.

3.4.2.1Temel metotlar

Tablo 3.4.3 en sık kullanılan metotları s = "Java Programlama" dizgisi üzerinde gösterir. Bu dizgi 16 karakterlidir; dördüncü indisteki boşluk da bir karakter sayılır. Tablodaki bütün sonuçlar JDK 21 ile çalıştırılarak alınmıştır.

Tablo 3.4.3: Temel dizgi metotları, s = "Java Programlama" için.
MetotNe yapar?Örnek ve sonuç
length()Karakter sayısını döndürür.s.length() → 16
charAt(i)i indisindeki karakteri (char) döndürür.s.charAt(0) → 'J'
indexOf(str)Alt dizginin ilk başladığı indis; bulunamazsa −1.s.indexOf("Programlama") → 5
s.indexOf("C++") → −1
indexOf(str, bas)Aramaya bas indisinden başlar.s.indexOf("a", 4) → 10
lastIndexOf(str)Son geçtiği yerin indisi.s.lastIndexOf('a') → 15
substring(i1, i2)i1 dahil, i2 hariç aradaki alt dizgi.s.substring(5, 12) → "Program"
substring(i1)i1'den sona kadar.s.substring(5) → "Programlama"
toLowerCase()Küçük harfli yeni dizgi."java programlama"
toUpperCase()Büyük harfli yeni dizgi."JAVA PROGRAMLAMA"
Tanım 3.4.4

Alt dizgi (substring), bir dizginin ardışık karakterlerinden oluşan parçasıdır. s.substring(a, b), s'nin a indisinden başlayıp b - 1 indisinde biten parçasını döndürür. Parçanın uzunluğu b - a'dır.

indexOf bir arama metodudur. Dizgiyi baştan sona tarar ve aranan metnin ilk geçtiği yerin başlangıç indisini verir. Aranan metin hiç geçmiyorsa −1 döndürür. −1 geçerli bir indis olamayacağı için “bulunamadı” anlamını güvenle taşır. Bu yüzden arama sonucu kullanılmadan önce çoğu zaman if (s.indexOf(x) != -1) biçiminde denetlenir. Metodun iki parametreli sürümü aramaya belli bir indisten başlar. s.indexOf("a", 4) çağrısı 1 ve 3'teki a harflerini atlar ve ilk olarak indis 10'daki a'yı bulur. lastIndexOf ise dizgiyi sondan başa tarar ve son eşleşmeyi verir. indexOf ve lastIndexOf hem dizgi hem tek karakter ('a') kabul eder.

substring metodunun en çok karıştırılan yanı bitiş indisinin hariç olmasıdır. Bu kuralın iki pratik yararı vardır. Birincisi, parçanın uzunluğu iki indisin farkıdır: substring(5, 12) 12 − 5 = 7 karakter verir. İkincisi, ardışık parçalar kolayca yan yana gelir: s.substring(0, k) + s.substring(k) her k için s'nin kendisini verir. Tek parametreli substring(5) ise substring(5, s.length()) ile aynıdır.

Geçersiz aralık verilirse substring de StringIndexOutOfBoundsException fırlatır. Bitiş, uzunluktan büyük olamaz; başlangıç da bitişten büyük olamaz. "Java".substring(2, 6) çağrısının iletisi aralığı matematikteki yarı açık aralık gösterimiyle verir:

Exception in thread "main" java.lang.StringIndexOutOfBoundsException:
    Range [2, 6) out of bounds for length 4

İletideki [2, 6) yazımı “2 dahil, 6 hariç” demektir. Bu gösterim substring'in kuralını da özetler. Buna karşılık s.substring(s.length()) geçerlidir ve boş dizgi döndürür. Başlangıç ile bitişin eşit olması boş bir aralık anlamına gelir.

3.4.2.2indexOf ve substring adım adım

Sunumdaki izleme slaytı tablodaki metotları sırayla çalıştırır ve her değişkenin aldığı değeri karakter kutuları üzerinde gösterir:

String str = "Java Programlama";
int indeks = str.indexOf("Programlama");
int uzunluk = str.length();
String alt = str.substring(5, 12);
String son = str.substring(5);
String kucuk = str.toLowerCase();
String buyuk = str.toUpperCase();

Kodda hiçbir şey yazdırılmaz; önemli olan her satırdan sonra değişkenlerin aldığı değerlerdir. Tablo 3.4.4 bu değerleri sırayla verir.

Tablo 3.4.4: str = "Java Programlama" üzerinde her satırdan sonra oluşan değer.
SatırDeğişkenDeğerAçıklama
2indeks5"Programlama" 5. indiste başlar
3uzunluk16indisler 0 … 15
4alt"Program"indis 5 … 11; 12 − 5 = 7 karakter
5son"Programlama"indis 5'ten sona kadar
6kucuk"java programlama"yeni dizgi; str değişmedi
7buyuk"JAVA PROGRAMLAMA"yeni dizgi; str değişmedi

Şekil 3.4.4 iki önemli adımı çizimle gösterir. Vurgulu kutular, metodun eriştiği karakterlerdir. substring(5, 12) adımında 12 numaralı kutu (l) vurgulanmaz; bitiş indisinin hariç olduğu çizimde de görülür.

J0a1v2a3␣4P5r6o7g8r9a10m11l12a13m14a155 ← ilk eşleşme J0a1v2a3␣4P5r6o7g8r9a10m11l12a13m14a155 dahil12 hariç
Şekil 3.4.4: (a) indexOf("Programlama") eşleşmenin başladığı indisi, 5'i verir. (b) substring(5, 12) 5–11 indislerini alır: "Program".

Son iki satırda toLowerCase() ve toUpperCase() çağrılır. İkisi de yeni bir dizgi döndürür ve bu dizgi başka bir değişkene atanır. str değişkeni program boyunca "Java Programlama" olarak kalır. Bir metodun “dizgiyi değiştirdiği” izlenimi yanıltıcıdır; bunun nedeni değiştirilemezlik kesiminde açıklanıyor.

Örnek 3.4.1

Bir e-posta adresini kullanıcı adı ve alan adı diye ikiye ayıralım. Adres e = "ogrenci@giresun.edu.tr" olsun. @ işaretinin yeri int at = e.indexOf('@'); ile bulunur. "ogrenci" yedi harflidir; harfler 0–6 indislerini kaplar ve @ indis 7'dedir. Kullanıcı adı @'dan önceki kısımdır: e.substring(0, at) = e.substring(0, 7) = ogrenci. Bitiş indisi hariç olduğu için @ parçaya girmez. Alan adı @'dan sonraki kısımdır: e.substring(at + 1) = e.substring(8) = giresun.edu.tr. Burada + 1 unutulsaydı sonuç @giresun.edu.tr olurdu. Yöntem adres uzunluğundan bağımsızdır; indisler elle sayılmaz, indexOf ile bulunur.

3.4.2.3Diğer yararlı metotlar

Temel metotların yanında birkaç metot daha gündelik kodda sık geçer. Sunumdaki örnek bunları bir arada dener:

String s = "  Merhaba Dünya  ";
System.out.println("[" + s.trim() + "]");
System.out.println("[" + s.replace('a', 'e') + "]");
String[] meyveler = "elma,armut,kiraz".split(",");
System.out.println(meyveler.length);
System.out.println(meyveler[1]);
System.out.println(String.join(" - ", meyveler));
System.out.println("ab".repeat(3));       // Java 11+
System.out.println("".isEmpty());
System.out.println("   ".isBlank());      // Java 11+
char[] dizi = "Java".toCharArray();
System.out.println(dizi.length + " " + dizi[0]);
System.out.println("Giresun".replace("sun", "SUN"));
System.out.println(String.valueOf(3.5) + 1);
System.out.println(Integer.parseInt("42") + 1);
[Merhaba Dünya]
[  Merhebe Dünye  ]
3
armut
elma - armut - kiraz
ababab
true
true
4 J
GireSUN
3.51
43

Satırları sırayla açıklayalım:

  • trim() dizginin başındaki ve sonundaki boşlukları atar. Aradaki boşluklara dokunmaz: " Merhaba Dünya " → [Merhaba Dünya]. Köşeli parantezler boşlukların görünmesi için eklendi. Kullanıcı girdisini işlemeden önce trim() çağırmak yaygın bir alışkanlıktır. Java 11 ile gelen strip() aynı işi Unicode'daki bütün boşluk karakterleri için yapar.
  • replace('a', 'e') bütün a harflerini e yapar: [ Merhebe Dünye ]. Yalnız ilk eşleşmeyi değil, hepsini değiştirir. Boşluklar korunmuştur, çünkü replace yeni dizgiyi s'nin kendisinden üretir. Önceki satırdaki trim() sonucu hiçbir yere atanmamıştı.
  • split(",") dizgiyi verilen ayraçtan böler ve parçaları bir String[] dizisi olarak döndürür. "elma,armut,kiraz" üç parçaya ayrılır; meyveler.length 3, meyveler[1] ise armut'tur.
  • String.join(" - ", meyveler) bölmenin tersini yapar: parçaları aralarına ayraç koyarak birleştirir. Sonuç elma - armut - kiraz'dır.
  • "ab".repeat(3) dizgiyi üç kez yan yana yazar: ababab. Bu metot Java 11'de eklendi.
  • isEmpty() uzunluk 0 ise true döndürür. isBlank() (Java 11) dizgi boşsa ya da yalnız boşluk karakterlerinden oluşuyorsa true döndürür. " " boş değildir ama boşluktan ibarettir: isEmpty() false, isBlank() true verir.
  • toCharArray() dizginin karakterlerini yeni bir char dizisine kopyalar. Dizinin uzunluğu 4, ilk öğesi 'J''dir. Dizi değiştirilebilir; bu yüzden karakterler üzerinde yer değiştirme gibi işler dizi üzerinde yapılıp sonuç new String(dizi) ile geri çevrilebilir.
  • replace tek karakterle sınırlı değildir; dizgi de alır: "Giresun".replace("sun", "SUN") → GireSUN.
  • Son iki satır sayı ile dizgi arasındaki dönüşümü gösterir. String.valueOf(3.5) bir dizgidir, "3.5"; ona 1 eklemek birleştirmedir ve sonuç 3.51 olur. Integer.parseInt("42") ise dizgiyi gerçek bir int'e çevirir; 1 eklenince 43 çıkar. Ondalıklı sayılar için Double.parseDouble kullanılır.

parseInt yalnız rakamlardan (ve başta isteğe bağlı bir işaretten) oluşan bir dizgiyi kabul eder. Boşluk ya da harf içeren bir metin verilirse NumberFormatException fırlatır:

Exception in thread "main" java.lang.NumberFormatException:
    For input string: " 12 "

Bu yüzden kullanıcıdan ya da bir dosyadan gelen sayı metni önce trim() ile temizlenir: Integer.parseInt(metin.trim()). Bölme ile birlikte kullanıldığında bu adım daha da önem kazanır. " 3, 12 " gibi bir satır virgülden bölününce parçaların başında ve sonunda boşluk kalır.

split'in iki inceliği daha vardır. Ardışık iki ayraç arasında boş bir parça oluşur. Dizginin sonundaki boş parçalar ise atılır. Ayrıca split'in parametresi bir düzenli ifadedir (regular expression): metin aramada kullanılan bir kalıp dilidir. " +" kalıbı “bir ya da daha çok boşluk” anlamına gelir:

System.out.println(Arrays.toString("a b  c".split(" ")));
System.out.println(Arrays.toString("a b  c".split(" +")));
System.out.println(Arrays.toString("a,,b,".split(",")));
[a, b, , c]
[a, b, c]
[a, , b]

İlk satırda iki ardışık boşluk boş bir parça üretir ve dizi dört öğeli olur. İkinci satır birden çok boşluğu tek ayraç sayar. Üçüncü satırda ortadaki boş parça kalır, sondaki atılır; dizi üç öğelidir. Nokta (.) ve dikey çizgi (|) düzenli ifadelerde özel anlam taşır. Bu yüzden "a.b".split(".") beklenen sonucu vermez; noktadan bölmek için split("\\.") yazılır.

3.4.3Değiştirilemezlik

Önceki kesimlerde her metodun yeni bir değer döndürdüğü birkaç kez vurgulandı. Bu bir rastlantı değil, String sınıfının temel tasarım kararıdır. Bu kesim kararın ne anlama geldiğini, bellekte nasıl göründüğünü ve doğurduğu maliyeti anlatır. Türkçe metinlerde büyük-küçük harf dönüşümünün tuzakları ve çok sayıda birleştirme için tasarlanan StringBuilder sınıfı da bu kesimdedir.

3.4.3.1Değiştirilemez nesne

Tanım 3.4.5

Değiştirilemez (immutable) nesne, oluşturulduktan sonra durumu hiç değişmeyen nesnedir. String nesneleri değiştirilemezdir: bir dizginin karakterleri ve uzunluğu nesne oluştuğu anda belirlenir ve sonra değişmez. toUpperCase, replace, substring, concat ve + orijinal nesneye dokunmaz; sonucu yeni bir nesne olarak döndürür.

Bir benzetme yardımcı olur. Dizgi bir taş tablete benzer. Tabletteki yazı kazınıp düzeltilemez. Değişiklik gerekirse yeni bir tablet yazılır ve eskisinin yerine konur. Sunumdaki örnek bu davranışı gösterir:

String s = "Giresun Üniversitesi";
s.toUpperCase();      // sonuç kaybolur
System.out.println(s);
s = s.toUpperCase();  // sonuç s'ye atanır
System.out.println(s);
Giresun Üniversitesi
GİRESUN ÜNİVERSİTESİ

İkinci satırdaki s.toUpperCase(); çağrısı çalışır ve büyük harfli yeni bir dizgi üretir. Ancak sonuç hiçbir değişkene atanmaz ve kaybolur. Bu yüzden ilk println orijinal metni yazar. Dördüncü satırda sonuç s'ye atanır. Burada da eski nesne değişmez; s değişkeni artık yeni nesneyi gösterir. Değişen nesne değil, değişkenin gösterdiği yerdir.

Şekil 3.4.5 bu iki durumu bellek çizimiyle verir. Sol bölümdeki yığın (stack), metodun yerel değişkenlerini tutar; s burada durur. Sağdaki heap ise nesnelerin yaşadığı bölgedir. s değişkeninden çıkan ok, gösterdiği nesneyi işaret eder.

Yığın (stack)Heapmain()dizgi havuzu"Giresun Üniversitesi""GİRESUN ÜNİVERSİTESİ"s Yığın (stack)Heapmain()dizgi havuzu"Giresun Üniversitesi""GİRESUN ÜNİVERSİTESİ""GİRESUN ÜNİVERSİTESİ"s Yığın (stack)Heapmain()dizgi havuzu"Giresun Üniversitesi""GİRESUN ÜNİVERSİTESİ"s
Şekil 3.4.5: Değiştirilemezlik bellekte. (a) toUpperCase() yeni bir nesne üretir, ama sonuç atanmadığı için s eski nesneyi göstermeyi sürdürür. (b) İkinci çağrı yine yeni bir nesne üretir ve bu kez referansı s'ye atanır. (c) s artık büyük harfli nesneyi gösterir; eski nesne değişmeden durur.

İlk çağrının ürettiği nesneyi hiçbir değişken göstermez. Kimsenin göstermediği bu tür nesneler çöp toplayıcıya (garbage collector) kalır. Çöp toplayıcı, hiçbir referansın göstermediği nesnelerin belleğini zamanı geldiğinde geri alan JVM bileşenidir. Programcının bu belleği elle serbest bırakması gerekmez.

Çıktının ikinci satırındaki GİRESUN ÜNİVERSİTESİ, noktalı büyük İ harfleriyle yazılmıştır. Bu sonuç Türkçe yerel ayarlı bir bilgisayarda alınır. İngilizce ayarlı bir bilgisayarda aynı program GIRESUN ÜNIVERSITESI yazar. Nedeni bir sonraki alt kesimde açıklanıyor.

Değiştirilemezliğin bir bedeli vardır: her değişiklik yeni bir nesne demektir. Java'yı tasarlayanlar bu bedeli şu yararlar için göze almıştır:

  • Güvenli paylaşım. Aynı dizgiyi gösteren iki değişken birbirini bozamaz. Bir metoda verilen dizginin o metot içinde değiştirilmesinden korkmak gerekmez.
  • Dizgi havuzu. İçeriği aynı dizgi sabitleri tek bir nesneyi paylaşabilir. Paylaşım ancak nesne değişmiyorsa güvenlidir. Havuz karşılaştırma kesiminde ayrıntılı işleniyor.
  • İş parçacıkları arasında güvenlik. Aynı anda çalışan program parçaları değişmeyen bir nesneyi ek önlem almadan ortak kullanabilir.
Sık yapılan hata

Dizgi metodunun sonucunu atamayı unutmak: ad.trim();, ad.toUpperCase(); ya da ad.replace(" ", ""); satırları ad'ı değiştirmez. Derleyici uyarı vermez, program çalışır ama hiçbir şey olmaz. Doğru yazım ad = ad.trim(); biçimindedir. Aynı nedenle s.charAt(0) = 'j'; gibi bir karakteri yerinde değiştirme girişimi derlenmez.

3.4.3.2Türkçe harfler ve yerel ayar

Tanım 3.4.6

Yerel ayar (locale), bir dil ve bölgeye özgü biçim kurallarının bütünüdür: büyük-küçük harf dönüşümü, ondalık ayırıcı, binlik ayırıcı, tarih biçimi. Java'da java.util.Locale sınıfıyla temsil edilir. Türkiye Türkçesi için yerel ayar tr-TR'dir.

Türkçe alfabede i harfinin noktalı ve noktasız iki biçimi vardır ve ikisinin de büyük harfi ayrıdır: i ↔ İ, ı ↔ I. İngilizcede ise yalnız i ↔ I çifti bulunur. Parametresiz toUpperCase() ve toLowerCase() bilgisayarın varsayılan yerel ayarını kullanır. Aynı program bu yüzden farklı makinelerde farklı sonuç verebilir:

import java.util.Locale;

String s = "Giresun Üniversitesi";
System.out.println(s.toUpperCase());
System.out.println(s.toUpperCase(Locale.ROOT));
System.out.println("TITLE".toLowerCase());
System.out.println("TITLE".toLowerCase().equals("title"));
System.out.println("TITLE".toLowerCase(Locale.ROOT));
GİRESUN ÜNİVERSİTESİ
GIRESUN ÜNIVERSITESI
tıtle
false
title

Çıktı Türkçe ayarlı bir bilgisayarda alınmıştır. İlk satırda varsayılan ayar Türkçe olduğu için i harfleri İ olur. İkinci satırda Locale.ROOT verilir. Bu, dilden bağımsız, temel Unicode kurallarını kullanan yerel ayardır; i harfleri I olur. Üçüncü satır tuzağı gösterir: "TITLE" Türkçe kurallarla küçültülünce I harfleri ı olur ve sonuç tıtle çıkar. Dördüncü satırdaki karşılaştırma bu yüzden false verir. Son satırda Locale.ROOT ile beklenen title elde edilir. Tablo 3.4.5 dört harfin iki ayardaki dönüşümünü özetler.

Tablo 3.4.5: Türkçe (tr) ve dilden bağımsız (Locale.ROOT) ayarlarda büyük-küçük harf dönüşümü.
DönüşümTürkçe (tr)ROOT / İngilizce
i → büyükİI
I → küçükıi
ı → büyükII
İ → küçükii̇ (i + birleşen nokta; iki karakter)

Tablonun son satırı daha az bilinen bir durumdur. İ harfi Türkçe dışındaki kurallarla küçültülünce tek bir i değil, i ile üstüne konan bir “birleşen nokta” karakteri çıkar. Görünüşte fark yoktur ama dizginin uzunluğu artar. "İSTANBUL".toLowerCase(Locale.ROOT) 9 karakterlik bir dizgi verir; "İki".toLowerCase(Locale.ROOT).equals("iki") ise false'tur.

Pratik kural şöyledir. Program içi metinlerde, yani komut adı, dosya uzantısı, protokol anahtarı gibi makinenin okuyacağı metinlerde Locale.ROOT kullanılır. Kullanıcıya gösterilecek Türkçe metinlerde ise Türkçe ayar açıkça verilir: metin.toUpperCase(Locale.forLanguageTag("tr-TR")). Böylece program hangi bilgisayarda çalışırsa çalışsın aynı sonucu üretir. Locale sınıfı java.util paketindedir ve import java.util.Locale; satırını gerektirir.

Sık yapılan hata

Kullanıcı girdisini girdi.toLowerCase().equals("exit") ile denetlemek. Türkçe ayarlı bir bilgisayarda kullanıcı EXIT yazarsa sonuç "exıt" olur ve koşul tutmaz. Hata yalnız bazı makinelerde ortaya çıktığı için bulunması zordur. Büyük-küçük harf farkını yok saymak için equalsIgnoreCase ya da toLowerCase(Locale.ROOT) kullanılır.

3.4.3.3Döngüde birleştirmenin maliyeti

Değiştirilemezliğin bedeli en çok döngüde birleştirme yapılırken hissedilir. Sunumdaki kodun ilk bölümü 1'den 5'e kadar sayıları bir dizgide toplar:

String sonuc = "";
for (int i = 1; i <= 5; i++) {
  sonuc += i + " ";   // her turda yeni String
}
System.out.println(sonuc);

StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
  sb.append(i).append(' ');   // aynı nesne büyür
}
String metin = sb.toString();
System.out.println(metin);

StringBuilder ters = new StringBuilder("Giresun");
ters.reverse();
System.out.println(ters);
StringBuilder k = new StringBuilder("Java");
k.insert(0, ">> ").append('!');
k.setCharAt(3, 'j');
System.out.println(k + " " + k.length());
1 2 3 4 5 
1 2 3 4 5 
nuseriG
>> java! 8

İlk döngüde sonuc += i + " "; satırı her turda yeni bir String nesnesi üretir. Eski içerik ve yeni parça bu nesneye kopyalanır, eski nesne de çöpe kalır. Bir turda kopyalanan karakter sayısı, o ana kadar biriken metnin uzunluğu kadardır. Her tur bir karakter ekliyorsa kopyalanan toplam karakter sayısı 1 + 2 + … + n = n(n + 1)/2 olur, yani yaklaşık n²/2. Örneğin bin turluk bir döngüde 500.500 karakter kopyalanır. Bu büyüme O(n²) ile gösterilir: tur sayısı iki katına çıkınca iş dört katına çıkar.

Beş turluk bu örnekte fark hissedilmez. Her tur iki karakter (rakam ve boşluk) ekler; toplam 2 + 4 + 6 + 8 + 10 = 30 karakter kopyalanır. Ama on binlerce satırlık bir rapor ya da büyük bir dosyanın içeriği bu yolla kurulursa program belirgin biçimde yavaşlar.

3.4.3.4StringBuilder

Tanım 3.4.7

StringBuilder, değiştirilebilir (mutable) bir karakter tamponudur (buffer). İçinde karakterleri tutan bir dizi ve bu dizinin kaç hücresinin dolu olduğunu gösteren bir uzunluk bulunur. append, insert, reverse, setCharAt ve deleteCharAt gibi metotlar yeni nesne üretmez; aynı nesnenin içeriğini değiştirir. İş bitince toString() ile bir String elde edilir.

Kodun ikinci bölümü aynı metni StringBuilder ile kurar. sb.append(i).append(' ') her turda tamponun sonuna bir sayı ve bir boşluk ekler. Tampon tek bir nesnedir ve gerektikçe büyür. Döngü bitince sb.toString() içerikten bir String üretir. İki yöntemin çıktısı aynıdır: 1 2 3 4 5 (sonda bir boşlukla).

append metodu tamponun kendisini döndürür. Bu yüzden çağrılar zincir gibi art arda yazılabilir: sb.append(i).append(' '). İlk append sb'yi döndürür, ikinci append yine sb üzerinde çalışır. append'in her ilkel tip ve String için ayrı sürümü vardır; sayı, karakter, mantıksal değer hepsi metne çevrilerek eklenir.

Kodun son bölümü diğer metotları dener. new StringBuilder("Giresun") içeriği "Giresun" olan bir tampon oluşturur. reverse() tamponu yerinde ters çevirir ve nuseriG yazılır. println'e bir StringBuilder verildiğinde toString() kendiliğinden çağrılır. Son örnekte "Java" tamponunun başına insert(0, ">> ") ile üç karakter eklenir, sonuna '!' konur ve indis 3'teki J setCharAt ile j yapılır. Sonuç >> java!, uzunluk 8'dir. String ile bu işlem mümkün değildir; karakteri değiştirmek için her seferinde yeni bir dizgi kurmak gerekir.

Sunumdaki izleme slaytı tamponun içini hücre hücre gösterir:

StringBuilder sb = new StringBuilder("Java");
sb.append(' ').append(8);
sb.insert(0, ">> ");
sb.setCharAt(3, 'j');
sb.deleteCharAt(sb.length() - 1);
sb.append("programlama dili");
String s = sb.toString();
System.out.println(s + " " + sb.length() + " " + sb.capacity());
>> java programlama dili 24 42

Bir StringBuilder'ın iki ölçüsü vardır. Uzunluk (length()) dolu hücre sayısıdır. Kapasite (capacity()) ise içerideki dizinin toplam hücre sayısıdır. Bir dizgiden oluşturulan tamponun kapasitesi, dizginin uzunluğuna 16 eklenerek bulunur. "Java" için bu 4 + 16 = 20'dir. Boş bir new StringBuilder()'ın kapasitesi ise 16'dır. Tablo 3.4.6 her satırdan sonraki durumu verir; değerler length() ve capacity() yazdırılarak doğrulanmıştır.

Tablo 3.4.6: StringBuilder izlemesi. Köşeli parantez içindeki metin tamponun dolu kısmıdır.
SatırİçerikUzunlukKapasiteAçıklama
new StringBuilder("Java")[Java]42016 + 4
append(' ').append(8)[Java 8]620sona eklenir, nesne aynı
insert(0, ">> ")[>> Java 8]920eski karakterler 3 hücre sağa kayar
setCharAt(3, 'j')[>> java 8]920indis 3 yerinde değişir
deleteCharAt(length() - 1)[>> java ]820sondaki 8 silinir
append("programlama dili")[>> java programlama dili]244224 > 20: tampon büyür
toString()—2442içerikten yeni bir String

insert ve deleteCharAt ucuz işlemler değildir. Başa bir şey eklemek için sonraki bütün karakterlerin sağa kaydırılması gerekir; ortadan silmek de sola kaydırma ister. Bu kaydırmalar tampon uzunluğuyla orantılı iş yapar. append ise yer olduğu sürece yalnız sona yazar ve sabit zamanda biter.

Son append 16 karakter ekler ve uzunluk 24'e çıkar. 24 karakter 20 hücreye sığmaz. Bu durumda StringBuilder daha büyük yeni bir dizi ayırır, eski içeriği oraya kopyalar ve eski diziyi bırakır. JDK'daki kural yeni kapasiteyi eskisinin iki katının iki fazlası olarak seçer: 2 × 20 + 2 = 42. Sunumdaki (20 + 1) × 2 yazımı da aynı sayıyı verir. Gereken uzunluk bundan da büyükse doğrudan o uzunluk alınır. Şekil 3.4.6 büyüme anını gösterir.

>>·Java·8length = 9capacity = 20 >>·java·programlama·dili…length = 24capacity = 4220 → 42: yeni dizi, içerik kopyalandı
Şekil 3.4.6: StringBuilder tamponu. (a) insert(0, ">> ") sonrası: eski karakterler üç hücre sağa kaymıştır. (b) 24 karakter 20 hücreye sığmaz; kapasite 42'ye çıkar ve içerik yeni diziye kopyalanır.

Büyüme sırasında kopyalama yapıldığı hâlde StringBuilder neden hızlıdır? Kapasite her büyümede yaklaşık iki katına çıkar. Boş bir tampona karakterler tek tek eklendiğinde kapasite 16, 34, 70, 142, 286, 574, 1150 diye ilerler. Bin karakter eklemek için yalnız altı büyüme gerekir. Bu büyümelerde kopyalanan toplam karakter sayısı, eklenen karakter sayısının birkaç katını geçmez. Bu yüzden n karakter eklemenin toplam maliyeti O(n)'dir; += ile yapılan birleştirmenin O(n²) maliyetiyle karşılaştırıldığında fark çok büyüktür.

Bu, + operatörünün hiç kullanılmaması gerektiği anlamına gelmez. Birkaç parçalık birleştirmelerde + okunaklıdır ve yeterince hızlıdır; Java derleyicisi tek bir ifadedeki birleştirmeleri zaten verimli biçimde çevirir. StringBuilder, bir döngü içinde çok sayıda parça birleştirilirken tercih edilir. Yaklaşık uzunluk baştan biliniyorsa new StringBuilder(1000) gibi bir başlangıç kapasitesi vermek büyüme sayısını azaltır.

3.4.4Kullanıcı girişi: Scanner

Bir program çoğu zaman kullanıcıdan bilgi ister: bir ad, bir yaş, bir cümle. Klavyeden gelen her şey önce karakter olarak gelir. Bu karakterlerin kelimeye, satıra ya da sayıya dönüştürülmesi Scanner sınıfının işidir. Okuma metotları birbirine benzese de davranışları ince noktalarda ayrılır. Bu ayrımlar bilinmezse programlar kullanıcının sorusunu atlamak gibi tuhaf hatalar verir.

3.4.4.1Scanner ve okuma metotları

Tanım 3.4.8

java.util.Scanner, bir kaynaktaki metni parçalara ayırarak okuyan sınıftır. new Scanner(System.in) klavyeden okuyan bir nesne oluşturur. Klavyede yazılan satır Enter'a basılınca bir girdi tamponuna girer; satır sonu karakteri (⏎) de tampona eklenir. Scanner tamponda bir imleç tutar ve her okuma imleci okunan kısmın sonuna taşır.

Dört okuma metodu sık kullanılır:

  • next() önce baştaki boşlukları (boşluk, sekme, satır sonu) atlar, sonra bir sonraki boşluğa kadar okur. Sonuç tek bir kelimedir.
  • nextLine() imlecin bulunduğu yerden satır sonuna kadar her şeyi okur. Boşlukları atlamaz. Satır sonu karakterini tüketir ama sonuca katmaz.
  • nextInt() ve nextDouble() baştaki boşlukları atlar ve bir sayı okur. Sayının arkasındaki satır sonunu tamponda bırakır. Sayı yerine harf girilirse InputMismatchException fırlatır.

Klavyeden okuyan bir program dosyanın başında import java.util.Scanner; satırını içerir. Okumalar bitince klavye.close() çağrılır. Sunumdaki örnek bir satırı önce iki kelime, sonra satırın geri kalanı olarak okur:

Scanner klavye = new Scanner(System.in);
System.out.println("bir metin giriniz");
String girdi = klavye.next();
System.out.println(girdi);    // bilgisayar
girdi = klavye.next();
System.out.println(girdi);    // ve
girdi = klavye.nextLine();
System.out.println(girdi);    //  programlamaya giriş

Kullanıcı şu satırı yazar ve Enter'a basar:

bilgisayar ve programlamaya giriş

Program şunu yazar:

bir metin giriniz
bilgisayar
ve
 programlamaya giriş

Çıktının son satırındaki baştaki boşluk dikkat çeker. “ve” kelimesinden sonra gelen boşluk satırın bir parçasıdır ve nextLine() boşlukları atlamaz. Bu yüzden okunan metin " programlamaya giriş" olur.

3.4.4.2next() ve nextLine() adım adım

Tablo 3.4.7 her okumada imlecin nereden nereye ilerlediğini verir. Tampondaki karakterlerin indisleri 0'dan başlar: "bilgisayar" 0–9, ilk boşluk 10, "ve" 11–12, ikinci boşluk 13, kalan metin 14–32 indislerini kaplar ve satır sonu 33'tedir.

Tablo 3.4.7: bilgisayar ve programlamaya giriş⏎ satırı okunurken imlecin konumu.
Çağrıİmleç önceOkunanİmleç sonraAçıklama
next()0"bilgisayar"10ilk boşluğa kadar okur; boşluk tamponda kalır
next()10"ve"13önce 10'daki boşluğu atlar
nextLine()13" programlamaya giriş"34boşluğu atlamaz; ⏎'yi tüketir, sonuca katmaz

Şekil 3.4.7 ikinci ve üçüncü okumanın sonunda tamponun durumunu gösterir. Okunmuş karakterler soluk, imlecin yeri bir üçgenle işaretlidir.

Girdi tamponubilgisayar␣ve␣programlamaya␣giriş⏎imleç Girdi tamponubilgisayar␣ve␣programlamaya␣giriş⏎imleç
Şekil 3.4.7: Girdi tamponu. (a) İkinci next() boşluğu atlayıp "ve"'yi okur; imleç yine bir boşluğun önündedir. (b) nextLine() boşluğu atlamadan satır sonuna kadar okur ve ⏎'yi tüketir.

Programcı açısından sonuç şudur: tek kelimelik bir yanıt (ad, şehir, komut) için next(), boşluk içerebilen bir yanıt (ad soyad, adres, cümle) için nextLine() kullanılır. next() ile ad soyad okunursa yalnız ad alınır, soyad tamponda kalır ve bir sonraki okumaya karışır.

3.4.4.3nextInt() sonrası nextLine() tuzağı

Sayı okuyan metotlarla nextLine() art arda kullanıldığında yeni başlayanların neredeyse hepsinin karşılaştığı bir hata ortaya çıkar. Sunumdaki program önce yaşı, sonra adı sorar:

Scanner klavye = new Scanner(System.in);
System.out.print("Yaş: ");
int yas = klavye.nextInt();
System.out.print("Ad: ");
String ad = klavye.nextLine();
System.out.println("[" + ad + "] " + yas);

Kullanıcı 19 yazıp Enter'a basar. Program adı sormadan biter:

Yaş: 19
Ad: [] 19

Neden böyle olduğu tampona bakınca anlaşılır (Şekil 3.4.8). Tamponda 19⏎ vardır. nextInt() yalnız 19'u okur ve imleci ⏎'nin önünde bırakır. Ardından gelen nextLine() görevini yerine getirir: imleçten satır sonuna kadar okur. İmleç ile satır sonu arasında hiçbir karakter olmadığı için sonuç boş dizgidir. nextLine() ⏎'yi tüketir ve hemen döner. Kullanıcı daha adını yazmaya fırsat bulamadan ad değişkeni "" olmuştur.

nextInt() sonrası tampon19⏎Ayşe⏎imleç ⏎'nin önünde
Şekil 3.4.8: nextInt() 19'u okuduktan sonra imleç satır sonu karakterinin önünde bekler.

Çözüm, sayıyı okuduktan sonra artakalan satır sonunu ayrı bir nextLine() çağrısıyla tüketmektir:

int yas = klavye.nextInt();
klavye.nextLine();          // ⏎'yi at
System.out.print("Ad: ");
String ad = klavye.nextLine();
Yaş: 19
Ad: Ayşe
[Ayşe] 19

Araya eklenen klavye.nextLine(); çağrısının sonucu kullanılmaz; tek görevi ⏎'yi tamponda temizlemektir. Bundan sonraki nextLine() kullanıcının yeni satırını bekler. İkinci bir çözüm, bütün girdiyi nextLine() ile okuyup sayıları kendisi çevirmektir: int yas = Integer.parseInt(klavye.nextLine().trim());. Bu yöntemde tamponda hiçbir zaman artık satır sonu kalmaz.

Örnek 3.4.2

Kullanıcıdan önce yaş, sonra ad-soyad isteyen ve sonucu printf ile yazan program, tuzağa düşmeden şöyle yazılır:

Scanner k = new Scanner(System.in);
System.out.print("Yaş: ");
int yas = k.nextInt();
k.nextLine();   // satır sonunu at
System.out.print("Ad: ");
String ad = k.nextLine();
System.out.printf("Ad: %s, Yaş: %d%n", ad, yas);

Kullanıcı 20 ve Ali Veli girdiğinde konsol şöyle görünür:

Yaş: 20
Ad: Ali Veli
Ad: Ali Veli, Yaş: 20

nextInt() 20'yi okur, ⏎ tamponda kalır. k.nextLine(); bu ⏎'yi tüketir. Son nextLine() kullanıcının yeni satırını bekler ve "Ali Veli"'yi boşluğuyla birlikte okur. Ad soyad için next() kullanılsaydı yalnız "Ali" okunurdu. printf satırı biçimli çıktı kesiminde açıklanıyor.

3.4.5Dizgileri karşılaştırmak

İki dizginin aynı olup olmadığını sormak basit görünür. Ama Java'da bu sorunun iki ayrı anlamı vardır: “iki değişken aynı nesneyi mi gösteriyor?” ve “iki nesnenin içeriği aynı mı?”. İlk soru == ile, ikincisi equals() ile sorulur. Bu kesim iki sorunun farkını, farkı bazen gizleyen dizgi havuzunu ve sıralama için kullanılan compareTo metodunu anlatır.

3.4.5.1== ile karşılaştırma

int gibi ilkel tiplerde == iki değeri karşılaştırır. String değişkenleri ise değer değil referans tutar. Bu yüzden == iki dizginin referanslarını, yani bellekteki adreslerini karşılaştırır, içeriklerini değil. İki değişken aynı nesneyi gösteriyorsa sonuç true'dur. İçerik aynı olsa bile nesneler farklıysa sonuç false'tur. Sunumdaki örnek:

String str1 = "Giresun";
String str2 = "Giresun";
String str3 = new String("Giresun");
System.out.println(str1 == str2);        // true
System.out.println(str1 == "Giresun");   // true
System.out.println(str1 == str3);        // false
System.out.println(str3 == "Giresun");   // false
true
true
false
false

Dört karşılaştırmada içerik hep "Giresun"'dur, ama sonuçların ikisi false'tur. str1 == str2'nin true çıkması bir şans değildir; dizgi havuzunun sonucudur. str3 ise new ile oluşturulmuş ayrı bir nesnedir.

3.4.5.2Dizgi havuzu

Tanım 3.4.9

Dizgi havuzu (string pool), JVM'in dizgi sabitlerini tuttuğu özel bölgedir. Kaynak kodda içeriği aynı olan bütün dizgi sabitleri havuzdaki tek bir nesneyi paylaşır. new String(...) ise havuza bakmaz; her çağrısında heap'te yeni bir nesne oluşturur.

Havuzun amacı bellek tasarrufudur. Bir programda "Giresun" sabiti yüz farklı yerde geçebilir. Her geçişte yeni bir nesne oluşturmak yerine hepsi aynı nesneyi gösterir. Bu paylaşım ancak dizgiler değiştirilemez olduğu için güvenlidir. Bir yerdeki kod "Giresun" nesnesini değiştirebilseydi öteki doksan dokuz yer de etkilenirdi.

Tablo 3.4.8 sunumdaki havuz izlemesini satır satır verir. Nesneler #1 ve #2 diye numaralandırılmıştır.

Tablo 3.4.8: Dizgi havuzu izlemesi. #1 havuzdaki nesne, #2 havuz dışındaki nesnedir.
Satırstr1str2str3Açıklama
String str1 = "Giresun";#1sabit havuzdaki #1'i verir
String str2 = "Giresun";#1#1aynı sabit: yeni nesne oluşmaz
String str3 = new String("Giresun");#1#1#2new havuz dışında yeni nesne üretir
str1 == str2#1 ile #1true
str1 == "Giresun"#1 ile #1true
str1 == str3#1 ile #2false
str3 == "Giresun"#2 ile #1false
Yığın (stack)Heapmain()dizgi havuzu"Giresun""Giresun"str1str2str3
Şekil 3.4.9: Üç bildirimden sonra bellek. str1 ve str2 havuzdaki aynı nesneyi, str3 havuz dışındaki ayrı bir nesneyi gösterir.

Havuza giren dizgiler yalnız kaynak koddaki sabitlerle sınırlı değildir. Derleme zamanında hesaplanabilen ifadeler de sabit sayılır. "Gire" + "sun" ifadesinin iki yanı da sabittir; derleyici onu doğrudan "Giresun" olarak yazar ve sonuç havuzdaki nesneyi gösterir. Bir yanında değişken bulunan birleştirme ise çalışma zamanında yapılır ve yeni bir nesne üretir. final ile tanımlanıp sabitle başlatılan bir değişken ise derleyici için bir sabittir:

String str1 = "Giresun";
String a = "Gire";
final String fa = "Gire";
System.out.println((a + "sun") == str1);   // false
System.out.println((fa + "sun") == str1);  // true

intern() metodu bir dizginin havuzdaki eşini döndürür. Havuzda aynı içerikli bir nesne varsa o nesnenin referansı, yoksa dizgi havuza eklenir ve kendisi döner. str3.intern() == str1 bu yüzden true'dur. intern() gündelik kodda nadiren gerekir; varlığı havuzun nasıl çalıştığını anlamaya yardım eder.

Havuz ve değiştirilemezlik adım adım izlenmek istenirse ders sitesindeki “Dizgi Havuzu” simülatörü kullanılabilir. Simülatör, sabitlerin havuza girişini, new String ile oluşan ayrı nesneleri ve StringBuilder tamponunun yerinde değişmesini görsel olarak gösterir.

3.4.5.3equals() ile içerik karşılaştırma

Tanım 3.4.10

a.equals(b), a ve b dizgilerinin içeriğini karakter karakter karşılaştırır. Uzunluklar ve her konumdaki karakter aynıysa true döndürür. Nesnelerin aynı olup olmadığına bakmaz. b null ise false döndürür.

Sunumdaki örnek aynı üç değişkeni bu kez equals ile karşılaştırır ve birkaç durum ekler:

String str1 = "Giresun";
String str2 = "Giresun";
String str3 = new String("Giresun");
System.out.println(str1.equals(str2));
System.out.println(str1.equals("Giresun"));
System.out.println(str1.equals(str3));
System.out.println(str3.equals("Giresun"));
System.out.println(str3.intern() == str1);
String a = "Gire";
String b = a + "sun";  // yeni nesne
System.out.println(b == str1);
System.out.println(b.equals(str1));
true
true
true
true
true
false
true

İlk dört satır, nesne hangi yolla oluşmuş olursa olsun true verir; içerik hep "Giresun"'dur. Beşinci satır intern()'ün havuzdaki eşi döndürdüğünü gösterir. Son iki satırda b, a değişkeniyle "sun" sabitinin çalışma zamanında birleştirilmesiyle oluşur. Ortaya çıkan nesne yenidir: b == str1 false, b.equals(str1) true'dur.

Pratikte en tehlikeli durum kullanıcı girdisidir. Scanner'dan okunan bir dizgi çalışma zamanında oluşur ve her zaman yeni bir nesnedir. Bu yüzden girdi == "evet" koşulu, kullanıcı tam olarak evet yazsa bile neredeyse her zaman false verir. Hata sinsidir: dizgi sabitleriyle yapılan denemelerde havuz yüzünden == doğru çalışıyor gibi görünür.

equals çağrısının bir de null yönü vardır. null, hiçbir nesneyi göstermeyen referans değeridir. null bir referans üzerinden metot çağrılamaz:

String cevap = null;
System.out.println("evet".equals(cevap)); // false
System.out.println(cevap + "!");          // null!
System.out.println(cevap.equals("evet")); // istisna

İlk satırda metot "evet" sabiti üzerinden çağrılır; sabit hiçbir zaman null olmadığı için çağrı güvenlidir ve null argümanla false döner. İkinci satırda birleştirme null'ı "null" metnine çevirir; istisna oluşmaz ve null! yazılır. Üçüncü satır ise NullPointerException fırlatır. Java 15'ten beri bu istisnanın iletisi varsayılan olarak hangi çağrının neden başarısız olduğunu açıkça söyler:

Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "String.equals(Object)" because "cevap" is null

İletideki değişken adı ("cevap"), sınıf dosyası hata ayıklama bilgisiyle derlendiğinde görünür. Geliştirme ortamları bu bilgiyi varsayılan olarak ekler. Komut satırında yalnız javac ile derlenen bir programda ise ad yerine yerel değişkenin sıra numarası yazılır:

Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "String.equals(Object)" because "<local1>" is null

javac -g ile derlenince ileti yine "cevap" adını gösterir. İki ileti de aynı hatayı anlatır: null olan bir değişken üzerinden equals çağrılmıştır. Güvenli yazım, sabiti öne koymaktır: "evet".equals(cevap).

3.4.5.4Karşılaştırma metotları

equals dışında dizgiler için birkaç karşılaştırma metodu daha vardır. Tablo 3.4.9 bunları döndürdükleri tiple birlikte listeler.

Tablo 3.4.9: Dizgi karşılaştırma metotları.
MetotDöndürdüğüAçıklama
equals(str)booleanİçerik birebir aynı mı?
equalsIgnoreCase(str)booleanBüyük-küçük harf farkı yok sayılınca aynı mı?
startsWith(str)booleanDizgi verilen metinle başlıyor mu?
endsWith(str)booleanDizgi verilen metinle bitiyor mu?
contains(str)booleanVerilen metin dizginin içinde geçiyor mu?
isEmpty() / isBlank()booleanUzunluk 0 mı? / Yalnız boşluktan mı oluşuyor? (isBlank: Java 11+)
compareTo(str)intSözlük sırası: negatif (önce), 0 (eşit), pozitif (sonra).
compareToIgnoreCase(str)intHarf büyüklüğünü yok sayan compareTo.

Adında IgnoreCase geçmeyen metotlar büyük-küçük harfe duyarlıdır. equals ve equalsIgnoreCase, null argüman alınca istisna fırlatmaz, false döndürür. Sunumdaki örnek bu metotları "Giresun Üni." dizgisi üzerinde dener; yorumlardaki sonuçlar programın gerçek çıktısıdır:

String s = "Giresun Üni.";
System.out.println(s.startsWith("Gir"));                // true
System.out.println(s.startsWith("Gır"));                // false
System.out.println(s.endsWith("Üni."));                 // true
System.out.println(s.endsWith("Üni"));                  // false
System.out.println(s.equalsIgnoreCase("Giresun Üni.")); // true
System.out.println(s.equalsIgnoreCase("Gıresun üni.")); // true
System.out.println(s.equalsIgnoreCase("GIresun Uni.")); // false
System.out.println(s.contains("Üni"));                  // true

Sonuçların bazıları ilk bakışta şaşırtıcıdır:

  • startsWith("Gır") false'tur. ı ile i ayrı karakterlerdir; Unicode kodları 305 ve 105'tir.
  • endsWith("Üni") false'tur, çünkü dizgi noktayla biter. Son karakter '.''dır.
  • equalsIgnoreCase("Gıresun üni.") true'dur. Metot karakterleri karşılaştırırken her iki yanı da büyük harfe çevirir. ı ile i'nin büyük biçimleri aynıdır, I; bu yüzden iki dizgi eşit sayılır. equalsIgnoreCase yerel ayara bakmaz; her makinede aynı sonucu verir.
  • equalsIgnoreCase("GIresun Uni.") false'tur. U ile Ü ayrı harflerdir ve hiçbir büyük-küçük dönüşümü onları eşitlemez.
Giresun·Üni.startsWith("Gir")endsWith("Üni.")contains("Üni")
Şekil 3.4.10: startsWith, contains ve endsWith metotlarının eşleştiği bölgeler.

Boş dizgiyle yapılan karşılaştırmalar da bilinmeye değer. Her dizgi boş dizgiyle başlar ve onu içerir: s.startsWith("") ve s.contains("") her zaman true'dur. s.indexOf("") ise 0 döndürür.

3.4.5.5compareTo ve sözlük sırası

Tanım 3.4.11

a.compareTo(b), iki dizgiyi sözlük sırasına (lexicographic order) göre karşılaştırır. a önce geliyorsa negatif, eşitse 0, sonra geliyorsa pozitif bir int döndürür. Karşılaştırma baştan başlar ve ilk farklı karakterde biter; sonuç bu iki karakterin Unicode kodlarının farkıdır. Farklı karakter yoksa ve biri ötekinin önekiyse sonuç uzunlukların farkıdır.

Sunumdaki örnek kuralın bütün durumlarını içerir:

System.out.println("elma".compareTo("armut"));
System.out.println("armut".compareTo("elma"));
System.out.println("elma".compareTo("elma"));
System.out.println("Elma".compareTo("elma"));
System.out.println("Elma".compareToIgnoreCase("elma"));
System.out.println("ev".compareTo("evet"));
System.out.println("çay".compareTo("dut"));
4
-4
0
-32
0
-2
131

Satırları kurala göre okuyalım:

  • "elma".compareTo("armut"): ilk karakterler farklıdır. 'e' − 'a' = 101 − 97 = 4. Pozitif: “elma” “armut”tan sonra gelir.
  • "armut".compareTo("elma"): aynı karakterler ters sırada, −4. Yer değiştirince işaret değişir.
  • Eşit dizgiler 0 verir.
  • "Elma".compareTo("elma"): 'E' − 'e' = 69 − 101 = −32. Unicode'da büyük harfler küçük harflerden önce gelir.
  • compareToIgnoreCase harf büyüklüğünü yok sayar ve 0 verir.
  • "ev".compareTo("evet"): ilk iki karakter aynıdır ve "ev" biter. Biri ötekinin önekidir; sonuç uzunluk farkıdır: 2 − 4 = −2.
  • "çay".compareTo("dut"): 'ç''nin kodu 231, 'd''nin kodu 100'dür. 231 − 100 = 131. Pozitif bir sonuç, “çay”ın “dut”tan sonra geldiği anlamına gelir.

Son satır önemli bir sınırı gösterir: compareTo Türkçe alfabe sırasını bilmez. Türkçede ç, c ile d arasındadır. Ama Unicode'da Türkçeye özgü harflerin kodları Latin harflerinden büyüktür: ç 231, ı 305, ş 351, ü 252, z ise 122. Unicode sırasına göre sıralanan bir Türkçe kelime listesinde ç, ı, ş ile başlayan kelimeler z'den sonraya düşer. Doğru Türkçe sıralama için java.text.Collator kullanılır:

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

String[] m = {"zeytin", "çilek", "Elma", "armut", "ıhlamur", "incir"};
String[] m2 = m.clone();
Arrays.sort(m);                       // Unicode sırası
System.out.println(Arrays.toString(m));
Collator tr = Collator.getInstance(Locale.forLanguageTag("tr-TR"));
Arrays.sort(m2, tr);                  // Türkçe alfabe sırası
System.out.println(Arrays.toString(m2));
[Elma, armut, incir, zeytin, çilek, ıhlamur]
[armut, çilek, Elma, ıhlamur, incir, zeytin]

Arrays.sort dizgileri compareTo ile sıralar. Sonuçta büyük harfle başlayan “Elma” en başa, “çilek” ve “ıhlamur” en sona düşer. Türkçe Collator ise alfabe sırasını uygular ve büyük-küçük harf farkını sıralamada ikinci plana atar. Kullanıcıya gösterilecek listelerde ikinci yol seçilir.

Sunumdaki izleme slaytı compareTo'nun adımlarını üç dizgi çifti üzerinde gösterir:

String a = "masa", b = "mavi";
int r1 = a.compareTo(b);
int r2 = "kar".compareTo("karne");
int r3 = "Zeki".compareTo("ali");
System.out.println(r1 + " " + r2 + " " + r3);
-3 -2 -7
Tablo 3.4.10: compareTo izlemesi. k karşılaştırılan indistir.
ÇiftkKarakterler (kodlar)Durum
"masa", "mavi"0'm' (109), 'm' (109)eşit, devam
1'a' (97), 'a' (97)eşit, devam
2's' (115), 'v' (118)farklı: 115 − 118 = −3
"kar", "karne"0–2hepsi eşit; "kar" biterönek: 3 − 5 = −2
"Zeki", "ali"0'Z' (90), 'a' (97)farklı: 90 − 97 = −7
"masa"mas115a"mavi"mav118i= -3
Şekil 3.4.11: "masa" ile "mavi" indis 2'de ayrılır; sonuç kodların farkıdır, 115 − 118 = −3.

Üçüncü çift, büyük harf kuralının ne kadar ileri gittiğini gösterir: Z bile a'dan önce gelir. Sonucun büyüklüğü genellikle bir anlam taşımaz; önemli olan işaretidir. Bu yüzden karşılaştırma if (a.compareTo(b) < 0) biçiminde yazılır, == -1 biçiminde yazılmaz. < ve > operatörleri dizgilerle çalışmaz; derleyici “bad operand types” hatası verir:

Kiyas.java:4: error: bad operand types for binary operator '<'
        if (a < b) System.out.println(a);
              ^
  first type:  String
  second type: String

3.4.6Biçimli çıktı

println değeri olduğu gibi yazar. Bir not çizelgesi, bir fatura ya da bir ölçüm tablosu yazdırılırken bu yetmez. Sütunların hizalı olması, ondalıklı sayıların belli sayıda basamakla yazılması gerekir. Java bu iş için printf metodunu ve onunla aynı kuralları kullanan String.format'ı sunar.

3.4.6.1printf ve biçim belirteçleri

Tanım 3.4.12

System.out.printf(biçim, parametreler…) bir biçim dizgisi ve ardından istenen sayıda parametre alır. Biçim dizgisi, düz metnin arasına yerleştirilmiş biçim belirteçleri (format specifier) içerir. Her belirteç % ile başlar ve bir tip harfiyle biter; aradaki isteğe bağlı kısım bayrak, genişlik ve hassasiyeti verir. Parametreler belirteçlerin yerine sırayla yerleşir.

Bir belirtecin genel biçimi %[bayrak][genişlik][.hassasiyet]tip'tir (Şekil 3.4.12). %-8.2f belirtecinde - bayrağı sola yaslar, 8 genişliği, .2 hassasiyeti, f ise tipi verir.

%başlangıç-sola yasla8genişlik.2hassasiyetftip
Şekil 3.4.12: Bir biçim belirtecinin parçaları: başlangıç (%), bayrak, genişlik, hassasiyet ve tip.

Tablo 3.4.11 en sık kullanılan tip harflerini verir. Ondalık ayırıcı yerel ayara bağlıdır. Türkçe ayarda virgül, Locale.US ile nokta kullanılır.

Tablo 3.4.11: Temel biçim belirteçleri (çıktılar tr-TR ayarında).
BelirteçTipÖrnek → çıktı
%dTam sayı (byte, short, int, long)42 → 42
%fOndalıklı (float, double)%.1f: 3.14159 → 3,1
%sDizgi (her değer metne çevrilir)%5s: "ab" → ␣␣␣ab
%cKarakter'Z' → Z
%bMantıksaltrue → true
%nSatır sonu—

printf kendiliğinden alt satıra geçmez. Satır sonu için biçim dizgisinin sonuna \n ya da %n eklenir. %n işletim sisteminin satır sonu karakterini kullanır; Windows'ta iki karakterlik \r\n, Linux ve macOS'ta \n yazar. Bu yüzden taşınabilir kodda %n tercih edilir.

Genişlik, değerin kaplayacağı en az karakter sayısıdır. Değer daha kısaysa boşlukla doldurulur: %Wd sağa yaslar, %-Wd sola yaslar. Değer genişlikten uzunsa kesilmez, alan büyür. Hassasiyet ondalıklı sayılarda virgülden sonraki basamak sayısıdır ve son basamak yuvarlanır: %.2f iki basamak yazar. %8.2f ise iki ondalık basamakla birlikte toplam 8 karakterlik bir alan ayırır.

3.4.6.2Genişlik ve hassasiyet

Sunumdaki örnek aynı üç değeri dört farklı biçimle yazar:

int i = 3;
float f = -17;
String s = "java";

System.out.printf("x: %d y: %f s: %s\n", i, f, s);
System.out.printf("x: %4d y: %5.2f s: %6s\n", i, f, s);
System.out.printf("x: %-4d y: %-7.2f s: %-6s\n", i, f, s);
System.out.printf("x: %-4d y: %-6.3f s: %-6s\n", i, f, s);
x: 3 y: -17,000000 s: java
x:    3 y: -17,00 s:   java
x: 3    y: -17,00  s: java··
x: 3    y: -17,000 s: java··

Çıktı Türkçe ayarda alınmıştır. Satır sonundaki boşluklar görünsün diye · ile gösterildi. Değişkenlerin ikisi açıklama ister: f bir float'tır ve -17 tam sayısı ona atanınca -17,0 olur. %f'ye hassasiyet verilmezse 6 ondalık basamak yazılır. Satırları sırayla inceleyelim:

  1. Genişlik verilmemiştir. Her değer kendi uzunluğu kadar yer kaplar: 3, -17,000000, java.
  2. %4d 3'ü dört karakterlik alana sağa yaslar: üç boşluk ve 3. %5.2f iki ondalıkla -17,00 yazar; bu 6 karakterdir ve 5 genişliğe sığmaz, alan büyür. %6s dört harfli java'nın soluna iki boşluk koyar.
  3. Eksi bayrağı sola yaslar; boşluklar değerin sağına eklenir. %-4d → 3···, %-7.2f → -17,00·, %-6s → java··.
  4. %-6.3f üç ondalıkla -17,000 yazar. Değer 7 karakterdir ve 6 genişlikten uzundur; dolgu yapılmaz.

Şekil 3.4.13 ikinci satırı karakter karakter gösterir. Her belirtecin kapladığı alan alt kısımda işaretlidir.

x:␣␣␣␣3␣y:␣-17,00␣s:␣␣␣java%4d (4)%5.2f (6)%6s (6)
Şekil 3.4.13: "x: %4d y: %5.2f s: %6s" çıktısı karakter karakter. %4d 4, %5.2f 6 (sığmadığı için büyüyen alan), %6s 6 karakter kaplar.

Genişlik en çok tablo yazdırırken işe yarar. Her satırda aynı belirteçler kullanılınca sütunlar alt alta düzgün hizalanır. Metin sütunları genellikle sola (%-10s), sayı sütunları sağa (%6d, %8.2f) yaslanır. Böylece sayıların birler basamağı alt alta gelir.

Örnek 3.4.3

Aşağıdaki satırın Türkçe ayardaki çıktısını karakter karakter bulalım:

String ad = "Ali";
double not1 = 72.456;
System.out.printf("[%-5s][%3d][%7.2f]%n", ad, 7, not1);

%-5s: "Ali" üç harflidir, beş karakterlik alana sola yaslanır ve sağına iki boşluk eklenir: Ali··. %3d: 7 tek basamaklıdır, soluna iki boşluk gelir: ··7. %7.2f: 72,456 iki ondalığa yuvarlanır, 72,46 olur. Bu metin 5 karakterdir; yedi karakterlik alanı doldurmak için başa iki boşluk eklenir: ··72,46. Sonuç:

[Ali  ][  7][  72,46]

Yuvarlama kuralı da bilinmeye değer. printf, sayının ondalık gösterimine bakar ve yarımı sıfırdan uzağa yuvarlar (HALF_UP). Math.round'dan farklı olarak negatif yarımlarda da sıfırdan uzaklaşır:

System.out.printf("[%.2f][%.2f][%.0f][%.0f][%.0f]%n", 2.345, 2.355, 0.5, 1.5, -2.5);
System.out.printf(Locale.US, "%,.2f %08.3f %+d %X%n", 1234567.891, 3.14159, 5, 255);
[2,35][2,36][1][2][-3]
1,234,567.89 0003.142 +5 FF

2,345 ve 2,355 beklendiği gibi yukarı yuvarlanır. 0,5 → 1, 1,5 → 2 ve −2,5 → −3 olur. İkinci satırdaki bayraklar sonraki alt kesimde açıklanıyor.

3.4.6.3Diğer biçimler ve String.format

System.out.printf("|%5s|%-5s|%05d|%.1f|%n", "ab", "ab", 42, 3.14159);
System.out.printf("%c %b %e %x %%%n", 'Z', true, 1234.5, 255);
String mesaj = String.format("%s: %d puan", "Ayşe", 85);
System.out.println(mesaj);
System.out.printf("%,d%n", 1234567);
System.out.printf(Locale.US, "%.2f%n", 2.5);       // import java.util.Locale;
System.out.println("%s = %d".formatted("x", 7));   // Java 15+
|   ab|ab   |00042|3,1|
Z true 1,234500e+03 ff %
Ayşe: 85 puan
1.234.567
2.50
x = 7

Bu örnekteki yeni öğeler şunlardır:

  • %05d: 0 bayrağı alanı boşluk yerine sıfırla doldurur: 00042. Fatura ve öğrenci numarası gibi sabit uzunluklu numaralarda kullanılır.
  • %e bilimsel gösterimdir: 1234,5 = 1,2345 × 103, çıktıda 1,234500e+03. %x onaltılık tabanda yazar: 255 → ff; büyük harf için %X. %% yüzde işaretinin kendisini yazar.
  • String.format aynı kuralları kullanır ama ekrana yazmaz; biçimlenmiş metni bir String olarak döndürür. Sonuç bir değişkende saklanabilir, bir dosyaya yazılabilir ya da başka bir metne eklenebilir.
  • %,d binlik ayırıcı koyar. Türkçe ayarda ayırıcı noktadır: 1.234.567. ABD ayarında virgüldür.
  • printf'in ilk parametresi bir Locale olabilir. Locale.US verilince ondalık ayırıcı nokta olur: 2.50. Başka bir programın okuyacağı veriler (CSV dosyası gibi) bu yolla yazılır.
  • Java 15 ile gelen formatted metodu biçim dizgisi üzerinden çağrılır: "%s = %d".formatted("x", 7) ile String.format("%s = %d", "x", 7) aynı sonucu verir.

Biçim dizgisi derleyici için sıradan bir metindir. Belirteç ile parametrenin tipi uyuşmazsa hata derleme sırasında değil, çalışırken ortaya çıkar. printf("%d", 3.5) çağrısı %d'nin beklediği tam sayı yerine bir double alır ve istisna fırlatır:

Exception in thread "main" java.util.IllegalFormatConversionException:
    d != java.lang.Double

İleti “d belirteci Double ile kullanılamaz” demektir. printf dönüşüm yapmaz; sayıyı kesip 3 yazmaz. Doğrusu %f ya da %.0f kullanmaktır. Belirteç sayısı parametre sayısından fazlaysa MissingFormatArgumentException fırlatılır. %s ise her tiple çalışır, çünkü her değer metne çevrilebilir.

3.4.7Uygulamalar

Önceki kesimlerin araçları, yani indisleme, döngü, karşılaştırma ve birleştirme, birlikte kullanılınca gerçek problemler çözülür. Bu kesim sunumdaki iki uygulamayı ve alıştırma slaytlarındaki küçük metotları ayrıntılı işler.

3.4.7.1Palindrom denetimi

Tanım 3.4.13

Palindrom, tersten okunuşu düzden okunuşuyla aynı olan metindir: “kayak”, “ana”, “kek”. Cümlelerde boşluk ve noktalama işaretleri yok sayılır.

Bir metnin palindrom olup olmadığını anlamanın en doğrudan yolu metni ters çevirip orijinaliyle karşılaştırmaktır:

String metin = "Kayak";
metin = metin.toLowerCase();
String ters = "";
for (int i = metin.length() - 1; i >= 0; i--) {
  ters += metin.charAt(i);
}
System.out.println(ters);
System.out.println(metin.equals(ters));
kayak
true

Kod üç adımda çalışır. Önce metin küçük harfe çevrilir; çevrilmeseydi K ile k farklı sayılırdı. toLowerCase() yeni bir dizgi döndürdüğü için sonuç yeniden metin'e atanır. İkinci adımda boş bir ters dizgisi hazırlanır ve döngü son indisten 0'a doğru iner. Her turda o indisteki karakter ters'in sonuna eklenir. Üçüncü adımda iki dizgi equals ile karşılaştırılır. Tablo 3.4.12 döngünün turlarını verir.

Tablo 3.4.12: metin = "kayak" için ters çevirme döngüsü.
ii >= 0charAt(i)ters (tur sonunda)
4doğru'k'"k"
3doğru'a'"ka"
2doğru'y'"kay"
1doğru'a'"kaya"
0doğru'k'"kayak"
−1yanlış—döngü biter
metink0a1y2a3k4terskayak
Şekil 3.4.14: Döngü bittiğinde metin ve ters. İçerikler aynıdır; equals true döndürür.

Karşılaştırmanın equals ile yapılması zorunludur. ters çalışma zamanında birleştirmeyle kurulmuş yeni bir nesnedir; metin == ters her zaman false verirdi. Döngüdeki ters += … satırı her turda yeni bir dizgi üretir. Kısa metinlerde bunun önemi yoktur, ama aynı iş new StringBuilder(metin).reverse().toString() ile hem kısa hem verimli yapılır.

Cümlelerde önce harf olmayan karakterler silinir. metin.replaceAll("[^\\p{L}]", "") çağrısı düzenli ifade kullanır: \p{L} Unicode'daki her harfi, [^…] ise “bunlar dışındaki her karakteri” anlatır. Böylece yalnız harfler kalır. “Ey Edip Adana'da pide ye” cümlesi temizlenip küçültülünce "eyedipadanadapideye" olur ve bu bir palindromdur.

Ters çevirme yöntemi metnin bir kopyasını oluşturur. Bellek harcamadan aynı denetim iki indisle de yapılır: biri baştan, biri sondan başlar ve ortada buluşana kadar karakterleri karşılaştırır. Bir eşitsizlik bulunduğu anda sonuç false'tur; geri kalan karakterlere bakmaya gerek kalmaz. İki yöntemin de maliyeti metnin uzunluğuyla doğru orantılıdır.

3.4.7.2Kelime frekansı

Bir metinde her kelimenin kaç kez geçtiğini saymak, metin işlemenin en temel problemlerinden biridir. Sunumdaki çözüm yalnız dizilerle ve iç içe iki döngüyle çalışır:

String metin = "Bir iki bir üç İki bir";
String[] kelimeler = metin.toLowerCase().split(" ");
boolean[] sayildi = new boolean[kelimeler.length];
for (int i = 0; i < kelimeler.length; i++) {
  if (sayildi[i]) {
    continue; // bu kelime daha önce sayıldı
  }
  int frekans = 1;
  for (int j = i + 1; j < kelimeler.length; j++) {
    if (kelimeler[i].equals(kelimeler[j])) {
      frekans++;
      sayildi[j] = true;
    }
  }
  System.out.println(kelimeler[i] + ": " + frekans);
}
bir: 3
iki: 2
üç: 1

Kod şu adımları izler:

  • metin.toLowerCase().split(" ") önce metni küçültür, sonra boşluklardan böler. Küçültme sayesinde “Bir/bir” ve “İki/iki” aynı kelime sayılır. Türkçe ayarda İ küçültülünce i olur. Dilden bağımsız kurallarla ise İ, i ile birleşen noktaya dönüşür ve “İki” ayrı bir kelime gibi sayılır; çıktı Türkçe ayara göredir.
  • sayildi dizisi her kelime için bir bayrak tutar. boolean dizisi oluşturulduğunda bütün öğeler false'tur.
  • Dış döngü her kelimeye bir kez bakar. Kelimenin bayrağı true ise o kelime daha önce sayılmıştır; continue turun geri kalanını atlar.
  • İç döngü yalnız sağdaki kelimeleri tarar. Eşleşen her kelime için sayaç artar ve eşleşenin bayrağı kaldırılır. Böylece aynı kelime ikinci kez raporlanmaz.

Kelimeler equals ile karşılaştırılır. split'in ürettiği parçalar çalışma zamanında oluşan ayrı nesnelerdir. == kullanılsaydı hiçbir eşleşme bulunmaz ve her kelimenin frekansı 1 çıkardı. Bu durum çalıştırılarak doğrulandı: == ile “bir” kelimesi yalnız bir kez sayılır.

Sunumdaki izleme slaytı aynı kodu "elma Armut elma kiraz ARMUT" metni üzerinde çalıştırır:

String metin = "elma Armut elma kiraz ARMUT";
String[] k = metin.toLowerCase().split(" ");
boolean[] sayildi = new boolean[k.length];
for (int i = 0; i < k.length; i++) {
  if (sayildi[i]) continue;
  int frekans = 1;
  for (int j = i + 1; j < k.length; j++) {
    if (k[i].equals(k[j])) {
      frekans++;
      sayildi[j] = true;
    }
  }
  System.out.println(k[i] + ": " + frekans);
}
elma: 2
armut: 2
kiraz: 1
Tablo 3.4.13: Kelime frekansı izlemesi. k = ["elma", "armut", "elma", "kiraz", "armut"]; bayraklar her dış turun sonundadır.
ik[i]Eşleşen jfrekanssayildiÇıktı
0elma22F F T F Felma: 2
1armut42F F T F Tarmut: 2
2elma——F F T F Tbayrak açık, continue
3kirazyok1F F T F Tkiraz: 1
4armut——F F T F Tbayrak açık, continue
kelma0falsearmut1falseelma2truekiraz3falsearmut4falsesayildi
Şekil 3.4.15: i = 0 turunun sonunda kelime dizisi ve bayraklar. İndis 2'deki “elma” sayıldı olarak işaretlenmiştir.

Algoritmanın maliyeti kelime sayısının karesiyle büyür. n kelime için iç döngü en kötü durumda (n − 1) + (n − 2) + … + 1 = n(n − 1)/2 karşılaştırma yapar. Birkaç cümlelik bir metin için bu çok azdır. Bir kitabın bütün kelimeleri sayılacaksa anahtar-değer çiftleri tutan bir koleksiyon (HashMap) çok daha hızlı sonuç verir.

3.4.7.3Küçük dizgi metotları

Sunumun alıştırma ve örnek problem slaytları, dizgi metotlarını birleştiren birkaç kısa metot içerir. Bu metotlar indisleme ve döngünün ortak kalıplarını gösterir.

Ters çevirme. tersCevir metodu palindrom örneğindeki döngüyü bir metoda taşır:

static String tersCevir(String s) {
  String ters = "";
  for (int i = s.length() - 1; i >= 0; i--) {
    ters += s.charAt(i);
  }
  return ters;
}

tersCevir("Giresun") çağrısı nuseriG döndürür. Büyük G sona geçmiştir; metot harf büyüklüğüne dokunmaz.

Karakter sayma. say metodu bir karakterin kaç kez geçtiğini bulur:

static int say(String s, char aranan) {
  int adet = 0;
  for (int i = 0; i < s.length(); i++) {
    if (s.charAt(i) == aranan) {
      adet++;
    }
  }
  return adet;
}

s.charAt(i) == aranan karşılaştırması doğrudur, çünkü iki yan da char'dır ve char ilkel bir tiptir. == burada referansı değil değeri karşılaştırır. say("Karadeniz", 'a') 2 döndürür: K büyük harftir ve 'a' ile eşleşmez; eşleşen yalnız indis 1 ve 3'teki a'lardır.

Baş harfi büyütme. basHarfBuyuk ilk harfi büyük, kalanını küçük yapar:

static String basHarfBuyuk(String kelime) {
  return kelime.substring(0, 1).toUpperCase()
       + kelime.substring(1).toLowerCase();
}

Metot iki parçayı ayrı ayrı işleyip birleştirir: substring(0, 1) ilk harf, substring(1) kalanıdır. basHarfBuyuk("jAVA") → Java. Türkçe ayarda basHarfBuyuk("istanbul") → İstanbul, basHarfBuyuk("ılgaz") → Ilgaz verir. Boş dizgiyle çağrılırsa substring(0, 1) geçersiz bir aralık ister ve StringIndexOutOfBoundsException fırlatılır. Sağlam bir sürüm başta if (kelime.isEmpty()) return kelime; denetimi yapar.

Sesli harf sayma. Bir karakterin bir kümede olup olmadığını anlamak için indexOf kullanılabilir:

String kelime = "Ordu";
String sesliler = "aeıioöuüAEIİOÖUÜ";
int sayac = 0;
for (int i = 0; i < kelime.length(); i++) {
  char c = kelime.charAt(i);
  if (sesliler.indexOf(c) != -1) {
    sayac++;
  }
}
System.out.println("Sesli: " + sayac);
Sesli: 2

sesliler dizgisi Türkçenin sekiz sesli harfini büyük ve küçük biçimleriyle içerir. Her karakter için sesliler.indexOf(c) çağrılır; sonuç −1 değilse karakter seslidir. "Ordu"'da O ve u sesli harftir: Sesli: 2. Bu kalıp sekiz dallı bir if zincirinden hem kısa hem okunaklıdır.

Kelime sayısı. Kelimeler arasında tek boşluk varsa kelime sayısı tek satırda bulunur: cumle.split(" ").length. "Java ile nesne yönelimli programlama" için sonuç 5'tir. Kelimeler arasında birden çok boşluk olabiliyorsa cumle.trim().split(" +").length yazılır.

Örnek 3.4.4

Sunumdaki ilk örnek problem değiştirilemezlik, substring, indexOf ve birleştirme sırasını bir arada sınar:

String s = "Karadeniz";
s.toUpperCase();
String t = s.substring(2, 5);
System.out.println(t + " " + t.length());
System.out.println(s.indexOf("de"));
System.out.println(s.indexOf("x"));
System.out.println(1 + 2 + "3" + 4 + 5);
rad 3
4
-1
3345

s.toUpperCase() sonucu atanmadığı için s değişmez. "Karadeniz"'in indisleri K0 a1 r2 a3 d4 e5 n6 i7 z8'dir. substring(2, 5) 2, 3, 4 indislerini alır: "rad", uzunluk 3. "de" indis 4'te başlar. "x" dizgide yoktur, −1. Son satır soldan sağa hesaplanır: 1 + 2 = 3 (iki sayı), 3 + "3" = "33", ardından "334" ve "3345".

İkinci problem havuzu sınar:

String a = "ders";
String b = "de" + "rs";
String c = new String("ders");
String d = c;
System.out.println(a == b);
System.out.println(a == c);
System.out.println(c == d);
System.out.println(a.equals(c));
true
false
true
true

"de" + "rs" derleme zamanında hesaplanan bir sabittir ve havuzdaki a ile aynı nesneyi gösterir: a == b true. c new ile oluşmuş ayrı bir nesnedir: a == c false. d = c referansı kopyalar; ikisi aynı nesnedir: true. İçerikler aynı olduğu için a.equals(c) true'dur.

Üçüncü problem baş harfleri ve e-posta parçalarını çıkarır:

public static String bas(String ad, String soyad) {
  return ad.charAt(0) + "." + soyad.charAt(0) + ".";
}
System.out.println(bas("Sercan", "Külcü"));
String e = "ogrenci@giresun.edu.tr";
int at = e.indexOf('@');
System.out.println(e.substring(0, at));
System.out.println(e.substring(at + 1));
S.K.
ogrenci
giresun.edu.tr

ad.charAt(0) + "." ifadesinde sağ yan bir dizgi olduğu için birleştirme yapılır: "S.". İki char doğrudan toplansaydı sonuç bir sayı olurdu; 'S' + '.' 83 + 46 = 129 verir. E-posta kısmı daha önce çözülen örneğin aynısıdır.

3.4.8Sık yapılan hatalar

Dizgilerle ilgili hataların bir kısmını derleyici yakalar; bunların iletisi okunursa düzeltmesi kolaydır. Bir kısmı ise derlenir ve çalışırken yanlış sonuç verir ya da istisna fırlatır. Tablo 3.4.14 ilk grubu, Tablo 3.4.15 ikinci grubu özetler. İletiler JDK 21'in javac'ından alınmıştır.

Tablo 3.4.14: Derleyicinin yakaladığı dizgi hataları.
HataDerleyici iletisiDüzeltme
String s = 'Merhaba';unclosed character literalTek tırnak yalnız tek karakter içindir; dizgi çift tırnakla yazılır.
char c = "a";incompatible types: String cannot be converted to charchar c = 'a'; ya da "a".charAt(0).
s.lengthcannot find symbol / symbol: variable lengthDizgide length() metottur. Dizide tersi: d.length.
string s = …;cannot find symbol / symbol: class stringSınıf adı String'dir; Java büyük-küçük harfe duyarlıdır.
int n = "42";incompatible types: String cannot be converted to intInteger.parseInt("42").
s.charAt(0) = 'j';unexpected type / required: variableDizgi değiştirilemez; yeni dizgi ya da StringBuilder.setCharAt.
s[0]array required, but String founds.charAt(0).
if (a < b) (dizgiler)bad operand types for binary operator '<'a.compareTo(b) < 0.

İlk satırdaki hata için javac aslında üç ileti verir: iki kez unclosed character literal ve bir kez not a statement. Derleyici tek tırnağı bir karakter sabitinin başlangıcı sanar, tırnağın kapanmadığını görür ve ardından gelenleri anlamlandıramaz. Bu tür zincirleme iletilerde ilk ileti asıl nedeni gösterir:

Tirnak.java:3: error: unclosed character literal
        String s = 'Merhaba';
                   ^
Tablo 3.4.15: Derlenen ama yanlış çalışan dizgi kodları.
HataBelirtiDüzeltme
İçeriği == ile karşılaştırmakKullanıcı girdisi doğru olsa da koşul false."evet".equals(girdi)
Sonucu atamayı unutmaks.toUpperCase(); sonrası s değişmez.s = s.toUpperCase();
Son indisi length() sanmakStringIndexOutOfBoundsException: Index 4 out of bounds for length 4s.charAt(s.length() - 1); döngüde i < s.length()
nextInt sonrası nextLineSatır boş okunur, soru atlanır.Araya bir klavye.nextLine();
null dizgide metot çağırmakNullPointerException: Cannot invoke "String.equals(Object)"Sabiti öne yazmak: "evet".equals(cevap)
substring'in bitişini dahil sanmaks.substring(5, 12) 8 değil 7 karakter verir.Uzunluk = bitiş − başlangıç.
Parantezsiz toplama"Toplam: " + x + y → Toplam: 105"Toplam: " + (x + y)
Varsayılan yerel ayarla harf dönüşümü"TITLE".toLowerCase() Türkçe ayarda tıtle.toLowerCase(Locale.ROOT) ya da equalsIgnoreCase
printf'te yanlış belirteçIllegalFormatConversionExceptionTam sayıya %d, ondalıklıya %f.

İkinci tablodaki hataların ortak noktası derleyicinin onları görememesidir. == ve equals ikisi de geçerli Java'dır. Sonucu atanmayan bir metot çağrısı da geçerli bir deyimdir. Bu hatalara karşı en iyi koruma, programı farklı girdilerle denemek ve çıktıyı beklenen değerle karşılaştırmaktır. Özellikle boş dizgi, tek karakterli dizgi, Türkçe harfler ve baştaki-sondaki boşluklar iyi birer deneme girdisidir.

3.4.9Özet

  • String karakter dizisini saklayan, değiştirilemez bir nesnedir. Dizgi sabiti ("…") ya da new ile oluşur; java.lang paketinde olduğu için import gerektirmez.
  • Birleştirme: + soldan sağa işlenir. İşlenenlerden biri dizgiyse birleştirme, ikisi de sayıysa toplama yapılır. char bir sayıdır: 'a' + 'b' = 195. Sayıları toplamak için parantez kullanılır.
  • İndis 0'dan length() - 1'e gider. charAt(i) bir char döndürür. Dışarı taşan indis StringIndexOutOfBoundsException fırlatır. Dizgide length() metottur, dizide length alandır.
  • Metotlar: indexOf (bulunamazsa −1), substring(a, b) (b hariç, uzunluk b − a), toUpperCase, replace, trim, split, join; hepsi yeni değer döndürür ve sonucun atanması gerekir.
  • Yerel ayar: parametresiz büyük-küçük harf dönüşümü makinenin ayarına bağlıdır. Türkçe ayarda i ↔ İ, ı ↔ I. Program içi metinlerde Locale.ROOT kullanılır.
  • StringBuilder: değiştirilebilir tampondur. Döngüde += ile birleştirme O(n²), append ile O(n) iş yapar. Kapasite dolunca yaklaşık iki katına çıkar (20 → 42).
  • Scanner: next() bir kelime, nextLine() satırın kalanını okur. nextInt() sonrası ⏎ tamponda kalır; ardından gelen nextLine() boş dizgi döndürür.
  • == / equals: == aynı nesne mi diye, equals aynı içerik mi diye sorar. Dizgilerin içeriği her zaman equals ile karşılaştırılır; null'a karşı sabit öne yazılır.
  • Dizgi havuzu: içeriği aynı dizgi sabitleri tek nesneyi paylaşır. new ve çalışma zamanındaki birleştirme yeni nesne üretir; intern() havuzdaki eşi verir.
  • compareTo: ilk farklı karakterin kod farkını, önek durumunda uzunluk farkını döndürür. Unicode sırasını kullanır; Türkçe alfabe sırası için Collator gerekir.
  • printf: %d, %f, %s, %c, %b, %n. %Wd sağa, %-Wd sola yaslar; %.2f iki ondalık basamak yazar. Sığmayan değer alanı büyütür. Ayırıcılar yerel ayara bağlıdır.

3.4.10Kavram listesi

3.4.11Kendini Dene

  1. System.out.println(2 + 3 + "x" + 2 + 3); ne yazdırır? Sondaki iki sayının toplamının yazılması için ifade nasıl değiştirilir?

    Cevap

    5x23 yazar. Soldaki iki sayı dizgiyle karşılaşılmadan önce toplanır (5). "x"'ten sonraki her + birleştirmedir. Sondaki toplam için parantez gerekir: 2 + 3 + "x" + (2 + 3) → 5x5.

  2. String s = "Karadeniz Teknik"; için s.indexOf(' '), s.substring(s.indexOf(' ') + 1) ve s.lastIndexOf('e') kaçtır?

    Cevap

    “Karadeniz” dokuz harflidir; boşluk indis 9'dadır. substring(10) boşluktan sonrasını verir: Teknik. T indis 10'da, e indis 11'dedir ve sonrasında başka e yoktur: lastIndexOf('e') = 11.

  3. StringBuilder sb = new StringBuilder("abc"); sb.append("de").insert(2, "X").reverse(); satırlarından sonra sb'nin içeriği ve uzunluğu nedir? Aynı işlemler String ile yapılsaydı kaç nesne oluşurdu?

    Cevap

    "abc" → append → "abcde" → insert(2, "X") → "abXcde" → reverse → edcXba, uzunluk 6. Bütün işlemler aynı nesne üzerinde yapılır. String ile her adım yeni bir dizgi üretirdi: başlangıç dizgisinin yanında üç yeni nesne daha.

  4. "Ankara".compareTo("Antalya") ve "abc".compareTo("ab") ne döndürür? Bu iki dizgi alfabetik sıraya konursa hangisi önce gelir?

    Cevap

    İlk çiftte indis 2'de 'k' (107) ile 't' (116) ayrılır: 107 − 116 = −9. Negatif olduğu için “Ankara” önce gelir. İkinci çiftte "ab", "abc"'nin önekidir; sonuç uzunluk farkıdır: 3 − 2 = 1. “ab” önce gelir.

  5. String x = "Ada"; String y = "A" + "da"; String z = new String("Ada"); String w = z.intern(); satırlarından sonra x == y, x == z, x == w ve z.equals(y) ne verir?

    Cevap

    true false true true. "A" + "da" derleme zamanında "Ada" sabitine dönüşür ve havuzdaki nesneyi gösterir. z havuz dışında ayrı bir nesnedir. intern() havuzdaki eşi, yani x'in nesnesini döndürür. İçerikler aynı olduğu için equals true verir.

  6. Türkçe ayarlı bir bilgisayarda System.out.printf("[%6.1f][%-4s][%3d]%n", 9.96, "ok", 12345); çıktısı nedir?

    Cevap

    [ 10,0][ok ][12345]. 9,96 bir ondalığa yuvarlanınca 10,0 olur; bu 4 karakterdir ve başa iki boşluk eklenir. "ok" sola yaslanır, sağa iki boşluk gelir. 12345 beş basamaklıdır ve 3 genişliğe sığmaz; alan büyür, sayı kesilmez.

  7. Klavyeden önce ␣␣42␣␣Ayşe Yılmaz⏎, sonra Son⏎ girilir. int n = k.nextInt(); String r = k.nextLine(); String t = k.next(); satırlarından sonra n, r ve t nedir?

    Cevap

    nextInt() baştaki boşlukları atlar ve 42'yi okur. nextLine() boşlukları atlamaz; satırın kalanını alır: r = " Ayşe Yılmaz" (başta iki boşluk). ⏎ tüketildiği için next() ikinci satırdaki kelimeyi okur: t = "Son".

  8. String u = "Merhaba"; u.replace('a', 'e'); u.concat("!"); System.out.println(u); ne yazdırır? Programcı Merhebe! görmek istiyorsa kod nasıl düzeltilir?

    Cevap

    Merhaba yazar. İki metot da yeni dizgi döndürür ama sonuçlar atanmadığı için kaybolur. Düzeltilmiş hâli: u = u.replace('a', 'e').concat("!"); ya da iki ayrı atama.

  9. "a-b--c".split("-") kaç öğeli bir dizi döndürür? "KIRMIZI".toLowerCase().equals("kirmizi") Türkçe ayarlı bir bilgisayarda ne verir?

    Cevap

    Ardışık iki tire arasında boş bir parça oluşur: ["a", "b", "", "c"], 4 öğe. İkinci ifade false verir: Türkçe kurallarla I küçültülünce ı olur ve sonuç "kırmızı"'dır. toLowerCase(Locale.ROOT) ya da equalsIgnoreCase true verir.

3.4.12İleri okuma

  • Oracle, The Java Tutorials, “Numbers and Strings” kesimi: String, StringBuilder ve biçimli sayı çıktısını kısa örneklerle anlatan resmî öğretici.
  • Oracle, Java SE 21 API Documentation, java.util.Formatter sınıfı: printf'te kullanılabilen bütün bayrakların, genişlik ve hassasiyet kurallarının ve tip harflerinin eksiksiz listesi.
  • J. Gosling vd., The Java Language Specification, Java SE 21, §3.10.5 ve §15.18.1: dizgi sabitlerinin havuzda paylaşılmasını ve + ile birleştirmenin kurallarını kesin biçimde tanımlar.
  • C. S. Horstmann, Core Java, Volume I: Fundamentals, Bölüm 3.6: dizgileri, StringBuilder'ı ve Unicode'un Java'daki karşılığını deneyimli bir programcının gözünden işler.

3.4.13Kaynaklar

  1. Oracle, The Java Tutorials: “Strings”, “Comparing Strings and Portions of Strings”, “The StringBuilder Class”, “Formatting Numeric Print Output”.
  2. Oracle, Java SE 21 API Documentation: java.lang.String, java.lang.StringBuilder, java.util.Scanner, java.util.Formatter, java.util.Locale.
  3. J. Gosling vd., The Java Language Specification, Java SE 21 Edition (§3.10.5 String Literals, §15.18.1 String Concatenation Operator +).
  4. P. Deitel, H. Deitel, Java: How to Program, 11. baskı, Pearson, 2017 (Bölüm 14: Strings, Characters and Regular Expressions).
  5. C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, 2022 (Bölüm 3.6: Strings).
  6. A. B. Downey, C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (Bölüm 6: Loops and Strings).

İlgili sayfalar: Bölüm 3.4 sunumu · Çalışma soruları · Dizgi Havuzu · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.