Можно ли вызвать конструктор для существующего экземпляра?

Известно, что с помощью sun.misc.Unsafe#allocateInstance можно создать объект, не вызывая никаких конструкторов класса.

Можно ли сделать наоборот: для существующего экземпляра вызвать для него конструктор?


Пояснение: это не вопрос о том, что я буду делать в рабочем коде. Мне любопытно узнать о внутренностях JVM и сумасшедших вещах, которые все еще можно сделать. Приветствуются ответы, относящиеся к какой-либо версии JVM.


person Oleg Pyzhcov    schedule 05.02.2018    source источник
comment
Если вы спрашиваете о внутреннем устройстве JVM, вы не спрашиваете о Java как о языке.   -  person Andy Turner    schedule 05.02.2018
comment
@AndyTurner Я спрашиваю, есть ли способ сделать то, о чем я спрашиваю, любыми необходимыми средствами.   -  person Oleg Pyzhcov    schedule 05.02.2018


Ответы (4)


JVMS §2.9 запрещает вызов конструктора на уже инициализированных объектах:

Методы инициализации экземпляра могут быть вызваны только внутри виртуальной машины Java с помощью инструкции invokespecial, и они могут быть вызваны только для неинициализированных экземпляров класса.

Однако по-прежнему технически возможно вызвать конструктор для инициализированного объекта с помощью JNI. CallVoidMethod не делает различий между <init> и обычными методами Java. Более того, спецификация JNI намекает, что CallVoidMethod может использоваться для вызова конструктора, хотя не говорит, должен ли экземпляр быть инициализирован или нет:

Когда эти функции используются для вызова закрытых методов и конструкторов, идентификатор метода должен быть получен из реального класса obj, а не из одного из его суперклассов.

Я убедился, что следующий код работает как в JDK 8, так и в JDK 9. JNI позволяет вам делать небезопасные вещи, но вы не должны полагаться на это в рабочих приложениях.

ConstructorInvoker.java

public class ConstructorInvoker {

    static {
        System.loadLibrary("constructorInvoker");
    }

    public static native void invoke(Object instance);
}

constructorInvoker.c

#include <jni.h>

JNIEXPORT void JNICALL
Java_ConstructorInvoker_invoke(JNIEnv* env, jclass self, jobject instance) {
    jclass cls = (*env)->GetObjectClass(env, instance);
    jmethodID constructor = (*env)->GetMethodID(env, cls, "<init>", "()V");
    (*env)->CallVoidMethod(env, instance, constructor);
}

TestObject.java

public class TestObject {
    int x;

    public TestObject() {
        System.out.println("Constructor called");
        x++;
    }

    public static void main(String[] args) {
        TestObject obj = new TestObject();
        System.out.println("x = " + obj.x);  // x = 1

        ConstructorInvoker.invoke(obj);
        System.out.println("x = " + obj.x);  // x = 2
    }
}
person apangin    schedule 05.02.2018
comment
Теперь действительно интересный вопрос… что произойдет, если TestObject будет иметь нетривиальный метод finalize()? - person Holger; 05.02.2018
comment
@Holger Отличный вопрос! По умолчанию HotSpot JVM регистрирует финализаторы в конце Object.<init>. То есть finalize() можно вызывать дважды для одного и того же объекта. С флагом -XX:-RegisterFinalizersAtInit финализаторы регистрируются сразу после размещения объектов, поэтому finalize() будет вызываться только один раз. - person apangin; 05.02.2018
comment
Ой. Интересно, что есть возможность изменить это поведение, поскольку это противоречит спецификации, JLS §12.6.1: «Объект o не может быть финализирован до тех пор, пока его конструктор не вызовет конструктор для объекта o и этот вызов завершился успешно (то есть без создания исключения).». Не похоже, что это все еще может быть гарантировано, если объект уже зарегистрирован при распределении. - person Holger; 05.02.2018

Конструктор не является методом экземпляра, поэтому вы не можете вызывать конструктор для экземпляра.

Если вы посмотрите на библиотеку отражения, вы увидите, что возвращаемый тип Class.getConstructor() — это Constructor, у которого нет методов, которые могут принимать экземпляр — его единственный соответствующий метод — newInstance(), который не принимает целевой экземпляр; он создает один.

С другой стороны, возвращаемый тип Class.getMethod() — это Method, первым параметром которого является экземпляр.

Constructor не Method.

person Bohemian♦    schedule 05.02.2018
comment
Спасибо за ваш ответ. Я знаю эту часть о регулярном API отражения. Однако в JVM создание экземпляра байт-кода выполняется двумя способами: new для выделения памяти и invokespecial для вызова конструктора. Так что теоретически можно добиться того, чего я хочу. - person Oleg Pyzhcov; 05.02.2018

В спецификации JVM для invokespecial:

Инструкция invokespecial безопасна по типу, если выполняются все следующие условия:

... (Материал о методах без инициализации)

  • Имя_метода: <init>.
  • Дескриптор указывает тип возвращаемого значения void.
  • Можно корректно извлекать типы, соответствующие типам аргументов, указанным в дескрипторе, и неинициализированный тип UninitializedArg из входящего стека операндов, что дает OperandStack.
  • ...

Если вы уже инициализировали экземпляр, это не неинициализированный тип, поэтому это не удастся.

Обратите внимание, что другие инструкции invoke* (invokevirtual, invokeinterface, invokestatic, invokedynamic) явно исключают вызов методов <init>, поэтому invokespecial — единственный способ их вызвать.

person Andy Turner    schedule 05.02.2018

Из JLS, раздел 8.8.

Конструкторы вызываются выражениями создания экземпляра класса (§15.9), преобразованиями и конкатенациями, вызванными оператором конкатенации строк + (§15.18.1), а также явными вызовами конструкторов из других конструкторов (§8.8.7).

...

Конструкторы никогда не вызываются выражениями вызова метода (§15.12).

Так что нет, это невозможно.

Если есть какое-то общее действие, которое вы хотите выполнить в конструкторе и в другом месте, поместите его в метод и вызовите его из конструктора.

person Andy Turner    schedule 05.02.2018
comment
Спасибо за Ваш ответ. Однако JLS охватывает только обычную Java. Например. он говорит ... it is possible to prevent class instantiation by declaring an inaccessible constructor, не учитывая отражение setAccessible(true) или Unsafe. Имея это в виду, совершенно невозможно предотвратить создание экземпляра класса. Мой вопрос примерно такого же уровня небезопасных манипуляций. - person Oleg Pyzhcov; 05.02.2018
comment
Сделав конструктор видимым, вы по-прежнему можете вызывать его только одним из указанных способов. - person Andy Turner; 05.02.2018
comment
@Oleg можно предотвратить создание экземпляра класса, даже если использовать отражение, объявив конструктор (обычно private), который генерирует исключение. См. этот ответ - person Bohemian♦; 05.02.2018
comment
@Bohemian, вы все равно можете создать экземпляр, используя Unsafe, поскольку, как я уже упоминал, это не вызовет ваш конструктор. - person Oleg Pyzhcov; 05.02.2018