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.
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 |
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
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
// Singleton: tail is emptyNonEmptyList<String> one = NonEmptyList.singleton("only");one.head(); // "only"one.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.
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.
// 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 — 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 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);}