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
adetdeğeri ayrıdır; tek fark değerlerin başladığı tabandır. Her depoda heradetiki 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 çıkarmaInteger.intValueekler. - 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.valueOfher ç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
equalsile karşılaştırılır;equalsdö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.