How to create Intersection Types in Java

dimitrilc 2 Tallied Votes 143 Views Share

Introduction

In Java, a common way to express a type that is a combination of two or more types is to just create an interface that extends the other types. The problem with this approach is that your code might be littered with interface declarations, polluting your code and namespace.

But there is a solution to this problem, and that is the usage of Intersection Type. The Intersection Type is somewhat of an elusive language feature in Java, so in this tutorial, we will learn how to use it on our code.

Goals

At the end of the tutorial, you would have learned:

  1. How to use intersection types.

Prerequisite Knowledge

  1. Basic Java.
  2. Basic Java Generics.

Tools Required

  1. A Java IDE such as IntelliJ Community Edition.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Java project.
  2. Create a package com.example.
  3. Create a class called Entry.
  4. Create the main() method inside Entry.java.

Not using Intersection Types

Before going into Intersection Types, let us look at how it is done without it. Add these two top-level interfaces into the Entry.java file.

interface Human { //1
   void talk();
}

interface Wolf { //2
   void howl();
}

In the code snippet above, we have two interfaces, Human and Wolf. But what if we want a new type that is both a human and a wolf? To do that, we add the top-level HumanWolf interface into Entry.java like below.

interface HumanWolf extends Wolf, Human {} //3

Also, in the Entry class, we need to add a method that will use the HumanWolf interface.

private static void boilerplate(HumanWolf werewolf){
   werewolf.howl();
   werewolf.talk();
}

I named the method boilerplate because we had to declare a completely new interface just to represent a type that is both Human and Wolf. In main(), we can call it using:

var werewolf = new HumanWolf(){
   @Override
   public void howl() {
       System.out.println("howls");
   }

   @Override
   public void talk() {
       System.out.println("talks");
   }
};

boilerplate(werewolf);

The code prints:

howls
talks

Using Intersection Types

Declaring a new sub-interface might seem okay. But there is another way to create our hybrid type, and that is using Intersection Types. To declare an intersection type, we would need to use an Ampersand(&) between the types in the generics parameter section on the method signature. Still inside the Entry.java class, create another method from the code below.

private static <T extends Wolf & Human> void noBoilerplate(T werewolf){
   werewolf.howl();
   werewolf.talk();
}

In the method above, we declare that T must be some type that is BOTH a Wolf and a Human. We did not need to declare the HumanWolf sub-interface at all. We can test our theory by attempting to use the method with the code below in main().

var wolf = (Wolf)() -> System.out.println("howls");
noBoilerplate(wolf);

var human = (Human)() -> System.out.println("Hello");
noBoilerplate(human);

In the code snippet above, we tried to pass either a Wolf or a Human instance into the noBoilerplate() method, but the code refuses to compile with the errors:

reason: no instance(s) of type variable(s) exist so that Wolf conforms to Human
reason: no instance(s) of type variable(s) exist so that Human conforms to Wolf

because the noBoilerplate() needs a type that implements both Wolf and Human.

We still need to declare a class that implements both Wolf and Human though, and the anonymous class syntax will not allow implementing more than one interface at once. In the Entry class, create a new inner static class using the code below.

private static class Werewolf implements Wolf, Human {
   @Override
   public void talk() {
       System.out.println("howls");
   }

   @Override
   public void howl() {
       System.out.println("talks");
   }
}

Finally, we can call the method in main() with:

noBoilerplate(new Werewolf());

Using Intersection Type to cast Lambdas

Intersection types can also be used to cast lambdas into a new type that conforms to two or more types. The only thing to watch out for when using this feature is whether the lambda can implement both types at once without any conflicting methods. This feature is commonly used to cast a lambda to a marker interface (an empty interface) because the marker interface does not introduce any conflict.

Let us create a new top-level interface called Alpha.

interface Alpha {}

In main(), add another method to test this feature. This method will check if a Wolf is also an Alpha.

private static boolean isAlpha(Wolf wolf){
   return wolf instanceof Alpha; //Wolf and Alpha are not related.
}

In main(), we call the code:

var wolf = (Wolf)() -> System.out.println("howls");
System.out.println(isAlpha(wolf));

var alphaWerewolf = (Alpha & Wolf)() -> System.out.println("Alpha howls");
System.out.println(isAlpha(alphaWerewolf));

And the output would be:

false
true

The variable declaration alphaWerewolf is where we casted the lambda into both an implementation of Wolf and an implementation of Alpha(it is empty so there is nothing to override).

Solution Code

package com.example;

public class Entry {
   public static void main(String[] args){
       var werewolf = new HumanWolf(){
           @Override
           public void howl() {
               System.out.println("howls");
           }

           @Override
           public void talk() {
               System.out.println("talks");
           }
       };

       boilerplate(werewolf);

//        var wolf = (Wolf)() -> System.out.println("howls");
//        noBoilerplate(wolf);
//
//        var human = (Human)() -> System.out.println("Hello");
//        noBoilerplate(human);

       noBoilerplate(new Werewolf());

       var wolf = (Wolf)() -> System.out.println("howls");
       System.out.println(isAlpha(wolf));

       var alphaWerewolf = (Alpha & Wolf)() -> System.out.println("Alpha howls");
       System.out.println(isAlpha(alphaWerewolf));
   }

   private static void boilerplate(HumanWolf werewolf){
       werewolf.howl();
       werewolf.talk();
   }

   private static <T extends Wolf & Human> void noBoilerplate(T werewolf){
       werewolf.howl();
       werewolf.talk();
   }

   private static class Werewolf implements Wolf, Human {
       @Override
       public void talk() {
           System.out.println("howls");
       }

       @Override
       public void howl() {
           System.out.println("talks");
       }
   }

   private static boolean isAlpha(Wolf wolf){
       return wolf instanceof Alpha; //Wolf and Alpha are not related.
   }
}

interface Human { //1
   void talk();
}

interface Wolf { //2
   void howl();
}

interface HumanWolf extends Wolf, Human {} //3

interface Alpha {}

Summary

We have learned how to use intersection types in Java. The full project code can be found here: https://github.com/dmitrilc/DaniwebJavaIntersectionType