Перечислите все валидные сигнатуры конструкторов класса clazz

Обновлено: 16.05.2024

Я думаю, почти любого Java разработчика когда-то спрашивали на собеседовании: «Какие есть методы у класса Object?»
Меня, по крайней мере, спрашивали неоднократно. И, если в первый раз это было неожиданностью (кажется, забыл про clone), то потом я был уверен, что уж методы Object'а-то я знаю;)

И каково же было мое удивление, когда спустя несколько лет разработки я наткнулся на собственное незнание сигнатуры метода getClass()

Под катом пара слов про Class, .class, .getClass и, собственно, сюрприз, на который я наткнулся.

Итак, у нас есть класс А и объект этого класса a:

0. A.class vs a.getClass()

Начнем с простого. При вызове getClass() может отработать полиморфизм, и результатом будет класс-потомок.

Тут была ложь, на которую мне указали в комментариях. class — это не статическое поле, коим может показаться (и даже не нативное-псевдо-статическое поле, как думал я), а особая конструкция языка. И, в отличие от статического поля, обратиться к нему через объект нельзя!

Но это так, цветочки. Идем дальше.

1. А что такое этот ваш Class?

A.class — объект класса Class. Смотрим в Class.java:

Это дженерик. Причем типизирован он, очевидно, этим самым A — классом, у которого вызвали .class

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

A.class возвращает объект класса Class:

2. А что же возвращает a.getClass()?

Собрав воедино все вышесказанное, можно догадаться, что:

Действительно, ввиду полиморфизма нужно не забывать, что фактический класс объекта a — не обязательно A — это может быть любой подкласс:

3. А что же написано в Object.java?

Все эти дженерики — это, конечно, замечательно, но как записать сигнатуру метода getClass синтаксисом java в классе Object?
А никак:

А на вопрос, почему не компилировался пример выше, ответит Максим Поташев джавадок к методу:

The actual result type is Class where |X| is the erasure of the static type of the expression on which getClass is called.

Так что в Object.java написана одна сигнатура, а компилятор подставляет другую.

Как получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах

Это уже поинтереснее! В текущем классе у нас нет констант и родительского класса. Давай добавим их для полноты картины. Создадим самый простой родительский класс Animal : И добавим в наш класс Cat наследование от Animal и одну константу: Теперь у нас полный набор! Давай испытаем возможности рефлексии :) Вот что мы получим в консоли: Как много подробной информации о классе мы получили! Причем не только о публичных, но и о приватных частях. Обрати внимание: private -переменные тоже отображены в списке. Собственно, «анализ» класса можно на этом считать завершенным: теперь с помощью метода analyzeClass() мы узнаем все, что только можно. Но это не все возможности, которые у нас есть при работе с рефлексией. Не будем ограничиваться простым наблюдением и перейдем к активным действиям! :)

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

Примеры использования Reflection - 3

Начнем с конструктора по умолчанию. Его пока нет в нашем классе Cat , поэтому давай добавим его: Вот как будет выглядеть код для создания объекта Cat с помощью рефлексии (метод createCat() ): Вводим в консоль: Вывод в консоль: Это не ошибка: значения name и age отображаются в консоли из-за того, что мы запрограммировали их вывод в методе toString() класса Cat . Здесь мы считываем имя класса, объект которого будем создавать, с консоли. Запущенная программа узнает имя класса, объект которого ей предстоит создать. Для краткости мы опустили код правильной обработки исключений, чтобы он не занял больше места, чем сам пример. В реальной программе, конечно, обязательно стоит обработать ситуации ввода некорректных имен и т.д. Конструктор по умолчанию — штука довольно простая, поэтому и создать экземпляр класса с его помощью, как видишь, несложно :) А с помощью метода newInstance() мы создаем новый объект этого класса. Другое дело, если конструктор класса Cat будет принимать на вход параметры. Удалим дефолтный конструктор из класса и попробуем запустить наш код снова. Что-то пошло не так! Мы получили ошибку, потому что вызвали метод для создания объекта через конструктор по умолчанию. А ведь такого конструктора у нас теперь нет. Значит при работе метода newInstance() механизм рефлексии будет использовать наш старый конструктор с двумя параметрами: А с параметрами-то мы ничего не сделали, как будто вообще забыли о них! Чтобы передать их в конструктор с помощью рефлексии, придется немного «похимичить»: Вывод в консоль: Давай рассмотрим подробнее, что в нашей программе происходит. Мы создали массив объектов Class . Они соответствуют параметрам нашего конструктора (у нас там как раз параметры String и int ). Мы передаем их в метод clazz.getConstructor() и получаем доступ к нужному конструктору. После этого остается только вызвать метод newInstance() с нужными параметрами и не забыть явно привести объект к нужному нам классу — Cat . В результате наш объект успешно создастся! Вывод в консоль: Едем дальше :)

Как получить и установить значение поля объекта по имени

Представь, что ты используешь класс, написанный другим программистом. При этом у тебя нет возможности его редактировать. Например, готовую библиотеку классов, упакованную в JAR. Прочитать код классов ты можешь, а вот поменять — нет. Программист, создавший класс в этой библиотеке (пусть это будет наш старый класс Cat ) не выспался перед финальным проектированием и удалил геттеры и сеттеры для поля age . Теперь этот класс попал к тебе. Он полностью соответствует твоим запросам, ведь тебе как раз нужны в программе объекты Cat . Но они нужны тебе с тем самым полем age ! Это проблема: достучаться до поля мы не можем, ведь оно имеет модификатор private , а геттеры и сеттеры удалил горе-разработчик этого класса :/ Что ж, рефлексия способна помочь нам и в этой ситуации! Доступ к коду класса Cat у нас есть: мы можем хотя бы узнать, какие у него есть поля и как они называются. Вооружившись этой информацией, решаем нашу проблему: Как и сказано в комментарии, с полем name все просто: для него разработчики класса предоставили сеттер. Создавать объекты из конструкторов по умолчанию ты тоже уже умеешь: для этого есть метод newInstance() . А вот со вторым полем придется повозиться. Давай разбираться, что у нас тут происходит :) Здесь мы, используя наш объект Class clazz , получаем доступ к полю age с помощью метода getDeclaredField() . Он дает нам возможность получить поле age в виде объекта Field age . Но этого пока недостаточно, ведь private -полям нельзя просто так присваивать значения. Для этого нужно сделать поле «доступным» с помощью метода setAccessible() : Тем полям, для которых это сделано, можно присваивать значения: Как видишь, у нас получился эдакий перевернутый с ног на голову сеттер: мы присваиваем полю Field age его значение, а также передаем ему объект, в который это поле должно быть присвоено. Запустим наш метод main() и увидим: Отлично, у нас все получилось! :) Посмотрим, какие еще возможности у нас есть…

Как вызвать метод объекта по имени

Производительность снижается. У методов, которые вызываются с помощью рефлексии, меньшая производительность по сравнению с методами, которые вызываются обычным способом.

Есть ограничения по безопасности. Механизм рефлексии позволяет менять поведение программы во время выполнения (runtime). Но в твоем рабочем окружении на реальном проекте могут быть ограничения, не позволяющие этого делать.

Риск раскрытия внутренней информации. Важно понимать, что использование рефлексии напрямую нарушает принцип инкапсуляции: позволяет нам получить доступ к приватным полям, методам и т.д. Думаю, не стоит объяснять, что к прямому и грубому нарушению принципов ООП стоит прибегать только в самых крайних случаях, когда иных способов решить задачу не существует по не зависящим от тебя причинам.

Зачем нужны конструкторы? Объясните подробно

Предположим, у нас есть класс с именем Student . И у нас есть имя переменной экземпляра и roll_number . Теперь, если мы создадим 1000 объектов, то JVM инициализирует эти значения своим типом по умолчанию Name = null и rollNo = 0 . Идентификация этих отдельных объектов невозможна, а присвоение значений каждому из объектов увеличит объем кода, что считается плохой практикой в программировании. Поэтому, чтобы этого избежать, используются конструкторы. То есть, цель конструктора в Java состоит в том, чтобы инициализировать значение переменных экземпляра класса.

Какие типы конструкторов существуют в Java?

  • Default Constructor (Конструктор по умолчанию)
  • No-Argument Constructor (Конструктор без аргументов)
  • Parameterized Constructor (Параметризованный конструктор)

Что такое Default Constructor в Java?

Конструктор по умолчанию (Default Constructor) — это конструктор, созданный JVM во время выполнения, если конструктор не определен в классе. Основная задача конструктора по умолчанию — инициализировать значения экземпляров в соответствии с их типом по умолчанию. Пример конструктора по умолчанию в Java: Теперь для этого класса, если мы создадим объект, то внутри JVM появится конструктор по умолчанию, которому присвоено значение по умолчанию. Теперь, если мы напечатаем значение, то получим:

Что такое No-Argument Constructor (Конструктор без аргументов)?

Конструктор без аргументов — это конструктор, который можно явно определить для инициализации значения экземпляров. Например:

Что такое Parameterized Constructor (Параметризованный конструктор)?

Параметризованный конструктор — это конструктор, который принимает параметр для инициализации экземпляров. Например:

Каковы правила определения конструктора?

Имя конструктора должно совпадать с именем класса.

В Java не должно быть возвращаемого типа конструктора.

Единственно применяемые модификаторы для конструкторов:

  • public
  • default
  • protected
  • private

Конструкторы могут принимать любое количество параметров.

Модификаторы final, synchronized, static и abstract в конструкторе недопустимы.

Конструктор не поддерживает оператор return внутри своего тела.

В конструкторе могут быть исключения с оператором throw .

Допустимо использовать throws clause с конструктором.

Конструктор не должен формировать рекурсию.

Когда мы можем использовать private (частный) конструктор?

Если мы не хотим создавать объекты определенного класса извне, мы можем использовать закрытые или частные (private) конструкторы. Объявив конструкторы закрытыми, мы можем создавать объекты только внутри класса. Классы Singleton — хороший пример использования частных конструкторов.

Каким будет модификатор доступа конструктора по умолчанию, если мы не определим его явно?

Модификатор доступа конструктора по умолчанию всегда будет таким же, как модификатор класса. Если класс публичный, то конструктор тоже будет публичным. Если класс частный, то конструктор тоже будет частным. Аналогично будет и с другими модификаторами доступа.

Напишите вывод по приведенному ниже фрагменту кода и поясните

Мы получим такой вывод, потому что если мы не укажем в конструкторе ключевое слово super() или this() в первой строке, то JVM сама автоматически поместит его во время выполнения. JVM делает это, потому что он наследуется от другого класса, и его функциональность также будет реализована в производном классе. Таким образом, присваивая экземплярам базового класса значения по умолчанию, JVM по умолчанию добавляет ключевое слово super() .

Рассмотрите код и укажите, является ли он действительным (valid) или недействительным (invalid). Поясните причину

Приведенный выше код недействителен, потому что он является одним и тем же конструктором внутри конструктора Scaler Academy . Это создает рекурсию в конструкторе, что недопустимо. Соответственно, мы получим ошибку времени компиляции, связанную с вызовом рекурсивного конструктора.

Можем ли мы использовать два конструктора в одном классе в Java?

  • Параметры конструкторов должны быть разными.
  • В конструкторе не должно быть рекурсии.

Можем ли мы переопределить (override) конструктор в Java?

Может ли конструктор быть окончательным (final) в Java?

Никакой конструктор не может быть окончательным (final). Это связано с тем, что ключевые слова final используются для остановки переопределения метода в производном классе. Но в конструкторе концепция переопределения неприменима, поэтому нет необходимости писать ключевое слово final . Если же мы напишем ключевое слово final в конструкторе, то получим ошибку времени компиляции под названием required return type (требуется возвращаемый тип), потому что компилятор рассматривает это как метод.

Может ли конструктор быть статическим (static) в Java?

Нет, конструктор Java не может быть статическим. Это связано с тем, что ключевые слова static используются, когда мы хотим, чтобы член принадлежал классу, а не объекту. Но конструкторы предназначены для инициализации объектов, поэтому компилятор будет рассматривать его как метод. Мы получим ошибку required return type.

Опишите разницу между super(), super и this(), this

Что такое деструкторы? Существует ли деструктор в Java?

Destructors (Деструкторы) используются для освобождения памяти, полученной программой. Например, если память необходима программе во время ее выполнения, то деструктор освобождает эту память, чтобы ее могли использовать другие программы. В Java нет понятия деструктора, поскольку работа по освобождению памяти в Java обрабатывается сборщиком мусора.

Что такое цепь конструкторов (constructor chaining) в Java?

Конструкторы классов. Java JDK 1.5 - 1

Конструктор имеется в любом классе. Даже если вы его не написали, компилятор Java сам создаст конструктор по умолчанию (default constructor). Этот конструктор пустой и не делает ничего, кроме вызова конструктора суперкласса. Т.е. если написать: то это эквивалентно написанию: В данном случае явно класса предка не указано, а по умолчанию все классы Java наследуют класс Object поэтому вызывается конструктор класса Object . Если в классе определен конструктор с параметрами, а перегруженного конструктора без параметров нет, то вызов конструктора без параметров является ошибкой. Тем не менее, в Java, начиная с версии 1.5, можно использовать конструкторы с аргументами переменной длины. И если есть конструктор, имеющий аргумент переменной длины, то вызов конструктора по умолчанию ошибкой не будет. Не будет потому, что аргумент переменной длины может быть пустым. Например, следующий пример не будет компилироваться, однако если раскомментарить конструктор с аргументом переменной длины, то компиляция и запуск пройдут успешно и в результате работы строки кода DefaultDemo dd = new DefaultDemo() ; вызовется конструктор DefaultDemo(int . v) . Естественно, что в данном случае необходимо пользоваться JSDK 1.5. Файл DefaultDemo.java Результат вывода программы при раскомментаренном конструкторе: Однако, в распространенном случае, когда в классе вообще не определено ни одного конструктора, вызов конструктора по умолчанию (без параметров) будет обязательным явлением, поскольку подстановка конструктора по умолчанию происходит автоматически.

Создание объекта и конструкторы

  • Ищется класс объекта среди уже используемых в программе классов. Если его нет, то он ищется во всех доступных программе каталогах и библиотеках. После обнаружения класса в каталоге или библиотеке выполняется создание, и инициализация статических полей класса. Т.е. для каждого класса статические поля инициализируются только один раз.
  • Выделяется память под объект.
  • Выполняется инициализация полей класса.
  • Отрабатывает конструктор класса.
  • Формируется ссылка на созданный и инициализированный объект. Эта ссылка и является значением выражения, создающего объект. Объект может быть создан и с помощью вызова метода newInstance() класса java.lang.Class . В этом случае используется конструктор без списка параметров.

Перегрузка конструкторов

Конструкторы одного класса могут иметь одинаковое имя и различную сигнатуру. Такое свойство называется совмещением или перегрузкой(overloading). Если класс имеет несколько конструкторов, то присутствует перегрузка конструкторов.

Параметризированные конструкторы

Сигнатура конструктора – это количество и типы параметров, а также последовательность их типов в списке параметров конструктора. Тип возвращаемого результата не учитывается. Конструктор не возвращает никаких параметров. Это положение объясняет в некотором смысле, как Java различает перегруженные конструкторы или методы. Java различает перегруженные методы не по возвращаемому типу, а по числу, типам и последовательности типов входных параметров. Конструктор не может возвращать даже тип void , иначе он превратится в обычный метод, даже не смотря на сходство с именем класса. Следующий пример демонстрирует это. Файл VoidDemo.java В результате программа выведет: Это лишний раз доказывает, что конструктором является метод без возвращаемых параметров. Тем не менее, для конструктора можно задать один из трех модификаторов public , private или protected . И пример теперь будет выглядеть следующим образом: Файл VoidDemo2.java В конструкторе разрешается записывать оператор return , но только пустой, без всякого возвращаемого значения. Файл ReturnDemo.java

Конструкторы, параметризированные аргументами переменной длины

В Java SDK 1.5 появился долгожданный инструмент – аргументы переменной длины для конструкторов и методов(variable-length arguments). До этого переменное количество документов обрабатывалось двумя неудобными способами. Первый из них был рассчитан на то, что максимальное число аргументов ограничено небольшим количеством и заранее известно. В таком случае можно было создавать перегружаемые версии метода, по одной на каждый вариант списка передаваемых в метод аргументов. Второй способ рассчитан на неизвестное заранее и большое количество аргументов. В этом случае аргументы помещались в массив, и этот массив передавался методу. Аргументы переменной длины чаще всего задействованы в последующих манипуляциях с инициализациями переменных. Отсутствие некоторых из ожидаемых аргументов конструктора или метода удобно заменять значениями по умолчанию. Аргумент переменной длины есть массив, и обрабатывается как массив. Например, конструктор для класса Checking с переменным числом аргументов будет выглядеть так: Символьная комбинация . сообщает компилятору о том, что будет использоваться переменное число аргументов, и что эти аргументы будут храниться в массиве, значение ссылки на который содержится в переменной n. Конструктор может вызываться с разным числом аргументов, включая их полное отсутствие. Аргументы автоматически помещаются в массив и передаются через n. В случае отсутствия аргументов длина массива равна 0. В список параметров наряду с аргументами переменной длины могут быть включены и обязательные параметры. В этом случае параметр, содержащий переменное число аргументов должен обязательно быть последним в списке параметров. Например: Вполне очевидное ограничение касается количества параметров с переменной длиной. В списке параметров должен быть только один параметр переменной длины. При наличии двух параметров переменной длины компилятору невозможно определить, где заканчивается один параметр и начинается другой. Например: Файл Checking.java Например, есть аппаратура, способная распознавать номера автомобилей и запоминать номера квадратов местности, где побывал каждый из автомобилей за день. Необходимо из общей массы зафиксированных автомобилей отобрать те, которые в течение дня побывали в двух заданных квадратах, скажем 22 и 15, согласно карте местности. Вполне естественно, что автомобиль может в течение дня побывать во многих квадратах, а может только в одном. Очевидно, что количество посещенных квадратов ограничено физической скоростью автомобиля. Составим небольшую программу, где конструктор класса будет принимать в качестве аргументов номер автомобиля как обязательный параметр и номера посещенных квадратов местности, число которых может быть переменным. Конструктор будет проверять, не появился ли автомобиль в двух квадратах, если появился, то вывести его номер на экран.

Передача параметров в конструктор

  • основные типы (примитивы);
  • ссылки на объекты.
  • конструктор не может менять значения входных параметров основных (примитивных) типов;
  • конструктор не может изменять ссылки входных параметров;
  • конструктор не может переназначать ссылки входных параметров на новые объекты.
  • изменять состояние объекта, передаваемого в качестве входного параметра.

Конструкторы и блоки инициализации, последовательность действий при вызове конструктора

  1. Все поля данных инициализируются своими значениями, предусмотренными по умолчанию (0, false или null).
  2. Инициализаторы всех полей и блоки инициализации выполняются в порядке их перечисления в объявлении класса.
  3. Если в первой строке конструктора вызывается другой конструктор, то выполняется вызванный конструктор.
  4. Выполняется тело конструктора.
  • присвоить значение в объявлении;
  • присвоить значения в блоке инициализации;
  • задать его значение в конструкторе.

Ключевое слово this в конструкторах

Конструкторы используют this чтобы сослаться на другой конструктор в этом же классе, но с другим списком параметров. Если конструктор использует ключевое слово this , то оно должно быть в первой строке, игнорирование этого правила приведет к ошибке компилятора. Например: Файл ThisDemo.java Результат вывода программы: В данном примере имеется два конструктора. Первый получает строку-аргумент. Второй не получает никаких аргументов, он просто вызывает первый конструктор используя имя "John" по-умолчанию. Таким образом, можно с помощью конструкторов инициализировать значения полей явно и по умолчанию, что часто необходимо в программах.

Ключевое слово super в конструкторах

Конструкторы используют super , чтобы вызвать конструктор суперкласса. Если конструктор использует super , то этот вызов должен быть в первой строке, иначе компилятор выдаст ошибку. Ниже приведен пример: Файл SuperClassDemo.java В этом простом примере конструктор Child() содержит вызов super() , который создает экземпляр класса SuperClassDemo , в дополнение к классу Child . Так как super должен быть первым оператором, выполняемым в конструкторе подкласса, этот порядок всегда одинаков и не зависит от того, используется ли super() . Если он не используется, то сначала будет выполнен конструктор по умолчанию (без параметров) каждого суперкласса, начиная с базового класса. Следующая программа демонстрирует, когда выполняются конструкторы. Файл Call.java Вывод этой программы: Конструкторы вызываются в порядке подчиненности классов. В этом есть определенный смысл. Поскольку суперкласс не имеет никакого знания о каком-либо подклассе, то любая инициализация, которую ему нужно выполнить, является отдельной. По возможности она должна предшествовать любой инициализации, выполняемой подклассом. Поэтому-то она и должна выполняться первой.

Настраиваемые конструкторы

Механизм идентификации типа во время выполнения является одним из мощных базовых принципов языка Java, который реализует полиморфизм. Однако такой механизм не страхует разработчика от несовместимого приведения типов в ряде случаев. Самый частый случай – манипулирование группой объектов, различные типы которых заранее неизвестны и определяются во время выполнения. Поскольку ошибки, связанные с несовместимостью типов могут проявиться только на этапе выполнения, то это затрудняет их поиск и ликвидацию. Введение настраиваемых типов в Java 2 5.0 частично отодвигает возникновение подобных ошибок с этапа выполнения на этап компиляции и обеспечивает недостающую типовую безопасность. Отпадает необходимость в явном приведении типов при переходе от типа Object к конкретному типу. Следует иметь ввиду, что средства настройки типов работают только с объектами и не распространяются на примитивные типы данных, которые лежат вне дерева наследования классов. Благодаря настраиваемым типам все приведения выполняются автоматически и скрыто. Это позволяет обезопасить от несоответствия типов и гораздо чаще повторно использовать код. Настраиваемые типы можно использовать в конструкторах. Конструкторы могут быть настраиваемыми, даже если их класс не является настраиваемым типом. Например: Поскольку конструктор GenConstructor задает параметр настраиваемого типа, который должен быть производным классом от класса Number , его можно вызвать с любы


Конструктор java.lang.Class является одной из самых охраняемых сущностей в языке Java. В спецификации чётко сказано, что объекты типа Class может создавать только сама JVM и что нам тут делать нечего, но так ли это на самом деле?

Предлагаю погрузиться в глубины Reflection API (и не только) и выяснить, как там всё устроено и насколько трудно будет обойти имеющиеся ограничения.

Эксперимент я провожу на 64-битной JDK 1.8.0_151 с дефолтными настройками. Про Java 9 будет в самом конце статьи.

Уровень 1. Простой

Начнём с самых наивных попыток и пойдём по нарастающей. Сперва посмотрим врагу в лицо:

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

Вполне ожидаемо данный код не будет работать и выдаст следующую ошибку:

С первой же попытки мы попали на первое предупреждение из метода setAccessible0 . Оно захардкожено специально для конструктора класса java.lang.Class :

Не проблема, ведь ключевой строкой в этом методе является последняя — установка поля override в значение true . Это легко сделать, используя грубую силу:

Уровень 2. Посложнее

Естественно, установка флага override — это не единственное ограничение, но теперь мы можем хотя бы продвинуться чуть дальше в работе метода newInstance . Достаточно далеко, чтобы спланировать дальнейшие действия. В этот раз ошибка будет следующая:

Нас занесло прямиком в класс пакета sun.reflect , а мы знаем, что основная магия должна происходить именно там. Самое время заглянуть в реализацию newInstance класса Constructor и узнать, как мы туда попали:

Из реализации становится понятно, что Constructor делегирует всю работу по инстанцированию другому объекту типа ConstructorAccessor . Он инициализируется ленивым образом и в дальнейшем не меняется. Внутренности метода acquireConstructorAccessor описывать не стану, скажу лишь, что в результате он приводит к вызову метода newConstructorAccessor объекта класса sun.reflect.ReflectionFactory . И именно для конструктора класса java.lang.Class (а ещё для абстрактных классов) данный метод возвращает объект InstantiationExceptionConstructorAccessorImpl . Он не умеет ничего инстанцировать, а только бросается исключениями на каждом обращении к нему. Всё это означает лишь одно: правильный ConstructorAccessor придётся инстанцировать самим.

Уровень 3. Нативный

Время узнать, каких вообще типов бывают объекты ConstructorAccessor (помимо описанного выше):

  • BootstrapConstructorAccessorImpl :
    используется для инстанцирования классов, которые сами являются реализацией ConstructorAccessor . Вероятно, спасает какой-то код от бесконечной рекурсии. Штука узкоспециализированная, трогать я её не буду;
  • GeneratedConstructorAccessor :
    самая интересная реализация, о которой я расскажу подробно, но позже;
  • связка NativeConstructorAccessorImpl и DelegatingConstructorAccessorImpl :
    то, что возвращается по умолчанию, и поэтому рассмотрится мною в первую очередь. DelegatingConstructorAccessorImpl попросту делегирует свою работу другому объекту, хранящемуся у него в поле. Плюс данного подхода в том, что он позволяет подменить реализацию на лету. Именно это на самом деле и происходит — NativeConstructorAccessorImpl для каждого конструктора отрабатывает максимум столько раз, сколько указано в системном свойстве sun.reflect.inflationThreshold (15 по умолчанию), после чего подменяется на GeneratedConstructorAccessor . Справедливости ради стоит добавить, что установка свойства sun.reflect.noInflation в значение "true" по сути сбрасывает inflationThreshhold в ноль, и NativeConstructorAccessorImpl перестаёт создаваться в принципе. По умолчанию это свойство имеет значение "false" .

Итак, для самого обычного класса при самых обычных обстоятельствах мы бы получили объект
NativeConstructorAccessorImpl , а значит, именно его и попробуем создать вручную:

Здесь нет никаких подвохов: объект создаётся без лишних ограничений, и всё, что нам остаётся, так это с его помощью инстанцировать java.lang.Class :

Но тут ждёт сюрприз:

Кажется, JVM не ожидает от пользователя столь нелогичных действий, особенно после всех предупреждений. Тем не менее, данный результат можно по праву считать достижением — завалил JVM, ни разу не воспользовавшись классами пакета sun.misc !

Уровень 4. Магический

Нативный вызов не работает — значит, теперь нужно разобраться с GeneratedConstructorAccessor .

На самом деле, это не просто класс, а целое семейство классов. Для каждого конструктора в рантайме генерируется своя уникальная реализация. Именно поэтому в первую очередь используется нативная реализация: генерировать байткод и создавать из него класс дело затратное. Сам процесс генерации класса запрятан в метод generateConstructor класса sun.reflect.MethodAccessorGenerator . Вызвать его вручную не составит труда:

Как и в случае с NativeConstructorAccessorImpl , тут нет подводных камней — данный код отработает и сделает ровно то, что от него ждут. Но давайте задумаемся на минутку: ну сгенерировали мы какой-то класс, откуда у него возьмутся права на вызов приватного конструктора? Такого быть не должно, поэтому мы просто обязаны сдампить сгенерированный класс и изучить его код. Сделать это несложно — встаём отладчиком в метод generateConstructor и в нужный момент дампим нужный нам массив байт в файл. Декомпилированная его версия выглядит следующим образом (после переименования переменных):

Такой код, естественно, обратно не скомпилируется, и этому есть две причины:

  • вызов new Class без скобочек. Он соответствует инструкции NEW , которая выделяет память под объект, но конструктор у него не вызывает;
  • вызов clazz.(classLoader) — это как раз вызов конструктора, который в таком явном виде в языке Java невозможен.

Данные инструкции разнесены для того, чтобы находиться в разных try-блоках. Почему сделано именно так, я не знаю. Вероятно, это был единственный способ обрабатывать исключения так, чтобы они полностью соответствовали спецификации языка.

Если закрыть глаза на нетипичную обработку исключений, то во всём остальном данный класс абсолютно нормален, но всё ещё непонятно, откуда у него вдруг права на вызов приватных конструкторов. Оказывается, всё дело в суперклассе:

В JVM есть известный костыль под названием sun.reflect.MagicAccessorImpl . Всякий его наследник обладает неограниченным доступом к любым приватным данным любых классов. Это именно то, что нужно! Раз класс магический, то он поможет получить инстанс java.lang.Class . Проверяем:

и опять получаем исключение:

Вот это уже действительно интересно. Судя по всему, обещанной магии не произошло. Или я ошибаюсь?

Стоит рассмотреть ошибку внимательнее и сравнить её с тем, как должен себя вести метод newInstance . Будь проблема в строке clazz.(classLoader) , мы бы получили InvocationTargetException . На деле же имеем IllegalAccessError , то есть до вызова конструктора дело не дошло. С ошибкой отработала инструкция NEW , не позволив выделить память под объект java.lang.Class . Здесь наши полномочия всё, окончены.

Уровень 5. Современный

Reflection не помог решить проблему. Может быть, дело в том, что Reflection старый и слабый, и вместо него стоит использовать молодой и сильный MethodHandles? Думаю, да. Как минимум, стоит попробовать.

И как только я решил, что Reflection не нужен, он тут же пригодился. MethodHandles — это, конечно, хорошо, но с помощью него принято получать лишь те данные, к которым есть доступ. А если понадобился приватный конструктор, то придётся выкручиваться по старинке.

Итак, нам нужен MethodHandles.Lookup с приватным доступом к классу java.lang.Class . На этот случай есть очень подходящий конструктор:

Получив lookup , можно получить объект MethodHandle , соответствующий требуемому нам конструктору:

После запуска этого метода я был откровенно удивлён — lookup делает вид, что конструктора вообще не существует, хотя он точно присутствует в классе!

Странно то, что причина исключения — NoSuchFieldError . Загадочно.

В этот раз ошибся именно я, но далеко не сразу это понял. Спецификация findConstructor требует, чтобы тип возвращаемого значения был void , несмотря на то, что у результата MethodType будет ровно таким, как я описал (всё потому, что метод , отвечающий за конструктор, действительно возвращает void по историческим причинам).
Так или иначе, путаницы можно избежать, ведь у lookup есть второй метод для получения конструктора, и он называется unreflectConstructor :

Данный метод уж точно корректно отработает и вернёт тот handle, который должен.

Момент истины. Запускаем метод инстанцирования:

Думаю, вы уже догадались, что ничего хорошего не произойдёт, но давайте хоть глянем на ошибку. Сейчас это что-то новенькое:

По умолчанию stacktrace отображается укороченным, поэтому я добавил
-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames в параметры запуска. Так становится проще понять, в какое странное место мы попали.

Не буду углубляться в то, какие классы генерирует MethodHandles , да это и не принципиально. Важно совсем другое — мы наконец-то докопались до использования sun.misc.Unsafe , и даже он не в силах создать объект java.lang.Class .

Метод allocaeInstance используется в тех местах, где нужно создать объект, но не вызывать у него конструктор. Такое бывает полезно, например, при десериализации объектов. По сути, это та же инструкция NEW , но не обременённая проверками прав доступа. Почти не обременённая, как мы только что увидели.

Раз даже Unsafe не смог, мне остаётся лишь прийти к печальному заключению: аллоцировать новый объект java.lang.Class невозможно. Интересно выходит — думал, что запрещён конструктор, а запрещена аллокация! Попробуем это дело обойти.

Уровень 6. Небезопасный

Предлагаю создать пустой объект и взглянуть, из чего же он состоит. Для этого возьмём Unsafe и аллоцируем новенький java.lang.Object :

На текущей JVM результатом будет область памяти в 12 байт, выглядящая вот так:


То, что вы здесь видите, это "заголовок объекта". По большому счёту, он состоит из двух частей — 8 байт markword, которые нас не интересуют, и 4 байта classword, которые важны.

Каким образом JVM узнаёт класс объекта? Она делает это путём чтения области classword, которая хранит указатель на внутреннюю структуру JVM, описывающую класс. Значит если в данное место записать другое значение, то и класс объекта изменится!

Дальнейший код очень, очень плохой, никогда так не делайте:

Мы прочитали classword объекта Object.class и записали его в classword объекта object . Результат работы следующий:

С натяжкой можно считать, что java.lang.Class мы аллоцировали. Мы молодцы! Теперь надо вызвать конструктор. Вы можете смеяться, но сейчас мы будем с помощью ASM генерировать класс, умеющий вызывать нужный конструктор. Естественно, при этом нужно унаследоваться от MagicAccessorImpl .

Так начинается создание класса (константы импортированы статически, так короче):

Так ему создаётся конструктор:

А так создаётся метод void construct(Class, ClassLoader) , который внутри себя вызывает конструктор у объекта Class :

Класс готов. Осталось загрузить, инстанцировать и вызвать нужный метод:

И это работает! Точнее так: повезло, что работает. Можно проверить, запустив следующий код:

Вывод будет таким:

О том, в какую область памяти записался этот ClassLoader и откуда потом прочитался, я тактично умолчу. И, как ожидалось, вызов практически любого другого метода на данном объекте приводит к немедленному краху JVM. А в остальном — цель выполнена!

Что там в Java 9?

В Java 9 всё почти так же. Можно проделать все те же действия, но с несколькими оговорками:

  • в параметры компилятора надо добавить --add-exports java.base/jdk.internal.reflect=sample (где sample — это имя вашего модуля);
  • в параметры запуска надо добавить:
    --add-opens java.base/jdk.internal.reflect=sample
    --add-opens java.base/java.lang=sample
    --add-opens java.base/java.lang.reflect=sample
    --add-opens java.base/java.lang.invoke=sample
    --add-opens java.base/jdk.internal.reflect=java.base
  • в зависимости модуля надо добавить requires jdk.unsupported ;
  • у конструктора java.lang.Class поменялась сигнатура, надо учесть.

Так же стоит учесть, что sun.reflect перенесли в jdk.internal.reflect и что класс MyConstructorInvocator теперь надо грузить тем же загрузчиком, что у MagicAccessorImpl .
ClassLoader.getSystemClassLoader() уже не сработает, у него не будет доступа.

Ещё исправили странную багу с NoSuchFieldError : теперь на его месте NoSuchMethodError , который там и должен быть. Мелочь, но приятно.

В целом, в Java 9 нужно намного сильнее постараться, чтобы выстрелить себе в ногу, даже если именно это и является главной целью. Думаю, это и к лучшему.

Выводы:

  • при желании в Java можно творить абсолютно безумные вещи, это забавно;
  • Reflection API не так уж и сложно устроен;
  • MagicAccessorImpl может не всё;
  • sun.misc.Unsafe может не всё, но почти;
  • Java 9 ещё сильнее старается вас обезопасить.

Не стоит слишком серьёзно воспринимать всё описанное. Сама по себе задача инстанцирования java.lang.Class совершенно бессмысленна. Здесь важны знания, полученные в процессе её решения.

Читайте также: