İçeriğe geç
academia.sh

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.
  • SD36Olcek ç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 putfield ile nesnenin içine gider ve çağırana görünür; parametreyi yeniden bağlamak astore ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat