Как работает приведение этого объекта к универсальному типу? - PullRequest
0 голосов
/ 16 ноября 2018

Насколько я понимаю, универсальные типы инвариантны , поэтому, если у нас 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>.

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

Ответы [ 4 ]

0 голосов
/ 16 ноября 2018

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

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

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

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

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

В частности, это задается правилом в & sect; 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 называет «доказуемо отличным», дано & sect; 4,5 :

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

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

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

И & 4.5; :

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

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

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

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

0 голосов
/ 16 ноября 2018

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

0 голосов
/ 16 ноября 2018

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 успешно, потому что функция тождества возвращает свой аргумент без изменений.

0 голосов
/ 16 ноября 2018

Приведение

return (UnaryOperator<T>) IDENTIFY_FN;

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

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

UnaryOperator<String> foo = (UnaryOperator) IDENTITY_FN;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...