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

FactoryWhen 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-guarded
NonEmptyList<String> nel = NonEmptyList.of("a", List.of("b", "c"));
// ["a", "b", "c"]
// singleton — exactly one element
NonEmptyList<String> single = NonEmptyList.singleton("only");
// ["only"]
// fromList — safe conversion from a plain List; empty list returns None
Option<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());
// None

Accessing elements

MethodReturnsNotes
head()First elementCore accessor — always present
getFirst()First elementSequencedCollection alias for head()
getLast()Last elementAlways 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 >= 1Always at least 1
NonEmptyList<String> nel = NonEmptyList.of("a", List.of("b", "c"));
String head = nel.head(); // "a" — always present, never null
List<String> tail = nel.tail(); // ["b", "c"] — unmodifiable, may be empty
List<String> all = nel.toList(); // ["a", "b", "c"] — all elements
int 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 empty
NonEmptyList<String> one = NonEmptyList.singleton("only");
one.head(); // "only"
one.getFirst(); // "only"
one.getLast(); // "only" — same element, no IndexOutOfBoundsException risk
one.tail(); // [] (empty unmodifiable list)
one.size(); // 1

size() 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 NonEmptyList
NonEmptyList<String> upper = nel.map(String::toUpperCase);
// ["A", "B", "C"]
// append — add an element at the end
NonEmptyList<String> appended = nel.append("d");
// ["a", "b", "c", "d"]
// prepend — add an element at the front
NonEmptyList<String> prepended = nel.prepend("z");
// ["z", "a", "b", "c"]
// concat — combine two NonEmptyLists
NonEmptyList<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 needed
int first = nel.getFirst(); // 1
int last = nel.getLast(); // 5
// reversed() — returns a new NonEmptyList in reverse order
NonEmptyList<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.SequencedCollection
SequencedCollection<Integer> sc = nel;
sc.getFirst(); // 1
sc.getLast(); // 5
// Mutating methods always throw — NonEmptyList is immutable
try { 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 log
NonEmptyList<AuditEvent> log = auditService.getEvents(userId); // guaranteed non-empty
AuditEvent 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 → None
Option<NonEmptyList<String>> noTags =
Stream.<String>empty()
.collect(NonEmptyList.toNonEmptyList());
// None
// Combine with other stream operations
Option<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 → None
Option<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.

MethodInputReturns
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 / FactoryPurpose
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 NonEmptyList
Validated<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 errors
Validated<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 in
List<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 errors
List<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 list
List<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 Set
Set<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 retained
NonEmptySet<String> nes = nel.toNonEmptySet();
// NonEmptySet["admin", "editor", "viewer"] (duplicate "admin" dropped)
nes.head(); // "admin"
nes.size(); // 3 (not 4)
// Singleton list — yields singleton set
NonEmptySet<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.equals
NonEmptyList<String> a = NonEmptyList.of("x", List.of("y", "z"));
NonEmptyList<String> b = NonEmptyList.of("x", List.of("y", "z"));
boolean same = a.equals(b); // true
int hash = a.hashCode(); // consistent with equals
// toString delegates to List.toString
String 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

ScenarioRecommendation
At least one element required by contractNonEmptyList<T>
Collection may legitimately be emptyList<T>
Accumulating validation errorsValidated<NonEmptyList<E>, A>
Reading from a source that could be emptyNonEmptyList.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);
}