Ders 06 / 10
İşleçler ve İfadeler
Tek karakterlik yazımlar sınıf dosyasına sessiz adımlar bırakır: `int` ile `long` toplandığında bir çevirme eklenir, `adet += 300` kaynakta yazılmayan bir daraltma koyar ve sonuç 45 çıkar, kısa devre bir dal komutu olarak belirir — sabitlerde ise hiçbir adım eklenmez.
İçindekiler
Önceki ders adın sınıf dosyasında nereye yazıldığını ölçtü: alana mı, yuvaya mı. Adlar yerlerine oturduğunda sıra onları birleştiren işleçlere gelir. Bir işleç kaynakta tek karakterdir; sınıf dosyasında ise bir ya da birkaç komuttur ve bu komutların bir bölümü kaynakta hiç yazılmaz. Bu ders o bölümü sayar.
Programlama Temelleri kursu işleçleri, önceliği, kısa devre değerlendirmeyi ve açık/örtük dönüşümü kavram olarak kurmuştu — bunlar tekrarlanmaz. Orada bir dönüşümün ne zaman genişletme, ne zaman daraltma olduğu ve daraltmanın neden sessiz bilgi kaybı üretebildiği anlatıldı. Burada ölçülen şey, o daraltmayı kimin eklediğidir: kaynakta hiçbir yerde dönüşüm yazmayan bir satır, sınıf dosyasında bir çevirme komutu taşıyabilir.
İşleç Bir Kısaltmadır
Java’nın aritmetik işleçleri yalnız dört genişlikte çalışır: int, long, float ve
double. byte, short ve char için ayrı toplama komutu yoktur. Bunun sonucu şudur:
küçük tiplerdeki değerler işleme girmeden önce int’e yükseltilir ve iki işlenen farklı
genişlikteyse dar olan geniş olana çevrilir. Buna sayısal yükseltme denir ve kaynakta
görünmez.
Yükseltmenin tersi de vardır. Bileşik atama — +=, -=, *= — sol tarafın tipini
korumak zorundadır. byte bir değişkene += ile bir int eklendiğinde toplama int
genişliğinde yapılır, sonra sonuç geri byte’a daraltılır. Bu daraltma kaynakta
yazılmaz; yazılsaydı derleyici zaten istemezdi. Aynı işlem adet = adet + 300 biçiminde
yazılsaydı derleme hatası verirdi, çünkü orada daraltmayı programcının yazması gerekir.
İki yön arasındaki asıl ayrım güvenliktir. Dar bir tipten geniş bir tipe geçiş genişletmedir ve hiçbir bilgi kaybetmez; derleyici onu sormadan yapar. Geniş bir tipten dar bir tipe geçiş daraltmadır ve sığmayan bitler atılır; derleyici onu normalde yapmaz, programcının yazmasını ister. Bileşik atama bu kuralın tek gediğidir: sol tarafın tipi zaten bellidir, bu yüzden derleyici daraltmayı isteyeceği yerde kendisi koyar. Ölçüm bu gediğin sınıf dosyasındaki karşılığını arar.
Ders bu iki yönü kurs boyunca sürdürülen depo kaydının iki alanı üzerinde ölçer: int adet
ve long agirlik. Aradaki genişlik farkı, yükseltmenin ölçülebileceği en dar örnektir.
Ölçüm Çekirdeği
Çekirdek önceki derslerinkidir; bu derste iki ölçüm okur. Birincisi çevirme komutlarıdır
— sınıf dosyasında bir genişliği başka bir genişliğe çeviren komutlar ayrı bir aileyi
oluşturur ve adları çevrilen iki tipi taşır (i2l, i2b). İkincisi dal komutlarının
sayısıdır: akışı bir noktadan başka bir noktaya götüren her komut sayılır.
- SD11 — Sağ sütun sınıf dosyasındaki çevirmeleri, dinamik çağrıları ve çağrıları listeler; kaynakta yazılmış olanı okuyucu düşer, kalanı derleyici eklemiştir.
- SD12 — Ölçülen bütün yöntemler statiktir ve gövdeleri tek satırdır; komut sayısındaki fark yöntem çağrısı düzeninden değil, yalnız ifadenin kendisinden gelir.
// Olcek.java — işlecin sınıf dosyasına bıraktığı çevirme, çağrı ve dal komutunu 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, int dal, 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, dal = 0;
for (CodeElement e : m.code().get()) {
if (e instanceof Instruction) komut++;
if (e instanceof BranchInstruction) dal++;
if (e instanceof ConvertInstruction c)
eklenen.add("cevirme " + c.opcode().name().toLowerCase(Locale.ROOT));
else if (e instanceof InvokeDynamicInstruction id)
eklenen.add("dinamik:" + id.name().stringValue());
else if (e instanceof InvokeInstruction iv)
eklenen.add(iv.name().stringValue());
}
yontemler.add(new Yontem(m.methodName().stringValue(), komut, dal, eklenen));
}
return yontemler;
}
}
On Üç İfade, Sınıf Dosyasında Ne Bırakıyor
Ölçülen sınıf on üç yöntem taşır ve her biri tek bir ifadedir. daralt ile acikDaralt
aynı işi yapar; tek fark, daraltmanın kaynakta yazılıp yazılmadığıdır. gecCevirme ile
erkenCevirme aynı çarpımı yapar; tek fark, çevirmenin çarpımdan önce mi sonra mı
istendiğidir. sabitAritmetik, sonSabitCarpim ve degiskenCarpim aynı çarpımı üç
ayrı yazımla verir.
// Islec.java — on üç ifade, sınıf dosyasında ne bırakıyor
public class Islec {
static final String KAYNAK = """
class Depo {
static long yukselt(int adet, long agirlik) { return adet + agirlik; }
static byte daralt(byte adet) { adet += 300; return adet; }
static byte acikDaralt(byte adet) { adet = (byte) (adet + 300); return adet; }
static int kucukToplam(byte adet, short raf) { return adet + raf; }
static long gecCevirme(int adet, int birim) { return adet * birim; }
static long erkenCevirme(int adet, int birim) { return (long) adet * birim; }
static String birlestir(String ad, int adet) { return ad + adet; }
static String sabitBirlestir() { return "vi" + "da"; }
static int sabitAritmetik() { return 24 * 60 * 60; }
static final int RAF = 24;
static int rafSayisi = 24;
static int sonSabitCarpim() { return RAF * 60 * 60; }
static int degiskenCarpim() { return rafSayisi * 60 * 60; }
static boolean kisaDevre(int adet, long agirlik) { return adet > 0 && agirlik > 0; }
static boolean tamDeger(int adet, long agirlik) { return (adet > 0) & (agirlik > 0); }
}
""";
public static void main(String[] args) throws Exception {
System.out.printf("%-16s %6s %4s %s%n", "yontem", "komut", "dal", "kaynakta yazilmayan adim");
for (Olcek.Yontem y : Olcek.oku(KAYNAK, "Depo")) {
if (y.ad().startsWith("<")) continue;
System.out.printf("%-16s %6d %4d %s%n", y.ad(), y.komut(), y.dal(),
y.eklenen().isEmpty() ? "-" : String.join(", ", y.eklenen()));
}
}
}
yontem komut dal kaynakta yazilmayan adim yukselt 5 0 cevirme i2l daralt 7 0 cevirme i2b acikDaralt 7 0 cevirme i2b kucukToplam 4 0 - gecCevirme 5 0 cevirme i2l erkenCevirme 6 0 cevirme i2l, cevirme i2l birlestir 4 0 dinamik:makeConcatWithConstants sabitBirlestir 2 0 - sabitAritmetik 2 0 - sonSabitCarpim 2 0 - degiskenCarpim 6 0 - kisaDevre 10 3 - tamDeger 14 4 -
İlk satır sayısal yükseltmenin sınıf dosyasındaki adıdır. adet + agirlik ifadesinde
adet bir int, agirlik bir long; toplama komutu ikisini birden alamaz, bu yüzden
derleyici araya bir i2l koyar. Kaynakta hiçbir dönüşüm yazmaz. Beş komutun biri,
programcının yazmadığı bir çeviridir.
İkinci satır bu dersin en sessiz adımıdır. adet += 300 yazımı yedi komut üretir ve
içinde bir i2b vardır: toplama int genişliğinde yapılır, sonuç byte’a daraltılır.
Daraltmayı derleyici kendiliğinden ekler. Bu, önceki kursta anlatılan sessiz bilgi kaybının
sınıf dosyasındaki karşılığıdır — kavram orada kuruldu, burada eklendiği yer gösteriliyor.
Bir alt satır bunun ne kadar birebir olduğunu ölçüyor. acikDaralt daraltmayı kaynakta
yazıyor ve sınıf dosyasında daralt ile aynı çıkıyor: aynı 7 komut, aynı i2b. İki
yazım sınıf dosyasında ayırt edilemez. += bir işleç değil, bu satırın kısaltmasıdır;
kısalttığı şeyin içinde bir dönüşüm vardır ve kısaltma onu gizler.
Dördüncü satır yükseltmenin her zaman komut üretmediğini gösteriyor. byte ile short
toplandığında ikisi de int’e yükselir, ama sınıf dosyasında hiçbir çevirme yoktur:
dört komut, sıfır eklenen adım. Bu tipler zaten int genişliğinde yüklenir, dolayısıyla
yükseltmenin ödediği bir komut yoktur. Yükseltme bir kural olarak her yerde vardır; komut
olarak yalnız genişlik gerçekten değiştiğinde belirir.
Sonraki iki satır çevirmenin yerini karşılaştırıyor. İkisi de aynı çarpımı
long olarak döndürüyor. gecCevirme tek bir i2l taşıyor ve o çevirme çarpımdan
sonradır: iki int çarpılıyor, sonuç long’a çevriliyor. erkenCevirme iki i2l
taşıyor — biri kaynakta yazılan dönüşüm, öteki ikinci işlenen için eklenen yükseltme — ve
çarpım artık long genişliğinde yapılıyor. Bir komut fazla, ama başka bir hesap.
Birleştirme satırları aynı + işlecini iki ayrı sonuca bağlıyor. Değişken içeren
birleştirme dört komutta bir dinamik çağrı bırakıyor; iki sabitin birleştirilmesi ise
iki komut ve hiçbir eklenen adım. Aynı işleç, biri bir çağrı, biri sıfır adım.
Son iki satır kısa devre değerlendirmenin sınıf dosyasındaki izidir. && kullanan biçim
10 komut ve 3 dal, & kullanan biçim 14 komut ve 4 dal üretiyor. &
biçimi iki karşılaştırmayı da doğruluk değerine çevirmek zorundadır; && ilk
karşılaştırma yanlış çıktığında ikinciye hiç girmeyen bir dal koyar. Kısa devre bir çalışma
zamanı kolaylığı değil, sınıf dosyasında duran bir atlamadır.
Eklenen Çevirmenin Görünen Sonucu
Buraya kadar sayılan adımların hepsinin bir değeri değiştiren karşılığı vardır.
- SD13 — Taşma ölçümünde kayıt sayısı 100000, birim ağırlık 50000 gramdır; çarpım
intgenişliğinde tutulamayacak kadar büyüktür ve iki yazım arasındaki tek fark çevirmenin yeridir. - SD14 — Kısa devre ölçümünde depo üç kayıt taşır ve ikisinin
adetalanı sıfırdır; sağ işlenen bir yöntem çağrısıdır ve o yöntem her çağrılışında sayacı artırır, böylece atlanan dalın kaç kez atlandığı sayılabilir.
// Sonuc.java — eklenen çevirmenin ve atlanan dalın görünen sonucu
public class Sonuc {
record Kayit(String ad, byte adet, long agirlik) {}
static int cagri = 0;
static boolean agirMi(Kayit k) { cagri++; return k.agirlik() > 0; }
public static void main(String[] args) {
byte adet = 1;
adet += 300;
System.out.println("byte adet = 1; adet += 300 -> " + adet);
int kayit = 100000, birim = 50000;
System.out.println("long gecCevirme(100000, 50000) -> " + (long) (kayit * birim));
System.out.println("long erkenCevirme(100000,50000) -> " + (long) kayit * birim);
Kayit[] depo = { new Kayit("vida", (byte) 0, 40L),
new Kayit("civata", (byte) 3, 90L),
new Kayit("somun", (byte) 0, 12L) };
cagri = 0;
int a = 0;
for (Kayit k : depo) if (k.adet() > 0 && agirMi(k)) a++;
int kisaDevre = cagri;
cagri = 0;
int b = 0;
for (Kayit k : depo) if ((k.adet() > 0) & agirMi(k)) b++;
System.out.println("&& sonuc: " + a + " agirMi cagrisi: " + kisaDevre);
System.out.println("& sonuc: " + b + " agirMi cagrisi: " + cagri);
}
}
byte adet = 1; adet += 300 -> 45 long gecCevirme(100000, 50000) -> 705032704 long erkenCevirme(100000,50000) -> 5000000000 && sonuc: 1 agirMi cagrisi: 1 & sonuc: 1 agirMi cagrisi: 3
İlk satır i2b komutunun ödettiğidir. Bir kayda 300 adet eklendi ve sonuç 45 oldu.
Kaynakta hiçbir dönüşüm yazılı değildir, derleyici uyarı vermez ve program çalışmaya devam
eder. Kararı programcı değil, eklenen adım vermiştir.
İkinci ve üçüncü satırlar çevirmenin yerinin neye mal olduğunu gösteriyor. Çarpım
int genişliğinde yapıldığında sonuç 705032704, long genişliğinde yapıldığında
5000000000. Sol taraftaki değişkenin long olması yetmez — sonucun genişliği,
çarpımdan sonra yapılan bir çeviriyle kurtarılamaz. Yükseltmenin işlem başına
uygulandığını gösteren en açık ölçüm budur.
Son iki satır kısa devrenin çalışma zamanındaki karşılığıdır. İki biçim de aynı sonucu
buluyor: 1 kayıt. Ödenen çağrı sayısı ayrı: && biçimi sağ işleneni 1 kez,
& biçimi 3 kez çalıştırıyor. Sınıf dosyasındaki bir dal komutu farkı, burada iki
yöntem çağrısı farkına dönüşüyor. Sağ işlenen bir yan etki taşısaydı iki biçim aynı
programı vermezdi; & ile && arasındaki seçim üslup değildir.
Sınırlayıcı Ölçüm: Sabitte Eklenen Adım Yok
Tablodaki üç satır aynı çarpımı üç ayrı yazımla veriyor ve üçünde de eklenen adım sıfır. Komut sayıları ayrı: 2, 2 ve 6.
- SD15 — Üç yöntem de aynı sayıyı döndürür; aralarındaki tek fark, çarpanların derleme zamanında bilinip bilinmediğidir.
sabitAritmetik iki komutta bitiyor: sınıf dosyasında çarpma komutu yok, doğrudan
sonuç yazılı. Derleme zincirinde tanıtılan sabit katlama, burada ölçülen bir şeye
dönüşüyor. sonSabitCarpim de iki komut — çünkü RAF bir static final alandır ve
başlangıç değeri derleme zamanında bellidir; derleyici adı değil, değeri yerine koyar
ve katlamayı yapar. degiskenCarpim altı komut: rafSayisi sonradan değişebileceği için
değeri alınıyor ve iki çarpma çalışma zamanında yapılıyor.
Bu üçlü dersin tezini sınırlıyor. Eklenen adım bir kural değil, bir koşula bağlı karşılıktır: işlenenlerin genişlikleri ayrıldığında çevirme eklenir, sol taraf dar olduğunda daraltma eklenir, işlenenlerden biri değişken olduğunda dinamik çağrı eklenir. İşlenenlerin hepsi derleme zamanında biliniyorsa eklenecek bir şey kalmaz — tersine, o zaman kaynakta yazılı olan işlem sınıf dosyasından çıkar. Eklenen adım bazen çıkarmadır.
Sınırın pratik karşılığı da vardır: bir sabiti static final bildirmek yalnız değişmezlik
bildirimi değildir, derleyiciye o değeri yerine koyma izni verir. Aynı sabit final
olmadan bildirildiğinde çalışma zamanında iki çarpma ödenir. Aradaki fark bu ölçümde dört
komuttur ve kaynağa bakarak görünmez.
Özet
- Sayısal yükseltme sınıf dosyasında bir çevirme komutudur:
intilelongtoplandığında beş komutun biri kaynakta yazılmayan biri2l’dir. - Bileşik atama daraltmayı kendiliğinden ekler;
adet += 300yedi komutta biri2bbırakır ve sonucu 45 yapar, derleyici uyarı üretmez. Daraltmanın kaynakta yazıldığı biçim sınıf dosyasında bundan ayırt edilemez. - Yükseltme her zaman komut üretmez:
byteileshorttoplandığında sınıf dosyasında hiçbir çevirme yoktur, çünkü ikisi de zatenintgenişliğinde yüklenir. - Yükseltme işlem başına uygulanır: aynı çarpım
intgenişliğinde 705032704,longgenişliğinde 5000000000 verir ve sonuculongbir değişkene atamak bunu düzeltmez. - Kısa devre bir dal komutudur:
&&biçimi 10 komut ve 3 dal,&biçimi 14 komut ve 4 dal üretir; çalışma zamanında sağ işlenen 1 ve 3 kez çalışır. - İşlenenlerin hepsi derleme zamanında biliniyorsa eklenen adım kalmaz: aynı çarpım sabitlerle 2, değişkenle 6 komuttur ve üçünde de eklenen adım sıfırdır.
Sonraki Adım
Bu derste akışı bir noktadan başka bir noktaya götüren dal komutu ilk kez ölçüldü ve bir
işlecin yan ürünü olarak ortaya çıktı. Sıradaki ders dalı asıl konusu yapar: if ve
switch ile yazılan aynı davranış sınıf dosyasında iki ayrı komuta derlenir ve iki
komutun arama maliyeti aynı değildir. Etiketli atlamanın iç içe döngülerde ne kazandırdığı
adım sayısıyla ölçülür, gelişmiş for döngüsünün dizide ve listede neden ayrı adım ürettiği
gösterilir — ve kazancın okunabilirlikte ne ödettiği aynı yerde yazılır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.