J2SE 5为Java编程语言引入了许多功能。 这些功能之一是自动装箱和拆箱,这是我几乎每天都没有考虑过的功能。 它通常很方便(尤其是与收藏夹一起使用时),但有时会导致一些令人讨厌的惊喜,即“怪异”和“疯狂” 。 在此博客文章中,我介绍了一种罕见的NoSuchMethodError案例(对我来说很有趣),该案例是由于在自动装箱/拆箱之前将使用Java版本编译的类与使用包括自动装箱/取消装箱的Java版本编译的类混合在一起而造成的。
下一个代码清单显示了一个简单的Sum
类,该类可以在J2SE 5之前编写。它已重载了“ add”方法,这些方法接受不同的原始数值数据类型,并且Sum>
每个实例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
类的所有客户端都需要向这些“添加”方法提供原语,或者,如果它们具有与原语相同的引用,则需要在将其中的一个调用“添加”方法。 在调用这些方法之前,客户端代码有责任从引用类型转换为相应的原始类型。 下一个代码清单中显示了如何完成此操作的示例。
不取消装箱:客户端将引用转换为基元
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的自动装箱和拆箱功能旨在解决这种情况下所需的繁琐工作。 通过取消装箱,客户端代码可以使用与预期的基本类型相对应的引用类型来调用上述“添加”方法,并且引用将自动“取消装箱”为原始形式,以便可以调用适当的“添加”方法。 Java语言规范的第5.1.8节(“取消装箱转换”)说明了提供的数字引用类型在取消装箱中将转换为哪些原语,该规范的第5.1.7节(“装箱转换”)列出了自动装箱的引用类型。来自自动装箱中的每个原语。
在此示例中,在调用Sum
的“ add”方法之前,将引用类型转换为对应的原始对等体,从而使客户方面的拆箱工作减少了,但并没有使客户端完全不必在提供它们之前处理数字值。 由于引用类型可以为null ,因此客户端可以为Sum
的“ add”方法之一提供null引用,并且当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();
}
在设计API时,可能需要避免客户端代码在Sum
上调用“ add”方法之前检查其引用是否为null。 消除这种需求的一种方法是更改“添加”方法以显式接受引用类型而不是原始类型。 然后, Sum
类可以在显式或隐式(取消装箱)对它进行解引用之前检查null。 接下来显示了经过修改的Sum
类,其中包含已更改的,更易于客户端使用的API。
用“ 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);
}
}
修改后的Sum
类对客户端更友好,因为它允许客户端将引用传递给它的任何“ add”方法,而不必担心传入的引用是否为null。 但是,如果涉及的任何一个类(客户端类或Sum
类的一个版本)都使用不同版本的Java编译,则像这样对Sum
类的API进行更改可能会导致NoSuchMethodError
。 特别是,如果客户端代码使用原语并且使用JDK 1.4或更早版本进行编译,并且Sum
类是所示的最新版本(期望使用引用代替原语)并且使用J2SE 5或更高版本进行编译,则将遇到类似以下内容的NoSuchMethodError
(“ S”表示它是期望基本short
的“ add”方法,而“ V”表示该方法返回void
)。
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
参考。
Exception in thread "main" java.lang.NoSuchMethodError: Sum.add(Ljava/lang/Short;)V
at Main.main(Main.java:9)
由此可见对Java开发人员的一些观察和提醒。
- 类路径很重要:
- 使用相同版本的Java(相同的
-source
和-target
)编译的Java.class
文件可以避免本文中的特定问题。
- 使用相同版本的Java(相同的
- 自动装箱和取消装箱的目的是很好的,并且通常非常方便,但是如果在一定程度上不牢记,可能会导致令人惊讶的问题。 在这篇文章中,仍然需要检查空值(或知道对象不是空值),这是由于拆箱而导致隐式取消引用的情况。
- 是否允许客户端传递null并让服务类代表它们检查null是API风格的问题。 在工业应用程序中,我将用每个方法的Javadoc注释中的
@param
声明是否对每个“添加”方法参数使用null。 在其他情况下,可能要让调用者负责确保任何传入的引用都不为空,并且如果调用者不遵守该约定,则抛出NullPointerException
是内容(该方法也应在方法的Javadoc)。 - 尽管通常会在完全删除某个方法或在该方法可用之前访问旧类或方法的API在类型或类型数方面发生更改时看到
NoSuchMethodError
。 在Java自动装箱和拆箱在很大程度上被视为理所当然的日子里,很容易想到将方法从采用原语转换为采用相应的引用类型不会产生任何影响,但是即使这种更改也会导致异常,如果并非所有涉及的类都基于支持自动装箱和拆箱的Java版本构建。 - 确定针对其编译特定
.class
文件的Java版本的一种方法是使用javap -verbose并在javap输出中查找“主要版本:”。 在本文示例中使用的类(针对JDK 1.4和Java SE 8编译)中,“主要版本”条目分别为48和52( Java类文件上Wikipedia条目的“常规布局”部分列出了主要版本) )。
幸运的是,由于构建通常会清理所有工件并在相对连续的基础上重建代码,因此本文中使用示例和文本演示的问题并不常见。 但是,在某些情况下可能会发生这种情况,最可能的情况之一是意外使用旧的JAR文件时,因为它位于运行时类路径上的等待中。
翻译自: https://www.javacodegeeks.com/2014/08/autoboxing-unboxing-and-nosuchmethoderror.html