что такое инстанцирование в java
Порядок инициализации и инстанцирования Java
Я пытаюсь объединить процесс инициализации и инстанцирования в JVM, но JLS немного тупо на нескольких деталях, поэтому, если кто-то не возражает, чтобы прояснить некоторые детали, это будет оценено. Это то, что мне удалось выяснить до сих пор.
инициализация
Рекурсивно Инициализировать статические конечные переменные класса и его интерфейсы, которые являются константами времени компиляции.
Возврат из рекурсии обработки статических блоков и статических полей в текстовом порядке.
Конкретизация
Рекурсивно Инициализировать конечные переменные экземпляра класса, которые являются константами времени компиляции.
Возврат из рекурсии обработки нестатических блоков и полей экземпляра в текстовом порядке, доводя их до конструкторов по мере их возврата.
Хорошо, так что теперь для вопросов.
обрабатываются интерфейсы в порядке декларации?
обрабатываются интерфейсы в отдельном рекурсивном стеке?
а) если да, интерфейсы обрабатываются до или после суперклассов?
б) если да, могу ли я исправить вывод о том, что один или другие (Интерфейс или Суперкласс) получает свои константные поля не компиляции, инициализированные до остальных констант компиляции.
Какую роль в этом процессе играют вызовы конструктора super() nefefault?
Я ошибаюсь в любых моих выводах?
Не хватает ли каких-либо других ключевых данных?
ОТВЕТЫ
Ответ 1
Важно различать инициализацию класса и инициализацию объекта.
Инициализация класса
Класс или интерфейс инициализируется после первого доступа, назначая поля постоянной времени компиляции, а затем рекурсивно инициализируя суперкласс (если он еще не инициализирован), затем обработка статических инициализаторов (которые включают инициализаторы для статических полей, которые не являются константами времени компиляции).
Как вы заметили, инициализация класса сама по себе не инициирует инициализацию интерфейсов, которые она реализует. Поэтому интерфейсы инициализируются при первом обращении к ним, как правило, путем чтения поля, которое не является постоянной времени компиляции. Этот доступ может возникать при оценке инициализатора, вызывая рекурсивную инициализацию.
Также стоит отметить, что инициализация не запускается путем доступа к полям, которые являются константами времени компиляции, поскольку они оцениваются в время компиляции:
Ссылка на поле, которое является постоянной переменной (§4.12.4), должно быть разрешено во время компиляции до значения V, обозначенного инициализатором постоянной переменной.
Если такое поле является статическим, тогда никакая ссылка на поле не должна присутствовать в коде в двоичном файле, включая класс или интерфейс, которые объявили это поле. Такое поле всегда должно быть инициализировано (§12.4.2); начальное значение по умолчанию для поля (если оно отличается от V) никогда не должно наблюдаться.
Если такое поле не статично, то никакая ссылка на поле не должна присутствовать в коде двоичного файла, кроме класса, содержащего это поле. (Это будет класс, а не интерфейс, поскольку интерфейс имеет только статические поля.) Класс должен иметь код для установки значения поля V во время создания экземпляра (§12.5).
Инициализация объектов
Объект инициализируется всякий раз, когда создается новый объект, как правило, путем оценки выражения создания экземпляра класса. Это происходит следующим образом:
Назначьте аргументы конструктора новым созданным переменным параметра для этого вызова конструктора.
Если этот конструктор начинается с явного вызова конструктора (§8.8.7.1) другого конструктора в том же классе (с использованием этого), то затем оценивайте аргументы и обрабатывайте вызов конструктора рекурсивно, используя эти пять шагов. Если вызов конструктора завершается внезапно, то эта процедура завершается внезапно по той же причине; в противном случае перейдите к шагу 5.
Этот конструктор не начинается с явного вызова конструктора другого конструктора в том же классе (используя это). Если этот конструктор относится к классу, отличному от Object, то этот конструктор начнет с явного или неявного вызова конструктора суперкласса (используя super). Оцените аргументы и обработайте вызов конструктора суперкласса рекурсивно, используя эти пять шагов. Если вызов конструктора завершается внезапно, то эта процедура завершается внезапно по той же причине. В противном случае перейдите к шагу 4.
Выполните инициализаторы экземпляра и инициализаторы переменных экземпляра для этого класса, присваивая значения инициализаторов переменной экземпляра соответствующим переменным экземпляра в порядке слева направо, в котором они отображаются в текстовом виде в исходном коде для класс. Если выполнение любого из этих инициализаторов приводит к исключению, то никакие новые инициализаторы не обрабатываются, и эта процедура завершается внезапно с тем же исключением. В противном случае перейдите к шагу 5.
Выполните оставшуюся часть тела этого конструктора. Если это выполнение завершается внезапно, то эта процедура завершается внезапно по той же причине. В противном случае эта процедура выполняется нормально.
Как мы видим на шаге 3, наличие явного вызова супер-конструктора просто изменяет конструкцию суперкласса.
Ответ 2
Ниже приведен пример, который печатает порядок каждого шага во время создания объекта.
InstanceCreateStepTest.java:
Исполнение:
Просто вызовите основной метод, а затем проверьте вывод.
Советы:
Пробуем улучшенный оператор instanceof в Java 14
Сегодня я хотел бы поиграться с этим новым оператором и рассмотреть особенности его работы более детально. Так как паттерн-матчинг по типу ещё не вошёл в главный репозиторий JDK, мне пришлось скачать репозиторий проекта Amber, в котором ведётся разработка новых синтаксических конструкций Java, и собрать JDK из этого репозитория.
Итак, первое, что мы сделаем — проверим версию Java, чтобы убедиться, что мы действительно используем JDK 14:
Теперь напишем небольшой кусок кода со «старым» оператором instanceof и запустим его:
Работает. Это стандартная проверка на тип с последующим приведением. Подобные конструкции мы пишем изо дня в день, какую бы версию Java мы бы не использовали, хоть 1.0, хоть 13.
Но теперь у нас в руках Java 14, и давайте перепишем код с использованием улучшенного оператора instanceof (повторяющиеся строки кода в дальнейшем буду опускать):
Давайте попробуем написать что-нибудь более навороченное и добавим второе условие, которое использует только что объявленную переменную:
Компилируется и работает. А что если поменять условия местами?
Ошибка компиляции. Чего и следовало ожидать: переменная str ещё не объявлена, а значит не может быть использована.
Кстати, что с мутабельностью? Переменная final или нет? Пробуем:
Ага, переменная final. Это значит, что слово «переменная» здесь вообще не совсем корректно. Да и компилятор использует специальный термин «pattern binding». Поэтому предлагаю отныне говорить не «переменная», а «биндинг паттерна» (к сожалению, слово «binding» не очень хорошо переводится на русский).
С мутабельностью и терминологией разобрались. Поехали экспериментировать дальше. Вдруг у нас получится «сломать» компилятор?
Что если назвать переменную и биндинг паттерна одним и тем же именем?
Логично. Перекрытие переменной из внешней области видимости не работает. Это эквивалентно тому, как если бы мы просто завели переменную obj второй раз в той же области видимости.
Компилятор надёжен как бетон.
Сработало. Компилятор не только надёжен, но ещё и умён.
Неужели не удастся «сломать» компилятор?
Ага! Вот это уже похоже на баг. Ведь все три условия абсолютно эквивалентны:
Но да ладно, давайте попробуем ещё что-нибудь. Будет ли работать такое:
Скомпилировалось. Это хорошо, поскольку этот код эквивалентен следующему:
А так как оба варианта эквивалентны, то и программист ожидает, что они будут работать одинаково.
Что насчёт перекрытия полей?
Компилятор не заругался. Это вполне логично, потому что локальные переменные всегда могли перекрывать поля. Для биндингов паттернов, видимо, тоже решили не делать исключения. С другой стороны, такой код довольно хрупок. Одно неосторожное движение, и вы можете не заметить, как ваша ветка if сломалась:
Кстати, используя это свойство, можно укоротить подобные цепочки:
Напишите в комментариях, что вы думаете по поводу такого стиля. Стали ли бы вы использовать такую идиому?
Что насчёт дженериков?
ИМХО, это довольно серьёзная проблема. С другой стороны, я не знаю, как можно было бы её исправить. Похоже, опять придётся полагаться на инспекции в IDE.
Выводы
В целом, новый паттерн-матчинг по типу работает очень круто. Улучшенный оператор instanceof позволяет делать не только тест на тип, но ещё и объявлять готовый биндинг этого типа, избавляя от необходимости ручного приведения. Это означает, что в коде будет меньше шума, и читателю будет гораздо проще разглядеть полезную логику. Например, большинство реализаций equals() можно будет писать в одну строчку:
С другой стороны, вызывают небольшие вопросы несколько спорных моментов:
Инстанцируем java.lang.Class
Конструктор java.lang.Class является одной из самых охраняемых сущностей в языке Java. В спецификации чётко сказано, что объекты типа Class может создавать только сама JRE и что нам тут делать нечего, но так ли это на самом деле?
Предлагаю погрузиться в глубины Reflection API (и не только) и выяснить, как там всё устроено и насколько трудно будет обойти имеющиеся ограничения.
Эксперимент я провожу на 64-битной JDK 1.8.0_151 с дефолтными настройками. Про Java 9 будет в самом конце статьи.
Уровень 1. Простой
Начнём с самых наивных попыток и пойдём по нарастающей. Сперва посмотрим врагу в лицо:
Ничего особенного этот конструктор собой не представляет. Компилятор не делает для него никаких исключений, и в байткоде конструктор тоже присутствует. Поэтому попробуем поступить так же, как мы бы поступили с любым другим классом:
Вполне ожидаемо данный код не будет работать и выдаст следующую ошибку:
Уровень 2. Посложнее
Уровень 3. Нативный
Время узнать, каких вообще типов бывают объекты ConstructorAccessor (помимо описанного выше):
Здесь нет никаких подвохов: объект создаётся без лишних ограничений, и всё, что нам остаётся, так это с его помощью инстанцировать java.lang.Class :
Но тут ждёт сюрприз:
Уровень 4. Магический
Такой код, естественно, обратно не скомпилируется, и этому есть две причины:
Данные инструкции разнесены для того, чтобы находиться в разных try-блоках. Почему сделано именно так, я не знаю. Вероятно, это был единственный способ обрабатывать исключения так, чтобы они полностью соответствовали спецификации языка.
Если закрыть глаза на нетипичную обработку исключений, то во всём остальном данный класс абсолютно нормален, но всё ещё непонятно, откуда у него вдруг права на вызов приватных конструкторов. Оказывается, всё дело в суперклассе:
и опять получаем исключение:
Вот это уже действительно интересно. Судя по всему, обещанной магии не произошло. Или я ошибаюсь?
Уровень 5. Современный
Reflection не помог решить проблему. Может быть, дело в том, что Reflection старый и слабый, и вместо него стоит использовать молодой и сильный MethodHandles? Думаю, да. Как минимум, стоит попробовать.
И как только я решил, что Reflection не нужен, он тут же пригодился. MethodHandles — это, конечно, хорошо, но с помощью него принято получать лишь те данные, к которым есть доступ. А если понадобился приватный конструктор, то придётся выкручиваться по старинке.
После запуска этого метода я был откровенно удивлён — lookup делает вид, что конструктора вообще не существует, хотя он точно присутствует в классе!
Данный метод уж точно корректно отработает и вернёт тот handle, который должен.
Момент истины. Запускаем метод инстанцирования:
Думаю, вы уже догадались, что ничего хорошего не произойдёт, но давайте хоть глянем на ошибку. Сейчас это что-то новенькое:
Раз даже Unsafe не смог, мне остаётся лишь прийти к печальному заключению: аллоцировать новый объект java.lang.Class невозможно. Интересно выходит — думал, что запрещён конструктор, а запрещена аллокация! Попробуем это дело обойти.
Уровень 6. Небезопасный
Предлагаю создать пустой объект и взглянуть, из чего же он состоит. Для этого возьмём Unsafe и аллоцируем новенький java.lang.Object :
На текущей JVM результатом будет область памяти в 12 байт, выглядящая вот так:
То, что вы здесь видите, это «заголовок объекта». По большому счёту, он состоит из двух частей — 8 байт markword, которые нас не интересуют, и 4 байта classword, которые важны.
Каким образом JVM узнаёт класс объекта? Она делает это путём чтения области classword, которая хранит указатель на внутреннюю структуру JVM, описывающую класс. Значит если в данное место записать другое значение, то и класс объекта изменится!
Дальнейший код очень, очень плохой, никогда так не делайте:
Так начинается создание класса (константы импортированы статически, так короче):
Так ему создаётся конструктор:
Класс готов. Осталось загрузить, инстанцировать и вызвать нужный метод:
И это работает! Точнее так: повезло, что работает. Можно проверить, запустив следующий код:
О том, в какую область памяти записался этот ClassLoader и откуда потом прочитался, я тактично умолчу. И, как ожидалось, вызов практически любого другого метода на данном объекте приводит к немедленному краху JRE. А в остальном — цель выполнена!
Что там в Java 9?
В Java 9 всё почти так же. Можно проделать все те же действия, но с несколькими оговорками:
В целом, в Java 9 нужно намного сильнее постараться, чтобы выстрелить себе в ногу, даже если именно это и является главной целью. Думаю, это и к лучшему.
Языки программирования
Страницы
Классы и объекты в Java
Базовая терминология и концепции ООП
Объявление метода с таким же именем как у класса называется конструктором. Конструктор выполняется при создании объекта из класса.
При создании объекта возвращается ссылочное значение (reference value). Ссылочное значение отсылает к объекту. Переменная метит место в памяти, куда может быть сохранено значение. Ссылка на объект (object reference) это переменная, которая хранит ссылочное значение. В Java объектами можно манипулировать только посредством их ссылочных значений или, что то же самое, посредством ссылок, которые содержат ссылочные значения.
Несколько ссылок могут ссылаться на один и тот же объект, т.е. они хранят ссылочное значение одного и того же объекта. Такие ссылки называются псевдонимами. Объектом можно управлять через любой из его псевдонимов.
Когда на объект больше нет ссылок, то его память при необходимости забирается и перераспределяется для других объектов. Этот процесс называется автоматическая сборка мусора. В Java эту заботу берет на себя среда исполнения.
Каждый созданный объект имеет свою копию полей определенных в классе. Поля объекта называются переменными экземпляра (instance variables). Значения переменных экземпляра конституциируют состояние объекта (state). Два разных объекта могут иметь одно и то же состояние, если их переменные экземпляра имеют одни и те же значения. Методы объекта определяющие его поведение называются методами экземпляра (instance methods). Эти методы принадлежат каждому объекту класса. Имплементация этих методов находится в общем использовании у всех объектов класса. Переменные и методы экземпляра, которые принадлежат объектам называются общим термином члены экземпляра (instance members).
В некоторых случаях члены должны принадлежать только классу, т.е. они не должны быть частью ни одного из экземпляров класса. Члены, которые принадлежат только классу называются статическими членами (static members). Поля объявленные с ключевым словом static называются статическими переменными (static variable). Статическая переменная инициализируется при загрузке класса в среду исполнения. Также существуют статические методы (static methods), которые принадлежат скорее к классу, чем к объектам этого класса.
Объекты взаимодействуют друг с другом через передачу сообщений. В Java это делается вызовом метода у объекта посредством бинарного оператора точки. Вызов метода содержит: получателя сообщения, метод для запуска, аргументы метода. Метод запускаемый у получателя может вернуть информацию отправителю посредством одиночного возвращаемого значения. Вызываемый метод должен быть определен для объекта, иначе будет ошибка компиляции.
Dot (.) нотация может использоваться вместе с ссылкой для доступа к полям объекта. При использовании dot нотации учитывается доступность (accessibility) члена. Члены в классе имеющие уровень доступности private не доступны извне класса.
Ассоциации: агрегация и композиция
Базовые элементы декларации класса
Инстанцирование
Каждый созданный объект имеет свою копию полей определенных в классе объекта. Два объекта стека, на которые ссылаются stack1 и stack2, будут иметь собственные поля stackArray и topOfStack.
Запуск методов
Статические члены класса
Клиент может получать доступ к статическим членам класса используя имя класса.
Также статические члены могут быть доступны из ссылок на объекты, но это считается плохим стилем.
Для сравнения, члены экземпляра могут быть доступны только по ссылкам на объекты.
Инстанцируем java.lang.Class
Конструктор java.lang.Class является одной из самых охраняемых сущностей в языке Java. В спецификации чётко сказано, что объекты типа Class может создавать только сама JVM и что нам тут делать нечего, но так ли это на самом деле?
Предлагаю погрузиться в глубины Reflection API (и не только) и выяснить, как там всё устроено и насколько трудно будет обойти имеющиеся ограничения.
Эксперимент я провожу на 64-битной JDK 1.8.0_151 с дефолтными настройками. Про Java 9 будет в самом конце статьи.
Уровень 1. Простой
Начнём с самых наивных попыток и пойдём по нарастающей. Сперва посмотрим врагу в лицо:
Ничего особенного этот конструктор собой не представляет. Компилятор не делает для него никаких исключений, и в байткоде конструктор тоже присутствует. Поэтому попробуем поступить так же, как мы бы поступили с любым другим классом:
Вполне ожидаемо данный код не будет работать и выдаст следующую ошибку:
Уровень 2. Посложнее
Уровень 3. Нативный
Время узнать, каких вообще типов бывают объекты ConstructorAccessor (помимо описанного выше):
Здесь нет никаких подвохов: объект создаётся без лишних ограничений, и всё, что нам остаётся, так это с его помощью инстанцировать java.lang.Class :
Но тут ждёт сюрприз:
Уровень 4. Магический
Такой код, естественно, обратно не скомпилируется, и этому есть две причины:
Данные инструкции разнесены для того, чтобы находиться в разных try-блоках. Почему сделано именно так, я не знаю. Вероятно, это был единственный способ обрабатывать исключения так, чтобы они полностью соответствовали спецификации языка.
Если закрыть глаза на нетипичную обработку исключений, то во всём остальном данный класс абсолютно нормален, но всё ещё непонятно, откуда у него вдруг права на вызов приватных конструкторов. Оказывается, всё дело в суперклассе:
и опять получаем исключение:
Вот это уже действительно интересно. Судя по всему, обещанной магии не произошло. Или я ошибаюсь?
Уровень 5. Современный
Reflection не помог решить проблему. Может быть, дело в том, что Reflection старый и слабый, и вместо него стоит использовать молодой и сильный MethodHandles? Думаю, да. Как минимум, стоит попробовать.
И как только я решил, что Reflection не нужен, он тут же пригодился. MethodHandles — это, конечно, хорошо, но с помощью него принято получать лишь те данные, к которым есть доступ. А если понадобился приватный конструктор, то придётся выкручиваться по старинке.
После запуска этого метода я был откровенно удивлён — lookup делает вид, что конструктора вообще не существует, хотя он точно присутствует в классе!
Данный метод уж точно корректно отработает и вернёт тот handle, который должен.
Момент истины. Запускаем метод инстанцирования:
Думаю, вы уже догадались, что ничего хорошего не произойдёт, но давайте хоть глянем на ошибку. Сейчас это что-то новенькое:
Раз даже Unsafe не смог, мне остаётся лишь прийти к печальному заключению: аллоцировать новый объект java.lang.Class невозможно. Интересно выходит — думал, что запрещён конструктор, а запрещена аллокация! Попробуем это дело обойти.
Уровень 6. Небезопасный
Предлагаю создать пустой объект и взглянуть, из чего же он состоит. Для этого возьмём Unsafe и аллоцируем новенький java.lang.Object :
На текущей JVM результатом будет область памяти в 12 байт, выглядящая вот так:
То, что вы здесь видите, это «заголовок объекта». По большому счёту, он состоит из двух частей — 8 байт markword, которые нас не интересуют, и 4 байта classword, которые важны.
Каким образом JVM узнаёт класс объекта? Она делает это путём чтения области classword, которая хранит указатель на внутреннюю структуру JVM, описывающую класс. Значит если в данное место записать другое значение, то и класс объекта изменится!
Дальнейший код очень, очень плохой, никогда так не делайте:
Так начинается создание класса (константы импортированы статически, так короче):
Так ему создаётся конструктор:
Класс готов. Осталось загрузить, инстанцировать и вызвать нужный метод:
И это работает! Точнее так: повезло, что работает. Можно проверить, запустив следующий код:
О том, в какую область памяти записался этот ClassLoader и откуда потом прочитался, я тактично умолчу. И, как ожидалось, вызов практически любого другого метода на данном объекте приводит к немедленному краху JVM. А в остальном — цель выполнена!
Что там в Java 9?
В Java 9 всё почти так же. Можно проделать все те же действия, но с несколькими оговорками:
В целом, в Java 9 нужно намного сильнее постараться, чтобы выстрелить себе в ногу, даже если именно это и является главной целью. Думаю, это и к лучшему.