Подтвердить что ты не робот

Может ли Java вывести аргументы типа из ограничений параметров типа?

Следующая тестовая программа выведена из более сложной программы, которая делает что-то полезное. Он успешно компилируется с компилятором Eclipse.

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> {}
}

Однако с Oracle JDK 1.7 javac он не справляется с этим:

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

Какой компилятор прав?

Одним из подозрительных аспектов вышеприведенного вывода является CAP#1 extends Foo<?,?>. Я ожидал бы, что ограничения переменной типа будут CAP#1 extends Foo<CAP#2,CAP#3>. Если бы это было так, то предполагаемая оценка CAP#1 соответствовала объявленным границам. Однако это может быть красная селедка, потому что C действительно следует понимать как CAP#1, но сообщение об ошибке относится к A и B.


Обратите внимание: если я заменю строку 26 следующим образом, оба компилятора принимают программу:

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

Однако теперь я не могу ссылаться на захваченные типы параметров Foo.

Обновление: Аналогично, принятое обоими компиляторами (но также и бесполезное):

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

Это по существу приводит к тому, что A и B тривиально выводятся как Object и поэтому явно не полезны ни в каком контексте. Тем не менее, это подтверждает мою теорию ниже, что javac будет выполнять вывод только по границам подстановочных знаков, а не по границам захвата. Если у кого-то нет лучших идей, это может быть (неудачный) ответ. (End Update)


Я понимаю, что весь этот вопрос, скорее всего, TL; DR, но я продолжу, если кто-то еще столкнется с этой проблемой...

Основываясь на JLS 7, §15.12.2.7 Вывод аргументов типа на основе фактических аргументов, я сделал следующий анализ:

Учитывая ограничение формы A << F, A = F или A >> F:

Изначально у нас есть одно ограничение формы A << F, которое указывает, что тип A можно преобразовать в тип F путем преобразования вызова метода (§5.3). Здесь A - Class<CAP#1 extends Foo<CAP#2, CAP#3>>, а F - Class<C extends Foo<A, B>>. Обратите внимание, что другие формы ограничений (A = F и A >> F) возникают только при повторении алгоритма вывода.

Далее, C следует определить как CAP#1 по следующим правилам:

(2.) В противном случае, если ограничение имеет вид A << F:

  • Если F имеет вид G<..., Yk-1, U, Yk+1, ...>, где U - выражение типа, которое включает Tj, то если A имеет супертип вида G<..., Xk-1, V, Xk+1, ...>где V - это выражение типа, этот алгоритм применяется рекурсивно к ограничению V = U.

Здесь G - Class, U и Tj - C, а V - CAP#1. Рекурсивное приложение к CAP#1 = C должно привести к ограничению C = CAP#1:

(3.) В противном случае, если ограничение имеет вид A = F:

  • Если F = Tj, то подразумевается ограничение Tj = A.

До этого момента анализ, похоже, согласуется с выходом javac. Возможно, точка расхождения заключается в том, продолжать ли пытаться вывести A и B. Например, учитывая это правило

  • Если F имеет вид G<..., Yk-1, ? extends U, Yk+1, ...>, где U включает Tj, то если A имеет супертип, который является одним из:
    • G<..., Xk-1, V, Xk+1, ...>, где V является выражением типа.
    • G<..., Xk-1, ? extends V, Xk+1, ...>.

Затем этот алгоритм применяется рекурсивно к ограничению V << U.

Если CAP#1 считается подстановочным знаком (который является захватом), то это правило применяется, и вывод продолжает рекурсивно с U как Foo<A, B> и V как Foo<CAP#2, CAP#3>. Как и выше, это даст A = CAP#2 и B = CAP#3.

Однако, если CAP#1 является просто переменной типа, то ни одно из правил не рассматривает его границы. Возможно, эта концессия в конце раздела в спецификации относится к таким случаям:

Алгоритм вывода типа следует рассматривать как эвристику, предназначенную для выполнения на практике. Если он не сможет вывести желаемый результат, вместо него могут использоваться явные параметры типа.

Очевидно, подстановочные знаки нельзя использовать в качестве явных параметров типа.: - (

4b9b3361

Ответ 1

Проблема заключается в том, что вы начинаете со следующего ограничения вывода:

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

Что дает вам решение для C, а именно C = # 1.

Затем вам нужно проверить, соответствует ли C объявленным границам - граница C - Foo, поэтому вы заканчиваете эту проверку:

#1 <: Foo<A,B>

который можно переписать как

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

отсюда:

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

Теперь здесь компилятор делает преобразование захвата LHS (здесь, где генерируются # 2 и # 3):

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

Что означает

A = #2

B = #3

Итак, мы имеем, что наше решение {A = # 2, B = # 3, C = # 1}.

Это допустимое решение? Чтобы ответить на этот вопрос, нам нужно проверить, совместимы ли предполагаемые типы с границами переменных вывода, после подстановки типов, поэтому:

[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

Следовательно, ошибка.

Спецификация не указана, когда речь идет о взаимодействии между выводами и захваченными типами, поэтому вполне нормально (но не хорошо!) иметь разные типы поведения при переключении между разными компиляторами. Однако некоторые из этих проблем обрабатываются как с точки зрения компилятора, так и с точки зрения JLS, поэтому такие проблемы должны быть исправлены в среднесрочной перспективе.

Ответ 2

Две вещи, которые я заметил:

  • CAP#1 не является подстановочным знаком, это переменная типа из конверсия захвата.

  • На первом этапе JLS упоминает, что U - это выражение типа, а Tj - параметр типа. JLS явно не определяет, что такое выражение типа, но мое чувство кишки состоит в том, что оно включает границы параметра типа. Если это так, U будет C extends Foo<A,B>, а V будет CAP#1 extends Foo<CAP#2, CAP#3>. Следуя алгоритму вывода типа:

V = UC = CAP#1 И Foo<CAP#2, CAP#3> = Foo<A, B>

Вы можете продолжить применять алгоритм вывода типа к указанному выше, и в итоге вы получите A= CAP#2 и B=CAP#3.

Я считаю, что вы обнаружили ошибку с компилятором Oracle