Вход
Регистрация
НОВОЕ
ОБЩЕСТВО
Настройки Настройки Ваши группы Ваши группы Ваш блог Ваш блог Обмен Обмен Выход
Группы Группы Блоги Блоги Форум Почта
Как глубоко вы можете изучать Java код с помощью Java reflection API
Предлагаем нашим читателям факультативную задачку на расширенное знание Java Core. Если вы выполните предложенное задание, то на любом собеседовании по Java вам будет достаточно легко доказать знание Java Core, потому что уровень погружения в предмет, требуемый в этой задаче, существенно выше среднего уровня, с которым обычно работают даже весьма опытные разработчики. Правда вам придётся кратко ввести интервьюирующего в суть проблемы, и, возможно, даже показать ему сложность задачи, поскольку далеко не все разработчики погружались в подобные глубины и им задача может показаться простой, но это только на первый взгляд. А когда вы удивите интервьюирующего знанием того, чего он сам может не знать, по Java Core вам наверняка поставят зачёт.

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

Ну а если вы учитесь на нашем курсе Java, то, помимо задела для собеседований, вам будут зачтены несколько домашних заданий по Java Core. Но стоит учитывать, что в данном случае вы углубляетесь в одном направлении, а всего таких направлений в разработке - многие десятки, и потому для всего остального вам не стоит ожидать зачёта домашних заданий, ведь каждое направление имеет свои глубины, и их знание нужно ещё доказать.

Сначала немного неформальных определений для Reflection API и Generics

В языке Java, как и в ряде других языков программирования, находят применение средства получения информации о интересующем нас компоненте. В англоязычной среде подобные средства принято называть Reflection API. Пользуясь таким инструментом вы можете получить описание класса, поля, метода или его аргумента. Но нередко встречается ситуация, когда конкретный тип, например поля, заранее неизвестен и заменён языковой конструкцией, предполагающей уточнение типа во время объявления в рамках использующего класса. Конструкции, позволяющие задавать такие уточнения, обычно называют Generics.

Теперь представим себе ситуацию, в которой нам необходимо написать программу, генерирующую исходный код класса, в котором присутствуют уточняемые типы. На первый взгляд всё кажется простым - берём Reflection API и читаем информацию по Generics, ну и генерируем исходный код. Но при более близком рассмотрении всплывают специфические детали, превращающие "дело на полчаса" в тягомотину с вылавливанием багов в течении года. Почему так может случиться? Потому что информация об уточняемых типах имеет высокий уровень сложности, легко запутывающий любого, кто смело ринулся в бой с мыслями о "деле на полчаса". Далее покажем эту сложность и как с ней можно поладить.

Для примера возьмём очень простую задачу: определить, каким типом уточнена декларация java.util.List<T>. Даже такое объявление, не смотря на его внешнюю простоту, может вызвать множество вопросов, плавно переходящих в необходимость обрабатывать очень большое количество вариантов, из которых при подходе "дел на полчаса" выбирают лишь один или два самых простых. При таком выборе можно надеяться на то, что все остальные никогда не встретятся и довольствоваться определением ситуаций типа List<String>. Но если речь идёт не об одноразово используемом компоненте, то очень велика вероятность того, что кто-то в конечном итоге обязательно применит ваш код с урезанными возможностями в непредвиденной вами ситуации. И вы получите очередной баг. Затем исправите свой код, добавив в него выявленный дополнительный вариант. А потом снова кто-то "захочет странного" и вы снова будете править баг. А потом ещё и ещё и много раз ещё. Потому что вариантов действительно может быть очень много, а в качестве примера посмотрите хотя бы на такой - List<Map<Integer,List<Set<? extends Number>>>.

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

Почему вариантов много?

Если говорить коротко, то проблема лежит в области разнообразия средств, предоставляемых языком Java. Перечислим далее только список элементов, в которых возможно применение конструкций с отложенным уточнением типа. В результате у нас получится иерархия, вершиной которой будет элемент языка под названием "класс". В рамках класса появляются следующие элементы: его предок, реализуемые интерфейсы, внутренние классы, поля, конструкторы, методы. Отдельно от класса могут быть уточнены методы и конструкторы. У методов или конструкторов можно получить информацию об аргументах и исключениях. Специфическим для методов, отличающим их от конструкторов, является возвращаемый ими тип результата. Из перечисленного видно, что перед нами уже появился далеко не самый скромный список вариантов. Но всё становится ещё хуже, когда мы вспомним о том, что в самих уточнениях допустимо рекурсивно уточнять значения типов, без ограничений на глубину рекурсии. Добавим к этому уточнения в виде неопределённых типов с ограничениями (wildcards). А ограничения у неопределённых типов опять могут рекурсивно включать уточнения, которые могут определяться ранее заданными уточняемыми типами, или же зависеть от них.

Небольшой пример псевдо-определения, включающего некоторые указанные выше элементы:

class Generic<D, T extends/super SomeClass<D, Z extends Map<X extends Number, D>>> extends SuperGeneric<? extends Z>

Но это ещё не всё, ведь мы только объявили места, для которых в дальнейшем можно уточнить тип. Теперь посмотрим на то, как может выглядеть уточнение. Мы можем уточнять тип, объявленный в любом из выше перечисленных мест, а сами места очень сильно отличаются по заложенным в них языком возможностям. Это означает, что в каждом случае уточнения типа мы столкнёмся с неодинаковыми вариантами, каждый из которых нам нужно учесть. Таким образом, к допустимой во множестве мест рекурсии с зависимостями мы добавляем ещё и специфические правила уточнения, умножающие наши печали с рекурсией на количество вариантов уточнения.

Здесь у читателя может возникнуть вопрос - а как разработчики компилятора справляются с такой большой сложностью? Но на самом деле разработчикам компилятора нужно обрабатывать даже меньше вариантов, чем может потребоваться при рефлексивном изучении класса. Основная сложность заключается в разборе рекурсивного определения уточняемых типов, с последующей проверкой на внутренние зависимости в рамках одного определения, а так же на совместимость уточнений с ограничениями уточняемого типа. Это непростая задача, но будучи решённой один раз, она полностью снимает с разработчиков компилятора проблему сложности при уточнении типов, ведь один и тот же алгоритм закрывает все потребности, как при объявлении уточняемых типов, так и при указании уточнений, поскольку в обоих случаях имеем одну и ту же задачу проверки совместимости и внутренних зависимостей.

Рефлексивное изучение класса потребует от нас все те же алгоритмы, что и в случае с компилятором, но добавит к их сложности ещё и необходимость работы с информацией в том формате, который нам предоставляет Reflection API. Этот формат ориентирован на достаточно узкую задачу - как-то отдать информацию. То есть она подаётся не в удобном для вашей конкретной задачи форме, а только так, как выбрали разработчики Reflection API, исходя из задачи уложить всё что нужно в существующую модель элементов языка, таких как класс, поле, метод и т.д. Значит, от вас теперь потребуется не только разобраться с Reflection API, но и самостоятельно придумать, как эту информацию применить, например, для проверки совместимости уточняемых типов в генерируемом вами классе. А если вы не проверите совместимость, то её проверит алгоритм компилятора и, очевидно, в большинстве случаев выдаст ошибку. Вот и получается - вы должны сделать работу компилятора, плюс, либо произвести нетривиальное преобразование информации от Reflection API, либо, не менее нетривиально, суметь её использовать без преобразований.

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

Что нужно сделать?

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

1. Собрать необходимую информацию о классе или его элементах средствами Java.
2. Произвести выбор уточнения, исходя из внешних по отношению к нашей задаче ограничений.
3. Проверить выбранное уточнение на совместимость с ограничениями в объявлении уточняемого типа, с учётом всех зависимостей по входящим в него дополнительным уточняемым типам (аналог задачи компилятора).

Собираем необходимую информацию, или какие средства предоставляет нам Reflection API

Рассмотрим список возможностей, которые реализованы у базовых рефлективных объектов типа: Class, Executable, Field, Method, Constructor. Добавим к ним возможности специализированных интерфейсов: GenericDeclaration, GenericArrayType, ParameterizedType, TypeVariable, WildcardType.

Как вы видите, одних только контейнеров для требующегося функционала насчитывается 9 штук (без Executable, являющегося предком Method и Constructor). И в каждом мы найдём от одного до 4-х методов. Суммарно всё это выглядит вот так (все типы, кроме Class, находятся в пакете java.lang.reflect):

java.lang.Class implements Type:
    Type getGenericSuperclass(); // обычно возвращает ParameterizedType с информацией о уточнении типов предка из текущего класса
    Type[] getGenericInterfaces(); // аналогично getGenericSuperclass, но для интерфейсов
Field:
    Type getGenericType() // возвращает информацию о уточнениях для данного поля
Method:
    Type getGenericReturnType() // возвращает информацию о уточнениях в возвращаемом типе для данного метода
Executable (предок Method и Constructor):
    Type[] getGenericParameterTypes() // возвращает информацию о уточнениях в параметрах для данного метода
    Type[] getGenericExceptionTypes() // возвращает информацию о уточнениях в исключениях для данного метода
    TypeVariable<?>[] getTypeParameters() // информация о уточняемых типах, определённых в рамках данного метода
GenericDeclaration:
    TypeVariable<?>[] getTypeParameters() // информация о уточняемых типах, определённых в рамках реализации данного интерфейса
GenericArrayType extends Type:
    Type getGenericComponentType() // информация о уточняемом типе элементов массива
ParameterizedType extends Type:
    Type[] getActualTypeArguments() // информация о уточнениях в рамках реализации данного интерфейса
    Type getOwnerType() // информация о типе, внутри которого получена реализация данного интерфейса
    Type getRawType() // информация о типе без уточнений
TypeVariable<D extends GenericDeclaration> extends Type:
    AnnotatedType[] getAnnotatedBounds()
    Type[] getBounds() // информация об ограничениях по предку или наследнику, включая интерфейсы
    D getGenericDeclaration() // информация об элементе, где объявляется данная переменная
    String getName() // название переменной
WildcardType extends Type:
    Type[] getLowerBounds() // информация об ограничении по наследнику
    Type[] getUpperBounds() // информация об ограничении по предку

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

Вторая проблема, касающаяся рефлективного определения уточняемых типов во время исполнения, относится к цели создания показанных выше элементов API. Цель данного фрагмента API состоит в "отображении слона по частям", то есть каждый тип отражает исключительно свою часть, а целостную картину не отражает ни один из них. Но нам для решения задачи требуется именно целостная картина, получаемая для любого элемента языка каким-то единообразным способом (то есть с минимумом используемых элементов).

Что конкретно мы хотим получить

Определим сначала простой и минималистичный набор элементов предполагаемого API, который решал бы нашу задачу. Уже на данном этапе мы будем вынуждены снова вернуться к языку Java, и в частности, к его спецификации, задающей ограничения на ожидаемый нами результат.

Язык определяет две составляющие уточняемых типов:

1. Декларация элемента языка, включающего возможность уточнения.
2. Собственно уточнение типов для заданного элемента в рамках языковой конструкции, определяющей контекст использования элемента.

Пример декларации выглядит так:

public class LearnGenerics<T, M extends Number & Serializable, Z extends T>

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

public T someField;
public List<? super Z> someInterestingField;
public LearnGenerics<Map<T,List<M>>, ? super Double, ExtendedMap<Z>> evenMoreInterestingField;


Приведём повторно набор вариантов в более общем виде:

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

Декларация класса LearnGenerics показывает нам, как объявляются уточняемые типы. Первые два его поля показывают пример способа использования уточняемых типов. А третье поле показывает пример частичного уточнения типов. Но здесь использованы лишь декларация класса и его полей, тогда как в предыдущем абзаце вы видели полный набор элементов, где допустимы такие конструкции. Очевидно, что с учётом полного набора количество возможных примеров выросло бы в несколько раз, но, к счастью, в остальных случаях принципиально ничего не меняется, а потому приведённого должно хватить для общего понимания проблемы.

Снова уточним нашу задачу

В случае генерации класса, например, наследующего от LearnGenerics, нам придётся решить, какие типы подставить (чем уточнить) вместо использованных в определении полей уточняемых типов. Для этого было бы неплохо иметь структуру, полностью описывающую все приведённые выше поля. В общем же случае эта структура должна описывать все элементы класса, где возможны уточнения. Имея такую структуру, мы могли бы последовательно извлекать из неё ограничения на уточнения в виде конкретных классов и интерфейсов, от которых должны наследовать наши подставленные типы, или, в случае использования ключевого слова super, предками которых они должны являться. При этом было бы желательно, что бы структура предоставляла не переменные, вроде T, M, D и т.д, а подставленные нами ранее уточнения, вычисленные для каждого конкретного места использования уточняемых типов.

То есть мы хотим получить структуру данных, отражающую всё показанное выше многообразие кратко и только для выбранной нами части класса, как, например, поля, метода, конструктора. Помимо указанных частей уточнению подлежат предок класса и вместе с ним его предки и все объявленные в них поля, методы, конструкторы с уточняемыми типами. Так же уточняются реализуемые классом интерфейсы. Эти составляющие модели языка представляют из себя часть исследуемого класса, а потому и сам класс, и все его внутренности должны прозрачным образом описываться предполагаемой структурой данных, тем самым полностью закрывая отражение активных элементов исследуемого класса. Под "активными элементами" будем понимать те, с которыми можно выполнять действия типа "взять значение", "изменить", "вызывать". Именно такие элементы обычно требуется описывать при генерации исходного кода и обычно это поля, методы, конструкторы.

Исходя из сказанного, мы можем ожидать от нашего API наличия всего трёх методов: getFieldGenericType, getReturnValueGenericType, getParameterGenericType. Эти методы соответствуют допустимым в Java составляющим интересующих нас элементов, а именно - полей, методов, конструкторов. Информация о классе нам важна в контексте возможности получить всё те же поля, методы, конструкторы, а так же все их ограничения.

Предложим для ожидаемой структуры данных название GenericType, то есть тип, объявляющий что-то уточняемое, или же указывающий уточнение. Первым элементом ожидаемой структуры определим поле type типа Class<?>, оно расскажет нам, какого типа интересующий нас элемент. Для исследования элементов, представляющих из себя массивы, добавим в нашу структуру поле arrayDimension типа int. Оно позволяет получить размерность массива с типом содержимого, объявленным в поле type. Если его значение равно нулю, то мы имеем дело не с массивом, а с классом, который содержится в поле type.

Для генерации простых классов указанной информации достаточно, но, поскольку в структурах данных очень часто используются внутренние уточняемые типы (простейший пример - List<T>), попробуем расширить нашу структуру.

В конструкциях вроде Map<T,List<M>> возможно несколько внутренних мест для уточнения. Используем для отображения такого случая в нашей структуре поле generics с типом данных List. Далее поймём, объекты какого типа могут содержаться в таком списке. Для этого заметим, что эти объекты будут обозначать либо переменную, подлежащую уточнению, либо элемент типа Object для случаев вроде "<?>", либо конкретный класс. Указанные элементы могут иметь ограничения в виде переменных или классов (Z extends T или Z extends Number). Этим требованиям удовлетворяет класс GenericType, если каждую неуточнённую переменную мы заменим на конкретный класс, выведенный из накладываемых на переменную ограничений. Аналогично должен быть заменён неопределённый тип (wildcard).

Итак, полный набор полей структуры GenericType может быть представлен следующим образом:

Class<?> type; // интересующий нас класс
int arrayDimension; // размерность массива с данными, являющимися экземплярами класса type, или 0, когда массива нет
List<GenericType> generics; // список уточняемых параметров декларации класса

Имея в нашем распоряжении указанную выше структуру и заполнив её корректными данными, нам останется лишь рекурсивно погружаться внутрь списка в поле generics и "один-в-один" записывать в генерируемый класс названия найденных в структуре типов.

Но сначала кто-то должен корректно заполнить предложенную структуру. Это означает, что необходимо отобразить информацию, доступную при помощи Reflection API, на нашу структуру, с учётом выбора уточняющих типов в каждом случае, а так же проверки их на корректность, то есть укладываются ли они в ограничения на предков и наследников.

Такое отображение лучше разбить на части, что бы не городить в одном месте решения сразу для нескольких задач и в результате быстро запутаться. Определим эти задачи. Сначала нам нужно собрать информацию, доступную через Reflection API. Это одна задача. Далее нам нужно для каждого уточняемого типа определить его ограничения. Это вторая задача. Затем нам нужно выбрать конкретные уточнения для каждого уточняемого типа, с учётом определённых ранее ограничений. Это третья задача.

Поскольку третья задача зависит от неизвестных нам факторов - для чего мы генерируем код, какие ограничения действуют при генерации, какие составляющие нужно генерировать, и так далее - оставим её разбор "на вкусное" заинтересовавшемуся читателю, самостоятельно выбирающему интересные именно для него случаи с соответствующими им ограничениями.

Сбор информации через Reflection API

Само Reflection API мы видели выше, так же мы перечислили возможные типы информации, которую можно собрать. Теперь нужно объединить Reflection API и некую структуру, куда мы будем складывать найденное. Эта структура во многом соответствует задаче, которую мы решали при создании GenericType, поэтому за основу можно взять именно её. Но, в отличии от GenericType, новая структура должна содержать информацию о переменных, вместо которых подставляют уточнения, и о неопределённых типах (wildcard). Кроме того, в некоторых случаях возможно наличие вариантов ограничений на тип - по предкам и по наследникам, что тоже нужно учесть. Помимо ограничения в виде класса-предка, ограничения на предков могут задаваться в виде интерфейсов, и эту информацию нам так же нужно где-то хранить.

Назовём новую структуру GenericDeclaration. Эта структура может содержать информацию о классе-уточнении, либо о переменной, вместо которой подставляется уточнение, либо о неопределённом типе (wildcard). Для двух последних случаев могут присутствовать ограничения по предку, а для второго - по наследнику. Если унаследовать GenericDeclaration от GenericType, то мы получим поле type и возможность отображения структур с массивами. Но в нём нет информации об интерфейсах, поэтому её добавим в класс GenericDeclaration в виде поля interfaces типа List<GenericType>.

Заметим, что в случае с переменными и неопределёнными типами нам тоже понадобится класс, определяющий ограничения по предку или наследнику, значит, поле type будет полезно во всех случаях, но в разных ролях. Сделаем ещё два класса-наследника - один для переменных (назовём GenericVariable), и другой для неопределённых типов (назовём GenericWildcard). В первом добавим поле variableName типа String и поле логического типа для указания на то, является ли поле type из GenericType ограничением по наследнику или предку, назовём это поле hasSuccessorConstraint, тогда значение false будет соответствовать ограничению по предку, а значение true будет соответствовать ограничению по наследнику. Заметим, что в большинстве случаев встречаются только ограничения по предку, поэтому значение по умолчанию (false) для поля hasSuccessorConstraint в большинстве случаев будет соответствовать реальному положению дел и его не нужно будет менять. В случае же с GenericWildcard нам не нужна дополнительная информация, кроме факта работы с неопределённым типом, поэтому класс GenericWildcard будте пустым, но своим типом подскажет нам, что это информация о неопределённом типе. Так же заметим, что поле type может содержать значение null, что будет означать отсутствие ограничений по предку или наследнику.

Итак, полный набор полей структуры GenericType и её наследников может быть представлен следующим образом:

GenericType
        Class<?> type; // интересующий нас класс, он же ограничитель на предков/наследников
        int arrayDimension; // размерность массива с данными, являющимися экземплярами нашего класса, или 0, когда нет массива
        List<GenericType> generics; // список уточняемых параметров декларации класса
    GenericDeclaration
            List<GenericType> interfaces; // реализуемые классом интерфейсы, возможно с уточняемыми типами
        GenericVariable
                String variableName;
                boolean hasSuccessorConstraint;
        GenericWildcard

Теперь мы имеем на входе класс, на основе которого будут выполняться интересующие нас действия, например, генерация наследника с методами, принимающими корректные типы данных, соответствующие полям класса-предка. Далее мы должны использовать Reflection API для сбора информации о уточняемых типах. Сбор необходимо производить как в самом целевом классе, так и в его предках, поскольку нас могут интересовать поля (или другая информация) и оттуда. На выходе мы можем получить список из структур типа GenericType, возможно включающий наследников GenericType. Первый элемент такого списка будет соответствовать целевому классу, второй - его предку, третий - предку предка, и так далее до класса Object, который в список включать не нужно из-за отсутствия полезной информации в таком случае.

Далее - самое простое - нужно решить поставленную задачу. Точнее, пока перед вами открылась картина задачи "Отобразить информацию от Reflection API на структуры-наследники GenericType для произвольного класса". Далее поговорим о второй составляющей.

Уточнение ограничений

Переменная, вместо которой подставляется уточнённый тип, может быть объявлена в дальнем предке интересующего нас класса, а во всех наследниках она может переопределяться новыми переменными, с какими-то дополнительными ограничениями, поэтому тип, например, поля, определяемый переменной из дальнего предка, можно узнать только пройдя по всей цепочке от дальнего предка к его последнему наследнику, что бы по пути подставлять все переопределяющие переменные и их ограничения. Далее показан пример декларации такой цепочки наследников, где ограничения на тип поля someField можно узнать лишь пройдя до класса A, а конкретный тип можно выяснить лишь имея декларацию поля или метода с классом A например - public A<Double> doubleField.

class A<T extends Number> extends B<T> {}
class B<T super Double> extends C<T> {}
class C<T>
{ T someField; }

Помимо транзитивной зависимости от объявлений в классах-наследниках и в месте использования уточняемого типа, из примера видно, что возможны одновременно действующие ограничения и по наследнику и по предку. Исходя из этого в рассмотренную ранее иерархию с корнем в GenericType, необходимо добавить соответствующую информацию. В частности, в класс GenericVariable нужно добавить поле substitution типа GenericType. Оно будет обозначать замену, произведённую в наследнике или в месте использования класса с уточняемыми типами. В GenericVariable так же нужно добавить поле successorLimit типа GenericType, обозначающее ограничение по наследникам, которое делает ненужным поле hasSuccessorConstraint. Из-за заметного отличия класса GenericVariable для двух случаев - для сбора информации и для уточнения ограничений, можно создать его наследника (назовём его GenericRefinement), специализированного именно на уточнении ограничений, а в GenericVariable скрыть поле hasSuccessorConstraint, заменив его методами hasSuccessorConstraint() и setSuccessorConstraint(boolean value). Тогда в наследнике можно переопределить оба метода так, что бы они отражали новую ситуацию, когда наличие ограничения по наследнику определяется наличием значения в поле successorLimit (оно не равно null). Метод setSuccessorConstraint(boolean value) переопределяется так, что бы при его использовании возникала исключительная ситуация, препятствующая неправильному использованию класса-наследника.

В результате имеем класс:

GenericRefinement extends GenericVariable
    GenericType substitution;
    GenericType successorLimit;

Этот класс нужно наполнить информацией. Для этого полученный на предыдущем этапе список элементов типа GenericType нужно обработать ещё раз, проходя от первого элемента (последнего наследника) до последнего (первого предка) заменяя все экземпляры GenericVariable на соответствующие им экземпляры GenericRefinement, заполняя их корректной информацией.

Имея в руках список после обработки с заменой на GenericRefinement, уже не так сложно проследить все цепочки объявлений переменных и дойти по ним до тех предков, в которых объявлена начальная переменная. Дойдя до объявления начальной переменной вы сможете корректно заполнить поля successorLimit и type наследником и предком, ограничивающими данный уточняемый тип. Кроме того, поле substitution поможет уточнить тип ещё и конкретными классами или ограничениями из неопределённых типов в наследниках. После этого для каждого поля, метода, конструктора, независимо от декларирующего его класса, будет доступна информация, лишённая неопределённости в виде переопределяемых переменных. Если тип уточнён классом - вам более ничего не нужно делать. Если уточнение - неопределённый тип, то далее у вас есть полная информация о его ограничениях, из которых можно вывести, какой тип есть смысл подставлять в данном месте. И если где-то остались неуточнённые переменные, то с ними можно поступить так же, как и с неопределёнными типами, но с учётом ограничений с двух сторон - по предку и по наследнику.

Таким образом, если решена задача уточнения полученной на первом этапе структуры значениями из классов-наследников, то вы практически полностью решили всю поставленную в данном тексте задачу, одновременно получив средство для изучения классов с уточняемыми типами. Далее вам остаётся лишь убедиться в работоспособности вашего решения, создав тестовый набор классов, с цепочкой в несколько предков и полями или методами в каждом классе, с уточняемым типом (лучше типами, то есть более одного) данных. Осталось задать в финальном тестовом наследнике уточнения для цепочки предков, а так же не забыть уточнять часть типов в промежуточных тестовых предках, дабы не получить одностороннее тестирование, когда все уточнения присутствуют лишь в финальном наследнике. Затем, используя список из GenericType, выведите на экран полный и актуальный тип (со всеми ограничениями) каждого поля или метода, объявленного в тестовых классах. Если после "ручной" проверки всё соответствует ожиданиям, то вам останется лишь дополнить обработку случаем, когда уточнение задаётся не в классе-наследнике, а в поле или методе, как например в таком случае - List<Integer> listField; Это несложно в сравнении с достигнутым вами ранее.

Отметим, что мы получили не конечный результат, а основу для, например, дальнейшей генерации. Поэтому, что бы упростить задачу создания генератора кода, нам стоит ещё раз обработать нашу структуру таким образом, что бы в ней остались только элементы типа GenericType, то есть с уже однозначно выбранными типами, без необходимости задумываться об ограничениях. Но разумеется, эту задачу вам придётся решать лишь тогда, когда вы столкнётесь с её необходимостью в виде поручения написать генератор или что-то ещё, требующее информацию о уточнениях типов.

Теперь немного вспомогательного кода. Для упрощения задачи работы с Reflection API предлагаем использовать очень простое средство просмотра информации о уточняемых типах. Это Java программа, которую можно скачать здесь, включает как исходный код, так и исполняемые файлы. Для запуска программы из командной строки желательно создать bat или sh файл (в зависимости от ОС) и записать в нём такую строчку (без кавычек): "java ru.soft.reflection.tree.ReflectionFrame вашКласс". В этой строке вместо аргумента "вашКласс" укажите выбранный вами класс с уточняемыми типами. Выбранный класс должен находиться в области видимости JVM, а потому не забудьте добавить путь к нему или jar-файлу с ним в примерно таком виде – «-cp path/to/your/class.jar». После запуска программа покажет окно с деревом, позволяющим просматривать информацию о каждом элементе указанного в качестве параметра класса, с упором на уточняемые типы и показом конкретного интерфейса, который возвращает тот или иной вызов Reflection API.

Ну и финальные замечания. Не стесняйтесь творить! Меняйте структуры, названия, и даже подходы, ведь главное - решение задачи, а не бюрократическое следование указаниям.


Глоссарий Глоссарий О сообществе О сообществе Конфиденциальность Конфиденциальность Работа с сайтом Работа с сайтом