Press "Enter" to skip to content

Effective Java Madde 42: Lambda Fonksiyonlarını İsimsiz Sınıflara Tercih Edin

Eskiden beri tek bir soyut metodu olan arayüzler (veya çok nadir de olsa soyut sınıflar) fonksiyon türleri olarak kullanılmıştır. Bunların nesneleri de fonksiyon nesneleri olarak bilinirler ve bir işlevi veya eylemi temsil ederler. Java’nın ilk sürümünden itibaren fonksiyon nesneleri yaratmanın birincil yolu isimsiz sınıflar (anonymous class) kullanmaktı (Madde 24). Aşağıda bir sıralama fonksiyonu kullanarak bir grup String nesnesini karakter sayısına göre sıralayan bir kod parçası görüyorsunuz. Sıralama fonksiyonunu yaratmak için isimsiz sınıf kullanılmıştır:

// Fonksiyon nesnesi yaratmak için isimsiz sınıf kullanımı - ESKİ
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

Klasik nesneye yönelik tasarım desenlerinde fonksiyon nesneleri gerektiği zaman isimsiz sınıflar kullanmak kabul edilebilir bir durumdu. Strateji deseni buna bir örnek olarak verilebilir. Comparator arayüzü sıralama işi için bir stratejiyi temsil ederken, yukarıdaki isimsiz sınıf bu stratejinin somutlaşmış halidir. Ancak isimsiz sınıflar yazarken ortaya çıkan gereksiz ve uzun kodlar Java’yı fonksiyonel programlama yapmak için elverişsiz bir hale getiriyordu.

Java 8’le birlikte tek bir soyut metodu olan arayüzlere özel bir anlam yüklenmiştir ve bunlara artık fonksiyonel arayüz denmektedir. Bunlardan nesne yaratmak için de lambda ifadeleri dile eklenmiştir. Lambdalar işlevsel olarak isimsiz sınıflara benzerdir ancak çok daha kısa bir biçimde ifade edilirler. Yukarıda örnekte isimsiz sınıfı lambda ile değiştirdiğimizde aşağıdaki gibi bir kod çıkacaktır:

// Fonksiyon nesnesi olarak isimsiz sınıf yerine lambda kullanımı
Collections.sort(words,
    (s1, s2) -> Integer.compare(s1.length(), s2.length()));

Dikkat ederseniz lambda ifadesi yazılırken lambda fonksiyonunun türü (Comparator<String>), parametrelerin türleri (hem s1 hem de s2 String türündedir) ve dönüş türü (int) kodda mevcut değildir. Derleyici bu türlerin ne olduğunu tür çıkarsama (type inference) denilen bir teknikle belirler. Tür çıkarsama kuralları çok karmaşık olduğundan bazı durumlarda bu çıkarım yapılamayabilir. Bu durumlarda türü bizim belirtmemiz gerekir. Lambda parametrelerinin türlerini programın okunabilirliğini kötü etkilemediği sürece yazmayın. Eğer derleyici size tür çıkarımı yapamadığını belirten bir hata verirse o zaman türleri açıkça belirtin. Nadir de olsa bazen dönüş değerleri veya lambda ifadelerinin bütünü için tür dönüşümü (cast) yapmanız gerekebilir.

Tür çıkarsama ile ilgili bir uyarı daha yapalım. Madde 26’da ham (raw) türleri kullanmayın, Madde 29’da üreysel (generic) türlere öncelik verin, Madde 30’da üreysel metotlara öncelik verin gibi tavsiyelerde bulunmuştuk. Bu tavsiyeler lambda fonksiyonları söz konusu olduğunda iki kat daha önem kazanmaktadır çünkü derleyici tür çıkarsama yaparken üreysellik mekanizmasını kullanmaktadır. Bunu yapmadığınız taktirde derleyici tür çıkarsama yapamayacaktır ve sizin türleri belirtmeniz gerekecektir. Örnek verecek olursak, yukarıda lambda kullandığımız örnekte String nesnelerini tutan words değişkeni List<String> yerine List türünde olursa lambda kodu derlenmeyecektir.

Bu arada, örneğimizdeki Comparator tanımını daha da kısaltmak için bu fonksiyonel arayüzün statik fabrika metotlarından birini kullanabiliriz (Madde 14, Madde 43):

Collections.sort(words, comparingInt(String::length));

Not: String::length ifadesi lambda fonksiyonlarını daha da kısaltmaya yarayan bir metot referansıdır. Madde 43’de bu anlatılmaktadır.

Hatta, Java 8’de List arayüzüne eklenen sort metodunu kullanarak bunu daha da kısaltabiliriz:

words.sort(comparingInt(String::length));

Lambda ifadelerinin dile eklenmesi önceden fonksiyon nesneleri kullanmanın mantıklı olmadığı yerlerde işimizi kolaylaştırmaktadır. Mesela, Madde 34’de tanımladığımız Operation enum türünü düşünelim. Her enum sabiti apply metodunu farklı biçimde gerçekleştirdiği için enum sabitlerinin her biri için bir sınıf gövdesi tanımlayıp apply metodunu geçersiz kılmıştık. Hafızanızı tazelemek için koda bakalım:

// Sınıf gövdesi kullanarak apply metodunu geçersiz kılan enum
public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) { 
            return x + y; 
        } 
    },
    MINUS("-") {
        public double apply(double x, double y) { 
            return x - y; 
        }
    },
    TIMES("*") {
        public double apply(double x, double y) { 
             return x * y; 
        } 
    },
    DIVIDE("/") {
        public double apply(double x, double y) { 
            return x / y; 
        }
    };
    private final String symbol;

    Operation(String symbol) { 
        this.symbol = symbol; 
    }

    @Override 
    public String toString() { 
        return symbol; 
    }
    public abstract double apply(double x, double y);
}

Madde 34’te enum sabitlerine özel sınıf gövdeleri yazmak yerine nesne alanları kullanmanın daha doğru olacağını söylemiştik. Ancak lambda fonksiyonları sayesinde enum sabitlerine özgü davranışlar belirlemek de kolaylaşmaktadır. Tek yapmamız gereken enum yapıcı metoduna her enum sabiti için farklı davranışları simgeleyen birer lambda geçmek olacaktır. Yapıcı metot lambda fonksiyonunu bir nesne alanı içinde saklayacak ve gerektiğinde bu fonksiyonu çalıştıracaktır. Şimdi bu şekilde kodu nasıl yazarız ona bakalım:

// Fonksiyon nesnesi ile enum sabitlerine özgü davranış belirleme
public enum Operation {
    PLUS  ("+", (x, y) -> x + y), 
    MINUS ("-", (x, y) -> x - y), 
    TIMES ("*", (x, y) -> x * y), 
    DIVIDE("/", (x, y) -> x / y);
    
    private final String symbol;
    private final DoubleBinaryOperator op;
    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }
    
    @Override 
    public String toString() { 
        return symbol; 
    }
    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    } 
}

Gördüğünüz gibi çok daha kısa ve anlaşılır bir kod ortaya çıktı. Burada DoubleBinaryOperator fonksiyonel arayüzünü kullandığımızı gözden kaçırmayın. Bu arayüz java.util.function (Madde 44) paketinde tanımlı çok sayıda fonksiyonel arayüzden bir tanesidir. İki tane double argüman alıp yine double türünde sonuç döndüren bir fonksiyonu temsil eder.

Bu kodu görünce enum sabitleri için gövde içinde metot tanımlama yönteminin gereksiz kaldığını düşünebilirsiniz ancak bu doğru değildir. Metotlar ve sınıfların aksine, lambda fonksiyonları isimsizdir ve dökümantasyonları olmaz. Eğer yaptığınız hesaplama ek açıklamaya ihtiyaç duyuyorsa veya birkaç satırdan fazla kod yazmayı gerektiriyorsa lambda kullanmayın. Lambda fonksiyonları için tek satır idealdir, üç satıra kadar da kabul edilebilir. Bunu ihlal ederseniz kodun okunabilirliğine zarar vermiş olursunuz. Eğer bir lambda uzunsa veya okunması zorsa ya bunu basitleştirmeye çalışın ya da tamamen kaldırın. Ayrıca, enum yapıcı metotlarına geçilen argümanlar statik bağlamda değerlendirildiği için lambda fonksiyonları enumların nesne alanlarına ve metotlarına erişemezler. Yani, önceki örnekte kullanılan enum sabitlerine gövde ekleyerek metotları geçersiz kılma yöntemi eğer yapılan hesaplamalar lambda fonksiyonunun okunabilirliğini bozacak kadar uzunsa veya nesne alanlarına/metotlarına erişmesi gerekiyorsa halen geçerlidir.

Aynı şekilde, lambda fonksiyonları varken artık isimsiz sınıflara da gerek kalmadığını düşünebilirsiniz. Bunun gerçekliği daha fazladır ama yine de isimsiz sınıflarla yapıp lambdalarda yapamadığınız birkaç şey vardır. Lambdalar ancak fonksiyonel arayüzlerle kullanılabilir. Bir soyut sınıftan nesne yaratmak istiyorsanız bunu isimsiz sınıfla yapabilirsiniz ama lambda ile yapamazsınız. Benzer şekilde, birden fazla soyut metodu olan arayüzlerden nesne yaratmak isterseniz de isimsiz sınıflar bunu destekler ama lambda desteklemez. Son olarak, bir lambda fonksiyonu kendisine referans edemez çünkü lambda içinde kullanacağınız this anahtar kelimesi onu çevreleyen nesneyi tutmaktadır. Dolayısıyla lambda içinde fonksiyon nesnesine erişmeniz gerekiyorsa isimsiz sınıf kullanmanız gerekir.

Lambdalar da aynen isimsiz sınıflarda olduğu gibi serileştirme (serialize) konusunda pek güvenli değillerdir. Bu sebeple lambda fonksiyonlarını ve isimsiz sınıf nesnelerini serileştirmeye kalkmayın. Böyle bir ihtiyaç doğarsa bir private static gömülü sınıf (nested class) tanımlayıp onun nesnelerini kullanın.

Özetle, Java 8’le dile eklenen lambdalar küçük fonksiyon nesnelerini ifade etmek için açık ara en iyi yoldur. Fonksiyonel arayüzlerle ifade edilemeyen türlerden fonksiyon nesneleri yaratmanız gerektiği durumlarda halen isimsiz sınıfları kullanabilirsiniz. Ayrıca, daha önceleri Java’da pek kullanışlı olmayan fonksiyonel programlama teknikleri lambda ifadeleri sayesinde artık kullanılabilir duruma gelmiştir.

Share

Leave a Reply

%d bloggers like this: