NonEmptyMap<K, V>

Runnable example: NonEmptyMapSample.java

What is NonEmptyMap<K, V>?

NonEmptyMap<K, V> is an immutable map guaranteed at construction time to contain at least one entry. The non-emptiness constraint is encoded in the static type — callers that receive a NonEmptyMap<K, V> never need to check whether it is empty.

Internal structure: a headKey + headValue (the first entry) and a tail (an unmodifiable Map<K, V> for the remaining entries, which may be empty). headKey() and headValue() are always present.

Null policy: NonEmptyMap is @NullMarked — null keys, values, or rest entries throw NullPointerException at construction time.

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

Use it when:

  • An API contract requires at least one map entry and you want the compiler to enforce it.
  • You are building a registry, configuration, or lookup table that must always have at least one entry.
  • You want to eliminate runtime isEmpty() guards on maps that are semantically never empty.

Creating instances

FactoryWhen input is empty?
NonEmptyMap.of(key, value, rest)N/A — head entry is always required
NonEmptyMap.singleton(key, value)N/A — always one entry
NonEmptyMap.fromMap(map)Returns None
// of(key, value, rest) — explicit head entry + additional entries
NonEmptyMap<String, Integer> nem = NonEmptyMap.of("alice", 10, Map.of("bob", 20, "carol", 30));
// {alice=10, bob=20, carol=30}
// Duplicate head key in rest is silently ignored — head value wins
NonEmptyMap<String, Integer> dedup = NonEmptyMap.of("alice", 10, Map.of("alice", 99, "bob", 20));
// {alice=10, bob=20} (alice=99 dropped)
// singleton — exactly one entry
NonEmptyMap<String, Integer> single = NonEmptyMap.singleton("alice", 10);
// {alice=10}
// fromMap — safe conversion from a plain Map; empty map returns None
Option<NonEmptyMap<String, Integer>> fromMap = NonEmptyMap.fromMap(someMap);
// Some({...}) or None

Accessing elements

NonEmptyMap<String, Integer> nem = NonEmptyMap.of("alice", 10, Map.of("bob", 20));
String headKey = nem.headKey(); // "alice" — always present, never null
Integer headValue = nem.headValue(); // 10 — always present, never null
int size = nem.size(); // 2 — always >= 1
// get — Option-based safe lookup
Option<Integer> found = nem.get("bob"); // Some(20)
Option<Integer> missing = nem.get("unknown"); // None
// containsKey
boolean hasAlice = nem.containsKey("alice"); // true
boolean hasDave = nem.containsKey("dave"); // false
// toMap — unmodifiable copy of all entries (not backed by internal storage)
Map<String, Integer> all = nem.toMap(); // {alice=10, bob=20}

size() always returns a value >= 1. headKey() and headValue() give direct, null-safe access to the first entry. get(key) returns Option<V> — no nulls, no NoSuchElementException.

Transformations

NonEmptyMap<String, Integer> nem = NonEmptyMap.of("alice", 10, Map.of("bob", 20));
// mapValues — transform every value; keys are unchanged
NonEmptyMap<String, String> labeled = nem.mapValues(v -> v + " pts");
// {alice="10 pts", bob="20 pts"}
// mapKeys — transform every key; values follow their original key
NonEmptyMap<String, Integer> upper = nem.mapKeys(String::toUpperCase);
// {ALICE=10, BOB=20}
// Colliding mapped keys: head key takes priority; conflicting tail entries are dropped
NonEmptyMap<String, Integer> collision = NonEmptyMap.of("a", 1, Map.of("A", 2));
NonEmptyMap<String, Integer> mapped = collision.mapKeys(String::toUpperCase);
// {A=1} (tail entry A=2 dropped because mapped key equals the new head key)

mapValues and mapKeys always return a new NonEmptyMap — the original is unchanged. When mapKeys produces collisions, the head key takes priority and conflicting tail entries are dropped.

Filter

NonEmptyMap<String, Integer> nem = NonEmptyMap.of("alice", 10, Map.of("bob", 5, "carol", 30));
// filter — predicate receives (key, value); returns Option because result may be empty
Option<NonEmptyMap<String, Integer>> highScorers = nem.filter((k, v) -> v >= 10);
// Some({alice=10, carol=30})
Option<NonEmptyMap<String, Integer>> none = nem.filter((k, v) -> v > 100);
// None
// filter on key
Option<NonEmptyMap<String, Integer>> startWithC = nem.filter((k, v) -> k.startsWith("c"));
// Some({carol=30})

filter returns Option<NonEmptyMap<K, V>> because filtering can produce an empty result. The predicate receives both the key and the value via BiPredicate<K, V>.

Merge

NonEmptyMap<String, Integer> scores1 = NonEmptyMap.of("alice", 10, Map.of("bob", 5));
NonEmptyMap<String, Integer> scores2 = NonEmptyMap.of("carol", 20, Map.of("bob", 15));
// merge — combine two maps; mergeFunction resolves conflicts
NonEmptyMap<String, Integer> combined = scores1.merge(scores2, Integer::sum);
// {alice=10, bob=20, carol=20} (bob: 5+15=20)
// merge with last-write-wins for conflicts
NonEmptyMap<String, Integer> lastWins = scores1.merge(scores2, (v1, v2) -> v2);
// {alice=10, bob=15, carol=20} (bob=15 from scores2)
// Disjoint maps — no conflict, all entries preserved
NonEmptyMap<String, Integer> a = NonEmptyMap.singleton("x", 1);
NonEmptyMap<String, Integer> b = NonEmptyMap.singleton("y", 2);
NonEmptyMap<String, Integer> merged = a.merge(b, Integer::sum);
// {x=1, y=2}

merge takes a second NonEmptyMap and a BinaryOperator<V> to resolve key conflicts. Because both inputs are non-empty, the result is guaranteed non-empty — no Option wrapping needed.

Interoperability

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

MethodReturnsNotes
keySet()NonEmptySet<K>All keys as a non-empty set; head key becomes set head
values()NonEmptyList<V>All values in insertion order; duplicates preserved
toMap()Map<K, V>Unmodifiable java.util.Map for standard API interop
toNonEmptyList()NonEmptyList<Map.Entry<K,V>>All entries in insertion order
fromMap(map)Option<NonEmptyMap<K,V>>Safe bridge from a plain Map; None if empty
NonEmptyMap<String, Integer> nem = NonEmptyMap.of("alice", 10, Map.of("bob", 20, "carol", 30));
// keySet() — all keys as a NonEmptySet; head key becomes set head
NonEmptySet<String> keys = nem.keySet();
// NonEmptySet["alice", "bob", "carol"]
keys.head(); // "alice"
keys.size(); // 3
// values() — all values as a NonEmptyList in insertion order; head value becomes list head
NonEmptyList<Integer> vals = nem.values();
// NonEmptyList[10, 20, 30]
vals.head(); // 10
vals.size(); // 3
// toMap() — unmodifiable java.util.Map for interop with standard APIs
Map<String, Integer> javaMap = nem.toMap();
// toNonEmptyList() — all entries as Map.Entry objects in insertion order
NonEmptyList<Map.Entry<String, Integer>> entries = nem.toNonEmptyList();
// [alice=10, bob=20, carol=30]
// fromMap() — bridge from plain Map (e.g. from external source)
Map<String, Integer> external = fetchScoresFromDatabase();
Option<NonEmptyMap<String, Integer>> safe = NonEmptyMap.fromMap(external);
safe.peek(scores -> processScores(scores));

equals, hashCode, and toString

Equality is structural and consistent with Map: two NonEmptyMap instances are equal if and only if their entry sets are equal (key equality + value equality, order ignored). hashCode delegates to toMap().hashCode(). toString prints in {key=value, ...} format.

When to use NonEmptyMap vs plain Map

ScenarioRecommendation
At least one entry required by contractNonEmptyMap<K, V>
Map may legitimately be emptyMap<K, V>
Reading from an external source that may be emptyNonEmptyMap.fromMap(map) → handle None
Merging two registries — result is always non-emptynem1.merge(nem2, mergeFunction)
Filtering entries — result may be emptynem.filter(predicate) → handle None

Real-world example

A role-to-permissions registry initialized with at least one role. The type system prevents constructing the service with an empty permissions map.

// A role-to-permissions registry that is always initialized with at least one entry.
// The compiler prevents calling this service with an empty map.
public class RolePermissionsService {
private final NonEmptyMap<String, Set<String>> permissions;
public RolePermissionsService(NonEmptyMap<String, Set<String>> permissions) {
this.permissions = permissions;
}
/** Returns the permissions for a role, or an empty set if the role is unknown. */
public Set<String> permissionsFor(String role) {
return permissions.get(role)
.getOrElse(Set.of());
}
/** Returns a new service with an additional role merged in. */
public RolePermissionsService withRole(String role, Set<String> perms) {
NonEmptyMap<String, Set<String>> updated =
permissions.merge(NonEmptyMap.singleton(role, perms), (existing, incoming) -> {
Set<String> merged = new HashSet<>(existing);
merged.addAll(incoming);
return Set.copyOf(merged);
});
return new RolePermissionsService(updated);
}
/** Returns only roles that have the given permission. */
public Option<NonEmptyMap<String, Set<String>>> rolesWithPermission(String permission) {
return permissions.filter((role, perms) -> perms.contains(permission));
}
}
// Usage
NonEmptyMap<String, Set<String>> initial = NonEmptyMap.of(
"admin", Set.of("read", "write", "delete"),
Map.of(
"editor", Set.of("read", "write"),
"viewer", Set.of("read")
)
);
RolePermissionsService svc = new RolePermissionsService(initial);
Set<String> editorPerms = svc.permissionsFor("editor"); // [read, write]
Option<NonEmptyMap<String, Set<String>>> canWrite = svc.rolesWithPermission("write");
// Some({admin=[read,write,delete], editor=[read,write]})