Что такое generic
Перейти к содержимому

Что такое generic

  • автор:

Generics

Начиная с 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-класса

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 («?»). Он не имеет ограничения в использовании(то есть имеет соответствие с любым типом) и в этом его плюсы. И теперь, мы можем вызвать dump с любым типом коллекции.

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; >>

Generics

Дженерики (generics) в языке программирования Java — это сущности, которые могут хранить в себе данные только определенного типа. Например, список элементов, в котором могут быть одни числа. Но не только: дженерик — обобщенный термин для разных структур.

Освойте профессию «Java-разработчик»

Можно представить дженерик как папку для бумаг, куда нельзя положить ничего, кроме документов определенного формата. Это удобно: помогает разделить разные данные и не допустить ситуаций, когда в сущность передается что-то не то.

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

В разных источниках можно услышать про «тип-дженерик», «класс-дженерик» или «метод-дженерик». Это нормально, ведь обобщение и параметризация касаются всех этих сущностей, а generics — общий термин.

Для чего нужны дженерики

С дженериками работают программисты на Java. Без этой возможности писать код, который работает только с определенным видом данных, было сложнее. Существовало два способа, и оба неоптимальные:

  • указывать проверку типа вкоде. Например, получать данные — и сразу проверять, а если они не те, выдавать ошибку. Это помогло бы отсеять ненужные элементы. Но если бы класс понадобилось сделать более гибким, например, создать его вариацию для другого типа, его пришлось бы переписывать или копировать. Не получилось бы просто передать другой специальный параметр, чтобы тот же класс смог работать еще с каким-то типом;
  • полагаться на разработчиков. Например, оставлять в коде комментарий «Этот класс работает только с числами». Слишком велик риск, что кто-то не заметит комментарий и передаст в объект класса не те данные. И хорошо, если ошибка будет заметна сразу, а не уже на этапе тестирования.

Поэтому появились дженерики: они решают эту проблему, делают написание кода проще, а защиту от ошибок надежнее.

Профессия / 14 месяцев
Java-разработчик

Освойте востребованный язык

Group 1321314345 (4)

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

Чтобы вернее понять принцип работы, нужно представлять, как устроены сущности в Java. Есть классы — это как бы «чертежи» будущих сущностей, описывающие, что они делают. И есть объекты — экземпляры классов, непосредственно существующие и работающие. Класс — как схема машины, объект — как машина.

Когда разработчик создает дженерик-класс, он приписывает к нему параметр в треугольных скобках — метку. К примеру, так:

Теперь при создании объекта этого класса нужно будет указать на месте T название типа, с которым будет работать объект. Например, myClass для целых чисел или myClass для строк. Сам класс остается универсальным, то есть общим. А вот каждый его объект специфичен для своего типа.

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

Что такое raw types

В Java есть понятие raw types. Так называют дженерик-классы, из которых удалили параметр. То есть изначально класс описали как дженерик, но при создании объекта этого класса тип ему не передали. То есть что-то вроде myClass<> — тип не указан.

Дословно это название переводится как «сырые типы». Пользоваться ими сейчас в коммерческой разработке — чаще всего плохая практика. Но в мире все еще много старого кода, который написали до появления дженериков. Если такой код еще не успели переписать, в нем может быть очень много «сырых типов». Это надо учитывать, чтобы не возникало проблем с совместимостью.

Дженерики-классы и дженерики-методы

Выше мы говорили, что дженериками могут быть разные сущности. Разберемся подробнее:

  • дженерик-классы (generic classes) это классы, «схемы» объектов с параметром. При создании объекта ему передается тип, с которым он будет работать;
  • дженерик-методы (generics methods) это методы, работающие по такому же принципу. Метод — это функция внутри объекта, то, что он может делать. Методу тип передается при вызове, сразу перед аргументами. Так можно создавать более универсальные функции и применять одну и ту же логику к данным разного типа.

Кстати, дженериками могут быть и встроенные классы или методы, и те, которые разработчик пишет самостоятельно. Например, встроенный ArrayList — список-массив — работает как дженерик.

Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке

Что будет, если передать дженерику не тот тип

Если объекту класса-дженерика передать не тот тип, который указали при его объявлении, он выдаст ошибку. Например, если в ходе работы экземпляра myClass в нем попытаются сохранить дробное число или даже строку, программа не скомпилируется. Вместо этого разработчик увидит ошибку: неверный тип.

Эта ошибка отличается от тех, которые возникнут, если не пользоваться дженериками. По ней сразу ясно, из-за чего она возникла и как можно ее исправить. Кроме того, она появляется сразу. Поэтому код становится легче отлаживать.

А если отправить «не тот» тип объекту без дженерика, действия с ним выполнятся с ошибкой. Но по этой ошибке не всегда очевидно, чем она вызвана. Худший вариант — код успешно запустится, но сработает неправильно: так ошибку будет найти еще сложнее.

Особенности дженериков

У дженериков есть несколько особенностей, о которых стоит знать при работе с ними. Если не учитывать эти детали, программировать будет как минимум менее удобно. А как максимум можно допустить ошибку и не понять, куда она закралась.

Выведение типа. Эта особенность касается объявления экземпляра класса, то есть создания объекта. Полная запись создания будет выглядеть так:

myClass objectForIntegers = new myClass();

objectForIntegers — это название объекта, оно может быть любым. То, что находится после знака «равно», — непосредственно команда «создать новый экземпляр класса».

Но полная запись очень громоздкая. Поэтому современные компиляторы Java способны на выведение типа — автоматическую его подстановку в записи после первого упоминания. То есть конструкцию myClass понадобится написать только один раз.

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

myClass objectForIntegers = new myClass<>();

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

Стирание типов. Важная деталь, которая касается работы дженериков, — они существуют только на этапе компиляции. В этом их суть: «не пропускать» данные ненужного типа в объект, а такие вещи определяет компилятор.

После компиляции код на Java превращается в байт-код. И на этом уровне никаких дженериков нет. myClass и myClass в байт-коде будут идентичны, просто с разными данными внутри.

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

О стирании типов важно помнить. Для запущенной программы в байт-коде дженериков не существует, и это может вызвать ошибки. Например, при сравнении myClass и myClass программа скажет, что они одинаковые. А иногда в объект в запущенном коде и вовсе получается передать данные другого типа.

«Дикие карты». Еще одна интересная и полезная особенность дженериков — так называемые wildcards, или «дикие карты». Это термин из спорта, означающий особое приглашение спортсмена на соревнование в обход правил. А в карточных играх так называют карты, которые можно играть вместо других, например джокера.

В основе wildcards в Java лежит такая же идея: изменить предустановленное поведение и сделать что-то в обход установленных рамок. Когда объявляется «дикая карта», в треугольных скобках вместо названия типа ставится вопросительный знак. Это означает, что сюда можно подставить любой тип.

Подставить wildcard можно не везде. Например, при создании класса это сделать не получится, а при объявлении объекта этого класса — получится. Чаще всего «дикую карту» используют при работе с переменными и с коллекциями.

Ограниченные «дикие карты». Кроме стандартной wildcard, существует еще несколько типов — ограниченные «дикие карты». С их помощью можно передать в объект данные не только конкретного типа, но и унаследованных от него — «потомков». Или же «предков» — типов, от которых был унаследован упомянутый.

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

Есть два вида ограничений:

  • upper bounding — ограничение сверху. За вопросительным знаком следует слово extends и название типа. В такой дженерик можно передавать названный тип и его потомков;
  • lower bounding — ограничение снизу. Ситуация наоборот: за вопросительным знаком слово super и тип, а подставлять можно элементы этого типа и его предков.

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

Java-разработчик

Java уже 20 лет в мировом топе языков программирования. На нем создают сложные финансовые сервисы, стриминги и маркетплейсы. Освойте технологии, которые нужны для backend-разработки, за 14 месяцев.

картинка (67)

Статьи по теме:

Обобщения (Generic)

Обобщения — это параметризованные типы. С их помощью можно объявлять классы, интерфейсы и методы, где тип данных указан в виде параметра. Обобщения добавили в язык безопасность типов, немного на тему говорится в статье про ArrayList.

Рассмотрим пример с обобщением.

 class Gen  < T ob; // объявление объекта типа T // Передать конструктору ссылку на объект типа T Gen(T o) < ob = o; >// Вернуть ob T getob() < return ob; >// Показать тип T void showType() < System.out.println("Тип T: " + ob.getClass().getName()); >> // Код для кнопки // Работаем с обобщённым классом // Создаём Gen-ссылку для Integer Gen iOb; // Создаём объект Gen iOb = new Gen(77); // Показать тип данных, используемый iOb iOb.showType(); // Получить значение iOb int value = iOb.getob(); System.out.println("Значение " + value); // Создадим объект Gen для String Gen strOb = new Gen("Обобщённый текст"); // Показать тип данных, используемый strOb strOb.showType(); // Получить значение strOb String str = strOb.getob(); System.out.println("Значение: " + str); 

В результате мы получим:

Типом T является java.lang.Integer Значение: 77 Типом T является java.lang.String Значение: Обобщённый текст

Изучим код. Мы объявили класс в следующей форме:

class Gen

В угловых скобках используется T — имя параметра типа. Это имя используется в качестве заполнителя, куда будет подставлено имя реального типа, переданного классу Gen при создании реальных типов. То есть параметр типа T применяется в классе всякий раз, когда требуется параметр типа. Угловые скобки указывают, что параметр может быть обобщён. Сам класс при этом называется обобщённым классом или параметризованным типом.

Далее тип T используется для объявления объекта по имени ob:

 T ob; // объявляет объект типа T 

Вместо T подставится реальный тип, который будет указан при создании объекта класса Gen. Объект ob будет объектом типа, переданного в параметре типа T. Если в параметре T передать тип String, то экземпляр ob будет иметь тип String.

Рассмотрим конструктор Gen().

 Get(T o)

Параметр o имеет тип T. Это значит, что реальный тип параметра o определяется типом, переданным параметром типа T при создании объекта класса Gen.

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

 T getob()

В именах переменных типа принято использовать заглавные буквы. Обычно для коллекций используется буква E, буквами K и V — типы ключей и значение (Key/Value), а буквой T (и при необходимости буквы S и U) — любой тип.

Как использовать обобщённый класс. Можно создать версию класса Gen для целых чисел:

 Gen iOb; 

В угловых скобках указан тип Integer, т.е. это аргумент типа, который передаётся в параметре типа T класса Gen. Фактически мы создаём версию класса Gen, в которой все ссылки на тип T становятся ссылками на тип Integer.

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

 iOb = new Gen(77); 

Полная версия записи может быть такой:

 iOb = new Gen(new Integer(88)); 

Но такая запись избыточна, так как можно использовать автоматическую упаковку значения 77 в нужный формат.

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

 int value = iOb.getob().intValue(); // избыточный код 

Обобщения работают только с объектами. Поэтому нельзя использовать в качестве параметра элементарные типы вроде int или char:

 Gen intOb = new Gen(44); // нельзя! 

Хотя объекты iOb и strOb имеют тип Gen , они являются ссылками на разные типы и их сравнивать нельзя.

 iOB = strOb; // нельзя! 

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

Обобщённый класс с двумя параметрами

Можно указать два и более параметров типа через запятую.

 class TwoGen  < T ob1; V ob2; // Передать конструктору ссылки на объекты двух типов TwoGen(T o1, V o2) < ob1 = o1; ob2 = o2; >void showTypes() < System.out.println("Тип T: " + ob1.getClass().getName()); System.out.println("Тип V: " + ob2.getClass().getName()); >T getob1() < return ob1; >V getob2() < return ob2; >// Используем созданный класс TwoGen twogenObj = new TwoGen(77, "Обобщённый текст"); // Узнаем типы twogenObj.showTypes(); // Узнаем значения int value = twogenObj.getob1(); System.out.println("Значение: " + value); String str = twogenObj.getob2(); System.out.println("Значение: " + str); > 

Обобщённые методы

Никто не запрещает создавать и методы с параметрами и возвращаемыми значениями в виде обобщений.

 public static T getSomething(T. a)

Шаблоны аргументов

Шаблон аргументов указывается символом ? и представляет собой неизвестный тип.

 boolean sameAvg(Stats ob) . 

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

Мы можем ограничить диапазон объектов, указав суперкласс.

 public void addItems(ArrayList list) 

В этом случае можно использовать классы, которые могут быть наследниками AnimalDog, Cat. А String или Integer вы уже не сможете использовать.

Пример обобщённого метода

 public static T getMiddle(T. a) < return a[a.length / 2]; >

Переменная типа вводится после модификаторов и перед возвращаемым типом.

Отдельно стоит упомянуть новинку JDK 7, позволяющую сократить код.

 MyClass mcObj = new MyClass(33, "Meow"); // старый способ в JDK 6 MyClass mcObj = new MyClass<>(33, "Meow"); // новый способ в JDK 7 

Во второй строке используются только угловые скобки, без указания типов.

Помните, что нельзя создать экземпляр типа параметра.

 class Gen  < T ob; Gen() < ob = new T(); // Недопустимо >> 

Под Android вам часто придётся использовать списочный массив, который использует обобщещние.

 ArrayList catNames = new ArrayList(); 

В Java 7, которую можно использовать в проектах для KitKat и выше, существует укороченная запись, когда справа не указывается тип (см. выше). Компилятор сам догадается по левой части выражения.

 ArrayList catNames = new ArrayList<>(); 

Что такое generic

Обобщения или generics (обобщенные типы и методы) позволяют нам уйти от жесткого определения используемых типов. Рассмотрим проблему, в которой они нам могут понадобиться.

Допустим, мы определяем класс для представления банковского счета. К примеру, он мог бы выглядеть следующим образом:

class Account < private int id; private int sum; Account(int id, int sum)< this.id = id; this.sum = sum; >public int getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

Класс Account имеет два поля: id — уникальный идентификатор счета и sum — сумма на счете.

В данном случае идентификатор задан как целочисленное значение, например, 1, 2, 3, 4 и так далее. Однако также нередко для идентификатора используются и строковые значения. И числовые, и строковые значения имеют свои плюсы и минусы. И на момент написания класса мы можем точно не знать, что лучше выбрать для хранения идентификатора — строки или числа. Либо, возможно, этот класс будет использоваться другими разработчиками, которые могут иметь свое мнение по данной проблеме. Например, в качестве типа id они захотят использовать какой-то свой класс.

И на первый взгляд мы можем решить данную проблему следующим образом: задать id как поле типа Object, который является универсальным и базовым суперклассом для всех остальных типов:

public class Program < public static void main(String[] args) < Account acc1 = new Account(2334, 5000); // id - число int acc1Id = (int)acc1.getId(); System.out.println(acc1Id); Account acc2 = new Account("sid5523", 5000); // id - строка System.out.println(acc2.getId()); >> class Account < private Object id; private int sum; Account(Object id, int sum)< this.id = id; this.sum = sum; >public Object getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

В данном случае все замечательно работает. Однако тогда мы сталкиваемся с проблемой безопасности типов . Например, в следующем случае мы получим ошибку:

Account acc1 = new Account("2345", 5000); int acc1Id = (int)acc1.getId(); // java.lang.ClassCastException System.out.println(acc1Id);

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

Писать для каждого отдельного типа свою версию класса Account тоже не является хорошим решением, так как в этом случае мы вынуждены повторяться.

Эти проблемы были призваны устранить обобщения или generics. Обобщения позволяют не указывать конкретный тип, который будет использоваться. Поэтому определим класс Account как обобщенный:

class Account < private T id; private int sum; Account(T id, int sum)< this.id = id; this.sum = sum; >public T getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

С помощью буквы T в определении класса class Account мы указываем, что данный тип T будет использоваться этим классом. Параметр T в угловых скобках называется универсальным параметром , так как вместо него можно подставить любой тип. При этом пока мы не знаем, какой именно это будет тип: String, int или какой-то другой. Причем буква T выбрана условно, это может и любая другая буква или набор символов.

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

Метод getId() возвращает значение переменной id, но так как данная переменная представляет тип T, то данный метод также возвращает объект типа T: public T getId() .

Используем данный класс:

public class Program < public static void main(String[] args) < Accountacc1 = new Account("2345", 5000); String acc1Id = acc1.getId(); System.out.println(acc1Id); Account acc2 = new Account(2345, 5000); Integer acc2Id = acc2.getId(); System.out.println(acc2Id); > > class Account < private T id; private int sum; Account(T id, int sum)< this.id = id; this.sum = sum; >public T getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

При определении переменной даннного класса и создании объекта после имени класса в угловых скобках нужно указать, какой именно тип будет использоваться вместо универсального параметра. При этом надо учитывать, что они работают только с объектами, но не работают с примитивными типами. То есть мы можем написать Account , но не можем использовать тип int или double, например, Account . Вместо примитивных типов надо использовать классы-обертки: Integer вместо int, Double вместо double и т.д.

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

Account acc1 = new Account("2345", 5000);

В этом случае в качестве первого параметра в конструктор передается строка.

А второй объект использует тип int (Integer):

Account acc2 = new Account(2345, 5000);

Обобщенные интерфейсы

Интерфейсы, как и классы, также могут быть обобщенными. Создадим обобщенный интерфейс Accountable и используем его в программе:

public class Program < public static void main(String[] args) < Accountableacc1 = new Account("1235rwr", 5000); Account acc2 = new Account("2373", 4300); System.out.println(acc1.getId()); System.out.println(acc2.getId()); > > interface Accountable < T getId(); int getSum(); void setSum(int sum); >class Account implements Accountable < private String id; private int sum; Account(String id, int sum)< this.id = id; this.sum = sum; >public String getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

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

Вторая стратегия представляет определение обобщенного класса, который также использует тот же универсальный параметр:

public class Program < public static void main(String[] args) < Accountacc1 = new Account("1235rwr", 5000); Account acc2 = new Account("2373", 4300); System.out.println(acc1.getId()); System.out.println(acc2.getId()); > > interface Accountable < T getId(); int getSum(); void setSum(int sum); >class Account implements Accountable < private T id; private int sum; Account(T id, int sum)< this.id = id; this.sum = sum; >public T getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

Обобщенные методы

Кроме обобщенных типов можно также создавать обобщенные методы, которые точно также будут использовать универсальные параметры. Например:

public class Program< public static void main(String[] args) < Printer printer = new Printer(); String[] people = ; Integer[] numbers = ; printer.print(people); printer.print(numbers); > > class Printer < public void print(T[] items) < for(T item: items)< System.out.println(item); >> >

Особенностью обобщенного метода является использование универсального параметра в объявлении метода после всех модификаторов и перед типом возвращаемого значения.

public void print(T[] items)

Затем внутри метода все значения типа T будут представлять данный универсальный параметр.

При вызове подобного метода перед его именем в угловых скобках указывается, какой тип будет передаваться на место универсального параметра:

printer.print(people); printer.print(numbers);

Использование нескольких универсальных параметров

Мы можем также задать сразу несколько универсальных параметров:

public class Program < public static void main(String[] args) < Accountacc1 = new Account("354", 5000.87); String sum = acc1.getSum(); System.out.printf("Id: %s Sum: %f \n", id, sum); > > class Account < private T id; private S sum; Account(T id, S sum)< this.id = id; this.sum = sum; >public T getId() < return id; >public S getSum() < return sum; >public void setSum(S sum) < this.sum = sum; >>

В данном случае тип String будет передаваться на место параметра T, а тип Double — на место параметра S.

Обобщенные конструкторы

Конструкторы как и методы также могут быть обобщенными. В этом случае перед конструктором также указываются в угловых скобках универсальные параметры:

public class Program < public static void main(String[] args) < Account acc1 = new Account("cid2373", 5000); Account acc2 = new Account(53757, 4000); System.out.println(acc1.getId()); System.out.println(acc2.getId()); >> class Account< private String id; private int sum; Account(T id, int sum) < this.id = id.toString(); this.sum = sum; >public String getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

В данном случае конструктор принимает параметр id, который представляет тип T. В конструкторе его значение превращается в строку и сохраняется в локальную переменную.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *