Шаг 67.
Язык программирования Java.
Ограничения и пределы обобщений

На этом шаге мы рассмотрим ограничения и пределы обобщений

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

  1. Параметрам типа нельзя приписывать простые типы.

    Примитивный тип нельзя подставить вместо типа параметров. Это означает, что не бывает объекта типа Pair<double>, а только объекты типа Pair<Double>. Это объясняется тем, что после стирания типов в классе Pair отсутствуют поля типа Object, и поэтому их нельзя использовать для хранения значений типа double. Вместо примитивных типов можно использовать их классы-обертки.

  2. Во время выполнения можно запрашивать только базовые типы.

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

    if (a instanceof Pair<String>) {...}

    Это же справедливо в отношении следующего кода:

    if (a instanceof Pair<T>) {...}

    А также в отношении приведении типов:

    Pair<String> a = (Pair<String>) b;

    Отметим, что функция getClass() всегда возвращает базовый тип. В следующем примере результатом сравнения оказывается логическое значение true, потому что при обоих вызовах метода getClass() возвращается объект типа Pair.class:

    Pair<String> stringPair = ...;
    Pair<Employee> employeePair = ...;
    if (stringPair.getClass() == employeePair.getClass()) {...}
  3. Массивы параметризованных типов недопустимы.

    Нельзя объявить массив параметризованных типов вроде следующего:

    Pair<String>[] table = new Pair<String>[10];

    После стирания типов таблицы становятся Pair[]. Но его можно преобразовать в тип Object[] следующим образом:

    Object[] objarray = table;

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

    Но стирание делает этот механизм неэффективным для обобщенных типов. Рассмотрим пример:

    objarray[0] = new Pair<Employee>();

    Приведенное выше присваивание пройдет проверку на сохранение в массиве, но выдаст ошибку соблюдения типов. Именно поэтому массивы параметризованных типов не допускаются.

    Следует иметь в виду, что не допускается только создание подобных массивов. Но разрешается объявить переменную, например, так:

    Pair<String>[]
  4. Предупреждения о переменном числе аргументов.

    Рассмотрим следующий пример:

    public static <T> void addAll(Collection<T> coll, T... ts) {
        for (T t : ts) {
            coll.add(t);
        }
    }

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

    Collection<Pair<String>> table = ...;
    Pair<String> pair1 = ...;
    Pair<String> pair2 = ...;
    Pair<String> pair3 = ...;
    Pair<String> pair4 = ...;
    Pair<String> pair5 = ...;
    addAll(table, pair1, pair2, pair3, pair4, pair5);

    Для вызова этого метода в виртуальной машине Java придется сформировать массив объектов типа Pair<String>, что не по правилам. Но эти правила были ослаблены, чтобы уведомлять в подобных случаях только о предупреждений.

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

    • Ввести аннотацию (с аннотациями мы познакомимся позже). @SuppressWarnings("unchecked") в тело метода, содержащего вызов метода addAll();
    • Начиная с версии Java SE 7, можно ввести аннотацию @SafeVarargs в тело самого метода addAll(), как это показано ниже:
      @SafeVarargs
      public static <T> void addAll(Collection<T> coll, T... ts) {...}

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

  5. Нельзя создавать экземпляры переменных типа.

    Переменные типа нельзя использовать в выражениях вроде new T {...}, new T[...] или T.class. Стирание типов может изменить обобщенный тип T на Object, а вызывать конструктор new Object(), конечно, не стоит.

  6. Переменные типа в статическом контексте обобщенных классов недействительны.

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

    public class Singleton<T> {
        private static T singleInstance;
    
        public static T getSingleInstance() {
            if (singleInstance == null) {
                   /*конструирование нового объекта*/
            }
            return singleInstance;
        }
    }

    Если бы такое было бы возможно, то программа должна была бы сконструировать экземпляр типа Singleton<Random> для общего генератора случайных чисел и экземпляр типа Singleton<String>. Но такое не возможно, так как после стирания типов остается только один класс Singleton и только одно поле singleInstance. Именно по этой причине статические поля и методы с переменными типа просто не допустимы.

  7. Нельзя генерировать или перехватывать экземпляры обобщенного класса в виде исключений.

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

    public class Problem<T> extends Throwable {....}

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

    public static <T extends Throwable> void doWork(Class t) {
            try {
                    /*код*/
            } catch (T e) {
                    /*обработчик исключения*/
            }
    }

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

    public static <T extends Throwable>  void doWork(T t) throws T {
            try {
                    /*код*/
            } catch (Throwable e) {
                    t.initCause(e);
                    throw t;
            }
    }
  8. Возможен конфликт после стирания типов.

    Не допускается создавать условия, приводящие к конфликтам после стирания обобщенных типов. Допустим, в обобщенный класс Pair вводится метод equals() следующим образом:

    public class Pair<T> {
        public boolean equals(T value) {
            return first.equals(value) && second.equals(value);
        }
        ...
    }

    Рассмотрим теперь класс Pair<String>. На первый взгляд кажется, что в нем два метода equals():

    boolean equals(Object);
    boolean equals(String);

    Но это заблуждение. В результате стирания типов метод boolean equals(T) становится методом boolean equals(Object), который вступает в конфликт с методом Object.equals(). Понятно, что в таком случае для разрешения конфликта следует переименовать соответствующий метод.

На следующем шаге мы рассмотрим правила наследования обобщенных типов

Предыдущий шаг Содержание Следующий шаг