Vavr: Revolutionizing Your Perception of Java

Vavr Core is a lightweight yet comprehensive functional programming library. It introduces immutable collections, functional control structures, and various semantic containers, enabling Java engineers to enjoy the expressiveness of the functional world while maintaining runtime compatibility.
What is Vavr
- Immutable Data Structures: Provides a complete set of immutable collections including List, Map, Stream, etc., naturally thread-safe.
- Functional Syntax Sugar: Functional interfaces covering 0~8 parameters, with additional tools for currying, partial application, composition, etc.
- Container Models: Types like
Option,Try,Either,Validationmake error handling and flow control more explicit. - Syntax Enhancements: Features like Property Checking and pattern matching fill gaps in native Java.
Adding Dependencies
Maven
<dependencies>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.4</version>
</dependency>
</dependencies>Gradle (Groovy DSL)
dependencies {
compile "io.vavr:vavr:0.10.4"
}Gradle 7+ (Recommended)
dependencies {
implementation "io.vavr:vavr:0.10.4"
}Testing features require additional dependency
io.vavr:vavr-test, see Property Checking section for details.
1. Tuples
1.1 Tuple Concept
In Vavr, Tuple1 ~ Tuple8 represent fixed-length, immutable combinations that can hold different types.
- Indexing starts from 1, accessed via
_1,_2, etc. - Typically used for returning multiple values or holding intermediate state, avoiding extra DTO declarations.
1.2 Creating Tuples
final Tuple2<Integer, String> user = Tuple.of(1, "Eli auk");
Assert.equals(user._1, 1);
Assert.equals(user._2, "Eli auk");Use
Tuple.of()static factory to create, type is automatically inferred based on element count.
1.3 Mapping Tuple Components Individually
final Tuple2<Integer, String> mapped = user.map(
a -> a + 1,
b -> b + "1"
);
Assert.equals(mapped._1, 2);
Assert.equals(mapped._2, "Eli auk1");1.4 Using a Single Mapping Function
final Tuple2<Integer, String> mapped = user.map((a, b) -> Tuple.of(a + 1, b + "1"));
Assert.equals(mapped._1, 2);
Assert.equals(mapped._2, "Eli auk1");When both fields need to be transformed together, you can directly return a new
Tuple, avoiding multiple iterations.
1.5 Transforming Tuples
final String result = user.apply((a, b) -> b.substring(2) + a);
Assert.equals(result, "i auk1");apply() directly uses each element as function parameters, returning any type of result, perfect for string concatenation or computing derived values.
2. Function
2.1 Functional Interfaces
Java 8 only has built-in functional interfaces with at most two parameters. Vavr extends this to Function0 ~ Function8, providing methods like memoized, curried, andThen, making it convenient to organize business logic in DSL style.
2.2 Creating Functions
Anonymous Class
Function2<Integer, Integer, Integer> sum = new Function2<>() {
@Override
public Integer apply(Integer a, Integer b) {
return a + b;
}
};Lambda
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;Static Factory
final Function1<Integer, Integer> inc = Function1.of(a -> a + 1);2.3 Composition
andThen: Current First, Then Parameter
Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiply = a -> a * 2;
Function1<Integer, Integer> pipeline = plusOne.andThen(multiply);
Assert.equals(pipeline.apply(2), 6);compose: Parameter First, Then Current
Function1<Integer, Integer> pipeline = plusOne.compose(multiply);
Assert.equals(pipeline.apply(2), 5);Memory tip:
andThenis “me first, you second”,composeis “you first, me second”.
2.4 Lifting
Partial functions throw exceptions on invalid input. Using lift() can wrap them into total functions:
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Option<Integer> safeDivide = Function2.lift(divide).apply(1, 0);
Assert.equals(safeDivide.isEmpty(), true);2.5 Partial Application
Fix some parameters to get a new function, useful for presetting context.
Function2<Integer, Integer, Integer> sum = Integer::sum;
Function1<Integer, Integer> add1 = sum.apply(1);
Function5<Integer, Integer, Integer, Integer, Integer, Integer> sum5 = (a, b, c, d, e) -> a + b + c + d + e;
Function2<Integer, Integer, Integer> add6 = sum5.apply(2, 3, 1);
Assert.equals(add1.apply(2), 3);
Assert.equals(add6.apply(4).apply(5), 15);Currying breaks FunctionN into a chain of single-parameter functions:
Function1<Integer, Integer> add1Curried = sum.curried().apply(1);
Function1<Integer, Function1<Integer, Function1<Integer, Function1<Integer, Integer>>>> curried = sum5.curried().apply(1);
Assert.equals(add1Curried.apply(2), 3);
Assert.equals(curried.apply(2).apply(3).apply(4).apply(5), 15);Difference Summary:
- Syntax: Partial application uses
.apply()to specify multiple parameters, currying uses.curried()first then.apply()one by one.- Return Type: Partial application directly gets a new
FunctionK, currying always returns a chain of single-parameter functions.- Flexibility: Partial application can bind multiple parameters at once, currying is better for chaining composition.
2.6 Memoization
Memoized functions compute only once, subsequent calls read from cache; compared to storing variables directly, functions are lazily executed and composable.
Function0<Integer> randomGenerator = Function0.of(() -> new Random().nextInt()).memoized();
Integer first = randomGenerator.apply();
Integer second = randomGenerator.apply();
Assert.equals(first, second);Common scenarios: caching expensive computations, lazy loading resources, composing multiple logic segments, or mocking dependencies in unit tests.
3. Value
3.1 Option
Encapsulates potentially null return values. API style is similar to Stream-Query’s Opp, supports chaining map, getOrElse, avoiding explicit null pointer checks.
3.2 Try
Captures exceptions and converts them to Success / Failure. You can also refer to Opp.ofTry in Stream-Query, quickly handling errors through methods like recover, onFailure.
3.3 Lazy
A lazy evaluation container with built-in memoization capability.
Lazy<Double> lazy = Lazy.of(Math::random);
lazy.isEvaluated(); // false
lazy.get(); // 0.123...
lazy.isEvaluated(); // true
Assert.equals(lazy.get(), lazy.get());Compared to Supplier, Lazy executes only once, while Supplier recomputes every time.
3.4 Either
Left value typically represents error, right value represents success.
public Either<String, Integer> compute() {
if (RandomUtil.randomBoolean()) {
return Either.right(42);
}
return Either.left("Computation failed");
}
Either<String, Integer> value = compute().map(i -> i * 2);
if (value.isRight()) {
System.out.println("Success: " + value.get());
} else {
System.out.println("Failure: " + value.getLeft());
}3.5 Validation
Validation is an applicative functor, suitable for aggregating errors from multiple fields.
class Person {
final String name;
final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
Validation<Seq<String>, Person> validatePerson(String name, int age) {
return Validation.combine(validateName(name), validateAge(age)).ap(Person::new);
}
private Validation<String, String> validateName(String name) {
return name == null || name.trim().isEmpty()
? Validation.invalid("Name cannot be empty")
: Validation.valid(name);
}
private Validation<String, Integer> validateAge(int age) {
return age < 0 || age > 150
? Validation.invalid("Age must be between 0 and 150")
: Validation.valid(age);
}
Validation<Seq<String>, Person> validPerson = validatePerson("John", 30);
Assert.equals(validPerson.get().age, 30);
Assert.equals(validPerson.get().name, "John");
Validation<Seq<String>, Person> invalidPerson = validatePerson(" ", -1);
Assert.equals(
invalidPerson.getError().asJava(),
List.of("Name cannot be empty", "Age must be between 0 and 150").asJava()
);4. Collection
Vavr’s collections are immutable by default, with chaining APIs that bring less boilerplate code.
4.1 List
Optional<Integer> reduce = Stream.of(1, 2, 3).reduce(Integer::sum);
int sum = IntStream.of(1, 2, 3).sum();
Number listSum = List.of(1, 2, 3).sum();
Assert.equals(listSum.intValue(), sum);4.2 Stream
Because of built-in tuples, Stream’s transformation capabilities are more friendly than the JDK version.
java.util.Map<Integer, Character> vanilla = Stream.of(1, 2, 3)
.collect(Collectors.toMap(a -> a, b -> (char) (b + 64)));
Map<Integer, Character> vavrMap = List.of(1, 2, 3).toMap(a -> a, b -> (char) (b + 64));
Assert.equals(vavrMap.get(1).get(), vanilla.get(1));
HashMap<Integer, Character> javaMap = List.of(1, 2, 3)
.toJavaMap(HashMap::new, a -> Tuple.of(a, (char) (a + 64)));
Assert.equals(javaMap.get(1), 'A');5. Property Checking
Requires additional dependency:
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr-test</artifactId>
<version>0.10.4</version>
</dependency>Example:
Arbitrary<Integer> ints = Arbitrary.integer();
Property.def("square(int) >= 0")
.forAll(ints)
.suchThat(i -> i * i >= 0)
.check()
.assertIsSatisfied();6. Pattern Matching
6.1 Basic Matching
String s = Match(1).of(
Case($(1), "one"),
Case($(2), "two"),
Case($(), "?")
);
Assert.equals(s, "one");
String opt = Match(null).of(
Case($(1), "one"),
Case($(2), "two"),
Case($(), "?")
);
Assert.equals(opt, "?");6.2 Multi-Condition Matching
int input = 5;
String output = Match(input).of(
Case($(n -> n > 0 && n < 3), "Between 1 and 2"),
Case($(n -> n > 3 && n < 6), "Between 4 and 5"),
Case($(), "Other")
);
Assert.equals(output, "Between 4 and 5");6.3 Predicate Matching
String s = Match(2).of(
Case($(a -> a == 1), "one"),
Case($(a -> a == 2), "two"),
Case($(), "?")
);
Assert.equals(s, "two");Built-in predicates (is(), isIn(), isNotNull(), isZero(), etc.) can make conditions more semantic:
String opt = Match(null).of(
Case($(is(1)), "one"),
Case($(is(2)), "two"),
Case($(), "?")
);
Assert.equals(opt, "?");Through Vavr, you can embrace immutable models and functional expressions while maintaining Java ecosystem compatibility, making business code more concise and safer.