Учебник по дженерикам Java

Generics в java были введены как одна из функций в JDK 5. «Java Generics» — это технический термин, обозначающий набор языковых функций, связанных с определением и использованием универсальных типов и методов. В Java универсальные типы или методы отличаются от обычных типов и методов тем, что у них есть параметры типа.

«Java Generics — это языковая функция, которая позволяет определять и использовать универсальные типы и методы».

Универсальные типы создаются для формирования параметризованных типов путем предоставления фактических аргументов типа, которые заменяют формальные параметры типа.

public class LinkedList<E>...LinkedList<String> list = new LinkedList();
  • Такой класс, как LinkedList<E>, представляет собой универсальный тип, имеющий параметр типа E.
  • Такие экземпляры, как LinkedList<Integer> или LinkedList<String>, называются параметризованными типами.
  • String и Integer — это соответствующие фактические аргументы типа.

1. Введение в дженерики

Если вы внимательно посмотрите на классы фреймворка коллекций, то вы увидите, что большинство классов принимают параметр/аргумент типа Object и возвращают значения из методов как Object. Теперь, в этой форме, они могут принимать любой тип Java в качестве аргумента и возвращать то же самое. Они по сути неоднородны, т.е. не имеют определенного похожего типа.

Программисты, такие как мы, часто хотели указать, что коллекция содержит элементы только определенного типа, например Integer или String или Employee. В исходной структуре коллекций наличие однородных коллекций было невозможно без добавления дополнительных проверок перед добавлением некоторых проверок в код. Для устранения этого ограничения были введены дженерики, чтобы быть очень конкретными. Они добавляют этот тип проверки параметров в ваш код во время компиляции, автоматически. Это избавляет нас от написания большого количества ненужного кода, который на самом деле не добавляет никакой ценности во время выполнения, если написан правильно.

«Говоря простым языком, обобщения обеспечивают безопасность типов в языке Java».

Без этого типа безопасности ваш код мог быть заражен различными ошибками, которые обнаруживаются только во время выполнения. Использование дженериков делает их выделенными во время компиляции и делает наш код надежным еще до того, как вы получите байт-код ваших файлов исходного кода Java.

«Дженерик повышает стабильность вашего кода, позволяя обнаруживать больше ошибок во время компиляции».

Итак, теперь у нас есть четкое представление о том, почему дженерики присутствуют в Java в первую очередь. Следующий шаг — получить некоторые знания о том, как они работают в Java. Что на самом деле происходит, когда вы используете дженерики в исходном коде?

2. Как работают дженерики?

2.1 Тип безопасности

В основе дженериков лежит « безопасность типов ». Что именно такое безопасность типов? Это просто гарантия компилятора, что если правильные типы используются в правильных местах, то не должно быть никаких ClassCastException во время выполнения.

Примером использования может быть список Integer, т.е. List<Integer>. Если вы объявляете список как List<Integer>, то Java гарантирует, что обнаружит и сообщит о любой попытке вставить любой нецелочисленный тип в указанный выше список.

List<Integer> list = new ArrayList<>();list.add(1);list.add("one"); //compiler error

2.2. Стирание типа

Другим важным термином в дженериках является « стирание типа ». По сути, это означает, что вся дополнительная информация, добавленная с помощью дженериков в исходный код, будет удалена из байт-кода, сгенерированного из него. Внутри байт-кода будет старый синтаксис Java, который вы получите, если вообще не будете использовать дженерики. Это обязательно поможет в генерации и выполнении кода, написанного до Java 5, когда дженерики не были добавлены в язык.

Давайте разберемся на примере.

List<Integer> list = new ArrayList<>();list.add(1000);

Если сравнить байт-код вышеприведенного примера с/без дженериков, то разницы не будет. Очевидно, что компилятор удалил всю информацию о дженериках. Таким образом, вышеприведенный код очень похож на нижеприведенный код без дженериков.

List list = new ArrayList();list.add(1000);

«Точно, дженерики в Java — это не что иное, как синтаксический сахар для вашего кода для обеспечения безопасности типов, и вся такая информация о типах стирается функцией стирания типов компилятором».

3. Типы дженериков

Теперь у нас есть некоторое понимание того, что такое дженерики. Теперь начнем изучать другие важные концепции, связанные с дженериками. Я начну с определения различных способов, которыми дженерики могут применяться к исходному коду.

3.1 Класс или интерфейс

Класс является обобщенным, если он объявляет одну или несколько переменных типа. Эти переменные типа известны как параметры типа класса. Давайте разберемся на примере.

DemoClass — это простой класс, имеющий одно свойство t(их может быть несколько); тип свойства — Object.

class DemoClass {private Object t;public void set(Object t) { this.t = t; }public Object get() { return t; }}

Здесь мы хотим, чтобы после инициализации класса определенным типом класс использовался только с этим конкретным типом. Например, если мы хотим, чтобы один экземпляр класса содержал значение t типа «String», то программист должен установить и получить только тип String.

Поскольку мы объявили тип свойства как Object, нет способа принудительно применить это ограничение. Программист может задать любой объект и может ожидать любой тип возвращаемого значения от метода get(), поскольку все типы Java являются подтипами класса Object.

Чтобы обеспечить соблюдение этого типа ограничений, мы можем использовать дженерики, как показано ниже:

class DemoClass<T> {//T stands for "Type"private T t;public void set(T t) { this.t = t; }public T get() { return t; }}

Теперь мы можем быть уверены, что класс не будет неправильно использован с неправильными типами. Пример использования DemoClass будет выглядеть так:

DemoClass<String> instance = new DemoClass<>();instance.set("lokesh"); //Correct usageinstance.set(1); //This will raise compile time error

Аналогия выше верна и для интерфейсов. Давайте быстро рассмотрим пример, чтобы понять, как информация о типе generics может использоваться в интерфейсах.

//Generic interface definitioninterface DemoInterface<T1, T2>{T2 doSomeOperation(T1 t);T1 doReverseOperation(T2 t);}//A class implementing generic interfaceclass DemoClass implements DemoInterface<String, Integer>{public Integer doSomeOperation(String t){//some code}public String doReverseOperation(Integer t){//some code}}

Надеюсь, я достаточно ясно объяснил, чтобы пролить свет на общие классы и интерфейсы. Теперь пришло время рассмотреть общие методы и конструкторы.

3.2 Метод или конструктор

Универсальные методы во многом похожи на универсальные классы. Они отличаются только в одном аспекте: область действия информации о типе находится только внутри метода(или конструктора). Универсальные методы — это методы, которые вводят свои собственные параметры типа.

Давайте разберем это на примере. Ниже приведен пример кода универсального метода, который можно использовать для поиска всех вхождений параметра типа в списке переменных только этого типа.

public static <T> int countAllOccurrences(T[] list, T item) {int count = 0;if(item == null) {for( T listItem : list )if(listItem == null)count++;}else {for( T listItem : list )if(item.equals(listItem))count++;}return count;} 

Если вы передадите список String и другую строку для поиска в этом методе, он будет работать нормально. Но если вы попытаетесь найти Number в списке String, он выдаст ошибку времени компиляции.

То же самое, что и выше, может быть примером универсального конструктора. Давайте также рассмотрим отдельный пример для универсального конструктора.

public static <T> int countAllOccurrences(T[] list, T item) {int count = 0;if(item == null) {for( T listItem : list )if(listItem == null)count++;}else {for( T listItem : list )if(item.equals(listItem))count++;}return count;} 

В этом примере конструктор класса Dimension также имеет информацию о типе. Таким образом, вы можете иметь экземпляр dimension со всеми атрибутами только одного типа.

4. Универсальные массивы

Массивы в любом языке имеют одинаковое значение, т. е. массив — это коллекция похожих типов элементов. В Java добавление любого несовместимого типа в массив во время выполнения вызовет исключение ArrayStoreException. Это означает, что массив сохраняет информацию о своем типе во время выполнения, а обобщенные типы используют стирание типа или удаляют любую информацию во время выполнения. Из-за вышеуказанного конфликта создание экземпляра обобщенного массива не допускается.

public class GenericArray<T> {// this one is finepublic T[] notYetInstantiatedArray;// causes compiler error; Cannot create a generic array of Tpublic T[] array = new T[5];}

В той же строке, что и вышеприведенные классы и методы обобщенных типов, мы можем иметь обобщенные массивы. Поскольку мы знаем, что массив — это коллекция элементов схожих типов, и добавление любого несовместимого типа вызовет исключение ArrayStoreException во время выполнения; чего не происходит с классами Collection.

Object[] array = new String[10];array[0] = "lokesh";array[1] = 10; //This will throw ArrayStoreException

Ошибку выше сделать не так уж и сложно. Она может случиться в любое время. Поэтому лучше также предоставить массиву информацию о типе, чтобы ошибка была обнаружена во время компиляции.

Другая причина, по которой массивы не поддерживают обобщения, заключается в том, что массивы ковариантны, что означает, что массив ссылок супертипа является супертипом массива ссылок подтипа. То есть Object[] является супертипом String[], а к массиву строк можно получить доступ через ссылочную переменную типа Object[].

Object[] objArr = new String[10]; // fineobjArr[0] = new String();

5. Дженерики с подстановочными знаками

В универсальном коде вопросительный знак(?), называемый подстановочным знаком, представляет неизвестный тип. Подстановочный параметризованный тип — это инстанциация универсального типа, где по крайней мере один аргумент типа является подстановочным знаком. Примерами подстановочных параметризованных типов являются Collection<?<, List<? extends Number<, Comparator<? super String> и Pair<String,?>. Подстановочный знак может использоваться в различных ситуациях: как тип параметра, поля или локальной переменной; иногда как возвращаемый тип(хотя лучшей практикой программирования является более конкретная информация). Подстановочный знак никогда не используется как аргумент типа для вызова универсального метода, создания экземпляра универсального класса или супертипа.

Наличие джокеров в разных местах также имеет разное значение. Например:

  • Коллекция обозначает все экземпляры интерфейса Collection независимо от аргумента типа.
  • Список обозначает все типы списков, где тип элемента является подтипом числа.
  • Comparator<? super String< обозначает все экземпляры интерфейса Comparator для типов аргументов типа, которые являются супертипами String.

Параметризованный тип с подстановочными знаками не является конкретным типом, который может появиться в новом выражении. Он просто намекает на правило, навязываемое дженериками, какие типы допустимы в любом конкретном сценарии, где используются подстановочные знаки.

Например, ниже приведены допустимые объявления с использованием джокеров:

Collection<?> coll = new ArrayList<String>();//ORList<? extends Number> list = new ArrayList<Long>();//ORPair<String,?> pair = new Pair<String,Integer>();

Ниже приведены примеры недопустимого использования подстановочных знаков, которые приведут к ошибке компиляции.

 List<? extends Number> list = new ArrayList<String>(); //String не является подклассом Number; поэтому ошибка//ИЛИComparator<? super String> cmp = new RuleBasedCollator(new Integer(100)); //Integer не является суперклассом String

Подстановочные знаки в дженериках могут быть как неограниченными, так и ограниченными. Давайте определим разницу в различных терминах.

5.1 Неограниченный подстановочный параметризованный тип

Универсальный тип, где все аргументы типа являются неограниченным подстановочным знаком «?» без каких-либо ограничений на переменные типа. Например:

ArrayList<?> list = new ArrayList<Long>();//orArrayList<?> list = new ArrayList<String>();//orArrayList<?> list = new ArrayList<Employee>(); 

5.2 Ограниченный подстановочный параметризованный тип

Ограниченные подстановочные знаки накладывают некоторые ограничения на возможные типы, которые мы можем использовать для создания экземпляра параметризованного типа. Это ограничение обеспечивается с помощью ключевых слов «super» и «extends». Чтобы различить их более четко, давайте разделим их на подстановочные знаки с верхней границей и подстановочные знаки с нижней границей.

5.3. Верхние граничные символы подстановки

Например, предположим, что вы хотите написать метод, который работает с List<String>, List<Integer> и List<double>, вы можете добиться этого, используя верхний ограниченный подстановочный знак, например, вы могли бы указать List<? extends Number>. Здесь Integer и Double являются подтипами класса Number. Проще говоря, если вы хотите, чтобы универсальное выражение принимало все подклассы определенного типа, вы будете использовать верхний ограниченный подстановочный знак с помощью ключевого слова «extends».

public class GenericsExample<T>{public static void main(String[] args){//List of IntegersList<Integer> ints = Arrays.asList(1,2,3,4,5);System.out.println(sum(ints));//List of DoublesList<Double> doubles = Arrays.asList(1.5d,2d,3d);System.out.println(sum(doubles));List<String> strings = Arrays.asList("1","2");//This will give compilation error as :: The method sum(List<? extends Number>) in the//type GenericsExample<T> is not applicable for the arguments(List<String>)System.out.println(sum(strings));}//Method will acceptprivate static Number sum(List<? extends Number> numbers){double s = 0.0;for(Number n : numbers)s += n.doubleValue();return s;}}

5.4 Нижние граничные джокеры

Если вы хотите, чтобы универсальное выражение принимало все типы, которые являются «супер»-типом определенного типа ИЛИ родительским классом определенного класса, то для этой цели вы будете использовать подстановочный знак нижней границы, используя ключевое слово «super».

В приведенном ниже примере я создал три класса, а именно SuperClass, ChildClass и GrandChildClass. Их связь показана в коде ниже. Теперь нам нужно создать метод, который каким-то образом получит информацию GrandChildClass(например, из БД) и создаст его экземпляр. И мы хотим сохранить этот новый GrandChildClass в уже существующем списке GrandChildClasses.

Проблема в том, что GrandChildClass также является подтипом ChildClass и SuperClass. Поэтому любой общий список SuperClasses и ChildClasses также может содержать GrandChildClasses. Здесь мы должны воспользоваться помощью подстановочного знака с нижней границей, используя ключевое слово 'super'.

public class GenericsExample<T>{public static void main(String[] args){//List of grand childrenList<GrandChildClass> grandChildren = new ArrayList<GrandChildClass>();grandChildren.add(new GrandChildClass());addGrandChildren(grandChildren);//List of grand childsList<ChildClass> childs = new ArrayList<ChildClass>();childs.add(new GrandChildClass());addGrandChildren(childs);//List of grand supersList<SuperClass> supers = new ArrayList<SuperClass>();supers.add(new GrandChildClass());addGrandChildren(supers);}public static void addGrandChildren(List<? super GrandChildClass> grandChildren){grandChildren.add(new GrandChildClass());System.out.println(grandChildren);}}class SuperClass{}class ChildClass extends SuperClass{}class GrandChildClass extends ChildClass{}

6. Что не допускается в дженериках?

До сих пор мы узнали о ряде вещей, которые можно сделать с помощью generics, чтобы избежать множества экземпляров ClassCastException в вашем приложении. Мы также увидели использование подстановочных знаков. Теперь пришло время определить некоторые задачи, которые не разрешено выполнять в generics.

6.1. У нас не может быть статического поля типа

Мы не можем определить статический параметризованный член generic в вашем классе. Любая попытка сделать это приведет к ошибке времени компиляции: Невозможно создать статическую ссылку на нестатический тип T.

public class GenericsExample<T>{private static T member; //This is not allowed}

6.2 Мы не можем создать экземпляр T

Любая попытка создать экземпляр T завершится ошибкой: Невозможно создать экземпляр типа T.

public class GenericsExample<T>{public GenericsExample(){new T();}}

6.3. Дженерики несовместимы с примитивами в объявлениях.

Да, это правда. Вы не можете объявлять обобщенные выражения, такие как List или Map<String, double>. Определенно, вы можете использовать классы-обертки вместо примитивов, а затем использовать примитивы при передаче фактических значений. Эти примитивы значений принимаются с помощью автоупаковки для преобразования примитивов в соответствующие классы-обертки.

final List<int> ids = new ArrayList<>(); //Not allowedfinal List<Integer> ids = new ArrayList<>(); //Allowed

6.4 Мы не можем создать универсальный класс исключений

Иногда программисту может понадобиться передать экземпляр универсального типа вместе с выбрасыванием исключения. Это невозможно сделать в Java.

// causes compiler errorpublic class GenericException<T> extends Exception {}

При попытке создать такое исключение вы получите следующее сообщение: Универсальный класс GenericException не может быть подклассом java.lang.Throwable.

На этом все, закрывая обсуждение дженериков Java на этот раз. Я расскажу о более интересных фактах и особенностях, связанных с дженериками, в следующих постах.

Напишите мне в комментариях, если что-то непонятно или у вас есть другие вопросы.

Прокрутить вверх