Статьи

Красота и странность дженериков

Недавно я готовился к экзамену Oracle Certified Professional, Java SE 7 Programmer, и мне пришлось столкнуться с некоторыми довольно странными конструкциями в области обобщений в Java. Тем не менее, я также видел несколько умных и элегантных частей кода. Я обнаружил, что этими примерами стоит поделиться не только потому, что они могут упростить ваш выбор дизайна и сделать код более надежным и пригодным для повторного использования, но также потому, что некоторые из них довольно сложны, когда вы не привыкли к генерикам. Я решил разбить этот пост на четыре главы, которые в значительной степени отображают мой опыт работы с дженериками во время учебы и работы.

Вы понимаете дженерики?

Когда мы оглянемся вокруг, мы увидим, что дженерики довольно интенсивно используются во многих различных средах во вселенной Java. Они простираются от каркасов веб-приложений до коллекций в самой Java. Поскольку эта тема была объяснена многими до меня, я просто перечислю ресурсы, которые я нашел ценными, и перейду к материалам, которые иногда вообще не упоминаются или не очень хорошо объясняются (обычно в заметках или статьях, размещаемых в Интернете). , Итак, если вам не хватает понимания основных понятий дженериков, вы можете ознакомиться с некоторыми из следующих материалов:

  • SCJP Sun сертифицированный программист для экзамена Java 6 Кэтрин Сьерра и Берт Бейтс
    • Для меня основная цель этой книги состояла в том, чтобы подготовиться к экзаменам OCP, предоставляемым Oracle. Но я понял, что заметки в этой книге, касающиеся дженериков, также могут быть полезны для всех, кто изучает дженерики и как их использовать. Однако, безусловно, стоит прочесть, что книга была написана для Java 6, поэтому объяснение не является полным, и вам придется самостоятельно искать недостающие вещи, такие как оператор Diamond.
  • Урок: Обобщения (Обновлено) от Oracle
    • Ресурс предоставлен самой Oracle. В этом руководстве по Java вы можете просмотреть множество простых примеров. Он предоставит вам общую ориентацию в дженериках и подготовит почву для более сложных тем, таких как темы в следующей книге.
  • Обобщения и коллекции Java от Мориса Нафталина и Филиппа Уодлера
    • Еще одна замечательная книга по Java от O’Reilly Media. Эта книга хорошо организована, и материал хорошо представлен со всеми деталями. Эта книга, к сожалению, также довольно устарела, поэтому применяются те же ограничения, что и в отношении первого ресурса.

Что нельзя делать с дженериками?

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

Статическое поле типа <T>

Одна распространенная ошибка, которую делают многие неопытные программисты, это пытаться объявить статические члены. Как вы можете видеть в следующем примере, любая попытка сделать это заканчивается ошибкой компилятора, подобной этой: Cannot make a static reference to the non-static type T

1
2
3
4
public class StaticMember<T> {
    // causes compiler error
    static T member;
}

Экземпляр типа <T>

Другая ошибка — попытаться создать экземпляр любого типа, вызвав new для универсального типа. При этом компилятор вызывает ошибку, говорящую: Cannot instantiate the type T

1
2
3
4
5
6
7
public class GenericInstance<T> {
 
    public GenericInstance() {
        // causes compiler error
        new T();
    }
}

Несовместимость с примитивными типами

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

01
02
03
04
05
06
07
08
09
10
11
12
13
public class Primitives<T> {
    public final List<T> list = new ArrayList<>();
 
    public static void main(String[] args) {
        final int i = 1;
 
        // causes compiler error
        // final Primitives<int> prim = new Primitives<>();
        final Primitives<Integer> prim = new Primitives<>();
 
        prim.list.add(i);
    }
}

Первое создание класса Primitives завершится неудачно во время компиляции с ошибкой, подобной этой: Syntax error on token "int", Dimensions expected after this token . Это ограничение можно обойти, используя тип обертки и немного магии автобокса.

Массив типа <T>

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

1
2
3
4
5
6
7
public class GenericArray<T> {
    // this one is fine
    public T[] notYetInstantiatedArray;
 
    // causes compiler error
    public T[] array = new T[5];
}

Однако, если вы попытаетесь напрямую создать экземпляр универсального массива, вы получите ошибку компилятора, подобную этой: Cannot create a generic array of T

Общий класс исключений

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

1
2
// causes compiler error
public class GenericException<T> extends Exception {}

Когда вы попытаетесь создать такое исключение, вы получите следующее сообщение: The generic class GenericException<T> may not subclass java.lang.Throwable .

Альтернативное значение ключевых слов super и extends

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

  • <? extends T>
    • Значение: подстановочный знак относится к любому типу, расширяющему тип T, и сам тип T.
  • <? super T>
    • Значение: подстановочный знак относится к любому супертипу T и самому типу T

Биты красоты

Одна из моих любимых вещей в Java — строгая типизация. Как мы все знаем, дженерики были введены в Java 5 и использовались для того, чтобы нам было легче работать с коллекциями (они использовались в большем количестве областей, чем просто коллекции, но это был один из основных аргументов в пользу дженериков на этапе проектирования) , Хотя дженерики обеспечивают только защиту времени компиляции и не вводят байт-код, они обеспечивают довольно эффективный способ обеспечения безопасности типов. Следующие примеры показывают некоторые приятные функции или варианты использования для обобщений.

Обобщения работают как с классами, так и с интерфейсами

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

1
2
3
public interface Comparable<T> {
    public int compareTo(T o);
}

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

Обобщения позволяют элегантно использовать границы

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

1
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }

Давайте посмотрим поближе. copy метода объявлена ​​как статический универсальный метод, возвращающий void. Он принимает два аргумента — назначение и источник (и оба они ограничены). Назначение ограничено для хранения только тех типов, которые являются супертипами типа T или T Источник, с другой стороны, ограничен, чтобы быть сделанным только из расширяющихся типов T типа или самого T типа. Эти два ограничения гарантируют, что обе коллекции, а также операция копирования остаются безопасными. Что нам не нужно заботиться о массивах, так как они предотвращают любые нарушения безопасности типов, ArrayStoreException исключение ArrayStoreException .

Дженерики поддерживают множественные границы

Нетрудно представить, почему нужно использовать больше, чем одно простое ограничивающее условие. На самом деле, это довольно легко сделать. Рассмотрим следующий пример: мне нужно создать метод, который принимает аргумент, который является Comparable и List чисел. Разработчик будет вынужден создать ненужный интерфейс ComparableList, чтобы выполнить описанный контракт в предварительные сроки.

01
02
03
04
05
06
07
08
09
10
11
public class BoundsTest {
    interface ComparableList extends List, Comparable {}
 
    class MyList implements ComparableList { ... }
 
    public static void doStuff(final ComparableList comparableList) {}
 
    public static void main(final String[] args) {
        BoundsTest.doStuff(new BoundsTest().new MyList());
    }
}

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

01
02
03
04
05
06
07
08
09
10
public class BoundsTest {
 
    class MyList<T> implements List<T>, Comparable<T> { ... }
 
    public static <T, U extends List<T> & Comparable<T>> void doStuff(final U comparableList) {}
 
    public static void main(final String[] args) {
        BoundsTest.doStuff(new BoundsTest().new MyList<String>());
    }
}

Биты странности

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

Неуклюжий код

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

1
2
3
4
5
public class AwkwardCode<T> {
    public static <T> T T(T T) {
        return T;
    }
}

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

Общий вызов метода

В последнем примере показано, как работает вывод типов в сочетании с обобщениями. Я наткнулся на эту проблему, когда увидел фрагмент кода, который не содержал общей сигнатуры для вызова метода, но утверждал, что прошел компиляцию. Когда кто-то имеет небольшой опыт работы с генериками, подобный код может поразить их с первого взгляда. Можете ли вы объяснить поведение следующего кода?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class GenericMethodInvocation {
 
    public static void main(final String[] args) {
        // 1. returns true
        System.out.println(Compare.<String> genericCompare("1", "1"));
        // 2. compilation error
        System.out.println(Compare.<String> genericCompare("1", new Long(1)));
        // 3. returns false
        System.out.println(Compare.genericCompare("1", new Long(1)));
    }
}
 
class Compare {
 
    public static <T> boolean genericCompare(final T object1, final T object2) {
        System.out.println("Inside generic");
        return object1.equals(object2);
    }
}

Хорошо, давайте разберемся с этим. Первый вызов genericCompare довольно прост. Я обозначаю, какие методы будут иметь аргументы методов, и предоставлю два объекта этого типа — здесь никаких загадок. Второй вызов genericCompare не компилируется, так как Long не является String . И, наконец, третий вызов genericCompare возвращает false . Это довольно странно, так как этот метод объявлен для приема двух параметров одного типа, но все же хорошо передать ему String литерал и объект Long . Это вызвано процессом стирания типа, выполняемым во время компиляции. Поскольку при вызове метода не используется синтаксис обобщений <String> , компилятор не может сказать вам, что вы передаете два разных типа. Всегда помните, что ближайший разделяемый унаследованный тип используется для поиска соответствующего объявления метода. Это означает, что когда genericCompare принимает object1 и object2 , они преобразуются в Object , но сравниваются как экземпляры String и Long из-за полиморфизма во время выполнения, поэтому метод возвращает false . Теперь давайте немного изменим этот код.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class GenericMethodInvocation {
 
    public static void main(final String[] args) {
        // 1. returns true
        System.out.println(Compare.<String> genericCompare("1", "1"));
        // 2. compilation error
        System.out.println(Compare.<String> genericCompare("1", new Long(1)));
        // 3. returns false
        System.out.println(Compare.genericCompare("1", new Long(1)));
 
        // compilation error
        Compare.<? extends Number> randomMethod();
        // runs fine
        Compare.<Number> randomMethod();
    }
}
 
class Compare {
 
    public static <T> boolean genericCompare(final T object1, final T object2) {
        System.out.println("Inside generic");
        return object1.equals(object2);
    }
 
    public static boolean genericCompare(final String object1, final Long object2) {
        System.out.println("Inside non-generic");
        return object1.equals(object2);
    }
 
    public static void randomMethod() {}
}

Этот новый пример кода модифицирует класс Compare , добавляя неуниверсальную версию метода genericCompare и определяя новый randomMethod , который ничего не делает и дважды GenericMethodInvocation из метода GenericMethodInvocation классе GenericMethodInvocation . Этот код делает возможным второй вызов genericCompare поскольку я предоставил новый метод, соответствующий данному вызову. Но это поднимает вопрос о еще одном странном поведении — является ли второй вызов общим или нет? Как оказалось — нет, это не так. Тем не менее, все еще возможно использовать синтаксис <String> обобщений. Чтобы продемонстрировать эту способность более четко, я создал новый вызов randomMethod с этим общим синтаксисом. Это возможно благодаря процессу удаления типа снова — стиранию этого общего синтаксиса.

Однако это меняется, когда на сцену выходит ограниченный шаблон. Компилятор посылает нам ясное сообщение в форме ошибки компилятора, говорящее: Wildcard is not allowed at this location , что делает невозможным компиляцию кода. Для того, чтобы код скомпилировался и запустился, вы должны закомментировать строку № 12. Когда код модифицируется таким образом, он выдает следующий вывод:

1
2
3
4
5
6
Inside generic
true
Inside non-generic
false
Inside non-generic
false
Ссылка: Красота и странность дженериков от нашего партнера JCG Якуба Стаса в блоге Якуба Стаса .