Java 8 autoboxing + generics: различное поведение с переменной по сравнению с методом

Я нашел фрагмент кода, который после переключения с Java 7 на Java 8 прекратил компиляцию. В нем нет ни одного нового материала Java 8, такого как лямбда или потоки.

Я сузил проблемный код до следующей ситуации:

GenericData<Double> g = new GenericData<>(1d);
Double d = g == null ? 0 : g.getData(); // type error!!!

Вероятно, вы можете предположить, что конструктор GenericData имеет один параметр этого общего типа, а метод getData() возвращает только этот общий тип. (Полный исходный код см. Ниже.)

Теперь меня беспокоит то, что в Java 7 этот код скомпилирован просто отлично, тогда как с Java 8 я получаю следующую ошибку:

CompileMe.java:20: error: incompatible types: bad type in conditional expression
Double d = g == null ? 0 : g.getData();
                       ^
int cannot be converted to Double

Кажется, что Java 7 удалось выполнить переход от int → double → Double, но сбой Java 8 с попыткой немедленно перейти от int → Double.

В частности, я считаю, что Java 8 действительно принимает код, когда я меняю его с getData() на data, т.е. получаю доступ к значению GenericData с помощью самой переменной вместо метода getter:

Double d2 = g == null ? 0 : g.data; // now why does this work...

Итак, у меня есть два вопроса:

  • Почему Java 8 не выдает такие типы, как Java 7, и не меняет мой int, чтобы удвоить значение до автоматического использования double-double?
  • Почему эта проблема возникает только с общим методом, но не с общей переменной?

Полный исходный код:

public class CompileMe {
    public void foo() {
        GenericData<Double> g = new GenericData(1d);
        Double d = g == null ? 0 : g.getData(); // type error!!!
        Double d2 = g == null ? 0 : g.data; // now why does this work...
    }
}

class GenericData<T> {
    public T data;
    public GenericData(T data) {
        this.data = data;
    }
    public T getData() {
        return data;
    }
}

Чтобы протестировать его, запустите компилятор следующим образом:

javac -source 1.7 -target 1.7 CompileMe.java   # ok (just warnings)
javac -source 1.8 -target 1.8 CompileMe.java   # error (as described above)

Наконец, если это имеет значение: я запускаю Windows 8 и Java 1.8.0_112 (64-разрядный).

Ответ 1

Вызовы вызова метода являются особенными в том, что они могут быть Poly-выражениями, подлежащими типу печати. ​​

Рассмотрим следующие примеры:

static Double aDouble() {
    return 0D;
}
…
Double d = g == null ? 0 : aDouble();

это компилируется без проблем

static <T> T any() {
    return null;
}
…
Double d = g == null ? 0 : any();

здесь вызов any() является выражением Poly, и компилятор должен сделать вывод T := Double. Это воспроизводит ту же ошибку.

Это первая несогласованность. Хотя ваш метод getData() относится к параметру типа T GenericData, это не общий метод (для определения того, что T здесь Double, существует/не должно быть никакого вывода типа.

JLS §8.4.4. Общие методы

Метод является общим, если он объявляет одну или несколько переменных типа

getData() не объявляет переменные типа, он использует только один.

JLS §15.12. Выражения вызова метода:

Выражение вызова метода - это выражение poly, если все верно:

  • ...
  • Метод, который должен быть вызван, как определено в следующих подразделах, является общим (§8.4.4) и имеет тип возврата, в котором упоминается хотя бы один из параметров типа метода.

Поскольку этот вызов метода не является поли-выражением, он должен вести себя как пример с вызовом aDouble(), а не any().

Но обратите внимание на §15.25.3:

Обратите внимание, что условное условное выражение не должно содержать poly-выражение в качестве операнда, чтобы быть поли-выражением. Это поли выражение просто в силу контекста, в котором он появляется. Например, в следующем коде условное выражение является политическим выражением, и каждый операнд считается в контексте назначения таргетинга Class<? super Integer>:

Class<? super Integer> choose(boolean b,
                              Class<Integer> c1,
                              Class<Number> c2) {
    return b ? c1 : c2;
}

Итак, это условное условное или числовое условное выражение?

§15.25. Условный оператор?: говорит:

Существует три вида условных выражений, классифицированных в соответствии со вторым и третьим выражениями операндов: булевыми условными выражениями, числовыми условными выражениями и условными условными выражениями. Правила классификации заключаются в следующем:

  • Если и второе, и третье выражения операндов являются булевыми выражениями, условное выражение является булевым условным выражением....
  • Если и второе, и третье выражения операнда представляют собой числовые выражения, условное выражение представляет собой числовое условное выражение.
    Для классификации условных выражений следующие выражения представляют собой числовые выражения:
    • Выражение автономной формы (§15.2) с типом, который преобразуется в числовой тип (§4.2, п. 5.1.8).
    • Числовое выражение в скобках (§15.8.5).
    • выражение для создания экземпляра класса (§15.9) для класса, который можно преобразовать в числовой тип.
    • Вызов вызова метода (§15.12), для которого выбранный наиболее специфический метод (§15.12.2.5) имеет тип возврата, который можно преобразовать в числовой тип.
    • Числовое условное выражение.
  • В противном случае условное выражение является условным условным выражением.

Таким образом, согласно этим правилам, не исключая генераторные вызовы методов, все показанные условные выражения являются числовыми условными выражениями и должны работать, поскольку только "в противном случае" они должны рассматриваться как условное условное выражение. Версия Eclipse, которую я тестировал, скомпилировала все из них, не сообщая о какой-либо ошибке.

Это приводит к странной ситуации, когда для случая any() нам нужна целевая типизация, чтобы узнать, что она имеет числовой тип возврата и выдает, что условное выражение представляет собой числовое условное выражение, то есть автономное выражение. Заметим, что для булевых условных выражений есть замечание:

Обратите внимание, что для универсального метода это тип перед созданием аргументов типа метода.

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

Но, как сказано, это применимо только к примеру any(), так как метод getData() не является общим.

Ответ 2

Это, похоже, известная проблема компилятора Oracle: Идентификатор ошибки: JDK-8162708

Цитата:

A ОПИСАНИЕ ПРОБЛЕМЫ:
Если у вас есть метод в общем классе, объявленном следующим образом:

class Foo<T> {
  public T getValue() {
    // returns a value ...
  }
}

и вы вызываете метод выше внутри тернарного оператора, как следует

Foo<Integer> foo = new Foo<>();
Float f = new Random().nextBoolean() ? foo.getValue() : 0f;

вы получаете синтаксическую ошибку от компилятора javac 1.8.

Но приведенный выше код компилируется без ошибок и предупреждений с javac 1.7 и 1.9.

Разрешение: Неразрешенный

Затронутые версии: 8

Из комментариев:

Эта проблема применима только к 8u, нет проблем в 7 и 9

Ответ 3

Должны утверждать, что это не ответ, а просто рассуждение. С моим кратким опытом в компиляторе (не Javac specific), он мог бы иметь какое-то отношение к тому, как анализируется код.

В следующем декомпилированном коде вы видите либо вызов метода GenericData.getData:()Ljava/lang/Object, либо ссылку на поле GenericData.data:Ljava/lang/Object, они оба сначала получают значение/метод с возвращенным объектом, а затем cast.

  stack=4, locals=4, args_size=1
     0: new           #2                  // class general/GenericData
     3: dup
     4: dconst_1
     5: invokestatic  #3                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
     8: invokespecial #4                  // Method general/GenericData."<init>":(Ljava/lang/Object;)V
    11: astore_1
    12: aload_1
    13: ifnonnull     23
    16: dconst_0
    17: invokestatic  #3                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
    20: goto          30
    23: aload_1
    24: invokevirtual #5                  // Method general/GenericData.getData:()Ljava/lang/Object;
    27: checkcast     #6                  // class java/lang/Double
    30: astore_2
    31: aload_1
    32: ifnonnull     39
    35: dconst_0
    36: goto          49
    39: aload_1
    40: getfield      #7                  // Field general/GenericData.data:Ljava/lang/Object;
    43: checkcast     #6                  // class java/lang/Double
    46: invokevirtual #8                  // Method java/lang/Double.doubleValue:()D
    49: invokestatic  #3                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
    52: astore_3
    53: return

Если сравнить выражение тернарного оператора с эквивалентным if-else:

Integer v = 10;
v = v != null ? 1 : 0;

     0: bipush        10
     2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     5: astore_1
     6: aload_1
     7: ifnull        14
    10: iconst_1
    11: goto          15
    14: iconst_0
    15: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    18: astore_1
    19: return

Integer v = 10;
if (v != null)
    v = 1;
else
    v = 0;

     0: bipush        10
     2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     5: astore_1
     6: aload_1
     7: ifnull        18
    10: iconst_1
    11: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    14: astore_1
    15: goto          23
    18: iconst_0
    19: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    22: astore_1
    23: return

В обеих версиях нет существенной разницы. Поэтому я не думаю, что есть скрытый секрет, делающий всю черную магию. Это результат того, как компилятор разбирает все выражение и основывается на контексте, чтобы выяснить тип, чтобы сделать все компоненты одинаково счастливыми. Например.

Double val = 0; // compilation error: context is clear, 0 is an integer, so Integer.valueOf(i), but don't match expected type - Double
val = 0 + g.getData(); // OK, enough context to figure out the type should be Double

Тем не менее, путаница заключается в том, почему генерируется общее поле, но не общий метод...

val = val == null ? 0 : g.data; // OK
val = val == null ? 0 : g.getData(); // Compilation error

РЕДАКТИРОВАТЬ: документ, цитируемый Хольгером, кажется хорошим объяснением.

Ответ 4

CompileMe.java:4: ошибка: несовместимые типы: плохой тип в

условное выражение

      Double d = g == null ? 0 : g.getData(); // type error!!!
int cannot be converted to Double

Здесь 0 является целым числом, и вы помещаете его в Double.

Попробуйте это

public class CompileMe {
    public static void main(String[] args) {
        GenericData<Double> g = new GenericData(1d);
        Double d = g == null ? 0d : g.getData(); // type error!!!
        Double d2 = g == null ? 0d : g.data; // now why does this work...
        System.out.println(d);
        System.out.println(d2);
    }
}

class GenericData<T> {
    public T data;
    public GenericData(T data) {
        this.data = data;
    }
    public T getData() {
       return data;
    }
}

Или используйте двойной литерал вместо Double wrapper class

    public class CompileMe {
    public static void main(String[] args) {
        GenericData<Double> g = new GenericData(1d);
        double d = g == null ? 0 : g.getData(); // type error!!!
        double d2 = g == null ? 0 : g.data; // now why does this work...
        System.out.println(d);
        System.out.println(d2);
    }
}

class GenericData<T> {
    public T data;
    public GenericData(T data) {
        this.data = data;
    }
    public T getData() {
        return data;
    }
}

Потому что расширение и бокс не будут происходить одновременно.