匿名类混淆的动态构建

2022-09-03 05:49:08

我正在尝试使用反射来制作匿名类的实例。但偶尔我在瞬时看到奇怪的行为。

请看这些类似的代码片段

public class HideAndSeek {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws IllegalAccessException, InstantiationException{

        final String finalString = "I'm final :)";

        Object object2 = new Object(){

            {
                System.out.println("Instance initializing block");
                System.out.println(finalString);
            }           

            private void hiddenMethod() {
                System.out.println("Use reflection to find me :)");
            }
        };

        Object tmp = object2.getClass().newInstance();
    }

}

此代码运行良好,并且预期输出

Instance initializing block
I'm final :)
Instance initializing block
I'm final :)

在此之后,我决定以简单的方式更改代码(刚刚添加了java.util.Calendar)

import java.util.Calendar;

    public class HideAndSeek {

        @SuppressWarnings("unchecked")
        public static void main(String[] args) throws IllegalAccessException, InstantiationException{

            final String finalString = "I'm final :)";

            final Calendar calendar = Calendar.getInstance();
            System.out.println(calendar.getTime().toString()); //works well

            Object object2 = new Object(){

                {
                    System.out.println("Instance initializing block");
                    System.out.println(finalString);

                    //simply added this line
                    System.out.println(calendar.getTime().toString());
                }           

                private void hiddenMethod() {
                    System.out.println("Use reflection to find me :)");
                }
            };

            Object tmp = object2.getClass().newInstance();
        }

    }

这是我得到的输出:

Wed Aug 17 02:08:47 EEST 2011
Instance initializing block
I'm final :)
Wed Aug 17 02:08:47 EEST 2011
Exception in thread "main" java.lang.InstantiationException: HideAndSeek$1
    at java.lang.Class.newInstance0(Unknown Source)
    at java.lang.Class.newInstance(Unknown Source)
    at HideAndSeek.main(HideAndSeek.java:29)

如您所见 - 尚未创建新实例。

谁能解释一下,这些变化的原因?

谢谢


答案 1

这是一个非常简单的问题,答案非常复杂。请耐心等待我试图解释它。

查看引发异常的源代码(我不确定为什么您的堆栈跟踪没有给出行号):ClassClass

try
{
  Class[] empty = {};
  final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
  // removed some code that was not relevant
}
catch (NoSuchMethodException e)
{
  throw new InstantiationException(getName());
}

您看到它正在被重写为 .这意味着 类类型 没有 no-arg 构造函数。NoSuchMethodExceptionInstantiationExceptionobject2

首先,什么类型是什么?使用代码object2

System.out.println("object2 class: " + object2.getClass());

我们看到

object2 类:类垃圾。新主$1

这是正确的(我在软件包垃圾,类NewMain中运行示例代码)。

那么 构造函数是什么?junk.NewMain$1

Class obj2Class = object2.getClass();
try
{
  Constructor[] ctors = obj2Class.getDeclaredConstructors();
  for (Constructor cc : ctors)
  {
    System.out.println("my ctor is " + cc.toString());
  }
}
catch (Exception ex)
{
  ex.printStackTrace();
}

这给了我们

我的克托是垃圾。NewMain$1(java.util.Calendar)

所以你的匿名班级正在寻找一个要传递的。然后,这将为您工作:Calendar

Object newObj = ctors[0].newInstance(Calendar.getInstance());

如果你有这样的东西:

final String finalString = "I'm final :)";
final Integer finalInteger = new Integer(30);
final Calendar calendar = Calendar.getInstance();
Object object2 = new Object()
{
  {
    System.out.println("Instance initializing block");
    System.out.println(finalString);
    System.out.println("My integer is " + finalInteger);
    System.out.println(calendar.getTime().toString());
  }
  private void hiddenMethod()
  {
    System.out.println("Use reflection to find me :)");
  }
};

那么我的调用将不起作用,因为 ctor 中没有足够的参数,因为现在它想要:newInstance

我的克托是垃圾。NewMain$1(java.lang.Integer,java.util.Calendar)

如果我然后用实例化它

Object newObj = ctors[0].newInstance(new Integer(25), Calendar.getInstance());

使用调试器进行内部查看显示的是 25,而不是最终值 30。finalInteger

事情稍微复杂一些,因为你是在静态上下文中完成上述所有操作的。如果你把上面的所有代码都移到一个非静态的方法中,就像这样(记住,我的类是垃圾。新主):

public static void main(String[] args)
{
  NewMain nm = new NewMain();
  nm.doIt();
}

public void doIt()
{
  final String finalString = "I'm final :)";
  // etc etc
}

你会发现你的内部类的ctor现在是(删除我添加的整数引用):

我的克托是垃圾。NewMain$1(垃圾.NewMain, java.util.Calendar)

Java 语言规范第 15.9.3 节是这样解释的:

如果 C 是一个匿名类,而 C 的直接超类 S 是一个内部类,则:

  • 如果 S 是局部类,并且 S 出现在静态上下文中,则参数列表中的参数(如果有)是构造函数的参数,按它们在表达式中出现的顺序排列。
  • 否则,i 相对于 S 的紧闭实例是构造函数的第一个参数,后跟类实例创建表达式的参数列表中的参数(如果有),按它们在表达式中出现的顺序排列。

为什么匿名构造函数会接受参数?

由于您无法为匿名内部类创建构造函数,因此实例初始值设定项块可用于此目的(请记住,您只有该匿名内部类的一个实例)。VM不了解内部类,因为编译器将所有内容作为单独的类(例如垃圾类。NewMain$1)。该类的 ctor 包含实例初始值设定项的内容。

这是由 JLS 15.9.5.1 匿名构造函数执行的

...匿名构造函数对于声明 C 的类实例创建表达式的每个实际参数都有一个正式参数。

您的实例初始值设定项具有对对象的引用。除了通过构造函数之外,编译器如何才能将该运行时值获取到内部类(该类仅作为 VM 的类创建)中?Calendar

最后(耶),最后一个紧迫问题的答案。为什么构造函数不需要 ?JLS 3.10.5的最后一位解释了:String

由常量表达式计算的字符串在编译时计算,然后被视为文本。

换句话说,您的值在编译时是已知的,因为它是一个文本,因此它不需要是匿名构造函数的一部分。为了证明这一点,我们将在JLS 3.10.5中测试下一个语句:String

在运行时通过串联计算的字符串是新创建的,因此是不同的。

因此更改代码:

String str1 = "I'm";
String str2 = " final!";
final String finalString = str1 + str2

你会发现你的 ctor 现在是(在非静态上下文中):

我的克托是垃圾。NewMain$1(垃圾.NewMain,java.lang.String,java.util.Calendar)

唷。我希望这是有道理的,并且是有帮助的。我学到了很多东西,这是肯定的!


答案 2

因为在第二种情况下,不再有默认构造函数。

在第一种情况下,最后一个字符串被内联,因为它是一个常量。

在第二种情况下,匿名内部类必须接受 Calendar 的实例进入其构造函数以捕获状态。您可以轻松确认这样做:

Object tmp = object2.getClass().getDeclaredConstructor(Calendar.class).newInstance(calendar);