NonEmptyList<T>
Runnable example:
NonEmptyListSample.java
What is NonEmptyList<T>?
NonEmptyList<T> is an immutable list guaranteed at construction time to contain
at least one element. The non-emptiness constraint is encoded in the static type —
callers that receive a NonEmptyList<T> never need to check whether it is empty.
Internal structure: a head (the first element) and a tail (an unmodifiable
List<T> for the remaining elements, which may be empty). head() is always
present.
Null policy: NonEmptyList is @NullMarked — null elements throw
NullPointerException at construction time.
NonEmptyList<T> pairs naturally with Validated error accumulation: a
Validated.Invalid always carries at least one error, so
Validated<NonEmptyList<E>, A> is the idiomatic type for accumulating validation
errors without losing any of them.
The design rationale — why a structural head+tail representation was chosen over runtime checks or existing JDK types — is documented in ADR-018 — NonEmptyList<T>, NonEmptySet<T>, NonEmptyMap<K,V> as structural guarantee types.
Use it when:
- An API contract requires at least one element and you want the compiler to enforce it.
- You are accumulating validation errors and need a structure that is never empty.
- You want to pass a non-empty collection without repeatedly guarding against empty lists.
Creating instances
| Factory | When input is empty? |
|---|---|
NonEmptyList.of(head, tail) | N/A — head is always required |
NonEmptyList.singleton(head) | N/A — always one element |
NonEmptyList.fromList(list) | Returns None |
NonEmptyList.fromOptional(optional) | Returns None |
stream.collect(NonEmptyList.collector()) | Returns None |
stream.collect(NonEmptyList.toNonEmptyList()) | Returns None |
// of(head, tail) — explicit head + tail; all elements null-guardedNonEmptyList<String> nel = NonEmptyList.of("a", List.of("b", "c"));// ["a", "b", "c"]
// singleton — exactly one elementNonEmptyList<String> single = NonEmptyList.singleton("only");// ["only"]
// fromList — safe conversion from a plain List; empty list returns NoneOption<NonEmptyList<String>> fromList = NonEmptyList.fromList(someList);
// collector — accumulate a Stream into Option<NonEmptyList<T>>Option<NonEmptyList<String>> fromStream = Stream.of("x", "y", "z").collect(NonEmptyList.collector());// Some(["x", "y", "z"])
Option<NonEmptyList<String>> empty = Stream.<String>empty().collect(NonEmptyList.collector());// NoneAccessing elements
| Method | Returns | Notes |
|---|---|---|
head() | First element | Core accessor — always present |
getFirst() | First element | SequencedCollection alias for head() |
getLast() | Last element | Always safe — no index arithmetic required |
tail() | List<T> | Remaining elements; unmodifiable; may be empty |
toList() | List<T> | All elements as an unmodifiable list |
size() | int >= 1 | Always at least 1 |
NonEmptyList<String> nel = NonEmptyList.of("a", List.of("b", "c"));
String head = nel.head(); // "a" — always present, never nullList<String> tail = nel.tail(); // ["b", "c"] — unmodifiable, may be emptyList<String> all = nel.toList(); // ["a", "b", "c"] — all elementsint size = nel.size(); // 3 — always >= 1
// SequencedCollection API (Java 21)String first = nel.getFirst(); // "a" — alias for head()String last = nel.getLast(); // "c" — always safe, no emptiness check needed
// Singleton: tail is emptyNonEmptyList<String> one = NonEmptyList.singleton("only");one.head(); // "only"one.getFirst(); // "only"one.getLast(); // "only" — same element, no IndexOutOfBoundsException riskone.tail(); // [] (empty unmodifiable list)one.size(); // 1size() always returns a value >= 1. tail() returns an unmodifiable List
that may be empty (for singletons).
Transformations
NonEmptyList<String> nel = NonEmptyList.of("a", List.of("b", "c"));
// map — apply a function to every element; returns a new NonEmptyListNonEmptyList<String> upper = nel.map(String::toUpperCase);// ["A", "B", "C"]
// append — add an element at the endNonEmptyList<String> appended = nel.append("d");// ["a", "b", "c", "d"]
// prepend — add an element at the frontNonEmptyList<String> prepended = nel.prepend("z");// ["z", "a", "b", "c"]
// concat — combine two NonEmptyListsNonEmptyList<String> other = NonEmptyList.of("x", List.of("y"));NonEmptyList<String> merged = nel.concat(other);// ["a", "b", "c", "x", "y"]All transformation methods (map, append, prepend, concat) return a new
NonEmptyList — the original is unchanged.
SequencedCollection integration (Java 21)
NonEmptyList<T> implements java.util.SequencedCollection<T>, the Java 21
interface that formalises a collection with a defined encounter order and
well-known first and last elements.
Because NonEmptyList is always non-empty, getFirst() and getLast() are
total functions — they never throw NoSuchElementException.
reversed() returns a new NonEmptyList with elements in reverse order;
the original is not modified.
All mutating methods always throw UnsupportedOperationException because
NonEmptyList is immutable. This covers both the SequencedCollection mutators
(addFirst, addLast, removeFirst, removeLast) and the inherited Collection
mutators (add, remove, addAll, removeAll, retainAll, removeIf, clear).
NonEmptyList<Integer> nel = NonEmptyList.of(1, List.of(2, 3, 4, 5));
// getFirst() / getLast() — always safe, no emptiness check neededint first = nel.getFirst(); // 1int last = nel.getLast(); // 5
// reversed() — returns a new NonEmptyList in reverse orderNonEmptyList<Integer> rev = nel.reversed();// rev.toList() → [5, 4, 3, 2, 1]// original unchanged → nel.toList() → [1, 2, 3, 4, 5]
// Works as a java.util.SequencedCollectionSequencedCollection<Integer> sc = nel;sc.getFirst(); // 1sc.getLast(); // 5
// Mutating methods always throw — NonEmptyList is immutabletry { nel.addFirst(0); } catch (UnsupportedOperationException e) { /* expected */ }try { nel.addLast(6); } catch (UnsupportedOperationException e) { /* expected */ }try { nel.removeFirst(); } catch (UnsupportedOperationException e) { /* expected */ }try { nel.removeLast(); } catch (UnsupportedOperationException e) { /* expected */ }
// Real-world: show the most recent and oldest event in a guaranteed-non-empty audit logNonEmptyList<AuditEvent> log = auditService.getEvents(userId); // guaranteed non-emptyAuditEvent newest = log.getFirst();AuditEvent oldest = log.getLast();Collecting a stream — toNonEmptyList
NonEmptyList.toNonEmptyList() is an alias for collector() that reads naturally
at stream call sites. It returns Some(NonEmptyList) for a non-empty stream and
None for an empty stream.
// Non-empty stream → Some(NonEmptyList)Option<NonEmptyList<String>> tags = Stream.of("java", "fp", "dmx-fun") .filter(t -> t.length() > 2) .collect(NonEmptyList.toNonEmptyList());// Some(["java", "dmx-fun"]) // "fp".length() == 2, filtered out by > 2
// Empty stream → NoneOption<NonEmptyList<String>> noTags = Stream.<String>empty() .collect(NonEmptyList.toNonEmptyList());// None
// Combine with other stream operationsOption<NonEmptyList<Integer>> evens = IntStream.rangeClosed(1, 10) .boxed() .filter(n -> n % 2 == 0) .collect(NonEmptyList.toNonEmptyList());// Some([2, 4, 6, 8, 10])Interoperability — Option
sequence and collector bridge between NonEmptyList and Option.
// fromOptional: Optional<T> → Option<NonEmptyList<T>>// Present → Some(singleton); empty → NoneOption<NonEmptyList<String>> fromPresent = NonEmptyList.fromOptional(Optional.of("hello"));// Some(["hello"])
Option<NonEmptyList<String>> fromEmpty = NonEmptyList.fromOptional(Optional.empty());// None
// sequence: NonEmptyList<Option<T>> → Option<NonEmptyList<T>>// Returns Some if every element is Some; None as soon as any element is None.NonEmptyList<Option<Integer>> opts = NonEmptyList.of(Option.some(1), List.of(Option.some(2), Option.some(3)));
Option<NonEmptyList<Integer>> result = NonEmptyList.sequence(opts);// Some([1, 2, 3])
NonEmptyList<Option<Integer>> withNone = NonEmptyList.of(Option.some(1), List.of(Option.none(), Option.some(3)));
Option<NonEmptyList<Integer>> missing = NonEmptyList.sequence(withNone);// None
// collector: Stream<T> → Option<NonEmptyList<T>>Option<NonEmptyList<String>> fromStream = Stream.of("hello", "world").collect(NonEmptyList.collector());// Some(["hello", "world"])Interoperability — Try, Either, Result
sequenceTry, sequenceEither, and sequenceResult each convert a
NonEmptyList of wrapped values into a single wrapped NonEmptyList.
All three are fail-fast: they return the first failure and stop processing.
| Method | Input | Returns |
|---|---|---|
NonEmptyList.sequenceTry(nel) | NonEmptyList<Try<T>> | Try<NonEmptyList<T>> |
NonEmptyList.sequenceEither(nel) | NonEmptyList<Either<E, T>> | Either<E, NonEmptyList<T>> |
NonEmptyList.sequenceResult(nel) | NonEmptyList<Result<T, E>> | Result<NonEmptyList<T>, E> |
Use Validated.sequenceNel when you need error accumulation (all errors collected);
use these when you need fail-fast behaviour (stop at the first failure).
// sequenceTry: NonEmptyList<Try<T>> → Try<NonEmptyList<T>>// Success if every element succeeds; Failure from the first failing element.NonEmptyList<Try<Integer>> tries = NonEmptyList.of( Try.success(1), List.of(Try.success(2), Try.success(3)));
Try<NonEmptyList<Integer>> allOk = NonEmptyList.sequenceTry(tries);// Success([1, 2, 3])
NonEmptyList<Try<Integer>> withFailure = NonEmptyList.of( Try.success(1), List.of(Try.failure(new RuntimeException("boom")), Try.success(3)));
Try<NonEmptyList<Integer>> failed = NonEmptyList.sequenceTry(withFailure);// Failure(RuntimeException("boom"))
// sequenceEither: NonEmptyList<Either<E, T>> → Either<E, NonEmptyList<T>>// right if every element is right; left from the first left element.NonEmptyList<Either<String, Integer>> eithers = NonEmptyList.of( Either.right(1), List.of(Either.right(2), Either.right(3)));
Either<String, NonEmptyList<Integer>> allRight = NonEmptyList.sequenceEither(eithers);// right([1, 2, 3])
NonEmptyList<Either<String, Integer>> withLeft = NonEmptyList.of( Either.right(1), List.of(Either.left("invalid"), Either.right(3)));
Either<String, NonEmptyList<Integer>> firstLeft = NonEmptyList.sequenceEither(withLeft);// left("invalid")
// sequenceResult: NonEmptyList<Result<T, E>> → Result<NonEmptyList<T>, E>// ok if every element is ok; err from the first error element.NonEmptyList<Result<Integer, String>> results = NonEmptyList.of( Result.ok(1), List.of(Result.ok(2), Result.ok(3)));
Result<NonEmptyList<Integer>, String> allOkResult = NonEmptyList.sequenceResult(results);// ok([1, 2, 3])
NonEmptyList<Result<Integer, String>> withErr = NonEmptyList.of( Result.ok(1), List.of(Result.err("not found"), Result.ok(3)));
Result<NonEmptyList<Integer>, String> firstErr = NonEmptyList.sequenceResult(withErr);// err("not found")Interoperability — Validated and error accumulation
NonEmptyList<E> is the canonical error carrier for Validated. The library
provides three dedicated helpers:
| Method / Factory | Purpose |
|---|---|
Validated.invalidNel(error) | Wrap one error in a singleton NEL |
Validated.sequenceNel(iterable) | Sequence Iterable<Validated<NEL<E>,A>>, concat errors |
Validated.traverseNel(iterable, validator) | Validate each element, accumulate all errors |
// invalidNel — wrap a single error in a singleton NonEmptyListValidated<NonEmptyList<String>, Email> emailV = Validated.invalidNel("email is required");Validated<NonEmptyList<String>, Password> passwordV = Validated.invalidNel("password too short");
// combine — use NonEmptyList::concat as the error merger to accumulate all errorsValidated<NonEmptyList<String>, Form> form = emailV.combine(passwordV, NonEmptyList::concat, Form::new);// Invalid(["email is required", "password too short"])
// sequenceNel — sequence a list of Validated<NEL<E>, A> with concat built inList<Validated<NonEmptyList<String>, Integer>> list = List.of( Validated.valid(1), Validated.invalidNel("too small"), Validated.invalidNel("out of range"));
Validated<NonEmptyList<String>, List<Integer>> allErrors = Validated.sequenceNel(list);// Invalid(["too small", "out of range"])
// traverseNel — validate each element of a collection and accumulate errorsList<String> inputs = List.of("42", "bad", "-1");
Validated<NonEmptyList<String>, List<Integer>> result = Validated.traverseNel(inputs, s -> { try { int n = Integer.parseInt(s); return n > 0 ? Validated.valid(n) : Validated.invalidNel("must be positive: " + s); } catch (NumberFormatException e) { return Validated.invalidNel("not a number: " + s); } });// Invalid(["not a number: bad", "must be positive: -1"])Stream and Iterable interop
NonEmptyList<T> implements SequencedCollection<T> (which extends Collection<T>
and Iterable<T>) and provides toStream() for integration with the standard Java
stream API.
NonEmptyList<String> nel = NonEmptyList.of("banana", List.of("apple", "cherry", "apricot"));
// toStream() — sequential Stream without materializing an intermediate listList<String> aFruits = nel.toStream() .filter(s -> s.startsWith("a")) .sorted() .collect(Collectors.toList());// ["apple", "apricot"]
// Use in for-each (Iterable)for (String fruit : nel) { System.out.println(fruit);}
// Convert to SetSet<String> set = nel.toStream().collect(Collectors.toSet());Interoperability — NonEmptySet and NonEmptyMap
toNonEmptySet() is the complementary conversion to NonEmptySet.toNonEmptyList().
Note that it is not a strict inverse: converting a list to a set deduplicates
elements, so a round-trip through toNonEmptySet().toNonEmptyList() may produce a
shorter list than the original.
NonEmptyList<String> nel = NonEmptyList.of("admin", List.of("editor", "admin", "viewer"));
// toNonEmptySet() — deduplicate while preserving head; insertion order retainedNonEmptySet<String> nes = nel.toNonEmptySet();// NonEmptySet["admin", "editor", "viewer"] (duplicate "admin" dropped)nes.head(); // "admin"nes.size(); // 3 (not 4)
// Singleton list — yields singleton setNonEmptySet<String> single = NonEmptyList.singleton("admin").toNonEmptySet();single.size(); // 1
// Round-trip: NonEmptySet → NonEmptyList → NonEmptySet (idempotent)NonEmptySet<String> roles = NonEmptySet.of("admin", Set.of("editor"));NonEmptySet<String> roundTrip = roles.toNonEmptyList().toNonEmptySet();assertThat(roundTrip).isEqualTo(roles);equals, hashCode, and toString
// Equality is structural — consistent with List.equalsNonEmptyList<String> a = NonEmptyList.of("x", List.of("y", "z"));NonEmptyList<String> b = NonEmptyList.of("x", List.of("y", "z"));
boolean same = a.equals(b); // trueint hash = a.hashCode(); // consistent with equals
// toString delegates to List.toStringString repr = a.toString(); // "[x, y, z]"Equality is structural and consistent with List: two NonEmptyList instances
are equal if and only if their element sequences are equal.
When to use NonEmptyList vs plain List
| Scenario | Recommendation |
|---|---|
| At least one element required by contract | NonEmptyList<T> |
| Collection may legitimately be empty | List<T> |
| Accumulating validation errors | Validated<NonEmptyList<E>, A> |
| Reading from a source that could be empty | NonEmptyList.fromList(list) → handle None |
| Stream whose emptiness depends on runtime data | .collect(NonEmptyList.collector()) |
Real-world example
Validating a registration form with full error accumulation — every field is validated independently and all errors are returned together.
// Validate all fields and accumulate every error — no short-circuiting.// invalidNel wraps a single error; NonEmptyList::concat merges two NELs.
public Validated<NonEmptyList<String>, RegistrationForm> validate(RawInput input) {
Validated<NonEmptyList<String>, Email> emailV = Email.parse(input.email()) .map(Validated::<NonEmptyList<String>, Email>valid) .getOrElseGet(() -> Validated.invalidNel("email is invalid"));
Validated<NonEmptyList<String>, String> nameV = input.name().isBlank() ? Validated.invalidNel("name must not be blank") : Validated.valid(input.name().trim());
Validated<NonEmptyList<String>, Password> passwordV = input.password().length() >= 8 ? Validated.valid(Password.of(input.password())) : Validated.invalidNel("password must be at least 8 characters");
return emailV .combine(nameV, NonEmptyList::concat, (e, n) -> new Partial(e, n)) .combine(passwordV, NonEmptyList::concat, (p, pass) -> new RegistrationForm(p.email(), p.name(), pass));}
// Caller receives all errors at once, never just the first one:switch (validate(input)) { case Validated.Valid<?, RegistrationForm>(var form) -> register(form); case Validated.Invalid<NonEmptyList<String>, ?>(var errs) -> errs.toList().forEach(System.err::println);}