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.
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
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); } }
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 } }
Wbudowane Functional Interfaces
Java 8 dostarcza gotowe interfejsy w pakiecie java.util.function:
Interface | Metoda | Zastosowanie | Przykład |
---|---|---|---|
Predicate<T> | boolean test(T t) | Testowanie warunków | x -> x > 0 |
Function<T,R> | R apply(T t) | Transformacja danych | s -> s.length() |
Consumer<T> | void accept(T t) | Operacje na danych | x -> 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) { Listnumbers = 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 } }
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) { Listnames = 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) { Listproducts = 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 SupplierrandomNumber = () -> 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 } }
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) { Listproducts = 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?
- Masz prostą operację do wykonania (1-2 linie kodu)
- Pracujesz ze Stream API
- Chcesz przekazać „zachowanie” jako parametr
- Zastępujesz klasy anonimowe
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) { Listnames = 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! } }
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.
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
.
TAK, ale różnica jest minimalna w większości aplikacji. Ważniejsze są czytelność kodu i łatwość utrzymania.
TAK, ale muszą być „effectively final” – czyli niezmieniane po inicjalizacji. Java automatycznie to sprawdza.
Predicate (testowanie), Function (transformacja), Consumer (operacje) i Supplier (dostarczanie). Te 4 pokrywają 90% przypadków użycia.
Przydatne zasoby:
- Oracle – java.util.function package
- Oracle Tutorial – Lambda Expressions
- Oracle – Stream API documentation
🚀 Zadanie dla Ciebie
Stwórz program który:
- Ma listę liczb całkowitych od 1 do 20
- Używając Predicate znajdź liczby parzyste większe od 10
- Używając Function przekształć je na ich kwadraty
- 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?