Intersection types are one of these relatively unknown and underestimated advanced features of Java generics whose proper usage can increase type-safety of our abstractions (or at least minimize boilerplate and ugly runtime checks).
Introducing Intersection Types
Simply put, an intersection type is a form of an anonymous type created by combining at least two different types.
At the first glance, it might sound like a definition that sits next to inheritance or is just a fancy term for implementing multiple interfaces – and that would not be that far from the truth – but intersection types are anonymous and don’t establish any hierarchical relation between combined types.
Imagine that we need to model two types of animals:
- those that can fly
- those that can swim
Naturally, we could use two interfaces to achieve that:
interface Flyable { void fly(); } interface Swimmable { void swim(); }
…but what if we wanted to represent animals that can do both?
We can simply implement two interfaces:
class SailfinFlyingfish implements Swimmable, Flyable { @Override public void fly() { System.out.println("*flap flap*"); } @Override public void swim() { System.out.println("*swim swim*"); } }
Well, it was super easy, barely an inconvenience.
But what if we wanted to create a method that accepts only animals that can both swim and fly?
public static void process(... animal) { animal.fly(); animal.swim(); }
Firstly, we could simply accept only SailfinFlyingfish instances – that would work but introduce unnecessary coupling to that particular type, and we want to avoid doing that.
There are two other basic non-idiomatic ways to achieve this that I keep seeing in legacy codebases.
The first one involves the introduction of a synthetic interface that would serve as a combination of these two – FlyableAndSwimmable:
interface FlyableAndSwimmable extends Flyable, Swimmable { }
And now we could implement our method:
public static void process(FlyableAndSwimmable animal) { animal.fly(); animal.swim(); }
But that involved quite a lot of boilerplate, isn’t very elastic after all, but at least is type-safe.
The other approach is significantly worse, it involves specifying only one of these types as a method parameter and deferring the other check till runtime:
public static void process(Flyable animal) { if (animal instanceof Swimmable) { animal.fly(); ((Swimmable) animal).swim(); } else { throw new IllegalArgumentException(); } }
Now, not only our business logic is hidden among casts and instanceof checks but the solution is no longer type-safe – a method would accept a flying but not swimming animal, and explore at runtime instead of compile-time.
Generics-based Intersection Types
If we have a look at the Java Language Specification, we can find a dedicated section (under generics):
An intersection type takes the form T1 & … & Tn (n > 0), where Ti (1 ≤ i ≤ n) are types.
– The Java Language Specification, Java SE 11 Edition
And indeed it turns out that we can use that syntax when defining generic type variables:
public static <T extends Flyable & Swimmable> void process(T animal) { animal.fly(); animal.swim(); }
It might look like a generics-based hack, but it’s actually a fully-fledged legitimate Java feature – just implemented in an interesting way.
And now we can simply pass any class that implements both interfaces to our process() method and the code will compile and work as intended:
SailfinFlyingfish fish = new SailfinFlyingfish(); process(fish);
As easy as it is – by leveraging this syntax we ended up with a type-safe and boilerplate-free solution.
Imagine that you want to create a method that accepts Function<T, R> which is also Serializable, or maybe accept only Iterable implementations that are also Autoclosable.
Actually, I used the same technique when writing the below article to enforce that all List instances supplied to my Spliterator provide constant-time element access:
A Case Study of Implementing an Efficient Shuffling Stream/Spliterator in Java
class ImprovedRandomSpliterator<T, LIST extends RandomAccess & List<T>> implements Spliterator<T> { private final Random random; private final List<T> source; private int size; ImprovedRandomSpliterator(LIST source, Supplier<? extends Random> random) { this.source = source; this.random = random.get(); this.size = this.source.size(); } // ... }
Since JDK10
Before JDK10, that approach could be only used when working with generic type parameters and casting, but it wasn’t allowed when declaring local variables:
// doesn't compile (Function<Integer, Integer> & Serializable) action = (Function<Integer, Integer> & Serializable) i -> i + 1;
So we always needed to choose one of the intersected types:
Function<Integer, Integer> action = (Function<Integer, Integer> & Serializable) i -> i + 1; // or Serializable action = (Function<Integer, Integer> & Serializable) i -> i + 1;
But since the introduction of local-variable-type-inference, we can do the same at the local variable level:
var action = (Function<Integer, Integer> & Serializable) i -> i + 1;
You can read more about it here.
Key Takeaways
Intersection types are quite an unknown Java feature that can provide us additional boilerplate-free type-safety when trying to represent combinations of multiple types.
Additionally, since JDK10 we can use them alongside local-variable-type-inference.