Intersection Types: Java Generics’ most underused feature

http://iteratrlearning.com/java/generics/2016/05/12/intersection-types-java-generics.html

Written on May 12, 2016

Generic types were added to java way back in 2004 and have had extensive usage by different libraries and application developers ever since. Not all generics features have had equal usage though. In this blog post we’re going to explore Intersection Types: which are both one of the lesser-known features that Java Generics offers.

Intersection types are a form of generic type that look a bit like <T extends A & B>, where T is a type parameter and A and B are two types. In order to understand intersection types we first need to talk about how the extends keyword is used in generics. If you see a generic type parameter like <T extends Comparable> then it means that you are going to require our generic type (T) to only be those classes that implement the interface Comparable. Remember when we say “extends” in generic types that could refer to either extending a class or also implementing an interface.

Intersection types are for situations where we want to be really greedy with our requirements! We want to say only allow classes that extend both classes in the case of our intersection types. But, why on earth would we want to do this? Let’s walk through a worked example to understand intersection types properly.

Deserializing DataInputStream and RandomAccessFile objects

Let’s suppose we’ve got a Person class and every Person has a name and an age that are instantiated in the constructor.

public Person(String name, int age)

Our system is trying to de-serialise person instances from an input stream, like a storage file or a network connection. In order to do this we’ve got a read method that takes a DataInputStream. It reads out the name as a Unicode string and the age as an int. After the Person instance has been read out the DataInputStream gets closed.

private static Person read(DataInputStream source) {
   try(DataInputStream input = source) {
       return new Person(input.readUTF(), input.readInt());
   }
   catch (IOException e) {
       e.printStackTrace();
       return null;
    }
}

After this code has been deployed in production for a while a new requirement appears. Software development is a job never done. The new requirement requires the code to de-serialise the data from a RandomAccessFile object. Ideally you would like to re-use the logic of the read method which accepts a source of type DataInputStream. Unfortunately, an object of type RandomAccessFile is not a subtype of DataInputStream so this won’t work.

Dummy interfaces

What can you do? You can notice that both DataInputStream and RandomAccessFile implement the interfaces DataInput and Closeable. Consequently, you could refactor the method read by introducing a “dummy” interface whose only purpose is to create a type that extends both DataInput and Closeable:

interface DataInputCloseable extends DataInput, Closeable {}

You can now refactor the method read to use this interface as follows:

private static Person read(DataInputCloseable source) {
   try(DataInputStream input = source) {
       return new Person(input.readUTF(), input.readInt());
   }
   catch (IOException e) {
       e.printStackTrace();
       return null;
   }
}

Thanks to this refactoring, you have gained both code re-use and flexibility. You are now able to re-use the same implementation of the method read but pass DataInputStream objects and RandomAccessFile objects.

Using intersection types

Nonetheless, you had to introduce a kind of “dummy” interface whose only purpose is to create a new type which extends two types (DataInput and Closeable). Not only that, but because you were depending upon classes in the core JDK libraries you couldn’t retrofit that interface to the actual implementing classes. This is a situation where intersection types can be convenient. They let you do just that without introducing an unnecessary interface or class in your code. You can refactor the method read as follows:

private static <I extends DataInput & Closeable> Person read(I source) {
    try(I input = source) {
        return new Person(input.readUTF(), input.readInt());
    }
    catch (IOException e) {
       e.printStackTrace();
       return null;
    }
}

In the code above, you introduced an intersection type <I extends DataInput & Closeable> which says the type parameter I extends both DataInput and Closeable. You can then use this type parameter as argument to the method to indicate the input should extends both DataInput and Closeable.

Conclusion

In this blog post, we showed that intersection types can be convenient in situations where you need “to extend from” two, or more, types in an existing library but you don’t have an existing interface or class to model that. In many cases this is unnecessary. Often introducing new types via an interface or class gives us an explicit name which helps the readability of your code. If the concept and name makes sense then that’s what we would recommend doing. In this case that wasn’t possible and intersection types helped save the day.

If you are interested in improving your Java skills, check out our Java Software Development Bootcamp

2018/7/7 posted in  Java