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).
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 |
// 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 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 |
fromSet(set) | Option<NonEmptySet<T>> | Safe bridge from a plain Set; None if 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"]
// 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));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]