Объекты 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.StringgetClass() возвращает 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() |
|---|---|---|---|
String | java.lang.String | String | java.lang.String |
int[] | [I | int[] | int[] |
String[] | [Ljava.lang.String; | String[] | java.lang.String[] |
вложенный Map.Entry | java.util.Map$Entry | Entry | java.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 Classvoid даже имеет void.class (и Void.TYPE). Классы массивов синтезируются JVM: int[].class, String[][].class. arrayClass.getComponentType() снимает одно измерение (String[].class.getComponentType() — это String.class). Эти различия важны при сопоставлении типов параметров в getMethod — getMethod("foo", int.class) и getMethod("foo", Integer.class) находят разные перегрузки.
Идентичность класса и загрузчики классов
Идентичность объекта Class определяется не только его именем — это пара (имя, определяющий загрузчик классов). Один и тот же файл .class, загруженный двумя разными загрузчиками классов, порождает два отдельных, несовместимых объекта Class. Приведение типов между ними выбрасывает ClassCastException, даже если имена совпадают. В обычном приложении (с одним загрузчиком) это практически незаметно, но является корнем многих головоломок «но это же тот же класс!» в серверах приложений, OSGi и системах горячей перезагрузки. Для повседневной рефлексии рассматривайте объекты Class как синглтоны для каждого типа и сравнивайте их с помощью ==.
Практический пример: исследование типов тремя способами
Программа получает объекты Class тремя способами, затем исследует несколько типов — обычный класс, интерфейс, массив, примитив и вложенный тип — чтобы выявить различия в именовании и структуре.
Что следует вынести из запуска:
- Все три пути сошлись к одному и тому же виду объекта: литерал
.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.