1. введение
Дженерики Java были введены в JDK 5.0 с целью уменьшения ошибок и добавления дополнительного уровня абстракции над типами.
Эта статья представляет собой краткое введение в дженерики в Java, цель, стоящую за ними, и как их можно использовать для улучшения качества нашего кода.
Дальнейшее чтение:
Ссылки на методы в Java
Получение полей из класса Java с помощью отражения
2. Потребность в дженериках
Давайте представим себе сценарий, в котором мы хотим создать список на Java для хранения Integer ; у нас может возникнуть соблазн написать:
List list = new LinkedList(); list.add(new Integer(1)); Integer i = list.iterator().next();
Удивительно, но компилятор будет жаловаться на последнюю строку. Он не знает, какой тип данных возвращается. Компилятор потребует явного приведения:
Integer i = (Integer) list.iterator.next();
Нет контракта, который мог бы гарантировать, что возвращаемый тип списка является целым числом . Определенный список может содержать любой объект. Мы знаем только, что получаем список, проверяя контекст. При просмотре типов он может гарантировать только то, что это Объект , поэтому требуется явное приведение, чтобы гарантировать безопасность типа.
Это приведение может раздражать, мы знаем, что тип данных в этом списке – Целое число . Актерский состав также загромождает наш код. Это может привести к ошибкам времени выполнения, связанным с типом, если программист допустит ошибку с явным приведением.
Было бы намного проще, если бы программисты могли выразить свое намерение использовать определенные типы, а компилятор мог бы обеспечить правильность такого типа. Это основная идея, лежащая в основе дженериков.
Давайте изменим первую строку предыдущего фрагмента кода на:
Listlist = new LinkedList<>();
Добавляя оператор diamond<>, содержащий тип, мы сужаем специализацию этого списка только до Integer type, т. е. указываем тип, который будет храниться внутри списка. Компилятор может принудительно применить тип во время компиляции.
В небольших программах это может показаться тривиальным дополнением, однако в больших программах это может значительно повысить надежность и облегчить чтение программы.
3. Общие методы
Универсальные методы-это те методы, которые написаны с одним объявлением метода и могут вызываться с аргументами разных типов. Компилятор обеспечит правильность любого используемого типа. Вот некоторые свойства универсальных методов:
- Универсальные методы имеют параметр типа (оператор diamond, заключающий тип) перед возвращаемым типом объявления метода
- Параметры типа могут быть ограничены (границы объясняются далее в статье)
- Общие методы могут иметь различные параметры типа, разделенные запятыми в сигнатуре метода
- Тело метода для универсального метода так же, как и обычный метод
Пример определения универсального метода преобразования массива в список:
publicList fromArrayToList(T[] a) { return Arrays.stream(a).collect(Collectors.toList()); }
В предыдущем примере в сигнатуре метода подразумевает, что метод будет иметь дело с общим типом T . Это необходимо, даже если метод возвращает void.
Как упоминалось выше, метод может иметь дело с несколькими универсальными типами, в этом случае все универсальные типы должны быть добавлены в сигнатуру метода, например, если мы хотим изменить приведенный выше метод для работы с типом T и типом G , он должен быть написан следующим образом:
public staticList fromArrayToList(T[] a, Function mapperFunction) { return Arrays.stream(a) .map(mapperFunction) .collect(Collectors.toList()); }
Мы передаем функцию, которая преобразует массив с элементами типа T в список с элементами типа G. Примером может быть преобразование Целого числа в его строковое представление:
@Test public void givenArrayOfIntegers_thanListOfStringReturnedOK() { Integer[] intArray = {1, 2, 3, 4, 5}; ListstringList = Generics.fromArrayToList(intArray, Object::toString); assertThat(stringList, hasItems("1", "2", "3", "4", "5")); }
Стоит отметить, что Oracle рекомендует использовать заглавную букву для представления общего типа и выбирать более описательную букву для представления формальных типов, например, в коллекциях Java T используется для типа, K для ключа, V для значения.
3.1. Ограниченные дженерики
Как упоминалось ранее, параметры типа могут быть ограничены. Ограниченный означает ” ограниченный “, мы можем ограничить типы, которые могут быть приняты методом.
Например, мы можем указать, что метод принимает тип и все его подклассы (верхняя граница) или тип все его суперклассы (нижняя граница).
Чтобы объявить верхний ограниченный тип, мы используем ключевое слово extends после типа, за которым следует верхняя граница, которую мы хотим использовать. Например:
publicList fromArrayToList(T[] a) { ... }
Ключевое слово extends используется здесь для обозначения того, что тип T расширяет верхнюю границу в случае класса или реализует верхнюю границу в случае интерфейса.
3.2. Множественные границы
Тип также может иметь несколько верхних границ следующим образом:
Если один из типов, расширяемых T , является классом (т. Е. Числом ), он должен быть помещен первым в список границ. В противном случае это приведет к ошибке во время компиляции.
4. Использование Подстановочных Знаков С Дженериками
Подстановочные знаки представлены знаком вопроса в Java ” ? ” и они используются для обозначения неизвестного типа. Подстановочные знаки особенно полезны при использовании дженериков и могут использоваться в качестве типа параметра, но сначала следует рассмотреть важное примечание.
Известно, что Объект является ли супертип всех классов Java, однако, коллекцией Объект не является супертипом какой-либо коллекции.
Например, List не является супертипом List и назначение переменной типа List переменной типа List приведет к ошибке компилятора. Это делается для предотвращения возможных конфликтов, которые могут возникнуть, если мы добавим разнородные типы в одну и ту же коллекцию.
То же правило применяется к любой коллекции типа и его подтипов. Рассмотрим этот пример:
public static void paintAllBuildings(Listbuildings) { buildings.forEach(Building::paint); }
если мы представим себе подтип Building , например, a Дом , мы не можем использовать этот метод со списком Дом , даже если Дом является подтипом Здание . Если нам нужно использовать этот метод с построением типа и всеми его подтипами, то ограниченный подстановочный знак может сделать волшебство:
public static void paintAllBuildings(List extends Building> buildings) { ... }
Теперь этот метод будет работать с типом Building и всеми его подтипами. Это называется верхним ограниченным подстановочным знаком, где тип Building является верхней границей.
Подстановочные знаки также могут быть указаны с нижней границей, где неизвестный тип должен быть супертипом указанного типа. Нижние границы могут быть заданы с помощью ключевого слова super , за которым следует конкретный тип, например, super T> означает неизвестный тип, который является суперклассом T и всех его родителей). super T>
5. Тип Стирания
Дженерики были добавлены в Java для обеспечения безопасности типов и для того, чтобы дженерики не вызывали накладных расходов во время выполнения, компилятор применяет процесс, называемый стирание типов для дженериков во время компиляции.
Стирание типа удаляет все параметры типа и заменяет их границами или Object , если параметр типа неограничен. Таким образом, байт-код после компиляции содержит только обычные классы, интерфейсы и методы, что гарантирует отсутствие создания новых типов. Правильное приведение также применяется к типу Object во время компиляции.
Это пример стирания типов:
publicList genericMethod(List list) { return list.stream().collect(Collectors.toList()); }
При стирании типа неограниченный тип T заменяется на Object следующим образом:
// for illustration public List
Если тип ограничен, то во время компиляции он будет заменен на связанный:
publicvoid genericMethod(T t) { ... }
изменится после компиляции:
public void genericMethod(Building t) { ... }
6. Универсальные и примитивные типы данных
Ограничение универсалий в Java заключается в том, что параметр типа не может быть примитивным типом.
Например, следующее не компилируется:
Listlist = new ArrayList<>(); list.add(17);
Чтобы понять, почему примитивные типы данных не работают , давайте вспомним, что универсальные типы являются функцией времени компиляции , что означает, что параметр типа стирается и все универсальные типы реализуются как тип Объект .
В качестве примера давайте рассмотрим метод add списка:
Listlist = new ArrayList<>(); list.add(17);
Подпись метода add является:
boolean add(E e);
И будет скомпилирован для:
boolean add(Object e);
Поэтому параметры типа должны быть преобразованы в Object . Поскольку примитивные типы не расширяют Объект , мы не можем использовать их в качестве параметров типа.
Однако Java предоставляет упакованные типы для примитивов, а также автобоксы и распаковку, чтобы развернуть их:
Integer a = 17; int b = a;
Итак, если мы хотим создать список, который может содержать целые числа, мы можем использовать оболочку:
Listlist = new ArrayList<>(); list.add(17); int first = list.get(0);
Скомпилированный код будет эквивалентен:
List list = new ArrayList<>(); list.add(Integer.valueOf(17)); int first = ((Integer) list.get(0)).intValue();
Будущие версии Java могут разрешить примитивные типы данных для универсальных. Проект Valhalla направлен на улучшение способа обработки дженериков. Идея состоит в том, чтобы реализовать специализацию дженериков, как описано в JEP 218 .
7. Заключение
Дженерики Java являются мощным дополнением к языку Java, поскольку они облегчают работу программиста и менее подвержены ошибкам. Универсальные алгоритмы обеспечивают корректность типов во время компиляции и, самое главное, позволяют реализовывать универсальные алгоритмы, не вызывая дополнительных накладных расходов для наших приложений.
Исходный код, сопровождающий статью, доступен на GitHub .