review and application
Greetings to all who are tired of endless checks on null
bulky blocks try-catch
and mutating collections. If you've ever dreamed of bringing some functionality to Java, I'm happy to introduce you to the Vavr library.
With Java 8, we finally got lambda expressions and the Stream API. It was a breath of fresh air after years of imperative programming. However, compared to other languages like Scala or Haskell, Java still feels like a language made for OOP rather than functional programming.
Functional programming offers us:
Immutability: objects do not change their state after creation.
Pure functions: the result of a function depends only on its input data and has no side effects.
Functions as first-class objects: functions can be passed, returned, and stored in variables.
Vavr aims to bring these concepts to Java.
Installation
For Maven:
Add inpom.xml
the following dependency:
<dependencies>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.4</version>
</dependency>
</dependencies>
For Gradle:
IN build.gradle
add:
dependencies {
implementation "io.vavr:vavr:0.10.4"
}
Vavr Syntax Overview
Corteges
Tuples allow you to combine multiple values of different types into a single immutable structure without having to create a separate class.
import io.vavr.Tuple;
import io.vavr.Tuple2;
Tuple2<String, Integer> user = Tuple.of("Alice", 30);
// Доступ к элементам
String name = user._1;
Integer age = user._2;
You can create tuples with up to 8 elements. Tuple8
.
Functions: composition, currying, memoization
Vavr extends Java functional interfaces by providing functions with arity up to 8 Function8
and adding some useful methods.
Function composition allows you to chain functions together, where the output of one method becomes the input of another:
import io.vavr.Function1;
Function1<Integer, Integer> multiplyBy2 = x -> x * 2;
Function1<Integer, Integer> subtract5 = x -> x - 5;
Function1<Integer, Integer> combined = multiplyBy2.andThen(subtract5);
int result = combined.apply(10); // (10 * 2) - 5 = 15
Currying turns a function with multiple arguments into a sequence of functions with one argument:
import io.vavr.Function3;
Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
Function1<Integer, Function1<Integer, Function1<Integer, Integer>>> curriedSum = sum.curried();
int result = curriedSum.apply(1).apply(2).apply(3); // 6
Memoization caches the function result for certain arguments, which can improve performance on repeated calls:
import io.vavr.Function1;
Function1<Integer, Integer> factorial = Function1.of(this::computeFactorial).memoized();
int result1 = factorial.apply(5); // Вычисляет и кэширует результат
int result2 = factorial.apply(5); // Возвращает кэшированный результат
// Реализация функции факториала
private int computeFactorial(int n) {
if (n == 0) return 1;
return n * computeFactorial(n - 1);
}
Functional types
Option
replaces the use null
representing a value that may be present Some
or absent None
:
import io.vavr.control.Option;
Option<String> maybeUsername = getUsername();
maybeUsername.map(String::toUpperCase)
.peek(name -> System.out.println("Hello, " + name))
.onEmpty(() -> System.out.println("No user logged in"));
Try
allows you to handle operations that may throw an exception in a functional style:
import io.vavr.control.Try;
Try<Integer> parsedNumber = Try.of(() -> Integer.parseInt("123"));
parsedNumber.onSuccess(num -> System.out.println("Parsed number: " + num))
.onFailure(ex -> System.err.println("Failed to parse number: " + ex.getMessage()));
Lazy
provides lazy evaluation and caching of the result:
import io.vavr.Lazy;
Lazy<Double> randomValue = Lazy.of(Math::random);
System.out.println(randomValue.isEvaluated()); // false
double value = randomValue.get(); // Вычисляет и возвращает значение
System.out.println(randomValue.isEvaluated()); // true
Either
represents a value of one of two possible types: Left
(usually a mistake) or Right
(usually successful outcome):
import io.vavr.control.Either;
Either<String, Integer> divisionResult = divide(10, 2);
divisionResult.peek(result -> System.out.println("Result: " + result))
.peekLeft(error -> System.err.println("Error: " + error));
// Реализация метода divide
public Either<String, Integer> divide(int dividend, int divisor) {
if (divisor == 0) {
return Either.left("Cannot divide by zero");
} else {
return Either.right(dividend / divisor);
}
}
Future
is used for asynchronous operations, allowing you to work with their results in a functional style:
import io.vavr.concurrent.Future;
Future<String> futureResult = Future.of(() -> longRunningOperation());
futureResult.onSuccess(result -> System.out.println("Operation completed: " + result))
.onFailure(ex -> System.err.println("Operation failed: " + ex.getMessage()));
Validation
used to accumulate errors during data validation, instead of stopping after the first error:
import io.vavr.collection.Seq;
import io.vavr.control.Validation;
Validation<Seq<String>, User> userValidation = Validation.combine(
validateName(""),
validateAge(-5)
).ap(User::new);
if (userValidation.isValid()) {
User user = userValidation.get();
} else {
Seq<String> errors = userValidation.getError();
errors.forEach(System.err::println);
}
// Реализация методов валидации
public Validation<String, String> validateName(String name) {
return (name != null && !name.trim().isEmpty())
? Validation.valid(name)
: Validation.invalid("Name cannot be empty");
}
public Validation<String, Integer> validateAge(int age) {
return (age > 0)
? Validation.valid(age)
: Validation.invalid("Age must be positive");
}
Functional collections
Vavr provides immutable collections that extend Iterable
and offer a rich functional API.
List
Immutable list with functional methods:
import io.vavr.collection.List;
List<String> fruits = List.of("apple", "banana", "orange");
List<String> uppercaseFruits = fruits.map(String::toUpperCase);
System.out.println(uppercaseFruits); // [APPLE, BANANA, ORANGE]
Stream
A lazy sequence that can be infinite:
import io.vavr.collection.Stream;
Stream<Integer> naturalNumbers = Stream.from(1);
Stream<Integer> evenNumbers = naturalNumbers.filter(n -> n % 2 == 0);
evenNumbers.take(5).forEach(System.out::println); // 2, 4, 6, 8, 10
Map
Immutable associative array:
import io.vavr.collection.HashMap;
HashMap<String, Integer> wordCounts = HashMap.of("hello", 1, "world", 2);
wordCounts = wordCounts.put("hello", wordCounts.get("hello").get() + 1);
System.out.println(wordCounts); // HashMap((hello, 2), (world, 2))
Set
Immutable set:
import io.vavr.collection.HashSet;
HashSet<String> colors = HashSet.of("red", "green", "blue");
HashSet<String> moreColors = colors.add("yellow").remove("green");
System.out.println(moreColors); // HashSet(red, blue, yellow)
Vavr Usage Examples
Handling Errors with Try and Either
Situation: there is a method that can throw an exception, and you want to handle it without using try-catch
:
import io.vavr.control.Try;
Try<String> fileContent = Try.of(() -> readFile("path/to/file.txt"));
fileContent.onSuccess(content -> System.out.println("File content: " + content))
.onFailure(ex -> System.err.println("Error reading file: " + ex.getMessage()));
Or using Either
for more explicit error handling:
import io.vavr.control.Either;
Either<String, String> result = readFile("path/to/file.txt");
result.peek(content -> System.out.println("File content: " + content))
.peekLeft(error -> System.err.println("Error: " + error));
// Реализация метода readFile
public Either<String, String> readFile(String path) {
try {
String content = new String(Files.readAllBytes(Paths.get(path)));
return Either.right(content);
} catch (IOException e) {
return Either.left("Failed to read file: " + e.getMessage());
}
}
Option for dealing with potentially missing values
Let's say we get a value from an external source, which can be null
:
Option<String> maybeEmail = Option.of(getUserEmail());
maybeEmail.filter(email -> email.contains("@"))
.peek(email -> System.out.println("Valid email: " + email))
.onEmpty(() -> System.out.println("Invalid or missing email"));
Future for asynchronous computations
Let's say you need to perform several independent asynchronous operations and wait for their results:
Future<String> future1 = Future.of(() -> fetchDataFromService1());
Future<String> future2 = Future.of(() -> fetchDataFromService2());
Future<List<String>> combinedFuture = Future.sequence(List.of(future1, future2));
combinedFuture.onSuccess(results -> {
String result1 = results.get(0);
String result2 = results.get(1);
System.out.println("Results: " + result1 + ", " + result2);
}).onFailure(ex -> System.err.println("Error fetching data: " + ex.getMessage()));
Pattern Matching in Java with Vavr
Pattern matching allows you to process different data options:
import static io.vavr.API.*;
import static io.vavr.Predicates.*;
Object input = getInput();
String output = Match(input).of(
Case($(instanceOf(Integer.class).and(i -> (Integer) i > 0)), "Positive integer"),
Case($(instanceOf(Integer.class).and(i -> (Integer) i < 0)), "Negative integer"),
Case($(instanceOf(String.class)), str -> "String: " + str),
Case($(), "Unknown type")
);
System.out.println(output);
With Vavr you can significantly improve the quality of your code and make development more enjoyable.
Start small: use
Option
instead ofnull
,Try
instead oftry-catch
.Introduce functional collections gradually: Replace mutable collections with immutable counterparts from Vavr.
Additional resources:
Official Vavr documentation: vavr.io
Vavr GitHub repository: github.com/vavr-io/vavr
Book: “Functional Programming in Java” Venkata Subramaniam
For all Java newbies, I recommend joining this open lesson where participants will learn Java using the example of ping-pong. The game project will help you better understand the relationship between writing code and the result of its execution, even if you have never programmed before. You can sign up on the course page.