Статьи

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

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

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

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

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 класса должны были бы предоставить примитивы для этих методов «добавления» или, если бы они имели ссылочные эквиваленты примитивов, должны были бы преобразовать ссылки на свои примитивные аналоги перед вызовом одного из «add» методы. На клиентском коде лежит обязанность выполнить это преобразование из ссылочного типа в соответствующий примитивный тип перед вызовом этих методов. Примеры того, как это можно сделать, показаны в следующем листинге кода.

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

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  («Преобразование бокса»).) той же спецификации перечисляет типы ссылок, которые автоматически упаковываются из каждого примитива в автобокс.

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

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

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, ожидающими ссылки, а не примитивами

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);
   }
}

The revised Sum class is more client-friendly because it allows the client to pass a reference to any of its «add» methods without concern for whether the passed-in reference is null or not. However, the change of the Sumclass’s API like this can lead to NoSuchMethodErrors if either class involved (the client class or one of the versions of the Sum class) is compiled with different versions of Java. In particular, if the client code uses primitives and is compiled with JDK 1.4 or earlier and the Sum class is the latest version shown (expecting references instead of primitives) and is compiled with J2SE 5 or later, a NoSuchMethodError like the following will be encountered (the «S» indicates it was the «add» method expecting a primitive short and the «V» indicates that method returned void).

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

On the other hand, if the client is compiled with J2SE 5 or later and with primitive values being supplied toSum as in the first example (pre-unboxing) and the Sum class is compiled in JDK 1.4 or earlier with «add» methods expecting primitives, a different version of the NoSuchMethodError is encountered. Note that theShort reference is cited here.

Exception in thread "main" java.lang.NoSuchMethodError: Sum.add(Ljava/lang/Short;)V
 at Main.main(Main.java:9)
There are several observations and reminders to Java developers that come from this.

  • Classpaths are important:
    • Java .class files compiled with the same version of Java (same -source and -target) would have avoided the particular problem in this post.
    • Classpaths should be as lean as possible to reduce/avoid possibility of getting stray «old» class definitions.
    • Build «clean» targets and other build operations should be sure to clean past artifacts thoroughly and builds should rebuild all necessary application classes.
  • Autoboxing and Unboxing are well-intentioned and often highly convenient, but can lead to surprising issues if not kept in mind to some degree. In this post, the need to still check for null (or know that the object is non-null) is necessary remains in situations when implicit dereferencing will take place as a result of unboxing.
  • It’s a matter of API style taste whether to allow clients to pass nulls and have the serving class check for null on their behalf. In an industrial application, I would have stated whether null was allowed or not for each «add» method parameter with @param in each method’s Javadoc comment. In other situations, one might want to leave it the responsibility of the caller to ensure any passed-in reference is non-null and would be content throwing a NullPointerException if the caller did not obey that contract (which should also be specified in the method’s Javadoc).
  • Although we typically see NoSuchMethodError when a method is completely removed or when we access an old class before that method was available or when a method’s API has changed in terms of types or number of types. In a day when Java autoboxing and unboxing are largely taken for granted, it can be easy to think that changing a method from taking a primitive to taking the corresponding reference type won’t affect anything, but even that change can lead to an exception if not all classes involved are built on a version of Java supporting autoboxing and unboxing.
  • One way to determine the version of Java against which a particular .class file was compiled is to use javap -verbose and to look in the javap output for the «major version:». In the classes I used in my examples in this post (compiled against JDK 1.4 and Java SE 8), the «major version» entries were 48 and 52 respectively (the General Layout section of the Wikipedia entry on Java class file lists the major versions).

Fortunately, the issue demonstrated with examples and text in this post is not that common thanks to builds typically cleaning all artifacts and rebuilding code on a relatively continuous basis. However, there are cases where this could occur and one of the most likely such situations is when using an old JAR file accidentally because it lies in wait on the runtime classpath.