Ders 06 / 13
`final` Anahtar Sözcüğü
Tek bir anahtar sözcük üç ayrı kısıt koyar ve üçünü de derleme zamanında uygular: yeniden atama, yeniden tanımlama ve genişletme. Sabit bir final alanın değeri okuyan sınıfa yazılır ve kaynak değiştiğinde eski değer yerinde kalır; final bir referans ise nesneyi dondurmaz.
İçindekiler
Önceki dersin son ölçümünde bir değer, ait olduğu sınıfı hiç başlatmadan okundu: Ust.TAVAN
bir static final alandı ve sınıf başlatma bloğu çalışmadan doğru sayı geldi. O sonucun
nedeni tek bir sözcükte, final sözcüğünde duruyordu ve orada yalnız bir yan etki olarak
göründü.
Bu ders o sözcüğü doğrudan ölçer. final üç ayrı yerde yazılabilir — bir değişkenin, bir
yöntemin ve bir sınıfın başında — ve üç ayrı kısıt koyar. Üç kısıtın ortak yanı, hepsinin
kararının derleme zamanında verilmesidir: hiçbiri çalışma zamanında sınanan bir koşul
değildir. Sorular üç tanedir: bu kısıtların kaçı derleyicide durur, sabit bir alanın değeri
nereye yazılır, ve final bir referans neyi korur?
Ölçüm Çekirdeği
Ölçüm üç şey yapar: bir kaynağı derler ve derlenip derlenmediğini bildirir, ürettiği sınıf
dosyasındaki final bayraklarını okur, ve derlenmiş sınıfları ayrı bir yükleyicide
çalıştırır.
- SN26 — Derleme denemelerinde kâhin kurgunun kendisidir: her denemede tek bir satır değişir, dolayısıyla derlemenin durması yalnız o satıra bağlanabilir. Sonuç tek bir çıkış koduyla okunur; hata metni okunmaz, çünkü metin yerel ayara bağlıdır.
- SN27 — Her deneme kendi geçici dizininde derlenir; denemeler birbirinin ürettiği sınıf dosyasını görmez.
- SN28 — Gömme ölçümünde
Sabitiki kez derlenir,Kullananbir kez; ikinci koşumda okunan sınıf dosyası birincisiyle aynı dosyadır. - SN29 — Koşum ayrı bir yükleyicide yapılır, böylece ilk koşumda yüklenen sınıf ikincisine taşınmaz.
- SN30 — Ortama bağlı hiçbir veri okunmaz; sayılan şey derlenen deneme sayısı, sınıf dosyasındaki bayraklar ve okunan değerdir.
// Olcek.java — derleme sonucu, sınıf dosyasındaki final bayrağı ve okuyan tarafın izi
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.lang.reflect.AccessFlag;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Olcek {
static Path yeniDizin() throws Exception { return Files.createTempDirectory("olcek"); }
static boolean derle(Path d, String ad, String kaynak) throws Exception {
Path k = d.resolve(ad + ".java");
Files.writeString(k, kaynak);
PrintWriter bos = new PrintWriter(Writer.nullWriter());
return ToolProvider.findFirst("javac").orElseThrow()
.run(bos, bos, "-d", d.toString(), "-cp", d.toString(), k.toString()) == 0;
}
static String finalBayraklari(Path d, String sinif) throws Exception {
ClassModel cm = ClassFile.of().parse(d.resolve(sinif + ".class"));
List<String> f = new ArrayList<>();
if (cm.flags().has(AccessFlag.FINAL)) f.add("sinif " + sinif);
for (FieldModel x : cm.fields())
if (x.flags().has(AccessFlag.FINAL)) f.add("alan " + x.fieldName().stringValue());
for (MethodModel x : cm.methods())
if (x.flags().has(AccessFlag.FINAL)) f.add("yontem " + x.methodName().stringValue());
return String.join(", ", f);
}
static List<String> izler(Path d, String sinif, String yontem) throws Exception {
ClassModel cm = ClassFile.of().parse(d.resolve(sinif + ".class"));
List<String> iz = new ArrayList<>();
for (MethodModel m : cm.methods()) {
if (!m.methodName().stringValue().equals(yontem) || m.code().isEmpty()) continue;
for (CodeElement e : m.code().get()) {
if (e instanceof FieldInstruction f)
iz.add("alan okumasi " + kisa(f.owner().asInternalName())
+ "." + f.name().stringValue());
else if (e instanceof ConstantInstruction c)
iz.add("sabit yukleme " + c.constantValue());
else if (e instanceof InvokeInstruction iv)
iz.add(switch (iv.opcode()) {
case INVOKEVIRTUAL -> "sanal cagri ";
case INVOKESTATIC -> "statik cagri ";
default -> "dogrudan cagri ";
} + kisa(iv.owner().asInternalName()) + "." + iv.name().stringValue());
}
}
return iz;
}
static Object calistir(Path d, String sinif, String yontem) throws Exception {
try (URLClassLoader cl = new URLClassLoader(new URL[] {d.toUri().toURL()}, null)) {
var m = cl.loadClass(sinif).getDeclaredMethod(yontem);
m.setAccessible(true);
return m.invoke(null);
}
}
static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}
Üç Kısıt, Üçü de Derleme Zamanında
İlk ölçüm dokuz kaynak derler. Denemeler çiftler hâlinde kurulmuştur: her final denemesinin
yanında, tek farkı o sözcüğün yokluğu olan bir eşi vardır.
// Kisit.java — final üç ayrı kısıt koyar, üçü de derleme zamanında
import java.nio.file.Path;
public class Kisit {
static final String[][] DENEME = {
{"yerel degiskene yeniden atama", "class T { void f() { int y = 1; y = 2; } }"},
{"final yerele yeniden atama", "class T { void f() { final int y = 1; y = 2; } }"},
{"alana yeniden atama", "class T { int a; T() { a = 1; } void f() { a = 2; } }"},
{"final alana yeniden atama", "class T { final int a; T() { a = 1; } void f() { a = 2; } }"},
{"final alana yapicida atama", "class T { final int a; T() { a = 1; } }"},
{"yontemi yeniden tanimlama", "class A { int f() { return 1; } } class T extends A { @Override int f() { return 2; } }"},
{"final yontemi yeniden tanimlama","class A { final int f() { return 1; } } class T extends A { int f() { return 2; } }"},
{"sinifi genisletme", "class A { } class T extends A { }"},
{"final sinifi genisletme", "final class A { } class T extends A { }"},
};
public static void main(String[] args) throws Exception {
System.out.printf("%-33s %s%n", "denenen yazim", "derleniyor mu");
for (String[] d : DENEME)
System.out.printf("%-33s %s%n", d[0], Olcek.derle(Olcek.yeniDizin(), "T", d[1]));
String kaynak = """
final class Bayrak {
final int sabitAlan = 1;
int digerAlan = 2;
final int kilitli() { return 1; }
int serbest() { final int y = 1; return y; }
}
""";
Path d = Olcek.yeniDizin();
Olcek.derle(d, "Bayrak", kaynak);
System.out.println();
System.out.println("sinif dosyasinda final bayragi tasiyanlar:");
System.out.println(" " + Olcek.finalBayraklari(d, "Bayrak"));
Path e = Olcek.yeniDizin();
Olcek.derle(e, "A", """
class A { final int kilitli() { return 1; } int serbest() { return 2; } }
class Cagiran {
static int a(A x) { return x.kilitli(); }
static int b(A x) { return x.serbest(); }
}
""");
System.out.println();
System.out.println("final yontem cagrisi : " + Olcek.izler(e, "Cagiran", "a"));
System.out.println("final olmayan yontem cagrisi: " + Olcek.izler(e, "Cagiran", "b"));
}
}
denenen yazim derleniyor mu yerel degiskene yeniden atama true final yerele yeniden atama false alana yeniden atama true final alana yeniden atama false final alana yapicida atama true yontemi yeniden tanimlama true final yontemi yeniden tanimlama false sinifi genisletme true final sinifi genisletme false sinif dosyasinda final bayragi tasiyanlar: sinif Bayrak, alan sabitAlan, yontem kilitli final yontem cagrisi : [sanal cagri A.kilitli] final olmayan yontem cagrisi: [sanal cagri A.serbest]
Dokuz denemenin dördü durdu ve dördü de bir final denemesiydi. Eşleri sorunsuz derlendi;
tek fark o sözcüktür. Beşinci satır kısıtın tam yerini gösterir: final bir alana atama
yasak değildir, yeniden atama yasaktır. Alan yapıcıda bir kez atanabilir ve orada
atanmazsa derleme yine durur; kısıt “hiç yazılmasın” değil, “tam bir kez yazılsın” biçimindedir.
Yedinci satır kursun sorusuna bağlanır. Bir yöntem final bildirildiğinde ona alt sınıftan
başka bir yanıt yazmak olanaksızdır. Önceki derste bir çağrının iki olası yanıtı vardı ve
hangisinin geleceğini çalışma zamanı tipi belirliyordu. final bu sayıyı bire indirir:
çalışma zamanı tipi ne olursa olsun tek bir gövde kalır, dolayısıyla soru anlamını yitirir.
final çalışma zamanı tipinin kararını bildirilen tipe taşımaz; kararı ortadan kaldırır.
Çıktının son iki satırı bu okumayı bir adım keskinleştiriyor. final bir yöntemin çağrısı da,
final olmayanınki de sınıf dosyasına sanal çağrı olarak yazılıyor; iki satır arasında
hiçbir fark yok. Yani derleyici çağrının düzeneğini değiştirmiyor, çağrının seçebileceği
gövde kümesini bire indiriyor. Önceki dersteki statik çağrı ile arasındaki fark budur: orada
yazım değişmeden hedef değişiyordu, burada hedef seçimi yerinde duruyor ama seçilecek
tek bir aday kalıyor.
Ortadaki blok kısıtların nerede durduğunu ayırır. Sınıf dosyasında final bayrağını üç şey
taşıyor: sınıfın kendisi, sabitAlan ve kilitli yöntemi. serbest yönteminin gövdesindeki
final int y bildirimi listede yok. Yerel değişkenin kısıtı sınıf dosyasına hiç
yazılmaz; o kısıt yalnız derleyicinin belleğinde yaşar ve derleme bittiğinde ortadan kalkar.
Öteki ikisi bayrak olarak kalır, çünkü onları başka bir derleme okuyacaktır: bir alt sınıf
yazan derleyici kilitli yönteminin bayrağını görmek zorundadır.
Sabit Değer Nereye Yazılıyor
Önceki dersin TAVAN ölçümü bir sorun bırakmıştı: sınıf başlatılmadığı hâlde değer nereden
geldi? Aşağıdaki ölçüm iki alanı yan yana koyar. TAVAN değerini doğrudan bir sayıdan alır;
TABAN aynı sayıyı bir yöntem çağrısından alır. İkisi de static finaldır.
// Gomme.java — sabit final alanın değeri okuyan sınıfa yazılır
import java.nio.file.Path;
public class Gomme {
static String sabit(int n) {
return """
class Sabit {
static final int TAVAN = %d;
static final int TABAN = hesapla();
static int hesapla() { return %d; }
}
""".formatted(n, n);
}
static final String KULLANAN = """
class Kullanan {
static int tavan() { return Sabit.TAVAN; }
static int taban() { return Sabit.TABAN; }
static String deger() { return "TAVAN=" + tavan() + " TABAN=" + taban(); }
}
""";
public static void main(String[] args) throws Exception {
Path d = Olcek.yeniDizin();
Olcek.derle(d, "Sabit", sabit(100));
Olcek.derle(d, "Kullanan", KULLANAN);
for (String y : new String[] {"tavan", "taban"})
System.out.println("Kullanan." + y + "() icindeki iz: " + Olcek.izler(d, "Kullanan", y));
System.out.println();
System.out.println("ilk kosum : " + Olcek.calistir(d, "Kullanan", "deger"));
Olcek.derle(d, "Sabit", sabit(200));
System.out.println("Sabit 200 ile yeniden derlendi, Kullanan derlenmedi");
System.out.println("ikinci kosum : " + Olcek.calistir(d, "Kullanan", "deger"));
}
}
Kullanan.tavan() icindeki iz: [sabit yukleme 100] Kullanan.taban() icindeki iz: [alan okumasi Sabit.TABAN] ilk kosum : TAVAN=100 TABAN=100 Sabit 200 ile yeniden derlendi, Kullanan derlenmedi ikinci kosum : TAVAN=100 TABAN=200
İlk iki satır soruyu yanıtlıyor. tavan yönteminin sınıf dosyasında Sabit sınıfına hiçbir
referans yok; yalnız 100 sayısının kendisi var. taban yönteminde ise bir alan okuması
duruyor ve o okumanın sahibi Sabittir. Kaynakta iki satır birbirinin aynıdır — ikisi de bir
noktadan sonra bir alan adı yazar — ama biri değeri okuyan sınıfa kopyalamış, öteki bir
bağ bırakmıştır.
Kopyalamanın bedeli ikinci koşumda görünüyor. Sabit 200 değeriyle yeniden derlendi;
Kullanan derlenmedi ve diskte eski hâliyle duruyor. Çalıştırıldığında TABAN yeni değeri,
200’ü veriyor: bağ duruyordu, bağ üzerinden okundu. TAVAN ise hâlâ 100 diyor.
Kaynağa bakan biri Sabit içinde 200 görecek, çalışan programda 100 okuyacaktır.
Bu, kursun sorduğu sorunun en sert biçimidir. Karar burada bildirilen tiple çalışma zamanı
tipi arasında değil, derleme zamanıyla çalışma zamanı arasında bölünmüştür — ama sonucu
aynıdır: iki taraftan hangisinin yanıt verdiğini bilmeyen bir programcı, kaynakta gördüğü
değerin çalıştığını sanır. Kusur Sabiti değiştiren kişide de değildir; Kullananın yeniden
derlenmemesindedir ve hiçbir yerde bir hata iletisi üretmez.
Sınırlayıcı Ölçüm: final Referans Nesneyi Dondurmaz
Üç kısıt da ölçüldü ve hepsi derleme zamanında duruyor. Şimdi bu tezin sınırı: final bir ada
bağlı nesnenin içi ne oluyor?
// Dondur.java — final referans nesneyi dondurmaz
import java.util.Arrays;
public class Dondur {
static class Kayit {
String ad;
int adet;
Kayit(String ad, int adet) { this.ad = ad; this.adet = adet; }
@Override public String toString() { return ad + ":" + adet; }
}
static final String GOVDE = """
class Deneme {
static class Kayit { int adet; }
void f() { final Kayit k = new Kayit(); %s }
}
""";
public static void main(String[] args) throws Exception {
final Kayit k = new Kayit("civata", 12);
final int[] agirlik = {10, 20, 30};
Kayit ilk = k;
System.out.println("baslangic : " + k + " " + Arrays.toString(agirlik));
k.ad = "somun";
k.adet = 99;
agirlik[0] = 99;
System.out.println("degisiklikten son: " + k + " " + Arrays.toString(agirlik));
System.out.println("referans ayni mi : " + (k == ilk));
System.out.println();
for (String[] y : new String[][] {{"k.adet = 1;", "alani degistirme"},
{"k = new Kayit();", "referansi yeniden baglama"}})
System.out.printf("%-26s -> derleniyor mu: %s%n", y[1],
Olcek.derle(Olcek.yeniDizin(), "Deneme", GOVDE.formatted(y[0])));
}
}
baslangic : civata:12 [10, 20, 30] degisiklikten son: somun:99 [99, 20, 30] referans ayni mi : true alani degistirme -> derleniyor mu: true referansi yeniden baglama -> derleniyor mu: false
k bir final yerel değişkendir ve iki alanının ikisi de değişti; agirlik de finaldır ve
ilk öğesi değişti. Buna karşılık k == ilk hâlâ doğru: referans hiç değişmedi. Korunan
şey nesne değil, ad ile nesne arasındaki bağdır.
Son iki satır ayrımı tek ölçüye indiriyor. Aynı final bildirimi üzerinde alanı değiştiren
satır derleniyor, referansı yeniden bağlayan satır derlenmiyor. final, Java Temelleri
kursunda ölçülen aktarım modelinin üstüne bir kısıt koyar ve o kısıt yalnız bağlamaya
değer: değerin kendisi bir referans olduğunda, referansın gösterdiği nesne bu kısıtın dışında
kalır.
Buradan iki sonuç çıkar. Birincisi, final bir alan değiştirilemezlik değildir; bir tipin
değiştirilemez olması için alanlarının final olması yetmez, o alanların gösterdiği nesnelerin
de değişmemesi gerekir. İkincisi, ölçümün gözlenemediği durum vardır: agirlik yerine bir
sayı ya da bir dizgi konsaydı ayrım hiç görünmezdi, çünkü o değerlerin değiştirilebilecek bir
içi yoktur. Kısıtın yetip yetmediği, final sözcüğüne değil tipe bağlıdır.
Özet
finalüç ayrı kısıt koyar ve dokuz denemenin dördü tam bu sözcük yüzünden durur: yeniden atama, yeniden tanımlama ve genişletme.- Kısıt “hiç atanmasın” değil, “tam bir kez atansın” biçimindedir;
finalbir alan yapıcıda atanabilir. finalbir yöntem çalışma zamanı tipinin verebileceği yanıt sayısını ikiden bire indirir; kararı öteki tarafa taşımaz, ortadan kaldırır.- Sınıf dosyasında
finalbayrağını sınıf, alan ve yöntem taşır; yerel değişkenin kısıtı hiç yazılmaz, çünkü onu başka bir derlemenin okumasına gerek yoktur. - Sabit bir
static finalalanın değeri okuyan sınıfa yazılır: kaynak 200 olduğu hâlde yeniden derlenmeyen okuyucu 100 okumayı sürdürür, hesaplanan alan ise 200 verir. finalbir referans nesneyi dondurmaz; alanları ve dizinin öğeleri değişir, yalnız ad ile nesne arasındaki bağ korunur.
Sonraki Adım
Bu dersin ölçümünde bir kısıt sınıf dosyasına hiç yazılmadı: final bir yerel değişkenin
kısıtı yalnız derleyicide durdu ve derleme bittiğinde ortadan kalktı. Öyleyse o kısıt neye
yarıyor? Java’da derleyicinin bu sözü yazılmadığı hâlde kendiliğinden istediği bir yer
vardır: bir yöntemin gövdesi içinde tanımlanan bir sınıf, dışarıdaki yerel değişkeni
kullandığında o değişkenin bir daha atanmamasını şart koşar. Şartın nedeni, o değişkenin
oraya kopyalanmasıdır — ve kopyayı taşımak için ortaya kaynakta hiç yazılmamış yeni sınıf
dosyaları çıkar. Sonraki ders bu dosyaları sayar: iç içe yazılan bir sınıf kaç ayrı dosya
üretiyor, hangileri dış örneğe gizli bir alan taşıyor, ve o gizli alanla kopyalanan değer
arasındaki fark ne zaman görünür oluyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.