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
| Factory | When 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 elementsNonEmptySet<String> nes = NonEmptySet.of("admin", Set.of("user", "moderator"));// [admin, user, moderator]
// Duplicate head in rest is silently ignoredNonEmptySet<String> dedup = NonEmptySet.of("admin", Set.of("admin", "user"));// [admin, user]
// singleton — exactly one elementNonEmptySet<String> single = NonEmptySet.singleton("admin");// [admin]
// fromSet — safe conversion from a plain Set; empty set returns NoneOption<NonEmptySet<String>> fromSet = NonEmptySet.fromSet(someSet);// Some([...]) or None
// fromOptional — bridge from Optional<T>; wraps the value as a singleton NonEmptySetOptional<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 emptyList<String> roles = List.of("admin", "editor", "viewer", "editor"); // duplicate droppedOption<NonEmptySet<String>> collected = roles.stream().collect(NonEmptySet.collector());// Some([admin, editor, viewer])
Option<NonEmptySet<String>> emptyCollected = Stream.<String>empty().collect(NonEmptySet.collector());// NoneAccessing elements
// Use a LinkedHashSet to guarantee insertion order in the rest argumentSet<String> rest = new LinkedHashSet<>(List.of("user", "moderator"));NonEmptySet<String> nes = NonEmptySet.of("admin", rest);
String head = nes.head(); // "admin" — always present, never nullint size = nes.size(); // 3 — always >= 1
// containsboolean hasAdmin = nes.contains("admin"); // trueboolean hasUnknown = nes.contains("unknown"); // false
// toSet — unmodifiable copy of all elements in insertion orderSet<String> all = nes.toSet(); // [admin, user, moderator]
// Iterable — use directly in for-each loopsfor (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 headNonEmptySet<String> upper = nes.map(String::toUpperCase);// [ADMIN, USER, MODERATOR]
// map with deduplication: "a" and "A" both map to "A" — only one "A" survivesNonEmptySet<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 argumentSet<Integer> rest = new LinkedHashSet<>(List.of(2, 3, 4, 5));NonEmptySet<Integer> nes = NonEmptySet.of(1, rest);
// filter — returns Option because no element may passOption<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 originalOption<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 appliesNonEmptySet<String> same = a.union(a);// [admin, user]
// intersection — may be empty, so returns OptionOption<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 NonEmptySetSet<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.
| Method | Returns | Notes |
|---|---|---|
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 valuesNonEmptyMap<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 APIsSet<String> javaSet = nes.toSet();
// toNonEmptyList() — ordered snapshot of elements as a NonEmptyListNonEmptyList<String> nel = nes.toNonEmptyList();// NonEmptyList["admin", "editor", "viewer"]
// toStream() — bridge to java.util.stream.Stream for pipeline operationsnes.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 NonEmptySetOptional<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 emptyList<String> permissions = List.of("read", "write", "read"); // duplicate "read" droppedOption<NonEmptySet<String>> perms = permissions.stream().collect(NonEmptySet.collector());// Some([read, write])
Option<NonEmptySet<String>> emptyPerms = Stream.<String>empty().collect(NonEmptySet.collector());// NoneInteroperability — 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);// NoneInteroperability — 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
| Scenario | Recommendation |
|---|---|
| At least one element required by contract | NonEmptySet<T> |
| Set may legitimately be empty | Set<T> |
| Reading from an external source that may be empty | NonEmptySet.fromSet(set) → handle None |
| Combining two non-empty sets — result always non-empty | nes1.union(nes2) |
| Finding common elements — result may be empty | nes.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); }}
// UsageUserAccess 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")); // trueNonEmptySet<String> allRoles = acl.combinedRoles(alice, bob);// [admin, editor, viewer]