W3docs

Объекты Class в Java

Получение объектов Class<T> в Java с помощью Object.getClass(), литералов .class и Class.forName.

Всё в рефлексии начинается с объекта Class. Для каждого типа, загружаемого JVM, — каждого класса, интерфейса, типа массива, перечисления, аннотации и даже каждого примитива — существует ровно один экземпляр Class, описывающий его. Этот объект является вашим дескриптором структуры типа: его имени, суперкласса, членов, аннотаций. В этой главе рассматриваются три способа получить Class, что содержит Class<T>, и небольшие сюрпризы, которые часто ставят людей в тупик.

Три способа получить Class

Существует ровно три пути, каждый из которых подходит для разных ситуаций.

1. Литерал .class — тип известен во время компиляции.

Class<String> c1 = String.class;
Class<int[]> c2 = int[].class;
Class<Integer> c3 = int.class == Integer.class ? null : int.class;   // see "primitives" below

Это безопасно во время компиляции и самый быстрый способ — поиска нет, компилятор встраивает ссылку напрямую. Используйте его всегда, когда можете назвать тип.

2. Object.getClass() — у вас есть экземпляр.

Object o = "hello";
Class<?> c = o.getClass();        // class java.lang.String

getClass() возвращает runtime-класс объекта, который может быть подклассом объявленного типа переменной. Object o = new ArrayList<>() даст o.getClass() == ArrayList.class, а не Object.class. Статический тип — Class<?>, потому что компилятор знает только, что o является каким-то Object.

3. Class.forName(String) — у вас есть только имя.

Class<?> c = Class.forName("java.util.ArrayList");

Это динамический путь: полностью квалифицированное имя класса в виде строки, разрешаемое во время выполнения. Выбрасывает ClassNotFoundException, если такой класс недоступен для загрузки. Именно это используют загрузчики плагинов и драйверы JDBC. Существует вариант «загружен, но не инициализирован»: Class.forName(name, false, classLoader) пропускает статические инициализаторы до первого активного использования класса.

Что содержит Class<T>

Объект Class — это богатое описание. Ключевые методы:

Class<?> c = ArrayList.class;

c.getName();              // "java.util.ArrayList"        (binary name)
c.getSimpleName();        // "ArrayList"
c.getCanonicalName();     // "java.util.ArrayList"        (source-like)
c.getPackageName();       // "java.util"
c.getSuperclass();        // class java.util.AbstractList
c.getInterfaces();        // [List, RandomAccess, Cloneable, Serializable]
c.getModifiers();         // int bitset → Modifier.isPublic(...) etc.
c.isInterface();          // false
c.isEnum(); c.isArray(); c.isPrimitive(); c.isAnnotation();

Из Class можно получить доступ к каждому члену: getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors(), а также их публичные/унаследованные аналоги get… (разделение из вводной главы). Последующие главы подробно рассматривают каждый из них.

Бинарное имя, простое имя и каноническое имя

Методы именования различаются способами, которые приводят к ошибкам при логировании или сравнении:

ТипgetName()getSimpleName()getCanonicalName()
Stringjava.lang.StringStringjava.lang.String
int[][Iint[]int[]
String[][Ljava.lang.String;String[]java.lang.String[]
вложенный Map.Entryjava.util.Map$EntryEntryjava.util.Map.Entry
анонимный классOuter$1"" (пустая строка)null

getName() — это бинарное имя: внутренняя форма JVM с $ для вложенности и загадочной кодировкой массивов [I / [L…;. Именно это ожидает Class.forName. getCanonicalName() — это форма для исходного кода, которую вы бы написали, и она равна null для типов, которые нельзя назвать в исходном коде (локальные, анонимные классы). Используйте getName() для циклических вызовов forName; используйте getSimpleName()/getCanonicalName() для вывода пользователю.

Примитивы и массивы тоже имеют объекты Class

Каждый примитив имеет свой собственный Class, отличный от его обёртки:

int.class == Integer.class      // false — two different Class objects
int.class.getName()             // "int"
Integer.TYPE == int.class       // true — TYPE is the primitive Class

void даже имеет void.classVoid.TYPE). Классы массивов синтезируются JVM: int[].class, String[][].class. arrayClass.getComponentType() снимает одно измерение (String[].class.getComponentType() — это String.class). Эти различия важны при сопоставлении типов параметров в getMethodgetMethod("foo", int.class) и getMethod("foo", Integer.class) находят разные перегрузки.

Идентичность класса и загрузчики классов

Идентичность объекта Class определяется не только его именем — это пара (имя, определяющий загрузчик классов). Один и тот же файл .class, загруженный двумя разными загрузчиками классов, порождает два отдельных, несовместимых объекта Class. Приведение типов между ними выбрасывает ClassCastException, даже если имена совпадают. В обычном приложении (с одним загрузчиком) это практически незаметно, но является корнем многих головоломок «но это же тот же класс!» в серверах приложений, OSGi и системах горячей перезагрузки. Для повседневной рефлексии рассматривайте объекты Class как синглтоны для каждого типа и сравнивайте их с помощью ==.

Практический пример: исследование типов тремя способами

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

java— editable, runs on the server

Что следует вынести из запуска:

  • Все три пути сошлись к одному и тому же виду объекта: литерал .class, вызов getClass() и поиск через forName — каждый из них дал полностью рабочий Class. Выбор пути зависит от того, что вам известно (тип, экземпляр или только имя) — результат одинаков по возможностям.
  • getClass() на переменной Greeter g вернул Robot, а не Greeter. Объявленный тип не имеет значения; getClass() всегда сообщает конкретный runtime-класс. Вот почему полиморфная диспетчеризация и рефлексивная интроспекция видят один и тот же «реальный» тип.
  • Три метода именования разошлись именно там, где предсказывает таблица: String[] вывел бинарное имя [Ljava.lang.String; из getName(), но читаемое String[] из простой и канонической форм. Если вам когда-либо нужно передать имя обратно в forName, это должна быть форма getName().
  • int.class == Integer.class было false, тогда как Integer.TYPE == int.class было true. Примитив и его обёртка — это разные объекты Class, а Integer.TYPE — просто псевдоним примитивного. Путаница с ними является классической причиной NoSuchMethodException при поиске перегрузки по типу параметра.
  • Robot.class == new Robot().getClass() было true: в рамках одного загрузчика классов тип отображается ровно на один объект Class, поэтому == является правильным способом сравнения. В коде с одним загрузчиком вам никогда не нужен .equals() для объектов Class.

Распространённые ошибки

  • forName запускает статические инициализаторы (в однопараметрической форме). Загрузка класса может иметь побочные эффекты. Используйте трёхпараметрическую форму с initialize=false, если вы хотите только проверить класс.
  • getSimpleName() может быть пустой (анонимные классы), а getCanonicalName() может быть null (локальные, анонимные). Не предполагайте, что они всегда являются пригодными для печати идентификаторами.
  • Обобщения стираются. List<String>.class недопустимо; есть только List.class. Объект Class не несёт информации о типовых аргументах — она находится в Type/ParameterizedType, отдельном (и более продвинутом) API рефлексии. Смотрите ограничения обобщений для понимания того, как работает стирание.

Имея объект Class в руках, следующая глава открывает первый ящик с членами: поля — их интроспекция, чтение и запись, даже если они приватные или final.

Практика

Практика
У вас есть 'Object o = new java.util.LinkedList<String>();', объявленный как 'Object'. Вы вызываете 'o.getClass().getName()'. Какая строка возвращается, и почему это не 'java.lang.Object'?
У вас есть 'Object o = new java.util.LinkedList<String>();', объявленный как 'Object'. Вы вызываете 'o.getClass().getName()'. Какая строка возвращается, и почему это не 'java.lang.Object'?
Was this page helpful?