为什么自动装箱在通过反射调用时不使用valueOf()方法?

20
据我了解,以下代码应该打印"true",但当我运行它时,它却打印"false"
public class Test {
    public static boolean testTrue() {
        return true;
    }

    public static void main(String[] args) throws Exception {
        Object trueResult = Test.class.getMethod("testTrue").invoke(null);
        System.out.println(trueResult == Boolean.TRUE);
    }
}

根据JLS§5.1.7.Boxing Conversion:
如果被装箱的值p是true、false、一个byte或char,它在范围内是从\u0000到\u007f的,或者是-128到127(inclusive)之间的int或short数字,则让r1和r2分别成为p的任意两个装箱转换的结果。它总是成立r1 == r2。
但是,在通过反射调用的方法中,装箱值总是通过new PrimitiveWrapper()创建的。
请帮我理解这个。

2
严格来说,Boolean.TRUE并不是“装箱转换的结果”。 - Jim Garrison
好的,这里没有自动装箱。JLS 的这一部分是关于自动装箱的。 - kumesana
嗯,“在反射的情况下”并不包含您引用的JLS部分。该部分是关于变量转换的连续性,当您使用语言通常将一种类型的值分配给另一种类型时。反射不是其中的一部分。 - kumesana
1
JLS 规定,在从 boolean 转换为 Boolean 的情况下需要进行装箱转换。但是在反射的情况下,转换是从 booleanObject。因此,在 Method.invoke() 的背后代码中,可以调用 new Boolean(b)boolean 转换为 Object,而不违反 JLS 的规定。 - Thomas Kläger
4个回答

13

invoke方法总是返回一个新的Object,任何返回的基本数据类型都会被封装。

如果返回值是一个基本数据类型,它将首先被适当地封装在一个对象中。

你所遇到的问题展示了术语“适当”存在歧义。即在封装期间,它不会使用Boolean.valueOf(boolean)方法。


2
只是提出一个可能的建议原因:反射API在1.1中被添加;Boolean.valueOf在1.4中被添加。也许为了向后兼容而保留了pre-valueOf行为。 - Andy Turner
1
@AndyTurner,你走在正确的道路上了。在1.4之前,Boolean没有valueOf方法(对于其他类型来说,需要更长时间),即使在那之后,也没有关于装箱转换的正式定义,这只是在下一个版本中引入的。但我认为保持这种行为的原因并不是向后兼容性,而是“没有人花时间去改变它”。但自从JDK 9以来,它已经改变了;根据JRE配置,您只需要执行多个调用才能遇到更改的行为。 - Holger

2
被引用的部分已经被多次重写,正如在Java 13 SE规范中是否需要缓存装箱的Byte对象?中讨论的那样。
你引用了使用到Java 7的版本:

如果被装箱的值ptruefalse、一个byte、一个范围在\u0000\u007f之间的char,或者一个在-128127之间的intshort数值,那么任意两个对p进行装箱转换的结果r1r2总是满足r1 == r2

请注意,它忘记提到long

Java 8中,规范说明如下:

如果被装箱的值p是介于-128127之间(包括-128和127)的整数字面量(§3.10.1),或布尔字面量truefalse(§3.10.3),或者是介于'\u0000''\u007f'之间的字符字面量(§3.10.4),则让ab分别表示p的任意两个装箱转换的结果。始终满足a == b

上述仅适用于字面量

Java 9以来,规范说明如下:

如果被装箱的值p是通过计算一个常量表达式(参见§15.28)得到的,其类型为boolean、char、short、int或long,并且结果为true、false、范围在'\u0000'和'\u007f'之间的字符,或者范围在-128到127之间的整数,则让a和b分别为p的两个装箱转换的结果。总是成立a == b。
这现在指的是常量表达式,包括long,并忽略了byte(在14版本中已重新添加)。虽然这不坚持字面值,但反射方法调用不是常量表达式,因此不适用。
即使我们使用旧规范的措辞,也不清楚实现反射方法调用的代码是否承担了装箱转换。原始代码源自装箱转换不存在的时代,因此它执行了包装对象的显式实例化,只要代码包含显式实例化,就不会进行装箱转换。
简而言之,反射操作返回的包装实例的对象标识是未指定的。
从实施者的角度来看,处理第一个反射调用的代码是本地代码,比Java代码更难更改。但自JDK 1.3以来,当调用次数超过阈值时,这些本地方法访问器会被生成的字节码替换。由于重复调用是性能关键的部分,因此重要的是查看这些生成的访问器。自JDK 9以来,这些生成的访问器使用了装箱转换的等效方式。
因此,运行以下适应的测试代码:
import java.lang.reflect.Method;

public class Test
{
    public static boolean testTrue() {
        return true;
    }

    public static void main(String[] args) throws Exception {
        int threshold = Boolean.getBoolean("sun.reflect.noInflation")? 0:
                Integer.getInteger("sun.reflect.inflationThreshold", 15);

        System.out.printf("should use bytecode after %d invocations%n", threshold);

        Method m = Test.class.getMethod("testTrue");

        for(int i = 0; i < threshold + 10; i++) {
            Object trueResult = m.invoke(null);
            System.out.printf("%-2d: %b%n", i, trueResult == Boolean.TRUE);
        }
    }
}

将在Java 9及更高版本中打印出来:
should use bytecode after 15 invocations
0 : false
1 : false
2 : false
3 : false
4 : false
5 : false
6 : false
7 : false
8 : false
9 : false
10: false
11: false
12: false
13: false
14: false
15: false
16: true
17: true
18: true
19: true
20: true
21: true
22: true
23: true
24: true

请注意,您可以调整JVM选项-Dsun.reflect.inflationThreshold=number以更改阈值,并使用-Dsun.reflect.noInflation=true让反射立即使用字节码。
更新:从JDK 18开始,值始终使用valueOf(…)进行装箱。

0

1.

具体来说,如果是通过反射调用的方法,则不包含您引用的JLS部分。您引用的部分是关于类型转换的,当您拥有一个作为另一种类型传递的类型值时进行类型转换。在这里,您正在考虑将布尔值转换为布尔对象。

但是类型转换意味着执行类似以下操作:

Boolean b = true;

或者

boolean b = true;
Boolean b2 = b;

反射不是一种应用类型转换的机制。

当必要时,反射方法调用将布尔返回值包装成Boolean对象,它并没有涉及到您引用的JLS的部分。

这就解释了为什么在这里没有违反JLS。

    2.

至于为什么反射不选择与此行为保持一致:

那是因为在早期版本的Java中,泛型出现之前就已经存在反射。而泛型是自动装箱突然变得方便的原因,自动装箱是不重复包装基本类型的“常见”值似乎很聪明的原因。

所有这些都是在反射已经存在一段时间,并且已经以特定方式运行的情况下定义的。这意味着已经存在使用反射的Java代码,并且很可能存在一些错误地依赖现有行为的现有代码。更改现有行为将破坏现有代码,因此应避免。


很可能是一些现有代码错误地依赖于现有行为。这需要非常具体的情况吧?我能想到唯一满足该语句的代码是 myReturn.booleanValue() && myReturn != Return.TRUE,但是任何理智的人都不会写这样的代码。我不是在说你对还是错,但如果你是对的,那么为了一些白痴依赖于实现细节,他们故意让每个Java用户的代码微不足道地变差了多年 - Michael
@Michael Meh,我从未为Sun工作过,也没有参与过官方Java版本的开发。但他们往往非常重视保持向后兼容性。他们接受派生的唯一原因是如果不这样做会有巨大的代价或者不可避免。而这并不会带来明显的代价。 - kumesana
他们会接受为此派生的唯一理由是,如果不这样做是不可避免的,或者没有这样做将会有巨大的代价。引入var是可以避免的,而不包括它也不会带来巨大的代价。近年来,他们在向后兼容性方面放宽了观点。它已经成为该语言的不利因素。 - Michael
@Michael 我认为它们是相关的,因为它们涵盖了自动收件箱,这是这里问题的起源。 - kumesana
1
在大多数情况下,某些东西没有被更改的原因是a)没有人为此打开了RFE或b)没有人有时间去做(但可能会在以后)。您无法想象,与此项目的规模相比,JDK开发所拥有的资源是如此之少。实际上,Reflection的行为在这方面已经发生了变化,但在第一次调用时您不会注意到(问题测试仅执行一次)。 - Holger
显示剩余2条评论

0

正如您在java.lang.reflect.Method类中所看到的,invoke方法具有以下签名:

 public Object invoke(Object obj, Object... args) { ... }

返回一个对象作为结果。

此外,Boolean.TRUE 定义为:

public static final Boolean TRUE = new Boolean(true);

这是一个被封装成true值的对象。

当你在代码中评估trueResult == Boolean.TRUE时,你正在检查trueResultBoolean.TRUE的引用是否相等。因为==用于比较值的相等性,在引用的情况下,它意味着两个引用指向内存中的一个对象

显然,这两个对象不相同(它们是两个单独的对象,并且在内存的不同部分实例化),因此trueResult == Boolean.TRUE的结果为false


你没有理解问题的要点。让我为你重新表述一下,希望你能明白为什么你的答案并没有回答这个问题:为什么通过反射调用方法会使用 Boolean 的构造函数自动装箱返回值(即创建一个新对象),而正常调用方法则使用 Boolean.valueOf 自动装箱(即返回 Boolean.TRUE)? - Michael

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接