Skip to content
A universal facade for JDK 9+ API, focused on Collection factory methods and Collector providers.
Java Groovy Kotlin
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.

UniJ: Universal JDK 9+ API Facade

Build (Linux) Build (Windows) Code coverage Codacy grade

Maven Central Javadocs Semantic Versioning Automated Release Notes by gren

UniJ is a facade for:

  1. unmodifiable List/Set/Map factory methods introduced in JDK 9+

    List<Integer> list = UniLists.of(1, 2, 1); // ⇒ [1, 2, 1]
    Set<Integer> set = UniSets.copyOf(list); // ⇒ [1, 2]
    Map<Integer, String> map = UniMaps.of(1, "a", 2, "b"); // ⇒ [1: a, 2: b]
  2. some new Collectors introduced in JDK 9+

    Set<String> set = Stream.of("a", "a", "b").collect(UniCollectors.toUnmodifiableSet()); // ⇒ [a, b]
    List<Integer> list = Stream.of(1, 2, 3, 4).collect(UniCollectors.filtering(i -> i > 2, UniCollectors.toUnmodifiableList())); // ⇒ [3, 4] 

Note: UniJ is meant only as a facade for the official JDK APIs. UniJ will not introduce any APIs of its own design (it may only introduce APIs that directly correspond to APIs in the latest stable release of the JDK; currently, it's JDK 12).


UniJ is to new parts of JDK 9+ API what SLF4J is to logging API — a facade.


End User Usage

Gradle (Kotlin DSL)
implementation(group = "pl.tlinkowski.unij", name = "pl.tlinkowski.unij.bundle.___", version = "x.y.z")
Gradle (Groovy DSL)
implementation group: 'pl.tlinkowski.unij', name: 'pl.tlinkowski.unij.bundle.___', version: 'x.y.z'
requires pl.tlinkowski.unij.bundle.___;

where ___ can be one of: jdk8, jdk11, guava_jdk8, or eclipse_jdk8 (see Bundles for details).

Library Usage

Non-Transitive Library Usage

Gradle (Kotlin DSL)
implementation(group = "pl.tlinkowski.unij", name = "pl.tlinkowski.unij.api", version = "x.y.z")
Gradle (Groovy DSL)
implementation group: 'pl.tlinkowski.unij', name: 'pl.tlinkowski.unij.api', version: 'x.y.z'
requires pl.tlinkowski.unij.api;

Transitive Library Usage

Gradle (Kotlin DSL)
api(group = "pl.tlinkowski.unij", name = "pl.tlinkowski.unij.api", version = "x.y.z")
Gradle (Groovy DSL)
api group: 'pl.tlinkowski.unij', name: 'pl.tlinkowski.unij.api', version: 'x.y.z'
requires transitive pl.tlinkowski.unij.api;


This library has been primarily designed for:

  1. End users stuck on JDK 8
  2. Library maintainers targeting JDK 8

End Users Stuck on JDK 8

If you're stuck on JDK 8, you can't use JDK 9's List.of() and friends.

What can you do?

  1. Depend on Guava or Eclipse Collections and program to their proprietary APIs.

  2. Or depend on UniJ and program to its JDK-like API.

The problem with option 1 is that you get somewhat far from the JDK 11 API and its specification. If you decide to upgrade to JDK 11 in the future, replacing their collections with JDK 11 ones will not be straightforward.

With option 2, there's no such problem. Just add a dependency on a UniJ bundle of your choosing (see End User Usage for detailed instructions) and enjoy the JDK 11 API on JDK 8! (note: you may also need to add certain external dependencies)

In the future, if you want to switch to JDK 11, you'll either:

  • change the dependency to pl.tlinkowski.unij.bundle.jdk11
  • or remove UniJ altogether (will be a simple matter of replacing all occurrences of UniLists with List, etc.)

Example: See sample end-user projects.

Library Maintainers Targeting JDK 8

If you're a library maintainer targeting JDK 8, you also can't use JDK 9's List.of() and friends. Instead, you're probably using:

  1. some ArrayLists and wrapping them using Collections.unmodifiableList

  2. or some external library providing immutable Lists, like Guava or Eclipse Collections

The problems with these options are as follows:

  1. If your users themselves use JDK 11's, Guava's or Eclipse's Collections:

    • you're wasting potential by not using the best Collection implementations available
    • you're introducing inconsistency regarding which Collection implementations are used in your users' app/library
  2. This would be a really bad option, because — by bundling with an external library — you'd impose a heavy dependency on your users. Even worse, if they already used one (e.g. Guava), and you bundled with another (e.g. Eclipse Collections), they'd end up with both!

To sum up, as a library maintainer, the choice of Collection implementations shouldn't be yours. It's the same as with logging - you shouldn't choose the logging backend, and should only program to a facade like SLF4J. That's precisely what UniJ lets you do with respect to Collection factory methods.

You have two options here:

  1. Add a non-transitive or a transitive dependency on the extremely lightweight pl.tlinkowski.unij.api and instruct your users to:

  2. Add a dependency on the still quite lightweight pl.tlinkowski.unij.bundle.jdk8 and instruct your users to:

    • (optionally) add a runtime-only dependency on an SFL4J binding

    • (optionally) override the default JDK 8 bindings by depending on some other bindings (UniJ supports multiple competing bindings at runtime, with the JDK 8 bindings having the lowest priority)

Option 1 ensures there are no redundant dependencies on the classpath/modulepath, while option 2 requires less dependencies to be explicitly added by the user if they're on JDK 8.

Example: See sample library-related projects (option 1).


UniJ has two kind of APIs:

  • User API: utility classes (this is what the user interacts with)
  • Service API: interfaces (this is what the binding provider implements)

The call chain looks as follows:

end user ⟷ User API ⟷ Service API ⟷ binding provider

In other words, the end user is oblivious of the Service API, and the binding provider is oblivious of the User API.

UniJ is somewhat similar to SLF4J (Simple Logging Facade for Java) in that it provides an API that can be implemented in many different ways and then injected at runtime as a Java service.

User API

The User API is defined in pl.tlinkowski.unij.api and consists of the following utility classes:

The API of these classes has strict equivalence to the corresponding JDK API in terms of:

  • null treatment (no nulls allowed)
  • duplicate handling (e.g. no duplicates allowed in of methods of UniSets and UniMaps)
  • consistency (e.g. only one empty collection instance)

Details of this equivalence can be found in Specification section.

Service API

Disclaimer: As an end user, you don't need to be concerned with this API. Read on only if you want to implement your own UniJ bindings or want to understand how UniJ works internally.

UniJ service API is defined in pl.tlinkowski.unij.service.api and consists of the following interfaces:

A module providing implementations of one or more of these interfaces constitutes a binding.

UniJ provides many predefined bindings, but custom bindings are also possible (see Bindings for details).

Why two APIs

Why are there two APIs in UniJ? So that:

  1. for the user, we can exactly mirror the JDK API (equivalent class names, static methods), e.g.:

    • List.of()UniLists.of()
    • Collectors.toUnmodifiableList()UniCollectors.toUnmodifiableList()
  2. for the binding provider, we can expose an interface with all the related method to be implemented together, e.g.:

    • UnmodifiableListFactory.of()
    • UnmodifiableListFactory.collector()


UniJ APIs come with a detailed specification expressed as Spock tests for the Service API interfaces.

The specification is based on the contract of the original JDK API (expressed mostly in JavaDoc), and tries to follow this contract as close as possible.

The specification is expressed as the following test classes defined pl.tlinkowski.unij.test:

Read the specs linked above to learn in detail which contract UniJ follows.


A binding is an implementation of the Service API available at runtime. If multiple bindings with the same functionality are present on the runtime classpath/modulepath, the one with the top priority (lowest number) will be selected.

Predefined Bindings

UniJ comes with a number of predefined bindings, which can all be found under subprojects/bindings.

Collection Factory API Bindings

UniJ currently provides four types of Collection factory API bindings:

  1. JDK 10 (pl.tlinkowski.unij.service.collect.jdk10): simply forwards all calls to the JDK

  2. JDK 8 (pl.tlinkowski.unij.service.collect.jdk8): provides regular mutable JDK 8 collections wrapped using Collections.unmodifiableList/Set/Map

  3. Guava (pl.tlinkowski.unij.service.collect.guava): provides Guava's ImmutableList/ImmutableSet/ImmutableMap implementations

  4. Eclipse Collections (pl.tlinkowski.unij.service.collect.eclipse): provides Eclipse's ImmutableList/ImmutableSet/ImmutableMap implementations

Miscellaneous API Bindings

UniJ currently provides two types of miscellaneous API bindings:

  1. JDK 11 (pl.tlinkowski.unij.service.misc.jdk11): simply forwards all calls to the JDK

  2. JDK 8 (pl.tlinkowski.unij.service.misc.jdk8): provides custom implementations based on the ones in JDK 11

Custom Bindings

You can provide custom bindings by:

  • implementing an interface from the Service API

  • annotating it with a special @UniJService annotation

  • providing a entry (for JDK 9+) and/or a META-INF entry (for JDK 8; I recommend Google's @AutoService for it)


@UniJService(priority = 1)
public class CustomUnmodifiableListFactory implements UnmodifiableListFactory {
  // ...

When providing a custom service implementation, one should also create the following Spock test for it:

class CustomUnmodifiableListFactorySpec extends UnmodifiableListFactorySpec {

  def setupSpec() {
    factory = new CustomUnmodifiableListFactory()

A test dependency on pl.tlinkowski.unij.test is needed for it.


A UniJ bundle is a module having no source (save for its and depending on the following three modules:

  1. pl.tlinkowski.unij.api module (transitive dependency)
  2. Collection factory API binding (= one of pl.tlinkowski.unij.service.collect.___ modules)
  3. miscellaneous API binding (= one of pl.tlinkowski.unij.service.misc.___ modules)

Predefined Bundles

Currently, UniJ provides the following four bundles:

  1. JDK 11 (pl.tlinkowski.unij.bundle.jdk11):

  2. pure JDK 8 (pl.tlinkowski.unij.bundle.jdk8):

  3. Guava on JDK 8 (pl.tlinkowski.unij.bundle.guava_jdk8):

  4. Eclipse on JDK 8 (pl.tlinkowski.unij.bundle.eclipse_jdk8):

External Dependencies


UniJ User API has an implementation dependency on SLF4J API to let you have insight into which implementation it chooses for each of its Service API interfaces.

As a result, if you use UniJ, you should also add a runtimeOnly dependency on one of its bindings. Otherwise, you'll see the following message at runtime:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".

SLF4J: Defaulting to no-operation (NOP) logger implementation

SLF4J: See for further details.

Guava / Eclipse Collections

The dependencies on Guava and Eclipse Collections (present only in modules with guava and eclipse in their name, respectively) are compileOnly dependencies.

Thanks to this, dependency on UniJ won't affect the version of Guava / Eclipse Collections you want to use, since you have to declare this dependency explicitly.

Depending on your use case, do the following:

  • implicit use (only through UniJ): add a runtimeOnly dependency on Guava / Eclipse Collections

  • explicit use (access to the entire library):

    • non-transitive: add an implementation dependency + requires entry to
    • transitive: add an api dependency + requires transitive entry to

Minimal supported versions are:

Kotlin Interoperability

This library is highly interoperable with Kotlin thanks to being annotated with regard to:

using annotations provided by Basic Java Annotations library.


If you wonder how UniJ's indirection (= its two extra layers: User API and Service API) affects performance, the answer is short: it effectively doesn't.

It turns out the JIT compiler simply optimizes all the indirection away.

You can verify this by running a JMH benchmark (UniListsBenchmark) where calls to UniLists (with a JDK 11 binding) are compared to direct JDK 11 API calls. The exact results can be found here.

Backport of Java 9+ to Java 8

If you're looking for a backport of Java 9+ to Java 8, you can use the following for:

  1. new APIs: UniJ

  2. new language features: Jabel

    • Jabel is an annotation processor that lets you use some language features of Java 9+ while still targeting JDK 8
  3. Java Platform Module System: Gradle Modules Plugin

    • Gradle Modules Plugin provides support for JPMS ( not only to standard JDK 9+ projects, but also to JDK 8 projects, thanks to its special mixed-release mode

Together, UniJ, Jabel, and Gradle Modules Plugin may provide you with pretty good "Java 9+"-like experience while still targeting / being on JDK 8.


Usage: JDK 8+.

Building: Gradle 5+, JDK 11+.

About the Author

See my webpage ( or find me on Twitter (@t_linkowski).

You can’t perform that action at this time.