Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can Java infer type arguments from type parameter bounds?

Tags:

The following test program is derived from a more complicated program that does something useful. It compiles successfully with the Eclipse compiler.

import java.util.ArrayList;
import java.util.List;

public class InferenceTest
{
    public static void main(String[] args)
    {
        final List<Class<? extends Foo<?, ?>>> classes =
            new ArrayList<Class<? extends Foo<?, ?>>>();
        classes.add(Bar.class);
        System.out.println(makeOne(classes));
    }

    private static Foo<?, ?> makeOne(Iterable<Class<? extends Foo<?, ?>>> classes)
    {
        for (final Class<? extends Foo<?, ?>> cls : classes)
        {
            final Foo<?, ?> foo = make(cls); // javac error here
            if (foo != null)
                return foo;
        }
        return null;
    }

    // helper used to capture wildcards as type variables
    private static <A, B, C extends Foo<A, B>> Foo<A, B> make(Class<C> cls)
    {
        // assume that a real program actually references A and B
        try
        {
            return cls.getConstructor().newInstance();
        }
        catch (final Exception e)
        {
            return null;
        }
    }

    public static interface Foo<A, B> {}

    public static class Bar implements Foo<Integer, Long> {}
}

However, with the Oracle JDK 1.7 javac, it fails with this:

InferenceTest.java:18: error: invalid inferred types for A,B; inferred type does not
 conform to declared bound(s)
            final Foo<?, ?> foo = make(cls);
                                      ^
    inferred: CAP#1
    bound(s): Foo<CAP#2,CAP#3>
  where A,B,C are type-variables:
    A extends Object declared in method <A,B,C>make(Class<C>)
    B extends Object declared in method <A,B,C>make(Class<C>)
    C extends Foo<A,B> declared in method <A,B,C>make(Class<C>)
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends Foo<?,?> from capture of ? extends Foo<?,?>
    CAP#2 extends Object from capture of ?
    CAP#3 extends Object from capture of ?
1 error

Which compiler is right?

One suspicious aspect of the output above is CAP#1 extends Foo<?,?>. I would expect the type variable bounds to be CAP#1 extends Foo<CAP#2,CAP#3>. If this were the case, then the inferred bound of CAP#1 would conform to the declared bounds. However, this might be a red herring, because C should indeed be inferred to be CAP#1, but the error message is regarding A and B.


Note that if I replace line 26 with the following, both compilers accept the program:

private static <C extends Foo<?, ?>> Foo<?, ?> make(Class<C> cls)

However, now I can't reference the captured types of the Foo parameters.

Update: Similarly accepted by both compilers (but also useless) is this:

private static <A, B, C extends Foo<? extends A, ? extends B>>
    Foo<? extends A, ? extends B> make(Class<C> cls)

It essentially causes A and B to be trivially inferred as Object, and therefore obviously not useful in any context. It does, however, lend credence to my theory below that javac will only perform inference on wildcard bounds, not capture bounds. If no one has any better ideas, this might be the (unfortunate) answer. (End Update)


I realize this whole question is likely TL;DR, but I'll continue in case someone else is hitting this issue...

Based on JLS 7, §15.12.2.7 Inferring Type Arguments Based on Actual Arguments, I've done the following analysis:

Given a constraint of the form A << F, A = F, or A >> F:

Initially, we have one constraint of the form A << F, which indicates that type A is convertible to type F by method invocation conversion (§5.3). Here, A is Class<CAP#1 extends Foo<CAP#2, CAP#3>> and F is Class<C extends Foo<A, B>>. Note that the other constraint forms (A = F and A >> F) only arise as the inference algorithm recurses.

Next, C should be inferred to be CAP#1 by the following rules:

(2.) Otherwise, if the constraint has the form A << F:

  • If F has the form G<..., Yk-1, U, Yk+1, ...>, where U is a type expression that involves Tj, then if A has a supertype of the form G<..., Xk-1, V, Xk+1, ...> where V is a type expression, this algorithm is applied recursively to the constraint V = U.

Here, G is Class, U and Tj are C, and V is CAP#1. Recursive application to CAP#1 = C should result in the constraint C = CAP#1:

(3.) Otherwise, if the constraint has the form A = F:

  • If F = Tj, then the constraint Tj = A is implied.

Up to this point, the analysis seems to agree with the javac output. Perhaps the point of divergence is whether to continue attempting to infer A and B. For example, given this rule

  • If F has the form G<..., Yk-1, ? extends U, Yk+1, ...>, where U involves Tj, then if A has a supertype that is one of:
    • G<..., Xk-1, V, Xk+1, ...>, where V is a type expression.
    • G<..., Xk-1, ? extends V, Xk+1, ...>.

Then this algorithm is applied recursively to the constraint V << U.

If CAP#1 is considered to be a wildcard (which it is a capture of), then this rule applies, and inference continues recursively with U as Foo<A, B> and V as Foo<CAP#2, CAP#3>. As above, this would yield A = CAP#2 and B = CAP#3.

However, if CAP#1 is simply a type variable, then none of the rules seem to consider its bounds. Perhaps this concession at the end of the section in the spec refers to such cases:

The type inference algorithm should be viewed as a heuristic, designed to perform well in practice. If it fails to infer the desired result, explicit type parameters may be used instead.

Obviously, wildcards cannot be used as explicit type parameters. :-(

like image 248
Trevor Robinson Avatar asked Jul 03 '13 01:07

Trevor Robinson


People also ask

Does Java support type inference?

Type inference is a Java compiler's ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable.

How does type inference work in Java?

Type inference represents the Java compiler's ability to look at a method invocation and its corresponding declaration to check and determine the type argument(s). The inference algorithm checks the types of the arguments and, if available, assigned type is returned.

What is a bounded type parameter in Java?

There may be times when you want to restrict the types that can be used as type arguments in a parameterized type. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.

Is it possible to declared a multiple bounded type parameter?

Multiple BoundsBounded type parameters can be used with methods as well as classes and interfaces. Java Generics supports multiple bounds also, i.e., In this case, A can be an interface or class. If A is class, then B and C should be interfaces. We can't have more than one class in multiple bounds.


1 Answers

The problem is that you start from the following inference constraint:

class<#1>, #1 <: Foo<?, ?>

Which gives you a solution for C, namely C = #1.

Then you need to check whether C conforms to declared bounds - the bound of C is Foo, so you end up with this check:

#1 <: Foo<A,B>

which can be rewritten as

Bound(#1) <: Foo<A, B>

hence:

Foo<?, ?> <: Foo<A, B>

Now, here the compiler does a capture conversion of the LHS (here's where #2 and #3 are generated):

Foo<#2, #3> <: Foo<A, B>

Which means

A = #2

B = #3

So, we have that our solution is { A = #2, B = #3, C = #1 }.

Is that a valid solution? In order to answer that question we need to check whether the inferred types are compatible with the inference variable bounds, after type substitution, so:

[A:=#2]A <: Object
#2 <: Object - ok

[B:=#3]B <: Object
#3 <: Object - ok

[C:=#1]C <: [A:=#2, B:=#3]Foo<A, B>
#1 <: Foo<#2, #3>
Foo<?, ?> <: Foo<#2, #3>
Foo<#4, #5> <: Foo<#2, #3> - not ok

Hence the error.

The spec is underspecified when it comes to the interplay between inference and captured types, so it's quite normal (but not good!) to have different behaviors when switching between different compilers. However some of these issues are being worked on, both from a compiler perspective and from a JLS perspective, so issues like this should get fixed in the medium term.

like image 128
Maurizio Cimadamore Avatar answered Sep 26 '22 03:09

Maurizio Cimadamore