Vavr для Java: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Vavr для Java: Полное руководство по функциональному программированию

1. Что такое Vavr и зачем она нужна?

Vavr (ранее известная как Javaslang) — это мощная библиотека для функционального программирования на Java, которая предоставляет неизменяемые коллекции, функциональные структуры данных и утилиты для работы с функциями высшего порядка. В отличие от стандартной Java Collections Framework, Vavr делает упор на иммутабельность (неизменяемость) и выразительность кода, что позволяет писать более предсказуемые, потокобезопасные и лаконичные программы.

Зачем нужна Vavr? В современной Java (начиная с 8 версии) появились лямбда-выражения и Stream API, но стандартные коллекции остались мутабельными. Это приводит к побочным эффектам, сложностям в многопоточном программировании и необходимости писать много шаблонного кода. Vavr решает эти проблемы, предлагая:

  • Неизменяемые коллекции (List, Set, Map) с богатым API, похожим на Scala или Kotlin.
  • Типы Option, Try, Either, Validation для безопасной обработки null, исключений и ошибок без try-catch блоков.
  • Кортежи (Tuple) для группировки разнотипных значений без создания DTO.
  • Функциональные интерфейсы (FunctionN, CheckedFunction) с композицией, мемоизацией и частичным применением.
  • Ленивые вычисления (Lazy) и Сопоставление с образцом (Pattern Matching).

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

2. Установка

Vavr распространяется через Maven Central. Для подключения к проекту используйте одну из систем сборки:

Maven

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.10.4</version>
</dependency>

Gradle

implementation 'io.vavr:vavr:0.10.4'

Gradle (Kotlin DSL)

implementation("io.vavr:vavr:0.10.4")

Для Java 9+ модульной системы добавьте в module-info.java:

requires vavr;

Актуальная версия на момент написания статьи — 0.10.4 (выпущена в 2019 году, но стабильна и широко используется). Проверить последнюю версию можно на MVNRepository.

3. Быстрый старт — минимальный рабочий пример

Создайте класс VavrDemo.java и запустите его:

import io.vavr.collection.List;
import io.vavr.control.Option;

public class VavrDemo {
    public static void main(String[] args) {
        // Создаём неизменяемый список
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        // Функциональная трансформация
        List<Integer> doubled = numbers
            .filter(n -> n % 2 == 0)   // чётные
            .map(n -> n * 2);          // удвоить

        System.out.println(doubled); // List(4, 8)

        // Option вместо Optional
        Option<String> maybeName = Option.of("Alice");
        String greeting = maybeName
            .map(name -> "Hello, " + name)
            .getOrElse("Hello, stranger");

        System.out.println(greeting); // Hello, Alice
    }
}

Этот пример показывает две ключевые возможности: неизменяемые коллекции с цепочкой методов и безопасную работу с null через Option.

4. Основные методы, функции и классы

4.1. Option — безопасная замена Optional

Сигнатура: Option<T> Option.of(T value) / Option.none()

Что делает: Контейнер, который может содержать значение (Some) или быть пустым (None). Отличается от java.util.Optional тем, что является сериализуемым, коллекцией (можно использовать в Stream) и имеет богатый API.

Option<String> some = Option.of("Hello");
Option<String> none = Option.none();

// Безопасное извлечение с запасным значением
String result = none.getOrElse("Default");
System.out.println(result); // Default

// Проверка наличия
some.peek(System.out::println); // Hello

// Трансформация
Option<Integer> length = some.map(String::length); // Some(5)

4.2. Try — обработка исключений без try-catch

Сигнатура: Try<T> Try.of(CheckedFunction0<T> supplier)

Что делает: Обёртка для операции, которая может выбросить исключение. Возвращает Success или Failure. Позволяет объединять операции, которые могут упасть, в цепочку.

Try<Integer> division = Try.of(() -> 10 / 0);

// Обработка результата
int result = division
    .recover(ex -> {
        System.err.println("Error: " + ex.getMessage());
        return 0;
    })
    .get();

System.out.println(result); // 0

// Цепочка Try
Try<Integer> parsed = Try.of(() -> Integer.parseInt("123"))
    .map(i -> i * 2);
System.out.println(parsed.get()); // 246

4.3. Either — результат с двумя возможными типами

Сигнатура: Either<L, R> Either.left(L value) / Either.right(R value)

Что делает: Контейнер, который содержит либо левое (обычно ошибка), либо правое (успех) значение. Часто используется для возврата ошибок без исключений.

Either<String, Integer> parseNumber(String s) {
    try {
        return Either.right(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Either.left("Invalid number: " + s);
    }
}

Either<String, Integer> result = parseNumber("42");
String output = result.fold(
    left -> "Error: " + left,
    right -> "Success: " + right
);
System.out.println(output); // Success: 42

4.4. List — неизменяемый связный список

Сигнатура: List<T> List.of(T... elements)

Что делает: Неизменяемый (persistent) односвязный список. Все операции (добавление, удаление) создают новый список, разделяя неизменные части с оригиналом. Богатый API: map, filter, flatMap, zip, groupBy и т.д.

List<String> names = List.of("Alice", "Bob", "Charlie");

// Добавление элемента (создаётся новый список)
List<String> withDavid = names.prepend("David");
System.out.println(withDavid); // List(David, Alice, Bob, Charlie)

// Фильтрация и трансформация
List<Integer> lengths = names
    .filter(name -> name.length() > 3)
    .map(String::length);
System.out.println(lengths); // List(5, 7)

// Свёртка
int totalLength = names.foldLeft(0, (acc, name) -> acc + name.length());
System.out.println(totalLength); // 16

4.5. HashMap — неизменяемая хэш-карта

Сигнатура: Map<K, V> HashMap.of(K k1, V v1, ...)

Что делает: Неизменяемая реализация Map на основе хэш-таблицы с поддержкой персистентности. Оптимизирована для частого чтения.

Map<String, Integer> ages = HashMap.of("Alice", 30, "Bob", 25);

// Добавление/обновление
Map<String, Integer> updated = ages.put("Charlie", 35);
System.out.println(updated); // HashMap((Alice, 30), (Bob, 25), (Charlie, 35))

// Безопасное получение
Option<Integer> age = ages.get("Alice");
System.out.println(age.getOrElse(0)); // 30

// Трансформация значений
Map<String, String> greetings = ages.mapValues(age -> "Age: " + age);
System.out.println(greetings); // HashMap((Alice, Age: 30), (Bob, Age: 25))

4.6. Tuple — кортежи для группировки значений

Сигнатура: Tuple2<T1, T2> Tuple.of(T1 t1, T2 t2) (до Tuple8)

Что делает: Неизменяемый кортеж, содержащий от 2 до 8 элементов разных типов. Позволяет избежать создания вспомогательных классов для временной группировки данных.

Tuple2<String, Integer> person = Tuple.of("Alice", 30);

// Доступ к элементам
String name = person._1;
int age = person._2;
System.out.println(name + " is " + age + " years old"); // Alice is 30 years old

// Трансформация
Tuple2<String, String> transformed = person.map(
    (n, a) -> Tuple.of(n.toUpperCase(), "Age: " + a)
);
System.out.println(transformed); // (ALICE, Age: 30)

// Распаковка
String result = person.apply((n, a) -> n + " (" + a + ")");
System.out.println(result); // Alice (30)

4.7. Lazy — ленивые вычисления

Сигнатура: Lazy<T> Lazy.of(Supplier<T> supplier)

Что делает: Позволяет отложить вычисление значения до первого обращения. Результат кэшируется, и при повторных вызовах возвращается сохранённое значение. Потокобезопасно.

Lazy<Double> heavyComputation = Lazy.of(() -> {
    System.out.println("Computing...");
    return Math.pow(2, 10);
});

// Первый вызов — вычисление
System.out.println(heavyComputation.get()); // Computing... 1024.0

// Второй вызов — кэшированное значение
System.out.println(heavyComputation.get()); // 1024.0 (без "Computing...")

4.8. Pattern Matching — сопоставление с образцом

Сигнатура: Match(value).of(Case($(predicate), result), ...)

Что делает: Реализация сопоставления с образцом, похожая на Scala. Позволяет писать выразительные условные конструкции без if-else цепочек.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;

Object value = 42;

String result = Match(value).of(
    Case($(n -> n instanceof Integer && (Integer)n > 0), "Positive integer"),
    Case($(0), "Zero"),
    Case($(n -> n instanceof String), "String: " + value),
    Case($(), "Other")
);

System.out.println(result); // Positive integer

// Сопоставление с Option
Option<String> opt = Option.of("Hello");
String output = Match(opt).of(
    Case($Some($()), "Has value: " + opt.get()),
    Case($None(), "Empty")
);
System.out.println(output); // Has value: Hello

4.9. Validation — валидация с накоплением ошибок

Сигнатура: Validation<E, T> Validation.combine(...).ap(Function)

Что делает: Позволяет выполнять валидацию нескольких полей и собирать все ошибки, а не останавливаться на первой. В отличие от Either, накапливает ошибки в стиле applicative functor.

Validation<String, String> validateName(String name) {
    return name != null && !name.isEmpty()
        ? Validation.valid(name)
        : Validation.invalid("Name is required");
}

Validation<String, Integer> validateAge(int age) {
    return age >= 0 && age < 150
        ? Validation.valid(age)
        : Validation.invalid("Invalid age");
}

// Комбинирование валидаций
Validation<Seq<String>, Person> validated = Validation.combine(
    validateName("Alice"),
    validateAge(30)
).ap(Person::new);

if (validated.isValid()) {
    System.out.println("Valid: " + validated.get());
} else {
    System.out.println("Errors: " + validated.getError());
}

4.10. FunctionN — функциональные интерфейсы с композицией

Сигнатура: Function2<T1, T2, R> Function2.of(BiFunction<T1, T2, R> f)

Что делает: Vavr предоставляет функциональные интерфейсы от Function0 до Function8 с методами для композиции (andThen, compose), частичного применения (curried, apply с меньшим числом аргументов) и мемоизации (memoized).

                    

Рекомендаций