Ders 06 / 15
Fonksiyonel Arayüzler
Tek soyut yöntem bir derleme zamanı sözüdür: iki yöntemli bir arayüz asla lambda hedefi olamaz, işaret yalnız bu sözü erken bozan tanımı yakalar. Arayüzün ikinci sözü — davranış — hiçbir yerde denetlenmez: tip sözü ailenin dördünde de derlenir, saflık ve yan etkisizlik sözü ise Predicate ve Function'da sessizce kırılabilir.
İçindekiler
Önceki konunun son dersi sıra kuralını dışarıdan verilen bir nesneye taşımıştı: karşılaştırıcı, çağıranın koleksiyona taktığı, tek yöntemli bir tipti. Kütüphane onu çağırdı, kararı vermedi. Bu ders o nesnenin kendisine bakar — tek soyut yöntemli bir tipin, fonksiyonel arayüzün, derleyiciye ne söylediğini ve hangi sözünün hiç kimse tarafından denetlenmediğini ölçer.
Standart kütüphanenin her satırı bir sözdür ve üç kaynaktan biri o sözü verir: arayüzün kendisi, seçilen gerçekleştirim ya da çağıranın uyduğu bir kural. Fonksiyonel arayüzler bu üçlünün en dar örneğidir — yalnızca bir soyut yöntem taşırlar — ve tam da bu darlık yüzünden lambda ifadelerinin hedefi olabilirler. Bu darlığı derleyici mi tutuyor, yoksa yalnız belgede mi duruyor? Arayüzün taşıdığı ikinci bir söz daha var: yöntemin nasıl davranması gerektiği. O söz nerede duruyor, kim denetliyor?
Tek Soyut Yöntem Derleme Zamanı Bir Sözdür
- FN1 — Dört kap biçimi denenir: tek yöntemli ve iki yöntemli arayüz, her ikisi de
@FunctionalInterfaceişaretli ve işaretsiz olarak. Kaynak metinler dersin içinde yazılır. - FN2 — Bir arayüzün tanımı derlenip derlenmediği ayrı, o arayüzü lambda hedefi olarak kullanan bir sınıfın derlenip derlenmediği ayrı ölçülür; ikisi karıştırılmaz.
- FN3 — Derleyicinin hata iletisinin metni basılmaz; yalnız derlenip derlenmediği bildirilir, çünkü ölçülen şey iletinin içeriği değil kapının açık olup olmadığıdır.
// Derleme.java — ortak çekirdek: verilen kaynak metni derleniyor mu
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;
class Derleme {
static boolean derlenirMi(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());
return ToolProvider.findFirst("javac").orElseThrow()
.run(bos, bos, "-d", d.toString(), k.toString()) == 0;
}
}
// Isaret.java — @FunctionalInterface isareti neyi yakaliyor
public class Isaret {
static final String TEK = "interface Islem { int uygula(int x); }";
static final String TEK_ISARETLI = "@FunctionalInterface\ninterface Islem { int uygula(int x); }";
static final String IKI = "interface Islem { int uygula(int x); int geriAl(int x); }";
static final String IKI_ISARETLI = "@FunctionalInterface\ninterface Islem { int uygula(int x); int geriAl(int x); }";
static final String[][] DENEMELER = {
{"tek yontem, isaretsiz", TEK},
{"tek yontem, isaretli", TEK_ISARETLI},
{"iki yontem, isaretsiz", IKI},
{"iki yontem, isaretli", IKI_ISARETLI},
};
public static void main(String[] args) throws Exception {
System.out.printf("%-24s %-20s %s%n", "bicim", "tanim derleniyor mu", "lambda hedefi olabiliyor mu");
for (String[] d : DENEMELER) {
String ad = d[0], govde = d[1];
boolean tanim = Derleme.derlenirMi(govde, "Islem");
boolean lambda = tanim && Derleme.derlenirMi(
govde + "\nclass Kullan { Islem i = x -> x; }", "Islem");
System.out.printf("%-24s %-20s %s%n", ad,
tanim ? "derlendi" : "derlenmedi",
!tanim ? "-" : (lambda ? "derlendi" : "derlenmedi"));
}
}
}
bicim tanim derleniyor mu lambda hedefi olabiliyor mu tek yontem, isaretsiz derlendi derlendi tek yontem, isaretli derlendi derlendi iki yontem, isaretsiz derlendi derlenmedi iki yontem, isaretli derlenmedi -
Dört satırın yalnız biri tanım aşamasında düşüyor: iki yöntemli ve işaretli arayüz hiç derlenmiyor, çünkü işaret derleyiciye “bu arayüzün tam olarak bir soyut yöntemi olsun” demektir ve derleyici bunu kontrol eder. Geri kalan üç satırın tanımı derleniyor — işaretsiz iki yöntemli arayüz de sıradan bir arayüz olarak geçerlidir, Java’da bir arayüzün birden çok soyut yöntemi olması yasak değildir.
Asıl ayrım ikinci sütunda. Tek yöntemli arayüzlerin ikisi de — işaretli olsun olmasın –
lambda hedefi olabiliyor. İki yöntemli, işaretsiz arayüz ise tanımı geçse bile lambda hedefi
olamıyor: x -> x ifadesi hangi yönteme karşılık geldiğini belirtmiyor, derleyici bunu
çözemiyor. İşaretin yaptığı ile yapmadığı burada ayrılıyor.
İşaret Garantiyi Eklemez, Erken Yakalar
Tablo tek bir sonucu doğruluyor: bir arayüzün lambda hedefi olabilmesi yapısal bir niteliktir — tam olarak bir soyut yöntem taşımak — ve bu nitelik işaretten bağımsız var olur ya da olmaz. İki yöntemli, işaretsiz arayüz de tanım olarak derleniyor ama zaten fonksiyonel değildi; yalnızca bunu kimse söylemedi, ta ki biri onu lambda ile kullanmaya çalışana kadar. O andaki hata, arayüzün tanımlandığı satırdan uzakta, kullanıldığı her noktada ayrı ayrı çıkar.
İşaret bu uzaklığı kapatıyor. İki yöntemli ve işaretli arayüz derleme anında düşüyor –
kullanımı beklemeden, tanımın kendisinde. İşaretin verdiği söz “bu arayüz fonkiyoneldir”
değil, “bu arayüz fonksiyonel olmaktan çıkarsa bana burada söyle” sözüdür. Garantiyi
arayüz zaten kendi yapısıyla veriyordu; işaret yalnız o garantinin bozulduğu anı, kullanım
yerine değil tanım yerine taşıyor. Standart kütüphanedeki Runnable, Comparator,
Predicate gibi tipler bu işareti taşır, ama işareti silseniz bile hâlâ birer soyut
yöntemli arayüzdürler ve hâlâ lambda hedefi olurlar — işaret onları fonksiyonel yapmaz,
yalnız bozulmalarını erkenden yakalar.
Nesneye Dayalı Java kursunda derleyicinin reddettiği dört şey sayılmıştı: erişim belirteci,
final, denetlenen istisna, tip parametresi. Tek soyut yöntem kuralı bu listeye eklenen
beşinci bir koruma değil, aynı korumanın standart kütüphaneye taşınmış hâlidir — burada da
derleyici bir yapıyı kabul etmiyor ve bunu tanım satırında bildiriyor. Ama bu korumanın
kapsamı dardır: yalnız arayüzün şekline bakar, kaç yöntem olduğuna. Şeklin arkasındaki
sözün ne olduğuna hiç bakmaz — ve bu ayrım aşağıdaki ölçümde ortaya çıkıyor.
Şeklin kendisi de sanıldığından daha az şey taşır. Nesneye Dayalı Java kursunda bir sınıfın
bir arayüzü uygulaması adla kurulur: implements satırında arayüzün adı geçer. Lambda
ifadesi bu adı hiç yazmaz; derleyici hedefin adına değil, yalnız biçimine bakar.
// Yapisal.java — ayni lambda, birbirinden habersiz iki fonksiyonel arayuzun hedefi olabiliyor mu
public class Yapisal {
static final String IKI_ARAYUZ = """
interface Katlayici { int uygula(int x); }
interface Donusturucu { int uygula(int x); }
class Kullan {
Katlayici k = x -> x * 3;
Donusturucu d = x -> x * 3;
}
""";
public static void main(String[] args) throws Exception {
System.out.println("iki ayrı, birbirini bilmeyen arayuz, ayni govdeli lambda: "
+ (Derleme.derlenirMi(IKI_ARAYUZ, "Katlayici") ? "derlendi" : "derlenmedi"));
}
}
iki ayrı, birbirini bilmeyen arayuz, ayni govdeli lambda: derlendi
Katlayici ile Donusturucu birbirini hiç bilmiyor, aralarında ne kalıtım var ne ortak bir
üst tip — ikisi de yalnızca aynı biçimde bir soyut yöntem taşıyor. Aynı lambda ifadesi ikisi
için de derleniyor. Bu, arayüzü uygulayan bir sınıfın davranışından ayrılır: orada ad
bağlayıcıdır, burada biçim yeterlidir. Derleyicinin denetlediği söz, tam olarak bu kadar
dardır — ne isim ne köken, yalnız parametre sayısı, parametre tipleri ve dönüş tipi.
Varsayılan, Statik ve Object Yöntemleri Sayılmaz
Tek soyut yöntem kuralı “tek yöntem” değil, “tek soyut yöntem” der. Bir arayüz varsayılan
gövdeli (default) ya da statik yöntemler taşıyabilir, bunlar soyut sayılmaz ve saymaya
girmez — çünkü çağıran onları uygulamak zorunda değildir, kütüphane onları zaten yazmıştır.
- FN4 — İki kap denenir: biri bir soyut yöntemin yanına bir
defaultve birstaticyöntem ekler; öteki bir soyut yöntemin yanınaObjectsınıfının kendi yöntemlerinden ikisini (equals,toString) soyut olarak yeniden bildirir. İkisi de@FunctionalInterfaceişaretlidir. - FN5 — Her iki kap için de hem tanımın derlenip derlenmediği hem lambda hedefi olabilip olmadığı ayrı ayrı ölçülür.
// Sayilmaz.java — varsayilan, statik ve Object yontemleri soyut sayaca girmiyor mu
public class Sayilmaz {
static final String COK_UYELI = """
@FunctionalInterface
interface Islem {
int uygula(int x);
default int ikiKat(int x) { return uygula(x) * 2; }
static Islem ozdeslik() { return x -> x; }
}
""";
static final String OBJECT_IMZALI = """
@FunctionalInterface
interface Islem {
int uygula(int x);
boolean equals(Object o);
String toString();
}
""";
public static void main(String[] args) throws Exception {
System.out.println("bir soyut + iki varsayilan/statik yontem : "
+ (Derleme.derlenirMi(COK_UYELI, "Islem") ? "derlendi" : "derlenmedi"));
System.out.println("bir soyut + Object'in iki yontemi : "
+ (Derleme.derlenirMi(OBJECT_IMZALI, "Islem") ? "derlendi" : "derlenmedi"));
boolean lambdaCokUyeli = Derleme.derlenirMi(
COK_UYELI + "\nclass K { Islem i = x -> x; }", "Islem");
boolean lambdaObjectImzali = Derleme.derlenirMi(
OBJECT_IMZALI + "\nclass K { Islem i = x -> x; }", "Islem");
System.out.println("ilkinde lambda hedefi olabiliyor mu : "
+ (lambdaCokUyeli ? "derlendi" : "derlenmedi"));
System.out.println("ikincisinde lambda hedefi olabiliyor mu : "
+ (lambdaObjectImzali ? "derlendi" : "derlenmedi"));
}
}
bir soyut + iki varsayilan/statik yontem : derlendi bir soyut + Object'in iki yontemi : derlendi ilkinde lambda hedefi olabiliyor mu : derlendi ikincisinde lambda hedefi olabiliyor mu : derlendi
Dört satır da derleniyor. İlk kap beklenen bir sonuç veriyor: ikiKat ve ozdeslik
soyut değildir, birer gövdeleri vardır, sayaca girmezler — arayüz hâlâ tek soyut yöntemlidir
ve işaret buna itiraz etmez. İkinci kap daha az sezgiseldir: equals ve toString soyut
olarak yeniden bildirilmiş olsalar bile derleyici onları saymaz, çünkü ikisi de Object
sınıfının herkese açık bir yöntemiyle eşleşir ve her gerçekleştirim Object’ten miras
aldığı için bu yöntemleri zaten dolaylı olarak sağlar. Kural üç soyut bildirimi değil, üç
bildirimden ikisinin zaten Object’te karşılığı olduğunu görür ve yalnız uygula’yı
sayar.
Bu ölçüm, tek soyut yöntem kuralının söylediğinden biraz daha dar bir kural olduğunu
gösteriyor: sayılan şey “arayüzdeki soyut bildirim sayısı” değil, “arayüzün her
gerçekleştirimin yeniden yazmak zorunda kalacağı yöntem sayısı”dır. Object’in
yöntemleri zaten her sınıfta vardır, o yüzden yeniden yazma yükümlülüğüne girmezler. Önceki
konudan tanıdık bir örnek burada açıklığa kavuşuyor: Comparator<T> bir düzine civarında
default ve static yöntem taşır (reversed, thenComparing, naturalOrder) ve yine de
fonksiyoneldir, çünkü çağıranın yeniden yazması gereken tek yer compare kalır. Üye
sayısıyla soyut yöntem sayısı aynı şey değildir.
Arayüzün Davranış Sözü Hiçbir Yerde Denetlenmez
Bir fonksiyonel arayüzün iki ayrı sözü vardır. Birincisi imzadır — kaç parametre, hangi
tipte, ne döndürüyor — ve bu söz yukarıdaki tablolarda ölçüldüğü gibi derleme zamanında
tutuluyor. İkincisi davranıştır: Predicate “aynı girdiye aynı sonucu ver”, Function
“yan etki yaratma” bekler. Bu ikinci söz hangi katmanda duruyor?
- FN6 — Standart ailenin dört üyesi örnek alınır:
Predicate,Function,Supplier,Consumer. Ailenin geri kalanı aynı örüntüyü izler. - FN7 — Saflık ihlali bir çağrı sayacıyla, yan etki ihlali bir biriktiriciyle gözle görülür kılınır; ikisi de yalnızca bu ölçüm için yazılmış yardımcı durumdur.
// Anlam.java — arayuzun tip sozu ile davranis sozu ayni yerde denetlenmiyor
import java.util.function.*;
public class Anlam {
static int cagriSayaci = 0;
public static void main(String[] args) {
Predicate<Integer> p = x -> x > 0;
Function<Integer, Integer> f = x -> x * 2;
Supplier<Integer> s = () -> 42;
Consumer<Integer> c = x -> { };
System.out.println("dort tip sozu de eslesti, dordu de derlendi");
System.out.println();
Predicate<Integer> kararsiz = x -> { cagriSayaci++; return cagriSayaci == 1; };
System.out.printf("ayni girdiyle ilk cagri : %s%n", kararsiz.test(5));
System.out.printf("ayni girdiyle ikinci cagri: %s%n", kararsiz.test(5));
StringBuilder gunluk = new StringBuilder();
Function<Integer, Integer> yanEtkili = x -> { gunluk.append(x).append(','); return x + 1; };
yanEtkili.apply(1);
yanEtkili.apply(2);
System.out.printf("yan etki biriktirdi mi : %s%n", !gunluk.isEmpty());
}
}
dort tip sozu de eslesti, dordu de derlendi ayni girdiyle ilk cagri : true ayni girdiyle ikinci cagri: false yan etki biriktirdi mi : true
İlk satır tip sözünün nerede durduğunu tekrar doğruluyor: dört değişken de kendi arayüzünün
imzasına uydu, derleyici bunu denetledi ve dördü de derlendi. Geri kalan üç satır ikinci
sözü sınıyor. kararsiz adlı Predicate, aynı girdiye (5) iki ayrı çağrıda iki ayrı
sonuç veriyor — önce true, sonra false — ve derleyici buna hiç karışmadı, çalışma
zamanı da bir istisna düşürmedi. yanEtkili adlı Function çağrıldıkça kendi kapsamının
dışındaki bir StringBuilder’a yazıyor; bu da ne derlenirken ne çalışırken engellendi.
Sonuç sessizdir: Predicate.test “aynı girdiye aynı sonucu ver” demez diye bir istisna
fırlatmaz, yalnızca yanlış davranan bir uygulamayı olduğu gibi çalıştırır. Ailenin tip sözü
yukarıdaki ilk satırda doğrulandı: dört üyenin dördü de kendi imzasına uydu, dördü de
derlendi. Davranış sözü ise ailede eşit dağılmaz. Predicate ve Function için saflık ve
yan etkisizlik gerçek birer belge sözüdür ve az önce ikisi de kırıldı, ikisi de sessiz
kaldı — derleyici karışmadı, çalışma zamanı bir istisna düşürmedi. Supplier için belge
daha da gevşektir: her çağrıda aynı ya da farklı bir değer üretmesi konusunda hiçbir
zorunluluk yoktur, yani orada kırılacak bir söz bile bulunmaz. Consumer ise tam tersi
yönde kuruludur — amacı zaten yan etkidir, belge onu bir kusur saymaz, bir söz gibi
davranmaz. Tabloya sığan tek genel kural şudur: derleyici tip sözünü ailenin dördünde de
eksiksiz tutar; davranış sözü ailede tektip değildir ve tutulduğu yerlerde bile hiçbir
katman onu denetlemez.
Bu, önceki dersle (Karşılaştırma ve Sıralama) kurulan bir gözlemin tekrarıdır ama farklı bir yerde: orada geçişli olmayan bir karşılaştırıcının küçük girdide sessizce yanlış sıra ürettiği ölçülmüştü. Burada aynı sessizlik, sıralamadan çok daha küçük ve çok daha sık kullanılan bir yüzeyde — tek satırlık bir lambda ifadesinde — ortaya çıkıyor. Fonksiyonel arayüz ne kadar dar olursa olsun, darlığın kendisi bir davranış garantisine dönüşmüyor.
Özet
- Bir arayüzün lambda hedefi olabilmesi yapısal bir koşuldur: tam olarak bir soyut yöntem. Bu koşul derleme zamanında tutulur, işaretten bağımsızdır.
@FunctionalInterfaceişareti garanti eklemez; yalnız bu yapısal koşulun ihlalini, kullanım yerine değil tanım yerine taşıyarak erken yakalar.- İki yöntemli, işaretsiz bir arayüz de derlenir ama lambda hedefi olamaz — fonksiyonel olmama hâli işaret olmadan da geçerlidir, yalnız daha geç fark edilir.
- Fonksiyonel arayüzlerin ikinci bir sözü vardır: davranış (saflık, yan etkisizlik). Bu söz ne derleyicide ne çalışma zamanında denetlenir; ihlali sessizdir, istisna düşürmez.
- Standart ailenin tip sözü dört üyede de derleyicide durur; davranış sözü ailede tektip
değildir —
PredicateveFunction’da kırılabilir ve kırıldığında sessiz kalır,Supplier’da böyle bir söz belgede bile yoktur.
Sonraki Adım
Bu ders fonksiyonel arayüzün kendisine baktı: tek soyut yöntemin nerede tutulduğunu, ikinci sözün nerede tutulmadığını. Bir lambda ifadesi yazıldığında bu tip yerinde bir şey daha yapar: çevresindeki adları yakalar. Sıradaki ders o tipin yerinde yazıldığında neyi yakaladığını ölçer — yerel bir değişkenle bir alanın yakalanması aynı kısıta mı bağlıdır, ve ikisi aynı sonucu mu üretir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.