\documentclass[../j-spec.tex]{subfiles} \begin{document} \section{Интерфейсы} \begin{longtable}{|p{35mm}|p{135mm}|} \hline Экран & Слова \\ \hline \endhead Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработчика на джава \\ \hline Отбивка & в сегодняшней лекции коснёмся одного из ключевых понятий программирования на языке джава -- обобщённого программирования. \\ \hline На прошлом уроке & На прошлом уроке мы поговорили об интерфейсах, рассмотрели понятие и принцип работы, поговорили о ключевом слове implements; коротко посмотрели на наследование и множественное наследование интерфейсов, реализацию, значения по умолчанию и частичная реализацию интерфейсов. Буквально два слова я сказал о функциональных интерфейсах и чуть подробнее об анонимных классах. \\ \hline На этом уроке & На этом уроке поговорим более крутом полиморфизме, чем интерфейсы. Казалось бы, куда уж круче, но сегодня на повестке обобщения или если у вас нет отторжения от неизбежных в разработке англицизмов -- дженерики. Сначала попробуем обойтись без них, потом сделаем тоже самое с ними, сравним. Обсудим работу дженерик интерфейсов, Обобщённые методы и подстановочные символы, а именно, ограничения сверху и снизу, дополнительно проговорим о таком явлении как загрязнение кучи. \\ \hline 08-01 & Итак, обобщение. Смысл в программировании примерно такой же, как и в обычной разговорной речи - отсутствие деталей реализации. \\ \hline 08-02 & Может показаться, что мы снова будем как-то всесторонне рассматривать интерфейсы и абстрактные классы, отсутствие реализаций и всё такое, но нет. Обобщения это не про отсутствие реализации, а про создание некоторого общего подхода к реализации одинаковых алгоритмов, но с разными данными. Запутал? Конечно! подойдём с другой стороны. Обобщения -- это некие конструкции, позволяющие работать данными не задумываясь что там именно лежит внутри структуры или метода (строки, числа или коты). То есть обобщения позволяют единообразно работать с разными типами данных. Можно сказать, что обобщения -- это некий поклон в сторону динамической типизации. Попробуем начать разбираться на примере. \\ \hline 08-03 & Как было сказано, обобщения позволяют единообразно работать с разными типами данных, поэтому создадим некоторый класс, который сможет хранить внутри любые данные джава. А любые данные в джава, как мы помним -- это объект. В таком случае необходимо создать класс Box, у которого будет единственное поле типа Object, в которое можно будет записать объект абсолютно любого типа. Далее, для начала, на примере чисел, напишем в метод мейне создадим два экземпляра и положим туда пару чисел. Надо бы например научиться складывать их. Пишем b1.getObj() + b2.getObj(). Как думаете, что там за ошибка скрывается за красным подчёркиванием среды разработки? там предупреждение среды о том, что оператор сложения не определён для объектов. Проблема в том, что я знаю, что тут лежат целые числа, а джава не особо. Чего не хватает? правильно, приведения типов к Integer. %% 2 можем создать пару других коробок со строками, и будет производиться уже не складывание чисел, а конкатенация строк. Проблема в том что при каждом вытаскивании данных из коробки надо делать приведение типов, чтобы указать какие именно там лежат данные. Можно попробовать эту проблему решить если мы например назовём нашу переменную в венгерской нотации, тогда при использовании спустя пятьдесят или сто строк мы будем видеть что переменная вроде как интовая или строковая. %% 3 Кроме постоянных перепроверок и кастов есть вторая проблема, она состоит в том, что мы, естественно, можем положить в первую коробку что угодно, например, строку и вполне ожидаемо получим ClassCastException. Допустим кто-то не понял мысль что ibox это коробка с интом, и хочет положить туда строку. Джава никак не запретит этого потому что строка это точно такой же равноправный объект. Как быть? первое, что приходит на ум - это try-catch но если можно какую-то ситуацию исправить ифом то лучше защититься ифом, конечно, получим примерно такой совсем не громоздкий код, как на слайде. Потому что перед строками с приведением типов желательно делать проверку на эти самые типы. Третья, самая неприятная проблема состоит в том, что все проблемы подобного рода проявляют себя только в рантайме, то есть у конечного пользователя перед глазами, когда мы уже никак это исправить не можем. \\ \hline 08-04 & К чему мы в итоге пришли? к тому что в принципе возможно создавать классы, которые могут работать с любыми типами данных, но при любом обращении к таким классам нам неплохо бы делать достаточно сложные проверки, чтобы каждый раз не вываливаться с исключениями. До появления дженериков в принципе так и делалось. Таким образом мы совершенно логично подошли к дженерикам, которые должны решить часть наших проблем. Обобщения появились в джава 1.5. Дженерики или обобщения -- это особые средства языка Java для реализации обобщённого или можно встретить термин шаблонизированного программирования: особого подхода к описанию данных и алгоритмов, позволяющего работать с различными типами данных без изменения внешнего описания. Обобщения добавляют в Java безопасность типов и делают управление проще. Исключается необходимость применять явные приведения типов, так как благодаря обобщениям все приведения выполняются неявно, в автоматическом режиме, платформой джава. Все дженерики работают только со ссылочными типами данных но это не проблема потому что для всех примитивов есть обёртки в виде ссылочного типа. \\ \hline 08-05 & Чтобы понять почему больше не надо делать явное приведение типов, и откуда берется какая-то безопасность, напишем в треугольных скобках букву . в принципе, там может быть любая буква, или даже нескольно, но принято писать именно 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 Но всё таки можете ставить такие, какие Вам нравятся и даже писать многобуквенные обозначения параметра, но должен предупредить, что это достаточно грубое нарушение стиля написания кода. Линейкой по пальцам никто бить не станет, но и всерьёз рассматривать к себе в команду не будут. \\ \hline 08-07 & \\ \hline 08-08 & \\ \hline 08-09 & \\ \hline 08-10 & \\ \hline 08-11 & \\ \hline 08-12 & \\ \hline 08-13 & \\ \hline 08-14 & \\ \hline 08-15 & \\ \hline 08-16 & \\ \hline 08-17 & \\ \hline 08-18 & \\ \hline 08-19 & \\ \hline \end{longtable} \end{document} Создание экземпляра обобщённого типа и обращение к нему При обращении к обобщённому типу нужно заменить параметры типа на конкретные классы или интерфейсы, например Box: Box integerBox; Можно подумать, что обращение к параметризованному типу похоже на обычный вызов метода, но вместо передачи аргумента в метод передается аргумент типа (type argument), например Integer в класс Box, в данном случае. Параметр типа и аргумент типа — это два разных понятия. Когда вы объявляете обобщённый тип Box, то здесь T является параметром типа. Когда происходит обращение к обобщённому типу, передается аргумент типа, например Integer . Это довольно похоже на различие формальных параметрах и аргументов методов. Как и любое другое объявление переменной Box integerBox НЕ создаёт экземпляр класса Box . Такой код просто объявляет переменную integerBox как Box Integer-ов. Обращение к обобщённому типу обычно называется параметризованным типом (parameterized type). Чтобы создать экземпляр класса, используется ключевое слово new, как обычно, и в дополнение указывается между именем класса и скобками с параметрами конструктора: Box integerBox = new Box(); После создания экземпляра можно обращаться к методам: // set integer. integerBox.setType(10); // get integer Integer intValue = integerBox.getType(); System.out.println(intValue); Вывод: 10 Бриллиантовая операция (Diamond operator) Начиная с Java 7 существует также бриллиантовая операция (diamond operator), которая позволяет указывать пустые аргументы типа <> там, где компилятор может вывести тип из контекста: Box integerBox = new Box<>(); Несколько параметров типа Для обобщенного типа можно объявлять более одного параметра, используя список, разделенный запятыми: public class Box { private K key; private V value; public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } Использовать можно следующим образом: public class App { public static void main( String[] args ) { Box box = new Box<>(); box.setKey(4); box.setValue("cats"); System.out.println("We have " + box.getKey() + " " + box.getValue() + " in box."); } } Вывод: We have 4 cats in box. Параметризованный тип Можно также заменить параметр типа на параметризованный тип, немного изменив класс Box: public class Box { private K key; private V value; public Box(){} public Box(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } Использовать, например, так: public class App { public static void main( String[] args ) { Box> box = new Box<>(); box.setKey(4); box.setValue(new Box<>(2, "cats")); System.out.println("We have " + box.getKey() + " boxes with " + box.getValue().getKey() + " " + box.getValue().getValue() + " in each box."); } } Вывод: We have 4 boxes with 2 cats in each box. Raw Types или сырые типы Сырой тип (raw type) — это имя обобщённого класса или интерфейса без аргументов типа (type arguments). Например, параметризованный тип создаётся так: Box integerBox = new Box<>(); Если убрать аргументы типа, то будет создан сырой тип: Box box = new Box(); Поэтому Box  — это сырой тип обобщённого типа Box . Однако необобщённый класс или интерфейс НЕ являются сырыми типами. Можно часто увидеть использование сырых типов в старом коде, поскольку многие классы, например коллекции, до Java 5 были необобщёнными. Когда используются сырые типы, по сути получается то же самое поведение, которое было до введения обобщений в Java. Для совместимости со старым кодом допустимо присваивать параметризованный тип своему собственному сырому типу: Box integerBox = new Box<>(); Box box = integerBox; // OK Но если попытаться присвоить параметризованному типу сырой тип, то будет предупреждение (warning): Box box = new Box(); Box integerBox = box; // Warning Также будет предупреждение (warning), если попытаться вызвать обобщённый метод в сыром типе: Box integerBox = new Box<>(); Box box = integerBox; box.setKey(4); // Warning Предупреждение показывает, что сырой тип обходит проверку обобщённого типа, что откладывает обнаружение ошибки на выполнение программы. Еще один момент, допустим, есть такой пример: public class App { public static void main( String[] args ) { Box> box = new Box<>(); box.setKey(4); box.setValue(new Box<>(2, "cats")); Box boxCopy = box; System.out.println("We have " + box.getKey() + " boxes with " + box.getValue().getKey() + " " + box.getValue().getValue() + " in each box."); System.out.println("We have " + boxCopy.getKey() + " boxes with " + boxCopy.getValue().getKey() + " " + boxCopy.getValue().getValue() + " in each box."); // Is not OK } } В данном случае будет ошибка: java: cannot find symbol symbol: method getKey() location: class java.lang.Object Это происходит по той причине, что до Java 5 обобщения не знали ничего о своих типах, поэтому возвращали Object. Для возможности взаимодействовать с типами, нужно сделать каст к соответствующему типу. В данном случае нужно сделать каст к Box: System.out.println("We have " + box1.getKey() + " boxes with " + ((Box)box1.getValue()).getKey() + " " + ((Box)box1.getValue()).getValue() + " in each box."); Сообщения об ошибках “unchecked” Как упоминалось выше, при использовании сырого типа можно столкнуться с предупреждениями вида: Note: .java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. Термин "unchecked" означает непроверенные, то есть компилятор не имеет достаточного количества информации для обеспечения безопасности типов. По умолчанию этот вид предупреждений выключен, поэтому компилятор даёт подсказку. Чтобы видеть все "unchecked" предупреждения нужно перекомпилировать код с опцией -Xlint:unchecked : javac -Xlint:unchecked .java Предупреждения будут такого вида: App.java:23: warning: [unchecked] unchecked call to setKey(K) as a member of the raw type Box boxCopy.setKey(4); ^ where K is a type-variable: K extends Object declared in class Box 1 warning Обобщённые методы Обобщённые методы похожи на обобщённые классы, но параметры типа относятся к методу, а не к классу. Допустимо делать обобщёнными статические и нестатические методы, а также конструкторы. Синтаксис обобщённого метода включает параметры типа внутри угловых скобок, которые указываются перед возвращаемым типом. class Utils { static void setIfNull(Box box, T t) { if (box.getValue() == null) { box.setValue(t); } } } Пример вызова обобщенного метода: public class App { public static void main( String[] args ) { Box box = new Box<>(); Utils.setIfNull(box, 13); System.out.println(box.getValue()); } } Вывод: 13 Более подробно выведение типа будет описано ниже. Ограниченные параметры типа В некоторых случаях имеет смысл ограничить  типы, которые можно использовать в качестве аргументов в параметризованных типах. Например, в коробке-Box нужно сделать так, чтобы хранились только числа и разного рода (наследники Number). Подобное ограничение можно сделать с помощью ограниченного параметра типа (bounded type parameters). Чтобы объявить ограниченный параметр типа нужно после имени параметра указать ключевое слово extends, а затем указать верхнюю границу (upper bound), которой в данном примере является класс Number. В этом контексте extends означает как extends, так и implements. public class Box { public V getValue() { return value; } public void setValue(V value) { this.value = value; } private V value; } В этом примере ограничены возможные типы, которые можно использовать в параметризованных классах Box, наследниками класса Number. Если попытаться указать, например, Box , то возникнет ошибка компиляции. Аналогичным образом можно создавать обобщённые методы с ограничением: class Utils { static void setIfNull(Box box, T t) { if (box.getValue() == null) { box.setValue(t); } } } Можно указать несколько верхних границ, перечисляя их через символ «&», но при этом только один класс может быть указан в списке верхних границ, и он должен стоять первым: class Bird{} interface Animal{} interface Man{} class Box { // ... } Обобщения, наследование и дочерние типы Как уже известно, можно присвоить объекту одного типа объект другого типа, если эти типы совместимы. Например, можно присвоить объект типа Integer переменной типа Object, так как Object является одним из супертипов Integer: Object someObject = new Object(); Integer someInteger = new Integer(13); someObject = someInteger;   // OK В объектно-ориентированной терминологии это называется связью «является» (“is a”). Так как Integer  является Object-ом, то такое присвоение разрешено. Но Integer также является и Number, поэтому следующий код тоже корректен: public void someMethod(Number n) { /* ... */ } someMethod(new Integer(10));   // OK someMethod(new Double(10.1));   // OK Это также верно для обобщений. Можно осуществить вызов обобщённого типа, передав Number в качестве аргумента типа, и любой дальнейший вызов будет разрешён, если аргумент совместим с Number : Box box = new Box(); box.add(new Integer(10));   // OK box.add(new Double(10.1));  // OK Теперь нужно рассмотреть метод: public void boxTest(Box n) { /* ... */ } Какой тип аргумента он будет принимать? Если посмотреть на сигнатуру, то можно увидеть, что он принимает один аргумент с типом Box. Но что это означает? Можно ли передать Box или Box , как можно можно было бы ожидать? Нет, нельзя, так как Box и Box не являются потомками Box. Это частое недопонимание принципов работы обобщений, и это важно знать. Запомнить: Для двух типов A и B (например, Number и Integer ), MyClass не имеет никакой связи или родства с MyClass , независимо от того, как A и B связаны между собой. Общий родитель MyClass и MyClass — это Object. Можно указать обобщённый класс или интерфейс в качестве родительского для своего класса или интерфейса. Связь между параметрами типа одного класса или интерфейса и другого определяются ключевыми словами extends и implements . Для классов коллекций, например, ArrayList implements List , и List extends Collection . Также ArrayList является дочерним типом для List , который является дочерним типом Collection . Наследование между типами сохраняется, пока не поменяется аргумент типа. Допустим, нужно определить свой собственный интерфейс большой коробки MegaBox, который связывает необязательное значение P с каждым элементом. Объявление может выглядеть так: interface MegaBox extends Box { void setMegaBox(int index, P val); ... } Следующие параметризованные типы являются дочерними типами для Box, но не связаны между собой: MegaBox MegaBox MegaBox Выведение типов Выведение типов — это возможность компилятора Java автоматически определять аргументы типа на основе контекста, чтобы вызов получился возможным. Алгоритм выведения типов определяет типы аргументов и, если есть, тип, в который присваивается результат или в котором возвращается результат. Далее алгоритм пытается найти наиболее конкретный тип, который работает со всеми аргументами. В приведённом ниже примере выведение типов определяет, что второй аргумент, они же Cat и ArrayList, передаваемые в методы pick  имеют тип Serializable : public class App { static T pick(T first, T second) { return second; } public static void main( String[] args ) { Serializable serializable1 = pick("d", new Cat()); Serializable serializable2 = pick("d", new ArrayList()); } } Непосредственно класс Cat: public class Cat implements Serializable { public void voice(){ // ... } // ... } Выведение типов и обобщённые методы В описании обобщённых методов уже рассказывалось о выведении типов, которое делает возможным вызов обобщённого метода так, будто это обычный метод, без указания типа в угловых скобках <>. Пример: Класс Box: public class Box { public T getValue() { return value; } public void setValue(T value) { this.value = value; } private T value; } Класс Cat: public class Cat { private String name; public Cat(String name){ this.name = name; } @Override public String toString() { return "Cat with name " + name; } } Основной класс: public class App { public static void addBox(U u, List> boxes){ Box box = new Box<>(); box.setValue(u); boxes.add(box); } public static void printBoxes(List> boxes){ for(Box box : boxes){ U value = box.getValue(); System.out.println("In box - " + value); } } public static void main( String[] args ) { ArrayList> catsInBoxes = new ArrayList<>(); addBox(new Cat("Kusya"), catsInBoxes); addBox(new Cat("Vasya"), catsInBoxes); addBox(new Cat("Murka"), catsInBoxes); printBoxes(catsInBoxes); } } Этот код выведет в консоль следующее: In box - Cat with name Kusya In box - Cat with name Vasya In box - Cat with name Murka Обобщённый метод addBox объявляет один параметр типа U . В большинстве случаев компилятор Java может вывести параметры типа вызова обобщённого метода, в результате чаще всего вовсе не обязательно их указывать. Например, чтобы вызвать обобщённый метод addBox, можно указать параметры типа так: App.addBox(new Cat("Kusya"), catsInBoxes); Либо можно опустить их, и тогда компилятор Java автоматически выведет тип Cat из аргументов метода: addBox(new Cat("Kusya"), catsInBoxes); Выведение типов и создание экземпляра обобщённого класса Также можно заменить аргументы типа, необходимые для вызова конструктора обобщённого класса пустым множеством параметров типа ( <> ), так как компилятор может вывести аргументы типа из контекста. Напоминание: эта пара угловых скобок называется бриллиантовой операцией (diamond operator). Объявление переменной на примере Box с двумя параметрами типов: Box> myBox = new Box>(); В данном примере, можно заменить параметризованный тип конструктора пустыми угловыми скобками ( <> ): Box> myBox = new Box<>(); Стоит обратить внимание, что для того чтобы воспользоваться выведением типов при создании экземпляра обобщённого класса, нужно использовать бриллиантовую операцию (diamond operator). В примере ниже компилятор сгенерирует предупреждение unchecked conversion warning, так как конструктор Box() обращается к сырому типу Box, а не к Box> : Box> myBox = new Box(); // unchecked conversion warning Выведение типа и обобщённые конструкторы обобщённых и необобщённых классов Конструкторы могут быть обобщёнными как в обобщённых, так и в необобщённых классах. Пример: public class Box { Box(U u){ // ... } public T getValue() { return value; } public void setValue(T value) { this.value = value; } private T value; } Создание экземпляра класса Box: Box box = new Box("Some message"); Эта инструкция создаёт экземпляр параметризованного типа Box . Инструкция явно указывает Cat в качестве формального параметра типа T обобщённого класса Box . Стоит обратить внимание, что конструктор этого обобщённого класса содержит параметр типа U. Компилятор выводит тип String для этого формального параметра U, так как фактически переданный аргумент является экземпляром класса String. Компилятор Java 7 и более поздней версии может вывести аргументы типа создаваемого экземпляра обобщённого класса с помощью бриллиантовой операции (diamong operator). Пример: Box box = new Box<>("Some message"); В этом примере компилятор выводит Cat для параметра типа T обобщённого класса Box. Он выводит тип String для параметра U обобщённого конструктора обобщённого класса. _Важно запомнить, что алгоритм выведения типа использует только аргументы вызова, целевые типы и возможно очевидный ожидаемый возвращаемый тип для выведения типов. Алгоритм выведения не использует последующий код программы. Целевые типы Компилятор Java пользуется целевыми типами для вывода параметров типа вызова обобщённого метода. Целевой тип выражения — это тип данных, который компилятор Java ожидает в зависимости от того, в каком месте находится выражение. Например, метод Box.emptyBox() , который присутствует в классе Box: public class Box { public static final Box EMPTY_BOX = new Box<>(); public T getValue() { return value; } public void setValue(T value) { this.value = value; } private T value; static Box emptyBox(){ return (Box) EMPTY_BOX; } } Есть следующая инструкция присвоения: public static void main( String[] args ) { Box box = Box.emptyBox(); } Эта инструкция ожидает экземпляр Box . Этот тип данных является целевым типом. Поскольку метод emptyBox  возвращает значение типа Box , компилятор выводит, что аргумент типа T будет типом String. Это работает как в Java 7, так и в Java 8. Также можно указывать аргумент типа напрямую: public static void main( String[] args ) { Box box1 = Box.emptyBox(); } Но в данном случае в этом нет необходимости. Это может быть необходимо в других случаях. Допустим, есть метод: static void boxWithString(Box box){ // ... } Если нужно вызвать метод boxWithString с пустой коробкой. В Java 7 следующий код не будет работать: boxWithString(Box.emptyBox()); Компилятор Java 7 сгенерирует примерно такую ошибку: java: incompatible types: com.myapp.qs.Box cannot be converted to com.myapp.qs.Box Компилятору необходимо значение аргумента типа для T, и он начинает с Object. В результате вызов Box.emptyBox() возвращает тип Box, который несовместим с методом boxWithString. Таким образом в Java 7 нужно указать аргумент типа так: boxWithString(Box.emptyBox()); В Java 8 в этом больше нет необходимости. Термин целевой тип расширен и включает аргументы методов. В этом случае boxWithString-у необходим аргумент типа Box Метод Box.emptyBox() возвращает значение типа Box, компилятор выводит аргумент типа T как String, используя целевой тип Box. Таким образом в Java 8 следующая инструкция успешно скомпилируется: boxWithString(Box.emptyBox()); Подстановочный символ (wildcard) В обобщённом коде знак вопроса (?), называемый подстановочным символом, означает  неизвестный тип. Подстановочный символ может использоваться в разных ситуациях: как параметр типа, поля, локальной переменной, иногда в качестве возвращаемого типа. Подстановочный символ никогда не используется в качестве аргумента типа для вызова обобщённого метода, создания экземпляра обобщённого класса или супертипа. Подстановочный символ, ограниченный сверху (Upper bounded wildcard) Можно использовать подстановочный символ, ограниченный сверху, чтобы ослабить ограничения переменной. Например, если необходимо написать метод, который работает с Box , Box  и Box , этого можно достичь с помощью ограниченного сверху подстановочного символа. Чтобы объявить ограниченный сверху подстановочный символ, нужно воспользоваться символом вопроса "?", с последующим ключевым словом extends, с последующим ограничением сверху. Стоит запомнить, что в этом контексте extends означает как расширение класса, так и реализацию интерфейса. Чтобы написать метод, который работает с коробками, в которых Number и дочерними типами от Number, например Integer, Double и Float, можно указать Box. Box вводит более жёсткое ограничение, чем Box, потому что оно соответствует только коробкам типа Number, а Box  соответствует коробкам типа Number и спискам всех его подклассов. Следующий пример, есть класс Animal, у которого присутствует поле name и переопределённый метод toString() для вывода информации о классах наследниках в будущем: public class Animal { private String name; public Animal(String name){ this.name = name; } @Override public String toString() { return this.getClass().getSimpleName() + " with name " + name; } } Класс Dog, который наследуется от Animal: public class Dog extends Animal{ public Dog(String name) { super(name); } } И аналогичный класс Cat, который тоже наследуется от Animal: public class Cat extends Animal { public Cat(String name) { super(name); } } Ограниченный сверху подстановочный символ  , где Animal  — любой тип, соответствует Animal и любому подтипу Animal. Метод printInfo может выводить информацию об объекте в коробке, который является наследником Animal, включая сам класс Animal. Следующий код демонстрирует, как метод принимает коробку с котиком и коробку с собакой, после чего выводит информацию о них: public class App { static void printInfo(Box animalInBox){ System.out.println("Information about animal: " + animalInBox); } public static void main( String[] args ) { Box catInBox = Box.emptyBox(); catInBox.setValue(new Cat("Vasya")); printInfo(catInBox); Box dogInBox = Box.emptyBox(); dogInBox.setValue(new Dog("Kusya")); printInfo(dogInBox); } } Вывод: Information about animal: Cat with name Vasya in box Information about animal: Dog with name Kusya in box Неограниченный подстановочный символ (Unbounded wildcard) Если просто использовать подстановочный символ, то получится подстановочный символ без ограничений. Box означает коробку с неизвестным содержимым (неизвестным типом). Неограниченный подстановочный символ полезен в двух случаях: Если нужен метод, который может быть реализован с помощью функциональности класса Object. Когда код использует методы обобщённого класса, которые не зависят от параметра типа. Например, List.size() или List.clear(). В реальности Class  используется так часто, потому что большинство методов Class не зависят от T. Измененный метод printInfo : public class App { static void printInfo(Box box){ System.out.println("Information about box: " + box); } public static void main( String[] args ) { Box catInBox = Box.emptyBox(); catInBox.setValue(new Cat("Vasya")); printInfo(catInBox); // java: incompatible types: com.myapp.qs.Box cannot be converted to com.myapp.qs.Box } } Цель метода printInfo  — вывод в консоль информации об объекте в коробке любого типа, но сейчас он её не выполняет, так как он может вывести в консоль только объект типа Object. Он не может принимать в качестве параметра Box, Box, Box и какие-либо ещё, как показано в попытке передать в метод коробку с котом Вася, так как они не являются дочерними типами для Box. Чтобы сделать этот метод более общим, нужно использовать Box : public class App { static void printInfo(Box box){ System.out.println("Information about box: " + box); } public static void main( String[] args ) { Box animalInBox = Box.emptyBox(); animalInBox.setValue(new Animal("Vasya")); printInfo(animalInBox); // OK } } Box  является дочерним типом для Box для любого конкретного типа вместо Animal, поэтому можно использовать printInfo для вывода в консоль информации о коробке с любым переданным в него типом. Важно запомнить, что Box  и Box  — это НЕ одно и то же. Можно вставить Object или любой дочерний тип от Object в Box. Но можно вставить только null в Box. Ограниченный снизу подстановочный символ (Lower bound Wildcard) Ограниченный снизу подстановочный символ ограничивает неизвестный тип так, чтобы он был либо указанным типом, либо одним из его предков. Можно указать либо только верхнюю границу для подстановочного символа, либо только нижнюю, но также можно указать оба ограничения сразу. Допустим, что необходимо модифицировать метод printInfo для вывода информации о котиках. Чтобы максимизировать гибкость, может захотеться, чтобы метод работал с Box, Box и Box  — всё, что могут хранить экземпляры класса Cat. Чтобы написать метод, который работает с Cat и супертипами Cat (такими как Cat, Animal и Object), можно указать Box. Вариант Box  более ограничен, чем Box, потому что он позволяет использовать только коробки с объектами типа Cat, тогда как Box соответствует коробкам с любым родительским классом от Cat и самому Cat в коробке. Следующий код демонстрирует вывод информации о котах и животных. И не возможности вывести информацию о Мейн-кунах, которые наследуются от Cat. Новый класс MaineCoon: public class MaineCoon extends Cat{ public MaineCoon(String name) { super(name); } // ... } Реализации в главном методе: public class App { static void printInfo(Box box){ System.out.println("Information about box: " + box); } public static void main( String[] args ) { Box animalInBox = Box.emptyBox(); animalInBox.setValue(new Animal("Rick")); printInfo(animalInBox); // OK Box catInBox = Box.emptyBox(); catInBox.setValue(new Cat("Vasya")); printInfo(catInBox); // OK Box maineCoonInBox = Box.emptyBox(); maineCoonInBox.setValue(new MaineCoon("Kate")); printInfo(maineCoonInBox); // Incompatible types!!! } } Вывод: // Without MaineCoon Information about box: Animal with name Rick in box Information about box: Cat with name Vasya in box // With MaineCoon java: incompatible types: com.myapp.qs.Box cannot be converted to com.myapp.qs.Box Подстановочные символы и дочерние типы Как было описано в пункте "Обобщения, наследование и дочерние типы", обобщённые классы или интерфейсы связаны не только из-за связи между их типами. Однако можно использовать подстановочные символы (wildcards) для создания связи между обобщёнными классами и интерфейсами. С данными обычными (необобщёнными) классами: class Animal { /* ... */ } class Cat extends Animal { /* ... */ } Имеет смысл написать вот такой код: public static void main( String[] args ) { Cat cat = new Cat("Vasya"); Animal animal = cat; } Этот пример показывает, что наследование следует правилу подчинённых типов: класс Cat является подклассом класса Animal, если он расширяет его. Это правило НЕ работает для обобщённых типов: public static void main( String[] args ) { Box catInBox = new Box<>(); Box animalInBox = catInBox; // Incompatible types!!! } Вывод: java: incompatible types: com.myapp.qs.Box cannot be converted to com.myapp.qs.Box Если Cat является дочерним типом для Animal, то какая связь между Box и Box? Не смотря на то, что Cat является подтипом Animal, Box не является подтипом Box. Это разные типы. Общим предком для Box и Box  является Box. Для того чтобы создать такую связь между этими классами, чтобы код мог иметь доступ к методам Animal через Box, используется подстановочный символ: public static void main( String[] args ) { Box catInBox = new Box<>(); Box animalInBox = catInBox; // OK. Box child type from Box } Так как Cat является дочерним типом от Animal, и animalInBox является коробкой, в которой тип Animal, теперь существует связь между catInBox (коробка с объектом типа Cat ) и animalInBox. Следующая диаграмма показывает связь между несколькими классами List, объявленными с ограниченными сверху подстановочными символами и ограниченными снизу подстановочными символами. Захват символа подстановки (Wildcard Capture) и вспомогательные методы В некоторых случаях компилятор может вывести тип подстановочного символа. Коробка может быть определена как Box, но при вычислении выражения компилятор выведет конкретный тип из кода. Этот сценарий называется захватом подстановочного символа. В большинстве случаев нет нужды беспокоиться о захвате подстановочного символа, кроме случаев, когда появляется фраза “capture of” в сообщении об ошибке. Следующий код выводит сообщение об ошибке, связанное с захватом подстановочного символа, при компиляции: static void testError(Box box){ box.setValue(box.getValue()); // capture of ? } Вывод: java: incompatible types: int cannot be converted to capture#1 of ? В этом примере компилятор обрабатываем параметр box как тип Object. Когда метод testError вызывает box.setValue(box.getValue()), компилятор не может подтвердить тип объекта, который будет класться в коробку, и генерирует ошибку. Когда возникает этот тип ошибки, это обычно означает, что компилятор верит, что вы присваиваете неправильный тип переменной. Обобщения были добавлены в Java именно для этого — чтобы усилить безопасность типов во время компиляции. Код пытается выполнить безопасную операцию, тогда как можно обойти ошибку компиляции? Это возможно исправить написав приватный вспомогательный метод (private helper method), который захватывает подстановочный символ. В этом случае можно обойти проблему с помощью создания приватного вспомогательного метода testErrorHelper(): private static void testErrorHelper(Box box){ box.setValue(box.getValue()); // OK } static void testError(Box box){ testErrorHelper(box); } Благодаря вспомогательному методу компилятор использует выведение типа для определения, что T является CAP#1 ( захваченная переменная в вызове). Пример теперь успешно компилируется. По соглашению вспомогательные методы обычно называются как "originalMethodNameHelper". Руководство по использованию подстановочного символа Когда использовать ограниченный сверху подстановочный символ (wildcard), и когда использовать ограниченный снизу подстановочный символ, - определить зачастую бывает довольно сложно. Здесь собраны советы по выбору необходимого ограничения для подстановочного символа. В этом обсуждении будет полезно думать о переменных, будто они представляют две функции: Входная переменная. Предоставляет данные для кода. Для метода copy(src, dst) параметр src предоставляет данные для копирования, поэтому он считается входной переменной. Выходная переменная. Содержит данные для использования в другом месте. В примере с copy(src, dst) параметр dst принимает данные и является выходной переменной. Некоторые переменную могут быть одновременно входными и выходными, такой случай тоже здесь рассматривается. Руководство: Входная переменная определяется с ограниченным сверху подстановочным символом, используя ключевое слово extends. Выходная переменная определяется с ограниченным снизу подстановочным символом, используя ключевое слово super. Если к входной переменной можно обращаться только используя методы класса Object, использовать стоит неограниченный подстановочный символ. Если переменная должна использоваться как входная и как выходная одновременно, то НЕ стоит использовать подстановочный символ. Это руководство не охватывает использование подстановочных символов в возвращаемых из методов типах. Не стоит использовать подстановочные символы в возвращаемых типах, потому что это будет принуждать других программистов разбираться с подстановочными символами. Стирание типа (Type Erasure) Обобщения были введены в язык программирования Java для обеспечения более жёсткого контроля типов во время компиляции и для поддержки обобщённого программирования. Для реализации обобщения компилятор Java применяет стирание типа (type erasure) к: Заменяет все параметры типа в обобщённых типах их границами или Object-ами, если параметры типа не ограничены. Сгенерированный байткод содержит только обычные классы, интерфейсы и методы. Вставляет приведение типов где необходимо, чтобы сохранить безопасность типа. Генерирует связующие методы, чтобы сохранить полиморфизм в расширенных (extended, наследующиеся от других) обобщённых типах. Стирание типа обеспечивает, что никакие новые классы не создаются для параметризованных типов, следовательно обобщения не приводят к накладным расходам во время выполнения. Стирание типа в обобщённых типах Во время процесса стирания типов компилятор Java стирает все параметры типа и заменяет каждый его ограничением, если параметр типа ограничен, либо Object-ом, если параметр типа неограничен. Следующий обобщённый класс, который представляет коробку: public class Box { 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 { 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 void setIfNull(Box box, T t) { if (box.getValue() == null) { box.setValue(t); } } Так как T неограничен, то компилятор Java заменяет его на Object : static void setIfNull(Box 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 void setIfNull(Box box, T t) { /* ... */} Компилятор Java заменит T на Animal : static void setIfNull(Box box, Animal t) { /* ... */} В разделе стирания типов обсуждается процесс, где компилятор удаляет информацию, связанную с параметрами типа и аргументами типа. Стирание типа имеет последствия, связанные с произвольным количеством параметров (varargs). Материализуемые типы (reifiable types) — это типы, информация о которых полностью доступа во время выполнения: примитивы, необобщённые типы, сырые типы, обращения к неограниченным подстановочным символам. Нематериализуемые типы (Non-reifiable types) — это типы, информация о которых удаляется во время компиляции стиранием типов: обращения к обобщённым типам, которые не объявлены с помощью неограниченных подстановочных символов. Во время выполнения о нематериализуемых типах (Non-reifiable types) нет всей информации. Примеры нематериализуемых типов: Box  и Box. Виртуальная машина Java не может узнать разницу между ними во время выполнения. В некоторых ситуациях нематериализуемые типы не могут использоваться, например, в выражениях instanceof или в качестве элементов массива. Загрязнение кучи (Heap pollution) Загрязнение кучи (heap pollution) возникает, когда переменная параметризованного типа ссылается на объект, который не является параметризованным типом. Такая ситуация возникает, если программа выполнила некоторую операцию, которая генерирует предупреждение unchecked warning во время компиляции. Предупреждение unchecked warning генерируется, если правильность операции, в которую вовлечён параметризованный тип (например приведение типа или вызов метода) не может быть проверена. Например, загрязнение кучи возникает при смешивании сырых типов и параметризованных типов, или при осуществлении непроверяемых преобразований типа. В обычных ситуациях, когда код компилируется в одно и то же время, компилятор генерирует unchecked warning, чтобы привлечь внимание к загрязнению кучи. Если компилируются различные части кода отдельно, то становится трудно определить потенциальную угрозу загрязнения кучи. Если обеспечить компиляцию кода без предупреждений, то загрязнение кучи (heap pollution) не сможет произойти. Ограничения обобщений Нельзя создавать экземпляры обобщённых типов с примитивными типами в качестве аргументов типа. Допустим, есть следующий класс пары ключ-значение: class Pair {     private K key;     private V value;     public Pair(K key, V value) {         this.key = key;         this.value = value;     }     // ... } При создании объекта Pair нельзя заменять примитивным типом формальные параметры K и V: Pair p = new Pair<>(8, 'a');  // compile-time error Можно заменить их только непримитивными типами: Pair p = new Pair<>(8, 'a'); Компилятор использует автоупаковку Integer.valueOf(8)  и Character('a'). Нельзя создавать экземпляры параметров типа Нельзя создать экземпляр параметра типа. Например, следующий код приведёт к ошибке компиляции: static void add(Box box) { T t = new T(); // compile-time error // ... box.setValue(t); } В качестве обходного пути можно создать объект параметра типа с помощью отражения (reflection - будет пройдено позднее): static void add(Box box, Class cls) throws Exception { T t = cls.newInstance(); // OK // ... box.setValue(t); } Вызвать метод add() можно вот так: public static void main( String[] args ) throws Exception { Box box = new Box<>(); add(box, String.class); } Нельзя объявлять статические поля с типом параметра типа Статические поля класса являются общими для всех объектов этого класса, поэтому статические поля с типом параметра типа запрещены. Следующий класс: public class Box { private static T value; // ... } Если бы статические поля с типом параметра типа были бы разрешены, то следующий код сбивал бы с толку: Box animalInBox = new Box<>(); Box catInBox = new Box<>(); Box dogInBox = new Box<>(); Так как статическое поле value является общим для animalInBox, catInBox  и dogInBox, то какого типа value? Оно не может быть Animal, Cat и Dog в одно и то же время, поэтому нельзя создавать статические поля с типом параметра типа. Обычно нельзя использовать приведение типа к параметризованному типу, если он не использует неограниченный подстановочный символ. Нельзя использовать приведения типа или instanceof с параметризованными типами Так как компилятор Java стирает все параметры типа из обобщённого кода, то нельзя проверить во время выполнения, какой параметризованный тип используется для обобщённого типа. Во время выполнения нет параметров типа, поэтому нет возможности различить Box и Box. Наибольшее, что можно сделать — это использовать подстановочный символ для проверки. Например: Box box1 = new Box<>(); Box box2 = (Box) box1; Однако в некоторых случаях компилятор знает, что параметр типа всегда верный и позволяет использовать приведение типа. Невозможно создавать массивы параметризованных типов Нельзя создавать массивы параметризованных типов. Например, следующий код не будет компилироваться: Box[] box1 = new Box()[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[]; // compilation error, but let's say it's possible stringLists[0] = new Box(); // OK stringLists[1] = new Box(); // there should be an ArrayStoreException exception, but the runtime cannot notice it. } Если бы массивы с параметризованными типами были бы разрешены, то предыдущий код не смог бы бросить исключение ArrayStoreException. Нельзя создавать, ловить (catch) или бросать (throw) объекты параметризованных типов Обобщённый класс не может расширять класс Throwable напрямую или не напрямую. Например, следующие классы не компилируются: // Extends Throwable non-direct class MathException extends Exception { /* ... */ }    // compilation error // Extends Throwable directly class QueueFullException extends Throwable { /* ... */ // compilation error Метод не может ловить (catch) экземпляр параметра типа: public static void execute(Box box) { try { J j = box.getValue(); // ... } catch (T e) { // compilation error // ... } } Однако можно использовать параметр типа в throws : class Parser {     public void parse(File file) throws T {     // OK         // ...     } } Нельзя перегружать метод так, чтобы формальные параметры типа стирались в один и тот же сырой тип Класс не может иметь два перегруженных метода, которые будут иметь одинаковую сигнатуру после стирания типов: public class Test {     public void print(Box strBox) { }     public void print(Box intBox) { } } Этот код не будет компилироваться. ВОПРОСЫ: Вопрос: Какой из этих параметров типа используется для универсального класса для возврата и приема объектов любого типа? K T N V Правильный ответ: 2 Вопрос: Какой из этих типов нельзя использовать для инициализации универсального типа? Primitive types Integer class FLoat class Правильный ответ: 1 Вопрос: что такое Сырой тип (raw type)? это имя обобщённого класса или интерфейса без аргументов типа (type arguments). это необобщённый класс или интерфейс. первый и второй варианты верны. Правильный ответ: 1 Например, параметризованный тип создаётся так: Box integerBox = new Box<>(); Если убрать аргументы типа, то будет создан сырой тип: Box box = new Box(); Поэтому Box  — это сырой тип обобщённого типа Box . Однако необобщённый класс или интерфейс НЕ являются сырыми типами. Вопрос: Что из следующего является недопустимым? ArrayList al1 = new ArrayList(); ArrayList al2 = new ArrayList(); ArrayList al3 = new ArrayList(); Всё допустимо Правильный ответ: 3 Целое число является дочерним классом числа, поэтому (2) допустимо. Но String не является дочерним классом Number, поэтому (3) недопустимо.