Small, pragmatic functional utilities for Java:
- Option – a lightweight alternative to
Optionalwith Kotlin-style helpers:apply,and,takeIf/Unless,ifInstance, and ergonomicifPresentOrElseoverloads. - Try – a result type representing success or failure, with
map/flatMap,recover,fold,onSuccess/onFailure, and bridges to Option / Either. - Either = Any<L,R> – a classic sum type (
Left / Right) featuringmapLeft,bimap,swap,joinLeft/joinRight, and conversions.
The codebase is fully covered by tests: 100% lines / 100% branches (JaCoCo).
- Installation
- Quick start
- Option
- Try
- Either (Any<L,R>)
- Kotlin ↔ Java equivalence
- Migration
- Design notes
- Testing & Coverage
- CI (GitHub Actions)
- Versioning & Compatibility
- License
- API cheatsheet
<dependency>
<groupId>io.github.bazhanovmaxim</groupId>
<artifactId>java-functional</artifactId>
<version>1.0.0</version>
</dependency>dependencies {
implementation("io.github.bazhanovmaxim:java-functional:1.0.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
testImplementation("org.assertj:assertj-core:3.26.0")
}dependencies {
implementation 'io.github.bazhanovmaxim:java-functional:1.0.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
testImplementation 'org.assertj:assertj-core:3.26.0'
}If you prefer consuming by tag or commit:
Gradle (Kotlin DSL)
repositories {
mavenCentral()
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.BazhanovMaxim:java-functional:<tag-or-commit>")
}Maven
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.BazhanovMaxim</groupId>
<artifactId>java-functional</artifactId>
<version><tag-or-commit></version>
</dependency>import org.bazhanov.option.Option;
import org.bazhanov.result.Try;
import org.bazhanov.any.*;
Option.of(" hello ")
.map(String::trim)
.takeIf(s -> !s.isEmpty())
.apply(System.out::println); // prints "hello"
// Error-safe parsing, no try/catch
int value = Option.of("42")
.runCatching(Integer::parseInt)
.recover(ex -> 0); // 42
// Bridge to Either and continue mapping
Any<String, Integer> either =
Option.of("21")
.runCatching(Integer::parseInt) // Try<Integer>
.toEither() // Either<Exception, Integer>
.map(x -> x * 2) // Right(42)
.mapLeft(Throwable::getMessage); // Left<String> on failureOption<T> wraps a value that may be absent. It does not carry error details (use Try for failures).
Option.of("A"); // wrap a value (null -> empty)
Option.ofNullable("A"); // alias
Option.<String>empty(); // emptyOption.of("abc").map(String::length); // Option(3)
Option.of("42").flatMap(s -> Option.of(Integer.parseInt(s)));
Option.of(6).filter(x -> x % 2 == 0); // Option(6) or empty
// Side-effects (return same instance if present)
Option.of("x").apply(System.out::println).and(() -> log.info("done"));
// Kotlin-style guards
Option.of("data").takeIf(s -> s.length() > 1); // else empty
Option.of("data").takeUnless(String::isBlank); // negated predicate
// Type checks
Option.of("x").isInstance(CharSequence.class); // true/false
Option.of("x").ifInstance(String.class).apply(System.out::println);
Option.of("x").ifInstance(CharSequence.class, cs -> System.out.println(cs.length()));
Option.of("x").ifInstance(String.class, () -> System.out.println("String detected"));
// Presence-based branching
Option.<String>empty().ifEmpty(() -> System.out.println("no value"));
Option.of("v").ifEmptyOrElse(
() -> System.out.println("empty"),
v -> System.out.println("got " + v)
);
// Fallbacks
Option.<String>empty().orElse("fallback"); // "fallback"
Option.<String>empty().orElseGet(() -> compute()); // lazy
Option.<String>empty().orElseThrow(() -> new IllegalStateException("nope"));Try<Integer> t = Option.of("N/A").runCatching(Integer::parseInt);
int n = t.onFailure(ex -> log.warn("bad number", ex))
.recover(ex -> 0); // 0Try<T> models a computation that either succeeds with a value or fails with an exception.
Try<Integer> ok = Try.success(10)
.map(x -> x * 2) // Success(20)
.onSuccess(v -> metrics.inc("ok"));
int n = Try.<Integer>failure(new IllegalStateException("boom"))
.map(x -> x + 1) // still Failure
.recover(ex -> 0); // 0
String msg = ok.fold(
ex -> "fail: " + ex.getMessage(),
v -> "ok: " + v
); // "ok: 20"
// Throw if needed:
int v = ok.getOrThrow();
// Interop:
ok.toOption(); // Option.of(20)
ok.toEither(); // Right(20) / Left(exception)Notes
Failure.equals(..)is value-based: exceptions are compared by class and message for predictable equality.- Checked exceptions in
getOrThrow()are wrapped inRuntimeException.
Any<L,R> is a classic sum type:
- Left(L) typically represents an error/diagnostic value.
- Right(R) is the successful branch.
Any<String, Integer> r = new Right<>(2)
.map(x -> x * 10) // Right(20)
.bimap(String::length, x -> x + 1); // Right(21)
Any<String, Integer> l = new Left<>("oops")
.map(x -> x * 10) // Left("oops")
.mapLeft(String::toUpperCase); // Left("OOPS")
r.exists(x -> x > 5); // true/false
r.filterOrElse(x -> x > 100, "too small"); // Left or Right
String folded = r.fold(
left -> "E: " + left,
right -> "R: " + right
);
Any<Integer,String> swapped = r.swap(); // Right<->Left swap
// Conversions
r.toOption(); // Right -> Option(value)
r.toOptional(); // Right -> java.util.OptionalKey functions
mapLeft(f)maps only the left branch.bimap(fLeft, fRight)maps both branches at once.joinLeft/Rightmerges twoAnyinstances by “pushing” the chosen branch forward.
| Kotlin | This library |
|---|---|
let |
map (or apply for side-effects) |
also |
apply |
run |
map, ifPresentOrElse |
takeIf |
Option.takeIf |
takeUnless |
Option.takeUnless |
Result<T> |
Try<T> |
Either<L,R> (Arrow) |
Any<L,R> (Left/Right) |
Optional usage |
Replace with |
|---|---|
Optional.ofNullable(v) |
Option.of(v) or Option.ofNullable(v) |
optional.isPresent() / .isEmpty() |
option.isPresent() / .isEmpty() |
optional.map(f) |
option.map(f) |
optional.flatMap(f) |
option.flatMap(f) |
optional.filter(p) |
option.filter(p) |
optional.orElse(x) / .orElseGet(s) |
option.orElse(x) / option.orElseGet(s) |
optional.orElseThrow(supplier) |
same in Option |
| — | Option.apply(Consumer) / and(Runnable) for scoped side-effects |
| — | Option.takeIf(p) / takeUnless(p) |
| — | Option.isInstance(Class) / ifInstance(...) / ifNotInstance(Class) |
optional.map(f).orElse(default) |
option.ifPresentOrElse(f, () -> default) (overload with Function + Supplier) |
| Vavr | This library |
|---|---|
io.vavr.control.Option |
org.bazhanov.option.Option |
Try (map, flatMap, recover, onSuccess) |
org.bazhanov.result.Try (same method names) |
Either<L,R> (mapLeft, bimap, swap) |
org.bazhanov.any.Any + Left/Right (same functions) |
Option.filter, peek |
Option.filter, apply |
Try.toEither() |
Try.toEither() (provided) |
| Arrow | This library |
|---|---|
Option (map, flatMap, filter, getOrElse) |
Option (same) |
Either<L,R> (map, mapLeft, bimap, swap) |
Any<L,R> + Left/Right |
Either.getOrElse {} |
either.fold(l -> fallback, r -> r) |
Result |
Try |
takeIf / takeUnless |
Option.takeIf / takeUnless |
- Option ≠ Try.
Optionis about presence, not failure. UseTryto carry exceptions. - Value-based
Failureequality. Equality compares exception class + message for deterministic behavior. - Immutability. All types are immutable; methods either return the same instance (when safe) or new values.
- Nulls only at the boundary. Internals use explicit emptiness (
empty) and explicit failures (Failure).
- Public APIs are covered with JUnit 5 + AssertJ.
- JaCoCo check is enforced in the build: 100% lines / 100% branches.
Local run:
mvn clean verify
# HTML report: target/site/jacoco/index.htmlMinimal workflow to build, test, and upload the JaCoCo HTML report:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Build & Test
run: mvn -B clean verify
- name: Upload JaCoCo HTML
if: always()
uses: actions/upload-artifact@v4
with:
name: jacoco-html
path: target/site/jacocoAdd a status badge pointing to your workflow if you want it in the header.
- Requires Java 17+ (tested on 17 and 21).
- Binary compatibility is maintained within minor versions whenever possible.
- Thread-safety: containers are immutable; thread safety of contained values is your responsibility.
MIT — see LICENSE.
- Construction:
of,ofNullable,empty - Presence:
get,isPresent/Empty/NotEmpty - Transforms:
map,flatMap,filter,mapTo - Side effects:
apply(Consumer),and(Runnable) - Guards:
takeIf,takeUnless - Type checks:
isInstance,ifInstance(Class),ifInstance(Class, Consumer),ifInstance(Class, Runnable),ifNotInstance - Branching:
ifPresent,ifEmpty,ifEmptyOrElse(Runnable, Consumer) - Ternary-like:
ifPresentOrElse(Function, Supplier)andifPresentOrElse(Supplier, Supplier) - Fallbacks:
orElse,orElseGet,orElseThrow - Interop:
toOptional,runCatching(ThrowingFunction) -> Try
- Construct:
success,failure - Inspect:
isSuccess/isFailure,getOrNull,exceptionOrNull - Transform:
map,flatMap - Side effects:
onSuccess,onFailure - Rescue:
getOrElse,recover - Collapse:
fold,getOrThrow - Interop:
toOption,toEither
- Types:
Left,Right - Inspect:
isLeft/isRight,getLeft/getRight,ifLeft/ifRight - Transform:
map,flatMap,mapLeft,bimap,swap - Logic:
filterOrElse,exists,fold,forEach - Combine:
joinLeft,joinRight - Interop:
toOption,toOptional