Эта статья является частью нашего Академического курса под названием Advanced Java .
Этот курс призван помочь вам наиболее эффективно использовать Java. В нем обсуждаются сложные темы, включая создание объектов, параллелизм, сериализацию, рефлексию и многое другое. Он проведет вас через ваше путешествие в мастерство Java! Проверьте это здесь !
Содержание
- 1. Введение
- 2. Обобщения и интерфейсы
- 3. Дженерики и классы
- 4. Обобщения и методы.
- 5. Ограничение дженериков
- 6. Обобщения, шаблоны и ограниченные типы
- 7. Обобщения и типовые выводы
- 8. Обобщения и аннотации
- 9. Доступ к параметрам общего типа
- 10. Когда использовать дженерики
- 11. Что дальше
- 12. Загрузите исходный код
1. Введение
Идея обобщений представляет собой абстракцию над типами (хорошо известными разработчикам C ++ как шаблоны). Это очень мощная концепция (которая возникла довольно давно), которая позволяет разрабатывать абстрактные алгоритмы и структуры данных и предоставлять конкретные типы для дальнейшей работы. Интересно, что дженерики не присутствовали в ранних версиях Java и были добавлены только в версии Java 5. И с тех пор будет справедливо сказать, что дженерики революционизировали способ написания программ на Java, предоставив гораздо более строгие гарантии типов и сделав код значительно более безопасным.
В этом разделе мы рассмотрим использование обобщений повсюду, начиная с интерфейсов, классов и методов. Предоставляя множество преимуществ, дженерики, однако, вводят некоторые ограничения и побочные эффекты, которые мы также собираемся охватить.
2. Обобщения и интерфейсы
В отличие от обычных интерфейсов, для определения общего интерфейса достаточно указать тип (или типы), с которыми он должен параметризоваться. Например:
1
2
3
4
5
|
package com.javacodegeeks.advanced.generics; public interface GenericInterfaceOneType< T > { void performAction( final T action ); } |
GenericInterfaceOneType
параметризован с одним типом T
, который может быть немедленно использован объявлениями интерфейса. Интерфейс может быть параметризован с более чем одним типом, например:
1
2
3
4
5
|
package com.javacodegeeks.advanced.generics; public interface GenericInterfaceSeveralTypes< T, R > { R performAction( final T action ); } |
Всякий раз, когда какой-либо класс хочет реализовать интерфейс, он имеет возможность предоставить точные замены типов, например, класс ClassImplementingGenericInterface
предоставляет String
как параметр типа T
универсального интерфейса:
1
2
3
4
5
6
7
8
9
|
package com.javacodegeeks.advanced.generics; public class ClassImplementingGenericInterface implements GenericInterfaceOneType< String > { @Override public void performAction( final String action ) { // Implementation here } } |
Стандартная библиотека Java имеет множество примеров универсальных интерфейсов, прежде всего в библиотеке коллекций. Объявлять и использовать универсальные интерфейсы очень просто, однако мы собираемся вернуться к ним еще раз при обсуждении ограниченных типов ( универсальных, подстановочных знаков и ограниченных типов ) и общих ограничений ( ограничение универсальных ).
3. Дженерики и классы
Подобно интерфейсам, разница между обычными и общими классами заключается только в параметрах типов в определениях классов. Например:
1
2
3
4
5
6
7
|
package com.javacodegeeks.advanced.generics; public class GenericClassOneType< T > { public void performAction( final T action ) { // Implementation here } } |
Обратите внимание, что любой класс ( конкретный , абстрактный или конечный ) может быть параметризован с использованием обобщений. Одна интересная деталь заключается в том, что класс может передавать (или не передавать) свой универсальный тип (или типы) в интерфейсы и родительские классы, не предоставляя точный экземпляр типа, например:
1
2
3
4
5
6
7
8
9
|
package com.javacodegeeks.advanced.generics; public class GenericClassImplementingGenericInterface< T > implements GenericInterfaceOneType< T > { @Override public void performAction( final T action ) { // Implementation here } } |
Это очень удобный метод, который позволяет классам налагать дополнительные границы на универсальный тип, все еще соответствующий контракту интерфейса (или родительского класса), как мы увидим в разделе Общие, групповые символы и ограниченные типы .
4. Обобщения и методы.
Мы уже видели несколько общих методов в предыдущих разделах при обсуждении классов и интерфейсов. Тем не менее, о них есть что сказать. Методы могут использовать универсальные типы как часть объявления аргументов или объявления типа возврата. Например:
1
2
3
4
5
|
public < T, R > R performAction( final T action ) { final R result = ...; // Implementation here return result; } |
Нет никаких ограничений на то, какие методы могут использовать универсальные типы, они могут быть конкретными, абстрактными , статическими или конечными . Вот пара примеров:
1
2
3
4
5
6
7
|
protected abstract < T, R > R performAction( final T action ); static < T, R > R performActionOn( final Collection< T > action ) { final R result = ...; // Implementation here return result; } |
Если методы объявлены (или определены) как часть универсального интерфейса или класса, они могут (или не могут) использовать универсальные типы своего владельца. Они могут определять собственные универсальные типы или смешивать их с типами из своего объявления класса или интерфейса. Например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
package com.javacodegeeks.advanced.generics; public class GenericMethods< T > { public < R > R performAction( final T action ) { final R result = ...; // Implementation here return result; } public < U, R > R performAnotherAction( final U action ) { final R result = ...; // Implementation here return result; } } |
Конструкторы классов также считаются своего рода методами инициализации, и поэтому могут использовать универсальные типы, объявленные их классом, объявлять собственные универсальные типы или просто смешивать оба (однако они не могут возвращать значения, поэтому параметризация возвращаемого типа не применима к конструкторам ), например:
1
2
3
4
5
6
7
8
9
|
public class GenericMethods< T > { public GenericMethods( final T initialAction ) { // Implementation here } public < J > GenericMethods( final T initialAction, final J nextAction ) { // Implementation here } } |
Это выглядит очень легко и просто, и это, безусловно, так. Тем не менее, существуют некоторые ограничения и побочные эффекты, вызванные тем, как дженерики реализованы на языке Java, и следующий раздел посвящен их решению.
5. Ограничение дженериков
Будучи одной из самых ярких особенностей языка, дженерики, к сожалению, имеют некоторые ограничения, в основном вызванные тем, что они были введены довольно поздно в уже зрелый язык. Скорее всего, для более тщательного внедрения потребовалось значительно больше времени и ресурсов, поэтому были достигнуты компромиссы для своевременной доставки генериков.
Во-первых, примитивные типы (такие как int
, long
, byte
, …) не допускаются к использованию в обобщениях. Это означает, что когда вам нужно параметризовать ваш универсальный тип примитивным, вместо него следует использовать соответствующий класс-оболочку ( Integer
, Long
, Byte
, …).
1
2
|
final List< Long > longs = new ArrayList<>(); final Set< Integer > integers = new HashSet<>(); |
Мало того, что из-за необходимости использовать обертки классов в обобщениях, это вызывает неявную упаковку и распаковку примитивных значений (эта тема будет подробно рассмотрена в части 7 руководства, « Общие рекомендации по программированию» ), например:
1
2
3
4
5
|
final List< Long > longs = new ArrayList<>(); longs.add( 0L ); // 'long' is boxed to 'Long' long value = longs.get( 0 ); // 'Long' is unboxed to 'long' // Do something with value |
Но примитивные типы — только одна из ловушек дженериков. Другой, более неясный, это стирание типа. Важно знать, что дженерики существуют только во время компиляции: компилятор Java использует сложный набор правил для обеспечения безопасности типов по отношению к дженерикам и использованию их параметров типа, однако полученный байт-код JVM удаляет все конкретные типы (и заменяется на Класс Object
). Вначале может удивить, что следующий код не компилируется:
1
2
3
4
5
6
7
|
void sort( Collection< String > strings ) { // Some implementation over strings heres } void sort( Collection< Number > numbers ) { // Some implementation over numbers here } |
С точки зрения разработчика, это совершенно правильный код, однако из-за стирания типов эти два метода сужаются до одной и той же сигнатуры, и это приводит к ошибке компиляции (со странным сообщением типа «Стирание метода sort (Collection <String>») ) аналогичен другому методу… »
1
2
|
void sort( Collection strings ) void sort( Collection numbers ) |
Еще один недостаток, вызванный стиранием типов, связан с тем, что невозможно использовать параметры типа обобщений любым осмысленным способом, например, для создания новых экземпляров типа, или для получения конкретного класса параметра типа, или для его использования в оператор instanceof
. Приведенные ниже примеры не проходят этап компиляции:
01
02
03
04
05
06
07
08
09
10
11
|
public < T > void action( final T action ) { if ( action instanceof T ) { // Do something here } } public < T > void action( final T action ) { if ( T. class .isAssignableFrom( Number. class ) ) { // Do something here } } |
И, наконец, также невозможно создать экземпляры массива, используя параметры типа generics. Например, следующий код не компилируется (на этот раз с чистым сообщением об ошибке «Не удается создать универсальный массив T» ):
1
2
3
|
public < T > void performAction( final T action ) { T[] actions = new T[ 0 ]; } |
Несмотря на все эти ограничения, дженерики по-прежнему чрезвычайно полезны и приносят большую пользу. В разделе « Доступ к параметрам универсального типа» мы рассмотрим несколько способов преодоления некоторых ограничений, накладываемых реализацией обобщенных типов на языке Java.
6. Обобщения, шаблоны и ограниченные типы
До сих пор мы видели примеры, использующие дженерики с параметрами неограниченного типа. Чрезвычайно мощная способность обобщений налагает ограничения (или границы) на тип, для которого они параметризованы, используя ключевые слова extends
и super
.
Ключевое слово extends
ограничивает параметр типа подклассом какого-либо другого класса или реализует один или несколько интерфейсов. Например:
1
2
3
|
public < T extends InputStream > void read( final T stream ) { // Some implementation here } |
Параметр типа T
в объявлении метода read
должен быть подклассом класса InputStream
. Это же ключевое слово используется для ограничения реализации интерфейса. Например:
1
2
3
|
public < T extends Serializable > void store( final T object ) { // Some implementation here } |
Хранилище методов требует своего параметра типа T
для реализации интерфейса Serializable
чтобы метод выполнял желаемое действие. Также можно использовать другой параметр типа в качестве привязки для ключевого слова extends
, например:
1
2
3
|
public < T, J extends T > void action( final T initial, final J next ) { // Some implementation here } |
Границы не ограничиваются отдельными ограничениями и могут быть объединены с помощью оператора &
. Может быть указано несколько интерфейсов, но разрешен только один класс. Комбинация класса и интерфейсов также возможна, с парой примеров, показанных ниже:
1
2
3
4
5
6
7
|
public < T extends InputStream & Serializable > void storeToRead( final T stream ) { // Some implementation here } public < T extends Serializable & Externalizable & Cloneable > void persist( final T object ) { // Some implementation here } |
Прежде чем обсуждать super
ключевое слово, нам нужно ознакомиться с концепциями подстановочных знаков. Если параметр типа не представляет интереса универсального класса, интерфейса или метода, он может быть заменен символом? подстановочные. Например:
1
2
3
|
public void store( final Collection< ? extends Serializable > objects ) { // Some implementation here } |
Хранилище методов на самом деле не заботится о том, с какими параметрами он вызывается, единственное, что нужно для обеспечения того, чтобы каждый тип реализовывал интерфейс Serializable
. Или, если это не имеет никакого значения, вместо этого можно использовать подстановочный знак без границ:
1
2
3
|
public void store( final Collection< ? > objects ) { // Some implementation here } |
В отличие от extends
, ключевое слово super
ограничивает параметр типа суперклассом некоторого другого класса. Например:
1
2
3
|
public void interate( final Collection< ? super Integer > objects ) { // Some implementation here } |
Используя верхние и нижние границы типов (с extends
и super
) вместе с подстановочными знаками типов, обобщенные элементы обеспечивают способ точной настройки требований к параметрам типа или, в некоторых случаях, полностью их пропускают, сохраняя семантику, ориентированную на тип.
7. Обобщения и типовые выводы
Когда дженерики нашли свой путь в язык Java, они взорвали объем кода, который разработчики должны были написать, чтобы удовлетворить правила синтаксиса языка. Например:
1
2
3
4
5
6
|
final Map< String, Collection< String > > map = new HashMap< String, Collection< String > >(); for ( final Map.Entry< String, Collection< String > > entry: map.entrySet() ) { // Some implementation here } |
Релиз Java 7 несколько решил эту проблему, внеся изменения в компилятор и введя новый оператор diamond <>. Например:
1
|
final Map< String, Collection< String > > map = new HashMap<>(); |
Компилятор может вывести параметры обобщенных типов с левой стороны и позволяет опустить их в правой части выражения. Это был значительный прогресс в том, чтобы сделать синтаксис обобщений менее многословным, однако возможности компилятора выводить параметры типа обобщений были весьма ограничены. Например, следующий код не компилируется в Java 7:
1
2
3
4
5
6
7
|
public static < T > void performAction( final Collection< T > actions, final Collection< T > defaults ) { // Some implementation here } final Collection< String > strings = new ArrayList<>(); performAction( strings, Collections.emptyList() ); |
Компилятор Java 7 не может определить параметр типа для Collections. emptyList ()
Collections. emptyList ()
и как таковой требует, чтобы он был передан явно:
1
|
performAction( strings, Collections.< String >emptyList() ); |
К счастью, версия Java 8 вносит больше улучшений в компилятор и, в частности, в вывод типов для дженериков, поэтому приведенный выше фрагмент кода успешно компилируется, избавляя разработчиков от ненужной типизации.
8. Обобщения и аннотации
Хотя мы собираемся обсудить аннотации в следующей части руководства, стоит упомянуть, что в эпоху до Java 8 дженерикам не разрешалось иметь аннотации, связанные с их параметрами типа. Но в Java 8 это изменилось, и теперь становится возможным аннотировать параметры типов универсальных элементов в местах, где они объявлены или используются. Например, вот как можно объявить универсальный метод, а его параметр типа украшен аннотациями:
1
2
3
|
public < @Actionable T > void performAction( final T action ) { // Some implementation here } |
Или просто еще один пример применения аннотации при использовании универсального типа:
1
2
|
final Collection< @NotEmpty String > strings = new ArrayList<>(); // Some implementation here |
В четвертой части урока « Как и когда использовать Enums и Annotations» мы рассмотрим несколько примеров того, как можно использовать аннотации, чтобы связать некоторые метаданные с параметрами универсального типа. Этот раздел просто дает вам ощущение, что можно дополнить дженерики аннотациями.
9. Доступ к параметрам общего типа
Как вы уже знаете из раздела Ограничение универсальных шаблонов , получить класс параметра универсального типа невозможно. Один простой прием для обхода, который требует передачи дополнительного аргумента, Class< T >
, в местах, где необходимо знать класс параметра типа T
Например:
1
2
3
|
public < T > void performAction( final T action, final Class< T > clazz ) { // Some implementation here } |
Это может привести к большому количеству аргументов, требуемых методами, но при тщательном проектировании это не так плохо, как кажется на первый взгляд.
Другой интересный вариант использования, который часто возникает при работе с обобщениями в Java, — это определение конкретного класса типа, с которым параметризован обобщенный экземпляр. Это не так просто и требует участия API отражения Java. Мы рассмотрим полный пример в части 11 учебного пособия « Поддержка отражений и динамических языков», а пока просто отметим, что экземпляр ParameterizedType
является центральной точкой для размышлений над обобщениями.
10. Когда использовать дженерики
Несмотря на все ограничения, ценность, которую дженерики добавляют в язык Java, просто огромна. В настоящее время трудно представить, что было время, когда у Java не было поддержки дженериков. Обобщения должны использоваться вместо необработанных типов ( Collection< T >
вместо Collection
, Callable< T >
вместо Callable
,…) или Object
чтобы гарантировать безопасность типов, определять четкие ограничения типов для контрактов и алгоритмов и значительно упрощать обслуживание кода. и рефакторинг.
Однако следует помнить об ограничениях текущей реализации обобщений в Java, стирании типов, а также о знаменитой неявной упаковке и распаковке примитивных типов. Дженерики — не серебряная пуля, решающая все проблемы, с которыми вы можете столкнуться, и ничто не заменит тщательного проектирования и вдумчивого мышления.
Было бы неплохо взглянуть на некоторые реальные примеры и почувствовать, как обобщенные элементы облегчают жизнь Java-разработчику.
Пример 1. Давайте рассмотрим типичный пример метода, который выполняет действия с экземпляром класса, который реализует некоторый интерфейс (скажем, Serializable
) и возвращает обратно измененный экземпляр этого класса.
1
2
|
class SomeClass implements Serializable { } |
Без использования обобщений решение может выглядеть так:
1
2
3
4
5
6
7
8
|
public Serializable performAction( final Serializable instance ) { // Do something here return instance; } final SomeClass instance = new SomeClass(); // Please notice a necessary type cast required final SomeClass modifiedInstance = ( SomeClass )performAction( instance ); |
Давайте посмотрим, как дженерики улучшают это решение:
1
2
3
4
5
6
7
|
public < T extends Serializable > T performAction( final T instance ) { // Do something here return instance; } final SomeClass instance = new SomeClass(); final SomeClass modifiedInstance = performAction( instance ); |
Ужасное приведение типов исчезло, так как компилятор может вывести правильные типы и доказать, что эти типы используются правильно.
Пример 2. Немного более сложный пример метода, который требует, чтобы экземпляр класса реализовал два интерфейса (скажем, Serializable
и Runnable
).
1
2
3
4
5
6
|
class SomeClass implements Serializable, Runnable { @Override public void run() { // Some implementation } } |
Без использования обобщений, простое решение состоит в том, чтобы ввести промежуточный интерфейс (или использовать чистый Object
в качестве крайней меры), например:
01
02
03
04
05
06
07
08
09
10
11
12
|
// The class itself should be modified to use the intermediate interface // instead of direct implementations class SomeClass implements SerializableAndRunnable { @Override public void run() { // Some implementation } } public void performAction( final SerializableAndRunnable instance ) { // Do something here } |
Несмотря на то, что это правильное решение, оно не выглядит лучшим вариантом и с растущим числом интерфейсов оно может стать действительно неприятным и неуправляемым. Давайте посмотрим, как дженерики могут помочь здесь:
1
2
3
|
public < T extends Serializable & Runnable > void performAction( final T instance ) { // Do something here } |
Очень понятный и лаконичный кусок кода, никакой промежуточный интерфейс или другие хитрости не требуются.
Совокупность примеров, в которых обобщения делают код понятным и понятным, на самом деле бесконечна. В следующих частях руководства будут часто использоваться дженерики для демонстрации других возможностей языка Java.
11. Что дальше
В этом разделе мы рассмотрели одну из отличительных особенностей языка Java, которая называется дженерики. Мы увидели, как обобщения делают код безопасным и лаконичным, проверяя, что правильные типы (с границами) используются повсеместно. Мы также рассмотрели некоторые ограничения дженериков и способы их преодоления. В следующем разделе мы собираемся обсудить перечисления и аннотации.
12. Загрузите исходный код
- Это был урок о том, как проектировать классы и интерфейсы. Вы можете скачать исходный код здесь: advanced-java-part-4