Дженерики Java
Дженерики (или обобщения) — это параметризованные типы.
Параметризованные типы позволяют объявлять классы, интерфейсы и методы, где тип данных, которыми они оперируют, указан в виде параметра. Используя дженерики, можно создать единственный класс, например, который будет автоматически работать с разными типами данных.
Классы, интерфейсы или методы, имеющие дело с параметризованными типами, называются параметризованными или обобщениями, параметризованными (обобщенными) классами или параметризованными (обобщёнными) методами.
Обобщения добавили в язык безопасность типов.
- Параметризованные классы
- Ограниченные типы
- Применение метасимвольных аргументов
- Параметризованные методы и конструкторы
- Параметризованные интерфейсы
- Иерархии параметризованных классов
- Использование оператора instanceof c параметризованными классами
- Ограничения присущие обобщениям
1. Параметризованные классы
Следующий пример демонстрирует использование параметризованного класса, который описывает матрицу:
public class Matrix < private T[] array; public Matrix(T[] array) < this.array = array.clone(); >public static void main(String[] args) < MatrixdoubleMatrix = new Matrix<>(new Double[2]); Matrix integerMatrix = new Matrix<>(new Integer[4]); Matrix byteMatrix = new Matrix<>(new Byte[7]); > >
В объявлении Matrix integerMatrix Integer является аргументом типа.
Java не создает разные версии класса Matrix или любого другого параметризованного класса. Имеется только одна версия класса Matrix , которая существует в прикладной программе.
Дженерики работают только с объектами! Следующий код является неправильным:
Gen strOb = new Gen (53); // Ошибка, нельзя использовать примитивные типы
Т обозначает имя параметра типа. Это имя используется в качестве заполнителя вместо которого в дальнейшем подставляется имя конкретного типа, передаваемого классу Matrix при создании объекта. Это означает, что обозначение Т применяется в классе Matrix всякий раз, когда требуется параметр типа. Всякий раз, когда объявляется параметр типа, он указывается в угловых скобках.
Обобщенные типы отличаются в зависимости от типов-аргументов. Следующий код не допустим:
doubleMatrix = integerMatrix; // Не верно!
Несмотря на то, что doubleMatrix и integerMatrix имеют тип Matrix , они являются ссылками на разные типы, потому что типы их параметров отличаются.
Обобщенный класс может быть объявлен с любым количеством параметров типа. Например:
public class TwoGen < private T obT; private V obV; public TwoGen(T obT, V obV) < this.obT = obT; this.obV = obV; >public void showTypes() < System.out.println("Тип T: " + obT.getClass().getName()); System.out.println("Тип V: " + obV.getClass().getName()); >public T getObT() < return obT; >public V getObV() < return obV; >> public class SimpleGen < public static void main(String[] args) < TwoGentwoGen = new TwoGen<>(88, "Generics"); twoGen.showTypes(); System.out.println("Значение T: " + twoGen.getObT()); System.out.println("Значение V: " + twoGen.getObV()); > >
2. Ограниченные типы
Указывая параметр типа, можно наложить ограничение сверху в виде верхней границы, где объявляется супер класс, от которого должны быть унаследованы все аргументы типов. С этой целью вместе с параметром указывается ключевое слово extends :
class Gen
Параметр типа Т может быть заменен только указанным супер классом или его подклассами.
Рассмотрим пример использования ограниченного типа:
public class Average < private T[] array; public Average(T[] array) < this.array = array; >public double average() < double sum = 0.0; for (T value : array) < sum += value.doubleValue(); >return sum / array.length; > > public class AverageDemo < public static void main(String[] args) < Integer[] intArray = ; Average integerAverage = new Average<>(intArray); System.out.println("Среднее значения для Integer " + integerAverage.average()); Double[] doubleArray = ; Average doubleAverage = new Average<>(doubleArray); System.out.println("Среднее значения для Double " + doubleAverage.average()); // Не откомпилируется, // потому что String не является наследником Number /* String[] strArray = ; Average strAverage = new Average<>(strArray); System.out.println("Среднее значения для String " + strAverage.average());*/ > >
В виде ограничения можно накладывать не только тип класса, но и тип интерфейса:
public class MyClass
Ограничение может включать в себя как тип класса, так и типы одного или нескольких интерфейсов:
class Gen
Тип класса должен быть задан первым. Накладывая на обобщенный тип ограничение, состоящее из класса и одного или нескольких интерфейсов, для их объединения следует воспользоваться логической операцией &: Таким образом, любой тип, передаваемый параметру Т , должен быть подклассом, производным от класса MyClass и реализующим интерфейсы MyInterface1 и MyInterface2 .
3. Применение метасимвольных аргументов
Представьте, что мы хотим добавить метод для сравнения средних значений массивов в класс Average из примера 3. Причем типы массивов могут быть разные:
Integer intArray[] = ; Double doubleArray[] = ; Average iob = new Average<>(intArray); Average dob = new Average<>(doubleArray); if (iob.sameAvg(dob)) < System.out.println("are the same.");>else
Так как Average параметризованный тип, какой тип параметра вы укажете для Average , когда создадите параметр метода типа Average ? Напрашивается следующий вариант:
boolean sameAvg(Average ob)
Но это не сработает, так как в этом случае метод sameAvg будет принимать аргументы только того же типа, что и существующий объект:
if (iob.sameAvg(iob)) < System.out.println("are the same.");>else
Чтобы создать обобщенную версию метода sameAvg() , следует воспользоваться другим средством обобщений Jаvа – метасимвольным аргументом. Метасимвольный аргумент обозначается знаком ? и представляет неизвестный тип.
boolean sameAvg(Average ob)
Мета символ не оказывает никакого влияния на тип создаваемых объектов класса Average . Это определяется оператором extends в объявлении класса Average. Мета символ просто совпадает с любым достоверным объектом класса Average .
Метасимвольные аргументы могут быть ограничены почти таким же образом, как и параметры типов. Ограничивать метасимвольный аргумент особенно важно при создании обобщенного типа, оперирующего иерархией классов. Например:
public class Average2 < private T[] array; public Average2(T[] array) < this.array = array.clone(); >public double average() < double sum = 0.0; for (T value : array) < sum += value.doubleValue(); >return sum / array.length; > boolean sameAvg(Average2 >ob) < return average() == ob.average(); >> public class AverageDemo2 < public static void main(String[] args) < Integer[] intArray = ; Average2 iob = new Average2<>(intArray); System.out.println("Среднее значения для Integer " + iob.average()); Double[] doubleArray = ; Average2 dob = new Average2<>(doubleArray); System.out.println("Среднее значения для Double " + dob.average()); Float[] floatArray = ; Average2 fob = new Average2<>(floatArray); System.out.println("Среднее значения для Float " + fob.average()); System.out.print("Средние значения для iob и dob "); if (iob.sameAvg(dob)) < System.out.println("одинаковые."); >else < System.out.println("разные."); >System.out.print("Средние значения для iob и fob "); if (iob.sameAvg(fob)) < System.out.println("одинаковые."); >else < System.out.println("разные."); >> >
4. Параметризованные методы и конструкторы
В методах параметризованного класса можно использовать параметр типа, а следовательно, они становятся параметризованными относительно параметра типа.
Но можно объявить параметризованный метод, в котором непосредственно используется один или несколько параметров типа. Более того, можно объявить параметризованный метод, входящий в не параметризованный класс. Например:
public class GenMethodDemo < /** * Является ли объект x элементом массива array * * @param x * @param array * @param * @param * @return */ public static boolean isIn(T x, V[] array) < for (V element : array) < if (x.equals(element)) < return true; >> return false; > public static void main(String[] args) < Integer[] intArray = ; if (isIn(2, intArray)) < System.out.println("2 входит в массив intArray"); >if (!isIn(7, intArray)) < System.out.println("7 не входит в intArray"); >System.out.println(); String[] strArray = ; if (isIn("two", strArray)) < System.out.println("two входит в массив strArray"); >if (!isIn("seven", strArray)) < System.out.println("seven не входит в массив strArray"); >> >
Конструкторы также могут быть обобщенными, даже если их классы таковыми не являются. Например:
public class GenConstructor < private double value; public GenConstructor(T arg) < value = arg.doubleValue(); >public void showValue() < System.out.println("value: " + value); >> public class GenConstructorDemo < public static void main(String[] args) < GenConstructor genConstructor1 = new GenConstructor(100); GenConstructor genConstructor2 = new GenConstructor(123.5F); genConstructor1.showValue(); genConstructor2.showValue(); >>
5. Параметризованные интерфейсы
В дополнение к обобщенным классам и методам вы можете объявлять параметризованные интерфейсы. Параметризованные интерфейсы специфицируются так же, как и обобщенные классы:
public interface MyInterface < T someMethod(T t); >public class MyClass implements MyInterface < @Override public T someMethod(T t) < return t; >public static void main(String[] args) < MyInterfaceobject = new MyClass<>(); String str = object.someMethod("some string"); > >
6. Иерархии параметризованных классов
Параметризованные классы могут быть частью иерархии классов так же, как и любые другие не параметризованные классы. То есть параметризованный класс может выступать в качестве супер класса или подкласса.
Ключевое отличие между параметризованными и не параметризованными иерархиями состоит в том, что в параметризованной иерархии любые аргументы типов, необходимые параметризованному супер классу, всеми подклассами должны передаваться по иерархии вверх.
public class GenericSuper < private T ob; public GenericSuper(T ob) < this.ob = ob; >private T getOb() < return ob; >> public class GenericSub extends GenericSuper < public GenericSub(T ob) < super(ob); >>
Подкласс параметризованного супер класса необязательно должен быть параметризованным, но в нем все же должны быть, указаны параметры типа, требующиеся его параметризованному супер классу. Подкласс может, если требуется, быть, дополнен и своими параметрами типа. Супер классом для параметризованного класса может быть класс не параметризованный.
7. Использование оператора instanceof c параметризованными классами
public class HierarchyDemo < public static void main(String[] args) < GenericSuperobject = new GenericSub<>(88); if (object instanceof GenericSuper>) < System.out.println("object is instance of GenericSuper"); >if (object instanceof GenericSub>) < System.out.println("object is instance of GenericSub"); >// Ошибка компиляции - информация об обобщенном типе недоступна во время выполнения /* if (object instanceof GenericSub) < System.out.println("object is instance of GenericSub"); >*/ > >
8. Ограничения присущие обобщениям
Обобщениям присущи некоторые ограничения. Рассмотрим их:
1. Нельзя создавать экземпляр по параметру типа. Ни обычный объект, ни массив:
public class GenRestriction < private T ob; private T[] array; public GenRestriction(T ob, T[] array) < // Недопустимо. //оb = new Т(); //array = new Т[10]; this.ob = ob; this.array = array; >>
2. Нельзя создать массив специфических для типа обобщенных ссылок:
public class GenArrays < public static void main(String[] args) < // Нельзя создать массив специфичных для типа обобщенных ссылок. // GenericSub[] gens = new GenericSub[10]; GenericSub>[] gens = new GenericSub>[10]; gens[0] = new GenericSub<>(34); > >
3. Нельзя создавать обобщенные статические переменные и методы. Но объявить статические обобщенные методы со своими параметрами типа все же можно:
public class GenericWrongStatic < // Неверно, нельзя создать статические переменные типа Т. //public static Т оb; // Неверно, ни один статический метод не может использовать Т. /* public static T getOb() < return оb; >*/ //Но объявить статические обобщенные методы со своими параметрами типа можно public static void getOb(V v) < System.out.println(v); >>
- Вложенные классы
- Задания
Пришел, увидел, обобщил: погружаемся в Java Generics
Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.
Работа с коллекциями
Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:
public long getSum(List accounts) < long sum = 0; for (int i = 0, n = accounts.size(); i < n; i++) < Object account = accounts.get(i); if (account instanceof Account) < sum += ((Account) account).getAmount(); >> return sum; >
Мы итерировались, пробегались по списку аккаунтов и проверяли, действительно ли элемент из этого списка является экземпляром класса Account — то есть счетом пользователя. Выполняли приведение типа нашего объекта класса Account и метод getAmount , который возвращал сумму на этом счете. Дальше все это суммировали и возвращали итоговую сумму. Требовалось выполнить два действия:
if (account instanceof Account) < // (1)
sum += ((Account) account).getAmount(); // (2)
Если не сделать проверку ( instanceof ) на принадлежность к классу Account , то на втором этапе возможен ClassCastException – то есть аварийное завершение программы. Поэтому такая проверка была обязательной.
С появлением Generics необходимость в проверке и приведении типа отпала:
public long getSum2(List accounts) < long sum = 0; for (Account account : accounts) < sum += account.getAmount(); >return sum; >
Теперь метод
getSum2(List accounts)
принимает в качестве аргументов только список объектов класса Account . Это ограничение указано в самом методе, в его сигнатуре, программист просто не может передать никакой другой список — только список клиентских счетов.
Нам не нужно выполнять проверку типа элементов из этого списка: она подразумевается описанием типа у параметра метода
List accounts
(можно прочитать как список объектов класса Account ). И компилятор выдаст ошибку, если что-то пойдет не так — то есть если кто-то попробует передать в этот метод список объектов, отличных от класса Account .
Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.
Принцип подстановки
Принцип подстановки Барбары Лисков – специфичное определение подтипа в объектно-ориентированном программировании. Идея Лисков о «подтипе» дает определение понятия замещения: если S является подтипом T , тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы.
| Тип | Подтип |
| Number | Integer |
| List | ArrayList |
| Collection | List |
| Iterable | Collection |
Примеры отношения тип/подтип
Вот пример использования принципа подстановки в Java:
Number n = Integer.valueOf(42); List aList = new ArrayList<>(); Collection aCollection = aList; Iterable iterable = aCollection;
Integer является подтипом Number , следовательно, переменной n типа Number можно присвоить значение, которое возвращает метод Integer.valueOf(42) .
Ковариантность, контравариантность и инвариантность
Сначала немного теории. Ковариантность — это сохранение иерархии наследования исходных типов в производных типах в том же порядке. Например, если Кошка — это подтип Животные, то Множество — это подтип Множество . Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:
Множество = Множество
Контравариантность — это обращение иерархии исходных типов на противоположную в производных типах. Например, если Кошка — это подтип Животные , то Множество — это подтип Множество . Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:
Множество = Множество
Инвариантность — отсутствие наследования между производными типами. Если Кошка — это подтип Животные, то Множество не является подтипом Множество и Множество не является подтипом Множество .
Массивы в Java ковариантны. Тип S[] является подтипом T[] , если S — подтип T . Пример присваивания:
String[] strings = new String[] ; Object[] arr = strings;
Мы присвоили ссылку на массив строк переменной arr , тип которой – «массив объектов» . Если бы массивы не были ковариантными, нам бы это сделать не удалось. Java позволяет это сделать, программа скомпилируется и выполнится без ошибок.
arr[0] = 42; // ArrayStoreException. Проблема обнаружилась на этапе выполнения программы
Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.
«Дженерики» инвариантны. Приведем пример:
List ints = Arrays.asList(1,2,3); List nums = ints; // compile-time error. Проблема обнаружилась на этапе компиляции nums.set(2, 3.14); assert ints.toString().equals("[1, 2, 3.14]");
Если взять список целых чисел, то он не будет являться ни подтипом типа Number , ни каким-либо другим подтипом. Он является только подтипом самого себя. То есть List — это List и ничего больше. Компилятор позаботится о том, чтобы переменная ints , объявленная как список объектов класса Integer, содержала только объекты класса Integer и ничего кроме них. На этапе компиляции производится проверка, и у нас в рантайме уже ничего не упадет.
Wildcards
Всегда ли Generics инварианты? Нет. Приведу примеры:
List ints = new ArrayList(); List nums = ints;
Это ковариантность. List — подтип List
List nums = new ArrayList(); List ints = nums;
Это контравариантность. List является подтипом List .
Запись вида "? extends . " или "? super . " — называется wildcard или символом подстановки, с верхней границей ( extends ) или с нижней границей ( super ). List может содержать объекты, класс которых является Number или наследуется от Number . List может содержать объекты, класс которых Number или у которых Number является наследником (супертип от Number ).
Запись вида T2 1 означает, что набор типов описываемых T2 является подмножеством набора типов описываемых T1
Более математическая интерпретация темы
Пара задачек для проверки знаний:
1. Почему в примере ниже compile-time error? Какое значение можно добавить в список nums ?
List ints = new ArrayList(); ints.add(1); ints.add(2); List nums = ints; nums.add(3.14); // compile-time error
Если контейнер объявлен с wildcard ? extends , то можно только читать значения. В список нельзя ничего добавить, кроме null . Для того чтобы добавить объект в список нам нужен другой тип wildcard — ? super
2. Почему нельзя получить элемент из списка ниже?
public static T getFirst(List list) < return list.get(0); // compile-time error >
Нельзя прочитать элемент из контейнера с wildcard ? super , кроме объекта класса Object
public static Object getFirst(List list)
The Get and Put Principle или PECS (Producer Extends Consumer Super)
Особенность wildcard с верхней и нижней границей дает дополнительные фичи, связанные с безопасным использованием типов. Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super ). Чтобы было легче запомнить, когда какой wildcard использовать, существует принцип PECS — Producer Extends Consumer Super.
- Если мы объявили wildcard с extends, то это producer. Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает.
- Если же мы объявили wildcard с super — то это consumer. Он только принимает, а предоставить ничего не может.
public static void copy(List dest, List src)
Метод осуществляет копирование элементов из исходного списка src в список dest . src — объявлен с wildcard ? extends и является продюсером, а dest — объявлен с wildcard ? super и является потребителем. Учитывая ковариантность и контравариантность wildcard, можно скопировать элементы из списка ints в список nums :
List nums = Arrays.asList(4.1F, 0.2F); List ints = Arrays.asList(1,2); Collections.copy(nums, ints);
Если же мы по ошибке перепутаем параметры метода copy и попытаемся выполнить копирование из списка nums в список ints , то компилятор не позволит нам это сделать:
Collections.copy(ints, nums); // Compile-time error
и Raw типы
Ниже приведен wildcard с неограниченным символом подстановки. Мы просто ставим , без ключевых слов super или extends :
static void printCollection(Collection c) < // a wildcard collection for (Object o : c) < System.out.println(o); >>
На самом деле такой «неограниченный» wildcard все-таки ограничен, сверху. Collection — это тоже символ подстановки, как и " ? extends Object ". Запись вида Collection равносильна Collection , а значит — коллекция может содержать объекты любого класса, так как все классы в Java наследуются от Object – поэтому подстановка называется неограниченной.
Если мы опустим указание типа, например, как здесь:
ArrayList arrayList = new ArrayList();
то, говорят, что ArrayList — это Raw тип параметризованного ArrayList . Используя Raw типы, мы возвращаемся в эру до дженериков и сознательно отказываемся от всех фич, присущих параметризованным типам.
Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения.
ArrayList strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // Ok strings = arrayList; // Unchecked assignment arrayList.add(1); //unchecked call
Wildcard Capture
Попробуем теперь реализовать метод, выполняющий перестановку элементов списка в обратном порядке.
public static void reverse(List list); // Ошибка! public static void reverse(List list) < Listtmp = new ArrayList(list); for (int i = 0; i < list.size(); i++) < list.set(i, tmp.get(list.size()-i-1)); // compile-time error >>
Ошибка компиляции возникла, потому что в методе reverse в качестве аргумента принимается список с неограниченным символом подстановки .
означает то же что и . Следовательно, согласно принципу PECS, list – это producer . А producer только продюсирует элементы. А мы в цикле for вызываем метод set() , т.е. пытаемся записать в list . И поэтому упираемся в защиту Java, что не позволяет установить какое-то значение по индексу.
Что делать? Нам поможет паттерн Wildcard Capture . Здесь мы создаем обобщенный метод rev . Он объявлен с переменной типа T . Этот метод принимает список типов T , и мы можем сделать сет.
public static void reverse(List list) < rev(list); >private static void rev(List list) < Listtmp = new ArrayList(list); for (int i = 0; i < list.size(); i++) < list.set(i, tmp.get(list.size()-i-1)); >>
Теперь у нас все скомпилируется. Здесь произошел захват символа подстановки (wildcard capture). При вызове метода reverse(List list) в качестве аргумента передается список каких-то объектов (например, строк или целых чисел). Если мы можем захватить тип этих объектов и присвоить его переменной типа X , то можем заключить, что T является X .
Более подробно о Wildcard Capture можно прочитать здесь и здесь.
Вывод
Если необходимо читать из контейнера, то используйте wildcard с верхней границей " ? extends ". Если необходимо писать в контейнер, то используйте wildcard с нижней границей " ? super ". Не используйте wildcard, если нужно производить и запись, и чтение.
Не используйте Raw типы! Если аргумент типа не определен, то используйте wildcard .
Переменные типа
Когда мы записываем при объявлении класса или метода идентификатор в угловых скобках, например или , то создаем переменную типа. Переменная типа — это неквалифицированный идентификатор, который можно использовать в качестве типа в теле класса или метода. Переменная типа может быть ограничена сверху.
public static > T max(Collection coll) < T candidate = coll.iterator().next(); for (T elt : coll) < if (candidate.compareTo(elt) < 0) candidate = elt; >return candidate; >
В этом примере выражение T extends Comparable определяет T (переменную типа), ограниченную сверху типом Comparable . В отличие от wildcard, переменные типа могут быть ограничены только сверху (только extends ). Нельзя записать super . Кроме того, в этом примере T зависит от самого себя, это называется recursive bound — рекурсивная граница.
Вот еще пример из класса Enum:
public abstract class Enum>implements Comparable, Serializable
Здесь класс Enum параметризован типом E, который является подтипом от Enum .
Multiple bounds (множественные ограничения)
Multiple Bounds – множественные ограничения. Записывается через символ " & ", то есть мы говорим, что тип, представленный переменной типа T , должен быть ограничен сверху классом Object и интерфейсом Comparable .
> T max(Collection coll)
Вывод
Переменная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure).
Type Erasure
Type Erasure представляет собой отображение типов (возможно, включая параметризованные типы и переменные типа) на типы, которые никогда не являются параметризованными типами или переменными типами. Мы записываем затирание типа T как |T| .
- Затиранием параметризованного типа GT1. Tn> является |G|
- Затиранием вложенного типа T.C является |T|.C
- Затиранием типа массива T[] является |T|[]
- Затиранием переменной типа является затирание ее левой границы
- Затиранием любого иного типа является сам этот тип
- добавляет приведение типов для обеспечения type safety, если это необходимо
- генерирует Bridge методы для сохранения полиморфизма
| T (Тип) | |T| (Затирание типа) |
| List< Integer>, List< String>, List< List< String>> | List |
| List< Integer>[] | List[] |
| List | List |
| int | int |
| Integer | Integer |
| > | Comparable |
| > | Object |
| LinkedCollection.Node | LinkedCollection.Node |
Эта таблица показывает, во что превращаются разные типы в процессе затирания, Type Erasure.

На скриншоте ниже два примера программы:
Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему?
В Java два разных метода не могут иметь одну и ту же сигнатуру. В процессе Type Erasure компилятор добавит bridge-метод public int compareTo(Object o) . Но в классе уже содержится метод с такой сигнатурой, что и вызовет ошибку во время компиляции.
Скомпилируем класс Name, удалив метод compareTo(Object o) , и посмотрим на получившийся байткод с помощью javap:
# javap Name.class Compiled from "Name.java" public class ru.sberbank.training.generics.Name implements java.lang.Comparable
Видим, что класс содержит метод int compareTo(java.lang.Object) , хотя мы его удалили из исходного кода. Это и есть bridge метод, который добавил компилятор.
Reifiable типы
- Примитивные типы (int, long, boolean)
- Непараметризованные (необобщенные) типы (String, Integer)
- Параметризованные типы, параметры которых представлены в виде unbounded wildcard (неограниченных символов подстановки) (List , Collection )
- Raw (несформированные) типы (List, ArrayList)
- Массивы, компоненты которых — Reifiable типы (int[], Number[], List[], List[)
Почему информация об одних типах доступна, а о других нет? Дело в том, что из-за процесса затирания типов компилятором информация о некоторых типах может быть потеряна. Если она потерялась, то такой тип будет уже не reifiable. То есть она во время выполнения недоступна. Если доступна – соответственно, reifiable.
Решение не делать все обобщенные типы доступными во время выполнения — это одно из наиболее важных и противоречивых проектных решений в системе типов Java. Так сделали, в первую очередь, для совместимости с существующим кодом. За миграционную совместимость пришлось платить — полная доступность системы обобщенных типов во время выполнения невозможна.
- Переменная типа (T)
- Параметризованный тип с указанным типом параметра (List ArrayList , List>)
- Параметризованный тип с указанной верхней или нижней границей (List, Comparable ). Но здесь стоит оговориться: List — не reifiable, а List — reifiable
И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception?
class MyException extends Exception
Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы.
class MyException extends Exception/ Generic class may not extend ‘java.lang.Throwable’ T t; >
Unchecked Warnings
Компиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем.
Heap Pollution
Как мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием " Heap Pollution " (загрязнение кучи). Вот пример:
static List t() < List l = new ArrayList(); l.add(1); List ls = l; // (1) ls.add(""); return ls; >
В строке (1) компилятор предупреждает об «Unchecked assignment».
Нужно привести и другой пример «загрязнения кучи» — когда у нас используются параметризованные объекты. Кусок кода ниже наглядно показывает, что недопустимо использовать параметризованные типы в качестве аргументов метода с использованием Varargs . В данном случае параметр метода m – это List… , т.е. фактически, массив элементов типа List . Учитывая правило отображения типов при затирании, тип stringLists превращается в массив raw списков ( List[] ), т.е. можно выполнить присваивание Object[] array = stringLists; и после записать в array объект, отличный от списка строк (1), что вызовет ClassCastException в строке (2).
static void m(List. stringLists) < Object[] array = stringLists; ListtmpList = Arrays.asList(42); array[0] = tmpList; // (1) String s = stringLists[0].get(0); // (2) >
Рассмотрим еще один пример:
ArrayList strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // (1) Ok arrayList.add(1); // (2) unchecked call
Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число.
Reflection
Хотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection.
- Все reifiable доступны через механизм Reflection
- Информация о типе полей класса, параметров методов и возвращаемых ими значений доступна через Reflection.
java.lang.reflect.Method.getGenericReturnType()
С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код:
List ints = new ArrayList(); Class k = ints.getClass(); assert k == ArrayList.class;
Переменная ints имеет тип List и она содержит ссылку на объект типа ArrayList < Integer>. Тогда ints.getClass() вернёт объект типа Class , так как List затирается в List . Объект типа Class можно присвоить переменной k типа Class , согласно ковариантности символов подстановки? extends . А ArrayList.class возвращает объект типа Class .
Вывод
Если информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable.
Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы.
Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса.
Type Inference
Термин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода:
List list = new ArrayList();
С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList :
List list = new ArrayList<>();
Компилятор выведет тип ArrayList из контекста – List . Этот процесс и называется type inference .
- Приведение (reduction)
- Объединение (incorporation)
- Разрешение (resolution)
Предположим у нас есть вот такой класс, который описывает связный список:
class List < static List nil() < . >; static List cons(Z head, List tail) < . >; E head() < . >>
Результат обобщенного метода List.nil() может быть выведен из правой части:
List ls = List.nil();
Механизм выбора типа компилятором показывает, что аргумент типа для вызова List.nil() действительно String — это работает в JDK 7, все хорошо.
Выглядит разумно, что компилятор также должен иметь возможность вывести тип, когда результат такого вызова обобщенного метода передается другому методу в качестве аргумента, например:
List.cons(42, List.nil()); //error: expected List, found List
В JDK 7 мы получили бы compile-time error. А в JDK 8 скомпилируется. Это и есть первая часть JEP-101, его первая цель — вывод типа в позиции аргумента. Единственная возможность осуществить это в версиях до JDK 8 — использовать явный аргумент типа при вызове обобщенного метода:
List.cons(42, List.nil());
Вторая часть JEP-101 говорит о том, что неплохо бы выводить тип в цепочке вызовов обобщенных методов, например:
String s = List.nil().head(); //error: expected String, found Object
Но данная задача не решена до сих пор, и вряд ли в ближайшее время появится такая функция. Возможно, в будущих версиях JDK необходимость в этом исчезнет, но пока нужно указывать аргументы вручную:
String s = List.nil().head();
После выхода JEP 101 на StackOverflow появилось множество вопросов по теме. Программисты спрашивают, почему код, который выполнялся на 7-й версии, на 8-й выполняется иначе – или вообще не компилируется? Вот пример такого кода:
class Test < static void m(Object o) < System.out.println("one"); >static void m(String[] o) < System.out.println("two"); >static T g() < return null; >public static void main(String[] args) < m(g()); >>
Посмотрим на байт-код после компиляции на JDK1.8:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: invokestatic #6 // Method g:()Ljava/lang/Object; 3: checkcast #7 // class "[Ljava/lang/String;" 6: invokestatic #8 // Method m:([Ljava/lang/String;)V 9: return LineNumberTable: line 15: 0 line 16: 9
Инструкция под номером 0 выполняет вызов метода g:()Ljava/lang/Object; Метод возвращает java.lang.Object . Далее, инструкция 3 производит приведение типа («кастинг») объекта, полученного на предыдущем шаге к типу массива java.lang.String , и инструкция 6 выполняет метод m:([Ljava/lang/String;) , что и напечатает в консоли «two».
А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7:
public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: invokestatic #6 // Method g:()Ljava/lang/Object; 3: invokestatic #7 // Method m:(Ljava/lang/Object;)V 6: return LineNumberTable: line 15: 0 line 16: 6
Мы видим, что здесь нет инструкции checkcast , которую добавила Java 8, так что вызовется метод m:(Ljava/lang/Object;) , а в консоли напечатается «one». Checkcast – результат нового выведения типа, который был усовершенствован в Java 8.
Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить.
Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную:
public static void main(String[] args)
Заключение
На этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы:
- Naftalin, Maurice; Wadler, Philip. Java Generics and Collections. O'Reilly Media. ISBN-13: 978-0596527754
- https://docs.oracle.com/javase/specs/jls/se8/html/index.html
- Язык программирования Java SE 8. Addison-Wesley. ISBN: 978-5-8459-1875-8
- Bloch, Joshua. Effective Java. Third Edition. Addison-Wesley. ISBN-13: 978-0-13-468599-1
Дженерики в Java для тех, кто постарше: стирание типов, наследование и принцип PECS
Рассказываем, как в любой непонятной ситуации правильно сочетать дженерик-типы.



Екатерина Степанова
Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.
В предыдущей статье «Дженерики для самых маленьких» мы рассказали о том, что такое дженерики (generics), зачем они нужны и как создавать дженерик-типы и методы. Там же говорили про ограничения (boundings) и wildcards. Без этих основ вам будет сложно разобраться с тем, что написано дальше. Поэтому освежите знания, если это необходимо.
Из этой статьи вы узнаете:
- почему ни один дженерик не доживает до выполнения программы;
- как создать наследника дженерик-класса;
- что не так с дженерик-типами классов-наследников;
- как переопределить метод с дженерик-типами;
- как wildcards с ограничениями «портят» коллекции и зачем нужен принцип PECS.
Почему ни один дженерик не доживает до выполнения программы
Воспользуемся примером из первой части рассказа о дженериках: там был класс Box — коробка для сбора мусора: можно было положить в неё или извлечь из неё только объект определённого типа:

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

Теперь точно не запутаетесь 🙂
Ещё больше хитростей дженериков и других особенностей Java — на курсе «Профессия Java-разработчик». Научим программировать на самом востребованном языке и поможем устроиться на работу.
Дженерики (Generics) в java
Начиная с JDK 1.5, в Java появились новые возможности для программирования. Одним из таких нововведений являются Generics. Generics являются аналогией с конструкцией “Шаблонов”(template) в С++, но имеет свои нюансы. Generics позволяют абстрагировать множество типов. Наиболее распространенными примерами являются Коллекции.
Вот типичное использование такого рода (без Generics):
1. List myIntList = new LinkedList(); 2. myIntList.add(new Integer(0)); 3. Integer x = (Integer) myIntList.iterator().next();
Как правило, программист знает, какие данные должны быть в List’e. Тем не менее, стоит обратить особое внимание на Приведение типа (“Cast”) в строчке 3. Компилятор может лишь гарантировать, что метод next() вернёт Object, но чтобы обеспечить присвоение переменной типа Integer правильным и безопасным, требуется Cast. Cast не только создает беспорядки, но дает возможность появление ошибки “Runtime Error” из-за невнимательности программиста.
И появляется такой вопрос: “Как с этим бороться? ” В частности: “Как же зарезервировать List для определенного типа данных?”
Как раз такую проблему решают Generics.
1. List myIntList = new LinkedList (); 2. myIntList.add(new Integer(0)); 3. Integer x = myIntList.iterator().next();
Обратите внимание на объявления типа для переменной myIntList. Он указывает на то, что это не просто произвольный List, а List. Мы говорим, что List является generic-интерфейсом, который принимает параметр типа – в этом случае, Integer. Кроме того, необходимо обратить внимание на то, что теперь Cast выполняется в строчке 3 автоматически.
Некоторые могут задуматься, что беспорядок в коде увеличился, но это не так. Вместо приведения к Integer в строчке 3, у нас теперь есть Integer в качестве параметра в строчке 1. Здесь существенное отличие. Теперь компилятор может проверить этот тип на корректность во время компиляции.
И когда мы говорим, что myIntList объявлен как List, это будет справедливо во всем коде и компилятор это гарантирует.
Эффект от Generics особенно проявляется в крупных проектах: он улучшает читаемость и надежность кода в целом.
- Свойства
- Пример реализации Generic-класса
- Несовместимость generic-типов
- Проблемы реализации Generics
- Ограничения Generic
- Преобразование типов
- Примеры кода
Свойства
- Строгая типизация
- Единая реализация
- Отсутствие информации о типе
Пример реализации Generic-класса
public interface List < E get(int i); set(int i, E e); add(E e); Iteratoriterator(); … >
Для того чтобы использовать класс как Generics, мы должны прописать после имени класса , куда можно подставить любое имя, wildcard и т.д.
После того как было объявлено имя generic-типа его можно использовать как обычный тип внутри метода. И когда в коде будет объявлен, к примеру, List, то Е станет Integer для переменной list (как показано ниже).
Теперь рассмотрим чем старая реализация кода отличается от новой:
List ─ список элементов E
List list = new List(); list.add(new Integer(1)); Integer i = (Integer) list.get(0);
List list = new List(); list.add(new Integer(1)); Integer i = list.get(0);
Как видите, больше не нужно приводить Integer, так как метод get() возвращает ссылку на объект конкретного типа (в данном случае – Integer).
Несовместимость generic-типов
Это одна из самых важных вещей, которую вы должны узнать о Generics
Как говориться: “В бочке мёда есть ложка дегтя”. Для того чтобы сохранить целостности и независимости друг от друга Коллекции, у Generics существует так называемая “Несовместимость generic-типов”.
Пусть у нас есть тип Foo, который является подтипом Bar, и еще G - наследник Коллекции. То G не является наследником G.
List li = new ArrayList(); List lo = li;
lo.add(“hello”); // ClassCastException: String -> int Integer li = lo.get(0);
Проблемы реализации Generics
- Решение 1 – Wildcard
Пусть мы захотели написать метод, который берет Collection и выводит на экран. И мы захотели вызвать dump для Integer.
void dump(Collection c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Object o = i.next(); System.out.println(o); >>
List l; dump(l); List l; dump(l); // Ошибка
В этом примере List не может использовать метод dump, так как он не является подтипом List.
Проблема в том что эта реализация кода не эффективна, так как Collection не является полностью родительской коллекцией всех остальных коллекции, грубо говоря Collection имеет ограничения.
Для решения этой проблемы используется Wildcard (“?”). Он не имеет ограничения в использовании(то есть имеет соответствие с любым типом) и в этом его плюсы. И теперь, мы можем назвать это с любым типом коллекции.
void dump(Collection c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Object o = i.next(); System.out.println(o); >>
- Решение 2 – Bounded Wildcard
Пусть мы захотели написать метод, который рисует List. И у Shape есть наследник Circle. И мы хотим вызвать draw для Circle.
void draw(List c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Shape s = i.next(); s.draw(); >>
List l; draw(l); List l; draw(l); // Ошибка
Проблема в том, что у нас не получиться из-за несовместимости типов. Предложенное решение используется, если метод который нужно реализовать использовал бы определенный тип и его подтипов. Так называемое “Ограничение сверху”. Для этого нужно вместо прописать .
void draw(List c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Shape s = i.next(); s.draw(); >>
- Решение 3 – Generic-Метод
Пусть вы захотели сделать метод, который берет массив Object и переносить их в коллекцию.
void addAll(Object[] a, Collection c) < for (int i = 0; i < a.length; i++) < c.add(a[i]); >>
addAll(new String[10], new ArrayList()); addAll(new Object[10], new ArrayList()); addAll(new Object[10], new ArrayList()); // Ошибка addAll(new String[10], new ArrayList()); // Ошибка
Напомним, что вы не можете просто засунуть Object в коллекции неизвестного типа. Способ решения этой проблемы является использование “Generic-Метод” Для этого перед методом нужно объявить и использовать его.
void addAll(T[] a, Collection c) < for (int i = 0; i < a.length; i++) < c.add(a[i]); >>
Но все равно после выполнение останется ошибка в третьей строчке :
addAll(new Object[10], new ArrayList()); // Ошибка
- Решение 4 – Bounded type argument
Реализуем метод копирование из одной коллекции в другую
void addAll(Collection c, Collection c2) < for (Iteratori = c.iterator(); i.hasNext(); ) < M o = i.next(); c2.add(o); >>
addAll(new AL(), new AL()); addAll(new AL(), new AL()); //Ошибка
Проблема в том что две Коллекции могут быть разных типов (несовместимость generic-типов). Для таких случаев было придуман Bounded type argument. Он нужен если метод ,который мы пишем использовал бы определенный тип данных. Для этого нужно ввести (N принимает только значения M). Также можно корректно писать . (Принимает значения нескольких переменных)
void addAll(Collection c, Collection c2) < for (Iteratori = c.iterator(); i.hasNext(); ) < N o = i.next(); c2.add(o); >>
- Решение 5 – Lower bounded wcard
Реализуем метод нахождение максимума в коллекции.
> T max(Collection c)
List il; Integer I = max(il); class Test implements Comparable List tl; Test t = max(tl); // Ошибка
- > обозначает что Т обязан реализовывать интерфейс Comparable.
Ошибка возникает из за того что Test реализует интерфейс Comparable. Решение этой проблемы – Lower bounded wcard(“Ограничение снизу”). Суть в том что мы будет реализовывать метод не только для Т, но и для его Супер-типов(Родительских типов). Например: Если мы напишем
List list;
Мы можем заполнить его List, List или List.
> T max(Collection c)
- Решение 6 – Wildcard Capture
Реализуем метод Swap в List
void swap(List list, int i, int j) < list.set(i, list.get(j)); // Ошибка >
Проблема в том, что метод List.set() не может работать с List, так как ему не известно как он List. Для решение этой проблемы используют “Wildcard Capture” (или “Capture helpers”). Суть заключается в том, чтобы обмануть компилятор. Напишем еще один метод с параметризованной переменной и будем его использовать внутри нашего метода.
void swap(List list, int i, int j) < swapImpl(list, i, j); > void swapImpl(List list, int i, int j)
Ограничения Generic
Также нужно запомнить простые правила для работы с Generics.
- Невозможно создать массив параметра типа
Collection c; T[] ta; new T[10]; // Ошибка !!
- Невозможно создать массив Generic-классов
new ArrayList>(); List[] la = new List[10]; // Ошибка !!
Преобразование типов
В Generics также можно манипулировать с информацией, хранящийся в переменных.
- Уничтожение информации о типе
List l = new ArrayList();
- Добавление информации о типе
List l = (List) new ArrayList(); List l1 = new ArrayList();
Примеры кода
- Первый пример:
List ls; List li; ls.getClass() == li.getClass() // True ls instanceof List // True ls instanceof List // Запрещено
- Второй пример:
Нахождение максимума в Коллекции Integer.
Collection c; Iterator i = c.iterator(); Integer max = (Integer) i.next(); while(i.hasNext()) < Integer next = (Integer) i.next(); if (next.compareTo(max) > 0) < max = next; >>
- С помощью Generics
Collection c; Iterator i = c.iterator(); Integer max = i.next(); while(i.hasNext()) < Integer next = i.next(); if (next.compareTo(max) >0) < max = next; >>
Вам також може сподобатися

Уроки по android разработке на Java 4 131
Вернуться к уроку 4 Перейти к уроку 5 [mlw_quizmaster quiz=4]

Инструменты android разработчика 2 13 788
Что такое Realm? Изучая хранение данных в БД по урокам на нашем канале, вы

Разработка игр для android 11 8 707
Как создать игру для андроид в Android Studio? Знакомимся с LibGDX – мультиплатформенным фреймворком

Уроки по android разработке на Java 4 947
Смотрите вторую десятку видеоуроков по android-программированию. Вы узнаете: 1. Как использовать ресурсы android приложения