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

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
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

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

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.

// 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 / 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 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);
}