Ders 09 / 10
Dizgiler
Aynı `+` işleci dizgide iki ayrı sonuç verir: iki sabit arasında sınıf dosyasında hiçbir iz bırakmaz, değişkenler arasında bir dinamik çağrıya dönüşür; döngüde ise 24 komutluk yazım 20 dizgi nesnesi bırakırken 27 komutluk biriktirici tek bir nesne üretir.
İçindekiler
Önceki ders String[] dizisine yazmanın çalışma zamanında denetlendiğini ölçtü; yazılan
değer her seferinde hazır bir dizgi nesnesiydi. Bu ders o nesneye bakar.
Dizginin değiştirilemez olduğu Programlama Temelleri kursunda kuruldu ve burada
tekrarlanmıyor. Buradaki soru şudur: değiştirilemezlik ne kadar tutuyor? Kaynakta yazılan
+ işleci sınıf dosyasında hangi adıma dönüşür, aynı sabiti iki kez yazmak kaç nesne
üretir, ve bir döngü içinde büyütülen metin yol boyunca kaç nesne bırakır?
Birleştirme İşleci Sınıf Dosyasında Kalmaz
- SD28 — Ölçülen kaynak metni dersin içinde yazılır; sınıf dosyasında bulunan her fazlalık derleyicinin eklediğidir.
- SD29 —
Olcekçekirdeği ortak tanımdan gelir. Bu ders onun yalnız kullandığı üç kaydını yazar: çağrı, dinamik çağrı ve nesne üretimi. Ölçümlerin davranışı değişmez. - SD30 — Ölçülen altı yöntemin dördü birleştirmenin dört ayrı biçimidir: iki sabit arasında, bir değişkenle, üç işleçle ve bir döngü boyunca. Kalan ikisi karşılaştırma içindir.
// Olcek.java — ortak ölçüm çekirdeği: derleyicinin sınıf dosyasına ne koyduğunu okur
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Olcek {
record Yontem(String ad, int komut, List<String> eklenen) {}
record Sinif(List<Yontem> yontem, List<String> alan) {}
static Sinif oku(String kaynak, String sinif) throws Exception {
Path d = Files.createTempDirectory("olcek");
Path k = d.resolve(sinif + ".java");
Files.writeString(k, kaynak);
PrintWriter bos = new PrintWriter(Writer.nullWriter());
if (ToolProvider.findFirst("javac").orElseThrow()
.run(bos, bos, "-d", d.toString(), k.toString()) != 0)
throw new IllegalStateException("derlenmedi: " + sinif);
ClassModel cm = ClassFile.of().parse(d.resolve(sinif + ".class"));
List<Yontem> yontemler = new ArrayList<>();
for (MethodModel m : cm.methods()) {
if (m.code().isEmpty()) continue;
List<String> eklenen = new ArrayList<>();
int komut = 0;
for (CodeElement e : m.code().get()) {
if (e instanceof Instruction) komut++;
if (e instanceof InvokeInstruction iv)
eklenen.add(kisa(iv.owner().asInternalName()) + "." + iv.name().stringValue());
else if (e instanceof InvokeDynamicInstruction id)
eklenen.add("dinamik:" + id.name().stringValue());
else if (e instanceof NewObjectInstruction n)
eklenen.add("yeni " + kisa(n.className().asInternalName()));
}
yontemler.add(new Yontem(m.methodName().stringValue(), komut, eklenen));
}
List<String> alanlar = new ArrayList<>();
for (FieldModel f : cm.fields()) alanlar.add(f.fieldName().stringValue());
return new Sinif(yontemler, alanlar);
}
static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}
// Birlestirme.java — dizgi birleştirme sınıf dosyasında neye dönüşüyor
public class Birlestirme {
static final String K = """
class DizgiOrnek {
static String birlestir(String ad, int adet) { return ad + adet; }
static String sabitBirlestir() { return "ba" + "sit"; }
static int uzunluk(String s) { return s.length(); }
static String satir(String ad, int adet, long agirlik) {
return ad + " x" + adet + " (" + agirlik + " g)";
}
static String donguyleBirlestir(String[] d) {
String s = "";
for (String x : d) s += x;
return s;
}
static String biriktiriciyle(String[] d) {
StringBuilder b = new StringBuilder();
for (String x : d) b.append(x);
return b.toString();
}
}
""";
public static void main(String[] args) throws Exception {
System.out.printf("%-18s %6s %s%n", "yontem", "komut", "kaynakta yazilmayan adim");
for (Olcek.Yontem y : Olcek.oku(K, "DizgiOrnek").yontem())
if (!y.ad().equals("<init>"))
System.out.printf("%-18s %6d %s%n", y.ad(), y.komut(),
y.eklenen().isEmpty() ? "-" : String.join(", ", y.eklenen()));
String ad = "kuzey";
int adet = 12;
long agirlik = 1800;
System.out.println("\nsatir yaziminin urettigi metin: "
+ ad + " x" + adet + " (" + agirlik + " g)");
}
}
yontem komut kaynakta yazilmayan adim birlestir 4 dinamik:makeConcatWithConstants sabitBirlestir 2 - uzunluk 3 String.length satir 5 dinamik:makeConcatWithConstants donguyleBirlestir 24 dinamik:makeConcatWithConstants biriktiriciyle 27 yeni StringBuilder, StringBuilder.<init>, StringBuilder.append, StringBuilder.toString satir yaziminin urettigi metin: kuzey x12 (1800 g)
Birinci satır dersin ana ölçümüdür. Kaynakta ad + adet yazılıdır ve orada bir işleç
durur; sınıf dosyasında ise bir dinamik çağrı vardır. Dizgi birleştirme, sınıf
dosyasında bir işleç olarak bulunmaz — çünkü karşılığı olan bir birleştirme komutu yoktur.
Derleyici işleci bir çağrıya çevirir; çağrının nasıl gerçekleştirileceği çalışma zamanına
bırakılır ve sınıf dosyasında yalnız işlemin adıyla kaynakta yazılan sabit parçalar durur.
Kaynağa bakan biri bir sayının metne çevrildiğini de göremez: adet bir tam sayıdır ve o
çevrim de aynı çağrının içindedir.
İkinci satır aynı işleci sıfıra indiriyor. "ba" + "sit" yazan yöntem 2 komuttur:
sabiti yükle, döndür. Ortada birleştirme yoktur. İki dizgi sabiti derleme sırasında tek bir
sabite katlanmıştır — ilk derste ölçülen sabit katlamanın dizgideki karşılığı budur. Aynı
+ işleci, bir satırda bir çağrı, öteki satırda hiçbir şey.
Üçüncü satır önceki dersle arasındaki farkı koyuyor. Dizide d.length bir komuttu; dizgide
s.length() bir çağrıdır ve ölçek onu eklenen adım olarak kaydediyor. Sözdizim de bunu
söyler: dizide ayraç yoktur, dizgide vardır. Uzunluk dizide nesnenin başlığından okunur,
dizgide ise bir yöntem çağrısıyla istenir; ikisi aynı sözcükle yazıldığı için karıştırılır,
sınıf dosyasında hiç benzemezler.
Dördüncü satır işlecin en çok kullanılan biçimini ölçüyor. Kayıt satırını kuran satir
yönteminde üç + işleci var, ama sınıf dosyasında tek bir dinamik çağrı duruyor ve
yöntemin tamamı 5 komut. Derleyici art arda gelen birleştirmeleri ayrı ayrı çevirmez;
bütün parçaları tek bir çağrıya toplar. Kaynağa bakarak “her + bir ara metin üretir”
demek bu yüzden yanlış olurdu — bir ifade içindeki birleştirmelerin sayısı, üretilen ara
nesnelerin sayısı değildir. Alt satır bu yazımın ürettiği metni gösteriyor: kaydın adı,
adedi ve gram cinsinden ağırlığı tek bir çağrıda birleşiyor.
Alt iki satır ikinci çifti kuruyor. Döngü içinde s += x yazan yöntem 24 komut,
biriktiriciyle yazılan yöntem 27 komut. Komut sayısına bakan biri birinciyi seçerdi.
Eklenen adım listesi ise ters yönü gösteriyor: birincide döngünün her turunda çalışan tek
bir dinamik çağrı var, ikincide bir nesne üretimi ve üç çağrı. Sayılar burada bir seçim
gerekçesi vermiyor; komut sayısı programın kaç kez ne yaptığını söylemez.
Döngüde Kalan Nesneler
- SD31 — Ölçüm süre ölçmez; üretilen dizgi nesnesi sayar. Her turda sonucun bir
önceki nesneyle aynı olup olmadığı
==ile sorulur ve ayrı çıkan sayılır. Hiçbir kimlik sayısı basılmaz. - SD32 — Parça sayısı üç ayrı değerle denenir; ölçülen şey tek bir sayı değil, sayının parça sayısıyla nasıl değiştiğidir.
// Uretim.java — döngüde kaç dizgi nesnesi kalıyor
public class Uretim {
static String[] parcalar(int n) {
String[] d = new String[n];
for (int i = 0; i < n; i++) d[i] = (i % 2 == 0) ? "kuzey" : "-";
return d;
}
static int islecle(String[] d) {
String s = "";
int uretilen = 0;
for (String x : d) {
String onceki = s;
s = s + x;
if (s != onceki) uretilen++;
}
return uretilen;
}
static boolean biriktiriciAyni(String[] d) {
StringBuilder b = new StringBuilder();
boolean ayni = true;
for (String x : d) ayni = ayni && (b.append(x) == b);
return ayni;
}
public static void main(String[] args) {
System.out.printf("%6s %22s %28s%n", "parca", "islecle uretilen dizgi",
"append ayni nesneyi dondurdu");
for (int n : new int[] {5, 10, 20})
System.out.printf("%6d %22d %28s%n", n, islecle(parcalar(n)),
biriktiriciAyni(parcalar(n)));
StringBuilder b = new StringBuilder();
for (String x : parcalar(5)) b.append(x);
System.out.println("\ntoString iki cagri ayni nesne mi: " + (b.toString() == b.toString()));
String isleclerle = "";
for (String x : parcalar(5)) isleclerle += x;
System.out.println("iki yol ayni metni mi veriyor: " + isleclerle.equals(b.toString()));
System.out.println("sonuc: " + b);
}
}
parca islecle uretilen dizgi append ayni nesneyi dondurdu
5 5 true
10 10 true
20 20 true
toString iki cagri ayni nesne mi: false
iki yol ayni metni mi veriyor: true
sonuc: kuzey-kuzey-kuzey
Sayı parça sayısıyla birebir gidiyor: 5 parça 5 nesne, 20 parça 20 nesne. Bunların yalnız
sonuncusu kullanılır; öteki on dokuzu kurulup atılır. Bu bir gerçekleştirim ayrıntısı
değil, değiştirilemezliğin doğrudan sonucudur. s += x yazımı s nesnesine bir ek
yapamaz, çünkü dizgi üzerinde yazma diye bir işlem yoktur; yapabileceği tek şey yeni bir
dizgi kurup adı ona bağlamaktır. Önceki dersteki dizinin sabit boyutu gibi, bu da bir yasak
değil, karşılığı olan bir işlemin yokluğudur.
Buradaki += yazımı da bir kısaltmadır ve kısaltmanın açılımı tipe göre değişir. Sayıda
+= sessiz bir daraltma çevirmesi ekler ve değeri yerinde değiştirir; dizgide bir dinamik
çağrı ekler, yeni bir nesne kurar ve adı ona yeniden bağlar. İki yazım aynı görünür,
iki eklenen adım ayrıdır ve ikisi de kaynakta yazılmamıştır.
Üçüncü sütun biriktiricinin neden ayrıldığını söylüyor: append çağrısı çağrıldığı
nesnenin kendisini döndürüyor, yirmi turun yirmisinde de. Biriktirici değiştirilebilir
bir tampon tutar; ekleme onun içine yazılır ve yeni nesne kurulmaz. Dizgi yalnız
toString çağrıldığında üretilir, o çağrı da bir kez yapılır. Alt satır bu çağrının
gerçekten üretim olduğunu doğruluyor: art arda iki toString çağrısı ayrı nesneler
veriyor.
Böylece iki yolun tablosu tamamlanıyor. İşleçli yol 24 komut ve n nesne, biriktiricili yol 27 komut ve 1 nesne; ikisi de aynı metni veriyor. Kursun ikinci iddiası burada tam biçimini alıyor: komut sayısı ile yapılan iş aynı yöne gitmez. Sınıf dosyasındaki komut sayısı bir yöntemin ne kadar yazıldığını ölçer, kaç kez çalıştığını değil. Döngü içindeki tek bir komut, döngü dışındaki üç komuttan daha çok iş yapar.
Buradan doğrudan bir kural çıkar ve o kural yazıma bakılarak uygulanabilir: birleştirme sayısı derleme zamanında belliyse işleç yeterlidir, çünkü tek çağrıya derlenir; sayı çalışma zamanında belirleniyorsa biriktirici kullanılır.
Sabit Üretilmez, Havuzdan Alınır
- SD33 — Kimlik ölçümünde yalnız
==veequalssonuçları basılır; hiçbir kimlik değeri ya da adres yazılmaz. - SD34 — Karşılaştırılan beş dizginin beşi de aynı harfleri taşır; ölçülen tek şey bunların aynı nesne olup olmadığıdır.
// Havuz.java — dizgi sabiti üretilmez, havuzdan alınır
public class Havuz {
static final String K = """
class HavuzOrnek {
static String bir() { return "kuzey"; }
static String iki() { return "kuzey"; }
static String uretilmis() { return new String("kuzey"); }
}
""";
static void yaz(String ad, String a, String b) {
System.out.printf("%-32s %7s %8s%n", ad, a == b, a.equals(b));
}
public static void main(String[] args) throws Exception {
System.out.printf("%-14s %6s %s%n", "yontem", "komut", "kaynakta yazilmayan adim");
for (Olcek.Yontem y : Olcek.oku(K, "HavuzOrnek").yontem())
if (!y.ad().equals("<init>"))
System.out.printf("%-14s %6d %s%n", y.ad(), y.komut(),
y.eklenen().isEmpty() ? "-" : String.join(", ", y.eklenen()));
String sabit = "kuzey";
String ikinci = "kuzey";
String sabitToplam = "kuz" + "ey";
String hesaplanan = "kuz" + sabit.substring(3);
String uretilmis = new String("kuzey");
System.out.printf("%n%-32s %7s %8s%n", "\"kuzey\" ile karsilastirilan", "==", "equals");
yaz("ikinci kez yazilan sabit", ikinci, sabit);
yaz("\"kuz\" + \"ey\"", sabitToplam, sabit);
yaz("calisma zamaninda birlestirilen", hesaplanan, sabit);
yaz("new String(\"kuzey\")", uretilmis, sabit);
yaz("hesaplanan.intern()", hesaplanan.intern(), sabit);
}
}
yontem komut kaynakta yazilmayan adim
bir 2 -
iki 2 -
uretilmis 5 yeni String, String.<init>
"kuzey" ile karsilastirilan == equals
ikinci kez yazilan sabit true true
"kuz" + "ey" true true
calisma zamaninda birlestirilen false true
new String("kuzey") false true
hesaplanan.intern() true true
Üst tablo dersin sınırlayıcı ölçümüdür. Bir dizgi sabiti döndüren yöntem 2 komuttur ve
eklenen adım sütunu boştur: hiçbir üretim, hiçbir çağrı, hiçbir çevirme yok. Sabit sınıf
dosyasında hazır durur ve çalışma zamanında üretilmez, yalnız yüklenir. Aynı sabiti
ikinci kez yazmak da ikinci bir nesne kurmaz. Buna karşılık new String("kuzey") yazımı
5 komuttur ve iki adım ekler: bir nesne üretimi ve bir yapıcı çağrısı. Kaynakta yazılan
tek fark bir anahtar sözcüktür; sınıf dosyasındaki fark üç komut ve bir nesnedir.
Alt tablo bunun kimlik üzerindeki sonucunu veriyor. İki kez yazılan sabit aynı
nesnedir; iki sabitin birleştirilmesi de aynı nesnedir, çünkü o birleştirme derleme
zamanında yapılmış ve sonuç yine bir sabit olmuştur. Aynı harfler çalışma zamanında
birleştirildiğinde ise sonuç ayrı bir nesnedir, ve new String her çağrıda ayrı bir
nesne verir. Beş satırın beşinde de equals doğru: içerik hep aynı, kimlik değil.
Bunları toplayan yer dizgi havuzudur: sabitler tek bir havuzda tutulur ve aynı harf
dizisi bir kez saklanır. Son satır havuza sonradan girmenin yolunu gösteriyor — intern
çağrısı, çalışma zamanında kurulmuş bir dizginin havuzdaki karşılığını verir ve sonuç
sabitle aynı nesne çıkar. Bunun tek pratik sonucu şudur: dizgiler == ile
karşılaştırılmaz. İşleç bazen doğru yanıt verir, ve tam da bu yüzden güvenilmezdir; doğru
yanıtı veren şey içerik değil, sabitin nereden geldiğidir.
intern çağrısı bu tabloyu düzeltmenin yolu gibi görünebilir, ama düzelttiği şey ölçümün
kendisi değildir: havuza her giriş bir arama ve bir saklama ister, ve bu ders o adımları
saymadı. Karşılaştırma için equals kullanmak hem daha az varsayım hem daha az adım
gerektirir; intern yalnız aynı metnin çok kez tutulduğu ve tek bir kopyada toplanmasının
istendiği durumda ölçülecek bir seçenektir.
Özet
- Dizgi birleştirme sınıf dosyasında bir işleç değildir: değişkenler arasındaki
+bir dinamik çağrıya dönüşür, sayıdan metne çevrim de o çağrının içindedir, ve bir ifadedeki üç işleç yine tek çağrı verir. - Dizide
d.lengthbir komuttur, dizgides.length()bir çağrıdır; aynı sözcük iki tipte ayrı adımlara derlenir. - İki dizgi sabitinin birleştirilmesi hiçbir adım eklemez: yöntem 2 komuttur, sabit derleme zamanında katlanmıştır.
- Döngüde
+=ile büyütülen metin parça başına bir dizgi bırakır — 20 parça 20 nesne — çünkü dizgi üzerinde yazma diye bir işlem yoktur; biriktiricide iseappendçağrıldığı nesnenin kendisini döndürür ve dizgi yalnıztoStringile bir kez üretilir. - İşleçli yol 24, biriktiricili yol 27 komuttur: daha az komut daha az iş demek değildir, çünkü komut sayısı kaç kez çalıştığını söylemez.
- Aynı harfleri taşıyan beş dizginin üçü sabitle aynı nesne, ikisi ayrıdır;
equalsbeşinde de doğrudur, bu yüzden dizgiler==ile karşılaştırılmaz.
Sonraki Adım
Bu derste bir dizginin == ile aynı, equals ile eşit çıkabildiği ölçüldü; iki ad bazen
tek bir nesneyi tuttu. Aynı soru bir yöntem çağrısında da sorulabilir: bir kayıt nesnesi
bir yönteme verildiğinde çağrılan yöntem hangi şeye erişir, ve o nesnenin alanını
değiştirmekle parametreyi yeniden bağlamak neden ayrı sonuçlar verir? Sıradaki ders
Java’nın tek aktarım modelini sınıf dosyasından okur ve kursu kapatır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.