Sorunun Tarihçesi: Java 5'in, tür silinmesi yoluyla generikleri tanıtması, önceden genel olmayan bytecode ile ikili uyumluluğu korumak amacıyla, dil tasarımcıları mevcut JVM istisna işleme mimarisini Java 1.0'da belirlenen biçimde sürdürdü. Sınıf dosyası formatı, istisna işleyicilerini Code niteliğindeki exception_table dizisi aracılığıyla temsil eder; bu dizi, her yakalanabilir istisna türü için somut CONSTANT_Class_info yapılarının sabit havuzu indekslerini saklar. Bu tasarım kararı, istisna işleme için genel çok biçimliliğe göre çalışma zamanı performansını ve doğrulama kolaylığını önceliklendirmiştir.
Sorun: Genel tür parametreleri, derleme sırasında sınırlara (genellikle Object) silindiğinden, çalışma zamanında exception_table girişini doldurmak için belirgin bir Class literal'ı mevcut değildir. JVM bytecode doğrulayıcı, yürütme başlamadan önce istisna işleyici dağıtım tablosunu oluşturmak için statik olarak çözülmüş sınıf referanslarına ihtiyaç duyar; bu, tür güvenli bir kontrol akışı aktarımı sağlar. catch (T e) gibi bir genel catch parametresi, çalışma zamanının çözülmemiş bir tür değişkenine karşı eşleşmesini gerektirir ve bu da istisna işleyicilerinin somut, yüklenebilir sınıflara kesin sınıf hiyerarşisi verilerini referans etmesi gerektiğine dair JVM spesifikasyonunun gerekliliğini ihlal eder.
Çözüm: Derleyici, genel catch parametrelerini derleme zamanında reddederek bu kısıtlamayı uygular; bu, geliştiricileri silinmiş sınırlara (genellikle Exception veya Throwable) sarılmaya ve açık dönüşümler ile instanceof kontrollerini kullanmaya zorlar. Alternatif olarak, istisna çeviri desenleri, kontrol edilen istisnaları alanına özgü çalışma zamanı istisnalarında sarar, orijinal sebebi yapıcı vasıtasıyla korur. Bu yaklaşımlar, exception_table static yapısının bütünlüğünü korurken, tür özel işleme mantığını dinamik tür denetimi veya sonuç monadları aracılığıyla sağlar; böylece catch ifadesi parametreleştirmesini ortadan kaldırır.
Dağıtılmış bir görev yürütme çerçevesi, uygulayıcıların belirli hata modlarını belirleyebileceği genel bir Task<T extends Exception> arayüzüne ihtiyaç duyuyordu. İlk tasarım, derleme zamanında hata işleme stratejileri için tür güvenliğini sağlamak amacıyla try { task.execute(); } catch (T failure) { handler.handle(failure); } kullanımını denedi ancak bu, genel catch kısıtlaması nedeniyle derleme hatasıyla sonuçlandı.
İlk çözüm, her istisna türü için aşırı yüklenmiş sarmalayıcı sınıfları (örneğin, IOExceptionTask, SQLExceptionTask) uygulamayı düşünmüştü. Bu yaklaşım, her hata modu için derleme zamanı tür güvenliği ve belirgin yöntem imzaları sağladı; ancak sistem ölçeklendikçe kombinatoryal patlama sorunuyla karşılaştı. Geliştiricileri tür kısıtlamalarını karşılamak amacıyla gereksiz alt sınıflar oluşturmaya zorladı, bakım yükünü artırdı ve DRY ilkesini ihlal etti.
İkinci çözüm, Throwable yakalamayı ve işleyici içinde instanceof doğrulamasından sonra denetlenmemiş dönüşümleri gerçekleştirmeyi önerdi. Bu, çağrı noktasında yansımalar aracılığıyla genel tür parametrelerini barındırmasına rağmen, özellikle fillInStackTrace maliyetleri nedeniyle istisna nesnesi oluşturma açısından önemli bir çalışma zamanı yükü getirdi; bu, filtrelenmiş istisnalar için bile geçerliydi. Aynı zamanda, kapsamlı kontrolü feda etti, program hatalarını istemeden yakalanan Error türleri veya silinmiş süper sınıf ile paylaşılan kontrol edilen istisnalar nedeniyle maskeleyebilirdi.
Seçilen çözüm, bir istisna çevirme stratejisini Result<T, E> monad deseni ile birleştirmeyi benimsedi. Doğrudan istisna fırlatmak yerine, görevler, bir sealed sınıf hiyerarşisi kullanarak başarı değerlerini veya yazılı hata türlerini içeren Result nesneleri döndürdü. Bu, genel catch ifadelerine ihtiyaç duymayı tamamen ortadan kaldırdı, hata yönetimini değer alanına taşıdı ve tür güvenliğini genel dönüş türleri ile koruyarak, istisna imzalarından bağımsız hale getirdi. Çerçeve, gereksiz kodda %40'lık bir azalma, hata işleme sırasında ClassCastException risklerini ortadan kaldırdı ve beklenen hata koşulları için istisna nesnesi oluşturmayı önleyerek performansı artırdı.
Metot imzaları neden throws T ifadesini beyan edebilirken, catch ifadeleri aynı tür parametresini kullanamaz?
JVM, class dosyası formatındaki Exceptions niteliğinin, bytecode doğrulama amacıyla silinmiş türleri (genellikle Throwable) saklamasına ve genel imzanın Signature niteliğinde yansıma verileri için korunmasına izin verir. Çalışma zamanı doğrulayıcı, silinmiş türle kontrol yapar ve derleyici, T'nin çağrı noktalarında geçerli istisna türlerine bağlı olmasını statik analiz aracılığıyla zorunlu kılar. Öte yandan, catch ifadeleri exception_table'da girişler gerektirmektedir; bu, belirli program sayacı aralıklarını işleyici ofsetlerine eşleştiren somut Class havuz indekslerini kullanır. Tür değişkenleri çalışma zamanında sınıf meta verisi taşımaz ve farklı çağrı noktalarında farklı türlere bağlanabilir olduğundan, JVM, istisna işleme için gerekli statik dağıtım eşlemesini oluşturamaz, bu da genel catch ifadelerini mimari açıdan imkânsız kılar; bunun nedeni throws ifadesinin esnekliğidir.
Tür silinmesi ile denetlenmiş istisna mekanizması arasındaki etkileşim, genel istisna yakalamanın izin verilmesi durumunda ince doğrulama risklerini nasıl oluşturur?
Eğer genel yakalama izin verilseydi, catch (T e) gibi bir kod, bir çağrı noktasında IOException'a ve başka bir çağrı noktasında SQLException'a bağlansa, kaynak seviyesinde tür güvenli görünürdü. Ancak silinme nedeniyle, JVM, her iki durumu da Exception'ı (silinmiş sınır) yakalamak olarak değerlendirirdi. Bu, aynı silinmiş süper sınıfı paylaşan istenmeyen denetlenmiş istisnaları yakalamaya izin verir ve Java Language Specification'ın denetlenen istisna yakalama kurallarını ihlal ederdi. Doğrulayıcı, yakalama bloklarının yalnızca istisna alt türlerini işlemelerini garanti eder; ancak silinmiş tür farklı denetlenmiş istisna türlerini tek bir işleyicide birleştirir, bu da SecurityException gibi çalışma zamanı istisnalarının yakalanmasına ve işlem görmesine neden olabilir; bu, ayrıcalık artışı açıkları veya sessiz hata yutma durumlarına neden olabilir.
Özel bir bytecode kalıbı, tür özel catch davranışını instanceof kontrolleri kullanarak simüle ederken derleyici ne üretir ve yerel istisna tablosu dağıtımı ile karşılaştırıldığında ne tür performans etkileri doğar?
Geliştiriciler catch (Exception e) { if (e instanceof SpecificType) { handle(e); } else { throw e; } } yazdığında, derleyici Exception için bir exception_table girişi ve ardından işleyici bloğunda checkcast veya instanceof bytecode talimatları üretir. Bu, iki aşamalı bir dağıtım yaratır: önce JVM, genel türü yakalar (istisna nesnesini oluşturur ve tam yığın izini fillInStackTrace aracılığıyla elde eder), ardından kullanıcı kodu filtrelemesi yapılır. Performans etkileri, filtrelenmiş istisnalar için bile istisna nesnesi tahsis etme üst yükü ve instanceof kontrolünden kaynaklanan ek dallanma tahmin hataları içerir. Bu, yerel istisna tablosu dağıtımı ile karşılaştırıldığında, JVM'nin dahili işleyici önbelleğini O(1) tür eşleşmesi için kullanarak, filtrelenmiş istisna nesneleri oluşturmadan çok daha hızlıdır; dolayısıyla instanceof yaklaşımı, yüksek frekanslı istisna senaryolarında kat kat daha yavaş çalışır.