Ders 11 / 13
İstisna Hiyerarşisi
Denetlenen ile denetlenmeyen istisna ayrımının derleme zamanında ne zorladığı ölçülür: dokuz yazımın yalnız biri reddedilir, bildirim sınıf dosyasında komut değil öznitelik olarak durur ve sanal makine onu denetlemez. Yakalamanın hangi dalı çalıştıracağına ise çalışma zamanı tipi karar verir.
İçindekiler
Önceki iki ders derleyicinin kapattığı kapıları saydı: yanlış tipe yazma derlenmedi, kovaryant görünüme yazma derlenmedi, kaptan okunan değeri aynı kaba geri yazmak derlenmedi. Hepsinde derleyici bir yasak koydu. Java’da derleyicinin kaynağa dayattığı bir de zorunluluk vardır: kimi olaylar bildirilmeden ya da işlenmeden bir yöntem yazılamaz.
İstisnanın bir nesne olduğu, yakalamanın bir sınıf aldığı ve hiyerarşinin kaç olayı tuttuğu Python Temelleri kursunda ölçüldü; o aritmetik burada tekrarlanmıyor. Buradaki soru kursun sorusudur ve iki yanıtı vardır: bir istisnayı bildirme zorunluluğuna bildirilen tip karar verir, hangi yakalama dalının çalışacağına ise çalışma zamanı tipi. Bu ders ikisini de aynı yerde ölçer.
Derleyicinin Zorladığı Tek Şey
- GN15 — Üç istisna türü kaynakta dersin içinde tanımlanır:
Exceptionaltında bir denetlenen istisna,RuntimeExceptionaltında bir denetlenmeyen istisna,Erroraltında bir ağır hata. - GN16 — Üç yazım biçimi aynı gövdeyi taşır; yalnız yöntemin bildirimi ve gövdenin çevresi değişir.
- GN17 — Derleyicinin ileti metni basılmaz; ölçülen şey yazımın kabul edilip edilmediğidir.
// Olcek.java — ortak ölçüm çekirdeği: sınıf dosyasını derleyip okur
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Olcek {
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<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); }
}
// Zorunlu.java — üç istisna türü, üç yazım biçimi
public class Zorunlu {
static final String TIPLER = """
class UstIstisna extends Exception { UstIstisna(String m) { super(m); } }
class SessizIstisna extends RuntimeException { SessizIstisna(String m) { super(m); } }
class AgirHata extends Error { AgirHata(String m) { super(m); } }
""";
static final String[] TURLER = {"UstIstisna", "SessizIstisna", "AgirHata"};
static final String BILDIRMEDEN = "class C { static void f() { throw new %1$s(\"olay\"); } }";
static final String BILDIREREK =
"class C { static void f() throws %1$s { throw new %1$s(\"olay\"); } }";
static final String YAKALAYARAK =
"class C { static void f() { try { throw new %1$s(\"olay\"); } catch (%1$s e) { } } }";
static String dene(String kalip, String tur) {
try {
Olcek.derle(TIPLER + kalip.formatted(tur), "C");
return "derlendi";
} catch (Exception e) {
return "derlenmedi";
}
}
public static void main(String[] args) {
System.out.printf("%-16s %-12s %-12s %s%n",
"istisna turu", "bildirmeden", "bildirerek", "yakalayarak");
int gecen = 0;
for (String t : TURLER) {
String a = dene(BILDIRMEDEN, t), b = dene(BILDIREREK, t), c = dene(YAKALAYARAK, t);
for (String s : new String[] {a, b, c}) if (s.equals("derlendi")) gecen++;
System.out.printf("%-16s %-12s %-12s %s%n", t, a, b, c);
}
System.out.println("dokuz yazimin derlenen sayisi: " + gecen);
}
}
istisna turu bildirmeden bildirerek yakalayarak UstIstisna derlenmedi derlendi derlendi SessizIstisna derlendi derlendi derlendi AgirHata derlendi derlendi derlendi dokuz yazimin derlenen sayisi: 8
Dokuz yazımın sekizi derleniyor. Reddedilen tek yazım tablonun sol üst köşesindedir: denetlenen bir istisnayı fırlatıp ne bildiren ne yakalayan yöntem. Denetlenen istisnanın derleme zamanında zorladığı şeyin tamamı budur — bir seçim yapmak. Bildirmek ile yakalamak arasında derleyici için hiçbir fark yoktur; ikisi de aynı kapıyı kapatır ve iki sütun da eşit derecede geçerlidir.
İkinci ve üçüncü satırlar ayrımın karşı yüzüdür. Denetlenmeyen istisna ile ağır hata için üç yazımın üçü de derleniyor: bildirilebilirler, yakalanabilirler, ama hiçbiri zorunlu değildir. Denetlenmeyen bir istisna kaynağın hiçbir yerinde iz bırakmadan bir yöntemden çıkabilir.
Buradan iki cümle çıkıyor. Birincisi: denetlenen/denetlenmeyen ayrımı bir davranış ayrımı değildir; ikisi de fırlatılır, ikisi de yakalanır, ikisi de çağrı yığıtını yukarı doğru katlar. Ayrım yalnız derleyicinin bir yazımı kabul edip etmemesinde görünür. İkincisi: bu zorunluluk bildirilen tipe bakar. Derleyici gövdenin gerçekte ne fırlatacağını çalıştırarak öğrenmez; çağrılan yöntemlerin bildirimlerini okur.
Bildirim Bir Özniteliktir, Komut Değil
- GN18 — İki kaynak da tek bir yöntem taşır ve gövdeleri aynı biçimdedir; yalnız fırlatılan istisnanın türü değişir.
// Bildirim.java — denetlenen istisna bildirimi sınıf dosyasının neresinde
import java.lang.classfile.*;
import java.lang.classfile.attribute.ExceptionsAttribute;
import java.util.List;
public class Bildirim {
static final String TIPLER = """
class UstIstisna extends Exception { UstIstisna(String m) { super(m); } }
class SessizIstisna extends RuntimeException { SessizIstisna(String m) { super(m); } }
""";
static final String BILDIREN = TIPLER
+ "class C { static void f() throws UstIstisna { throw new UstIstisna(\"olay\"); } }";
static final String BILDIRMEYEN = TIPLER
+ "class C { static void f() { throw new SessizIstisna(\"olay\"); } }";
static void yaz(String ad, String kaynak) throws Exception {
for (MethodModel m : Olcek.derle(kaynak, "C").methods())
if (m.methodName().stringValue().equals("f"))
System.out.printf("%-14s tanim %-6s bildirim ozniteligi %s%n", ad,
m.methodType().stringValue(),
m.findAttribute(Attributes.exceptions())
.map(ExceptionsAttribute::exceptions).orElse(List.of())
.stream().map(e -> Olcek.kisa(e.asInternalName())).toList());
}
public static void main(String[] args) throws Exception {
yaz("denetlenen", BILDIREN);
yaz("denetlenmeyen", BILDIRMEYEN);
System.out.println();
System.out.println("denetlenen firlatma : " + Olcek.komutlar(BILDIREN, "C", "f"));
System.out.println("denetlenmeyen firlatma: " + Olcek.komutlar(BILDIRMEYEN, "C", "f"));
}
}
denetlenen tanim ()V bildirim ozniteligi [UstIstisna] denetlenmeyen tanim ()V bildirim ozniteligi [] denetlenen firlatma : [new, dup, ldc, invokespecial, athrow] denetlenmeyen firlatma: [new, dup, ldc, invokespecial, athrow]
İki yöntemin tanımı aynıdır: ()V. Yöntemin sanal makineye görünen kimliği parametre
tipleriyle dönüş tipinden oluşur; fırlatabileceği istisna oraya girmez. Bildirim ayrı bir
öznitelikte durur ve denetlenmeyen istisnada o öznitelik boştur.
Alt satırlar bunun kod tarafındaki karşılığıdır: iki fırlatma birebir aynı beş komuttur. Nesneyi üret, çoğalt, iletiyi yükle, yapıcıyı çağır, fırlat. Sınıf dosyasının yürütülen bölümünde denetlenen ile denetlenmeyen istisna arasında hiçbir fark yoktur. Fark yalnız derleyicinin okuduğu öznitelikte durur — tıpkı tip parametresinin yalnız imza özniteliğinde durması gibi. Bu kursun ilk dersinde ölçülen kalıp burada bir kez daha çıkıyor: bilgi sınıf dosyasında veri olarak yazılıdır, denetim olarak değil.
Sanal Makine Bildirime Bakmaz
- GN19 — Ölçüm hiçbir dış araç kullanmadan, dilin kendi içinde kurulur; istisna nesnesi ile ilgili tek bilgi olarak sınıf adı basılır.
// Gizli.java — sanal makine bildirimi denetliyor mu
class UstIstisna extends Exception {
UstIstisna(String m) { super(m); }
}
public class Gizli {
@SuppressWarnings("unchecked")
static <T extends Throwable> void firlat(Throwable t) throws T {
throw (T) t;
}
static void bildirmeyen() {
Gizli.<RuntimeException>firlat(new UstIstisna("olay"));
}
public static void main(String[] args) {
try {
bildirmeyen();
System.out.println("istisna cikmadi");
} catch (Throwable t) {
System.out.println("bildirmeyen yontemden cikan : " + t.getClass().getSimpleName());
System.out.println("denetlenen istisna mi : "
+ (t instanceof RuntimeException || t instanceof Error ? "hayir" : "evet"));
System.out.println("sanal makine yolu kesti mi : hayir");
}
}
}
bildirmeyen yontemden cikan : UstIstisna denetlenen istisna mi : evet sanal makine yolu kesti mi : hayir
bildirmeyen yöntemi hiçbir istisna bildirmiyor ve bildirim özniteliği boş. Buna karşın o
yöntemden denetlenen bir istisna çıkıyor ve çalışma boyunca hiçbir şey bu yolu kesmiyor.
Bunun nasıl olduğu bu konunun iki yarısını birleştiriyor. firlat yöntemi T tip
parametresiyle bildirilmiş, çağrı yerinde T yerine RuntimeException konmuş, yani
derleyici için bu yöntem denetlenmeyen bir istisna fırlatır. Gövdedeki dönüşüm ise T’ye
yapılıyor ve tip silme yüzünden o dönüşüm sınıf dosyasında hiçbir denetim bırakmıyor.
Derleyicinin bildirimle kurduğu güvence, tip parametresinin silinmesiyle açılan boşluktan
geçip gidiyor.
Ölçümün söylediği kural şudur: denetlenen istisna bildirimi derleyiciler arası bir sözleşmedir, çalışma zamanı güvencesi değildir. Sanal makine bir yöntemden çıkan istisnanın o yöntemin bildiriminde yazıp yazmadığını sınamaz. Bu, kursun ölçüsüne göre saf bir “bildirilen tip” kararıdır ve çalışma zamanı tipinin bu konuda söyleyecek hiçbir şeyi yoktur.
Bildirim Daraltılabilir, Genişletilemez
- GN20 — Beş denemede yalnız alt sınıfın bildirimi değişir; üst sınıfın bildirimi, istisna tipleri ve çağıran kod her denemede aynıdır.
// Devir.java — yeniden tanımlayan yöntem bildirimi genişletebilir mi
public class Devir {
static final String KALIP = """
class UstIstisna extends Exception { UstIstisna(String m) { super(m); } }
class AltIstisna extends UstIstisna { AltIstisna(String m) { super(m); } }
class BagimsizIstisna extends Exception { BagimsizIstisna(String m) { super(m); } }
class Ust { void f() throws UstIstisna { } }
class Alt extends Ust { @Override void f() %s { } }
class C { static void c(Ust u) { try { u.f(); } catch (Exception e) { } } }
""";
static void dene(String bildirim) {
String ad = bildirim.isEmpty() ? "(bildirimsiz)" : bildirim;
try {
Olcek.derle(KALIP.formatted(bildirim), "C");
System.out.printf("Alt.f() %-28s -> derlendi%n", ad);
} catch (Exception e) {
System.out.printf("Alt.f() %-28s -> derlenmedi%n", ad);
}
}
public static void main(String[] args) throws Exception {
for (String b : new String[] {"throws UstIstisna", "throws AltIstisna", "",
"throws BagimsizIstisna", "throws RuntimeException"})
dene(b);
System.out.println();
String cagri = """
class UstIstisna extends Exception { UstIstisna(String m) { super(m); } }
class Ust { void f() throws UstIstisna { } }
class Alt extends Ust { @Override void f() { } }
class C { static void c() { %s.f(); } }
""";
for (String bildirilen : new String[] {"new Ust()", "new Alt()"}) {
String etiket = bildirilen.equals("new Ust()") ? "Ust" : "Alt";
try {
Olcek.derle(cagri.formatted(bildirilen), "C");
System.out.printf("bildirilen tip %-4s cagrisi islemeden -> derlendi%n", etiket);
} catch (Exception e) {
System.out.printf("bildirilen tip %-4s cagrisi islemeden -> derlenmedi%n", etiket);
}
}
}
}
Alt.f() throws UstIstisna -> derlendi Alt.f() throws AltIstisna -> derlendi Alt.f() (bildirimsiz) -> derlendi Alt.f() throws BagimsizIstisna -> derlenmedi Alt.f() throws RuntimeException -> derlendi bildirilen tip Ust cagrisi islemeden -> derlenmedi bildirilen tip Alt cagrisi islemeden -> derlendi
Üst tablonun kuralı tek yönlüdür. Yeniden tanımlayan yöntem üst sınıfın bildirdiği istisnayı aynen bildirebilir, onun bir alt tipini bildirebilir, hiçbir şey bildirmeyebilir — ama başka bir denetlenen istisna ekleyemez. Denetlenmeyen istisna bildirmek her durumda serbesttir, çünkü onun için zaten hiçbir zorunluluk yoktu.
Alt iki satır bu kuralın gerekçesini ölçüyor ve kursun sorusunu tam ortasından yanıtlıyor.
Alt sınıfının yöntemi hiçbir istisna bildirmiyor. Aynı nesne Alt olarak adlandırıldığında
çağrı işlemeden derleniyor; Ust olarak adlandırıldığında derlenmiyor. Nesne aynı,
çalıştırılacak gövde aynı, sonuç aynı — değişen tek şey adın bildirilen tipi. Derleyici
işleme zorunluluğunu yalnız oradan okuduğu için, alt sınıfın bildirimi genişletmesine izin
verilseydi Ust üzerinden yazılmış hiçbir çağrı güvenilir olmazdı.
Yakalamanın İki Tarafı
// Yakala.java — yakalamanın iki tarafı: hangi dal yazılabilir, hangi dal çalışır
class UstIstisna extends Exception {
UstIstisna(String m) { super(m); }
}
class AltIstisna extends UstIstisna {
AltIstisna(String m) { super(m); }
}
public class Yakala {
static final String KALIP = """
class UstIstisna extends Exception { UstIstisna(String m) { super(m); } }
class AltIstisna extends UstIstisna { AltIstisna(String m) { super(m); } }
class BagimsizIstisna extends Exception { BagimsizIstisna(String m) { super(m); } }
class C {
static void firlat() throws UstIstisna { throw new AltIstisna("olay"); }
static void y() throws UstIstisna { try { firlat(); } catch (%s e) { } }
}
""";
static void dene(String dal) {
try {
Olcek.derle(KALIP.formatted(dal), "C");
System.out.printf("catch (%-19s -> derlendi%n", dal + " e)");
} catch (Exception e) {
System.out.printf("catch (%-19s -> derlenmedi%n", dal + " e)");
}
}
static void firlat() throws UstIstisna {
throw new AltIstisna("olay");
}
public static void main(String[] args) {
for (String dal : new String[]
{"UstIstisna", "AltIstisna", "BagimsizIstisna", "RuntimeException", "Exception"})
dene(dal);
System.out.println();
try {
firlat();
} catch (UstIstisna e) {
System.out.println("UstIstisna dali yakaladi, nesnenin sinifi: "
+ e.getClass().getSimpleName());
}
try {
firlat();
} catch (AltIstisna e) {
System.out.println("iki dal varken calisan dal : AltIstisna");
} catch (UstIstisna e) {
System.out.println("iki dal varken calisan dal : UstIstisna");
}
}
}
catch (UstIstisna e) -> derlendi catch (AltIstisna e) -> derlendi catch (BagimsizIstisna e) -> derlenmedi catch (RuntimeException e) -> derlendi catch (Exception e) -> derlendi UstIstisna dali yakaladi, nesnenin sinifi: AltIstisna iki dal varken calisan dal : AltIstisna
Üst tablo yakalamanın derleme zamanı yüzüdür ve reddedilen tek dal ilginç olanıdır.
BagimsizIstisna denetlenen bir istisnadır ve try bloğunun içindeki çağrının bildiriminde
geçmiyor; derleyici o dalı yazdırmıyor bile. Buradaki karar tümüyle bildirilen tiplerden
gelir: derleyici bloğun içinden hangi denetlenen istisnaların çıkabileceğini bildirimlerden
hesaplar ve o kümenin dışında kalan bir dalı ölü kabul eder. RuntimeException dalının
geçmesi aynı kuralın diğer yüzüdür — denetlenmeyen istisna her yerden çıkabileceği için hiçbir
zaman ölü sayılmaz.
Alt satırlar aynı yakalamanın çalışma zamanı yüzüdür ve bu dersin sınırlayıcı ölçümüdür.
Fırlatan yöntem UstIstisna bildiriyor, ama fırlattığı nesnenin sınıfı AltIstisna.
UstIstisna dalı bu nesneyi yakalıyor ve yakalanan nesnenin sınıfı AltIstisna olarak
okunuyor. İki dal yan yana konduğunda çalışan dal AltIstisna — yani seçim bildirilen tipten
değil, çalışma zamanı tipinden yapılıyor.
Aynı yapıda iki karşıt yanıt bulunması bu kursun temel iddiasının bu konudaki biçimidir. Bir
catch dalının yazılabilir olması bildirilen tipe, çalışması çalışma zamanı tipine
bağlıdır. Yanlış tarafa bakan bir programcı bunu iki ayrı biçimde ödeyebilir: dar bir dalı
üst tipe göre yazıp hiç çalışmadığını görmek, ya da geniş bir dalın altına yazdığı dar dalın
derlenmediğini görmek.
Özet
- Denetlenen istisnanın derleme zamanında zorladığı tek şey bir seçimdir: bildirmek ya da yakalamak. Dokuz yazımdan yalnız biri reddedilir; denetlenmeyen istisna ile ağır hatanın üç yazımı da geçerlidir.
- Denetlenen ile denetlenmeyen istisna arasında davranış farkı yoktur; fark yalnız derleyicinin bir yazımı kabul edip etmemesindedir.
- Bildirim yöntemin tanımına girmez, ayrı bir öznitelikte durur; iki türün fırlatma kodu birebir aynı beş komuttur.
- Sanal makine bildirimi denetlemez: tip parametresi üzerinden fırlatılan denetlenen bir istisna, hiçbir şey bildirmeyen bir yöntemden çıkabilir. Bildirim bir derleme zamanı sözleşmesidir.
- Yeniden tanımlayan yöntem bildirimi daraltabilir, genişletemez; aynı nesneye
Ustdiye bakan çağrı işlemek zorundadır,Altdiye bakan çağrı değildir. - Sınırlayıcı ölçüm: yakalamanın hangi dalının yazılabileceğine bildirilen tip, hangi dalın çalışacağına çalışma zamanı tipi karar verir. Aynı yapıda iki karşıt yanıt bir arada durur.
Sonraki Adım
Bu derste bir istisna fırlatıldı, bildirildi ve yakalandı; her ölçümde tek bir olay vardı ve onun nereye gittiği izlendi. Oysa gerçek bir gövdede istisna genellikle yalnız değildir: bir kaynak açılmışsa, gövde düşerken o kaynağın kapatılması gerekir ve kapatma da düşebilir. Bu durumda iki istisna aynı anda ortaya çıkar ve biri diğerinin üstünü örtebilir. Sıradaki ders elle yazılan kapatma ile derleyicinin ürettiği kapatmayı yan yana koyup kaç istisnanın kaybolduğunu, kaçının korunduğunu ve kapatmanın hangi sırayla yapıldığını sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.