Как работает приведение этого объекта к универсальному типу?

Насколько я понимаю, универсальные типы являются инвариантами, поэтому, если у нас есть B в качестве подтипа A, то List<B> имеет никакого отношения к List<A>. Таким образом, приведение не будет работать для List<A> и List<B>.

Из Effective Java Third Edition у нас есть следующий фрагмент кода:

// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTIFY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identifyFunction() {
    return (UnaryOperator<T>) IDENTIFY_FN; //OK But how, why?
}

public static void main(String[] args) {
    String[] strings = {"a", "b", "c"};
    UnaryOperator<String> sameString = identifyFunction();
    for (String s : strings) {
        System.out.println(sameString.apply(s));
    }
}

Здесь я в замешательстве. Мы привели IDENTIFY_FN, тип которого UnaryOperator<Object>, к UnaryOperator<T>, который имеет другой параметр типа.

Когда происходит стирание типа, String является подтипом Object, но, насколько я знаю, UnaryOperator<String> не является подтипом UnaryOperator<Object>.

Объект и T как-то связаны? И как происходит литье в этом случае?

Ответ 1

Обобщения не существуют во время выполнения. Во время выполнения каждый UnaryOperator<T> является UnaryOperator<Object>. Приведение необходимо для успокоения компилятора во время компиляции. Во время выполнения это бессмысленно.

Ответ 2

Это приведение компилируется, потому что это частный случай сужающего преобразования. (Согласно §5.5, сужающие преобразования являются одним из типов преобразований, разрешенных для приведения, поэтому большая часть этого ответа будет сосредоточена на правилах сужения преобразований.)

Обратите внимание, что хотя UnaryOperator<T> не является подтипом UnaryOperator<Object> (поэтому приведение не является "понижением"), оно все же считается сужающим преобразованием. Из §5.6.1:

Преобразование сужающего ссылки обрабатывает выражения ссылочного типа S как выражения другого ссылочного типа T, где S не является подтипом T [...] В отличие от расширяющегося преобразования ссылок, типы не должны быть напрямую связаны. Однако существуют ограничения, которые запрещают преобразование между определенными парами типов, когда можно статически доказать, что никакое значение не может быть обоих типов.

Некоторые из этих "боковых" приведений заканчиваются неудачей из-за специальных правил, например, следующее не удастся:

List<String> a = ...;
List<Double> b = (List<String>) a;

В частности, это дается правилом в §5.1.6.1, которое гласит, что:

  • Если существует параметризованный тип X который является супертипом T, и параметризованный тип Y который является супертипом S, так что стирания X и Y одинаковы, то X и Y не доказуемо различимы (§4.5),

    Используя в качестве примера типы из пакета java.util, не существует сужающего ссылочного преобразования из ArrayList<String> в ArrayList<Object> или наоборот, поскольку аргументы типа String и Object различимы доказуемо. По той же причине не существует сужающего преобразования ссылок из ArrayList<String> в List<Object> или наоборот. Отказ от доказуемо различных типов - это простой статический элемент, предотвращающий "глупые" сужающие преобразования ссылок.

Другими словами, если a и b имеют общий супертип с одним и тем же стиранием (в данном случае, например, List), то они должны быть тем, что JLS называет "доказуемо отличным", заданным в §4.5:

Два параметризованных типа доказуемо различны, если выполняется одно из следующих условий:

  • Они являются параметризацией различных объявлений обобщенных типов.

  • Любой из их аргументов типа доказуемо различим.

И §4.5.1:

Два типа аргументов доказуемо различны, если выполняется одно из следующих условий:

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

  • Один аргумент типа - это переменная типа или подстановочный знак с верхней границей (от преобразования захвата, если необходимо) S; и другой аргумент типа T не является переменной типа или подстановочным знаком; и ни один |S| <: |T| |S| <: |T| ни |T| <: |S| |T| <: |S| ,

  • Каждый аргумент типа является переменной типа или подстановочным знаком, с верхними границами (от преобразования захвата, если необходимо) S и T; и ни один |S| <: |T| |S| <: |T| ни |T| <: |S| |T| <: |S| ,

Таким образом, учитывая приведенные выше правила, List<String> и List<Double> доказуемо различаются (через 1-е правило из 4.5.1), потому что String и Double являются аргументами разных типов.

Однако UnaryOperator<T> и UnaryOperator<Object> не могут быть доказуемо различимы (через 2-е правило из 4.5.1), потому что:

  1. Один аргумент типа - это переменная типа (T, с верхней границей Object.)

  2. Граница переменной этого типа совпадает с аргументом типа для другого типа (Object).

Поскольку UnaryOperator<T> и UnaryOperator<Object> не доказуемо различимы, сужающее преобразование разрешено, поэтому приведение компилируется.


Один способ подумать о том, почему компилятор допускает некоторые из этих приведений, но не другие: в случае переменной типа он не может доказать, что T определенно не является Object. Например, у нас может быть такая ситуация:

UnaryOperator<String> aStringThing = Somewhere::doStringThing;
UnaryOperator<Double> aDoubleThing = Somewhere::doDoubleThing;

<T> UnaryOperator<T> getThing(Class<T> t) {
    if (t == String.class)
        return (UnaryOperator<T>) aStringThing;
    if (t == Double.class)
        return (UnaryOperator<T>) aDoubleThing;
    return null;
}

В этих случаях мы на самом деле знаем, что приведение правильное, пока никто не делает что-то смешное (например, неконтролируемое приведение аргумента Class<T>).

Так что в общем случае приведения к UnaryOperator<T> мы могли бы на самом деле делать что-то законное. По сравнению со случаем приведения List<String> к List<Double>, мы можем довольно авторитетно сказать, что это всегда неправильно.

Ответ 3

JLS позволяет такое приведение:

Преобразование из типа S в параметризованный тип T не проверяется, если не выполнено хотя бы одно из следующих условий:

  • S <: T

  • Все аргументы типа T являются неограниченными подстановочными знаками.

  • [...]

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

Кроме того, во время процесса стирания типа, identifyFunction и IDENTIFY_FN компилируется в:

private static UnaryOperator IDENTIFY_FN;

public static UnaryOperator identifyFunction() {
    return IDENTIFY_FN; // cast is removed
}

и checkcast добавляется на сайт вызова:

System.out.println(sameString.apply(s));
                         ^
INVOKEINTERFACE java/util/function/UnaryOperator.apply (Ljava/lang/Object)Ljava/lang/Object
CHECKCAST java/lang/String
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

checkcast успешно, потому что функция тождества возвращает свой аргумент без изменений.

Ответ 4

В ролях

return (UnaryOperator<T>) IDENTIFY_FN;

в основном сводится к приведению к необработанному типу UnaryOperator, потому что T стирается во время выполнения и игнорируется для целей приведения во время компиляции. Вы можете привести универсальный тип к его необработанному типу (по причинам обратной совместимости), но вы должны получить предупреждение "unchecked".

Это также будет работать, например:

UnaryOperator<String> foo = (UnaryOperator) IDENTITY_FN;