Обобщения в Java – часть 2

ОГЛАВЛЕНИЕ

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

Непроверенное предупреждение

Компилятор Java предупреждает, если не может проверить безопасность типов. Это происходит при смешении обобщенного и необобщенного кода (это плохая идея). Разработка приложений без проверки таких предупреждений является риском. Лучше рассматривать предупреждения как ошибки.
Рассмотрите следующий пример:

public class Test
{
    public static void foo1(Collection c)
    {
    }

    public static void foo2(Collection<Integer> c)
    {
    }

    public static void main(String[] args)
    {
        Collection<Integer> coll = new ArrayList<Integer>();
        foo1(coll);

        ArrayList lst = new ArrayList();
        foo2(lst);
    }
}

Имеется метод foo1, принимающий традиционный Collection(коллекция) в качестве параметра. Метод foo2, напротив, принимает обобщенную версию Collection. Объект традиционного ArrayList отправляется методу foo2. Поскольку ArrayList может содержать объекты разных типов, внутри метода foo2, компилятор не в состоянии гарантировать, что Collection<Integer> будет содержать только экземпляры Integer. В данном случае компилятор выдает предупреждение, показанное ниже:

Предупреждение: строка (22) [не проверено] обнаружено непроверенное преобразование: 
          java.util.ArrayList требуется:
java.util.Collection<java.lang.Integer>

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

Есть еще одна проблема. В методе main отправляется обобщенный Collection из Integer методу foo1. Хотя компилятор не жалуется на это, это опасно. Что если внутри метода foo1 в коллекцию добавляются объекты типов, отличных от Integer? Это нарушит безопасность типов.

Возникает вопрос, почему компилятор разрешил рассматривать обобщенный тип как традиционный тип. Причина в том, что на уровне байтового кода нет понятия обобщений. Подробно это описано в разделе “Реализация обобщений”.

Ограничения

В использовании обобщений есть ряд ограничений. Нельзя создать массив обобщенных коллекций. Любой массив коллекций шаблонов разрешен, но является опасным с позиции безопасности типов. Нельзя создать обобщение элементарного типа. Например, ArrayList<int> запрещен. Нельзя создать параметризованные статические поля внутри обобщенного класса или иметь статические методы с параметризованными типами в качестве параметров. Например, рассмотрите следующее:

class MyClass<T>
{
    private Collection<T> myCol1; // нормально
    private static Collection<T> myCol2; // ошибка
}

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

new T();
или

new T[10];

Можно сгенерировать исключение обобщенного типа; однако в блоке catch придется использовать конкретный тип, вместо обобщенного.

Можно унаследовать класс от другого обобщенного класса; но нельзя унаследовать от параметрического типа. Например, тогда как:

class MyClass2<T> extends MyClass<T>
{
}

нормально,

class MyClass2<T> extends T
{
}

нет.

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

class MyList implements MyCollection<Integer>
{
    //...
}

нормально,

class MyList implements MyCollection<Integer>, MyCollection<Double>
{
    //...
}

нет.

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


Реализация обобщений

Обобщения – средство уровня языка Java. Одной из целей разработки обобщений было сохранение двоичной совместимости на уровне байтового кода. Не требуя никаких изменений в JVM и сохранив такой же формат файлов класса (байтовый код), можно с легкостью смешать обобщенный код и необобщенный код. Но это дорого обходится. Можно ослабить безопасность типов, прежде всего обеспечиваемую обобщениями.

Имеет ли значение то, что обобщения находятся на уровне языка, а не на уровне байт-кода? Есть две причины для беспокойства. Первая – если это только средство уровня языка: что произошло бы, если бы и когда бы другие языки работали на виртуальной машине Java(JVM)? Если другие языки, работающие на JVM, являются динамическими языками (Groovy, Ruby, Python, …), то ничего страшного. Но если попытаться запустить строго типизированный язык на JVM –  возникнет проблема. Вторая причина – если это просто средство уровня языка (по сути, макрос), то можно было бы передавать правильные типы во время выполнения, например, с помощью Reflection.

Увы, обобщения в Java не обеспечивают эффективную безопасность типов. Они не служат полностью для того, для чего были созданы.

Стирание

Если обобщения являются средством уровня языка, что происходит при компиляции обобщенного кода? Код лишается всех параметрических типов, и каждая ссылка на параметрический тип заменяется классом (как правило, Object или нечто более специальное). Этот процесс называется стиранием типов.
По документации: “Главное преимущество данного подхода в том, что он обеспечивает полную совместимость между обобщенным кодом и устаревшим кодом, использующим непараметризованные типы (именуемые сырыми типами). Основные недостатки в том, что информация о типе параметра недоступна при выполнении, и что автоматически сгенерированные приведения типов могут не сработать при взаимодействии с некорректно функционирующим устаревшим кодом. Однако есть способ добиться гарантированной безопасности типов времени выполнения для обобщенных коллекций даже при взаимодействии с некорректно функционирующим устаревшим кодом.

Несмотря на то, что это обеспечивает взаимодействие обобщенного и необобщенного кода, это компрометирует безопасность типов. Рассматривается действие стирания на код.

Рассмотрите пример кода:

class MyList<T>
{
    public T
ref;
}

Запуск javap –c дает следующий байтовый код:

javap -c MyList
Compiled from "Test.java"
class com.agiledeveloper.MyList extends java.lang.Object{
public java.lang.Object ref;

com.agiledeveloper.MyList();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

Тип T члена ref класса был стерт на (заменен на) тип Object.

Не все типы всегда стираются на или заменяются на Object. Взгляните на следующий пример:

class MyList<T extends Vehicle>
{
    public T ref;
}

В данном случае тип T заменяется на Vehicle, как показано ниже:

javap -c MyList
Compiled from "Test.java"
class com.agiledeveloper.MyList extends java.lang.Object{
public com.agiledeveloper.Vehicle ref;

com.agiledeveloper.MyList();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

Теперь рассмотрите пример:

class MyList<T extends Comparable>
{
    public T ref;
}

Здесь тип T заменяется на интерфейс Comparable.

Наконец, если используется ограничение нескольких пределов, как в:

class MyList<T extends Vehicle & Comparable>
{
    public T ref;
}

то тип T заменяется на Vehicle. Первый тип в ограничении нескольких пределов используется как тип при стирании.

Воздействие стирания

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

ArrayList<Integer> lst  = new ArrayList<Integer>();
lst.add(new Integer(1));
Integer val = lst.get(0);

Это преобразуется в:

ArrayList lst = new ArrayList();
lst.add(new Integer(1));
Integer val = (Integer) lst.get(0);

При присваивании lst.get(0) к val производится приведение типа в преобразованном коде. Если бы код был написан без применения обобщений, делалось бы то же самое. Обобщения в Java, в этом смысле, служат синтаксическим сахаром.

Местонахождение?

Был разобран способ обращения с обобщениями в Java и  была рассмотрена степень обеспечения безопасности типов. Еще ряд проблем, связанных с обобщениями, будет обсуждаться в следующей части.

Заключение

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