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).