Неожиданное добавление строки в список‹Целые›

Я не понимаю, как компилятор обрабатывает следующий код, поскольку он выводит Test, а я ожидал ошибку.

List<Integer> b = new ArrayList<Integer>();
List a = b;
a.add("test");
System.out.println(b.get(0));

Я надеялся, что кто-нибудь скажет мне, какие точные шаги выполняет компилятор при выполнении кода, чтобы я мог понять вывод. Мое текущее понимание таково:

  1. Компилятор проверяет во время компиляции, существует ли метод добавления, поддерживающий тип аргумента, в классе List, который является add(Object e) в качестве необработанного типа.
  2. Однако во время выполнения он пытается вызвать add(Object e) из фактического объекта List‹Integer>, который не содержит этот метод, поскольку фактический объект не имеет необработанного типа, а вместо этого содержит метод < strong>добавить(целое число e).

Если в фактическом объекте List‹Integer> нет метода add(Object e) , как он каким-то образом добавляет строку в список целых чисел?


person John    schedule 06.10.2018    source источник
comment
Список не выполняет проверку типов. Ведь это всего лишь массив ссылок, так что да, работать будет, но небезопасно!   -  person Willem Van Onsem    schedule 06.10.2018
comment
Список a является необработанным, что означает, что в нем хранится Object ссылок. Таким образом, вы можете добавить к нему любой класс Java, поскольку все неявно расширяет Object. Ошибка возникнет, если вы попытаетесь добавить String в список b. Это приведет к сбою во время компиляции, поскольку сработают дженерики, чтобы этого не произошло.   -  person Tim Biegeleisen    schedule 06.10.2018
comment
Хороший вопрос, вы довольно близки. Ваш пункт 1 на высоте, но в Java есть что-то под названием стирание типа, поэтому ваш пункт 2 фактически не применяется.   -  person Ray Toal    schedule 06.10.2018
comment
Если в реальном объекте List‹Integer› нет метода add(Object e) , вы ошибаетесь. Это List, во время выполнения это List<Object>, так что есть есть add(Object). Компилятор вставляет приведение типа, и это то, что не удастся.   -  person Elliott Frisch    schedule 06.10.2018
comment
Обобщения — это проверка во время компиляции, и, выполняя List a = b, вы отключаете эту проверку во время компиляции.   -  person Peter Lawrey    schedule 06.10.2018
comment
тот же курс, что и stackoverflow.com/q/52672926/85421?   -  person user85421    schedule 06.10.2018


Ответы (2)


Вы совсем рядом. Время компиляции проверяет все:

a имеет тип List, поэтому вызов

a.add("test");

выходит. b относится к типу (времени компиляции) ArrayList<Integer>, поэтому

b.get(0)

тоже проверяет. Обратите внимание, что проверки выполняются только для типов переменных времени компиляции. Когда компилятор видит a.add("test"), он не знает значение времени выполнения объекта, на который ссылается переменная a. В общем, на самом деле не может (об этом есть результат в теоретической информатике), хотя анализ типа потока управления может уловить много таких вещей. Такие языки, как TypeScript, могут делать удивительные вещи во время компиляции.

Теперь вы можете предположить, что во время выполнения такие вещи могут быть проверены. Увы, в Java они не могут. Java стирает общие типы. Подробности можно найти в статье о стирании типов Java. TL;DR заключается в том, что List<Integer> во время компиляции становится необработанным List во время выполнения. У JVM не было способа «овеществить» дженерики (хотя в других языках они есть!), поэтому, когда дженерики были введены, было принято решение, что Java просто удалит дженерики. Таким образом, во время выполнения в вашем коде нет проблем с типом.

Давайте посмотрим на скомпилированный код:

   0: new           #2                  // class java/util/ArrayList
   3: dup
   4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
   7: astore_1
   8: aload_1
   9: astore_2
  10: aload_2
  11: ldc           #4                  // String test
  13: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
  18: pop
  19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  22: aload_1
  23: iconst_0
  24: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
  29: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  32: return

Здесь сразу видно, что проверки типов во время выполнения нет. Таким образом, полный (но, казалось бы, легкомысленный) ответ на ваш вопрос заключается в том, что Java проверяет типы только во время компиляции на основе типов переменных (известных во время компиляции), но параметры универсального типа стираются и код запускается без них.

person Ray Toal    schedule 06.10.2018
comment
Спасибо за ваш комментарий. Я до сих пор не понимаю этого на 100% из-за того, что мне нужно взглянуть на то, как компилятор Java обрабатывает время компиляции и время выполнения объектов. Как только я это полностью пойму, держу пари, твой пост будет иметь для меня гораздо больше смысла. Стирание шрифта также очень поможет. - person John; 06.10.2018
comment
Согласитесь, что это неинтуитивно. Самая важная часть, которую нужно понять, это то, что когда компилятор видит a.add(test), компилятор только спрашивает каков объявленный тип a? Поскольку a объявлен как List, компилятор говорит, что это нормально. Несмотря на то, что объект, на который ссылается a, является List<Integer>, компилятор обычно не знает об этом. Он проверяет выражения только на основе объявленного типа, а не на основе того, какие значения будут присутствовать во время выполнения. Стирание типа объясняет, почему программа запускается, но почему она компилируется, причина в том, что a объявлено только с типом List. Удачной учебы! - person Ray Toal; 07.10.2018

Сюрприз здесь в том, что b.get(0) не имеет проверки во время выполнения. Мы ожидаем, что код будет интерпретирован компилятором как нечто вроде:

System.out.println((Integer)b.get(0)); // throws CCE

Действительно, если бы мы попытались:

Integer str = b.get(0); // throws CCE

мы получили бы время выполнения ClassCastException.

На самом деле мы даже получили бы ту же ошибку, переключая printf вместо println:

System.out.printf(b.get(0)); // throws CCE

Как это имеет смысл?

Это ошибка, которую нельзя исправить из-за обратной совместимости. Если целевой контекст может разрешить удаление контрольного приведения, то он исключается, несмотря на изменение семантики. И в этом случае перегрузка меняется с println(Integer) на println(Object). Хуже того, существует перегрузка println(char[]) с другим поведением!

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

person Tom Hawtin - tackline    schedule 06.10.2018