← Kursa Dön
📄 Text · 15 min

Functional Interfaces Derinlemesine

Java 8 ile gelen lambda ifadeleri, Java'ya fonksiyonel programlama özelliklerini kazandırdı. Ama lambda'lar havada uçmaz — bir yere inmeleri gerekir. O iniş noktası functional interface'tir. Tek bir abstract method'u olan herhangi bir interface, bir lambda'nın evi olabilir.

Bunu şöyle düşün: Lambda bir fiş, functional interface ise priz. Fişin şekli (parametre ve dönüş tipi) prize uymalı. java.util.function paketi, Java'nın sana sunduğu standart priz koleksiyonudur — çoğu ihtiyacı karşılar. Bazen kendi prizini de yaparsın ama önce mevcut olanları tanımalısın.

Bu derste Java'nın standart functional interface'lerini, method reference türlerini, fonksiyon birleştirmeyi (composition) ve gerçek dünya kullanım senaryolarını derinlemesine inceleyeceğiz.


@FunctionalInterface Annotation

Bir interface'in functional interface olması için tek bir abstract method içermesi yeterlidir. @FunctionalInterface annotation'ı zorunlu değil ama koymalısın — derleyici seni korur.

@FunctionalInterface
interface Greeting {
    String greet(String name);
}

// Kullanım — lambda ile
Greeting hello = name -> "Merhaba, " + name + "!";
System.out.println(hello.greet("Ali"));  // Merhaba, Ali!

Annotation koyduğunda, eğer interface'e ikinci bir abstract method eklersen derleme hatası alırsın:

@FunctionalInterface
interface Greeting {
    String greet(String name);
    String farewell(String name);  // DERLEME HATASI!
    // "Multiple non-overriding abstract methods found"
}

default ve static method'lar abstract değildir — bunları istediğin kadar ekleyebilirsin:

@FunctionalInterface
interface Greeting {
    String greet(String name);  // Tek abstract method

    default String greetAll(String... names) {  // default — OK
        return String.join(", ", names) + " — herkese merhaba!";
    }

    static Greeting formal() {  // static — OK
        return name -> "Sayın " + name;
    }
}

java.util.function Paketi

Java, en yaygın fonksiyon imzaları için hazır functional interface'ler sunar. Kendi yazmana gerek kalmaz — ve herkes aynı tipleri kullanınca kod okunabilirliği artar.

Predicate<T> — Test Et

Bir değer alır, boolean döner. "Bu koşul sağlanıyor mu?" sorusuna cevap verir.

import java.util.function.Predicate;

Predicate<String> isLong = s -> s.length() > 10;
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isNotEmpty = s -> s != null && !s.isEmpty();

System.out.println(isLong.test("Merhaba"));         // false
System.out.println(isEven.test(42));                 // true
System.out.println(isNotEmpty.test(""));             // false

// Stream ile kullanım
List<String> names = List.of("Ali", "Ayşe", "Mehmetcan", "Elif");
names.stream()
    .filter(isLong.negate())   // 10 karakterden kısa olanlar
    .forEach(System.out::println);

Function&lt;T, R&gt; — Dönüştür

Bir değer alır (T), başka bir değer döner (R). Dönüşüm (transformation) işlemlerinde kullanılır.

import java.util.function.Function;

Function<String, Integer> length = String::length;
Function<String, String> toUpper = String::toUpperCase;
Function<Integer, String> intToHex = Integer::toHexString;

System.out.println(length.apply("Java"));      // 4
System.out.println(toUpper.apply("hello"));    // HELLO
System.out.println(intToHex.apply(255));       // ff

Consumer&lt;T&gt; — Tüket

Bir değer alır, hiçbir şey döndürmez (void). Yan etki (side effect) üreten işlemler için: loglama, yazdırma, veritabanına yazma.

import java.util.function.Consumer;

Consumer<String> printer = System.out::println;
Consumer<String> logger = msg -> System.err.println("[LOG] " + msg);

printer.accept("Merhaba");  // Konsola: Merhaba
logger.accept("İşlem başladı");  // Stderr'e: [LOG] İşlem başladı

// Stream ile
List.of("Ali", "Veli", "Deli").forEach(printer);

Supplier&lt;T&gt; — Üret

Parametre almaz, bir değer döner. Factory, lazy initialization, default value senaryolarında kullanılır.

import java.util.function.Supplier;

Supplier<Double> random = Math::random;
Supplier<List<String>> emptyList = ArrayList::new;
Supplier<LocalDateTime> now = LocalDateTime::now;

System.out.println(random.get());      // 0.7234...
System.out.println(emptyList.get());   // []
System.out.println(now.get());         // 2024-03-15T14:30:00

// Optional ile — değer yoksa Supplier'dan üret
Optional<String> name = Optional.empty();
String result = name.orElseGet(() -> "Varsayılan");

BiFunction&lt;T, U, R&gt; — İki Parametre, Bir Sonuç

İki parametre alır, bir değer döner.

import java.util.function.BiFunction;

BiFunction<String, Integer, String> repeat =
    (text, count) -> text.repeat(count);

BiFunction<Double, Double, Double> power = Math::pow;

System.out.println(repeat.apply("ha", 3));    // hahaha
System.out.println(power.apply(2.0, 10.0));   // 1024.0

UnaryOperator&lt;T&gt; ve BinaryOperator&lt;T&gt;

Girdi ve çıktı tipi aynı olan özel durumlar.

import java.util.function.UnaryOperator;
import java.util.function.BinaryOperator;

// UnaryOperator<T> = Function<T, T>
UnaryOperator<String> trim = String::trim;
UnaryOperator<String> shout = s -> s.toUpperCase() + "!";

System.out.println(trim.apply("  hello  "));  // "hello"
System.out.println(shout.apply("java"));       // "JAVA!"

// BinaryOperator<T> = BiFunction<T, T, T>
BinaryOperator<Integer> max = Integer::max;
BinaryOperator<String> concat = String::concat;

System.out.println(max.apply(3, 7));           // 7
System.out.println(concat.apply("Hel", "lo")); // Hello

// Stream reduce ile
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);  // BinaryOperator
System.out.println(sum);  // 15

Hızlı Referans Tablosu

InterfaceParametreDönüşMethodKullanım
Predicate<T>Tbooleantest()Filtreleme
Function<T,R>TRapply()Dönüştürme
Consumer<T>Tvoidaccept()Yan etki
Supplier<T>Tget()Üretme
BiFunction<T,U,R>T, URapply()İki girdili dönüşüm
UnaryOperator<T>TTapply()Aynı tipte dönüşüm
BinaryOperator<T>T, TTapply()İki aynı tipte birleştirme

Primitive Specialization

Generics primitive tiplerle çalışmaz — Predicate<int> yazamazsın. Java, boxing/unboxing overhead'ini önlemek için primitive özelleşmeleri sunar:

import java.util.function.*;

// int versiyonları
IntPredicate isPositive = n -> n > 0;
IntFunction<String> intToString = Integer::toString;
IntConsumer printInt = System.out::println;
IntSupplier randomInt = () -> (int)(Math.random() * 100);
IntUnaryOperator doubleIt = n -> n * 2;
IntBinaryOperator add = Integer::sum;

// Tip dönüşümleri
ToIntFunction<String> stringLength = String::length;     // T → int
IntToDoubleFunction intToDouble = n -> n * 1.5;          // int → double

isPositive.test(5);           // true — boxing yok!
stringLength.applyAsInt("hi"); // 2 — boxing yok!

Aynı pattern long ve double için de var: LongPredicate, DoubleFunction, ToDoubleFunction vs.

⚠️ Dikkat: Yüksek performans gerektiren kodda (milyon iterasyon, tight loop) primitive versiyonları kullan. Normal iş mantığında Predicate<Integer> gayet yeterli — okunabilirliği bozma.


Method Reference Türleri

Lambda ifadeleri yerine method reference kullanabilirsin — daha kısa ve okunabilir.

1. Static Method Reference

// Lambda
Function<String, Integer> parse = s -> Integer.parseInt(s);
// Method reference
Function<String, Integer> parse = Integer::parseInt;

// Lambda
BinaryOperator<Integer> max = (a, b) -> Math.max(a, b);
// Method reference
BinaryOperator<Integer> max = Math::max;

Format: ClassName::staticMethodName

2. Instance Method Reference (Belirli Nesne)

String greeting = "Hello, World!";

// Lambda
Supplier<String> upper = () -> greeting.toUpperCase();
// Method reference — belirli bir nesnenin method'u
Supplier<String> upper = greeting::toUpperCase;

// Lambda
Predicate<String> contains = s -> greeting.contains(s);
// Method reference
Predicate<String> contains = greeting::contains;

Format: instance::methodName

3. Instance Method Reference (Keyfi Nesne)

// Lambda
Function<String, String> toUpper = s -> s.toUpperCase();
// Method reference — herhangi bir String nesnesinin method'u
Function<String, String> toUpper = String::toUpperCase;

// Lambda
BiPredicate<String, String> startsWith = (s, prefix) -> s.startsWith(prefix);
// Method reference
BiPredicate<String, String> startsWith = String::startsWith;

Format: ClassName::instanceMethodName — ilk parametre this olur.

Bu tür, ilk gördüğünde kafa karıştırabilir. String::toUpperCase yazıyorsun ama toUpperCase static bir method değil. Java burada ilk parametreyi (s) method'un çağrıldığı nesne olarak kullanır: s.toUpperCase().

4. Constructor Reference

// Lambda
Supplier<ArrayList<String>> listFactory = () -> new ArrayList<>();
// Constructor reference
Supplier<ArrayList<String>> listFactory = ArrayList::new;

// Lambda
Function<String, File> fileCreator = path -> new File(path);
// Constructor reference
Function<String, File> fileCreator = File::new;

// Stream ile
List<String> names = List.of("Ali", "Veli", "Deli");
List<StringBuilder> builders = names.stream()
    .map(StringBuilder::new)    // her isim için new StringBuilder(name)
    .toList();

Format: ClassName::new


Composing Functions

Functional interface'lerin gerçek gücü birleştirme (composition) yeteneğindedir. Küçük, odaklı fonksiyonlar yazarsın, sonra bunları zincirleyerek karmaşık davranışlar oluşturursun.

Function: andThen() ve compose()

Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addExclamation = s -> s + "!";

// andThen: soldan sağa — önce trim, sonra upper, sonra !
Function<String, String> shout = trim
    .andThen(toUpper)
    .andThen(addExclamation);

System.out.println(shout.apply("  hello  "));  // "HELLO!"

// compose: sağdan sola — önce toUpper, sonra trim çalışır
Function<String, String> pipeline = trim.compose(toUpper);
// Dikkat: compose sıralama karışıklığına yol açar, andThen daha okunabilir

andThen doğal okuma sırasını takip eder: A sonra B sonra C. compose matematiksel notasyonu takip eder: f∘g(x) = f(g(x)). Pratikte andThen tercih edilir.

Predicate: and(), or(), negate()

Predicate<String> isNotNull = s -> s != null;
Predicate<String> isNotEmpty = s -> !s.isEmpty();
Predicate<String> isShort = s -> s.length() < 5;

// and: ikisi de doğruysa
Predicate<String> isValid = isNotNull.and(isNotEmpty);

// or: biri doğruysa
Predicate<String> isNullOrEmpty = isNotNull.negate().or(isNotEmpty.negate());

// negate: tersine çevir
Predicate<String> isLong = isShort.negate();

// Karmaşık koşul
Predicate<String> filter = isNotNull
    .and(isNotEmpty)
    .and(isShort.negate());  // null değil, boş değil, kısa değil

List<String> words = List.of("hi", "", "hello", "magnificent", "ok");
words.stream()
    .filter(filter)
    .forEach(System.out::println);  // hello, magnificent

Consumer: andThen()

Consumer<String> print = System.out::println;
Consumer<String> log = s -> System.err.println("[LOG] " + s);

// Her iki consumer da çalışır
Consumer<String> printAndLog = print.andThen(log);
printAndLog.accept("Test");
// Stdout: Test
// Stderr: [LOG] Test

Custom Functional Interface

Standart interface'ler çoğu durumda yeterli. Ama bazen daha spesifik bir imza lazım — özellikle checked exception fırlatman gerektiğinde.

// Checked exception fırlatan fonksiyon — Function<T,R> bunu desteklemez
@FunctionalInterface
interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;

    // Utility: checked exception'ı unchecked'a çevir
    static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f) {
        return t -> {
            try {
                return f.apply(t);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }
}

// Kullanım
List<String> urls = List.of(
    "https://example.com",
    "https://google.com"
);

// URL::new checked exception fırlatır — normal Function'da kullanılamaz
// ThrowingFunction.unchecked ile sarmalıyoruz
List<URL> parsed = urls.stream()
    .map(ThrowingFunction.unchecked(URL::new))
    .toList();

Üç veya daha fazla parametreli fonksiyonlar için de custom interface yazarsın:

@FunctionalInterface
interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);
}

TriFunction<String, String, String, String> fullName =
    (first, middle, last) -> first + " " + middle + " " + last;

System.out.println(fullName.apply("Ali", "Can", "Yılmaz"));
// Ali Can Yılmaz

Stream API ile Entegrasyon

Functional interface'lerin en doğal kullanım yeri Stream API'dir. Her Stream operasyonu bir functional interface kabul eder:

List<Employee> employees = List.of(
    new Employee("Ali", "Engineering", 15000),
    new Employee("Ayşe", "Marketing", 12000),
    new Employee("Mehmet", "Engineering", 18000),
    new Employee("Fatma", "Marketing", 14000),
    new Employee("Can", "Engineering", 16000)
);

// Her operasyonun arkasındaki functional interface:
Map<String, Double> avgSalaryByDept = employees.stream()
    .filter(e -> e.salary() > 13000)           // Predicate<Employee>
    .collect(Collectors.groupingBy(
        Employee::department,                    // Function<Employee, String>
        Collectors.averagingDouble(
            Employee::salary                     // ToDoubleFunction<Employee>
        )
    ));

// {Engineering=16333.33, Marketing=14000.0}

Stream operasyonları ve karşılıkları:

Stream MethodFunctional Interface
filter()Predicate<T>
map()Function<T, R>
flatMap()Function<T, Stream<R>>
forEach()Consumer<T>
reduce()BinaryOperator<T>
sorted()Comparator<T>
peek()Consumer<T>

Gerçek Dünya: Validation Chain

Functional interface'ler ile esnek bir validation sistemi kurabiliriz:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

record ValidationResult(boolean valid, List<String> errors) {
    static ValidationResult ok() {
        return new ValidationResult(true, List.of());
    }
    static ValidationResult fail(String error) {
        return new ValidationResult(false, List.of(error));
    }
}

class Validator<T> {
    private final List<Function<T, ValidationResult>> rules = new ArrayList<>();

    Validator<T> addRule(Predicate<T> check, String errorMessage) {
        rules.add(obj -> check.test(obj)
            ? ValidationResult.ok()
            : ValidationResult.fail(errorMessage));
        return this;
    }

    ValidationResult validate(T obj) {
        List<String> errors = rules.stream()
            .map(rule -> rule.apply(obj))
            .filter(r -> !r.valid())
            .flatMap(r -> r.errors().stream())
            .toList();

        return errors.isEmpty()
            ? ValidationResult.ok()
            : new ValidationResult(false, errors);
    }
}

// Kullanım
record User(String name, String email, int age) {}

class Main {
    public static void main(String[] args) {
        Validator<User> userValidator = new Validator<User>()
            .addRule(u -> u.name() != null && !u.name().isBlank(),
                     "Name cannot be empty")
            .addRule(u -> u.email() != null && u.email().contains("@"),
                     "Invalid email format")
            .addRule(u -> u.age() >= 18,
                     "Must be at least 18 years old")
            .addRule(u -> u.age() <= 120,
                     "Age seems unrealistic");

        User validUser = new User("Ali", "ali@example.com", 25);
        User invalidUser = new User("", "invalid-email", 15);

        System.out.println(userValidator.validate(validUser));
        // ValidationResult[valid=true, errors=[]]

        System.out.println(userValidator.validate(invalidUser));
        // ValidationResult[valid=false, errors=[Name cannot be empty,
        //   Invalid email format, Must be at least 18 years old]]
    }
}

Bu tasarımın güzelliği: kurallar birer Predicate — istediğin kadar ekle, çıkar, birleştir. Validator sınıfı kuralların ne olduğunu bilmez, sadece uygular.

Gerçek Dünya: Transformation Pipeline

Veri dönüşüm pipeline'ları functional interface'lerin bir diğer güçlü kullanım alanıdır:

import java.util.function.Function;
import java.util.function.UnaryOperator;

class Pipeline<T> {
    private Function<T, T> combined = Function.identity();

    Pipeline<T> addStep(UnaryOperator<T> step) {
        combined = combined.andThen(step);
        return this;
    }

    T execute(T input) {
        return combined.apply(input);
    }
}

class Main {
    public static void main(String[] args) {
        // Metin temizleme pipeline'ı
        Pipeline<String> textCleaner = new Pipeline<String>()
            .addStep(String::trim)
            .addStep(String::toLowerCase)
            .addStep(s -> s.replaceAll("[^a-z0-9\\s]", ""))
            .addStep(s -> s.replaceAll("\\s+", " "));

        String dirty = "  Hello,  WORLD!!!  How   Are   You?  ";
        String clean = textCleaner.execute(dirty);
        System.out.println(clean);  // "hello world how are you"

        // Fiyat hesaplama pipeline'ı
        Pipeline<Double> priceCalculator = new Pipeline<Double>()
            .addStep(price -> price * 0.9)    // %10 indirim
            .addStep(price -> price * 1.18)   // KDV ekle
            .addStep(price -> Math.round(price * 100) / 100.0);  // Yuvarla

        System.out.println(priceCalculator.execute(100.0));  // 106.2
    }
}

💡 İpucu: Pipeline pattern'i ETL (Extract-Transform-Load) süreçlerinde, veri temizlemede, resim işlemede ve request/response middleware'lerinde yaygın olarak kullanılır. Her adım bağımsız, test edilebilir ve değiştirilebilirdir.


Özet

  • Functional interface, tek abstract method'u olan interface'tir. @FunctionalInterface annotation'ı derleyici koruması sağlar — mutlaka ekle.

  • java.util.function paketi standart functional interface'leri sunar: Predicate (test), Function (dönüştür), Consumer (tüket), Supplier (üret), BiFunction, UnaryOperator, BinaryOperator.

  • Method reference dört türde gelir: static (Integer::parseInt), belirli instance (str::length), keyfi instance (String::toUpperCase), constructor (ArrayList::new). Lambda'nın kısa hali — okunabilirliği artırır.

  • Composition ile küçük fonksiyonları birleştir: andThen(), compose(), and(), or(), negate(). Karmaşık davranışları basit parçalardan oluştur.

  • Custom functional interface checked exception veya 3+ parametre gerektiğinde yaz. Standartları bilmeden custom yazma — tekerleği yeniden icat etme.

  • Stream API functional interface'lerin doğal ortamıdır. filterPredicate, mapFunction, forEachConsumer, reduceBinaryOperator.