Динамическое построение путаницы анонимного класса

Я пытаюсь сделать экземпляры анонимных классов, используя отражение. Но, случайно, я видел странное поведение во время создания.

Пожалуйста, посмотрите на эти похожие фрагменты кода

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

Это очень простой вопрос с очень сложным ответом. Пожалуйста, несите меня, когда я пытаюсь это объяснить.

Глядя на исходный код, в котором исключение выражено в Class (я не уверен, почему ваша трассировка стека не дает номера строк в Class):

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

вы видите, что NoSuchMethodException возвращается в качестве InstantiationException. Это означает, что для типа класса object2 нет конструктора no-arg.

Во-первых, какой тип object2? С кодом

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

мы видим, что

object2 class: class junk.NewMain $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();
}

что дает нам

my ctor is junk.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 :)");
  }
};

тогда мой вызов newInstance не будет работать, потому что в ctor недостаточно аргументов, потому что теперь он хочет:

my ctor is junk.NewMain $1 (java.lang.Integer, java.util.Calendar)

Если я затем создаю экземпляр, используя

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

заглянуть внутрь, используя отладчик, показывает, что finalInteger равно 25, а не окончательное значение 30.

Вещи немного сложны, потому что вы все это делаете в статическом контексте. Если вы возьмете весь свой код выше и переместите его в нестатический метод, подобный этому (помните, что мой класс junk.NewMain):

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

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

вы найдете, что ctor для вашего внутреннего класса теперь (удаление моей добавленной ссылки Integer):

мой ctor - хлам. NewMain $1 (junk.NewMain, java.util.Calendar)

Спецификация языка Java, раздел 15.9.3 объясняет это следующим образом:

Если C - анонимный класс, а прямой суперкласс C, S - внутренний класс, то:

  • Если S - локальный класс, а S - в статическом контексте, то аргументы в списке аргументов, если они есть, являются аргументами конструктор, в том порядке, в котором они появляются в выражении.
  • В противном случае немедленно включающий экземпляр я по отношению к S является первым аргументом конструктора, за которым следует аргументы в списке аргументов создания экземпляра класса выражение, если оно есть, в том порядке, в котором они появляются в выражении.

Почему анонимный конструктор вообще принимает аргументы?

Поскольку вы не можете создать конструктор для анонимного внутреннего класса, блок инициализации экземпляра служит для этой цели (помните, что у вас есть только один экземпляр этого анонимного внутреннего класса). VM не знает внутреннего класса, поскольку компилятор отделяет все как отдельные классы (например, junk.NewMain $1). Ctor для этого класса содержит содержимое инициализатора экземпляра.

Это explayed by JLS 15.9.5.1 Анонимные конструкторы:

... анонимный конструктор имеет один формальный параметр для каждого фактического аргумент для выражения создания экземпляра класса, в котором C является объявлено.

Инициализатор экземпляра имеет ссылку на объект Calendar. Как еще компилятор собирается получить это значение времени выполнения во внутреннем классе (который создается как просто класс для виртуальной машины), кроме как через конструктор?

Наконец, (yay), ответ на последний горящий вопрос. Почему конструктору не требуется String? Последний бит JLS 3.10.5 объясняет, что:

Строки, вычисленные с помощью константных выражений, вычисляются во время компиляции и затем обрабатывались так, как если бы они были литералами.

Другими словами, ваше значение String известно во время компиляции, потому что оно буквально, поэтому оно не обязательно должно быть частью анонимного конструктора. Чтобы доказать это, мы проверим следующий оператор в JLS 3.10.5:

Строки, вычисленные путем конкатенации во время выполнения, создаются и поэтому различны.

Измените свой код:

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

и вы обнаружите, что ваш ctor теперь (в нестационарном контексте):

my ctor is junk.NewMain $1 (junk.NewMain, java.lang.String, java.util.Calendar)

Уф. Надеюсь, это имеет смысл и было полезно. Я многому научился, это точно!

Ответ 2

Потому что во втором случае конструктор по умолчанию больше не существует.

В первом случае окончательная строка вставляется, потому что она является константой.

Во втором случае анонимный внутренний класс должен принять экземпляр Calendar в свой конструктор для захвата состояния. Вы можете легко подтвердить, что это:

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