Skip to content
IRC-CodingIRC-Coding
Функциональное программированиеLambda-выраженияFunctional InterfacesHigher Order FunctionsStreamsЯзык программированияПрограммирование

Функциональное программирование: Lambda и Functional Interfaces

Lambda-выражения, Functional Interfaces, Higher-Order Functions, Pure Functions, Immutability и Streams в функциональном программировании.

S

schutzgeist

12 min read
Функциональное программирование: Lambda и Functional Interfaces

Функциональное программирование: Lambda-выражения и Functional Interfaces

Это подробное руководство по функциональному программированию с разбором Lambda-выражений, Functional Interfaces и Higher-Order Functions на практических примерах.

Суть в двух словах

Функциональное программирование сосредоточено на вычислениях через функции вместо изменения состояния. Lambda-выражения это встроенные функциональные литералы, которые связываются с Functional Interfaces.

Краткое определение

Функциональное программирование это парадигма, которая рассматривает функции как основные строительные блоки. В отличие от императивного подхода, здесь избегают изменений состояния.

Lambda-выражения описывают анонимное поведение с параметрами, телом и опциональным типом возврата. Тип выводится из целевого контекста (Target Type).

Functional Interfaces в Java содержат ровно один абстрактный метод и включают Higher-Order Functions:

  • Predicate: boolean test(T t) проверка условий
  • Function: R apply(T t) трансформации
  • Consumer: void accept(T t) обработка
  • Supplier: T get() создание значений

Основные принципы:

  • Pure Functions: неизменяемый вход производит детерминированный выход
  • Immutability: данные остаются неизменяемыми
  • Referential Transparency: вызов можно заменить результатом
  • Higher-Order Functions: функции как параметры или возвращаемые значения

Ключевые моменты для изучения

  • Lambda-выражения: анонимные функции с компактным синтаксисом
  • Functional Interfaces: ровно один абстрактный метод
  • Higher-Order Functions: функции как параметры или возвращаемые значения
  • Pure Functions: отсутствие побочных эффектов, детерминированность
  • Immutability: неизменяемые структуры данных
  • Streams API: декларативная обработка данных
  • Method References: компактная ссылка на методы
  • Релевантно для практики: современный Java, функциональные подходы

Основные компоненты

  1. Lambda-выражения: (x, y) -> x + y
  2. Functional Interfaces: Predicate<T>, Function<T,R>
  3. Pure Functions: без побочных эффектов
  4. Immutability: неизменяемые объекты
  5. Higher-Order Functions: map(), filter(), reduce()
  6. Streams: последовательная обработка данных
  7. Method References: String::length
  8. Closures: доступ к переменным внешней области

Практические примеры

1. Lambda-выражения и Functional Interfaces в Java

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

public class FunctionalProgrammingDemo {
    
    public static void main(String[] args) {
        List<String> namen = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
        List<Integer> zahlen = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // Lambda mit Predicate (Filter)
        Predicate<String> laengerAlsVier = name -> name.length() > 4;
        List<String> langeNamen = namen.stream()
            .filter(laengerAlsVier)
            .collect(Collectors.toList());
        System.out.println("Lange Namen: " + langeNamen);
        
        // Lambda mit Function (Map/Transformation)
        Function<String, Integer> stringLaenge = String::length; // Method Reference
        List<Integer> laengen = namen.stream()
            .map(stringLaenge)
            .collect(Collectors.toList());
        System.out.println("Namenlängen: " + laengen);
        
        // Lambda mit Consumer (ForEach)
        Consumer<String> drucker = name -> System.out.println("Hallo " + name);
        namen.forEach(drucker);
        
        // Lambda mit Supplier (Erzeugen)
        Supplier<Double> zufallszahl = () -> Math.random();
        System.out.println("Zufallszahl: " + zufallszahl.get());
        
        // Komplexe Lambda-Ausdrücke
        Predicate<Integer> istGerade = n -> n % 2 == 0;
        Predicate<Integer> istGroesserAlsFuenf = n -> n > 5;
        
        // Predicate kombinieren
        Predicate<Integer> istGeradeUndGroesser = istGerade.and(istGroesserAlsFuenf);
        
        List<Integer> gefilterteZahlen = zahlen.stream()
            .filter(istGeradeUndGroesser)
            .collect(Collectors.toList());
        System.out.println("Gerade und >5: " + gefilterteZahlen);
        
        // Higher-Order Function
        Function<Integer, Predicate<Integer>> groesserAls = grenzwert -> 
            zahl -> zahl > grenzwert;
        
        Predicate<Integer> groesserAlsDrei = groesserAls.apply(3);
        List<Integer> groessereZahlen = zahlen.stream()
            .filter(groesserAlsDrei)
            .collect(Collectors.toList());
        System.out.println(">3: " + groessereZahlen);
    }
}

2. Pure Functions и Immutability

// Императивный подход (с побочными эффектами)
class ImperativeRechner {
    private int summe = 0;
    
    public void addiere(int wert) {
        this.summe += wert; // Побочный эффект: состояние изменяется
    }
    
    public int getSumme() {
        return summe;
    }
}

// Функциональный подход (Pure Functions)
class FunctionalRechner {
    
    // Pure Function: без побочных эффектов, детерминирован
    public static int addiere(int a, int b) {
        return a + b;
    }
    
    // Pure Function с неизменяемыми данными
    public static List<Integer> filtereGerade(List<Integer> zahlen) {
        return zahlen.stream()
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
    }
    
    // Pure Function с трансформацией
    public static List<Integer> quadriere(List<Integer> zahlen) {
        return zahlen.stream()
            .map(n -> n * n)
            .collect(Collectors.toList());
    }
    
    // Higher-Order Function
    public static List<Integer> verarbeite(List<Integer> zahlen, 
                                         Function<Integer, Integer> operation) {
        return zahlen.stream()
            .map(operation)
            .collect(Collectors.toList());
    }
    
    // Pure Function с композицией
    public static Function<Integer, Integer> multipliziereMit(int faktor) {
        return zahl -> zahl * faktor;
    }
    
    public static Function<Integer, Integer> addiereZu(int wert) {
        return zahl -> zahl + wert;
    }
}

// Неизменяемый класс данных
public final class Person {
    private final String name;
    private final int alter;
    
    public Person(String name, int alter) {
        this.name = name;
        this.alter = alter;
    }
    
    // Pure Function для изменений (создает новый объект)
    public Person mitNeuemAlter(int neuesAlter) {
        return new Person(this.name, neuesAlter);
    }
    
    public Person mitNeuemName(String neuerName) {
        return new Person(neuerName, this.alter);
    }
    
    // Getter (без Setter для сохранения неизменяемости)
    public String getName() { return name; }
    public int getAlter() { return alter; }
    
    @Override
    public String toString() {
        return name + " (" + alter + ")";
    }
}

// Использование
public class PureFunctionDemo {
    public static void main(String[] args) {
        // Императивный подход
        ImperativeRechner imperativ = new ImperativeRechner();
        imperativ.addiere(5);
        imperativ.addiere(3);
        System.out.println("Imperativ: " + imperativ.getSumme()); // 8
        
        // Функциональный подход
        int ergebnis1 = FunctionalRechner.addiere(5, 3);
        int ergebnis2 = FunctionalRechner.addiere(5, 3); // Всегда одинаковый результат
        
        List<Integer> zahlen = Arrays.asList(1, 2, 3, 4, 5);
        List<Integer> gerade = FunctionalRechner.filtereGerade(zahlen);
        List<Integer> quadrate = FunctionalRechner.quadriere(zahlen);
        
        System.out.println("Gerade: " + gerade);
        System.out.println("Quadrate: " + quadrate);
        
        // Higher-Order Function
        Function<Integer, Integer> verdoppeln = n -> n * 2;
        List<Integer> verdoppelt = FunctionalRechner.verarbeite(zahlen, verdoppeln);
        System.out.println("Verdoppelt: " + verdoppelt);
        
        // Функциональная композиция
        Function<Integer, Integer> multiplizieren = FunctionalRechner.multipliziereMit(2);
        Function<Integer, Integer> addieren = FunctionalRechner.addiereZu(10);
        Function<Integer, Integer> kombiniert = multiplizieren.andThen(addieren);
        
        List<Integer> kombiniertErgebnis = FunctionalRechner.verarbeite(zahlen, kombiniert);
        System.out.println("Kombiniert (x*2+10): " + kombiniertErgebnis);
        
        // Неизменяемость
        Person alice = new Person("Alice", 25);
        Person aliceAelter = alice.mitNeuemAlter(26);
        
        System.out.println("Original: " + alice);      // Alice (25)
        System.out.println("Verändert: " + aliceAelter); // Alice (26)
    }
}

3. Streams API и декларативное программирование

import java.util.*;
import java.util.stream.*;

public class StreamAPIDemo {
    
    public static void main(String[] args) {
        List<Person> personen = Arrays.asList(
            new Person("Alice", 25, "Entwicklung"),
            new Person("Bob", 30, "Marketing"),
            new Person("Charlie", 35, "Entwicklung"),
            new Person("Diana", 28, "Vertrieb"),
            new Person("Eve", 32, "Entwicklung")
        );
        
        // Declarative Datenverarbeitung mit Streams
        
        // 1. Filtern und Transformieren
        List<String> entwicklerNamen = personen.stream()
            .filter(p -> p.getAbteilung().equals("Entwicklung")) // Filter
            .map(Person::getName)                                // Transformieren
            .sorted()                                            // Sortieren
            .collect(Collectors.toList());                       // Sammeln
        
        System.out.println("Entwickler: " + entwicklerNamen);
        
        // 2. Komplexe Pipeline mit mehreren Operationen
        Map<String, Double> durchschnittsAlterProAbteilung = personen.stream()
            .collect(Collectors.groupingBy(
                Person::getAbteilung,
                Collectors.averagingInt(Person::getAlter)
            ));
        
        System.out.println("Durchschnittsalter: " + durchschnittsAlterProAbteilung);
        
        // 3. Reduce für Aggregation
        int gesamtesAlter = personen.stream()
            .mapToInt(Person::getAlter)
            .reduce(0, Integer::sum); // Alternative: .sum()
        
        System.out.println("Gesamtes Alter: " + gesamtesAlter);
        
        // 4. Optional für sichere Verarbeitung
        Optional<Person> aeltestePerson = personen.stream()
            .max(Comparator.comparing(Person::getAlter));
        
        aeltestePerson.ifPresent(p -> 
            System.out.println("Älteste Person: " + p.getName()));
        
        // 5. Custom Collector
        String alleNamen = personen.stream()
            .map(Person::getName)
            .collect(Collectors.joining(", "));
        
        System.out.println("Alle Namen: " + alleNamen);
        
        // 6. Parallel Streams für Performance
        List<Integer> grosseZahlen = IntStream.range(1, 1_000_000)
            .boxed()
            .collect(Collectors.toList());
        
        long anzahlPrime = grosseZahlen.parallelStream()
            .filter(StreamAPIDemo::istPrimzahl)
            .count();
        
        System.out.println("Anzahl Primzahlen: " + anzahlPrime);
    }
    
    // Pure Function für Primzahlprüfung
    private static boolean istPrimzahl(int n) {
        if (n <= 1) return false;
        if (n <= 3) return true;
        if (n % 2 == 0 || n % 3 == 0) return false;
        
        for (int i = 5; i * i <= n; i += 6) {
            if (n % i == 0 || n % (i + 2) == 0) return false;
        }
        return true;
    }
}

// Person-Klasse für Beispiele
class Person {
    private final String name;
    private final int alter;
    private final String abteilung;
    
    public Person(String name, int alter, String abteilung) {
        this.name = name;
        this.alter = alter;
        this.abteilung = abteilung;
    }
    
    public String getName() { return name; }
    public int getAlter() { return alter; }
    public String getAbteilung() { return abteilung; }
    
    @Override
    public String toString() {
        return name + " (" + alter + ", " + abteilung + ")";
    }
}

Streams API позволяет писать обработку данных в функциональном стиле. Вместо явных циклов с временными переменными мы описываем, что именно нужно сделать.

Первый пример демонстрирует фильтрацию и трансформацию. filter() отбирает элементы по предикату, map() преобразует каждый элемент, sorted() упорядочивает результат, а collect() собирает значения в финальную структуру.

Со сложными операциями вроде группировки stream API справляется элегантно. groupingBy() группирует элементы по ключу, а вложенный averagingInt() вычисляет среднее значение в каждой группе.

reduce() агрегирует поток в единственное значение. Здесь он суммирует все возраста, но можно использовать и другие операции. Методы вроде sum() в IntStream упрощают частые случаи.

Optional безопасно работает с результатами, которые могут быть пусты. Вместо проверок на null вызываем ifPresent(), который выполнит операцию только если значение присутствует.

Пользовательские collectors позволяют собирать потоки нестандартными способами. Collectors.joining() с разделителем соединяет строки в одно значение.

Для обработки больших наборов данных parallelStream() распределяет вычисления между потоками. В примере выше параллельное вычисление ускоряет поиск простых чисел на миллионе элементов.

4. Higher-Order Functions и closures

import java.util.function.*;
import java.util.*;

public class HigherOrderFunctionsDemo {
    
    // Higher-Order Function: Nimmt Funktion als Parameter
    public static <T, R> List<R> mappe(List<T> liste, Function<T, R> mapper) {
        List<R> ergebnis = new ArrayList<>();
        for (T element : liste) {
            ergebnis.add(mapper.apply(element));
        }
        return ergebnis;
    }
    
    // Higher-Order Function: Gibt Funktion zurück
    public static Function<Integer, Integer> multiplizierer(int faktor) {
        return zahl -> zahl * faktor; // Closure: faktor ist gebunden
    }
    
    // Higher-Order Function: Gibt Predicate zurück
    public static Predicate<String> laengerAls(int mindestlaenge) {
        return text -> text.length() > mindestlaenge;
    }
    
    // Higher-Order Function mit mehreren Funktionen
    public static <T> List<T> verarbeiteKette(List<T> liste, 
                                           List<Function<T, T>> funktionen) {
        List<T> ergebnis = new ArrayList<>(liste);
        
        for (Function<T, T> funktion : funktionen) {
            ergebnis = mappe(ergebnis, funktion);
        }
        
        return ergebnis;
    }
    
    // Currying (vereinfacht)
    public static Function<Integer, Function<Integer, Integer>> addiereCurried() {
        return a -> b -> a + b;
    }
    
    // Function Composition
    public static <T> Function<T, T> komponiere(Function<T, T> f, Function<T, T> g) {
        return x -> f.apply(g.apply(x));
    }
    
    public static void main(String[] args) {
        List<String> woerter = Arrays.asList("Java", "Python", "JavaScript", "C++");
        List<Integer> zahlen = Arrays.asList(1, 2, 3, 4, 5);
        
        // Higher-Order Function verwenden
        List<Integer> laengen = mappe(woerter, String::length);
        System.out.println("Längen: " + laengen);
        
        // Funktion zurückgeben und verwenden
        Function<Integer, Integer> verdoppeln = multiplizierer(2);
        Function<Integer, Integer> verdreifachen = multiplizierer(3);
        
        List<Integer> verdoppelt = mappe(zahlen, verdoppeln);
        List<Integer> verdreifacht = mappe(zahlen, verdreifachen);
        
        System.out.println("Verdoppelt: " + verdoppelt);
        System.out.println("Verdreifacht: " + verdreifacht);
        
        // Predicate Higher-Order Function
        Predicate<String> laengerAlsDrei = laengerAls(3);
        List<String> langeWoerter = woerter.stream()
            .filter(laengerAlsDrei)
            .collect(Collectors.toList());
        
        System.out.println("Lange Wörter: " + langeWoerter);
        
        // Funktionskette
        List<Function<Integer, Integer>> funktionen = Arrays.asList(
            n -> n * 2,    // verdoppeln
            n -> n + 10,   // addieren
            n -> n / 3     // teilen
        );
        
        List<Integer> verarbeitet = verarbeiteKette(zahlen, funktionen);
        System.out.println("Verarbeitete Zahlen: " + verarbeitet);
        
        // Currying
        Function<Integer, Function<Integer, Integer>> addiere = addiereCurried();
        Function<Integer, Integer> addiereFuenf = addiere.apply(5);
        int ergebnis = addiereFuenf.apply(3); // 5 + 3 = 8
        
        System.out.println("Currying Ergebnis: " + ergebnis);
        
        // Function Composition
        Function<Integer, Integer> quadrieren = n -> n * n;
        Function<Integer, Integer> inkrementieren = n -> n + 1;
        
        Function<Integer, Integer> quadrierenDannInkrementieren = komponiere(inkrementieren, quadrieren);
        Function<Integer, Integer> inkrementierenDannQuadrieren = komponiere(quadrieren, inkrementieren);
        
        System.out.println("3²+1: " + quadrierenDannInkrementieren.apply(3)); // 10
        System.out.println("(3+1)²: " + inkrementierenDannQuadrieren.apply(3)); // 16
    }
}

Higher-order functions принимают функции как аргументы или возвращают новые функции. Это ключевой инструмент функционального программирования.

Метод mappe() демонстрирует простейший случай: он принимает список и функцию-преобразователь, применяя её к каждому элементу. Благодаря обобщённым типам этот метод работает с любыми типами данных.

multiplizierer() возвращает функцию, которая умножает число на переданный коэффициент. Здесь lambda захватывает переменную faktor из окружающего scope. Это closure. Важно: Java требует, чтобы захватываемые переменные были effectively final (не изменялись после захвата).

Аналогично laengerAls() конструирует предикат с заданной минимальной длиной строки. Каждый вызов возвращает другой предикат с разными граничными значениями.

verarbeiteKette() показывает, как применять последовательность функций. Результат каждой функции становится входом для следующей. Это полезно при обработке данных в несколько этапов.

Currying преобразует функцию нескольких аргументов в цепь функций одного аргумента. addiereCurried() возвращает функцию, которая при вызове с первым аргументом возвращает новую функцию, ожидающую второго аргумента. Это позволяет создавать специализированные функции частичным применением.

Function composition соединяет несколько функций в одну. komponiere() берёт две функции f и g и возвращает их композицию, где сначала применяется g, потом f. Порядок важен: g выполняется “внутри”, f “снаружи”. Это точно отражает математическую нотацию (f ∘ g)(x) = f(g(x)).

В примере выше quadrierenDannInkrementieren возводит число в квадрат, затем прибавляет единицу. inkrementierenDannQuadrieren делает обратное. При x=3 первое дает 10 (3²+1), второе дает 16 ((3+1)²).

5. Функциональное программирование в Python

from typing import List, Callable, Optional
from functools import reduce
import operator

# Pure Functions
def addiere(a: int, b: int) -> int:
    return a + b

def filtere_gerade(zahlen: List[int]) -> List[int]:
    return [n for n in zahlen if n % 2 == 0]

def quadriere(zahlen: List[int]) -> List[int]:
    return [n * n for n in zahlen]

# Higher-Order Functions
def verarbeite(zahlen: List[int], operation: Callable[[int], int]) -> List[int]:
    return [operation(n) for n in zahlen]

def multiplizierer(faktor: int) -> Callable[[int], int]:
    return lambda x: x * faktor

# Function Composition
def komponiere(f: Callable, g: Callable) -> Callable:
    return lambda x: f(g(x))

# Currying
def addiere_curried(a: int):
    return lambda b: a + b

# Unveränderliche Datenklasse
from dataclasses import dataclass

@dataclass(frozen=True)
class Person:
    name: str
    alter: int
    abteilung: str
    
    def mit_neuem_alter(self, neues_alter: int) -> 'Person':
        return Person(self.name, neues_alter, self.abteilung)

# Verwendung
def funktionale_demo():
    # Pure Functions
    zahlen = [1, 2, 3, 4, 5]
    gerade = filtere_gerade(zahlen)
    quadrate = quadriere(zahlen)
    
    print(f"Gerade: {gerade}")
    print(f"Quadrate: {quadrate}")
    
    # Higher-Order Functions
    verdoppeln = multiplizierer(2)
    verdreifachen = multiplizierer(3)
    
    verdoppelt = verarbeite(zahlen, verdoppeln)
    verdreifacht = verarbeite(zahlen, verdreifachen)
    
    print(f"Verdoppelt: {verdoppelt}")
    print(f"Verdreifacht: {verdreifacht}")
    
    # Function Composition
    quadrieren = lambda x: x * x
    inkrementieren = lambda x: x + 1
    
    quadrieren_dann_inkrementieren = komponiere(inkrementieren, quadrieren)
    inkrementieren_dann_quadrieren = komponiere(quadrieren, inkrementieren)
    
    print(f"3²+1: {quadrieren_dann_inkrementieren(3)}")  # 10
    print(f"(3+1)²: {inkrementieren_dann_quadrieren(3)}")  # 16
    
    # Currying
    addiere_fuenf = addiere_curried(5)
    ergebnis = addiere_fuenf(3)  # 8
    print(f"Currying Ergebnis: {ergebnis}")
    
    # Reduce für Aggregation
    summe = reduce(operator.add, zahlen, 0)
    produkt = reduce(operator.mul, zahlen, 1)
    
    print(f"Summe: {summe}")
    print(f"Produkt: {produkt}")
    
    # Immutability
    alice = Person("Alice", 25, "Entwicklung")
    alice_aelter = alice.mit_neuem_alter(26)
    
    print(f"Original: {alice}")
    print(f"Verändert: {alice_aelter}")

if __name__ == "__main__":
    funktionale_demo()

Синтаксис Lambda в сравнении

Java Lambda-выражения

// Различные формы Lambda
Predicate<String> leer = s -> s.isEmpty();
Predicate<String> leer2 = String::isEmpty; // Method Reference

Function<Integer, String> toString = i -> i.toString();
Function<Integer, String> toString2 = Object::toString;

Consumer<String> drucker = s -> System.out.println(s);
Consumer<String> drucker2 = System.out::println;

Supplier<Integer> zufall = () -> (int)(Math.random() * 100);

Python Lambda-выражения

# Lambda-выражения
leer = lambda s: len(s) == 0
verdoppeln = lambda x: x * 2

# Higher-Order Functions с Lambda
zahlen = [1, 2, 3, 4, 5]
verdoppelt = list(map(lambda x: x * 2, zahlen))
gerade = list(filter(lambda x: x % 2 == 0, zahlen))

JavaScript Lambda-выражения

// Arrow Functions
const leer = s => s.length === 0;
const verdoppeln = x => x * 2;

// Higher-Order Functions
const zahlen = [1, 2, 3, 4, 5];
const verdoppelt = zahlen.map(x => x * 2);
const gerade = zahlen.filter(x => x % 2 === 0);

Преимущества и недостатки

Плюсы функционального подхода

  • Тестируемость: чистые функции легко проверяются
  • Параллелизм: отсутствие побочных эффектов обеспечивает безопасную многопоточность
  • Переиспользование: функции высшего порядка дают большую гибкость
  • Читаемость: декларативный код часто понятнее императивного
  • Надежность: меньше ошибок из-за отсутствия изменения состояния

Минусы

  • Кривая обучения: требует привычки думать функционально
  • Производительность: функциональные абстракции могут иметь накладные расходы
  • Память: неизменяемость может потребовать больше памяти
  • Отладка: stack trace может быть сложнее

Частые вопросы на собеседованиях

  1. В чем разница между Lambda-выражением и анонимным классом? Lambda-выражение имеет компактный синтаксис для функционального интерфейса, анонимный класс требует больше кода.

  2. Объясните чистые функции! Это функции без побочных эффектов, которые при одном и том же входе всегда возвращают один и тот же результат.

  3. Что такое функция высшего порядка? Функция, которая принимает другие функции как параметры или возвращает функции.

  4. Почему неизменяемость важна? Предотвращает неожиданные изменения состояния и упрощает параллельную обработку.

Основные ссылки

  1. https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
  2. https://docs.oracle.com/javase/tutorial/collections/streams/
  3. https://www.python.org/doc/essays/list2str.html

Рекомендуемая книга

Keine Bücher für Kategorie "programming-languages" gefunden.

Назад к блогу
Share:

Nächster Artikel in Разработка программного обеспечения

Weiterlesen
Generics: типобезопасность и параметры типов

Похожие статьи