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

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

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
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 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"]
// 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

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]