gb-java-devel/scenarios/jtd3-08b.tex

620 lines
98 KiB
TeX
Raw Normal View History

2023-03-11 21:38:50 +03:00
\documentclass[../j-spec.tex]{subfiles}
\begin{document}
\section{Интерфейсы}
\begin{longtable}{|p{35mm}|p{135mm}|}
\hline
Экран & Слова \\ \hline
\endhead
Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработчика на джава \\ \hline
Отбивка & в сегодняшней лекции коснёмся одного из ключевых понятий программирования на языке джава -- обобщённого программирования. \\ \hline
На прошлом уроке & На прошлом уроке мы поговорили об интерфейсах, рассмотрели понятие и принцип работы, поговорили о ключевом слове implements; коротко посмотрели на наследование и множественное наследование интерфейсов, реализацию, значения по умолчанию и частичная реализацию интерфейсов. Буквально два слова я сказал о функциональных интерфейсах и чуть подробнее об анонимных классах. \\ \hline
2023-03-12 23:05:12 +03:00
На этом уроке & На этом уроке поговорим более крутом полиморфизме, чем интерфейсы. Казалось бы, куда уж круче, но сегодня на повестке обобщения или если у вас нет отторжения от неизбежных в разработке англицизмов -- дженерики. Сначала попробуем обойтись без них, потом сделаем тоже самое с ними, сравним. Обсудим работу дженерик интерфейсов, Обобщённые методы и подстановочные символы, а именно, ограничения сверху и снизу, дополнительно проговорим о таком явлении как загрязнение кучи. \\ \hline
2023-03-11 21:38:50 +03:00
2023-03-12 23:05:12 +03:00
08-01 & Итак, обобщение. Смысл в программировании примерно такой же, как и в обычной разговорной речи - отсутствие деталей реализации. \\ \hline
2023-03-11 21:38:50 +03:00
2023-03-12 23:05:12 +03:00
08-02 & Может показаться, что мы снова будем как-то всесторонне рассматривать интерфейсы и абстрактные классы, отсутствие реализаций и всё такое, но нет. Обобщения это не про отсутствие реализации, а про создание некоторого общего подхода к реализации одинаковых алгоритмов, но с разными данными. Запутал? Конечно! подойдём с другой стороны. Обобщения -- это некие конструкции, позволяющие работать данными не задумываясь что там именно лежит внутри структуры или метода (строки, числа или коты). То есть обобщения позволяют единообразно работать с разными типами данных. Можно сказать, что обобщения -- это некий поклон в сторону динамической типизации. Попробуем начать разбираться на примере. \\ \hline
08-03 & Как было сказано, обобщения позволяют единообразно работать с разными типами данных, поэтому создадим некоторый класс, который сможет хранить внутри любые данные джава. А любые данные в джава, как мы помним -- это объект. В таком случае необходимо создать класс Box, у которого будет единственное поле типа Object, в которое можно будет записать объект абсолютно любого типа. Далее, для начала, на примере чисел, напишем в метод мейне создадим два экземпляра и положим туда пару чисел. Надо бы например научиться складывать их. Пишем b1.getObj() + b2.getObj(). Как думаете, что там за ошибка скрывается за красным подчёркиванием среды разработки? там предупреждение среды о том, что оператор сложения не определён для объектов. Проблема в том, что я знаю, что тут лежат целые числа, а джава не особо. Чего не хватает? правильно, приведения типов к Integer.
%% 2
можем создать пару других коробок со строками, и будет производиться уже не складывание чисел, а конкатенация строк. Проблема в том что при каждом вытаскивании данных из коробки надо делать приведение типов, чтобы указать какие именно там лежат данные. Можно попробовать эту проблему решить если мы например назовём нашу переменную в венгерской нотации, тогда при использовании спустя пятьдесят или сто строк мы будем видеть что переменная вроде как интовая или строковая.
%% 3
Кроме постоянных перепроверок и кастов есть вторая проблема, она состоит в том, что мы, естественно, можем положить в первую коробку что угодно, например, строку и вполне ожидаемо получим ClassCastException. Допустим кто-то не понял мысль что ibox это коробка с интом, и хочет положить туда строку. Джава никак не запретит этого потому что строка это точно такой же равноправный объект. Как быть? первое, что приходит на ум - это try-catch но если можно какую-то ситуацию исправить ифом то лучше защититься ифом, конечно, получим примерно такой совсем не громоздкий код, как на слайде. Потому что перед строками с приведением типов желательно делать проверку на эти самые типы. Третья, самая неприятная проблема состоит в том, что все проблемы подобного рода проявляют себя только в рантайме, то есть у конечного пользователя перед глазами, когда мы уже никак это исправить не можем. \\ \hline
2023-05-09 23:34:17 +03:00
08-04 & К чему мы в итоге пришли? к тому что в принципе возможно создавать классы, которые могут работать с любыми типами данных, но при любом обращении к таким классам нам неплохо бы делать достаточно сложные проверки, чтобы каждый раз не вываливаться с исключениями. До появления дженериков в принципе так и делалось. Таким образом мы совершенно логично подошли к дженерикам, которые должны решить часть наших проблем. Обобщения появились в джава 1.5. Дженерики или обобщения -- это особые средства языка Java для реализации обобщённого или можно встретить термин шаблонизированного программирования: особого подхода к описанию данных и алгоритмов, позволяющего работать с различными типами данных без изменения внешнего описания. Обобщения добавляют в Java безопасность типов и делают управление проще. Исключается необходимость применять явные приведения типов, так как благодаря обобщениям все приведения выполняются неявно, в автоматическом режиме, платформой джава. Все дженерики работают только со ссылочными типами данных но это не проблема потому что для всех примитивов есть обёртки в виде ссылочного типа.
% 02
допустим пишем всеми любимые аркадные стрелялки. у нас есть корабль, враги, наши пули, пули врагов, просто какие-то летающие помехи, влияющие на здоровье. За минуту активной игры может насоздаваться до полутысячи разных объектов и каждый раз их отдавать на сборку мусора - удовольствие сомнительное, потому что сборка это очень дорогостоящий процесс, поэтому принято создавать так называемые пулы объектов, в которых лежит 300 пуль, десяток врагов, полсотни камней, и в этом пуле объекты просто переключаются из активного в неактивное состояние. А класс непосредственно игрового объекта наследует поведение дженерика, чтобы не описывать его каждый раз, то есть содержит основную общую для всех объектов логику -- достать из пула объект как будто он новый, положить в пул использованный объект, отработать столкновение объекта с другим объектом, подвигать объект по координатной сетке, и так далее.
\\ \hline
2023-03-12 23:05:12 +03:00
08-05 & Чтобы понять почему больше не надо делать явное приведение типов, и откуда берется какая-то безопасность, напишем в треугольных скобках букву <T>. в принципе, там может быть любая буква, или даже нескольно, но принято писать именно T, чтобы обозначить Type, Тип. Мы не можем на этапе описания класса сказать что это будет за тип (число, строка или кот), но точно знаем что какой-то тип будет. пишем поле этого типа, геттер-сеттер-конструктор. Соответственно если мы напишем Т не в треугольных скобках при описании класса, то джава будет думать что это какой то реально существующий класс, который она просто не видит. А так мы намекаем, что это обобщение и тип будет задаваться при создании объекта. Естественно поменять его будет нельзя потому что джава это язык сильной статической типизации.
% 2
При вызове конструктора, обратите внимание, будет указано, что конструктор ждёт инт, где же Т? а вот при указании типа в левой части, этот тип подставился во все места класса, где компилятор увидел букву Т. При получении значений из integerBox и stringBox не требуется преобразование типов, integerBox.getValue() сразу возвращает Integer, а stringBox.getValue() - String. То есть приведение типов выполняется неявно и автоматически.
Первая проблема решена.
% 3
Если объект создан как Integer, то мы не сможем записать в него строку. При попытке написать такую строку кода, получится ошибка на этапе компиляции. То есть обобщения отслеживают корректность используемых типов данных. Попробуем положить в одну из коробок строку -- компилятор не даёт нам это сделать. Вторая проблема -- проверка на инстансоф и третья -- допущение и выбрасывание ошибок в рантайме -- решились сами собой. Ну и на самом деле несмотря на то что мы пишем все вот эти красивые скобки и не делаем проверки -- внутри самой джавы это всё равно превратится в те же самые приведения и проверки, которые мы только что закомментировали и от которых отказались. \\ \hline
08-06 & По соглашению переменные типа именуются одной буквой в верхнем регистре. Это сильно отличается от соглашения об именовании переменных, классов и интерфейсов. Без такого отличия было бы трудно отличить переменную типа от класса и интерфейса. Буквы в фигурных скобках - их там совершенно неожиданно может быть больше одной, разделённых запятой. По конвенциям буквы обычно пишутся первые от английского слова, лучше всего обозначающего то, что там внутри -- T = type, E = entity (element), K, V = key, value, N = numeric
2023-05-09 23:34:17 +03:00
Но всё таки можете ставить такие, какие Вам нравятся и даже писать многобуквенные обозначения параметра, но должен предупредить, что это достаточно грубое нарушение стиля написания кода. Линейкой по пальцам никто бить не станет, но и всерьёз рассматривать к себе в команду скорее всего не будут. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-07 & Вроде всё так классно, что аж не хочется переключаться на этот слайд, но надо. Дженерики, конечно, хороши, но всё же накладывают на работу с собой некоторые ограничения. Как думаете, что произойдёт, если я попытаюсь написать метод номер один внутри нашего дженерика?
% 2
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
А что надо написать в теле метода, чтобы создался объект класса Т? правильный ответ -- ничего. !Ограничение дженерика в том, что невозможно внутри дженерика создать экземпляр параметризующего класса Т!, потому что на этапе компиляции мы об этом классе вообще ничего не знаем, из чего он будет состоять, какие у него будут конструкторы и будут ли конструкторы. Ведь даже конструктора без аргументов там может не быть (как известно, если есть другие то пустой не создаётся). Это ограничение возможно обойти, используя паттерн абстрактная фабрика, но это совсем другая история.
% 3
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Также нельзя создавать внутри дженерика массив из обобщённого типа данных. Но это тоже не так уж критично потому что всегда можно подать такой массив снаружи.
% 4
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
А можем ли мы создать статическое поле типа Т? Конечно же нет, всё по той же причине -- конкретный тип для параметра Т мы узнаём только при создании *объекта* дженерика, то есть у нас получается слишком много противоречий, чтобы понять какое значение будет у статического поля.
% 5
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Интерфейсы обобщённого типа бывают, о них чуть позже, а вот исключение обобщённого типа создать нельзя. Впрочем, это, наверное, и к лучшему.\\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-08 & Посмотрим, на этот раз, внимательно, на то как принято работать с объектами обобщённых классов. При обращении к обобщённому классу нужно заменить параметры типа на конкретные классы или интерфейсы, например строку, целое число или кота. Может показаться, что обращение к параметризованному типу похоже на обычный вызов метода, но вместо передачи аргумента в метод в круглых скобках, передается аргумент типа в треугольных, например Integer или Стринг в класс GBox, в описанном нами случае.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Параметр типа и аргумент типа -- это два разных понятия, как и при вызове методов, помните, там было различие, кто параметр, а кто аргумент метода? Когда объявляется обобщённый тип GBox<T>, то T является параметром типа, а когда происходит обращение к обобщённому типу, передается аргумент типа, например Integer. Это довольно похоже на различие в методах, поэтому будет несложно запомнить.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Как и любое другое объявление переменной просто GBox<Integer> integerBox сам по себе НЕ создаёт экземпляр класса GBox. Такой код просто объявляет идентифиактор GBox, но сразу уточняет, что это будет коробка с целыми числами. Такой уже созданный идентификатор обычно называется параметризованным типом. То есть, получается, что мы уже знаем примитивные типы, ссылочные типы, интерфейсные типы, а теперь ещё и параметризированные. Интересно.
% 2
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Чтобы создать экземпляр класса, используется ключевое слово new, как обычно, и в дополнение указывается, что создаётся не просто GBox, а обобщённый, поэтому пишется <Integer> в треугольных скобках между именем вызываемого конструктора и скобками с параметрами этого конструктора. Но компиляторы почти сразу, а именно, с седьмой версии, оказались достаточно умными, чтобы понять, какой именно тип нужно подставить в треугольные скобки (по-умному это называется выведением типа из контекста), поэтому, если тип совпадает с аргументом типа в идентификаторе, в скобках экземпляра его можно не писать, как на 43й строке. Это называется бриллиантовый оператор. После создания экземпляра становится возможно обращаться к методам, тут всё как обычно.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Дополнительно обращаю ваше внимание на то, что в описанном нами случае невозможно создать экземпляр без начального значения, это привычное поведение, потому что при создании класса GBox был создан конструктор, принимающий в качестве параметра значение, а значит конструктор по-умолчанию перестал создаваться неявно. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-09 & Никаких ограничений, кроме здравого смысла, на количество параметризированных типов не накладывается. Часто можно встретить обобщения с двумя типами, например, в коллекциях, хранящих пары ключ-значение. Там параметризированные типы так и называются ки и вэлью. Достаточно написать список из нужных внутри класса типов через запятую. Также, как нет ограничений на использование типов внутри угловых скобок, кроме упомянутого выше, что типы должны быть ссылочными. На слайде видно, что использование только что написанного обобщённого GBox вполне допустимо. Единственное, за чем, на мой личный взгляд, важно следить, это чтобы код оставался читаемым и не пестрел множественными треугольниками скобок. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-10 & Сырой тип -- это имя обобщённого класса или интерфейса без аргументов типа, то есть это, фактически, написание идентификатора и вызов конструктора обобщённого класса как обычного, без треугольных скобок. Можно часто увидеть использование сырых типов в старом коде, поскольку многие классы, например из фреймворка коллекций, до Java 5 были необобщёнными, как вы помните, обобщения появились только в пятой джаве, а люди умудрялись программировать и до этого эпохального события. Когда используются сырые типы, по сути получается то же самое поведение, которое было до введения обобщений в Java. Как видно на первом снимке экрана -- метод, возвращающий объект из коробки, возвращает объект.
% 2
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Это довольно логично, потому что никто не указал ни идентификатору ни конструктору, что именно планируется в эту коробку складывать. Ситуация меняется, если указать тип, мы это уже видели, не лишним будет посмотреть ещё раз на втором снимке экрана.
% 3
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Поэтому GBox -- это сырой тип обобщённого типа GBox<T>. Однако необобщённый класс или интерфейс НЕ являются сырыми типами. Для совместимости со старым кодом допустимо присваивать параметризованный тип своему собственному сырому типу, как это сделано на третьем снимке экрана. Компилятор, конечно сразу начинает довольно интенсивно сигнализировать о том, что наш код не совсем современно написан, но в целях демонстрации я думаю, он потерпит.
% 4
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Также, если попытаться присвоить параметризованному типу сырой тип, то буквально каждое слово в программе будет с предупреждением, или если попытаться вызвать обобщённый метод в сыром типе. Предупреждения показывают, что сырой тип обходит проверку обобщённого типа, что откладывает обнаружение ошибки на время выполнения программы. Поэтому, если в программе видим много предупреждений среды разработки -- перестаём работать с сырыми типами, а если вдруг всё-таки нужно именно сырой тип, и сыпятся какие-то ошибки, например, невозможность использования каких-то присущих только обобщённым типам методов, то, соответственно, приводим типы \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-11 & Как уже упоминалось, при использовании сырого типа можно столкнуться с предупреждениями среды, которые на самом деле предупреждения компилятора, и обычно имеют вид, представленный в верхней части слайда. Термин "unchecked" означает непроверенные, то есть компилятор не имеет достаточного количества информации для обеспечения безопасности типов. По умолчанию этот вид предупреждений выключен, поэтому компилятор в терминале на самом деле даёт подсказку. Чтобы увидеть все "unchecked" предупреждения нужно перекомпилировать код с опцией -Xlint:unchecked. Конечно, среды проектирования обо многом предупреждают, иногда даже о большем, чем может предупредить компилятор, но часто разработчики на эти предупреждения не обращают внимания, особенно когда проект большой и предупреждения говорят о вещах, которые действительно нельзя изменить. Поэтому, я считаю, что знать о разных способах сделать свой код лучше и надёжнее -- полезно, и ручная компиляция в терминале -- один из них. Конечно, это не значит, что всем нужно обязательно закрыть среду разработки и с этих пор работать только в терминале. Мои слова означают только то, что к написанному Вами коду нужно относиться внимательно и придирчиво его перепроверять. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-12 & Обобщённые методы похожи на обобщённые классы, но параметры типа относятся к методу, а не к классу. Допустимо делать обобщёнными статические и нестатические методы, а также конструкторы \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-13 & Синтаксис обобщённого метода включает параметры типа внутри угловых скобок, которые указываются перед возвращаемым типом. На слайде видим метод, который работает с созданной нами коробкой. Здесь, также, как и при описании класса необходимо указать Т в угловых скобках с единственной целью -- показать компилятору, что Т, которые он найдёт далее в параметрах и теле метода -- это не класс Т, а обобщённый параметр. Если Т перед возвращаемым значением не написать -- будет ошибка компиляции, и подсказка, говорящая о невозможности найти класс Т. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-14 & Пусть вас это не сбивает с толку, мы вроде бы только что начали говорить про обобщённые метода, но чтобы продолжить я думаю, нам нужно немного синхронизироваться, по традиции, я прошу вас ответить на несколько вопросов. Обобщения это способ описания или можно ещё сказать создания общих 1. классов для разных пакетов; 2. алгоритмов для разных типов данных; 3. библиотек для разных приложений.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
... 30сек ...
Конечно же это способ создания единообразно хранимых и используемых алгоритмов для разных типов данных, только что мы описывали алгоритм хранения всего чего угодно в коробке.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Что именно значит буква в угловых скобках? 1. название обобщённого класса 2. имя класса, с которым будет работать обобщение 3. название параметра, используемого в обобщении
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
... 30сек ...
Верный ответ третий, никакое это не имя класса и не название обобщения, это, можно сказать, заполнитель, плейсхолдер, который будет использоваться в обобщении для того, чтобы вместо него подставилось имя класса, переданное в аргументе.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Возможно ли передать обобщённым аргументом примитивный тип? 1. да 2. нет 3. только строку
... 30сек ...
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
очевидно, третий вариант отпадает, поскольку это оксюморон, а верный вариант ответа -- нет. нельзя передавать в угловых скобках примитивы. Многие причины, по которым этого нельзя делать, станут очевидны из дальнейшего повествования, не переключайтесь. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-15 & Передавать один или несколько типов данных через запятую -- это интересно и забавно, но что если нам нужно, чтобы в обобщённом аргументе было возможно использовать только строго определённые типы данных? О, я понимаю, как это звучит, я только что развязал вам руки и сказал что вы не особенно то ограничены строгостью типов, и сразу же говорю, что надо что-то там ограничивать. Конечно надо! В качестве простого примера можно взять нашу пресловутую коробку и сказать, что теперь она может не только хранить в себе какие-то данные, но и, скажем, складывать их. И если со сложением чисел и конкатенацией строк проблем не должно быть, то как складывать котиков или потоки ввода-вывода -- понятно становится не сразу. Применение bounded type parameters позволяет более точно задавать ограничения на используемые типы данных, что помогает писать более безопасный и читаемый код. Вместо того, чтобы допускать все возможные типы, которые могут быть переданы в параметры метода или класса, можно ограничиться только теми, которые соответствуют определенным требованиям. Именно поэтому мы говорим, что в некоторых случаях имеет смысл ограничить типы, которые можно использовать в качестве аргументов в параметризованных типах.
% 2
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Например, в коробке предлагается сделать так, чтобы в ней хранились только числа и прочие интересные наследники класса Number. Подобное ограничение можно сделать с помощью ограниченного параметра типа (bounded type parameters). Для тех, кому лень смотреть в англо-русский словарь, напомню, что намбэ -- это и есть число. Разве что ограничение переведено более неоднозначно, параметр типа у нас получается связанным. видимо, по рукам и ногам, не меньше.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Чтобы объявить ограниченный сверху параметр типа нужно после имени параметра указать ключевое слово extends, а затем указать верхнюю границу (upper bound), которой в данном примере является класс Number. В случае, который можно наблюдать на слайде, в коробку можно положить всё, что намбэ и всё, что так или иначе может быть сведено к нему, то есть его самого и все его частные случаи.
% 3
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Опять же, написанное достаточно точно и легко переводится с английского на русский -- коробка, в которую можно положить данные какого-то типа, являющиеся числом или расширяющего возможности числа.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
В примере, который мы рассматриваем, типы, которые можно использовать в параметризованных классах Box, ограничены наследниками класса Number. Если попытаться указать, например, Box<String>, то возникнет ошибка компиляции. Аналогичным образом можно создавать обобщённые методы с ограничением, тогда компилятор, кажется, вообще хочет сказать, в каком мире строка это наследник числа? Также возможно задать несколько границ. при этом, важно помнить, что здесь как и в объявлении классов, может быть только один класс-наследник и он указывается первым, все остальные границы могут быть только интерфейсами и указываются через знак амерсанд, то есть возможно указать что наследование будет от классов и интерфейсов, например, <N extends Number \& Serializable \& Clonable \& Comparable> и т д. Непривычно то, что несмотря на то что Number или как на слайде -- птица -- это класс, а Serializable или на слайде Man и Animal это интерфейс -- мы всё равно пишем extends (спасибо уважаемые разработчики джавы за отсутствие путаницы, да). само собой, на всякий случай, повторю ещё раз, если унаследоваться в угловых скобках от двух классов, а не от интерфейсов -- будет ошибка. Множественное наследование запрещено во всей джаве без исключений. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-16 & Как мы уже успели неоднократно убедиться, возможно присвоить объекту одного типа объект другого типа, если эти типы совместимы. Например, можно присвоить объект типа Integer переменной типа Object, так как Object является одним из супертипов Integer. В объектно-ориентированной терминологии это называется связью «является» (“is a”). Собственно можно просто перевести словосочетание «целое число является объектом» с русского на английский и получить корректное объектно-ориентированное утверждение -- integer is an object.
% 02
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Так как Integer является Object-ом, то такое присвоение разрешено. Но Integer также является и Number, поэтому код, в котором инты или даблы передаются в качестве аргументов метода тоже корректен. Такое поведение, как было показано несколькими слайдами выше, верно для обобщений.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
% 03
То есть, ещё раз, предположим, что существует метод, описанный без каких-то вайлдкардов, но с дженерик параметром. Какой тип аргумента он будет принимать? Если посмотреть на сигнатуру, то можно увидеть, что он принимает один аргумент с типом GBox<Number>. Получается, что метод очень хороший. так ли это? Можно ли передать Box<Integer> или Box<Double> , как можно можно было бы ожидать? Нет, нельзя, так как Box<Integer> и Box<Double> не являются потомками Box<Number> всё ломается... странно. почему это выглядит так странно? Это частое недопонимание принципов работы обобщений, и это важно знать. Наследование не работает в дженериках так, как оно работает в обычной джаве. Коробка с Number не может в себе хранить коробку с Integer. Дженерик нас защищает от того, чтобы мы попытались положить в коробку строк - не строку. То есть мы, допустим, кладём туда инт, как наследника Number, а потом бац и внезапно флоут. Получится путаница. так что именно таким образом наследоваться не получится.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Из этого можем сделать вывод что если мы нашему методу дадим коробку из интов -- он не будет работать. Поэтому в аргументе надо указать что это будет любой тип, но наследник Number, то есть использовать маску <вопросительный знак extends Number>. Ограничивать маску можно как сверху так и снизу, ограничение сверху это extends - то есть класс и его наследники, или <? super Number> - то есть класс и его родители (в случае с намбером только обжект). таким образом, дженерики защищают нас и самих себя от путаницы и не дают складывать в одни и те же контейнеры разные типы данных. То есть внутри метода мы не можем положить в коробку конкретный тип, допустим, флоут. потому что дженерик думает «а что если у меня там инты будут, у меня ж тогда всё сломается?» и на всякий случай запрещает.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Поскольку коробку с чем то ещё кроме Number и его наследников мы создавать не можем, маскИрование при вызове метода будет избыточно, и можно просто указать private static void boxTest(GBox n) без параметра в угловых скобках. Маски обычно нужны когда мы хотим поставить какое то ограничение. в этой ситуации мы уже ограничили коробки числами при создании коробок.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
%% 04
Что мы в результате видим: мы вроде как создаём псевдо-динамическую типизацию... на самом деле никаких дженериков и уж тем более переменных типов данных в джаве не существует. Всё что мы видим это так называемый синтаксический сахар. То есть, когда вы используете дженерики, во время компиляции произойдёт так называемое стирание, и все эти обобщённые типы данных преобразуются в обжекты и соответствующие проверки и приведения типов. Зачем нам это знать? На будущее весьма пригодится, потому что если мы возьмём у нашего дженерика getClass.getName() - он будет просто типом, информация из треугольных скобок удалится и её не будет. То есть, ещё раз, сама по себе Java не знает ни о каких дженериках. Ну и из этого логично следует, что однажды указав что-то в треугольных скобках, мы не сможем это дело поменять на какой то другой тип данных \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-17 & Я только что упомянул, что ограничивать типы можно как сверху, так и снизу. Что ж, никто меня за язык не тянул, надо придумать какой-нибудь хороший пример, где без таких ограничений всё будет печально ломаться. Поговорим на примере дженерик методов. Зачем в принципе нужны такие методы? когда нужно объединить несколько похожих, но всё же разных типов данных. Как вы думаете, вот мы подаём на вход два типа - инт и флоат. какой будет выбран в итоге для работы внутри метода? *правильно, ближайший старший для них обоих - Number*. и виртуальная машина не возьмёт объект потому что объект не является наследником Number'a. как думаете, зачем такие сложности? ведь можно просто подать на вход два Number'a и не выдумывать.
% 02
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
С ходу сложно придумать какой то пример, но давайте попробуем решить такую задачу: даны два списка - один с интами другой с намберами, и нам очень захотелось написать метод, который будет перекидывать из одного списка числа в другой. опишем для этого несложный метод, пока что не обращайте внимание на наличие тут коллекции, она нужна чтобы не описывать скучные методы хранения, добавления и прочие. есть два сценария - копировать намберы в инты и инты в намберы, как думаете, оба ли эти варианта окажутся рабочими, и если не оба, то какой будет правильным?
% 03
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Классно, если сразу быстро догадались, правильный рабочий вариант -- второй, копировать более точный тип в более общий. заодно, оцените, насколько хороший получился метод, как вы считаете? запускаем с копированием интов в намберы и наоборот. вроде всё хорошо и восьмая джава нас простит, приведя такие типы, но мы всё же попробуем её сломать.
% 04
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
опишем два класса: естественно, животное и наследник животного кот, какие вообще можно было написать другие классы непонятно. И создадим обобщённые списки на их основе. далее воспользуемся уже написанным методом копирования списков, мы же пока что предполагаем, что он хороший, надо бы в этом удостовериться. Давайте немного подольше посмотрим на этот код, он не сложный, в нём почти нет пугающих дженериков, всё должно быть хорошо и понятно, несложный метод, кот, животное, два списка, копирование, вывод на экран
% 05
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Запустив этот код мы всё равно видим что списки соединились, в списке котов появилось животное... ну что же, джава, раз ты такая умная...
% 06
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
то коту мы добавляем метод голос, опять же ничего сложного, просто метод в котором кот будет мяукать и раз уж джава утверждает что получившийся в результате слияния список отличный - вызовем у одного из товарищей из списка метод голос. Видим, что на этапе компиляции всё хорошо и джава не видит никаких проблем, запускаем код
% 07
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
а вот и проблемка, да? при попытке вызвать у более общего животного метод более частного кота у нас исключение невозможности приведения типов. оказывается энимал не так уж и легко может находиться в одном списке с котами, а значит, к нам на помощь спешат кто? верно, привет дженерики.
% 08
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Мы вынуждены сказать, что метод будет работать только с каким-то одним типом <T> и списки будут <T> и в форе тоже будем перебирать Т. Напоминаю, что если не указать Т в объявлении метода то джава не поймёт что это обобщённый метод и будет искать какой то тип Т описанный в библиотеках и естественно его не найдёт. но тогда получается что всё, что мы можем -- это в список котов класть котов, а в список животных класть животных. Вернулись к тому с чего начали? но кот же наследник животного почему бы не класть в список животных котов? верно, значит нам надо сказать методу, что источником может быть кто то, кто тип или его наследники, а приёмником Тип или его родители, и пусть метод, виртуальная машина и другие механизмы уже сам разбираются, кто подходит под эти параметры. Соответственно, в методе мейн можно заметить, что копировать животных в котов не получится. Теперь неверные варианты будут отсекаться на этапе компиляции, и джава просто не даст нам написать как то не так. Честно говоря, не думаю, что это кому то может прям сильно пригодиться в ежедневной работе, а загуглить по мере возникновения задачи вроде бы несложно, но всё же знать в какую сторону гуглить -- было бы неплохо. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-18 & Выведение типов -- это возможность компилятора Java автоматически определять аргументы типа на основе контекста, чтобы вызов получился возможным. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-19 & Алгоритм выведения типов определяет типы аргументов, а также, если это применимо, тип, в который присваивается результат или в котором возвращается результат. Далее алгоритм пытается найти наиболее конкретный тип, который работает со всеми аргументами и подходит к данной ситуации. Это достаточно несложная автоматизация, о которой особенно задумываться не нужно, потому что это как раз механизм, помогающий программисту не думать. Допустим, мы добавили к коту реализацию интерфейса Serializable. и попробуем сделать что-то странное -- написать метод, который будет работать с одним и только одним типом данных Т. Как видим, никакие аргументы не связаны наследованием. В таком методе выведение типов определяет, что вторые аргументы метода пик, а именно Cat и ArrayList, передаваемые в методы pick имеют тип Сериалайзабл, но этого недостаточно, потому что первый аргумент тоже должен быть того же типа. Внезапно оказывается, что строка -- это тоже сериалайзабл и внутри метода наименьший общий делитель (в смысле наиточнейший общий тип) будет определён не как обжект, а как сериализуемое. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-20 & Коротко проговорим некоторые другие варианты выведения типов, они все примерно про одно и тоже, так что сложно быть не должно
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Выведение типов и обобщённые методы -- В описании обобщённых методов я уже говорил о выведении типов, которое делает возможным вызов обобщённого метода так, будто это обычный метод, без указания типа в угловых скобках. Можно долго и скучно писать очередной пример, смысл которого сведётся к тому, что вы видите на экране, есть коты, есть коробки, есть метод, который кладёт котов в коробки. Очевидно, что обобщённый метод addBox объявляет один параметр типа U. В большинстве случаев компилятор Java может вывести параметры типа вызова обобщённого метода, в результате чаще всего вовсе не обязательно их указывать. Например, чтобы вызвать обобщённый метод addBox, можно указать параметры типа как это указано на второй строке либо можно вовсе опустить их, и тогда компилятор Java автоматически выведет тип Cat из аргументов метода при вызове как на третьей строке.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Выведение типов и создание экземпляра обобщённого класса. Можно заменить аргументы типа, необходимые для вызова конструктора обобщённого класса пустым множеством параметров типа (пустые треугольные скобки, также называемые, напомню, бриллиантовой операцией), так как компилятор может вывести аргументы типа из контекста. Можно сказать, что это довольно привычная ситуация, потому что все примеры создания объектов, которые мы рассматриваем, включают в себя выведение типа конструктора и бриллиантовую операцию.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Выведение типа и обобщённые конструкторы обобщённых и необобщённых классов. Очевидно, что конструкторы могут быть обобщёнными как в обобщённых, так и в необобщённых классах. Вот, например, в этой ситуации мы можем явно указать что у коробки будет обобщённый аргумент Кот, а у конструктора -- вообще какой-то другой аргумент, например, строка или число. Компилятор выведет тип String для этого формального параметра U, так как фактически переданный аргумент является экземпляром класса String. Опять же, в джава 1.7 и выше предпочтительно явно использовать бриллиантовую операцию. Важно запомнить, что алгоритм выведения типа использует только аргументы вызова, целевые типы и возможно очевидный ожидаемый возвращаемый тип для выведения типов. Алгоритм выведения не использует последующий код программы. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-21 & Целевые типы. Целевой тип выражения -- это тип данных, который компилятор Java ожидает в зависимости от того, в каком месте находится выражение. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-22 & Компилятор Java пользуется целевыми типами для вывода параметров типа вызова обобщённого метода. Выражаясь проще -- ещё на шажок приближает нас к динамической типизации, позволяя не особенно думать, к чему приводить типы в методах дженерик классов. Например, как в методе Box.emptyBox() со сто пятнадцатой строки. В методе мейн на 121й строке инициализация ожидает экземпляр Box<String>. Этот тип данных является целевым типом. Поскольку метод emptyBox возвращает значение обобщённого типа Box<T>, компилятор сам сообразит, что аргумент T будет типом String. Это работает как в Java 7, так и в более поздних версиях. Конечно, можно указывать аргумент типа напрямую, но в данном случае в этом нет необходимости. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-23 & Перед тем, как продолжить, предлагаю немного расслабиться, припомнить о чём уже успели поговорить и ответить буквально на два несложных вопроса. Что из перечисленного является недопустимым? Слишком плохо будут восприниматься на слух варианты ответов, поэтому скажу, чем они отличаются -- в идентификаторе тип с ограничением по намбэ, в конструкторе намбэ, интеджер и стринг и есть хороший четвёртый вариант -- всё допустимо
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
... 30 сек...
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Правильный ответ: 3. Первое допустимо по очевидной причине, число -- это точно число. Целое число является дочерним классом числа, поэтому (2) допустимо. Но String не является дочерним классом Number, поэтому (3) недопустимо.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Хорошо, предположим ситуацию немного сложнее, параметры метода ограничены с одной стороны для источника и с другой стороны для назначения, вызов метода вы также можете видеть на экране. Какой тип данных будет взят в качестве Т в параметре метода? Animal Cat или Object
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
... 30 сек...
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
как и было сказано ранее -- будет подобран такой тип, который будет подходить под эти ограничения, а поскольку никто не может быть супертипом объекта, а кот -- это не супертип животного, очевидно, в качестве Т будет взят энимал. Эта замечательная информация пригодится нам для следующей темы.
\\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-24 & Есть ощущение, что я недостаточно подробно рассказал про подстановочные символы. Со всей ответственностью, помощью всех источников интернета, Герберта Шилдта и личного опыта -- исправляюсь. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-25 & В обобщённом коде знак вопроса, называемый подстановочным символом, означает неизвестный тип. Подстановочный символ может использоваться в разных ситуациях: как параметр типа, поля, локальной переменной, иногда в качестве возвращаемого типа. Подстановочный символ никогда не используется в качестве аргумента типа для вызова обобщённого метода, создания экземпляра обобщённого класса или супертипа. А вот подробнее о том, как можно его использовать -- сейчас и поговорим. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-26 & Я уже упоминал, что можно ограничить передаваемый тип сверху, чтобы уточнить и ограничить тип. Можно использовать подстановочный символ, ограниченный сверху, чтобы ослабить ограничения переменной. Например, если необходимо написать метод, который работает с коробками Integer-ов, коробками Double-ов и коробками Number-ов, этого можно достичь с помощью ограниченного сверху подстановочного символа. Чтобы объявить ограниченный сверху подстановочный символ, нужно воспользоваться символом вопроса "?", с последующим ключевым словом extends, с последующим ограничением сверху. Не лишним будет дополнительно проговорить, что в этом контексте extends означает как расширение класса, так и реализацию интерфейса.
% 02
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Чтобы написать метод, который работает с коробками, в которых Number и дочерними типами от Number, например Integer, Double и Float, можно указать Box<? extends Number>. В то время, как Box<Number> ввёл бы более жёсткое ограничение, чем Box<? extends Number>, потому что конструкция Box<Number> соответствует только коробкам типа Number, в то время, как если это будет просто аргумент, а не дженерик аргумент -- никаких проблем с наследованием не будет, программирование не сломалось, а Box<? extends Number> соответствует коробкам типа Number и коробкам всех подклассов намбэ.
% 03
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Картинки -- это конечно интересно и всё такое, но надо бы и в коде дополнительно рассмотреть. Будем опять мучить традиционный Animal, у которого есть поле name и переопределённый метод toString() для вывода информации, также сразу добавим котика с пёсиком, прямых наследников животного.
% 04
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Подготовим коробочку и соответствующий метод, выводящий информацию о её содержимом. Внутри метода сделаем ограниченный сверху подстановочный символ <? extends Animal>, где вместо Animal-ом может быть любой тип, а значит эта подстановка будет соответствовать Animal и любому подтипу Animal. Метод printInfo может выводить информацию о коробке, внутри которой мы будем выводить информацию об объекте, содержащемся в ней. А объект, как мы помним в нашем случае является наследником Animal, включая сам класс Animal.
% 05
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Осталось только создать котика, пёсика, аккуратно положить их в коробки и вывести информацию о коробках нашим удобным методом. Видим, как великолепно дженерики справляются со своей задачей. А как мы помним со слайов чуть ранее, если бы мы просто запросили в параметре Животное, передать кота не получилось бы. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-27 & Если просто использовать подстановочный символ, то получится подстановочный символ без ограничений. Box<?> означает коробку с неизвестным содержимым (неизвестным типом). Казалось бы, зачем такой синтаксис существует? Во первых, потому что если Вы используете обобщённые типы, Вы можете продолжать их использовать, не особенно заботясь о том, что Вам в данной конкретной ситуации не нужно использовать обобщённую функциональность. Отсюда можно сделать вывод о том, что неограниченный подстановочный символ полезен в двух случаях: Если нужен метод, который может быть реализован с помощью функциональности класса Object. Когда код использует методы обобщённого класса, которые не зависят от параметра типа. В программах, использующих АПИ рефлексии языка конструкция тип Класс с подстановочным символом без ограничений используется чаще других конструкций, потому что большинство методов объекта Класс не зависят от расположенного внутри Т. Но более подробно о классе Класс позже, на лекциях по рефлексии.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
пролистать два слайда назад
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Можно обратить внимание, что, например, метод с предыдущего слайда, принтинфо, не использует никаких методов животного, Цель метода printInfo — вывод в консоль информации об объекте в коробке любого типа
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
пролистать обратно три слайда вперёд
% 02
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
поэтому в параметре метода можно заменить коробку с наследниками животного на коробку с чем угодно, потому что всё равно там будет использоваться только метод коробки туСтринг. В то же время не лишним будет напомнить, что нельзя указать в обобщённом аргументе Обжект, потому что это будет означать именно обжект, а не обжект и его наследников, как это бывает в обычной программе. Важно запомнить, что Box с Object в треугольных скобках и Box с вопросительным знаком в треугольных скобках -- это НЕ одно и то же. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-28 & Ограниченный снизу подстановочный символ ограничивает неизвестный тип так, чтобы он был либо указанным типом, либо одним из его предков. Более наглядно -- на слайде, более понятно уже не получится -- либо сам тип либо его предки по иерархии наверх вплоть до обжекта. В целом, в обобщённых конструкциях можно указать либо только верхнюю границу для подстановочного символа, либо только нижнюю, но также можно указать оба ограничения сразу, хотя для этого варианта придётся продумать довольно нетривиальную стратегию передачи параметра, потому что напрямую сделать это как показано на слайде -- не выйдет.
% 02
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
То есть, если написать тот же самый метод принтИнфо, с параметром коробка и обобщённым аргументом не экстендс, а супер животное, то этот код также не будет работать, потому что метод будет ожидать не животное и наследников, а животное и родителей, то есть обжект. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-29 &
Обобщённые классы или интерфейсы связаны не только из-за связи между их типами. Однако можно использовать подстановочные символы (wildcards) для создания связи между обобщёнными классами и интерфейсами. С обычными необобщёнными классами Кота и Животного имеет смысл писать какой-нибудь простой незатейливый код, который мы уже десятки, если не сотни раз видели и писали сами. Этот код показывает, что наследование работает по правилу подчинённых типов: класс Cat является подклассом класса Animal, и расширяет его.
% 02
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
И правило не работает для обобщённых типов. Но, если Cat является дочерним типом для Animal, то какая связь между Коробкой с <Cat> и Коробкой с <Animal>? Несмотря на то, что Cat является подтипом Animal, Box<Cat> не является подтипом Box<Animal>. Это разные типы. Общим предком для Box<Animal> и Box<Cat> является Box с подстановочным символом <?>.
% 03
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Для того чтобы создать такую, хорошо визуализируемую связь между этими классами коробок, чтобы код мог иметь доступ к методам Animal через Box<Cat>, как раз и используется он, подстановочный символ. Так как Cat является дочерним типом от Animal, и животное в коробке является коробкой, в которой тип Animal, теперь существует связь между котом в коробке, точнее, коробкой с объектом типа Cat) и коробкой с животным. \\ \hline
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
08-30 & В некоторых случаях компилятор может вывести тип подстановочного символа. Коробка может быть определена как коробка с неограниченным подстановочным символом, но при вычислении выражения компилятор выведет конкретный тип из кода, такой сценарий называется захватом подстановочного символа. Что это нам даёт? То, что в большинстве случаев нет нужды беспокоиться о захвате подстановочного символа, кроме случаев, когда в сообщении об ошибке появляется фраза “capture of”.
2023-03-12 23:05:12 +03:00
Следующий код выводит сообщение об ошибке, связанное с захватом подстановочного символа, при компиляции:
static void testError(Box<?> box){
box.setValue(box.getValue()); // capture of ?
}
Вывод:
2023-05-09 23:34:17 +03:00
%java: incompatible types: int cannot be converted to capture#1 of ?
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
В этом примере компилятор обрабатываем параметр box как тип Object. Когда метод testError вызывает box.setValue(box.getValue()), компилятор не может подтвердить тип объекта, который будет класться в коробку, и генерирует ошибку. Когда возникает этот тип ошибки, это обычно означает, что компилятор верит, что вы присваиваете неправильный тип переменной. Обобщения были добавлены в Java именно для этого — чтобы усилить безопасность типов во время компиляции.
Код пытается выполнить безопасную операцию, тогда как можно обойти ошибку компиляции? Это возможно исправить написав приватный вспомогательный метод (private helper method), который захватывает подстановочный символ. В этом случае можно обойти проблему с помощью создания приватного вспомогательного метода testErrorHelper():
2023-03-12 23:05:12 +03:00
private static <T> void testErrorHelper(Box<T> box){
box.setValue(box.getValue()); // OK
}
static void testError(Box<?> box){
testErrorHelper(box);
}
2023-05-09 23:34:17 +03:00
%Благодаря вспомогательному методу компилятор использует выведение типа для определения, что T является CAP#1 (захваченная переменная в вызове). Пример теперь успешно компилируется.
По соглашению вспомогательные методы обычно называются как "originalMethodNameHelper".
2023-03-12 23:05:12 +03:00
Руководство по использованию подстановочного символа
Когда использовать ограниченный сверху подстановочный символ (wildcard), и когда использовать ограниченный снизу подстановочный символ, - определить зачастую бывает довольно сложно. Здесь собраны советы по выбору необходимого ограничения для подстановочного символа.
В этом обсуждении будет полезно думать о переменных, будто они представляют две функции:
2023-05-09 23:34:17 +03:00
Входная переменная. Предоставляет данные для кода. Для метода copy(src, dst) параметр src предоставляет данные для копирования, поэтому он считается входной переменной.
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
Выходная переменная. Содержит данные для использования в другом месте. В примере с copy(src, dst) параметр dst принимает данные и является выходной переменной.
2023-03-12 23:05:12 +03:00
Некоторые переменную могут быть одновременно входными и выходными, такой случай тоже здесь рассматривается.
Руководство:
2023-05-09 23:34:17 +03:00
Входная переменная определяется с ограниченным сверху подстановочным символом, используя ключевое слово extends.
Выходная переменная определяется с ограниченным снизу подстановочным символом, используя ключевое слово super.
2023-03-12 23:05:12 +03:00
Если к входной переменной можно обращаться только используя методы класса Object, использовать стоит неограниченный подстановочный символ.
Если переменная должна использоваться как входная и как выходная одновременно, то НЕ стоит использовать подстановочный символ.
2023-05-09 23:34:17 +03:00
Это руководство не охватывает использование подстановочных символов в возвращаемых из методов типах. Не стоит использовать подстановочные символы в возвращаемых типах, потому что это будет принуждать других программистов разбираться с подстановочными символами. \\ \hline
08-31 & \\ \hline
08-32 & \\ \hline
08-33 & \\ \hline
08-34 & \\ \hline
08-35 & \\ \hline
08-36 & \\ \hline
08-37 & \\ \hline
08-38 & \\ \hline
08-39 & \\ \hline
\end{longtable}
\end{document}
2023-03-12 23:05:12 +03:00
Стирание типа (Type Erasure)
Обобщения были введены в язык программирования Java для обеспечения более жёсткого контроля типов во время компиляции и для поддержки обобщённого программирования. Для реализации обобщения компилятор Java применяет стирание типа (type erasure) к:
Заменяет все параметры типа в обобщённых типах их границами или Object-ами, если параметры типа не ограничены. Сгенерированный байткод содержит только обычные классы, интерфейсы и методы.
Вставляет приведение типов где необходимо, чтобы сохранить безопасность типа.
Генерирует связующие методы, чтобы сохранить полиморфизм в расширенных (extended, наследующиеся от других) обобщённых типах.
Стирание типа обеспечивает, что никакие новые классы не создаются для параметризованных типов, следовательно обобщения не приводят к накладным расходам во время выполнения.
Стирание типа в обобщённых типах
Во время процесса стирания типов компилятор Java стирает все параметры типа и заменяет каждый его ограничением, если параметр типа ограничен, либо Object-ом, если параметр типа неограничен.
Следующий обобщённый класс, который представляет коробку:
public class Box<T> {
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
private T value;
@Override
public String toString() {
return value.toString() + " in box";
}
}
Так как параметр T неограничен, то компилятор заменяет его Object-ом:
public class Box {
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
private Object value;
@Override
public String toString() {
return value.toString() + " in box";
}
}
В следующем примере обобщённый класс Box использует ограниченный параметр:
public class Box<T extends Animal> {
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
private T value;
@Override
public String toString() {
return value.toString() + " in box";
}
}
Компилятор Java заменяет ограниченный параметр T первой границей Animal:
public class Box {
public Animal getValue() {
return value;
}
public void setValue(Animal value) {
this.value = value;
}
private Animal value;
@Override
public String toString() {
return value.toString() + " in box";
}
}
Стирание типа в обобщённых методах
Компилятор Java также стирает параметры типа обобщённых методов. Следующий обобщённый метод:
static <T> void setIfNull(Box<T> box, T t) {
if (box.getValue() == null) {
box.setValue(t);
}
}
Так как T неограничен, то компилятор Java заменяет его на Object :
static void setIfNull(Box<Object> box, Object t) {
if (box.getValue() == null) {
box.setValue(t);
}
}
Даны ранее рассмотренные три класса:
public class Animal { /* ... */ }
public class Dog extends Animal { /* ... */ }
public class Cat extends Animal { /* ... */ }
Обновленный метод setIfNull:
static <T extends Animal> void setIfNull(Box<T> box, T t) { /* ... */}
Компилятор Java заменит T на Animal :
static void setIfNull(Box<Animal> box, Animal t) { /* ... */}
2023-05-09 23:34:17 +03:00
В разделе стирания типов обсуждается процесс, где компилятор удаляет информацию, связанную с параметрами типа и аргументами типа. Стирание типа имеет последствия, связанные с произвольным количеством параметров (varargs).
Материализуемые типы (reifiable types) — это типы, информация о которых полностью доступа во время выполнения: примитивы, необобщённые типы, сырые типы, обращения к неограниченным подстановочным символам.
Нематериализуемые типы (Non-reifiable types) — это типы, информация о которых удаляется во время компиляции стиранием типов: обращения к обобщённым типам, которые не объявлены с помощью неограниченных подстановочных символов. Во время выполнения о нематериализуемых типах (Non-reifiable types) нет всей информации. Примеры нематериализуемых типов: Box<String> и Box<Number>. Виртуальная машина Java не может узнать разницу между ними во время выполнения. В некоторых ситуациях нематериализуемые типы не могут использоваться, например, в выражениях instanceof или в качестве элементов массива.
2023-03-12 23:05:12 +03:00
Загрязнение кучи (Heap pollution)
2023-05-09 23:34:17 +03:00
Загрязнение кучи (heap pollution) возникает, когда переменная параметризованного типа ссылается на объект, который не является параметризованным типом. Такая ситуация возникает, если программа выполнила некоторую операцию, которая генерирует предупреждение unchecked warning во время компиляции. Предупреждение unchecked warning генерируется, если правильность операции, в которую вовлечён параметризованный тип (например приведение типа или вызов метода) не может быть проверена. Например, загрязнение кучи возникает при смешивании сырых типов и параметризованных типов, или при осуществлении непроверяемых преобразований типа.
2023-03-12 23:05:12 +03:00
В обычных ситуациях, когда код компилируется в одно и то же время, компилятор генерирует unchecked warning, чтобы привлечь внимание к загрязнению кучи. Если компилируются различные части кода отдельно, то становится трудно определить потенциальную угрозу загрязнения кучи. Если обеспечить компиляцию кода без предупреждений, то загрязнение кучи (heap pollution) не сможет произойти.
Ограничения обобщений
Нельзя создавать экземпляры обобщённых типов с примитивными типами в качестве аргументов типа.
Нельзя создавать экземпляры параметров типа
Нельзя создать экземпляр параметра типа. Например, следующий код приведёт к ошибке компиляции:
static <T> void add(Box<T> box) {
T t = new T(); // compile-time error
// ...
box.setValue(t);
}
В качестве обходного пути можно создать объект параметра типа с помощью отражения (reflection - будет пройдено позднее):
static <T> void add(Box<T> box, Class<T> cls) throws Exception {
T t = cls.newInstance(); // OK
// ...
box.setValue(t);
}
Вызвать метод add() можно вот так:
public static void main( String[] args ) throws Exception {
Box<String> box = new Box<>();
add(box, String.class);
}
Нельзя объявлять статические поля с типом параметра типа
Статические поля класса являются общими для всех объектов этого класса, поэтому статические поля с типом параметра типа запрещены. Следующий класс:
public class Box<T> {
private static T value;
// ...
}
Если бы статические поля с типом параметра типа были бы разрешены, то следующий код сбивал бы с толку:
Box<Animal> animalInBox = new Box<>();
Box<Cat> catInBox = new Box<>();
Box<Dog> dogInBox = new Box<>();
2023-05-09 23:34:17 +03:00
Так как статическое поле value является общим для animalInBox, catInBox и dogInBox, то какого типа value? Оно не может быть Animal, Cat и Dog в одно и то же время, поэтому нельзя создавать статические поля с типом параметра типа. Обычно нельзя использовать приведение типа к параметризованному типу, если он не использует неограниченный подстановочный символ.
2023-03-12 23:05:12 +03:00
Нельзя использовать приведения типа или instanceof с параметризованными типами
2023-05-09 23:34:17 +03:00
Так как компилятор Java стирает все параметры типа из обобщённого кода, то нельзя проверить во время выполнения, какой параметризованный тип используется для обобщённого типа. Во время выполнения нет параметров типа, поэтому нет возможности различить Box<Integer> и Box<Animal>. Наибольшее, что можно сделать — это использовать подстановочный символ для проверки. Например:
2023-03-12 23:05:12 +03:00
Box<Integer> box1 = new Box<>();
Box<Number> box2 = (Box<Number>) box1;
Однако в некоторых случаях компилятор знает, что параметр типа всегда верный и позволяет использовать приведение типа.
Невозможно создавать массивы параметризованных типов
Нельзя создавать массивы параметризованных типов. Например, следующий код не будет компилироваться:
Box<Integer>[] box1 = new Box<Integer>()[20];
Следующий код показывает, что случится при вставке различных типов в массив:
Object[] strings = new String[2];
strings[0] = "hello"; // OK
strings[1] = 100; // ArrayStoreException
А если попробовать то же самое с обобщённой коробкой, то будет такая проблема:
public static void main( String[] args ) throws Exception {
Object[] stringLists = new Box<String>[]; // compilation error, but let's say it's possible
stringLists[0] = new Box<String>(); // OK
stringLists[1] = new Box<Integer>(); // there should be an ArrayStoreException exception, but the runtime cannot notice it.
}
2023-05-09 23:34:17 +03:00
Если бы массивы с параметризованными типами были бы разрешены, то предыдущий код не смог бы бросить исключение ArrayStoreException.
2023-03-12 23:05:12 +03:00
Нельзя создавать, ловить (catch) или бросать (throw) объекты параметризованных типов
2023-05-09 23:34:17 +03:00
Обобщённый класс не может расширять класс Throwable напрямую или не напрямую. Например, следующие классы не компилируются:
2023-03-12 23:05:12 +03:00
// Extends Throwable non-direct
2023-05-09 23:34:17 +03:00
class MathException<T> extends Exception { /* ... */ } // compilation error
2023-03-12 23:05:12 +03:00
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compilation error
Метод не может ловить (catch) экземпляр параметра типа:
public static <T extends Exception, J> void execute(Box<J> box) {
try {
J j = box.getValue();
// ...
} catch (T e) { // compilation error
// ...
}
}
2023-05-09 23:34:17 +03:00
Однако можно использовать параметр типа в throws:
2023-03-12 23:05:12 +03:00
class Parser<T extends Exception> {
2023-05-09 23:34:17 +03:00
public void parse(File file) throws T { // OK
// ...
}
2023-03-12 23:05:12 +03:00
}
Нельзя перегружать метод так, чтобы формальные параметры типа стирались в один и тот же сырой тип
Класс не может иметь два перегруженных метода, которые будут иметь одинаковую сигнатуру после стирания типов:
public class Test {
2023-05-09 23:34:17 +03:00
public void print(Box<String> strBox) { }
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
public void print(Box<Integer> intBox) { }
2023-03-12 23:05:12 +03:00
}
Этот код не будет компилироваться.
ВОПРОСЫ:
Вопрос: Какой из этих параметров типа используется для универсального класса для возврата и приема объектов любого типа?
K
T
N
V
Правильный ответ: 2
Вопрос: Какой из этих типов нельзя использовать для инициализации универсального типа?
Primitive types
Integer class
FLoat class
Правильный ответ: 1
Вопрос: что такое Сырой тип (raw type)?
это имя обобщённого класса или интерфейса без аргументов типа (type arguments).
это необобщённый класс или интерфейс.
первый и второй варианты верны.
Правильный ответ: 1
Например, параметризованный тип создаётся так:
Box<Integer> integerBox = new Box<>();
Если убрать аргументы типа, то будет создан сырой тип:
Box box = new Box();
Поэтому Box  — это сырой тип обобщённого типа Box<T> . Однако необобщённый класс или интерфейс НЕ являются сырыми типами.
2023-05-09 23:34:17 +03:00
To specify both upper and lower bounds for a type parameter using wildcards in Java, you can use the following syntax:
```
Type parameterName <? extends UpperBound & ? super LowerBound>
```
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
For example, if you want to specify a type parameter named "T" that has an upper bound of "Number" and a lower bound of "Integer", you can write:
```
public class MyClass<T extends Number & Comparable<? super Integer>> {
// class implementation here
}
```
2023-03-12 23:05:12 +03:00
2023-05-09 23:34:17 +03:00
This will ensure that the type parameter "T" can only be instantiated with types that are both a subclass of "Number" and implement the "Comparable" interface with an argument that is a superclass of "Integer".