İçeriğe geç
academia.sh

Ders 04 / 10

İlkel Tipler ve Sarmalayıcılar

Bir harflik kaynak farkı sınıf dosyasında bir yöntem çağrısıdır: `Integer adet = 5` satırı `Integer.valueOf`, ters yönü `Integer.intValue` ekler; aynı 200 kutulama depoya göre 100 ile 200 arasında nesne üretir ve önbelleğin bittiği yerde kimlik ile değer ayrılır.

İçindekiler

Önceki ders ad çözümlemesinin derleme zamanında bittiğini ölçtü: içe aktarım satırı sınıf dosyasında hiçbir iz bırakmıyordu, çünkü derleyici adı çözüp yerine tam nitelikli karşılığını koyuyordu. Geriye bir soru kaldı. Ad çözümlemesi derleme zamanında bitiyorsa, adın taşıdığı değerin kendisi ne zaman ve nerede belirleniyor.

Java bu soruya tek yanıt vermez. int adet = 5 yazıldığında değer yöntemin kendi yerel alanında, sayının kendisi olarak durur. Integer adet = 5 yazıldığında öbekte bir nesne bulunur ve yerel alanda yalnız o nesnenin referansı durur. Kaynaktaki fark tek harftir; sınıf dosyasındaki fark bir yöntem çağrısıdır. Programlama Temelleri kursu temel veri tiplerini ve örtük dönüşümü kavram olarak kurmuştu — bu tekrarlanmaz. Orada bir değerin tipinin ne belirlediği anlatıldı; burada ölçülen şey, derleyicinin bu geçiş için sınıf dosyasına kaç adım koyduğudur.

İki Tip Ailesi

Java’nın tipleri ikiye ayrılır. İlkel tipler sekiz tanedir ve sayıdan başka bir şey taşımazlar; referans tipleri ise nesnelerin adresini taşır. Her ilkel tipin bir sarmalayıcı sınıfı vardır: aynı değeri bir nesnenin içinde tutan referans tipi.

İlkel tip Genişlik (bit) Sarmalayıcı
boolean belirtilmemiş Boolean
byte 8 Byte
short 16 Short
char 16 Character
int 32 Integer
long 64 Long
float 32 Float
double 64 Double

Ayrımın kaynaktaki görünürlüğü düşüktür, çünkü iki aile arasındaki geçiş yazılmaz. Bir int değeri bir Integer bekleyen yere kendiliğinden girer (otomatik kutulama), bir Integer de int bekleyen yere kendiliğinden girer (kutudan çıkarma). İki yönde de kaynakta yalnız bir eşittir işareti görünür.

Ayrımın var olma nedeni de buradadır. Bir ilkel tip nesne değildir: yöntemi yoktur, null olamaz ve referans bekleyen hiçbir yere giremez. Bir liste, bir eşleme ya da tip değişkeni alan herhangi bir yapı yalnız referans saklar; bir int değerini listeye koymak için önce onu saracak bir nesne gerekir. Sarmalayıcı bu boşluğu doldurur ve otomatik kutulama, o dolduruşu kaynaktan silen bir kısaltmadır. Sonuç şudur: List<Integer> yazan bir kaynak, int yazan bir kaynağın yaptığı işi yapmaz — arada bir nesne katmanı vardır ve o katmanın bedeli sınıf dosyasında durur.

Kurs boyunca tek bir veri taşıyıcı kullanılır: bir depo kaydı olan Kayit, üç alanlı — String ad, int adet, long agirlik (gram). Bu ders kutulamayı adet alanı üzerinde ölçer.

Ölçüm Çekirdeği

Ölçüm, verilen bir kaynak metnini derleyiciyi program içinden çağırarak derler, ürettiği sınıf dosyasını okur ve her yöntem için iki şey döndürür: kaç komut üretildiği ve o yöntemde hangi çağrıların bulunduğu. Derleyici ve sınıf dosyası okuyucusu standart kitaplığın parçasıdır.

  • SD1 — Kâhin kurgunun kendisidir: ölçülen kaynağı biz yazdığımız için ne yazdığımızı biliriz. Sağ sütun sınıf dosyasındaki bütün çağrıları listeler; kaynakta yazılmış olanı okuyucu düşer, kalan derleyicinin eklediğidir.
  • SD2 — Ölçülen kaynak metni sabittir ve her koşumda aynı derleyiciye verilir; sayılar tek bir derleyici koşumundan gelir ve o koşumda belirlenimcidir.
  • SD3 — Yapıcı (<init>) tabloya alınmaz; kaynakta yazılmayan üyeler ayrı bir konudur.
// Olcek.java — verilen kaynağı derler, sınıf dosyasındaki komutları ve çağrıları 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 List<Yontem> oku(String kaynak, String sinif) throws Exception {
        Path d = Files.createTempDirectory("olcek");
        Files.writeString(d.resolve(sinif + ".java"), kaynak);
        PrintWriter bos = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow().run(bos, bos, "-d", d.toString(),
                d.resolve(sinif + ".java").toString()) != 0)
            throw new IllegalStateException("derlenmedi: " + sinif);
        List<Yontem> yontemler = new ArrayList<>();
        for (MethodModel m : ClassFile.of().parse(d.resolve(sinif + ".class")).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 TypeCheckInstruction t && t.opcode() == Opcode.CHECKCAST)
                    eklenen.add("tip denetimi " + kisa(t.type().asInternalName()));
            }
            yontemler.add(new Yontem(m.methodName().stringValue(), komut, eklenen));
        }
        return yontemler;
    }

    static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}

Atamanın Eklediği Çağrı

Ölçülen kaynak beş yöntem taşır. kutula bir int değerini Integer adına bağlar, kutuAc tersini yapar. diziToplami ile listeToplami aynı döngüyü yazar; tek fark gezilen şeyin int[] mi yoksa List<Integer> mi olduğudur. say bir sayacı adet değeriyle günceller.

// Kutulama.java — ilkel tip ile sarmalayıcı arasındaki geçişin sınıf dosyasındaki izi
public class Kutulama {
    static final String KAYNAK = """
        import java.util.List;
        import java.util.Map;
        class Depo {
            static Object kutula(int adet) { Integer k = adet; return k; }
            static int kutuAc(Integer k) { int adet = k; return adet; }
            static int diziToplami(int[] adetler) { int t = 0; for (int x : adetler) t += x; return t; }
            static int listeToplami(List<Integer> adetler) { int t = 0; for (int x : adetler) t += x; return t; }
            static void say(Map<String, Integer> sayac, String ad, int adet) { sayac.put(ad, adet); }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.printf("%-14s %6s  %s%n", "yontem", "komut", "sinif dosyasindaki cagri");
        int toplam = 0;
        for (Olcek.Yontem y : Olcek.oku(KAYNAK, "Depo")) {
            if (y.ad().equals("<init>")) continue;
            System.out.printf("%-14s %6d  %s%n", y.ad(), y.komut(),
                    y.eklenen().isEmpty() ? "-" : String.join(", ", y.eklenen()));
            toplam += y.eklenen().size();
        }
        System.out.println("toplam: " + toplam);
    }
}
yontem          komut  sinif dosyasindaki cagri
kutula              5  Integer.valueOf
kutuAc              5  Integer.intValue
diziToplami        24  -
listeToplami       20  List.iterator, Iterator.hasNext, Iterator.next, tip denetimi Integer, Integer.intValue
say                 7  Integer.valueOf, Map.put
toplam: 9

kutula kaynağında hiçbir çağrı yazmaz; sınıf dosyasında Integer.valueOf vardır. kutuAc de hiçbir çağrı yazmaz; sınıf dosyasında Integer.intValue vardır. Otomatik kutulama bir dil kolaylığı değil, kaynakta görünmeyen bir yöntem çağrısıdır ve tersi de öyledir. say satırı SD1’in nasıl okunacağını gösterir: Map.put kaynakta yazılıdır ve düşülür, Integer.valueOf yazılı değildir ve kalır.

Alt iki satır kursun ikinci iddiasını ödüyor. diziToplami 24 komut üretiyor ve kaynakta yazılmayan hiçbir adım taşımıyor: gelişmiş for bir dizide sayaçlı bir döngüye çevriliyor ve toplama doğrudan sayılar üzerinde yapılıyor. listeToplami 20 komut üretiyor — daha az — ama kaynakta yazılmayan beş adım taşıyor: yineleyici alınıyor, iki kez sorgulanıyor, dönen nesne Integer’a denetleniyor ve intValue ile sayıya açılıyor. Daha az komut, daha çok eklenen adım. Komut sayısı ile eklenen adım sayısı aynı yöne gitmez; hangisinin sayıldığı söylenmeden bir ölçüm karşılaştırılamaz.

Bu beş adımın dördü döngü gövdesinin içindedir, yani öğe başına ödenir. Sınıf dosyası bunu göstermez; gösterdiği şey adımın varlığıdır. Kaç kez ödendiğini görmek için koşmak gerekir.

Aynı Sayıda Kutulama, İki Ayrı Nesne Sayısı

Integer.valueOf her çağrıldığında yeni bir nesne üretmez. Belirtim, int değerleri için -128 ile 127 arasındaki her değerin kutulamasında aynı nesnenin döndürülmesini zorunlu kılar; bu aralığın dışı gerçekleştirime bırakılmıştır. Bunun ölçülebilir sonucu şudur: aynı sayıda kutulama, değerlerin nereye düştüğüne göre ayrı sayıda nesne üretir.

  • SD4 — Üç depo da 100 kayıt taşır ve her kaydın adet değeri ayrıdır; tek fark değerlerin başladığı tabandır. Her depoda her adet iki kez kutulanır, yani üç ölçüm de 200 kutulama çağrısı yapar.
  • SD5 — Ayrı nesne sayısı, üretilen sarmalayıcılar kimliklerine göre bir eşleme içinde toplanarak bulunur; hiçbir nesnenin kimlik değeri basılmaz, yalnız kaç ayrı nesne olduğu sayılır.
// Uretim.java — aynı sayıda kutulama, iki ayrı nesne sayısı
import java.util.IdentityHashMap;
import java.util.Map;

public class Uretim {
    record Kayit(String ad, int adet, long agirlik) {}

    static Kayit[] depo(int n, int taban) {
        Kayit[] d = new Kayit[n];
        for (int i = 0; i < n; i++) d[i] = new Kayit("K" + i, taban + i, 1000L + i);
        return d;
    }

    static int ayriNesne(Kayit[] depo) {
        Map<Integer, Boolean> kimlik = new IdentityHashMap<>();
        for (int tur = 0; tur < 2; tur++)
            for (Kayit k : depo) kimlik.put(k.adet(), Boolean.TRUE);   // her put bir valueOf
        return kimlik.size();
    }

    public static void main(String[] args) {
        System.out.printf("%-22s %8s %8s %10s%n", "depo", "kutulama", "deger", "ayri nesne");
        for (int taban : new int[] {1, 100, 1000}) {
            Kayit[] d = depo(100, taban);
            System.out.printf("adet %4d..%-4d %8d %8d %10d%n",
                    taban, taban + 99, 2 * d.length, d.length, ayriNesne(d));
        }
        Kayit[] buyuk = depo(100, 1000);
        long t = 0;
        for (Kayit k : buyuk) t += k.adet();
        System.out.println("ilkel toplama: " + t + " | uretilen sarmalayici: 0");
    }
}
depo                   kutulama    deger ayri nesne
adet    1..100       200      100        100
adet  100..199       200      100        172
adet 1000..1099      200      100        200
ilkel toplama: 104950 | uretilen sarmalayici: 0

Üç satırda da aynı kod çalışıyor, aynı sayıda kutulama yapılıyor ve aynı sayıda ayrı değer kutulanıyor. Üretilen nesne sayısı 100, 172 ve 200. Kaynakta bu farkı üreten hiçbir şey yoktur; fark tümüyle eklenen adımın, yani Integer.valueOf’un kararıdır.

Orta satır sınırı doğrudan gösteriyor. adet değerleri 100 ile 199 arasında dağıldığında 128’in altındaki 28 değer önbellekten dönüyor ve her biri için tek nesne oluşuyor; kalan 72 değer iki turda iki ayrı nesne üretiyor. Toplam 28 artı 144, yani 172. Sayının kendisi bir eşiği işaretliyor.

Son satır ölçünün diğer ucudur. Aynı depo int üzerinden toplandığında hiç sarmalayıcı üretilmez: t += k.adet() satırında kutulama yoktur, çünkü long da int de ilkel tiptir. Bir döngüde sarmalayıcı kullanmanın bedeli, o döngünün her adımında bir nesne üretilmesi olabilir — ve bu, kaynakta hiçbir yerde yazmaz.

Kimlik ile Değerin Ayrıldığı Yer

Nesne üretimindeki bu fark yalnız sayıda kalmaz; görünen bir sonucu vardır. İki referans tipi == ile karşılaştırıldığında değerleri değil, aynı nesne olup olmadıkları sınanır. Önbellek aralığında iki kutulama aynı nesneyi verdiği için == doğru sonuç verir gibi görünür; aralığın dışında aynı kod yanlış sonuç verir.

// Onbellek.java — sarmalayıcı kimliğinin ve değerinin ayrıldığı yer
public class Onbellek {
    record Kayit(String ad, int adet, long agirlik) {}

    static Kayit kimlikleAra(Kayit[] depo, Integer aranan) {
        for (Kayit k : depo) {
            Integer adet = k.adet();            // kutulama: Integer.valueOf
            if (adet == aranan) return k;       // iki sarmalayici: kimlik karsilastirmasi
        }
        return null;
    }

    static Kayit degerleAra(Kayit[] depo, Integer aranan) {
        for (Kayit k : depo) {
            Integer adet = k.adet();
            if (adet.equals(aranan)) return k;  // deger karsilastirmasi
        }
        return null;
    }

    public static void main(String[] args) {
        Kayit[] depo = { new Kayit("vida", 127, 40L), new Kayit("civata", 128, 90L) };
        System.out.printf("%-10s %-12s %-12s%n", "aranan", "kimlikle", "degerle");
        for (Integer aranan : new Integer[] {127, 128}) {
            Kayit a = kimlikleAra(depo, aranan), b = degerleAra(depo, aranan);
            System.out.printf("adet %-5d %-12s %-12s%n", aranan,
                    a == null ? "bulunamadi" : a.ad(), b == null ? "bulunamadi" : b.ad());
        }
        System.out.println();
        Integer a = 127, b = 127, c = 128, d = 128;
        System.out.println("Integer 127 == 127 : " + (a == b) + " | equals: " + a.equals(b));
        System.out.println("Integer 128 == 128 : " + (c == d) + " | equals: " + c.equals(d));
        Long e = 127L, f = 127L;
        Double g = 1.0, h = 1.0;
        System.out.println("Long    127 == 127 : " + (e == f) + " | equals: " + e.equals(f));
        System.out.println("Double  1.0 == 1.0 : " + (g == h) + " | equals: " + g.equals(h));
    }
}
aranan     kimlikle     degerle
adet 127   vida         vida
adet 128   bulunamadi   civata

Integer 127 == 127 : true | equals: true
Integer 128 == 128 : false | equals: true
Long    127 == 127 : true | equals: true
Double  1.0 == 1.0 : false | equals: true

kimlikleAra 127 adetli kaydı buluyor, 128 adetli kaydı bulamıyor. Kaynakta iki arama arasında hiçbir fark yoktur; aranan değer bir arttığı için sonuç değişmiştir. Bu, kursun dördüncü okumasının en açık örneğidir: kararı programcı değil, eklenen adım vermiştir. Aynı kod küçük depolarda doğru, büyük depolarda yanlış çalışır ve derleyici uyarı üretmez.

Alt tablo sınırı çiziyor. Integer ve Long için 127 kimlik olarak eşit, 128 değil; Double için 1.0 bile eşit değil, çünkü kesirli sarmalayıcılarda önbellek yoktur. equals sütunu dört satırda da doğru: değer karşılaştırması kutulamanın hangi nesneyi döndürdüğünden etkilenmez. Kural buradan çıkar — sarmalayıcılar == ile değil, equals ile karşılaştırılır; == yalnız ilkel tiplerde değer karşılaştırmasıdır.

Aynı gözlem sınırlayıcı ölçümü de verir: bu dersin ölçtüğü eklenen adımın, kimlik üzerinde hiçbir etkisinin olmadığı bir aralık vardır. -128 ile 127 arasında kutulama yeni nesne üretmez, == beklendiği gibi davranır ve nesne sayısı değer sayısına eşittir. Eklenen adımın görünürlüğü, değerin hangi aralığa düştüğüne bağlıdır.

Özet

  • İlkel tip ile sarmalayıcı arasındaki geçiş kaynakta bir eşittir işaretidir, sınıf dosyasında bir çağrıdır: kutulama Integer.valueOf, kutudan çıkarma Integer.intValue ekler.
  • Aynı döngü dizide 24 komut ve 0 eklenen adım, List<Integer> üzerinde 20 komut ve 5 eklenen adım üretir; daha az komut, daha çok eklenen adım.
  • Integer.valueOf her çağrıda yeni nesne üretmez: aynı 200 kutulama, değerlerin aralığına göre 100, 172 ya da 200 ayrı nesne bırakır.
  • Belirtim -128 ile 127 arasında aynı nesnenin döndürülmesini zorunlu kılar; bu aralıkta == doğru görünür, dışında aynı kod yanlış sonuç verir ve derleyici uyarı üretmez.
  • Sarmalayıcılar equals ile karşılaştırılır; equals dört ölçümün dördünde de doğru sonuç verdi, == yalnız ilkel tiplerde değer karşılaştırmasıdır.

Sonraki Adım

Bu ders bir değerin hangi biçimde tutulduğunu ölçtü: sayının kendisi mi, yoksa sayıyı saran bir nesne mi. Geriye adın kendisi kaldı. Sıradaki ders aynı sayıda adı üç ayrı yere yazar — bir yöntemin içine, bir örneğe ve sınıfın kendisine — ve sınıf dosyasında bu üç adın nereye düştüğünü sayar: hangisi bir yuvaya, hangisi bir alana yazılıyor, bir bloğun içinde açılan ad neden sınıf dosyasında hiç ad olarak görünmüyor ve iç blokta aynı adı yeniden bildirmek neden derleme hatası veriyor.

İ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