NonEmptySet<T>

Runnable example: NonEmptySetSample.java

What is NonEmptySet<T>?

NonEmptySet<T> is an immutable set guaranteed at construction time to contain at least one element. The non-emptiness constraint is encoded in the static type — callers that receive a NonEmptySet<T> never need to check whether it is empty.

Internal structure: a head (the first element) and a tail (an unmodifiable Set<T> for the remaining elements, which may be empty). head() is always present.

Null policy: NonEmptySet is @NullMarked — null elements throw NullPointerException at construction time.

Insertion order preserved: backed by LinkedHashSet, so iteration follows insertion order with the head element always first.

Implements Iterable<T>: use directly in for-each loops, or adapt to streams via StreamSupport.stream(nes.spliterator(), false).

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 need set semantics (no duplicates) with the additional guarantee of non-emptiness.
  • You want to eliminate isEmpty() guards on sets that are semantically never empty (e.g. a user’s assigned roles, a product’s tags).

Creating instances

FactoryWhen input is empty?
NonEmptySet.of(head, rest)N/A — head is always required
NonEmptySet.singleton(head)N/A — always one element
NonEmptySet.fromSet(set)Returns None
NonEmptySet.fromOptional(opt)Returns None
NonEmptySet.collector()Returns None
// of(head, rest) — explicit head element + additional elements
NonEmptySet<String> nes = NonEmptySet.of("admin", Set.of("user", "moderator"));
// [admin, user, moderator]
// Duplicate head in rest is silently ignored
NonEmptySet<String> dedup = NonEmptySet.of("admin", Set.of("admin", "user"));
// [admin, user]
// singleton — exactly one element
NonEmptySet<String> single = NonEmptySet.singleton("admin");
// [admin]
// fromSet — safe conversion from a plain Set; empty set returns None
Option<NonEmptySet<String>> fromSet = NonEmptySet.fromSet(someSet);
// Some([...]) or None
// fromOptional — bridge from Optional<T>; wraps the value as a singleton NonEmptySet
Optional<String> present = Optional.of("admin");
Option<NonEmptySet<String>> fromPresent = NonEmptySet.fromOptional(present);
// Some([admin])
Option<NonEmptySet<String>> fromEmpty = NonEmptySet.fromOptional(Optional.empty());
// None
// collector() — build NonEmptySet from a Stream; None if the stream is empty
List<String> roles = List.of("admin", "editor", "viewer", "editor"); // duplicate dropped
Option<NonEmptySet<String>> collected =
roles.stream().collect(NonEmptySet.collector());
// Some([admin, editor, viewer])
Option<NonEmptySet<String>> emptyCollected =
Stream.<String>empty().collect(NonEmptySet.collector());
// None

Accessing elements

// Use a LinkedHashSet to guarantee insertion order in the rest argument
Set<String> rest = new LinkedHashSet<>(List.of("user", "moderator"));
NonEmptySet<String> nes = NonEmptySet.of("admin", rest);
String head = nes.head(); // "admin" — always present, never null
int size = nes.size(); // 3 — always >= 1
// contains
boolean hasAdmin = nes.contains("admin"); // true
boolean hasUnknown = nes.contains("unknown"); // false
// toSet — unmodifiable copy of all elements in insertion order
Set<String> all = nes.toSet(); // [admin, user, moderator]
// Iterable — use directly in for-each loops
for (String role : nes) {
System.out.println(role);
}

size() always returns a value >= 1. head() gives direct, null-safe access to the first element. toSet() returns an unmodifiable java.util.Set for standard API interop.

Transformations

NonEmptySet<String> nes = NonEmptySet.of("admin", Set.of("user", "moderator"));
// map — transform every element; result is deduplicated, head always comes from mapping head
NonEmptySet<String> upper = nes.map(String::toUpperCase);
// [ADMIN, USER, MODERATOR]
// map with deduplication: "a" and "A" both map to "A" — only one "A" survives
NonEmptySet<String> collision = NonEmptySet.of("a", Set.of("A"));
NonEmptySet<String> mapped = collision.map(String::toUpperCase);
// [A] (size 1 — head "a" maps to "A", tail "A" is a duplicate and dropped)

map always returns a new NonEmptySet — the original is unchanged. Because mapping can produce duplicates, the result is automatically deduplicated; the head element always takes priority.

Filter

// Use a LinkedHashSet to guarantee insertion order in the rest argument
Set<Integer> rest = new LinkedHashSet<>(List.of(2, 3, 4, 5));
NonEmptySet<Integer> nes = NonEmptySet.of(1, rest);
// filter — returns Option because no element may pass
Option<NonEmptySet<Integer>> evens = nes.filter(n -> n % 2 == 0);
// Some([2, 4])
Option<NonEmptySet<Integer>> none = nes.filter(n -> n > 100);
// None
// All elements pass — same size as original
Option<NonEmptySet<Integer>> all = nes.filter(n -> n > 0);
// Some([1, 2, 3, 4, 5])

filter returns Option<NonEmptySet<T>> because filtering can produce an empty result. If no element passes the predicate, None is returned.

Set operations

NonEmptySet<String> a = NonEmptySet.of("admin", Set.of("user"));
NonEmptySet<String> b = NonEmptySet.of("user", Set.of("moderator"));
// union — always non-empty (both inputs are non-empty)
NonEmptySet<String> union = a.union(b);
// [admin, user, moderator]
// union with fully overlapping sets — deduplication applies
NonEmptySet<String> same = a.union(a);
// [admin, user]
// intersection — may be empty, so returns Option
Option<NonEmptySet<String>> common = a.intersection(Set.of("user", "moderator"));
// Some([user])
Option<NonEmptySet<String>> empty = a.intersection(Set.of("unknown"));
// None
// intersection accepts any java.util.Set, not just NonEmptySet
Set<String> external = fetchAllowedRoles();
Option<NonEmptySet<String>> allowed = a.intersection(external);

union always returns a non-empty set (both inputs are non-empty), so no Option wrapping is needed. intersection may yield an empty result, so it returns Option<NonEmptySet<T>>. intersection accepts any java.util.Set<? extends T>, not just NonEmptySet.

Interoperability

Cross-type conversions bridge NonEmptySet with NonEmptyMap, NonEmptyList, and the standard Java collections.

MethodReturnsNotes
toNonEmptyMap(valueMapper)NonEmptyMap<T, V>Elements become keys; mapper produces values
toNonEmptyList()NonEmptyList<T>Ordered snapshot; head element preserved
toSet()Set<T>Unmodifiable java.util.Set for standard API interop
toStream()Stream<T>Bridge to java.util.stream.Stream for pipeline operations
fromSet(set)Option<NonEmptySet<T>>Safe bridge from a plain Set; None if empty
fromOptional(optional)Option<NonEmptySet<T>>Wraps the present value as singleton; None if empty
collector()Collector<T, ?, Option<NonEmptySet<T>>>Build from a Stream; None if stream is empty
NonEmptySet<String> nes = NonEmptySet.of("admin", Set.of("editor", "viewer"));
// toNonEmptyMap(valueMapper) — each element becomes a key; mapper produces values
NonEmptyMap<String, Integer> permCount = nes.toNonEmptyMap(role -> loadPermissions(role).size());
// NonEmptyMap{admin=3, editor=2, viewer=1}
permCount.headKey(); // "admin"
permCount.headValue(); // 3
// Identity map (element → itself)
NonEmptyMap<String, String> identity = nes.toNonEmptyMap(r -> r);
// {admin=admin, editor=editor, viewer=viewer}
// toSet() — unmodifiable java.util.Set for interop with standard APIs
Set<String> javaSet = nes.toSet();
// toNonEmptyList() — ordered snapshot of elements as a NonEmptyList
NonEmptyList<String> nel = nes.toNonEmptyList();
// NonEmptyList["admin", "editor", "viewer"]
// toStream() — bridge to java.util.stream.Stream for pipeline operations
nes.toStream()
.filter(role -> !role.equals("viewer"))
.forEach(System.out::println);
// admin
// editor
// fromSet() — bridge from plain Set (e.g. from external source)
Set<String> external = fetchRolesFromDatabase();
Option<NonEmptySet<String>> safe = NonEmptySet.fromSet(external);
safe.peek(roles -> processRoles(roles));
// fromOptional() — bridge from Optional<T>; wraps the value as a singleton NonEmptySet
Optional<String> maybeRole = Optional.of("admin");
Option<NonEmptySet<String>> fromOpt = NonEmptySet.fromOptional(maybeRole);
// Some([admin])
Option<NonEmptySet<String>> fromEmptyOpt = NonEmptySet.fromOptional(Optional.empty());
// None
// collector() — build NonEmptySet from a Stream; None if the stream is empty
List<String> permissions = List.of("read", "write", "read"); // duplicate "read" dropped
Option<NonEmptySet<String>> perms =
permissions.stream().collect(NonEmptySet.collector());
// Some([read, write])
Option<NonEmptySet<String>> emptyPerms =
Stream.<String>empty().collect(NonEmptySet.collector());
// None

Interoperability — Option

sequenceOption turns a NonEmptySet<Option<T>> into an Option<NonEmptySet<T>>. It returns Some only when every element is Some; the first None short-circuits and returns None.

// sequenceOption: NonEmptySet<Option<T>> → Option<NonEmptySet<T>>
// Some if every element is Some; None as soon as any element is None (fail-fast in inspection).
NonEmptySet<Option<String>> allPresent = NonEmptySet.of(
Option.some("admin"), Set.of(Option.some("editor"), Option.some("viewer")));
Option<NonEmptySet<String>> result = NonEmptySet.sequenceOption(allPresent);
// Some([admin, editor, viewer])
NonEmptySet<Option<String>> withAbsent = NonEmptySet.of(
Option.some("admin"), Set.of(Option.none(), Option.some("viewer")));
Option<NonEmptySet<String>> missing = NonEmptySet.sequenceOption(withAbsent);
// None

Interoperability — Try, Either, Result

sequenceTry, sequenceEither, and sequenceResult follow the same fail-fast-in-inspection pattern: the method stops iterating as soon as it encounters a failure, Left, or Err.

// sequenceTry: NonEmptySet<Try<T>> → Try<NonEmptySet<T>>
// Success if every element succeeds; Failure from the first failing element.
NonEmptySet<Try<String>> tries = NonEmptySet.of(
Try.success("admin"), Set.of(Try.success("editor"), Try.success("viewer")));
Try<NonEmptySet<String>> allOk = NonEmptySet.sequenceTry(tries);
// Success([admin, editor, viewer])
NonEmptySet<Try<String>> withFailure = NonEmptySet.of(
Try.failure(new RuntimeException("boom")), Set.of(Try.success("editor")));
Try<NonEmptySet<String>> failed = NonEmptySet.sequenceTry(withFailure);
// Failure(RuntimeException("boom"))
// sequenceEither: NonEmptySet<Either<E, T>> → Either<E, NonEmptySet<T>>
// Right if every element is Right; Left from the first Left element.
NonEmptySet<Either<String, Integer>> eithers = NonEmptySet.of(
Either.right(10), Set.of(Either.right(20), Either.right(30)));
Either<String, NonEmptySet<Integer>> allRight = NonEmptySet.sequenceEither(eithers);
// Right([10, 20, 30])
NonEmptySet<Either<String, Integer>> withLeft = NonEmptySet.of(
Either.left("invalid"), Set.of(Either.right(20)));
Either<String, NonEmptySet<Integer>> firstLeft = NonEmptySet.sequenceEither(withLeft);
// Left("invalid")
// sequenceResult: NonEmptySet<Result<T, E>> → Result<NonEmptySet<T>, E>
// Ok if every element is Ok; Err from the first error element.
NonEmptySet<Result<Integer, String>> results = NonEmptySet.of(
Result.ok(10), Set.of(Result.ok(20), Result.ok(30)));
Result<NonEmptySet<Integer>, String> allOkResult = NonEmptySet.sequenceResult(results);
// Ok([10, 20, 30])
NonEmptySet<Result<Integer, String>> withErr = NonEmptySet.of(
Result.err("not found"), Set.of(Result.ok(20)));
Result<NonEmptySet<Integer>, String> firstErr = NonEmptySet.sequenceResult(withErr);
// Err("not found")

equals, hashCode, and toString

Equality is structural and consistent with Set: two NonEmptySet instances are equal if and only if their element sets are equal (order ignored). hashCode delegates to toSet().hashCode(). toString prints in [element, ...] format.

When to use NonEmptySet vs plain Set

ScenarioRecommendation
At least one element required by contractNonEmptySet<T>
Set may legitimately be emptySet<T>
Reading from an external source that may be emptyNonEmptySet.fromSet(set) → handle None
Combining two non-empty sets — result always non-emptynes1.union(nes2)
Finding common elements — result may be emptynes.intersection(other) → handle None

Real-world example

An access-control service where every user must have at least one role. The type system prevents creating a user with no roles.

// An access-control service where every user must have at least one role.
// The compiler prevents creating a user with no roles.
public record UserAccess(String username, NonEmptySet<String> roles) {}
public class AccessControlService {
/** Checks whether the user has ALL of the required roles. */
public boolean hasAllRoles(UserAccess user, Set<String> required) {
return user.roles().intersection(required)
.map(common -> common.size() == required.size())
.getOrElse(false);
}
/** Checks whether the user has ANY of the required roles. */
public boolean hasAnyRole(UserAccess user, NonEmptySet<String> required) {
return user.roles().intersection(required.toSet())
.isDefined();
}
/** Combines two users' roles (e.g. for a temporary delegation). */
public NonEmptySet<String> combinedRoles(UserAccess a, UserAccess b) {
return a.roles().union(b.roles());
}
/** Promotes a user by adding a new role. */
public UserAccess addRole(UserAccess user, String newRole) {
NonEmptySet<String> updatedRoles = user.roles().union(NonEmptySet.singleton(newRole));
return new UserAccess(user.username(), updatedRoles);
}
}
// Usage
UserAccess alice = new UserAccess("alice", NonEmptySet.of("admin", Set.of("editor")));
UserAccess bob = new UserAccess("bob", NonEmptySet.singleton("viewer"));
AccessControlService acl = new AccessControlService();
boolean aliceCanEdit = acl.hasAnyRole(alice, NonEmptySet.singleton("editor")); // true
NonEmptySet<String> allRoles = acl.combinedRoles(alice, bob);
// [admin, editor, viewer]