Utilities for Collections

GitLab Docs Pipeline Coverage License
A Java module providing unmodifiable collection implementations with consistent behaviour guarantees, fluent builders, and useful utilities. All collections require non-null values (and keys), and the non-sorted map and set variants preserve insertion order. See morimekta.net/utils for procedures on releases.

Getting Started

To add to Maven, include this dependency in pom.xml:

<dependency>
    <groupId>net.morimekta.utils</groupId>
    <artifactId>collect</artifactId>
    <version>5.1.1</version>
</dependency>

To add to Gradle, include this in the dependencies block of build.gradle:

implementation 'net.morimekta.utils:collect:5.1.1'

Unmodifiable Collections

The core of this module is a set of unmodifiable collection classes, largely inspired by the immutable collections in Guava. Each collection type provides static factory methods (listOf, setOf, mapOf), as* conversion methods, stream collectors, and fluent builders.

  • UnmodifiableList -- An array-backed list that cannot be modified. Supports no-copy slicing through subList.
  • UnmodifiableSet -- A hash-table-backed set that cannot be modified. Preserves insertion order when iterating, similar to LinkedHashSet.
  • UnmodifiableMap -- A hash-table-backed map that cannot be modified. Preserves insertion order when iterating, similar to LinkedHashMap.
  • UnmodifiableSortedSet -- An array-backed sorted set implementing NavigableSet. Supports no-copy slicing for subSet, headSet, and tailSet.
  • UnmodifiableSortedMap -- An array-backed sorted map implementing NavigableMap. Supports no-copy slicing for subMap, headMap, and tailMap.

All collection builders accept duplicate values. Set builders silently ignore duplicates (keeping the first entry), and map builders overwrite the value for duplicate keys, matching the behaviour of standard Set and Map implementations.

Factory Methods and Builders

import net.morimekta.collect.UnmodifiableList;
import net.morimekta.collect.UnmodifiableMap;
import net.morimekta.collect.UnmodifiableSet;

import static net.morimekta.collect.UnmodifiableList.listOf;
import static net.morimekta.collect.UnmodifiableMap.mapOf;
import static net.morimekta.collect.UnmodifiableSet.setOf;

// Factory methods for small collections.
var names = listOf("Alice", "Bob", "Charlie");
var ids   = setOf(1, 2, 3);
var ages  = mapOf("Alice", 30, "Bob", 25);

// Builders for larger or dynamic collections.
var list = UnmodifiableList.<String>newBuilder()
        .add("first")
        .addAll(names)
        .build();

var map = UnmodifiableMap.<String, Integer>newBuilder()
        .put("Alice", 30)
        .put("Bob", 25)
        .build();

Stream Collectors

Each collection type provides a static toList(), toSet(), toMap(), or toSortedSet() collector for use with Java streams.

import java.util.List;
import java.util.Set;

import static net.morimekta.collect.UnmodifiableSortedSet.toSortedSet;

interface Example {
    String getName();

    static Set<String> getNames(List<Example> examples) {
        return examples.stream()
                       .map(Example::getName)
                       .collect(toSortedSet());
    }
}

Common Operations

The UnmodifiableCollection base class provides several convenience methods that return new unmodifiable collections: append, map, filtered, sorted, sortedBy, and reversed. Maps provide withEntry and withEntries for deriving new maps with additional entries.

import static net.morimekta.collect.UnmodifiableList.listOf;

var names = listOf("Charlie", "Alice", "Bob");

// Each operation returns a new unmodifiable collection.
var sorted   = names.sorted();                    // [Alice, Bob, Charlie]
var filtered = names.filtered(n -> n.length() > 3); // [Charlie, Alice]
var upper    = names.map(String::toUpperCase);    // [CHARLIE, ALICE, BOB]
var extended = names.append("Diana");             // [Charlie, Alice, Bob, Diana]

Extra Collectors

The ExtraCollectors utility class provides additional stream collectors for batching and grouping operations.

  • inBatchesOf(int) -- Splits the stream into a list of lists, each with at most the specified number of items.
  • inNumBatches(int) -- Splits the stream into a fixed number of lists of approximately equal size.
  • groupingBy(Function) -- Groups items by a key function, collecting values into lists. Unlike Collectors.groupingBy, this accepts duplicate keys gracefully.
  • groupingByAll(Function) -- Groups each item under all keys returned by the key function, so a single item can appear in multiple groups.
import java.util.List;

import static net.morimekta.collect.util.ExtraCollectors.inBatchesOf;
import static net.morimekta.collect.util.ExtraCollectors.groupingBy;

// Process items in batches of at most 10.
List<List<Job>> batches = jobs.stream()
        .collect(inBatchesOf(10));

// Group users by their role.
var byRole = users.stream()
        .collect(groupingBy(User::getRole));

Utilities

Binary

The Binary class wraps a byte array in an immutable container with a variety of convenience methods. It supports no-copy slicing, hex and base64 encoding and decoding, and conversion to and from ByteBuffer, BitSet, and streams.

import net.morimekta.collect.util.Binary;

// Parse from hex, then slice and convert.
var data  = Binary.fromHexString("deadbeef");
var slice = data.slice(1, 2);
var hex   = slice.toHexString(); // "adbe"
var b64   = data.toBase64();

// Read from an input stream.
var fromStream = Binary.read(inputStream, length);

Pair and Tuple

Pair<F, S> holds two typed values with equals, hashCode, and toString support. Tuple holds a fixed number of untyped, nullable values backed by an unmodifiable array.

import net.morimekta.collect.util.Pair;

var pair = Pair.pairOf("key", 42);
String key = pair.first;
int value  = pair.second;

LazyCachedSupplier

LazyCachedSupplier provides a lazily initialized, memoized value supplier. The value is computed on the first call to get() and cached for subsequent calls. Thread-safe, but may invoke the underlying supplier more than once under contention. Primitive-typed variants LazyCachedBoolean, LazyCachedInteger, and LazyCachedLong are also available.

import static net.morimekta.collect.util.LazyCachedSupplier.lazyCache;

var config = lazyCache(() -> loadExpensiveConfig());
// The config is not loaded until the first call to get().
var value = config.get();

SetOperations

The SetOperations utility class provides standard mathematical set operations: union, intersect, subtract, difference, and cartesianProduct. All operations preserve the set type: if the input is a SortedSet, the result will also be a sorted set.

import java.util.Set;

import static net.morimekta.collect.UnmodifiableSet.setOf;
import static net.morimekta.collect.util.SetOperations.*;

var a = setOf(1, 2, 3);
var b = setOf(2, 3, 4);

var u = union(a, b);      // {1, 2, 3, 4}
var i = intersect(a, b);  // {2, 3}
var s = subtract(a, b);   // {1}
var d = difference(a, b); // {1, 4}

SecureHashable

The SecureHashable interface defines a secureHashCode() method that produces a 64-bit hash with good numerical distribution across the full long range. Static helper methods are provided for hashing arbitrary objects, CharSequence values, and byte arrays.