Unit 27: Type Inference
Learning Objectives
After this unit, students should:
- be familiar how Java infers missing type arguments
We have seen in the past units the importance of types in preventing run-time errors. Utilizing types properly can help programmers catch type mismatch errors that could have caused a program to fail during run-time, possibly after it is released and shipped.
By including type information everywhere in the code, we make the code explicit in communicating the intention of the programmers to the reader. Although it makes the code more verbose and cluttered — it is a small price to pay for ensuring the type correctness of the code and reducing the likelihood of bugs as the code complexity increases.
Java, however, allows the programmer to skip some of the type annotations and try to infer the type argument of a generic method and a generic type, through the type inference process.
The basic idea of type inference is simple: Java will figure out which matching types would lead to successful type checks (if any), and pick the most specific ones.
Diamond Operator
One example of type inference is the diamond operator <>
when we new
an instance of a generic type:
1 |
|
Java can infer that p
should be an instance of Pair<String,Integer>
since the compile-time type of p
is Pair<String,Integer>
. The line above is equivalent to:
1 |
|
Type Inferencing
We have been invoking
1 2 3 4 5 6 7 8 9 10 11 12 |
|
by explicitly passing in the type argument Shape
(also called type witness in the context of type inference).
1 |
|
We could remove the type argument <Shape>
so that we can call contains
just like a non-generic method:
1 |
|
and Java could still infer that S
should be Shape
. The type inference process looks for all possible types that match. In this example, the type of the two arguments must match. Let's consider each individually first:
- An object of type
Shape
is passed as an argument to the parameterobj
. SoS
might beShape
or, if widening type conversion has occurred, one of the other supertypes ofShape
. Therefore, we can say thatShape <: S <: Object
. - A
Seq<Circle>
has been passed intoSeq<? extends S>
. A widening type conversion occurred here, so we need to find all possibleS
such thatSeq<Circle>
<:Seq<? extends S>
. This is true only ifS
isCircle
, or another supertype ofCircle
. Therefore, we can say thatCircle <: S <: Object
.
Solving for these two constraints on S
, we get the following:
1 |
|
We therefore know that S
could be Shape
or one of its supertypes: GetAreable
and Object
. We choose the lower bound, so S
is inferred to be Shape
.
Type inferencing can have unexpected consequences. Let's consider an older version of contains
that we wrote:
1 2 3 4 5 6 7 8 9 10 11 |
|
Recall that we want to prevent nonsensical calls where we are searching for an integer in an array of strings.
1 2 |
|
But, if we write:
1 |
|
The code compiles! Let's go through the type inferencing steps to understand what happened. Again, we have two parameters:
strArray
has the typeString[]
and is passed toT[]
. SoT
must beString
or its superclassObject
(i.e.String <: T <: Object
). The latter is possible since Java array is covariant.123
is passed as typeT
. The value is treated asInteger
and, therefore,T
must be eitherInteger
, or its superclassesNumber
, andObject
(i.e.Integer <: T <: Object
).
Solving for these two constraints:
1 |
|
T
can only have the type Object
, so Java infers T
to be Object
. The code above is equivalent to:
1 |
|
And our version 0.4 of contains
actually is quite fragile and does not work as intended. We were bitten again by the fact that the Java array is covariant.
Target Typing
The example above performs type inferencing on the parameters of the generic methods. Type inferencing can involve the type of the expression as well. This is known as target typing. Take the following upgraded version of findLargest
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
and we call
1 |
|
We have a few more constraints to check:
- Due to target typing, the return type of
T
must be a subtype ofShape
(i.e.T <: Shape
) - Due to the bound of the type parameter,
T
must be a subtype ofGetAreable
(i.e.T <: GetAreable
) Seq<Circle>
must be a subtype ofSeq<? extends T>
, soT
must be a supertype ofCircle
(i.e.Circle <: T <: Object
)
Solving for all three of these constraints:
1 |
|
The lower bound is Circle
, so the call above is equivalent to:
1 |
|
Further Type Inference Examples
We now return to our Circle
and ColoredCircle
classes and the GetAreable
interface. Recall that Circle
implements GetAreable
and ColoredCircle
inherits from Circle
.
Now lets consider the following method signature of a generic method foo
:
1 |
|
Then we consider the following code excerpt:
1 |
|
What does the java compiler infer T
to be? Lets look at all of the constraints on T
.
-
First we can say that the return type of
foo
must be a subtype ofColoredCircle
, therefore we can sayT <: ColoredCircle
. -
T
is also a bounded type parameter, and therefore we also knowT <: Circle
. -
Our method argument is of type
Seq<GetAreable>
and must be a subtype ofSeq<? extends T>
, soT
must be a supertype ofGetAreable
(i.e.GetAreable <: T <: Object
).
We can see that there no solution to our contraints, T
can not be both a subtype of ColoredCircle
and a supertype of GetAreable
and therefore the Java compiler can not find a type T
. The Java compiler will throw an error stating the inference variable T
has incompatible bounds.
Lets consider, one final example using the following method signature of a generic method bar
:
1 |
|
Then we consider the following code excerpt:
1 |
|
What does the java compiler infer T
to be? Again, lets look at all of the constraints on T
.
-
We can say that the return type of
bar
must be a subtype ofGetAreable
, therefore we can sayT <: GetAreable
. -
Our method argument is of type
Seq<Circle>
and must be a subtype ofSeq<? super T>
, soT
must be a subtype ofCircle
(i.e.T <: Circle
).
Solving for these two constraints:
1 |
|
Whilst ColoredCircle
is also a subtype of Circle
it is not included in the above statement and therefore the compiler does not consider this class during type inference. Indeed, the compiler cannot be aware1 of all subtypes of Circle
and there could be more than one subtype. Therefore T
can only have the type Circle
, so Java infers T
to be Circle
.
Rules for Type Inference
We now summarize the steps for type inference. First, we figure out all of the type constraints on our type parameters, and then we solve these constraints. If no type can satisfy all the constraints, we know that Java will fail to compile. If in resolving the type constraints for a given type parameter T
we are left with:
Type1 <: T <: Type2
, thenT
is inferred asType1
Type1 <: T
2, thenT
is inferred asType1
T <: Type2
, thenT
is inferred asType2
where Type1
and Type2
are arbitrary types.
-
Due to evolving specifications of software, at the time of compilation, a subtype may not have even been conceived of or written yet! ↩
-
Note that
T <: Object
is implicit here. We can see that this case could also be written asType1 <: T <: Object
, and would therefore also be explained by the previous case (Type1 <: T <: Type2
). ↩