Что можно типизировать java
Что можно типизировать java
Обобщения или generics (обобщенные типы и методы) позволяют нам уйти от жесткого определения используемых типов. Рассмотрим проблему, в которой они нам могут понадобиться.
Допустим, мы определяем класс для представления банковского счета. К примеру, он мог бы выглядеть следующим образом:
И на первый взгляд мы можем решить данную проблему следующим образом: задать id как поле типа Object, который является универсальным и базовым суперклассом для всех остальных типов:
Проблема может показаться искуственной, так как в данном случае мы видим, что в конструктор передается строка, поэтому мы вряд ли будем пытаться преобразовывать ее к типу int. Однако в процессе разработки мы можем не знать, какой именно тип представляет значение в id, и при попытке получить число в данном случае мы столкнемся с исключением java.lang.ClassCastException.
Писать для каждого отдельного типа свою версию класса Account тоже не является хорошим решением, так как в этом случае мы вынуждены повторяться.
Эти проблемы были призваны устранить обобщения или generics. Обобщения позволяют не указывать конкретный тип, который будет использоваться. Поэтому определим класс Account как обобщенный:
После объявления класса мы можем применить универсальный параметр T : так далее в классе объявляется переменная этого типа, которой затем присваивается значение в конструкторе.
Используем данный класс:
Например, первый объект будет использовать тип String, то есть вместо T будет подставляться String:
В этом случае в качестве первого параметра в конструктор передается строка.
А второй объект использует тип int (Integer):
Обобщенные интерфейсы
Интерфейсы, как и классы, также могут быть обобщенными. Создадим обобщенный интерфейс Accountable и используем его в программе:
При реализации подобного интерфейса есть две стратегии. В данном случае реализована первая стратегия, когда при реализации для универсального параметра интерфейса задается конкретный тип, как например, в данном случае это тип String. Тогда класс, реализующий интерфейс, жестко привязан к этому типу.
Вторая стратегия представляет определение обобщенного класса, который также использует тот же универсальный параметр:
Обобщенные методы
Кроме обобщенных типов можно также создавать обобщенные методы, которые точно также будут использовать универсальные параметры. Например:
Особенностью обобщенного метода является использование универсального параметра в объявлении метода после всех модификаторов и перед типом возвращаемого значения.
Затем внутри метода все значения типа T будут представлять данный универсальный параметр.
При вызове подобного метода перед его именем в угловых скобках указывается, какой тип будет передаваться на место универсального параметра:
Использование нескольких универсальных параметров
Мы можем также задать сразу несколько универсальных параметров:
Обобщенные конструкторы
Конструкторы как и методы также могут быть обобщенными. В этом случае перед конструктором также указываются в угловых скобках универсальные параметры:
В данном случае конструктор принимает параметр id, который представляет тип T. В конструкторе его значение превращается в строку и сохраняется в локальную переменную.
Дженерики в Java для самых маленьких: синтаксис, границы и дикие карты
Разбираемся, зачем нужны дженерики и как добавить их в свой код.
Оля Ежак для Skillbox Media
У нас в парадной подъезде рядом с почтовыми ящиками стоит коробка. Предполагалось, что туда будут выбрасывать бумажный спам, который какие-то вредители упорно кладут в эти самые ящики. Но в коробке вместе с бумажками лежат пустые бутылки и банки, подозрительного вида пакеты, а в нынешних реалиях — ещё и использованные медицинские маски. Почему люди так делают? Потому что так можно.
Теперь представьте, что содержимое коробки вы отвозите на переработку, а перед этим каждый раз приходится отделять бумагу от прочего мусора. Не хотели бы вы заполучить такую коробку, которая не даст положить в себя что-то, кроме бумаги? Если ваш ответ «да» — вам понравятся дженерики (generics).
Содержание
Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.
Знакомимся с дженериками
До появления дженериков программисты могли неявно предполагать, что какой-то класс, интерфейс или метод работает с элементами определённого типа.
Посмотрите на этот фрагмент кода:
Здесь предполагается, что метод printSomething работает со списком строк. Мы можем догадаться об этом, потому что в цикле все элементы приводятся (преобразуются) к классу String, а потом ещё и метод length этого класса вызывается.
Но смотрите, что сделали программисты Саша и Маша, — они поленились заглянуть внутрь метода и положили в список: один — число, а вторая — экземпляр StringBuilder.
Вот только тестировщик назначил баг не кому-то из них, а Паше, который написал метод printSomething, — потому что ошибка произошла именно во время его выполнения.
Паша быстро нашёл истинных виновников и попросил их исправить заполнение списка. Но на будущее решил подстраховаться от подобных ситуаций и переписал метод с использованием дженериков. Вот так:
Теперь, если кто-то захочет положить в массив нестроковый элемент, ошибка станет заметной сразу — ещё на этапе компиляции.
Обратите внимание, что во второй версии Пашиного метода item не приводится насильно к типу String. Мы просто получаем в цикле очередной элемент списка, и компилятор соглашается, что это, очевидно, будет строка. Код стал менее громоздким, читать его стало проще.
Объявляем дженерик-классы и создаём их экземпляры
Давайте запрограммируем ту самую коробку, о которой шла речь в начале статьи: создадим класс Box, который умеет работать только с элементами определённого типа. Пусть для простоты в этой коробке пока будет только один элемент:
В классе два метода:
Во всех случаях, кроме заголовка класса, символ T пишется без угловых скобок, он обозначает один и тот же параметр типа.
Теперь создадим коробку для бумаги. Пусть за бумагу отвечает класс Paper, а значит, экземпляр правильной коробки создаётся вот так:
Это полный вариант записи, но можно и короче:
Так как слева мы уже показали компилятору, что нужна коробка именно для бумаги, справа можно опустить повторное упоминание Paper — компилятор «догадается» о нём сам.
Это «угадывание» называется type inference — выведение типа, а оператор « <>» — это diamond operator. Его так назвали из-за внешнего сходства с бриллиантом.
E — element, для элементов параметризованных коллекций;
K — key, для ключей map-структур;
V — value, для значений map-структур;
N — number, для чисел;
T — type, для обозначения типа параметра в произвольных классах;
S, U, V и так далее — применяются, когда в дженерик-классе несколько параметров.
Дженерик-классы хороши своей универсальностью: с классом Box теперь можно создать не только коробку для бумаги, но и, например, коробки для сбора пластика или стекла:
А можно пойти ещё дальше и создать дженерик-класс с двумя параметрами для коробки с двумя отсеками. Вот так:
Теперь легко запрограммировать коробку, в одном отсеке которой будет собираться пластик, а во втором — стекло:
Обратите внимание, что type inference и diamond operator позволяют нам опустить оба параметра в правой части.
Объявляем и реализуем дженерик-интерфейсы
Объявление дженерик- интерфейсов похоже на объявление дженерик-классов. Продолжим тему переработки и создадим интерфейс пункта переработки GarbageHandler сразу с двумя параметрами: тип мусора и способ переработки:
Реализовать (имплементить) этот интерфейс можно в обычном, не дженерик- классе:
Но можно пойти другим путём и сначала объявить дженерик-класс с двумя параметрами:
Или скомбинировать эти два способа и написать дженерик-класс только с одним параметром:
Дженерик-классы и дженерик-интерфейсы вместе называются дженерик-типами.
Можно создавать экземпляры дженерик-типов «без расшифровки», то есть никто не запретит вам объявить переменную типа Box — просто Box:
Для такого случая даже есть термин — raw type, то есть «сырой тип». Эту возможность оставили в языке для совместимости со старым кодом, который был написан до появления дженериков.
В новых программах так писать не рекомендуется. Да и зачем? Ведь при таком способе теряются все преимущества использования дженериков.
Пишем дженерик-методы
В примерах выше мы уже видели параметризованные методы в дженерик-классах и интерфейсах. Типизированными могут быть как параметры метода, так и возвращаемый тип.
До этого мы использовали в методах только те обозначения типов, которые объявлены в заголовке дженерик-класса или интерфейса, но это не обязательно. Предположим, у нашего пункта переработки есть ещё опция: сбор опасных отходов, которые сотрудники вывозят на утилизацию в другое место. Напишем метод для этого:
У метода transfer есть свой личный параметр для типа, который не обязан совпадать ни с типом T, ни с типом S. При первом упоминании новый параметр, как и в случае с заголовком класса или интерфейса, пишется в угловых скобках.
Дженерик-методы можно объявлять и в обычных (не дженерик) классах и интерфейсах. Наш класс для переработки мог быть выглядеть так:
Здесь дженерики используются только в методе.
Обратите внимание на синтаксис: параметры типов объявляются после модификатора доступа ( public), но перед возвращаемым типом ( void). Они перечисляются через запятую в общих угловых скобках.
Ограничиваем дженерики сверху и снизу
Давайте немного расширим наше представление о мусоре и введём для него дополнительное свойство — массу «типичного представителя», то есть массу одной пластиковой бутылки или листка бумаги, например.
Теперь попробуем использовать эту массу в методе уже знакомого класса Box:
И получим ошибку при компиляции: мы не рассказали компилятору, что T — это какой-то вид мусора. Исправим это с помощью так называемого upper bounding — ограничения сверху:
Теперь метод getItemWeight успешно скомпилируется.
Здесь T extends Garbage означает, что в качестве T можно подставить Garbage или любой класс-наследник Garbage. Из уже известных нам классов это могут быть, например, Paper или Plastic. Так как и у Garbage, и у всех его наследников есть метод getWeight, его можно вызывать в новой версии дженерик-класса Box.
Для одного класса или интерфейса можно добавить сразу несколько ограничений. Вспомним про интерфейс для пункта приёма мусора и введём класс для метода переработки — HandleMethod. Тогда GarbageHandler можно переписать так:
Wildcards
Термин wildcard пришёл в программирование из карточной игры. В покере, например, так называют карту, которая может сыграть вместо любой другой. Джокер — известный пример такой «дикой карты».
Wildcard нельзя подставлять везде, где до этого мы писали буквенные обозначения. Не получится, например, объявить класс Box или дженерик-метод, который принимает такой тип:
Wildcards удобно использовать для объявления переменных и параметров методов совместно с классами из Java Collection Framework — здесь собраны инструменты Java для работы с коллекциями. Если вы не очень хорошо знакомы с ними, освежите знания, прочитав эту статью.
В примере ниже мы можем подставить вместо «?» любой тип, в том числе Paper, поэтому строка успешно скомпилируется:
Wildcards можно применять для ограничений типов:
Это уже знакомое нам ограничение сверху, upper bounding, — вместо «?» допуст им Garbage или любой его класс-наследник, то есть Paper подходит.
Но можно ограничить тип и снизу. Это называется lower bounding и выглядит так:
Собираем понятия, связанные с дженериками
Мы не успели разобраться с более сложными вещами — например, с заменами аргументов типов в классах-наследниках, с переопределением дженерик-методов, не узнали об особенностях коллекций с wildcards.
Обо всём этом и не только — в следующих статьях. А пока соберём небольшой словарик из терминов, которые связаны с использованием дженериков, — они пригодятся при чтении специальной литературы:
Термин | Расшифровка |
---|---|
Дженерик-типы (generic types) | Дженерик-класс или дженерик-интерфейс с одним или несколькими параметрами в заголовке |
Параметризованный тип (parameterized types) | Вызов дженерик-типа. Для дженерик-типа List параметризованным типом будет, например, List |
Параметр типа (type parameter) | Используются при объявлении дженерик-типов. Для Box T — это параметр типа |
Аргумент типа (type argument) | Тип объекта, который может использоваться вместо параметра типа. Например, для Box Paper — это аргумент типа |
Wildcard | Обозначается символом «?» — неизвестный тип |
Ограниченный wildcard (bounded wildcard) | Wildcard, который ограничен сверху — или снизу — |
Сырой тип (raw type) | Имя дженерик-типа без аргументов типа. Для List сырой тип — это List |
Ещё больше о дженериках, коллекциях и других элементах языка Java узнайте на нашем курсе «Профессия Java-разработчик». Научим программировать на самом востребованном языке и поможем устроиться на работу.
Переменные ссылочного типа хранят адрес ячейки в памяти, в которой лежит значение этой переменной.
В этом их ключевое отличие от примитивных типов, когда в переменной хранится само значение.
Все ссылочные типы в Java наследуются от типа Object.
Обобщение типа данных, generic
Начиная с Java 5 появились новые возможности для программирования, к которым следует отнести поддержку обобщенного программирования, названная в Java generic. Эта возможность позволяет создавать более статически типизированный код. Соответственно, программы становятся более надежными и проще в отладке.
generic являются аналогией с конструкцией «Шаблонов»(template) в С++. Ожидалось, что дженерики Java будут похожи на шаблоны C++. На деле оказалось, что различия между generic’ами Java и шаблонами С++ довольно велики. В основном generic в Java получился проще, чем их C++-аналог, однако он не является упрощенной версией шаблонов C++ и имеют ряд значительных отличий. Так, в языке появилось несколько новых концепций, касающихся generic’ов – это маски и ограничения.
Рассмотрим 2 примера без использования и с использованием generic. Пример без использования generic с приведением типа (java casting):
В данном примере программист знает тип данных, размещамый в List’e. Тем не менее, необходимо обратить особое внимание на приведение типа («java casting»). Компилятор может лишь гарантировать, что метод next() вернёт Object, но чтобы обеспечить присвоение переменной типа Integer правильным и безопасным, требуется java casting. Приведение типа не исключает возможности появления ошибки «Runtime Error» из-за невнимательности разработчика.
Возникает вопрос: «Как с этим бороться? Каким образом зарезервировать List для определенного типа данных?». Данную проблему решают дженерики generic. В следующем примере используется generic без приведения типов.
В примере вместо приведения к Integer, был определен тип списка List. В этом заключается существенное отличие, и компилятор может проверить данный тип на корректность во время компиляции во всем коде. Эффект от generic особенно проявляется в крупных проектах: он улучшает читаемость и надежность кода в целом.
Свойства Generics
Объявление generic-класса
Объявить generic-класс совсем несложно. Пример такого объявления :
Пример использования generic-класса GenericSample :
Проблемы реализации generic
1. Wildcard
Рассмотрим процедуру dump, которой в качестве параметров передается Collection для вывода значений в консоль.
Проблема состоит в том что данная реализация кода не эффективна, так как Collection не является полностью родительской коллекцией всех остальных коллекций, грубо говоря Collection имеет ограничения. Для решения этой проблемы используется Wildcard («?»), который не имеет ограничения в использовании, то есть имеет соответствие с любым типом, и в этом его плюсы. И теперь, мы можем вызвать это с любым типом коллекции.
2. Bounded Wildcard
Рассмотрим процедуру draw, которая рисует фигуры, наследующие свойства родителя Shape. Допустим у Shape есть наследник Circle, и его необходимо «изобразить».
Использование позволяет использовать тип Cycle и всех его предков вполоть до Object.
3. Generic метод
Определим процедуру addAll, которая в качестве параметров получает массив данных Object[] и переносит его в коллекцию Collection
Ошибки, возникающие в последних строках связаны с тем, что нельзя просто вставить Object в коллекции неизвестного типа. Способ решения этой проблемы является использование «generic метода«. Для этого перед методом нужно объявить и использовать его.
Но все равно после выполнение останется ошибка в третьей строчке :
Допустим имеется функция, которая находит ближайший объект к точке Glyph из заданной коллекции. Glyph – это базовый тип, и может иметься неограниченное количество потомков этого типа. Также может иметься неограниченное количество коллекций, хранящих элементы, тип которых соответствует одному из этих потомков. Хотелось бы, чтобы функция могла работать со всеми подобными коллекциями, и возвращала элемент, тип которого совпадал бы с типом элемента коллекции, а не приводился к Glyph. Следующий пример не очень удачный:
Функция выглядит неплохо, но, тем не менее, не лишена недостатков. Получается так, что функции можно передать коллекцию любого типа. Это усложняет реализацию функции, порождая необходимость проверки типа элемента. Будет гораздо лучше написать так:
Теперь все встает на свои места, и в функцию можно передать только коллекцию, элементы которой реализуют интерфейс Glyph. generic сделал свое дело, код получился более типобезопасным.
4. Generic-классы
Наследование можно применять и для параметров generic-классов:
Как в методах, так и в классах можно задать более одного базового интерфейса, который должен реализовывать generic-параметр. Это делается при помощи следующего синтаксиса:
В данном примере generic-параметр должен реализовывать не только интерфейс Glyph, но и MoveableGlyph. Ограничений на количество интерфейсов, которые должен реализовывать переданный тип, нет. Но в класс можно передать только один, т.к. в Java нет множественного наследования. Типы в этом списке могут быть generic-типами, но ни один конкретный интерфейс не может появляться в списке более одного раза, даже с разными параметрами:
5. Bounded type argument
Метод копирования из одной коллекции в другую
6. Lower bounded wildcard
Метод нахождения максимума в коллекции
6. Wildcard Capture
Реализация метода Swap в List
Ограничения generic
Невозможно создать массив generic’ов :
Невозможно создать массив generic-классов :
Преобразование типов
В Generics также можно манипулировать с информацией, хранящийся в переменных.
Наследование исключений в generic’ах
Возможность использовать параметр generic-класса или метода в throws позволяет при описании абстрактного метода не ограничивать разработчика, использующего класс или интерфейс, конкретным типом исключения. Но использовать тип, заданный в качестве параметра, в catch-выражениях нельзя.
Необходимо добавить, что тип, переданный в качестве параметра, должен обязательно быть наследником Throwable.
Таким образом, generic-и в Java получились проще и внесли несколько интересных концепций, таких как маски (wildcard) и ограничения, которые, добавили удобство при работе и помогли решить проблемы. Но, как и любое усложнение языка, эти нововведения затрудняют его понимание и изучение. Появление generic-ов сделало язык Java более выразительным и строгим; такие изменения только на пользу.
Пришел, увидел, обобщил: погружаемся в Java Generics
Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.
Работа с коллекциями
Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:
С появлением Generics необходимость в проверке и приведении типа отпала:
Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.
Принцип подстановки
Тип | Подтип |
Number | Integer |
List | ArrayList |
Collection | List |
Iterable | Collection |
Примеры отношения тип/подтип
Вот пример использования принципа подстановки в Java:
Ковариантность, контравариантность и инвариантность
Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.
«Дженерики» инвариантны. Приведем пример:
Wildcards
Всегда ли Generics инварианты? Нет. Приведу примеры:
Это ковариантность. List — подтип List