Functional Interfaces w Javie – Przewodnik dla Początkujących

TL;DR: Functional interfaces to interfejsy z dokładnie jedną metodą abstrakcyjną. Java 8 wprowadza lambdy i wbudowane interfejsy jak Predicate, Function, Consumer. Zastępują anonimowe klasy, czyniąc kod krótszym i bardziej czytelnym.

Czym są Functional Interfaces w Javie?

Functional interface to interfejs, który zawiera dokładnie jedną metodę abstrakcyjną. Java 8 wprowadziła je wraz z wyrażeniami lambda, rewolucjonizując sposób pisania kodu.

Functional interface to jak pilot do telewizora z jednym przyciskiem – ma jedno konkretne zadanie do wykonania. Lambda expression to sposób na szybkie „naciśnięcie tego przycisku” bez tworzenia całego nowego pilota.
Functional Interface – interfejs z dokładnie jedną metodą abstrakcyjną, może zawierać metody default i static

Dlaczego to ważne?

Functional interfaces rozwiązują problem rozwlekłego kodu z klasami anonimowymi. Zamiast pisać 5 linii kodu dla prostej operacji, piszesz jedną linię z lambdą. To kluczowe dla programowania funkcyjnego w Javie i pracy z Stream API.

Co się nauczysz:

  • Jak tworzyć własne functional interfaces z adnotacją @FunctionalInterface
  • Jak używać wbudowanych interfejsów: Predicate, Function, Consumer, Supplier
  • Jak zastąpić klasy anonimowe wyrażeniami lambda
  • Kiedy i dlaczego używać functional interfaces w praktyce
  • Jak unikać typowych błędów z lambda expressions
Wymagania wstępne: Podstawy Java (klasy, interfejsy), znajomość kolekcji ArrayList/List

Tworzenie Własnego Functional Interface

Zaczniemy od stworzenia własnego functional interface:

@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);
    
    // Można dodać metody default
    default void printResult(int result) {
        System.out.println("Wynik: " + result);
    }
}
Adnotacja @FunctionalInterface nie jest obowiązkowa, ale compiler sprawdzi czy interfejs rzeczywiście ma jedną metodę abstrakcyjną.

Użycie naszego interfejsu – stary sposób vs nowy:

public class FunctionalInterfaceExample {
    public static void main(String[] args) {
        
        // STARY SPOSÓB - klasa anonimowa (5 linii)
        Calculator oldWay = new Calculator() {
            @Override
            public int calculate(int a, int b) {
                return a + b;
            }
        };
        
        // NOWY SPOSÓB - lambda expression (1 linia!)
        Calculator newWay = (a, b) -> a + b;
        
        // Użycie identyczne
        System.out.println(oldWay.calculate(5, 3)); // 8
        System.out.println(newWay.calculate(5, 3)); // 8
    }
}
Pro tip: Lambda można zapisać w różnych formach: (a, b) -> a + b, (int a, int b) -> { return a + b; } lub Integer::sum

Wbudowane Functional Interfaces

Java 8 dostarcza gotowe interfejsy w pakiecie java.util.function:

InterfaceMetodaZastosowaniePrzykład
Predicate<T>boolean test(T t)Testowanie warunkówx -> x > 0
Function<T,R>R apply(T t)Transformacja danychs -> s.length()
Consumer<T>void accept(T t)Operacje na danychx -> System.out.println(x)
Supplier<T>T get()Dostarczanie danych() -> new ArrayList()

Predicate – Testowanie Warunków

Predicate zwraca true lub false. Idealny do filtrowania:

import java.util.function.Predicate;
import java.util.Arrays;
import java.util.List;

public class PredicateExample {
    public static void main(String[] args) {
        List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // Predicate do sprawdzania liczb parzystych
        Predicate isEven = number -> number % 2 == 0;
        
        // Filtrowanie z Predicate
        numbers.stream()
               .filter(isEven)
               .forEach(System.out::println); // 2, 4, 6, 8, 10
        
        // Łączenie Predicate
        Predicate isGreaterThan5 = number -> number > 5;
        
        numbers.stream()
               .filter(isEven.and(isGreaterThan5)) // parzyste I większe od 5
               .forEach(System.out::println); // 6, 8, 10
    }
}
Pułapka: Predicate.and() i Predicate.or() to metody default – nie mylić z operatorami && i ||

Function – Transformacja Danych

Function pobiera jeden argument i zwraca wynik (może być innego typu):

import java.util.function.Function;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FunctionExample {
    public static void main(String[] args) {
        List names = Arrays.asList("Anna", "Bartek", "Czesław");
        
        // Function do zamiany na wielkie litery
        Function toUpperCase = name -> name.toUpperCase();
        
        // Function do pobierania długości
        Function getLength = name -> name.length();
        
        // Transformacja nazw na wielkie litery
        List upperNames = names.stream()
                                      .map(toUpperCase)
                                      .collect(Collectors.toList());
        System.out.println(upperNames); // [ANNA, BARTEK, CZESŁAW]
        
        // Transformacja nazw na długości
        List lengths = names.stream()
                                    .map(getLength)
                                    .collect(Collectors.toList());
        System.out.println(lengths); // [4, 6, 7]
        
        // Łączenie Function
        Function upperAndReverse = toUpperCase.andThen(s -> new StringBuilder(s).reverse().toString());
        
        names.stream()
             .map(upperAndReverse)
             .forEach(System.out::println); // ANNA -> ANNA, BARTEK -> KETRAB, CZESŁAW -> WAŁSEZC
    }
}

Consumer – Operacje bez Zwracania Wyniku

Consumer pobiera argument ale nic nie zwraca – służy do operacji „pobocznych”:

import java.util.function.Consumer;
import java.util.Arrays;
import java.util.List;

public class ConsumerExample {
    public static void main(String[] args) {
        List products = Arrays.asList("Laptop", "Mysz", "Klawiatura");
        
        // Consumer do wypisywania
        Consumer print = product -> System.out.println("Produkt: " + product);
        
        // Consumer do logowania
        Consumer log = product -> System.out.println("[LOG] Przetwarzam: " + product);
        
        // Użycie Consumer
        products.forEach(print);
        
        // Łączenie Consumer
        Consumer printAndLog = print.andThen(log);
        
        System.out.println("\n--- Print i Log ---");
        products.forEach(printAndLog);
    }
}

Supplier – Dostarczanie Danych

Supplier nie pobiera argumentów, ale zwraca wynik. Używany do „lazy loading”:

import java.util.function.Supplier;
import java.util.Random;

public class SupplierExample {
    public static void main(String[] args) {
        
        // Supplier dla losowej liczby
        Supplier randomNumber = () -> new Random().nextInt(100);
        
        // Supplier dla aktualnego czasu
        Supplier currentTime = () -> System.currentTimeMillis();
        
        // Użycie - za każdym razem nowa wartość
        System.out.println("Losowa liczba: " + randomNumber.get());
        System.out.println("Czas: " + currentTime.get());
        
        // Supplier jako factory
        Supplier stringBuilderFactory = () -> new StringBuilder("Hello ");
        
        StringBuilder sb1 = stringBuilderFactory.get();
        StringBuilder sb2 = stringBuilderFactory.get();
        
        sb1.append("World");
        sb2.append("Java");
        
        System.out.println(sb1); // Hello World
        System.out.println(sb2); // Hello Java
    }
}
Typowy błąd: Mylenie Consumer z Function. Consumer nic nie zwraca (void), Function zwraca wynik.

Praktyczne Zastosowania

Functional interfaces świetnie współpracują ze Stream API:

import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;

public class PracticalExample {
    public static void main(String[] args) {
        List products = Arrays.asList(
            new Product("Laptop", 2500.0, Category.ELECTRONICS),
            new Product("Książka", 25.0, Category.BOOKS),
            new Product("Telefon", 1200.0, Category.ELECTRONICS),
            new Product("Słuchawki", 150.0, Category.ELECTRONICS)
        );
        
        // Predicate - filtrowanie
        Predicate isElectronics = p -> p.getCategory() == Category.ELECTRONICS;
        Predicate isExpensive = p -> p.getPrice() > 500;
        
        // Function - transformacja
        Function toUpperName = p -> p.getName().toUpperCase();
        
        // Consumer - operacja
        Consumer printWithPrefix = name -> System.out.println(">> " + name);
        
        // Łączenie wszystkiego w pipeline
        products.stream()
                .filter(isElectronics.and(isExpensive))  // tylko drogie elektronika
                .map(toUpperName)                        // nazwy na wielkie litery
                .forEach(printWithPrefix);               // wypisz z prefiksem
        
        // Wynik: >> LAPTOP, >> TELEFON
    }
}

class Product {
    private String name;
    private double price;
    private Category category;
    
    // konstruktor, gettery...
    public Product(String name, double price, Category category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }
    
    public String getName() { return name; }
    public double getPrice() { return price; }
    public Category getCategory() { return category; }
}

enum Category {
    ELECTRONICS, BOOKS
}

Kiedy Używać Functional Interfaces?

Używaj gdy:

  • Masz prostą operację do wykonania (1-2 linie kodu)
  • Pracujesz ze Stream API
  • Chcesz przekazać „zachowanie” jako parametr
  • Zastępujesz klasy anonimowe
Uwaga: Dla skomplikowanych operacji (5+ linii) lepiej stworzyć normalną metodę i użyć method reference.

Method References – Jeszcze Krótszy Zapis

Jeśli lambda tylko wywołuje istniejącą metodę, możesz użyć method reference:

import java.util.Arrays;
import java.util.List;

public class MethodReferenceExample {
    public static void main(String[] args) {
        List names = Arrays.asList("anna", "bartek", "czesław");
        
        // Lambda expression
        names.stream()
             .map(name -> name.toUpperCase())
             .forEach(name -> System.out.println(name));
        
        // Method reference - krótszy zapis
        names.stream()
             .map(String::toUpperCase)
             .forEach(System.out::println);
             
        // Oba robią to samo!
    }
}
Czy mogę użyć więcej niż jednej metody abstrakcyjnej w functional interface?

NIE. Functional interface może mieć dokładnie jedną metodę abstrakcyjną. Może mieć dodatkowo metody default i static, ale tylko jedna może być abstrakcyjna.

Kiedy używać lambda, a kiedy method reference?

Method reference gdy lambda tylko wywołuje istniejącą metodę: s -> s.length() staje się String::length. Lambda gdy robisz coś więcej: s -> s.length() > 5.

Czy functional interfaces są szybsze niż klasy anonimowe?

TAK, ale różnica jest minimalna w większości aplikacji. Ważniejsze są czytelność kodu i łatwość utrzymania.

Czy mogę używać zmiennych z zewnętrznego zakresu w lambda?

TAK, ale muszą być „effectively final” – czyli niezmieniane po inicjalizacji. Java automatycznie to sprawdza.

Które functional interfaces powinienem znać na początek?

Predicate (testowanie), Function (transformacja), Consumer (operacje) i Supplier (dostarczanie). Te 4 pokrywają 90% przypadków użycia.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Stwórz program który:

  1. Ma listę liczb całkowitych od 1 do 20
  2. Używając Predicate znajdź liczby parzyste większe od 10
  3. Używając Function przekształć je na ich kwadraty
  4. Używając Consumer wypisz wyniki z prefiksem „Kwadrat: „

Spróbuj napisać to w jednym łańcuchu stream()!

Functional interfaces to fundament programowania funkcyjnego w Javie. Pozwalają na pisanie krótszego, bardziej czytelnego kodu i są kluczowe dla pracy z nowoczesnymi API jak Stream. Jaki functional interface używasz najczęściej w swoich projektach?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry