Ders 07 / 15
Lambda İfadeleri
Bir lambda ifadesi çevresindeki bir yerel değişkeni yakaladığında o değişkenin etkin olarak final olması derleme zamanında zorunludur; bir alanı yakaladığında böyle bir kısıt yoktur ve nesne sonradan değişince aynı lambda başka bir sonuç verir. Yakalanan yerel değişkenin donan şey değeridir, gösterdiği nesne değil.
İçindekiler
Önceki ders fonksiyonel arayüzün kendisine baktı: tek soyut yöntemin derleme zamanında tutulduğunu, davranış sözünün hiçbir yerde tutulmadığını ölçtü. Bir lambda ifadesi yazıldığı anda bu tipin yerinde bir şey daha olur — ifade, kendi gövdesinin dışındaki adlara başvurur ve o adları yakalar. Yerel bir değişkenle bir alanın yakalanması aynı kısıta mı bağlıdır, ve ikisi aynı sonucu mu üretir?
Bu ders bu soruyu doğrudan koşumla yanıtlar: aynı gövdeli, aynı işi yapan iki lambda — biri bir yerel değişkeni, öteki bir alanı okuyarak — iki ayrı çağrı biçiminde kaç ayrı sonuç üretiyor? Kapanış (closure) kavramının kendisi — bir işlevin kendi tanım ortamını beraberinde taşıması — Programlama Temelleri ve JavaScript’te Nesneler ve Fonksiyonlar kurslarında kuruldu ve burada tekrar açılmaz. Ölçülen şey kapanışın ne olduğu değil, Java’nın onu hangi kısıtla sınırladığıdır.
Lambda Bir Değişkeni Değeriyle Yakalar
- FN8 — Aynı çarpma işlemi iki ayrı
IntUnaryOperatorile yazılır: biri bir yerel değişkeni, öteki statik bir alanı okur. İkisi de kurulduğunda çarpan 2’dir. - FN9 — Lambda kurulduktan sonra alan 5’e, sonra 7’ye değiştirilir; yerel değişkene ise hiç dokunulmaz — zaten dokunulamaz, aşağıdaki bölüm nedenini ölçer.
- FN10 — Her iki lambda da aynı
applyAsInt(10)çağrısıyla iki kez okunur; dönen değerler birebir karşılaştırılır.
// Yakalama.java — degisken mi alan mi: lambda hangisini donduruyor
import java.util.function.IntUnaryOperator;
public class Yakalama {
static int carpan = 2;
public static void main(String[] args) {
int carpanYerel = 2;
IntUnaryOperator yerelYakalayan = x -> x * carpanYerel;
System.out.println("yerel yakalama, ilk cagri : " + yerelYakalayan.applyAsInt(10));
carpan = 5;
System.out.println("yerel yakalama, alan degisti: " + yerelYakalayan.applyAsInt(10));
IntUnaryOperator alanYakalayan = x -> x * carpan;
System.out.println("alan yakalama, carpan=5 : " + alanYakalayan.applyAsInt(10));
carpan = 7;
System.out.println("alan yakalama, carpan=7 : " + alanYakalayan.applyAsInt(10));
}
}
yerel yakalama, ilk cagri : 20 yerel yakalama, alan degisti: 20 alan yakalama, carpan=5 : 50 alan yakalama, carpan=7 : 70
yerelYakalayan iki çağrıda da 20 veriyor — aradaki satırda statik alan 5’e
değişmiş olsa bile. Lambda kurulduğu anda carpanYerel’in o anki değerini almış ve o
değeri kendi içine gömmüş; dışarıda ne olursa olsun bu kopyayı okuyor. alanYakalayan ise
iki çağrıda iki ayrı sonuç veriyor: 50, sonra 70. Bu lambda bir değeri gömmedi,
carpan alanına giden bir yolu gömdü ve her çağrıda o yoldan güncel değeri okuyor.
Aynı gövde biçimi — x -> x * bir_şey — iki ayrı yakalama biçiminde iki ayrı sayıda
sonuç üretiyor: yerel değişkende bir sonuç, alanda iki ayrı sonuç. Sorunun yanıtı
burada net: yerel değişkenle alanın yakalanması aynı kısıta bağlı değildir ve ikisi aynı
sonucu üretmez.
Etkin Olarak Final Kısıtı Derleme Zamanındadır
Yerel değişkenin neden sabit kaldığı bir önceki ölçümde görüldü; peki bu sabitlik
derleyicinin bir kararı mı, yoksa yalnızca bu örnekte mi böyle çıktı? Java’nın kuralı açıktır:
bir lambda ifadesi çevreleyen kapsamdaki bir yerel değişkeni yakalıyorsa, o değişken etkin
olarak final olmak zorundadır — yani ilk atamasından sonra bir daha hiç yeniden
atanmamalıdır, final sözcüğü yazılmasa bile.
- FN11 — Aynı yöntem gövdesi iki biçimde denenir: yerel değişken yalnız bir kez atanıp lambda tarafından okunur; ikinci biçimde lambda kurulduktan sonra aynı değişkene bir kez daha atama yapılır.
- FN12 — Derleyicinin ürettiği hata iletisinin metni basılmaz, yalnız derlenip derlenmediği bildirilir.
// 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;
}
}
// EtkinFinal.java — yerel degiskeni yeniden atamak lambda'yi neden bozuyor
public class EtkinFinal {
static final String TEMEL = """
import java.util.function.IntSupplier;
class Deneme {
static IntSupplier yap() {
int sayac = 0;
IntSupplier s = () -> sayac;
%s
return s;
}
}
""";
public static void main(String[] args) throws Exception {
System.out.println("degismeyen yerel : "
+ (Derleme.derlenirMi(TEMEL.formatted(""), "Deneme") ? "derlendi" : "derlenmedi"));
System.out.println("lambda sonrasi yeniden atama: "
+ (Derleme.derlenirMi(TEMEL.formatted("sayac = 1;"), "Deneme") ? "derlendi" : "derlenmedi"));
}
}
degismeyen yerel : derlendi lambda sonrasi yeniden atama: derlenmedi
sayac değişkenine lambda kurulduktan sonra bir kez daha atama yapılan ikinci biçim
derlenmiyor — ve bu, çalışma zamanında değil derleme zamanında düşen bir hatadır. Kaynak
kodda sayac = 1; satırı lambda’nın çağrılmasından önce mi sonra mı çalışacağı önemli
değildir; derleyici denetim akışını yürütmez, yalnız değişkenin kapsamı içinde birden
fazla atama olup olmadığına bakar. Bir atama varsa değişken etkin olarak final’dir ve
yakalanabilir; ikinci bir atama görülürse — denetim akışında hiç ulaşılmayacak olsa bile –
yakalama derlenmez.
Bu, önceki dersle aynı örüntüdür: derleyici burada da bir yapıya bakıyor, bir davranışa
değil. Yerel değişkenin gerçekten değişip değişmediğini değil, kaynakta kaç kez atandığını
sayıyor. Alan için böyle bir sayım yoktur — carpan alanı istenildiği kadar yeniden
atanabilir ve derleyici hiç itiraz etmez, çünkü alan yakalaması yerel değişken yakalaması
gibi bir kopya çıkarmaz. Kısıtın gerekçesi de buradan çıkıyor: bir yerel değişken yöntem
döndükten sonra yığıttan silinir, ama lambda ondan sonra da yaşayabilir; derleyici bu
çelişkiyi değişkenin değerini kopyalayıp lambda’nın kendi içine gömerek çözer, ve kopyayı
tutarlı tutabilmek için o değişkenin tek atamalık kalmasını ister. Alan ise yığıtta değil
nesnenin ya da sınıfın kendisinde yaşar; lambda hangi nesneye ait olduğunu tutar, kopya
çıkarmaya gerek kalmaz.
Klasik ve Gelişmiş Döngüde Aynı Ad İki Ayrı Değişkendir
Etkin olarak final kısıtının en görünür sonucu döngülerde çıkar. Gelişmiş for döngüsü
(for (String s : liste)) ile klasik for döngüsü (for (int i = 0; ...; i++)) aynı işi
yapıyor gibi görünür, ama bir lambda döngü değişkenini yakalamak istediğinde ikisi ayrışır.
- FN13 — Üç ögelik bir listeden gelişmiş
forile geçilir; her yinelemede kurulan lambda, o yinelemenin değişkenini bir listeye ekler. Döngü bitince listedeki lambdaların hepsi çağrılır. - FN14 — Aynı deneme klasik
forile, artan bir sayaçla tekrarlanır; burada beklenen şey çalışma zamanı sonucu değil, derlenip derlenmediğidir.
// Dongu.java — klasik ve gelismis for donguleri lambda yakalamasinda ayni mi
import java.util.*;
import java.util.function.Supplier;
public class Dongu {
public static void main(String[] args) {
List<String> kaynak = List.of("armut", "kiraz", "elma");
List<Supplier<String>> yakalayanlar = new ArrayList<>();
for (String s : kaynak) {
yakalayanlar.add(() -> s);
}
System.out.print("gelismis for, uc lambdanin sonucu: ");
for (Supplier<String> y : yakalayanlar) System.out.print(y.get() + " ");
System.out.println();
}
}
gelismis for, uc lambdanin sonucu: armut kiraz elma
Üç lambda üç ayrı değer veriyor: armut, kiraz, elma. Bu, “yakalama değeri dondurur”
kuralıyla ilk bakışta çelişir gibi görünür — döngü değişkeni s üç kez yeniden atanmadı
mı? Hayır: gelişmiş for her yinelemede s adında yeni bir yerel değişken kurar; o
tek atamalık değişken bir sonraki yinelemede başka bir değişkenle değiştirilir, aynı
değişken yeniden atanmaz. Üç lambda üç ayrı s’i yakaladı, bu yüzden üç ayrı değer
donduruldu.
// DonguKlasik.java — klasik for'da sayac degiskeni yakalanabiliyor mu
public class DonguKlasik {
static final String KAYNAK = """
import java.util.*;
import java.util.function.IntSupplier;
class Deneme {
static void yap() {
List<IntSupplier> yakalayanlar = new ArrayList<>();
for (int i = 0; i < 3; i++) {
yakalayanlar.add(() -> i);
}
}
}
""";
public static void main(String[] args) throws Exception {
System.out.println("klasik for, i yakalanabiliyor mu: "
+ (Derleme.derlenirMi(KAYNAK, "Deneme") ? "derlendi" : "derlenmedi"));
}
}
klasik for, i yakalanabiliyor mu: derlenmedi
Klasik for döngüsünde durum tam tersidir. i tek bir değişkendir ve i++ her
yinelemede onu yeniden atar; üç yineleme, aynı i üzerinde üç ayrı atama demektir. Bu,
etkin olarak final tanımını doğrudan bozar ve lambda i’yi yakalamaya çalıştığı satırda
derleme durur — daha önce ölçülen sayac = 1; örneğiyle birebir aynı kural, burada döngü
gövdesinin içinde. İki döngü sözdizimsel olarak benzer görünse de, gelişmiş for her turda
yeni bir ad kurarken klasik for tek bir adı tekrar tekrar atıyor — ve lambda yakalaması
tam olarak bu farkı ortaya çıkarıyor.
Dört Sözdizimi, Aynı Yakalama Kuralı
Bir lambda ifadesinin gövdesi tek bir ifade olabilir, süslü parantezli bir blok olabilir,
parametre tipi açıkça yazılabilir ya da hiç yazılmayabilir — ve aynı işi yapan bir yöntem
zaten varsa, lambda hiç yazılmadan bir yöntem referansı (Sınıf::yöntem) ile de
belirtilebilir. Bu dört biçim ayrı ayrı sözdizimidir; hedef arayüz aynı olduğu sürece
dördü de aynı derleme yolundan geçer.
- FN15 — Aynı
Function<Integer, Integer>dört ayrı biçimde yazılır ve aynı girdiyle (6) çağrılır.
// Sozdizim.java — dort ayri yazim, ayni sonuc
import java.util.function.Function;
public class Sozdizim {
static int kare(int x) { return x * x; }
public static void main(String[] args) {
Function<Integer, Integer> ifade = x -> x * x;
Function<Integer, Integer> govdeli = x -> { return x * x; };
Function<Integer, Integer> tipYazili = (Integer x) -> x * x;
Function<Integer, Integer> yontemReferansi = Sozdizim::kare;
System.out.println("ifade biciminde : " + ifade.apply(6));
System.out.println("govde biciminde : " + govdeli.apply(6));
System.out.println("tip yazilarak : " + tipYazili.apply(6));
System.out.println("yontem referansiyla : " + yontemReferansi.apply(6));
}
}
ifade biciminde : 36 govde biciminde : 36 tip yazilarak : 36 yontem referansiyla : 36
Dört satır da aynı sonucu veriyor, çünkü dördü de aynı hedefe — Function<Integer, Integer>’ın tek soyut yöntemine — bağlanıyor. govdeli biçimi return sözcüğünü zorunlu
kılar ve birden çok deyime izin verir; ifade biçimi tek bir değeri örtük olarak döndürür.
tipYazili parametre tipini açıkça yazar, ifade ve govdeli bu tipi hedef arayüzden
çıkarır. yontemReferansi ise hiç lambda yazmaz — zaten var olan kare yöntemine bir
isim üzerinden işaret eder.
Yöntem referansı yakalama kuralından muaf değildir, yalnız yakalayacağı bir şey olmayabilir.
Statik bir yönteme işaret eden Sozdizim::kare hiçbir şey yakalamaz, çünkü statik bir
yöntemin bağlı olduğu bir örnek yoktur. Ama bir örnek yöntemine, o örnek üzerinden
işaret eden bir referans — liste::size gibi — tıpkı () -> liste.size() lambda’sı gibi
liste değişkenini yakalar ve aynı kısıtlara, aynı sonuca tabidir; iki yazım aynı derleme
adımından geçer, yalnızca biri kısaltmadır. Sözdizim değişse de yakalama kuralı değişmiyor
– kuralın koşulu lambda anahtar sözcüğü değil, çevreleyen kapsamdan bir adın okunmasıdır.
Donan Değer, Donmayan Nesne
Buraya kadarki ölçüm bir yanılgıya açıktır: “yerel değişken sabittir” cümlesi, yakalanan nesnenin de sabit kaldığı izlenimini verebilir. Sınırlayıcı ölçüm bunu ayırıyor.
- FN16 — Bir liste değişkeni bir kez atanıyor ve hiç yeniden atanmıyor — etkin olarak final kuralını sağlıyor. Lambda listenin boyutunu okuyor, listenin kendisini değil.
// Referans.java — yerel degisken degeri donduruyor, nesneyi dondurmuyor
import java.util.*;
import java.util.function.Supplier;
public class Referans {
public static void main(String[] args) {
List<String> liste = new ArrayList<>(List.of("alfa"));
Supplier<Integer> boyutOku = () -> liste.size();
System.out.println("lambda kurulunca boyut : " + boyutOku.get());
liste.add("beta");
liste.add("gama");
System.out.println("liste degisince boyut : " + boyutOku.get());
}
}
lambda kurulunca boyut : 1 liste degisince boyut : 3
liste değişkeni bir kez atandı ve bir daha hiç yeniden atanmadı; etkin olarak final
kısıtı baştan sona sağlandı. Buna rağmen boyutOku.get() iki çağrıda iki ayrı sayı
veriyor: 1, sonra 3. Çelişki görünüşte; kısıtın dondurduğu şey liste
değişkeninin kendisi — yani hangi nesneyi gösterdiği — oldu, o nesnenin içeriği
değil. liste hâlâ aynı ArrayList nesnesini gösteriyor, ama o nesnenin içine iki öge daha
eklendi ve lambda her çağrıda güncel içeriği okuyor.
Bu, bir önceki bölümün “yerel yakalama sabit kalır” gözlemini sınırlıyor: sabit kalan
referanstır, referansın gösterdiği değişebilir nesne değil. Etkin olarak final kuralı bir
değişmezlik (immutability) garantisi vermez; yalnızca bir adın akış boyunca aynı nesneyi
göstermesini zorunlu kılar. İlkel Tipler ve Sarmalayıcılar dersinde ölçülen değiştirilemez
dizgi ile burada yakalanan değişebilir liste bu yüzden ayrı davranıyor — ilkinde nesnenin
kendisi hiç değişemez, ikincisinde nesne yerinde değişebiliyor ve yakalama bunu engellemiyor.
Özet
- Bir lambda ifadesi yerel bir değişkeni yakaladığında o değişkenin değerini kopyalayıp içine gömer; alan yakaladığında bir kopya çıkarmaz, alana giden yolu tutar.
- Aynı gövdeli iki lambda, iki yakalama biçiminde iki ayrı sonuç sayısı üretir: yerel yakalamada değişmeyen tek bir sonuç, alan yakalamada alan değiştikçe değişen ayrı sonuçlar.
- Yakalanan yerel değişkenin etkin olarak
finalolması derleme zamanında zorunludur; lambda kurulduktan sonra bile olsa aynı değişkene ikinci bir atama yapılması derlemeyi durdurur. - Alan yakalamada böyle bir kısıt yoktur; alan yakalama tarihinden sonra da yeniden atanabilir ve derleyici buna karışmaz.
- Sınırlayıcı ölçüm: etkin olarak final kısıtı değişkenin gösterdiği nesneyi değil, yalnız değişkenin kendisini dondurur. Yakalanan referansın gösterdiği liste sonradan değişirse lambda değişmiş içeriği görür.
- Lambda ifadesinin dört sözdizimi (ifade, gövde, açık tip, yöntem referansı) aynı hedefe bağlanır ve aynı sonucu verir; yöntem referansı yakalama kuralından muaf değildir, yalnız yakalayacağı bir şey olmayabilir.
Sonraki Adım
Bu ders tek bir lambda ifadesinin çevresinden ne aldığını ve dört sözdiziminin aynı hedefe nasıl bağlandığını ölçtü. Standart kütüphanenin asıl kullanım biçimi böyle tek bir lambda değil, birbirine art arda bağlanmış birkaç lambdadır — bir akış zinciri. Her halka kendi başına doğru çalışsa da, zincire dizildiğinde ayrı bir soru doğar: zincirin hangi parçası kaynağın sırasına bağlıdır, hangisi her koşulda aynı sırayı verir, hangisi bir sonlandırıcı olmadan hiç çalışmaz ve hangi kural bütünüyle çağırana kalır. Sıradaki ders bu soruyu bir akış zincirinin on ayrı işlemi üzerinden koşturarak yanıtlar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.