Что такое Reflection и как его использовать?
Reflection, рефлексия – это средства манипуляции данными на основе знания о структуре классов этих данных, инструменты метапрограммирования.
Класс Class<T> используется как точка входа в мир рефлекшена. Его экземпляры предоставляют саму метаинформацию о содержимом класса и основные методы для работы с ним. Все классы относящиеся Java Reflection находятся в пакетах java.lang и java.lang.reflect.
Экземпляр класса Class можно получить тремя способами:
🔘 Литералом .class;
🔘 Статическим фабричным методом Class.forName();
🔘 Методом getClass() экземпляров класса.
Использование Reflection API медленное и небезопасное. Оно позволяет ломать инвариантность состояний экземпляра, нарушать инкапсуляцию, и даже менять финальные поля.
Использовать рефлексию естественно в тестовом коде, в инструментах разработки, в фреймворках (особенно в связке с runtime-аннотациями). Рефлекшн в ординарном бизнес-коде обычно говорит о больших проблемах проектирования.
Нередко на интервью просят продемонстрировать пример использования рефлекшна. Один из самых близких для backend-разработчика примеров – инициализация классов-конфигураций в Spring Framework. Фреймворк с помощью рефлекшна сканирует внутренности таких классов. Поля и методы, помеченные специальными аннотациями, воспринимаются как объявления элементов экосистемы фреймворка.
Java Guru🤓 #java
Reflection, рефлексия – это средства манипуляции данными на основе знания о структуре классов этих данных, инструменты метапрограммирования.
Класс Class<T> используется как точка входа в мир рефлекшена. Его экземпляры предоставляют саму метаинформацию о содержимом класса и основные методы для работы с ним. Все классы относящиеся Java Reflection находятся в пакетах java.lang и java.lang.reflect.
Экземпляр класса Class можно получить тремя способами:
Использование Reflection API медленное и небезопасное. Оно позволяет ломать инвариантность состояний экземпляра, нарушать инкапсуляцию, и даже менять финальные поля.
Использовать рефлексию естественно в тестовом коде, в инструментах разработки, в фреймворках (особенно в связке с runtime-аннотациями). Рефлекшн в ординарном бизнес-коде обычно говорит о больших проблемах проектирования.
Нередко на интервью просят продемонстрировать пример использования рефлекшна. Один из самых близких для backend-разработчика примеров – инициализация классов-конфигураций в Spring Framework. Фреймворк с помощью рефлекшна сканирует внутренности таких классов. Поля и методы, помеченные специальными аннотациями, воспринимаются как объявления элементов экосистемы фреймворка.
Java Guru🤓 #java
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11❤5🔥5
В чём отличия интерфейса от абстрактного класса?
Главное отличие – это семантика. Интерфейсы появились еще до Java, как важная концепция ООП. Смысл интерфейса – некое поведение, описание свойства. Причем если придерживаться принципа сегрегации интерфейсов, это описание единственного аспекта поведения.
Класс, даже абстрактный – это комбинация всех свойств и их реализаций, которыми определяются сущности некоторой категории (собственно, класса).
Отсюда вытекает естественность и необходимость множественного наследования для интерфейсов. Опыт таких языков как C++ показал, что множественное наследование классов не нужно и проблемно (см. проблема ромбовидного наследования). По факту же обычно нужно всего лишь переиспользование кода, что не относится к ООП и реализуется в некоторых языках «интерфейсами с независимым состоянием» – примесями.
В Java интерфейс в отличие от абстрактного класса не может иметь состояния. Реализация поведения же допустима только в двух случаях: для статических методов, и default для обычных. Статические методы являются частью всего класса, а не экземпляров. Дефолтная реализация, как говорилось ранее, добавлена только как хак для сохранения совместимости.
В интерфейсах, как публичных описаниях, не имеют смысла и запрещены непубличные члены. Отсюда синтаксическое отличие: модификатор public, как и abstract для методов или static для полей, можно не писать. Запрещены и модификаторы, несовместимые с abstract: final, synchronized и прочие.
На уровне скомпилированного байткода тоже есть небольшие различия: интерфейс помечается флагом ACC_INTERFACE а для класса генерируется конструктор по-умолчанию.
И есть еще одно небольшое отличие. Интерфейс с одним методом можно использовать как функциональный, и инстанциировать лямбда-выражением. Для абстрактного класса даже с единственным методом такое не сработает.
Java Guru🤓 #java
Главное отличие – это семантика. Интерфейсы появились еще до Java, как важная концепция ООП. Смысл интерфейса – некое поведение, описание свойства. Причем если придерживаться принципа сегрегации интерфейсов, это описание единственного аспекта поведения.
Класс, даже абстрактный – это комбинация всех свойств и их реализаций, которыми определяются сущности некоторой категории (собственно, класса).
Отсюда вытекает естественность и необходимость множественного наследования для интерфейсов. Опыт таких языков как C++ показал, что множественное наследование классов не нужно и проблемно (см. проблема ромбовидного наследования). По факту же обычно нужно всего лишь переиспользование кода, что не относится к ООП и реализуется в некоторых языках «интерфейсами с независимым состоянием» – примесями.
В Java интерфейс в отличие от абстрактного класса не может иметь состояния. Реализация поведения же допустима только в двух случаях: для статических методов, и default для обычных. Статические методы являются частью всего класса, а не экземпляров. Дефолтная реализация, как говорилось ранее, добавлена только как хак для сохранения совместимости.
В интерфейсах, как публичных описаниях, не имеют смысла и запрещены непубличные члены. Отсюда синтаксическое отличие: модификатор public, как и abstract для методов или static для полей, можно не писать. Запрещены и модификаторы, несовместимые с abstract: final, synchronized и прочие.
На уровне скомпилированного байткода тоже есть небольшие различия: интерфейс помечается флагом ACC_INTERFACE а для класса генерируется конструктор по-умолчанию.
И есть еще одно небольшое отличие. Интерфейс с одним методом можно использовать как функциональный, и инстанциировать лямбда-выражением. Для абстрактного класса даже с единственным методом такое не сработает.
Java Guru🤓 #java
👍16🔥7❤3
Что будет результатом запуска кода?
Anonymous Quiz
13%
Ошибка компиляции
20%
Executing MyService
8%
BeanNotFoundException
54%
NoUniqueBeanDefinitionException
4%
MyService
👍6🔥2
Что если оба реализуемых интерфейса объявляют один и тот же метод?
Если объявление полностью одинаково – нет никакой проблемы, класс-реализация должен просто определить этот метод.
Когда у обоих интерфейсов объявлены методы с одинаковой сигнатурой, но разными возвращаемыми типами – всё зависит от того, какие именно эти типы.
Переопределение метода (override) еще с Java 5 ковариантно относительно возвращаемого типа. То есть, в наследнике тип результата метода может быть наследником: super метод возвращает Number, @Override метод возвращает Integer.
Если типы не связаны отношением наследования, например String и Long – такой класс невозможно реализовать.
Для примитивов никакой ковариантности возвращаемого типа нет. Даже если типы совместимы относительно присваивания: int→long, int→Integer. В любом из таких случаев будет ошибка о несовместимости возвращаемых типов, для примитивов они должны совпадать в точности.
Если различие в части throws, методы объявлены выбрасывающими разные типы исключений. Правила здесь те же, что для возвращаемых типов – работает ковариантность. Отличие лишь в том, что исключений примитивных типов не бывает, а даже для не являющихся родителем и наследником исключений всегда есть вариант, удовлетворяющий обоим – отсутствие выбрасываемых исключений вообще.
Java Guru🤓 #java
Если объявление полностью одинаково – нет никакой проблемы, класс-реализация должен просто определить этот метод.
Когда у обоих интерфейсов объявлены методы с одинаковой сигнатурой, но разными возвращаемыми типами – всё зависит от того, какие именно эти типы.
Переопределение метода (override) еще с Java 5 ковариантно относительно возвращаемого типа. То есть, в наследнике тип результата метода может быть наследником: super метод возвращает Number, @Override метод возвращает Integer.
Если типы не связаны отношением наследования, например String и Long – такой класс невозможно реализовать.
Для примитивов никакой ковариантности возвращаемого типа нет. Даже если типы совместимы относительно присваивания: int→long, int→Integer. В любом из таких случаев будет ошибка о несовместимости возвращаемых типов, для примитивов они должны совпадать в точности.
Если различие в части throws, методы объявлены выбрасывающими разные типы исключений. Правила здесь те же, что для возвращаемых типов – работает ковариантность. Отличие лишь в том, что исключений примитивных типов не бывает, а даже для не являющихся родителем и наследником исключений всегда есть вариант, удовлетворяющий обоим – отсутствие выбрасываемых исключений вообще.
Java Guru🤓 #java
👍10🔥3❤2
Как изменить значение приватного финального поля?
Стоит сразу сказать, это очень плохая практика. Такое изменение грубо нарушает принципы сокрытия данных, и потенциально ломает инвариантность состояния объекта.
Для этого трюка необходимо прибегнуть к использованию Reflection API.
Сначала получим дескриптор поля – экземпляр класса Field. У объекта метакласса Class<X> интересующего нас класса вызовем метод getDeclaredField(). Просто getField() не сработает, потому что он работает только с публичными полями. Параметром передается строка с именем поля.
Полученного экземпляра Field уже достаточно для доступа к изменяемым приватным полям. Перед обращением требуется сделать его доступным, вызвав setAccessible(true).
Сам доступ осуществляется методами get*() и set*(). Так как Field представляет дескриптор поля класса, без привязки к конкретному экземпляру класса, экземпляр передается параметром в методы доступа. Для статического поля передается null.
Чтобы побороть неизменяемость финального поля, нужно снять его модификатор final. Все модификаторы поля хранятся в поле modifiers дескриптора. То есть, нужно также с помощью рефлекшена сделать доступным и обновить поле уже объекта Field.
Поле modifiers хранит модификаторы в виде битовой маски. Для изменения придется прибегнуть к битовым операторам.
Полный код установки значения 42 в поле myField объекта myObject выглядит так:
Java Guru🤓 #java
Стоит сразу сказать, это очень плохая практика. Такое изменение грубо нарушает принципы сокрытия данных, и потенциально ломает инвариантность состояния объекта.
Для этого трюка необходимо прибегнуть к использованию Reflection API.
Сначала получим дескриптор поля – экземпляр класса Field. У объекта метакласса Class<X> интересующего нас класса вызовем метод getDeclaredField(). Просто getField() не сработает, потому что он работает только с публичными полями. Параметром передается строка с именем поля.
Полученного экземпляра Field уже достаточно для доступа к изменяемым приватным полям. Перед обращением требуется сделать его доступным, вызвав setAccessible(true).
Сам доступ осуществляется методами get*() и set*(). Так как Field представляет дескриптор поля класса, без привязки к конкретному экземпляру класса, экземпляр передается параметром в методы доступа. Для статического поля передается null.
Чтобы побороть неизменяемость финального поля, нужно снять его модификатор final. Все модификаторы поля хранятся в поле modifiers дескриптора. То есть, нужно также с помощью рефлекшена сделать доступным и обновить поле уже объекта Field.
Поле modifiers хранит модификаторы в виде битовой маски. Для изменения придется прибегнуть к битовым операторам.
Полный код установки значения 42 в поле myField объекта myObject выглядит так:
Field field = myObject.class.getDeclaredField( "myField" );
field.setAccessible( true );
Field modifiersField = Field.class.getDeclaredField( "modifiers" );
modifiersField.setAccessible( true );
modifiersField.setInt( field, field.getModifiers() & ~Modifier.FINAL );
field.setInt(myObject, 42);
Java Guru🤓 #java
👍9🔥4❤2
При вызове метода executeTask() три раза, сколько экземпляров TaskProcessor будет создано?
Anonymous Quiz
51%
Один, так как TaskManager синглтон
2%
Два, просто на удачу
41%
Три, так как TaskProcessor prototype
3%
Ошибка компиляции
2%
RuntimeException
👍11🔥5❤1🥴1
Как это будет:
Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Java-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.
Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_sh_bot
Реклама. ООО "ШОРТКАТ", ИНН: 9731139396, erid: 2Vtzquwsnpa
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3👍2🔥2
Что произойдёт при запуске Spring-приложения с таким классом?
Anonymous Quiz
22%
Не запустится, так как нельзя использовать обе эти аннотации на одном классе
8%
Запустится, Spring создаст два разных бина: один с @Component, другой с @Service
9%
Запустится, но Spring не создаст ни одного бина из-за конфликта
55%
Запустится, будет зарегистрирован один бин
6%
Запустится, но будет RuntimeException
👍16🔥5
Курс «Java Developer. Professional» — это структурированное обучение для разработчиков, которые хотят выйти на новый уровень, освоить актуальный стек технологий и уверенно претендовать на позиции уровня Middle+.
Программа OTUS постоянно обновляется, соответствуя требованиям рынка, а диплом ценится работодателями.
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3❤2🔥2
Что будет результатом кода?
Anonymous Quiz
34%
Ошибка компиляции
6%
RuntimeException
46%
0
13%
null
2%
number
👍7🔥7😁3
Как реализовать собственный стрим?
Любой стрим определяется его сплитератором. Spliterator – это специальный разделяемый внутренний итератор.
Есть много способов получить готовый сплитератор или стрим, но чтобы создать полностью свою специфическую логику перебора элементов, придется написать собственный сплитератор.
Поток создается из сплитератора одним из статических методов класса StreamSupport. Вызов его методов осуществляется самим фреймворком. Вкратце его работа выглядит так:
• Элементы перебираются методом tryAdvance, пока он не выдаст false. Через параметр action к элементу применяются последующие операции.
• При применении промежуточных и терминальных операций учитываются характеристики потока, изначально задаваемые методом characteristics.
• Когда обработка стрима распараллеливается, методом trySplit от начала последовательности элементов «откусывается» часть, и возвращается завернутой в новый сплитератор. Текущий продолжает идти по оставшемуся хвосту. В идеале, по возможности эта часть – половина элементов потока. Если разделить уже нельзя, возвращается null.
Java Guru🤓 #java
Любой стрим определяется его сплитератором. Spliterator – это специальный разделяемый внутренний итератор.
Есть много способов получить готовый сплитератор или стрим, но чтобы создать полностью свою специфическую логику перебора элементов, придется написать собственный сплитератор.
Поток создается из сплитератора одним из статических методов класса StreamSupport. Вызов его методов осуществляется самим фреймворком. Вкратце его работа выглядит так:
• Элементы перебираются методом tryAdvance, пока он не выдаст false. Через параметр action к элементу применяются последующие операции.
• При применении промежуточных и терминальных операций учитываются характеристики потока, изначально задаваемые методом characteristics.
• Когда обработка стрима распараллеливается, методом trySplit от начала последовательности элементов «откусывается» часть, и возвращается завернутой в новый сплитератор. Текущий продолжает идти по оставшемуся хвосту. В идеале, по возможности эта часть – половина элементов потока. Если разделить уже нельзя, возвращается null.
Java Guru🤓 #java
👍7🔥5❤3
Бесплатный урок по Apache Kafka⭐️
Учим работать с реальными исходными данными, а не на теоретических примерах.
✅Расскажем про язык Кафки: топики, партиции, продюсеры-консьюмеры, кластер, ноды.
✅Рассмотрим: как работают очереди сообщений, сколько должно быть консьюмеров для эффективной вычитки, как повысить надёжность кластера с помощью репликации данных.
✅Покажем, как развернуть кластер Кафки на своём ПК с 3 нодами, schema-registry и авторизацией.
Обычно в инструкциях кластер из 1 ноды, зукипера и 1 брокера, но это не наш путь, смотрим сразу на практике.
Забрать урок👉🏻 в боте
Учим работать с реальными исходными данными, а не на теоретических примерах.
✅Расскажем про язык Кафки: топики, партиции, продюсеры-консьюмеры, кластер, ноды.
✅Рассмотрим: как работают очереди сообщений, сколько должно быть консьюмеров для эффективной вычитки, как повысить надёжность кластера с помощью репликации данных.
✅Покажем, как развернуть кластер Кафки на своём ПК с 3 нодами, schema-registry и авторизацией.
Обычно в инструкциях кластер из 1 ноды, зукипера и 1 брокера, но это не наш путь, смотрим сразу на практике.
Забрать урок👉🏻 в боте
👍3❤2🔥2
Как работают параллельные стримы?
Основная цель, ради которой в Java 8 был добавлен Stream API – удобство многопоточной обработки.
Обычный стрим будет выполняться параллельно после вызова промежуточной операции parallel(). Некоторые стримы создаются уже многопоточными, например результат вызова Collection#parallelStream(). Для распараллеливания используется единый общий ForkJoinPool.
Внутри реализации потока его сплиттератор оборачивается в AbstractTask, который и отправляется на выполнение в пул. AbstractTask при выполнении считывает estimateSize сплиттератора и текущую степень параллелизма пула. На основе этих данных он принимает решение, распараллелить ли сплиттератор на два методом trySplit().
У удобства такого решения есть обратная сторона. Так как пул единый, нагрузка распределяется на всех пользователей параллельных стримов в программе. Если в одном потоке выполняются долгие блокирующие операции, это может ударить по производительности в совершенно не связанном с ним другом потоке.
Если всё же требуется использовать отдельный пул потоков, сам стрим выполняется как задача этого отдельного пула.
Java Guru🤓 #java
Основная цель, ради которой в Java 8 был добавлен Stream API – удобство многопоточной обработки.
Обычный стрим будет выполняться параллельно после вызова промежуточной операции parallel(). Некоторые стримы создаются уже многопоточными, например результат вызова Collection#parallelStream(). Для распараллеливания используется единый общий ForkJoinPool.
Внутри реализации потока его сплиттератор оборачивается в AbstractTask, который и отправляется на выполнение в пул. AbstractTask при выполнении считывает estimateSize сплиттератора и текущую степень параллелизма пула. На основе этих данных он принимает решение, распараллелить ли сплиттератор на два методом trySplit().
У удобства такого решения есть обратная сторона. Так как пул единый, нагрузка распределяется на всех пользователей параллельных стримов в программе. Если в одном потоке выполняются долгие блокирующие операции, это может ударить по производительности в совершенно не связанном с ним другом потоке.
Если всё же требуется использовать отдельный пул потоков, сам стрим выполняется как задача этого отдельного пула.
Java Guru🤓 #java
👍9🔥6🤯2
На открытом уроке «Облако в кармане: запускаем всю инфраструктуру для теста при сборке» от OTUS мы расскажем, как избежать ручных настроек и запусков. Вместо этого вы научитесь автоматизировать весь процесс с помощью Docker, DockerCompose и TestContainers, интегрируя их с Gradle.
Урок полезен для разработчиков на Kotlin и Java, которые работают с автотестами — интеграционными и end-to-end.
В ходе урока вы освоите:
▫️Автоматический запуск всей необходимой инфраструктуры для тестирования.
▫️Создание Docker-образов для тестов и деплоя.
▫️Разработку автотестов, которые поднимут ваши навыки на новый уровень.
Участники получат скидку на курс «Kotlin Backend Developer. Professional».
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3👍2🔥2
Как инстанцировать экземпляр generic типа?
Внутри класса class Foo<T> на generic параметре T невозможно выполнить никакой оператор: нельзя взять его .class, нельзя применить его в instanceof. Также и вызов на нем оператора new приведет к ошибке.
Причина этих ограничений кроется в стирании типов. Дженерик параметры правильно воспринимать скорее как ограничения типов, чем как конкретные типы. Эти ограничения действуют для более строгих проверок на этапе компиляции. В рантайме же информация о конкретных переданных типах-параметрах стирается. А все эти операторы выполняются именно в рантайме.
Стандартный простой способ действия здесь – кроме значения типа T передавать еще и объект-дескриптор для этого типа, экземпляр класса Class<T>. Объект может быть создан из дескриптора рефлекшеном.
Но существует один хак, способный справиться со стиранием типов. Тип-параметр все-таки остается в одном месте в рантайме. Метод метакласса наследника определившего конкретный тип getGenericSuperclass() возвращает класс, которым параметризован родитель.
Java Guru🤓 #java
Внутри класса class Foo<T> на generic параметре T невозможно выполнить никакой оператор: нельзя взять его .class, нельзя применить его в instanceof. Также и вызов на нем оператора new приведет к ошибке.
Причина этих ограничений кроется в стирании типов. Дженерик параметры правильно воспринимать скорее как ограничения типов, чем как конкретные типы. Эти ограничения действуют для более строгих проверок на этапе компиляции. В рантайме же информация о конкретных переданных типах-параметрах стирается. А все эти операторы выполняются именно в рантайме.
Стандартный простой способ действия здесь – кроме значения типа T передавать еще и объект-дескриптор для этого типа, экземпляр класса Class<T>. Объект может быть создан из дескриптора рефлекшеном.
Но существует один хак, способный справиться со стиранием типов. Тип-параметр все-таки остается в одном месте в рантайме. Метод метакласса наследника определившего конкретный тип getGenericSuperclass() возвращает класс, которым параметризован родитель.
Java Guru🤓 #java
👍20🔥7❤4
Как написать иммутабельный класс?
Immutable (неизменяемый) класс – это класс, состояние экземпляров которого невозможно изменить после создания.
С иммутабельным классом всегда легче работать. Его состояние не поменяется, значит обращаться к нему в многопоточной среде можно без дополнительной синхронизации. Функции, зависящие только от состояния экземпляра будут возвращать один и тот же результат от вызова к вызову – это облегчает например реализацию hashCode(). Также вместо нескольких одинаковых экземпляров можно использовать один закэшированный объект, экономя память (паттерн Приспособленец).
Шаги, которые необходимо предпринять, чтобы класс стал immutable:
1. Запретите расширение класса – либо объявите его final, либо закройте доступ наследникам ко всем способам мутации, перечисленным в следующих пунктах;
2. Сделайте все поля финальными;
3. Не выставляйте наружу методов-мутаторов, которые меняют состояние;
4. Не отдавайте наружу поля ссылочного изменяемого типа (объекты классов, массивы) – если объект под ссылкой не иммутабельный, должна возвращаться его глубокая копия (defensive copy);
5. Создавайте объект правильно (подробнее в следующем посте).
Если вам нужны преимущества иммутабельного объекта, но также нужно иногда изменять его, подойдет подход copy on write: каждый метод-мутатор должен мутировать и возвращать не сам объект, а только что созданную его копию. Оригинал всё так же остается неизменным.
Java Guru🤓 #java
Immutable (неизменяемый) класс – это класс, состояние экземпляров которого невозможно изменить после создания.
С иммутабельным классом всегда легче работать. Его состояние не поменяется, значит обращаться к нему в многопоточной среде можно без дополнительной синхронизации. Функции, зависящие только от состояния экземпляра будут возвращать один и тот же результат от вызова к вызову – это облегчает например реализацию hashCode(). Также вместо нескольких одинаковых экземпляров можно использовать один закэшированный объект, экономя память (паттерн Приспособленец).
Шаги, которые необходимо предпринять, чтобы класс стал immutable:
1. Запретите расширение класса – либо объявите его final, либо закройте доступ наследникам ко всем способам мутации, перечисленным в следующих пунктах;
2. Сделайте все поля финальными;
3. Не выставляйте наружу методов-мутаторов, которые меняют состояние;
4. Не отдавайте наружу поля ссылочного изменяемого типа (объекты классов, массивы) – если объект под ссылкой не иммутабельный, должна возвращаться его глубокая копия (defensive copy);
5. Создавайте объект правильно (подробнее в следующем посте).
Если вам нужны преимущества иммутабельного объекта, но также нужно иногда изменять его, подойдет подход copy on write: каждый метод-мутатор должен мутировать и возвращать не сам объект, а только что созданную его копию. Оригинал всё так же остается неизменным.
Java Guru🤓 #java
👍15🔥8❤3