Ders 10 / 10
Değer ve Referans Aktarımı
Java'nın tek bir aktarım modeli vardır ve sınıf dosyası bunu yazar: beş ayrı tipteki beş argüman aynı biçimde yüklenir, parametrenin alanına yazmak çağırana görünür, parametreyi yeniden bağlamak görünmez, ve değiştirilemez bir tipte iki durum ayırt edilemez.
İçindekiler
Önceki ders bir dizginin == ile aynı, equals ile eşit çıkabildiğini ölçtü; iki ad bazen
tek bir nesneyi tutuyordu. Aynı soru bir çağrının sınırında da sorulabilir.
Değerle ve referansla aktarım Programlama Temelleri kursunda kavram olarak kuruldu ve burada tekrarlanmıyor. Buradaki soru şudur: Java bu kavramların hangisini gerçekleştirir, ve bunu sınıf dosyasından nasıl okuruz? Kaynağa bakan biri bir kaydı yönteme verdiğinde nesnenin mi yoksa ona giden referansın mu geçtiğini göremez; sınıf dosyası görür.
Argümanlar Tek Bir Biçimde Yüklenir
- SD35 — Ölçülen kaynak dersin içinde yazılır; sınıf dosyasında bulunan her fazlalık derleyicinin eklediğidir.
- SD36 —
Olcekçekirdeğinin bu derste kullanılan iki ölçümü yazılır: yöntem başına komut sayısı ile eklenen adım, ve bir yöntemin komut adları dizisi. - SD37 — Çağrı ölçümündeki beş parametrenin tipleri bilerek ayrıdır: iki ilkel tip (biri iki yuva kaplar), bir sınıf, bir dizi ve değiştirilemez bir sınıf.
// 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) {}
static ClassModel derle(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);
return ClassFile.of().parse(d.resolve(sinif + ".class"));
}
static List<Yontem> oku(String kaynak, String sinif) throws Exception {
List<Yontem> yontemler = new ArrayList<>();
for (MethodModel m : derle(kaynak, sinif).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));
}
return yontemler;
}
static List<String> komutlar(String kaynak, String sinif, String yontem) throws Exception {
List<String> adlar = new ArrayList<>();
for (MethodModel m : derle(kaynak, sinif).methods())
if (m.methodName().stringValue().equals(yontem))
for (CodeElement e : m.code().orElseThrow())
if (e instanceof Instruction i)
adlar.add(i.opcode().name().toLowerCase(Locale.ROOT));
return adlar;
}
static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}
// Aktarim.java — argüman yükleme ve parametreye yazma sınıf dosyasında
public class Aktarim {
static final String K = """
class AktarimOrnek {
static class Kayit { String ad; int adet; long agirlik; }
static void sayiAl(int adet) {}
static void agirlikAl(long agirlik) {}
static void kayitAl(Kayit k) {}
static void diziAl(int[] d) {}
static void metinAl(String s) {}
static void cagir(int adet, long agirlik, Kayit k, int[] d, String s) {
sayiAl(adet); agirlikAl(agirlik); kayitAl(k); diziAl(d); metinAl(s);
}
static void alaniDegistir(Kayit k) { k.adet = 99; }
static void yenidenBagla(Kayit k) { k = new Kayit(); k.adet = 99; }
static void sayiyiArtir(int adet) { adet = adet + 1; }
static void metniUzat(String s) { s = s + " x"; }
}
""";
public static void main(String[] args) throws Exception {
System.out.println("cagir: " + Olcek.komutlar(K, "AktarimOrnek", "cagir"));
System.out.println();
System.out.printf("%-14s %6s %s%n", "yontem", "komut", "kaynakta yazilmayan adim");
for (Olcek.Yontem y : Olcek.oku(K, "AktarimOrnek"))
if (y.ad().endsWith("Degistir") || y.ad().endsWith("Bagla")
|| y.ad().endsWith("Artir") || y.ad().endsWith("Uzat"))
System.out.printf("%-14s %6d %s%n", y.ad(), y.komut(),
y.eklenen().isEmpty() ? "-" : String.join(", ", y.eklenen()));
for (String y : new String[] {"alaniDegistir", "yenidenBagla"})
System.out.printf("%-14s %s%n", y, Olcek.komutlar(K, "AktarimOrnek", y));
}
}
cagir: [iload_0, invokestatic, lload_1, invokestatic, aload_3, invokestatic, aload, invokestatic, aload, invokestatic, return] yontem komut kaynakta yazilmayan adim alaniDegistir 4 - yenidenBagla 8 yeni AktarimOrnek$Kayit, AktarimOrnek$Kayit.<init> sayiyiArtir 5 - metniUzat 4 dinamik:makeConcatWithConstants alaniDegistir [aload_0, bipush, putfield, return] yenidenBagla [new, dup, invokespecial, astore_0, aload_0, bipush, putfield, return]
İlk satır kursun bu derse ait tezidir. Beş ayrı tipte beş argüman geçiyor ve sınıf
dosyasında beş yükleme komutu var: iload, lload ve üç aload. Hepsi aynı işi
yapıyor — bir yerel değişken yuvasındaki değeri yığıta koyuyor — ve aralarındaki tek fark
değerin genişliği. Tam sayı bir yuva, uzun tam sayı iki yuva kaplar; bu yüzden üçüncü
argüman aload_3 ile yükleniyor, ikinci argüman iki numarayı birden almıştır.
Burada olmayan şey, olandan daha çok konuşuyor. Nesneyi kopyalayan bir komut yok. Bir referansı adresiyle geçiren ayrı bir komut yok. Sınıf dosyasında “değerle geçir” ve “referansla geçir” diye iki ayrı yol bulunmuyor; tek yol var ve o yol yuvadaki değeri kopyalayıp yığıta koymaktır. Kayıt, dizi ve dizgi için yuvada duran değer bir referanstır; kopyalanan şey nesne değil, o referanstır. Java’nın aktarım modeli bu tek cümledir ve ölçümün geri kalanı bu cümlenin sonuçlarıdır.
Alt tablo iki sonucu ayırıyor. alaniDegistir 4 komuttur ve hiçbir adım eklenmez:
referansı yükle, değeri yükle, alana yaz. Yazma putfield ile nesnenin içine gider.
yenidenBagla ise 8 komuttur ve iki adım ekler: bir nesne üretimi ve bir yapıcı
çağrısı. Komut dizisi devamını yazıyor — astore_0, yani yeni referans yuvanın üstüne
yazılır. Yuva çağrılan yöntemin kendi yuvasıdır; çağıranın yuvasıyla arasında hiçbir bağ
yoktur.
Son iki satır aynı ayrımın ilkel tipteki ve dizgideki karşılığıdır: sayiyiArtir yuvaya
yazar, metniUzat bir dinamik çağrıyla yeni dizgi kurup yine yuvaya yazar.
Görünen Değişiklik ile Görünmeyen
- SD38 — Görünürlük ölçümünde hiçbir kimlik sayısı basılmaz; kimlik yalnız
==ile sorulur. - SD39 — Her ölçümden önce alan bilinen bir değere kurulur; böylece değişen ile değişmeyen ayrılabilir.
- SD40 — Değiştirilemez tip ölçümünde ilkel tiple dizgi aynı biçimde denenir. İkisi için de yalnız yeniden bağlama yazılabilir, çünkü dizgide yazılabilir bir üye yoktur.
// Gorunur.java — hangi değişiklik çağırana görünüyor
class Kayit {
String ad;
int adet;
long agirlik;
}
public class Gorunur {
static boolean ayniNesneMi(Kayit gelen, Kayit disaridaki) { return gelen == disaridaki; }
static void alaniDegistir(Kayit k) { k.adet = 99; }
static void yenidenBagla(Kayit k) { k = new Kayit(); k.adet = 99; }
static void takas(Kayit a, Kayit b) { Kayit g = a; a = b; b = g; }
static void diziyeYaz(int[] d) { d[0] = 99; }
static void sayiyiArtir(int adet) { adet = adet + 1; }
static void metniUzat(String s) { s = s + " x"; }
public static void main(String[] args) {
Kayit k = new Kayit();
k.ad = "kuzey";
k.adet = 12;
System.out.println("parametre disaridakiyle ayni nesne mi: " + ayniNesneMi(k, k));
alaniDegistir(k);
System.out.println("alaniDegistir sonrasi adet : " + k.adet);
k.adet = 12;
yenidenBagla(k);
System.out.println("yenidenBagla sonrasi adet : " + k.adet);
Kayit a = new Kayit();
a.ad = "kuzey";
Kayit b = new Kayit();
b.ad = "guney";
takas(a, b);
System.out.println("takas sonrasi a.ad, b.ad : " + a.ad + " " + b.ad);
int[] d = {12, 18};
diziyeYaz(d);
System.out.println("diziyeYaz sonrasi d[0] : " + d[0]);
int adet = 12;
sayiyiArtir(adet);
String metin = "kuzey";
metniUzat(metin);
System.out.println("sayi ve metin sonrasi : " + adet + " " + metin);
}
}
parametre disaridakiyle ayni nesne mi: true alaniDegistir sonrasi adet : 99 yenidenBagla sonrasi adet : 12 takas sonrasi a.ad, b.ad : kuzey guney diziyeYaz sonrasi d[0] : 99 sayi ve metin sonrasi : 12 kuzey
İlk satır önceki bölümün cümlesini çalışma zamanında doğruluyor: parametredeki referans ile
dışarıdaki referans aynı nesneyi gösteriyor. Nesne kopyalanmadı. Bu yüzden ikinci satır
şaşırtıcı değil — alana yazmak çağırana görünüyor, adet 99 oldu.
Üçüncü satır ayrımı koyuyor. yenidenBagla da alana 99 yazıyor, ama önce parametreyi
yeni bir nesneye bağladığı için yazdığı yer başka bir nesnedir; çağıranın kaydı 12
kalıyor. Farkı yapan şey yazmanın kendisi değil, hangi nesneye yazıldığıdır. Dördüncü
satır bunun en bilinen biçimidir: iki parametreyi takas eden yöntem çağırana hiçbir şey
yapmaz, çünkü takas ettiği şey iki yuvadır.
Beşinci satır kuralın diziye uygulanışıdır: dizi de bir nesnedir, parametredeki referans
aynı diziyi gösterir ve d[0] = 99 yazması çağırana görünür.
Buradan uygulanabilir bir kural çıkar. Bir yöntemin çağırana görünen etkisi ancak erişilebilir bir nesnenin içine yazdığında doğar; parametreye yapılan atama hiçbir zaman görünmez. Çağıranın verdiği nesnenin değişmesini istemiyorsak, ya nesnenin değiştirilebilir bir üyesi olmamalı ya da yöntem kendi kopyasını kurmalıdır.
Değiştirilemez Tipte Ayrım Gözlenmez
Son satır dersin sınırlayıcı ölçümüdür ve iki değeri yan yana koyar: sayı 12, metin kuzey. İkisi de değişmedi.
Sayının değişmemesi beklenendir; adet bir ilkel tiptir ve yuvada duran değer sayının
kendisidir. Asıl sonuç metindedir. metniUzat bir referans aldı, o referansın gösterdiği
nesne çağıranınkiyle aynıydı, ve çağıran yine hiçbir değişiklik görmedi. Yanıtı önceki
ders ölçtü: dizgide yazılabilir bir üye yoktur. s = s + " x" satırı nesnenin içine
yazmaz, yazamaz; yeni bir dizgi kurup parametrenin yuvasına koyar.
Bunun ölçüm açısından anlamı şudur: değiştirilemez bir tiple bu dersin sorusu sorulamaz. Dizgi parametresi bir ilkel tip parametresinden ayırt edilemez, dolayısıyla dizgiye bakarak Java’nın hangi modeli kullandığına karar verilemez. Ayrımı gözlenebilir kılan şey aktarım biçimi değil, nesnenin değiştirilebilir bir üyesi olmasıdır.
Özet
- Sınıf dosyasında argüman geçirmenin tek bir biçimi vardır: yuvadaki değeri yığıta koymak. Beş ayrı tipteki beş argüman beş yükleme komutuyla geçer; tek fark, değerin kapladığı yuva sayısıdır.
- Referans tiplerinde yuvada duran değer bir referanstır; kopyalanan şey nesne değil o referanstır, ve parametre ile dışarıdaki ad aynı nesneyi gösterir.
- Parametrenin alanına yazmak
putfieldile nesnenin içine gider ve çağırana görünür; parametreyi yeniden bağlamakastoreile yöntemin kendi yuvasına yazar ve görünmez. - İki parametreyi takas eden yöntem çağırana hiçbir şey yapmaz; dizinin bir öğesine yazan yöntemin etkisi ise görünür, çünkü dizi de bir nesnedir.
- Değiştirilemez bir tipte ayrım gözlenmez: dizgi parametresi ilkel tip parametresinden ayırt edilemez, çünkü nesnenin içine yazılacak bir üye yoktur.
Kurs Kapanışı
Kurs tek bir soruyla açıldı: yazılan satır, sınıf dosyasında kaç adıma ve hangi adımlara dönüşüyor? On ders bu soruyu on ayrı biçime sordu ve yanıtı her seferinde çalıştırılmış bir ölçümden aldı. Toplandığında tek bir kural çıkıyor: Java’da sözdizim bir kısaltmadır. Kaynak, sınıf dosyasına konan adımların tamamını göstermez; gösterdiği şey, o adımların hangi biçimde isteneceğidir.
| Ders | Ölçülen biçim | Eklenen adım | Sınırlayıcı ölçüm |
|---|---|---|---|
| Kaynak, Bayt Kodu ve Sanal Makine | kaynak metni ile sınıf dosyasının karşılaştırılması | 12 yöntemin 8’inde, toplam 13 adım | kaynakta duran birleştirme dosyada hiç yok |
| Program Yaşam Döngüsü | giriş noktası ve başlatma sırası | üye eklenir: varsayılan yapıcı, 3 alan ve 5 yöntem | yapıcı yazılınca eklenen yapıcı kaybolur |
| Paketler ve İçe Aktarım | içe aktarım ile tam nitelikli ad | sıfır adım: iki dosya bayt bayt aynı | paket adı dosyayı beş bayt büyütür |
| İlkel Tipler ve Sarmalayıcılar | ilkel tip ile sarmalayıcı arasındaki geçiş | Integer.valueOf ve Integer.intValue |
önbellek aralığında == doğru, dışında yanlış |
| Değişkenler ve Kapsam | bildirimin bulunduğu yer | alan komutları ya da yalnız bir yuva | blok kapsamı aynı 20 komutu, bir eksik yuvayla üretir |
| İşleçler ve İfadeler | sayısal yükseltme ve bileşik atama | i2l ve i2b çevirmeleri, kısa devrede dal |
iki sabitte hiçbir adım eklenmez |
| Denetim Akışı | seçim ve döngü biçimleri | seçimde iki ayrı arama komutu; gelişmiş for listede 5 adım |
aynı döngü dizide 24 komut ve sıfır adım üretir |
| Diziler | dizi üretimi, uzunluk ve erişim | dizi üretimi; arraylength; öğe başına yazma |
kovaryansın tip denetimi komut sayısında görünmez |
| Dizgiler | birleştirme, havuz ve biriktirici | dinamik çağrı; biriktiricide üretim ve üç çağrı | iki sabitin birleştirilmesi 2 komut, sıfır adım |
| Değer ve Referans Aktarımı | argüman yükleme ve parametreye yazma | yeniden bağlamada üretim ve yuvaya yazma | değiştirilemez tipte ilkel tiple ayrım gözlenmez |
Tablonun son sütunu kursun yöntemidir. Her ders eklenen adımın olmadığı ya da kaybolduğu bir durumu da ölçtü, çünkü her yerde bulunan bir şey hiçbir şeyi açıklamaz. Sabit katlama, yazılmış bir yapıcı, önbellek aralığı, ilkel tipli dizi — her biri kuralın sınırını çiziyor. Kursun ikinci okuması da buradan gelir: komut sayısı ile eklenen adım aynı yöne gitmez, ve daha az komut daha az iş demek değildir.
Bu kursta sınıf yalnız bir kap olarak kullanıldı. Oysa ölçümler boyunca sınıfın kendisi de sorular üretti: kaynakta yazılmayan bir yapıcı, kaynakta yazılmayan üç alan ve beş yöntem, bir kaydın alanına yazıldığında çağırana görünen değişiklik. Sıradaki kurs olan Nesneye Dayalı Java sınıfı kap olmaktan çıkarıp konu yapar. Aynı ölçü orada da işini görecek — bir çağrının hangi yönteme gittiğine kimin ve ne zaman karar verdiği yine sınıf dosyasından okunabilir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.