İçeriğe geç
academia.sh

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ı IntUnaryOperator ile 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ş for ile 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 for ile, 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 final olması 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat