Продолжается подписка на наши издания! Вы не забыли подписаться?

Использование обобщенных типов (generics) в J2SE 5.0


Потребность в generic-ах
Использование Generic-ов
Реализация generic-типов
Generic-методы
Групповые символы
Изменения в спецификации Java, JVM и API
За сценой
Java Generic-и vs. шаблоны C++
Заключение

В J2SE 1.4.x можно иметь коллекцию объектов, причем элементы этой коллекции могут быть любого ссылочного типа.
Некоторая дополнительная нагрузка на разработчика при этом все же ложится, поскольку приходится следить за типами объектов,
содержащихся в коллекции. Кроме того, в коллекциях не могут содержаться примитивные типы данных – int, float, double и т.п.
Generic-и – это одно из наиболее востребованных расширений Java, и они наконец появились в J2SE 5.0.
Эта статья представляет введение в Java-программирование с использованием generic-ов.

Потребность в generic-ах

Основным мотивом введения generic-ов в Java было неудобство работы с нетипизированными коллекциями. Использование нетипизированных коллекций приводит к большим трудозатратам, нежели типизированных, причем может порождать ошибки. Эти ошибки обнаруживаются только во время исполнения. Можно каждый раз, когда требуется коллекция, создавать ее типизированный вариант. Это избавляет от проблем, связанных с применением коллекций, но приводит к большому объему дублированию кода, создаваемого с помощью печально известной технологии copy-paste. Generic-и позволяют избежать подобных проблем за счет введения специальных параметров типов и обобщения реализации классов и методов. Они позволяют не создавать отдельную копию типизированной коллекции с измененным типом элемента, а создать единую обобщенную реализацию, в которой тип элемента заменен параметром типа, и впоследствии переложить работу по созданию специализированных коллекций на компилятор.

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

LinkedList list = new LinkedList();
list.add(new Integer(1));
Integer num = (Integer) list.get(0);

Как видите, при извлечении элемента из списка нужно приведение. Приведение безопасно, так как будет проверено во время исполнения, но при приведении к типу, отличному от извлекаемого, но не являющемуся его супертипом, будет сгенерировано исключение ClassCastException.

При использовании generic-типов предыдущий отрывок кода может быть записан так:

LinkedList<Integer> list = new LinkedList<Integer>();
list.add(new Integer(1));
Integer num = list.get(0);

Здесь говорится, что LinkedList – это generic-класс, принимающий параметр типа, в данном случае - Integer.

Как видите, больше не нужно приводить Integer, так как метод get() возвращает ссылку на объект конкретного типа (в данном случае – Integer). Если вам нужно приписать извлеченный элемент к другому типу, ошибка произойдет при компиляции, а не во время исполнения. Такая ранняя статическая проверка увеличивает типобезопасность Java.

Предыдущий пример можно переписать следующим образом, используя autoboxing:

LinkedList<Integer> list = new LinkedList<Integer>();
list.add(1);
int num = list.get(0);

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

Ex1.java

import java.util.*;

public class Ex1 
{

  private void testCollection() 
  {
    List list = new ArrayList();
    list.add(new String("Hello world!"));
    list.add(new String("Good bye!"));
    list.add(new Integer(95));
    printCollection(list);
  }

  private void printCollection(Collection c) 
  {
    Iterator i = c.iterator();
    while(i.hasNext()) 
    {
      String item = (String) i.next();
      System.out.println("Item: "+item);
    }
  }

  public static void main(String argv[]) 
  {
    Ex1 e = new Ex1();
    e.testCollection();
  }
}

Обратите внимание, что снова потребовалось явное приведение типов – в методе printCollection. Этот класс отлично компилируется, но во время исполнения генерируется ClassCastException, так как происходит попытка привести значение типа Integer, хранящееся в одном из элементов коллекции, к строке:

Item: Hello world!
Item: Good bye!
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer
        at Ex1.printCollection(Ex1.java:16)
        at Ex1.testCollection(Ex1.java:10)
        at Ex1.main(Ex1.java:23)

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

С использованием generic-ов класс Ex1, приведенный выше, можно переписать так:

Ex2.java

import java.util.*;

public class Ex2 
{

  private void testCollection() 
  {
    List<String> list = new ArrayList<String>();
    list.add(new String("Hello world!"));
    list.add(new String("Good bye!"));
    list.add(new Integer(95));
    printCollection(list);
  }

  private void printCollection(Collection c) 
  {
    Iterator<String> i = c.iterator();
    while(i.hasNext()) 
    {
      System.out.println("Item: "+i.next());
    }
  }

  public static void main(String argv[]) 
  {
    Ex2 e = new Ex2();
    e.testCollection();
  }
}

Если попытаться скомпилировать этот код, во время компиляции будет выдана ошибка, говорящая, что нельзя добавлять Integer в коллекцию элементов типа String. Таким образом, generic-и улучшают проверку типов при компиляции и, следовательно, больше ошибок приведения типов отлавливается при компиляции, а не во время исполнения.

Обратите внимание на новый синтаксис, использованный для создания экземпляра ArrayList. ArrayList теперь – параметризованный тип. Параметризованный тип состоит из имени класса или интерфейса X и секции параметров <T1, T2, ..., Tn>, которая должна соответствовать числу объявленных параметров Х, и каждый аргумент должен удовлетворять требованиям, предъявляемым к соответствующим параметром типа. Следующий отрывок кода показывает части нового определения класса для ArrayList:

public class ArrayList<E> extends AbstractList<E> implements List<E>, 
    RandomAccess, Cloneable, Serializable 
{
   // ...
}

Здесь Е – параметр типа. Он действует как заглушка для типа, который будет определен при использовании списка.

Реализация generic-типов

У generic-типов есть один или более параметров. Ниже приведен пример с единственным параметром Е. Параметризованный тип должен быть ссылочным типом, таким образом, примитивные типы не могут быть параметризованными.

interface List<E> 
{
   void add(E x);
   Iterator<E> iterator();
}

interface Iterator<E> 
{
   E next();
   boolean hasNext();
}


class LinkedList<E> implements List<E> 
{
   // реализация   
}

Здесь Е представляет тип элементов, содержащихся в коллекции. Можно рассматривать Е как заглушку, которая будет заменена конкретным типом. Например, в конструкции LinkedList<String> Е заменяется на string.

В некоторых случаях может потребоваться вызвать методы типа элемента, например, hashCode() и equals() у Object. Вот пример, принимающий два параметра типа:

class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V> 
{

  // ...

  public V get(Object k) 
  {
    ...
    int hash = k.hashCode();  
    ...
  }
  // ...
}

Следует отметить, что переменные K и V должны быть заменены конкретными типами – подтипами Object.

Generic-методы

Применение generic-ов не ограничено классами и интерфейсами, можно определить и generic-методы. Статические и нестатические методы, а также конструкторы могут быть параметризованы почти так же, как классы и интерфейсы, за исключением чуть-чуть отличающегося синтаксиса. Generic-методы вызываются так же, как обычные.

Перед тем, как привести пример generic-метода, рассмотрим следующий код, печатающий все элементы коллекции:

public void printCollection(Collection c) 
{
  Iterator i = c.iterator();
  for(int k = 0;k<c.size();k++) 
  {
    System.out.printn(i.next());
  }
}

С использованием generic-ов это можно переписать так (Collection<?> здесь – коллекция неизвестного типа):

void printCollection(Collection<?> c) 
{
   for(Object o:c) 
   {
      System.out.println(e);
   }
}

Этот пример использует такую возможность generic-ов, как использование групповых символов (wildcards).

Групповые символы

Существует три типа групповых символов:

  1. "? extends Type": семейство подтипов типа Type. Наиболее полезный групповой символ.
  2. "? super Type": семейство сверхтипов типа Type.
  3. "?": набор всех типов или any.

В качестве примера использования групповых символов рассмотрим метод draw(), который должен уметь рисовать такие фигуры, как круг, прямоугольник или треугольник. Реализация может быть такой, как показана ниже. Здесь Shape – абстрактный класс с тремя субклассами: Circle, Rectangle и Triangle.

public void draw(List<Shape> shape) 
{
  for(Shape s: shape) 
  {
    s.draw(this);
  }
}

Нужно упомянуть, что метод draw() может быть вызван только для List<Shape>, и не может быть вызван для списков List<Circle>, List<Rectangle> или List<Triangle>. Чтобы метод принимал любой (any) вид фигуры, он должен выглядеть так:

public void draw(List<? extends Shape> shape) 
{
   // остальной код тот же
}

Вот еще один пример generic-метода, использующего групповые символы для сортировки списка по возрастанию. По существу, все элементы списка должны реализовать интерфейс Comparable.

public static <T extends Comparable<? super T>> void sort(List<T> list)
{
   Object a[] = list.toArray();
   Arrays.sort(a);
   ListIterator<T> i = list.listIterator();
   for(int j=0; j<a.length; j++) 
   {
      i.index();
      i.set((t)a[j]);
   }
}

Изменения в спецификации Java, JVM и API

Для поддержки generic-типов необходимо внести некоторые изменения в язык Java, виртуальную машину и Java API. Заметные изменения в Java API связаны с иерархией коллекций в пакете java.util, изменениями в классе java.lang.Class и пакете java.lang.reflect, дающими возможность обращаться к объявлению типа, метода, конструктора или поля, и получать информацию о generic-типе. Подробнее об этом можно узнать по адресу http://www.jcp.org/en/jsr/detail?id=14 (JSR 14: Adding Generics to the Java Programming Language).

За сценой

Generic-и реализуются front-end-ом компилятора Java, который производит некоторый набор преобразований, именуемый erasure, то есть процесс трансляции или переписывания кода, в котором используются generic-и, в обычный код (при помощи отображения нового синтаксиса на текущую спецификацию JVM). Другими словами, эта конверсия стирает всю информацию generic-типов – то есть все, что находится в угловых скобках. Например, LinkedList<Integer> станет LinkedList. Компилятор производит подстановку конкретных типов, заменяя ими все вхождения параметров типа, а в случае некорректности типов в результирующем коде производится приведение к подходящему типу.

Java Generic-и vs. шаблоны C++

Generic-и выглядят похоже на шаблоны C++, но это не одно и тоже. Generic-и просто обеспечивают типобезопасность во время компиляции и исключают потребность в приведениях типов. Главное различие – в инкапсуляции: компилятор указывает на ошибки в тех местах, где они произошли, и исходный код не показывается клиентам. Generic-и используют технику, известную как type erasure, описанную выше, а компилятор отслеживает generic-и внутренне, причем все экземпляры используют один и тот же класс/файл при компиляции и во время исполнения. В отличие от C++, компилятор Java производит больший лексический и семантический контроль.

Заключение

Generic-и – новая возможность J2SE 5.0 и большой довесок к самому языку Java. Эта возможность обеспечивает типобезопасность во время компиляции для коллекций и устраняет необходимость в рутинных приведениях типов.


Copyright © 1994-2016 ООО "К-Пресс"