Эта статья является частью нашего Академического курса под названием 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 requiredfinal 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 implementationsclass 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