Статьи

Автобокс, распаковка и NoSuchMethodError

J2SE 5 представил многочисленные возможности языка программирования Java . Одной из таких функций является автобокс и распаковка , функция, которой я пользуюсь почти ежедневно, даже не задумываясь об этом. Это часто удобно (особенно при использовании с коллекциями), но время от времени это приводит к неприятным сюрпризам , « странностям » и « безумию ». В этой записи блога я рассматриваю редкий (но интересный для меня) случай NoSuchMethodError, возникающий в результате смешивания классов, скомпилированных с версиями Java, перед автоматической коробкой / распаковкой с классами, скомпилированными с версиями Java, которые включают автобокс / распаковку.

В следующем листинге кода показан простой класс Sum который мог быть написан до J2SE 5. Он перегружен методами «add», которые принимают разные примитивные числовые типы данных, и каждый экземпляр Sum> просто добавляет все типы чисел, предоставленные ему через любой из его перегруженные методы «добавить».

Sum.java (версия до J2SE 5)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.ArrayList;
 
public class Sum
{
   private double sum = 0;
 
   public void add(short newShort)
   {
      sum += newShort;
   }
 
   public void add(int newInteger)
   {
      sum += newInteger;
   }
 
   public void add(long newLong)
   {
      sum += newLong;
   }
 
   public void add(float newFloat)
   {
      sum += newFloat;
   }
 
   public void add(double newDouble)
   {
      sum += newDouble;
   }
 
   public String toString()
   {
      return String.valueOf(sum);
   }
}

До того, как распаковка стала доступной, любые клиенты из указанного выше класса Sum должны были бы предоставить примитивы для этих методов «добавления» или, если у них были ссылочные эквиваленты примитивов, необходимо преобразовать ссылки в их примитивные аналоги перед вызовом одного из « добавить »методы. На клиентском коде лежит обязанность выполнить это преобразование из ссылочного типа в соответствующий примитивный тип перед вызовом этих методов. Примеры того, как это можно сделать, показаны в следующем листинге кода.

Нет распаковки: клиент преобразует ссылки в примитивы

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
private static String sumReferences(
   final Long longValue, final Integer intValue, final Short shortValue)
{
   final Sum sum = new Sum();
   if (longValue != null)
   {
      sum.add(longValue.longValue());
   }
   if (intValue != null)
   {
      sum.add(intValue.intValue());
   }
   if (shortValue != null)
   {
      sum.add(shortValue.shortValue());
   }
   return sum.toString();
}

Функция автобоксирования и распаковки в J2SE 5 была предназначена для решения этих посторонних усилий, необходимых в таком случае. При распаковке клиентский код может вызывать вышеупомянутые методы «add» с типами ссылок, соответствующими ожидаемым типам примитивов, и ссылки будут автоматически «распаковываться» в примитивную форму, чтобы можно было вызывать соответствующие методы «add». Раздел 5.1.8 («Преобразование распаковки») Спецификации языка Java объясняет, в какие примитивы предоставленные числовые ссылочные типы преобразуются в распаковку, а в Разделе 5.1.7 («Преобразование бокса») этой же спецификации перечисляются типы ссылок, которые помещаются в автобокс. с каждого примитива в автобокс.

boxingAndUnboxing_transparentbg

В этом примере распаковка уменьшила усилия клиента с точки зрения преобразования ссылочных типов в их соответствующие примитивные аналоги перед вызовом методов «добавления» Sum , но это не полностью освободило клиента от необходимости обрабатывать числовые значения перед их предоставлением. , Поскольку ссылочные типы могут иметь значение null , клиент может предоставить нулевую ссылку на один из методов «добавления» Sum и, когда Java пытается автоматически распаковать этот ноль в соответствующий примитив, генерируется исключение NullPointerException . Следующий листинг кода адаптирует это сверху, чтобы указать, что преобразование ссылки на примитив больше не требуется на стороне клиента, но проверка на нулевое значение все еще необходима, чтобы избежать исключения NullPointerException .

Распаковка автоматически скрывает ссылку на Примитив: все еще необходимо проверить на нулевое значение

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
private static String sumReferences(
   final Long longValue, final Integer intValue, final Short shortValue)
{
   final Sum sum = new Sum();
   if (longValue != null)
   {
      sum.add(longValue);
   }
   if (intValue != null)
   {
      sum.add(intValue);
   }
   if (shortValue != null)
   {
      sum.add(shortValue);
   }
   return sum.toString();
}

Требование клиентского кода проверять их ссылки на null перед вызовом методов «add» в Sum может быть тем, чего мы хотим избежать при разработке нашего API. Один из способов устранить эту потребность — изменить методы «add» для явного принятия ссылочных типов, а не примитивных типов. Затем класс Sum может проверять наличие нуля, прежде чем явно или неявно (распаковывать) разыменовать его. Пересмотренный класс Sum с этим измененным и более дружественным к клиенту API показан ниже.

Класс Sum с методами «add», ожидающими ссылки, а не примитивы

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.ArrayList;
 
public class Sum
{
   private double sum = 0;
 
   public void add(Short newShort)
   {
      if (newShort != null)
      {
         sum += newShort;
      }
   }
 
   public void add(Integer newInteger)
   {
      if (newInteger != null)
      {
         sum += newInteger;
      }
   }
 
   public void add(Long newLong)
   {
      if (newLong != null)
      {
         sum += newLong;
      }
   }
 
   public void add(Float newFloat)
   {
      if (newFloat != null)
      {
         sum += newFloat;
      }
   }
 
   public void add(Double newDouble)
   {
      if (newDouble != null)
      {
         sum += newDouble;
      }
   }
 
   public String toString()
   {
      return String.valueOf(sum);
   }
}

Пересмотренный класс Sum является более дружественным к клиенту, потому что он позволяет клиенту передавать ссылку на любой из его методов «add», не заботясь о том, является ли переданная ссылка нулевой или нет. Однако такое изменение API класса Sum может привести к NoSuchMethodError если NoSuchMethodError либо из задействованных классов (клиентский класс или одна из версий класса Sum ) скомпилирован с разными версиями Java. В частности, если клиентский код использует примитивы и скомпилирован с использованием JDK 1.4 или более ранней версии, а класс Sum является последней показанной версией (ожидая ссылок вместо примитивов) и скомпилирован с использованием J2SE 5 или более поздней версии, NoSuchMethodError подобная следующей. («S» указывает, что это был метод «add», ожидающий примитивного short а «V» указывает, что метод возвратил void ).

1
2
Exception in thread "main" java.lang.NoSuchMethodError: Sum.add(S)V
 at Main.main(Main.java:9)

С другой стороны, если клиент скомпилирован с J2SE 5 или более поздней версии и примитивные значения передаются в Sum как в первом примере (предварительная распаковка), а класс Sum скомпилирован в JDK 1.4 или более ранней версии с ожидаемыми методами «add» Примитивы, другая версия NoSuchMethodError встречается. Обратите внимание, что здесь приведена Short ссылка.

1
2
Exception in thread "main" java.lang.NoSuchMethodError: Sum.add(Ljava/lang/Short;)V
 at Main.main(Main.java:9)

Из этого следует несколько наблюдений и напоминаний для разработчиков Java.

  • Классовые пути важны:
    • Файлы Java .class скомпилированные с одинаковой версией Java (с одинаковыми -source и -target ), избежали бы конкретной проблемы в этом посте.
    • Путь к классам должен быть как можно более скудным, чтобы уменьшить / исключить возможность получения ошибочных «старых» определений классов.
    • Сборка «чистых» целей и другие операции сборки должны быть тщательно очищены от прошлых артефактов, а сборки должны перестраивать все необходимые классы приложений.
  • Автобокс и распаковка являются благими намерениями и часто очень удобны, но могут привести к неожиданным проблемам, если не учитывать их в некоторой степени. В этом посте необходимость по-прежнему проверять наличие нуля (или знать, что объект не равен нулю) необходима остается в ситуациях, когда неявная разыменование будет иметь место в результате распаковки.
  • Вопрос стиля API заключается в том, разрешать ли клиентам передавать значения NULL и проверять класс обслуживания на наличие значений NULL от их имени. В промышленном приложении я бы сказал, разрешен ли null для каждого параметра метода «add» с @param в комментарии Javadoc каждого метода. В других ситуациях можно поручить вызывающей стороне гарантировать, что любая передаваемая ссылка не равна нулю, и будет NullPointerException если вызывающая сторона не подчиняется этому контракту (что также должно быть указано в методе Javadoc).
  • Хотя обычно мы видим NoSuchMethodError когда метод полностью удален, или когда мы обращаемся к старому классу до того, как этот метод был доступен, или когда API метода изменился с точки зрения типов или количества типов. В тот день, когда автобокс и распаковка Java в значительной степени считаются само собой разумеющимся, может быть легко предположить, что изменение метода с принятия примитива на выбор соответствующего ссылочного типа ни на что не повлияет, но даже это изменение может привести к исключению, если не все задействованные классы построены на версии Java, поддерживающей автоматическую коробку и распаковку.
  • Один из способов определить версию Java, для которой был скомпилирован конкретный файл .class — использовать javap -verbose и посмотреть в выводе javap «главную версию:». В классах, которые я использовал в своих примерах в этом посте (скомпилировано для JDK 1.4 и Java SE 8), записи « основной версии » составляли 48 и 52 соответственно (в разделе « Общий формат » записи Википедии о файле класса Java перечислены основные версии). ).

К счастью, проблема, продемонстрированная в примерах и тексте в этом посте, не столь распространена благодаря сборкам, которые обычно очищают все артефакты и перестраивают код на относительно постоянной основе. Однако существуют случаи, когда это может произойти, и одна из наиболее вероятных таких ситуаций — случайное использование старого файла JAR, поскольку он находится в режиме ожидания на пути к классам времени выполнения.

Ссылка: Autoboxing, Unboxing и NoSuchMethodError от нашего партнера JCG Дастина Маркса в блоге Inspired by Actual Events .