Как компилятор Java выбирает тип времени выполнения для параметризованного типа с несколькими границами? - PullRequest
0 голосов
/ 04 мая 2018

Я бы хотел лучше понять, что происходит, когда компилятор Java встречает вызов метода, подобного приведенному ниже.

<T extends AutoCloseable & Cloneable>
void printType(T... args) {
    System.out.println(args.getClass().getComponentType().getSimpleName());
}

// printType() prints "AutoCloseable"

Мне ясно, что во время выполнения нет типа <T extends AutoCloseable & Cloneable>, поэтому компилятор делает наименьшую ошибку, которую он может сделать, и создает массив с типом одного из двух ограничивающих интерфейсов, отбрасывая другой.

В любом случае, если порядок интерфейсов переключен, результат все тот же.

<T extends Cloneable & AutoCloseable>
void printType(T... args) {
    System.out.println(args.getClass().getComponentType().getSimpleName());
}

// printType() prints "AutoCloseable"

Это побудило меня провести еще одно расследование и посмотреть, что происходит, когда меняются интерфейсы. Мне кажется, что компилятор использует какое-то правило строгого порядка, чтобы решить, какой интерфейс является наиболее важным , и порядок появления интерфейсов в коде не играет роли.

<T extends AutoCloseable & Runnable>                             // "AutoCloseable"
<T extends Runnable & AutoCloseable>                             // "AutoCloseable"
<T extends AutoCloseable & Serializable>                         // "Serializable"
<T extends Serializable & AutoCloseable>                         // "Serializable"
<T extends SafeVarargs & Serializable>                           // "SafeVarargs"
<T extends Serializable & SafeVarargs>                           // "SafeVarargs"
<T extends Channel & SafeVarargs>                                // "Channel"
<T extends SafeVarargs & Channel>                                // "Channel"
<T extends AutoCloseable & Channel & Cloneable & SafeVarargs>    // "Channel"

Вопрос: Как компилятор Java определяет тип компонента массива varargs параметризованного типа при наличии нескольких границ?

Я даже не уверен, что JLS говорит что-либо об этом, и ни одна из информации, которую я нашел путем поиска в Google, не охватывает эту конкретную тему.

Ответы [ 2 ]

0 голосов
/ 18 мая 2018

Это очень интересный вопрос. Соответствующей частью спецификации является §15.12.4.2. Оцените аргументы :

Если вызываемый метод является методом переменной arity m, он обязательно имеет n > 0 формальных параметров. Последний формальный параметр m обязательно имеет тип T[] для некоторых T, и m обязательно вызывается с k ≥ 0 действительными выражениями аргумента.

Если m вызывается с k n фактическими выражениями аргумента или, если m вызывается с k = n фактические выражения аргумента и тип k '-ого выражения аргумента несовместимы с присваиванием T[], тогда список аргументов (e<sub>1</sub>, ..., e<sub>n-1</sub>, e<sub>n</sub>, ..., e<sub>k</sub>) оценивается так, как если бы оно было записано как (e<sub>1</sub>, ..., e<sub>n-1</sub>, new | T[] | { e<sub>n</sub>, .. ., e<sub>k</sub> }), где | T[] | обозначает стирание (§4.6) T[].

Интересно неясно, что на самом деле означает «какой-то * 1047». Простейшим и наиболее простым решением будет объявленный тип параметра вызванного метода; это было бы совместимо с назначением, и нет никакого фактического преимущества использования другого типа. Но, как мы знаем, javac не идет по этому пути и использует какой-то общий базовый тип всех аргументов или выбирает некоторые границы в соответствии с неким неизвестным правилом для типа элемента массива. В настоящее время вы можете даже найти некоторые приложения в дикой природе, полагаясь на это поведение, предполагая получить некоторую информацию о фактическом T во время выполнения путем проверки типа массива.

Это приводит к некоторым интересным последствиям:

static AutoCloseable[] ARR1;
static Serializable[]  ARR2;
static <T extends AutoCloseable & Serializable> void method(T... args) {
    ARR1 = args;
    ARR2 = args;
}
public static void main(String[] args) throws Exception {
    method(null, null);
    ARR2[0] = "foo";
    ARR1[0].close();
}

javac решает создать массив фактического типа Serializable[] здесь, несмотря на то, что тип параметра метода равен AutoClosable[] после применения стирания типа, что является причиной, по которой назначение String возможно во время выполнения , Таким образом, произойдет сбой только в последнем операторе при попытке вызвать для него метод close() с

Exception in thread "main" java.lang.IncompatibleClassChangeError: Class java.lang.String does not implement the requested interface java.lang.AutoCloseable

Здесь обвиняется класс String, хотя мы могли бы поместить любой объект Serializable в массив, поскольку фактическая проблема заключается в том, что поле static формально объявленного типа AutoCloseable[] ссылается на объект фактический тип Serializable[].

Хотя это специфическое поведение JSM HotSpot, которое мы когда-либо заходили так далеко, поскольку его верификатор не проверяет назначения, когда задействованы типы интерфейса (включая массивы типов интерфейса), но откладывает проверку, реализует ли интерфейс настоящий класс до последнего возможного момента при попытке вызвать на нем интерфейсный метод.

Интересно, что приведения типов строги, когда появляются в файле класса:

static <T extends AutoCloseable & Serializable> void method(T... args) {
    AutoCloseable[] a = (AutoCloseable[])args; // actually removed by the compiler
    a = (AutoCloseable[])(Object)args; // fails at runtime
}
public static void main(String[] args) throws Exception {
    method();
}

Хотя решение javac для Serializable[] в приведенном выше примере кажется произвольным, должно быть ясно, что независимо от того, какой тип он выбирает, одно из назначений полей будет возможно только в JVM с проверкой слабого типа. Мы также можем выделить более фундаментальный характер проблемы:

// erased to method1(AutoCloseable[])
static <T extends AutoCloseable & Serializable> void method1(T... args) {
    method2(args); // valid according to generic types
}
// erased to method2(Serializable[])
static <T extends Serializable & AutoCloseable> void method2(T... args) {
}
public static void main(String[] args) throws Exception {
    // whatever array type the compiler picks, it would violate one of the erased types
    method1();
}

Хотя это на самом деле не отвечает на вопрос о том, что использует настоящее правило javac (кроме того, что оно использует «some T»), оно подчеркивает важность обработки массивов, созданных для параметра varargs , как предназначено: временное хранилище (не присваивать полям) произвольного типа, о котором вам лучше не беспокоиться.

0 голосов
/ 07 мая 2018

Как правило, когда компилятор сталкивается с вызовом параметризованного метода, он может вывести тип ( JSL 18.5.2 ) и может создать в вызывающей программе правильно типизированный массив vararg.

Правила - это в основном технические способы сказать «найти все возможные типы ввода и проверить их» (такие случаи, как void, троичный оператор или лямбда). Остальное - здравый смысл, такой как использование наиболее специфического общего базового класса ( JSL 4.10.4 ). Пример:

public class Test {
   private static class A implements AutoCloseable, Runnable {
         @Override public void close () throws Exception {}
         @Override public void run () {} }
   private static class B implements AutoCloseable, Runnable {
         @Override public void close () throws Exception {}
         @Override public void run () {} }
   private static class C extends B {}

   private static <T extends AutoCloseable & Runnable> void printType( T... args ) {
      System.out.println( args.getClass().getComponentType().getSimpleName() );
   }

   public static void main( String[] args ) {
      printType( new A() );          // A[] created here
      printType( new B(), new B() ); // B[] created here
      printType( new B(), new C() ); // B[] which is the common base class
      printType( new A(), new B() ); // AutoCloseable[] - well...
      printType();                   // AutoCloseable[] - same as above
   }
}
  • JSL 18.2 определяет, как обрабатывать ограничения для вывода типа, например, AutoCloseable & Channel уменьшается до Channel. Но правила не помогают ответить на этот вопрос.

Получение AutoCloseable[] из вызова может выглядеть странно, конечно, потому что мы не можем сделать это с помощью кода Java. Но на самом деле фактический тип не имеет значения. На уровне языка args - это T[], где T - это «виртуальный тип», который является как A, так и B ( JSL 4.9 ).

Компилятору просто нужно убедиться, что его использование удовлетворяет всем ограничениям, и тогда он знает, что логика исправна, и ошибки типа не будет (именно так разработан Java generic). Конечно, компилятору все еще нужно создать массив real , и для этой цели он создает «универсальный массив». При этом предупреждение "unchecked generic array creation" ( JLS 15.12.4.2 ).

Другими словами, если вы передаете только AutoCloseable & Runnable и вызываете только Object, AutoCloseable и Runnable методы в printType, фактический тип массива не имеет значения. На самом деле, байт-коды printType будут одинаковыми, независимо от типа передаваемого массива.

Поскольку printType не заботится о типе массива vararg, getComponentType() не имеет и не должно иметь значения. Если вы хотите получить интерфейсы, попробуйте getGenericInterfaces(), который возвращает массив.

  • Из-за стирания типа ( JSL 4.6 ) порядок интерфейсов T влияет на ( JSL 13.1 ) подпись скомпилированного метода и байт-код. Будет использован первый интерфейс AutoClosable, например, проверка типа не будет выполняться при вызове AutoClosable.close() в printType.
  • Но это не связано с интерференцией типов вызовов методов вопроса, т. Е. Почему создается и передается AutoClosable[]. Многие типы безопасности проверяются перед стиранием, поэтому порядок не влияет на безопасность типов. Я думаю, что это часть того, что JSL подразумевает под "The order of types... is only significant in that the erasure ... is determined by the first type" ( JSL 4.4 ). Это означает, что заказ в противном случае незначителен.
  • Независимо от того, это правило стирания действительно приводит к угловым случаям, таким как добавление printType(AutoCloseable[]) вызывает ошибку компиляции, тогда как добавление printType( Runnable[]) не делает. Я считаю, что это неожиданный побочный эффект и действительно выходит за рамки.
  • P.S. Копание слишком глубоко может вызвать безумие , учитывая, что я думаю, что я Овис Овен , просмотр источника в сборку и пытается ответить на английском вместо J answeŕS͡L̴̀. Мой показатель здравомыслия b҉ȩyon̨d͝ r̨̡͝e̛a̕l̵ numb͟ers͡ . Вернуться. ̠̝͕B̭̳͠͡ͅẹ̡̬̦̙f͓͉̼̻o̼͕̱͎̬̟̪r҉͏̛̣̼͙͍͍̠̫͙ȩ̵̮̟̱̫͚ ̢͚̭̹̳̣̩̱͠..t̷҉̛̫͔͉̥͎̬ò̢̱̪͉̲͎͜o̭͈̩̖̭̬ .. ̮̘̯̗l̷̞͍͙̻̻͙̯̣͈̳͓͇a̸̢̢̰͓͓̪̳͉̯͉̼͝͝t̛̥̪̣̹̬͔̖͙̬̩̝̰͕̖̮̰̗͓̕͢ę̴̹̯̟͉̲͔͉̳̲̣͝͞.̬͖͖͇͈̤̼͖́͘͢.͏̪̱̝̠̯̬͍̘̣̩͉̯̹̼͟͟͠.̨͠҉̬̘̹ͅ
...