Что такое Generics в Java?

JavaSpecificationRequest (JSR) 14 предлагает ввести универсальные типы и методы в язык программирования Java.С момента появления Java разработчики умоляли добавить в язык дженерики. Это номер один запрошенный запрос на улучшение (RFE) на Параде ошибок Java — разработчиков.

Дженерики использовались в других языках программирования в течение многих лет, и теперь они будут частью выпуска Java 1.5 «Tiger», который должен выйти в конце 2003 года.

Что такое дженерики? Они идут под другими названиями, которые вы, вероятно, слышали раньше, такими как параметризованные типы или шаблоны. Они позволяют программисту работать с общими, многоразовыми классами (такими как java.util.List, java.util.Map) безопасным способом.

Зачем они нужны?

Двумя основными преимуществами дженериков в Java являются:

  • Уменьшение количества приведений в вашей программе, тем самым уменьшая количество потенциальных ошибок в вашей программе.
  • Повышение четкости кода

Уменьшение кастинга

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

classDangerousCast {

public static void main(String[] args) {
Stack stack = new Stack();
stack.push(new Integer(1));
stack.push(«2»);
stack.push(new Integer(3));

while(!stack.isEmpty()) {
Integer integer = (Integer) stack.pop();
. . . .
}
}

}

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

classCompilerSavesTheDay {

public static void main(String[] args) {
Stack<Integer> stack = new Stack<Integer>();
stack.push(new Integer(1));
stack.push(«2»); // Compiler Error generated by this call.
stack.push(new Integer(3));

while(!stack.isEmpty()) {
Integerinteger = stack.pop();
. . . .
}
}
}

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

Повышение четкости кода

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

publicVectorgetAccounts();

publicVector<Account>getAccounts();

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

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

// Создание экземпляра структуры данных
Listlist = newLinkedList();
list.add(new LinkedList());

// Добавьте к нему value
((List) list.get(0)).add(«value»);

// Получить значение
String value = (String) ((List) list.get(0)).get(0);
Let’s look and see how much cleaner the code sample above looks utilizing generics.

// Создание экземпляра структуры данных.
List<List<String>> list = new LinkedList<List<String>>();
list.add(new LinkedList<String>());

// Добавьте к нему value
list.get(0).add(«value»);

// Извлеките значение
String value = list.get(0).get(0);

Автобокс\распаковка

Еще одна функция, которая вводится в Java 1.5, — это автобокс/распаковка примитивных типов (таких как int, boolean) в их соответствующий ссылочный тип (такой как Integer, Boolean). Эта функция напрямую не связана с добавлением дженериков, но ее стоит отметить, поскольку она также улучшит ясность кода, устраняя трудоемкую работу преобразования между примитивными типами и типами-оболочками.

Вот небольшой пример кода, который показывает, насколько монотонным может быть использование ArrayList для хранения int без автобокса/распаковки.

ListintList = newArrayList();

for (inti=0; i< 10; i++) {
intList.add(newInteger(i));
}

int sum = 0;
for (int i=0; i <intList.size(); i++) {
// и вызовите intValue (), чтобы получить int.
intnum = ((Integer) intList.get(i)).intValue();
sum += num;
}
Вот тот же код с использованием дженериков и автобоксинга/распаковки.
List<Integer>intList = new ArrayList<Integer>();

for (int i=0; i < 10; i++) {

intList.add(i);
}

int sum = 0;
for (int i=0; i <intList.size(); i++) {
sum += intList.get(i);
}

В оставшейся части этой статьи все примеры кода для дженериков также будут использовать автобокс/распаковку.

Использование

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

Использование API универсальных коллекций

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

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

// Объявите карту, которая принимает целочисленные ключи и строковые значения.
Map<Integer, String> months = new HashMap<Integer, String>();
months.put(1, «Январь»);
months.put(2, «Февраль»);
months.put(3, «Март»);
….
Строка month = months.get(1); // возвращает «Январь»
// Объявите список строковых значений.
List<String> days = new ArrayList<String>();
days.add(«Воскресенье»);
days.add(«Понедельник»);
days.add(«Вторник»);

// Определите пользовательский компаратор, который вызовет нисходящий порядок
// для строковых объектов в процедуре сортировки.
Comparator<String> comparator = new Comparator<String>() {
publicint compare(String s1, String s2) { // Ignore null for brevity
return -s1.compareTo(s2);
}
};
// Сортировка списка дней в порядке убывания.
Collections.sort(days, comparator);
Stringday = days.get(0); // возвращает среду
// Этот код все еще работает, но генерирует предупреждение компилятора.
List uncheckedDaysList = new ArrayList();
uncheckedDaysList.add(«Sunday»);
uncheckedDaysList.add(«Monday»);
uncheckedDaysList.add(«Tuesday»);
String uncheckedDay = (String) uncheckedDaysList.get(0);

Определение универсальных классов и интерфейсов

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

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

public class Pair {
private Object first;
private Object second;

public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}

public Object getFirst() {
returnthis.first;
}

public Object getSecond() {
returnthis.second;
}

public static void main(String[] args) {
// номер месяца, пара названий месяцев
Pair jan = new Pair(new Integer(1), «Январь»);
intmonthNum = ((Integer) jan.getFirst()).intValue();
String monthName = (String) jan.getSecond();
// это зимний флаг, пара названий месяцев
Pair dec = new Pair(Boolean.TRUE, «December»);
booleanisWinter = ((Boolean) dec.getFirst()).booleanValue();
monthName = (String) dec.getSecond();
}

}

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

publicclassPair<T1, T2> {
private T1 first;
private T2 second;

public Pair(T1 first, T2 second) {
this.first = first;
this.second = second;
}

public T1 getFirst() {
returnthis.first;
}

public T2 getSecond() {
returnthis.second;
}

public static void main(String[] args) {
// номер месяца, пара названий месяцев
Pair<Integer, String>jan =
new Pair<Integer, String>(1, «Январь»);
intmonthNum = jan.getFirst();

StringmonthName = jan.getSecond();

// это зимний флаг, пара названий месяцев
Pair<Boolean, String>dec =
new Pair<Boolean, String>(true, «Декабрь»);
booleanisWinter = dec.getFirst();
monthName = dec.getSecond();
}
}

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

publicinterfaceObjectPool<T> {

public T getPooledObject();

public void releasePooledObject(T obj);
}

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

Коварные типы возврата

Еще одна новая функция Java 1.5-поддержка ковариантного типы возврата. Поддержка ковариантных типов возврата позволит методу, переопределяющему метод суперкласса, возвращать подкласс типа возврата метода суперкласса.

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

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

Например, клонирование обычно реализуется путем переопределения метода clone из объекта. Единственная проблема заключается в том, что мы не можем изменить тип возвращаемого значения с Object на наш клонируемый класс, поскольку Java (до 1.5) не допускает ковариантных типов возвращаемых значений. Давайте посмотрим, как мы будем реализовывать клонирование перед ковариантными типами возвращаемых значений.

publicclassCloneableClass {
….

public Object clone() {
….
}

public static void main(String[] args) {
CloneableClassoldWay = new CloneableClass();
CloneableClass clone = (CloneableClass) oldWay.clone();
}

}

Вы должны заметить, что несмотря на то, что мы вызываем clone на экземпляре CloneableClass, нам все равно нужно привести возвращаемый тип к CloneableClass, это кажется слишком большой работой, а также может привести к ClassCastException, если мы случайно приведем его к неправильному типу. Давайте посмотрим, как ковариантные типы возвращаемых значений делают этот процесс более простым и менее подверженным ошибкам:

publicclassCloneableClass {
….

publicCloneableClass clone() {
….
}

public static void main(String[] args) {
CloneableClassnewWay = new CloneableClass();
CloneableClass clone = newWay.clone();
}

}

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

Готовы начать?

Если вы хотите начать писать код, использующий дженерики, до выхода Java 1.5, вы можете скачать прототип компилятора для JSR014 с веб-сайта Sun. Вам также будет полезно скачать спецификацию дженериков. Обратите внимание, что вам нужно будет быть членом Java Developer connection, чтобы загрузить прототип и спецификацию.

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

Инструменты с поддержкой дженериков

Вот список инструментов с поддержкой дженериков:

  • IntelliJ-IDEA (EAP) — это IDE, которая включает в себя поддержку дженериков.
  • Omnicore Код Гайд 6.0-это Java IDE с поддержкой дженериков.
  • Адаптер компилятора для JSR014 для JakartaAnt

Заключение

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

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

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