avatarViraj Shetty

Summary

This context discusses the concept of Intersection Types in Java, a complex type created by concatenating multiple types with the '&' character.

Abstract

The context begins with a question about the type of a variable in a Java list containing an Integer and a String. Despite the elements being different, the Java Virtual Machine (JVM) infers the type as an Intersection Type, which combines the types of all elements. The context explains that Intersection Types cannot be explicitly declared but are created dynamically during type inference or by using Java Generics. It provides an example of using Intersection Types with Java Generics to create a method that finds the greater of two numbers. The context also discusses the JVM's ability to infer Intersection Types in certain situations, such as in switch expressions, and the use of the 'var' keyword to preserve these types. The context concludes by warning against overusing Intersection Types and advocates for their use only as a last resort.

Opinions

  • The author emphasizes the importance of understanding Intersection Types in Java, as they are a complex type that can be inferred by the JVM.
  • The author suggests that Intersection Types are often hidden from plain sight by the 'var' keyword, highlighting the need for awareness of their existence.
  • The author warns against overusing Intersection Types, suggesting that they should only be used as a last resort to solve a problem.
  • The author provides an example of using Intersection Types with Java Generics to create a method that finds the greater of two numbers, demonstrating their usefulness in certain situations.
  • The author suggests that the JVM can infer Intersection Types in certain situations, such as in switch expressions, emphasizing the dynamic nature of type inference in Java.
  • The author advocates for the use of the 'var' keyword to preserve Intersection Types, as they cannot be explicitly declared.
  • The author concludes by suggesting that Intersection Types are a complex but important feature in Java, and that understanding them is worth the time and effort.

Java Intersection Type — You MUST know this

Let’s fully understand this important feature in Java

Photo by Christopher Gower on Unsplash

If you prefer the YouTube video version, click on the link https://youtu.be/JK2BKF0at-0

I regularly upload videos to my YouTube Channel at https://www.youtube.com/@viraj_shetty

Let’s start with a simple question. If you look at the variable declaration below, what do you think is the type of variable list ?

var list = List.of(5, "New York");

In Java, the type of a variable is decided at compile time and for that reason Java is considered statically typed. Since the elements of this list are an Integer and a String, the most reasonable answer would be List<Object>. However, if you type the above code into an IDE and hover your mouse over the variable, you will get the shock of your life. The IDE tells us that the type is

  List<Serializable & Comparable<? extends Serializable & Comparable<?> & Constable & ConstantDesc> & Constable & ConstantDesc>

The JVM automatically inferred the type of the element of the list by recognizing that both String and Integer implements Serializable, Comparable and so on. Based on this, it generated a type for the element by concatenating these types with an & character.

It’s not important what the above means at this point. But what you see is that the type of each element of the list is not inferred as a simple String, Integer or any other Type but a more complex type called an Intersection Type.

So, what is an Intersection Type?

Intersection Type

Here’s a bookish explanation.

If T1, T2, … Tn are types (classes or interfaces) in Java, then an Intersection Type takes the form as below.

T1 & T2 & .. Tn

A class of this type extends or implements directly or indirectly from T1 and T2 and .. Tn.

For example, the Intersection Type below represents a type which extends from a Number but implements Comparable as well — both.

Number & Comparable

But, we cannot explicitly declare a variable of an Intersection Type. For example, the following is not allowed and the compiler will give an error.

// This will give Compile Error !! 
Number & Comparable value = null;

If we cannot declare a variable of an Intersection Type, how do they get created ? In many cases, the JVM dynamically creates these during type inference; but developers can also create them using Java Generics.

Generics and Intersection Types

To appreciate Intersection Types, let’s take an example of a method which wishes to find the greater of two numbers. Our first naive implementation is probably to write something like below.

  boolean greater(Number n1, Number n2) {
      // how do we write this code ?
  }

Number is an abstract class in Java and there are concrete implementations like Integer, Long, Float, BigDecimal and so on. Since this method must work for all types of numbers which can be compared, this is not an easy task. The Number abstract class does not implement Comparable and so we cannot use the Comparable interface to compare these two numbers.

In order for us to use the Comparable interface, the type of parameters n1 and n2 must be a Number class as well as it should implement Comparable interface. We can express this by using Intersection Types. But to do so, we will have to use Java Generics as follows.

<T extends Number & Comparable> 
            boolean greater(T n1, T n2) {
   return n1.compareTo(n2) > 0;
}

The parameters of the method is a parameterized type T which is declared to extend from both Number and Comparable (see the special Java Generics syntax within angular brackets to express this).

Now all Number objects which are Comparable can be passed to method greater(..) as parameters. More importantly, within the method greater(..), we can use the compareTo(..) method from the Comparable interface to compare the two Numbers.

Here are some of the examples of using the method greater(..) by passing different types.

// Using Integers. Returns False
boolean intCheck = greater(1, 2);

// Using BigDecimal. Returns True
BigDecimal big1 = new BigDecimal(1000.02);
BigDecimal big2 = new BigDecimal(1000.01);
boolean bigCheck = greater(big1, big2);

Inferred Intersection Type

Quite frequently, the JVM surprises us by inferring a particular type as an Intersection Type. We already saw an example at the start of this article with a horrendous looking Intersection Type.

Here’s another example of a switch expression where the JVM infers the type of the variable result. Recall that with the new switch expression, it evaluates to a single value and can be assigned to a variable. But nothing stops the developer from returning objects of different types within the cases — as you see below.

var result = switch(city) {

   case "New York" -> "Crazy";
   default -> BigDecimal.ZERO;

};

The type of variable result at compile time will be inferred by the JVM as below.

Serializable & Comparable<? extends Serializable & Comparable<?>>

Intersection Type and “var”

It’s no accident that we are using var to declare variable result. Using var preserves this Intersection Type and we can call the methods of any of the different types comprising the Intersection Type.

To explain this, let’s take an example of a situation which happens very often. To simplify the problem, interfaces B and C directly extends from interfaces X and Y but in practice it may extend indirectly from these interfaces.

interface X {
    void x();
}

interface Y {
    void y();
}

// Both B and C use X and Y
interface B extends X, Y {
    void b();
}


interface C extends X, Y {
    void c();
}


// Type of 'result' ?
var result = switch(city) {

   case "New York" -> new B() {
      // Impl not shown
   };

   default -> new C() {
      // Impl not shown               
   };

};

Here we are returning an implementation of B and C in the switch cases. What’s the Type associated with variable result ? Since B and C implement both interfaces X and Y (that’s the intersection between the two return types), the return type is a type which extends both X and Y. So, the JVM assigns the Intersection Type X & Y to result. But, as mentioned before — we cannot declare result with that type.

// Type associated with result
X & Y

// Compile error
X & Y result = switch(city) {..}

We avoid that restriction by using var for variable result. By doing that we are able to preserve the returned Intersection Type. As a result, we can call any of the methods associated with interfaces X and Y.

The following calls are valid.

result.x();
result.y();

But the following calls are invalid and will give a compile error.

result.a();
result.b();

Without using var, we would have assigned the switch expression to an Object, interface X or Y. These assignments would be valid but would not fully express the return type of switch.

// These are valid but does not fully 
// express the return type

X result = switch(city) {..}
Y result = switch(city) {..}
Object result = switch(city) {..}

Note that the example given above is simply to show how the JVM sometimes infers the type of the variable to be a Intersection Type. I would not advocate designing in this way which could lead to confusion among the developers. Use Intersection Type as a last resort to solve a problem.

If this post was helpful, please click the clap 👏 button below a few times to show your support. Thanks for reading !

I regularly upload videos to my YouTube Channel at https://www.youtube.com/@viraj_shetty

Also check out my discounted Udemy Courses at https://www.mudraservices.com/courses/

Summary

As part of Java Development, you will encounter situations where the type of a particular variable may look weird (separated by &). These are Intersection Types and they are usually hidden from plain sight by the var keyword. They represent types which are more complex and it’s well worth our time to be aware of them.

Java
Core Java
Java Generic
Recommended from ReadMedium