Как компилятор Java выбирает тип времени выполнения для параметризованного типа с несколькими ограничениями?

Я хотел бы лучше понять, что произойдет, когда компилятор 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, не охватывает эту тему.

Ответ 1

Как правило, когда компилятор встречает вызов параметризованного метода, он может отображать тип (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-ролик). Конечно, компилятору все равно нужно создать реальный массив, и с этой целью он создает "общий массив". Таким образом, предупреждение " unchecked generic array creation " (JLS 15.12.4.2).

Другими словами, до тех пор, пока вы передаете только AutoCloseable & Runnable и вызывается только методы Object, AutoCloseable и Runnable в printType, фактический тип массива не имеет значения. Фактически, bytecodes 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[]). Я считаю, что это неожиданный побочный эффект и действительно выходит за рамки.
  • PS копания слишком глубоко может привести к безумию, учитывая, что я думаю, что я OVIS ницы, просмотреть исходный код в сборке, и изо всех сил, чтобы ответить на английском языке, а не JSL. Мой рейтинг здравомыслия - b҉ȩyon̨d͝ r̨̡͝e̛a̕l̵ numb͟ers. Вернуться. ̠̝͕B̭̳̭̳̭̳͠͠͠ẹ̡̬̦̙ẹ̡̬̦̙͓͉̼̻͓͉̼̻̼͕͎̬̟̪̼͕͎̬̟̪҉͏̛̣̼͙͍͍̠̫͙ȩ̵̮̟̫͚ ̢͚̭̹̳̣̩͠..t̷҉̛̫͔͉̥͎̬ò̢̪͉͎͜o̭͈̩̖̭̬.. ̮̘̯̗l̷̞͍͙̻̻͙̯̣͈̳͓͇a̸̢̢̰͓͓̪̳͉̯͉̼͝͝t̛̥̪̣̹̬͔̖͙̬̩̝̰͕̖̮̰̗͓̕͢ę̴̹̯̟͉͔͉̳̣͝͞.̬͖͖͇͈̤̼͖͘͢.͏̪̝̠̯̬͍̘̣̩͉̯̹̼͟͟͠.̨͠҉̬̘̹

Ответ 2

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

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

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

Его интересная неопределенность в отношении того, что такое "некоторый T ". Простейшим и наиболее прямолинейным решением будет объявленный тип параметра вызываемого метода; это будет совместимость с назначением, и нет фактического преимущества использования другого типа. Но, как мы знаем, 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[] после применения стирания 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 с проверкой типа lax. Мы могли бы также подчеркнуть более фундаментальный характер проблемы:

// 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 (кроме того, что он использует "некоторое T "), он подчеркивает важность обработки массивов, созданных для параметра varargs, как предполагалось: временное хранилище (не назначаемое полям) произвольного типа вам лучше не беспокоиться.