Что такое дженерики программирование
Перейти к содержимому

Что такое дженерики программирование

  • автор:

Что такое дженерики в C#?

Дженерики (Generics) в C# — это механизм, позволяющий создавать универсальные классы, структуры, интерфейсы, методы и делегаты, которые работают с различными типами данных без потери типовой информации и производительности. Основная цель дженериков — обеспечить повторное использование кода и обеспечить типобезопасность при работе с разными типами данных.

Дженерики вводят параметры типа, представленные в угловых скобках (), которые заменяются на реальные типы данных во время создания экземпляров обобщенного класса или вызова обобщенного метода.

Пример использования дженериков в классе:

public class GenericList
private T[] elements;
private int count = 0;
public GenericList(int capacity)
elements = new T[capacity];
>
public void Add(T item)
elements[count] = item;
count++;
>
public T GetElementAt(int index)
return elements[index];
>
>

В данном примере T является параметром типа. Вместо него в реальных объектах будут подставляться другие типы данных, например, int, string, или любой пользовательский тип. Создание экземпляра обобщенного класса:

GenericList intList = new GenericList(10);
GenericList stringList = new GenericList(5);

Преимущества использования дженериков:

  1. Повышение безопасности типов.
  2. Улучшение производительности, так как обобщенные типы предоставляют возможность создания эффективного кода, избегая упаковки (boxing) и распаковки (unboxing) значимых типов.
  3. Повторное использование кода, так как один обобщенный класс может быть использован с различными типами данных.

Введение в дженерики – обобщенные классы и функции

В программировании иногда приходится обрабатывать разные типы данных похожим образом. С другой стороны, в языках со статической типизацией, к которым относится Kotlin, параметры функции типизированы. Это значит, что если функция принимает целое число, ей нельзя передать вещественное.

Частично проблему решает наследование, ведь мы можем присваивать переменным родительских типов объекты дочерних:

fun main()  val a: Int = 10 val b: Double = 1.5 fraction(a) // 2.0 fraction(b) // 0.3 >
fun fraction(n: Number)  println(n.toDouble() / 5) >

В данном случае Number – это класс Kotlin, который является родительским для числовых типов данных. Однако подобное не всегда подходит. Например, мы хотели бы возвращать из функции данные заранее точно неизвестного типа.

Некоторые языки программирования позволяют создавать так называемые дженерики (generics) – обобщенные функции и классы. Рассмотрим пример определения и вызова обобщенной функции на языке Kotlin.

fun main()  val a: Int = 10 val b: String = "Hello" val c: ListInt> = listOf(1, 5) val aa: ListInt> = doTwo(a) val bb: ListString> = doTwo(b) val cc: ListListInt>> = doTwo(c) println(aa) // [10, 10] println(bb) // [Hello, Hello] println(cc) // [[1, 5], [1, 5]] >
fun T> doTwo(obj: T): ListT>  val list: ListT> = listOf(obj, obj) return list >

Функция doTwo() не только способна принимать разный тип данных, но и возвращает разное.

T – это неизвестный на момент определения функции тип параметра. О том, что функция параметризована таким образом, что она обобщенная, относится к дженерикам, сообщается угловыми скобками перед ее именем. Буква T – это просто соглашение, обозначить неизвестный тип можно любым идентификатором.

Другими словами, записывая перед именем функции , мы говорим, что везде где в функции будет встречаться идентификатор T , его нужно будет заменить на тип, который будет известен в момент вызова функции. Когда функция doTwo() вызывается с аргументом-целым числом, то T становится Int , когда со списком – T становится List . Когда мы вызываем функцию, передавая ей строку, то тип параметра obj – это String , а возвращаемого из функции значения – List .

Не обязательно, чтобы все параметры функции-дженерика были параметризованы. Так ниже, у функции parePrint неизвестный тип имеет только один параметр, у второго тип определен – Char .

fun main()  val a: Int = 10 val b: String = "Hello" val c: ListInt> = listOf(10, 16, 3) parePrint(a, ') // parePrint(b, '[') // [Hello] parePrint(c, '"') // "[10, 16, 3]" >
fun T> parePrint(obj: T, p: Char)  when(p)  '(', ')' -> println("($obj)") '[', ']' -> println("[$obj]") ', '>' -> println("") else -> println("$p$obj$p") > >

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

fun main()  val a: Int = 10 val b: Double = 1.5 println(fraction(a, 5)) // 2.0 println(fraction(b, 3)) // 0.5 >
fun T: Number> fraction(n: T, f: Int): Double  return n.toDouble() / f >

В отличие от приведенного в начале урока примера обычной функции, в которой параметр n имеет тип Number , здесь n в момент вызова функции принимает более конкретный тип. Например, Int .

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

class SomethingT>  val prop: T constructor(p: T)  prop = p > >
fun main()  val a: SomethingInt> = Something(10) val b: SomethingString> = Something("Hello") println(a.prop) // 10 println(b.prop) // Hello >

Такой класс называют параметризованным, так как на его основе создаются объекты по сути разных типов. В примере мы не можем объект типа Something присвоить переменной, объявленной как Something .

Класс выше описан через вторичный конструктор для наглядности. Обычно используется первичный конструктор. Класс будет выглядеть так:

class SomethingT>(p: T)  val prop: T = p >
class SomethingT>(val prop: T)

С подобным мы уже сталкивались, используя стандартную библиотеку Kotlin. Так массивы, списки и словари – это параметризованные классы.

fun main() { val a: ListInt> = listOf(4, 5) val b: MapChar, Int> = mapOf('a' to 2, 'b' to 10) }

В случае со словарями у класса не один, а два обобщенных параметра. В объявлении класса это выглядит примерно так:

class SomethingT, V>(p: T, q: V)  val prop: T = p val qty: V = q >

Какими типами окажутся поля prop и qty определится только при создании объекта.

fun main()  val a: SomethingString, Int> a = Something("Hello", 5) >

Дженерики в Go — подробности из блога разработчиков

В Go 1.18 добавлена поддержка дженериков. Это самое большое нововведение с момента первого Open Source выпуска Go. Не будем пытаться охватить все детали, затронем всё важное. Подробное описание со множеством примеров смотрите в документе с предложением по улучшению языка. Материалом делимся к старту курса по Backend-разработке на Go.

Введение в дженерики

За основу этого поста взято наше выступление на GopherCon 2021:

Точное описание изменений в Go см. в обновлённой спецификации языка. (Внимание: в фактической реализации 1.18 на то, что разрешено в документе с предложением по улучшению, наложены ограничения. Спецификация должна быть точной. В будущих выпусках некоторые ограничения могут быть сняты.)

Дженерики — это способ написания кода, который не зависит от конкретных применяемых типов. Функции и типы теперь могут быть написаны для любого набора типов.

С дженериками в язык добавляются три важные функциональные возможности:

  1. Типы как параметры для функций и типов.
  2. Определение интерфейсных типов как наборов типов, в том числе типов без методов.
  3. Выведение типа, когда во многих случаях типы аргументов при вызове функции опускаются.

Типы как параметры

В функциях и типах теперь параметром может быть тип. Список таких параметров выглядит как список обычных параметров, но вместо круглых скобок используются квадратные.

Чтобы показать принцип работы, начнём с простой функции Min без параметров, для значений с плавающей точкой:

func Min(x, y float64) float64 < if x < y < return x >return y >

Параметризуем эту функцию для работы с разными типами, вместо типа float64 добавив список с одним параметром типа T:

import "golang.org/x/exp/constraints" func GMin[T constraints.Ordered](x, y T) T < if x < y < return x >return y >

И вызовем её с типом в качестве аргумента:

x := GMin[int](2, 3)

Указание в GMin типа int как аргумента называется инстанцированием. В компиляторе инстанцирование происходит в два этапа:

  1. Замена всех аргументов-типов на соответствующие типам параметры.
  2. Проверка, что каждый тип соответствует своим ограничениям. Подробности позже. Если второй этап не пройден, инстанцирование не происходит и программа не будет работать.

После инстанцирования оставшаяся без параметров функция вызывается так же, как и любая другая. Например, в этом коде:

fmin := GMin[float64] m := fmin(2.71, 3.14)

при инстанцировании GMin[float64] фактически получается исходная функция Min для значений с плавающей точкой. Эту функцию можно вызывать.

У типов теперь тоже могут быть параметры:

type Tree[T interface<>] struct < left, right *Tree[T] value T >func (t *Tree[T]) Lookup(x T) *Tree[T] < . >var stringTree Tree[string]

Здесь в дженерик-типе Tree хранятся значения параметра типа T. В дженерик-типах могут быть и методы, такие как Lookup выше. Чтобы использовать дженерик-тип, его нужно инстанцировать. Tree[string] — пример инстанцирования Tree с типом-аргументом string.

Наборы типов

Рассмотрим подробнее аргументы-типы, применяемые для инстанцирования типа как параметра.

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

Аналогично, в списках типов как параметров тип есть у каждого параметра. Но тип-параметр — сам по себе тип, а значит, типы-параметры определяют наборы типов. Такой набор (метатип) также называется ограничением типа.

В параметризованной функции GMin ограничение типа импортируется из пакета constrains. В ограничении Ordered описывается набор всех типов со значениями, которые можно упорядочить или, другими словами, сравнить через операторы < (или и т. д.).

Это ограничение гарантирует передачу в GMin только типов с упорядоченными значениями. Кроме того, значения параметра этого типа могут использоваться в теле функции GMin с оператором сравнения

В Go ограничения типа должны быть интерфейсами, поэтому интерфейсный тип может быть типом для значения и метатипом. Интерфейсы определяют методы. Поэтому очевидно, что мы можем выразить ограничения типа, требующие наличия определённых методов.

До недавнего времени в спецификации Go было заявлено, что интерфейс определяет набор методов, примерно соответствующий перечисленному в интерфейсе набору. Любой тип, реализующий все методы набора, реализует соответствующий интерфейс:

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

Два этих подхода приводят к одному результату: для каждого набора методов можно представить соответствующий набор типов, реализующих эти методы, и это набор определяемых интерфейсом типов.

Но для наших целей подход с набором типов предпочтительнее: можно явно добавлять типы в набор и, таким образом, по-новому управлять набором типов. Для этого мы расширили синтаксис интерфейсных типов. Например, interface < int|string|bool >определяет набор типов int, string и bool:

По новому подходу этому интерфейсу соответствуют только int, string или bool.

Рассмотрим фактическое определение contraints.Ordered:

type Ordered interface

Здесь интерфейс Ordered — это набор всех целочисленных типов, типов числа с плавающей точкой и строковых типов. Вертикальная полоса обозначает объединение типов (или наборов типов в данном случае).

Integer и Float — это интерфейсные типы, аналогично определённых в пакете constraints. Обратите внимание: нет методов, определяемых интерфейсом Ordered.

Что касается ограничений типа, конкретный тип (например, string) нас обычно не так интересует, как все строковые типы. Вот для чего нужен токен ~: выражение ~string означает набор всех типов с базовым типом string. Это сам тип string и все типы, объявленные с такими определениями, как type MyString string.

Конечно, методы всё равно нужно указывать в интерфейсах и с сохранением обратной совместимости. В Go 1.18, как и прежде, в интерфейсе могут иметь место методы и встроенные интерфейсы, а ещё — встроенные неинтерфейсные типы, объединения и наборы базовых типов.

Набор определяемых интерфейсом типов, когда интерфейс используется в качестве ограничения типа, точно указывает типы, разрешённые как типы-аргументы для соответствующего параметра типа.

В теле параметризованной функции, тип операнда которой — тип-параметр P с ограничением C, операции разрешены, если они разрешены всеми типами в наборе типов C. Сейчас здесь есть ряд ограничений реализации, но в обычном коде встреча с ними маловероятна.

Используемым как ограничения интерфейсам можно присваивать имена (например, Ordered). Или они могут быть литеральными интерфейсами, встроенными в список типов-параметров. Например:

[S interface<~[]E>, E interface<>]

Здесь S — это тип среза, тип конкретного элемента среза может быть любым.

Это типичный случай, поэтому внешний interface<> для интерфейсов в позиции ограничения можно опустить и просто написать:

[S ~[]E, E interface<>]

Пустой интерфейс часто встречается в списках типов как параметров, да и в обычном коде на Go тоже. Поэтому в качестве псевдонима для пустого интерфейсного типа в Go 1.18 появился новый предварительно объявляемый идентификатор any. С ним получаем идиоматический код:

[S ~[]E, E any]

Интерфейсы как наборы типов — это новый мощный механизм и ключевой фактор для работы ограничений типа на Go. Пока интерфейсы с новыми синтаксическими формами могут использоваться только в качестве ограничений, но нетрудно представить, насколько в целом могут быть полезны ограничивающие тип интерфейсы с явным определением типов.

Выведение типов

Новая значительная возможность языка — выведение типов. Это самое сложное, но и важное нововведение, допускающее при написании кода применение естественного стиля, где вызываются параметризованные функции.

Выведение типа-аргумента функции

С типами-параметрами связана необходимость передачи типов как аргументов, которая может привести к перегруженности кода.

Вернёмся к параметризованной функции GMin:

func GMin[T constraints.Ordered](x, y T) T

Тип-параметр T нужен, чтобы не указывать обычные типы x и y. Как мы видели ранее, эта функция может вызываться с помощью аргумента с явно заданным типом:

var a, b, m float64 m = GMin[float64](a, b) // explicit type argument

В компиляторе во многих случаях тип-аргумент для T может выводиться из обычных аргументов. Результат — столь же чёткий код, но короче:

var a, b, m float64 m = GMin(a, b) // no type argument

Эффект достигается сопоставлением типов аргументов a и b с типами параметров x и y.

Такое выведение аргументов-типов из типов аргументов в функцию называется выведением типа аргумента функции. Оно происходит только для параметров типа, которые используются в параметрах функции, а не исключительно в результатах функции или в её теле.

Например, выведение типа не применяется к таким функциям, как MakeT[T any]() T, в которых T используется только для результата.

Выведение типа ограничения

Язык поддерживает выведение типа ограничения. Чтобы описать его, начнём с этого примера масштабирования среза целых чисел:

// Scale returns a copy of s with each element multiplied by c. // This implementation has a problem, as we will see. func Scale[E constraints.Integer](s []E, c E) []E < r := make([]E, len(s)) for i, v := range s < r[i] = v * c >return r >

Это параметризованная функция, она работает для среза любого целочисленного типа.

Рассмотрим многомерный тип Point, где каждый Point — это список целых чисел, определяющих координаты точки. Конечно, у этого типа есть методы:

type Point []int32 func (p Point) String() string < // Details not important. >

Чтобы масштабировать Point, пригодится функция Scale:

// ScaleAndPrint doubles a Point and prints it. func ScaleAndPrint(p Point) < r := Scale(p, 2) fmt.Println(r.String()) // DOES NOT COMPILE >

Но она не компилируется и завершается ошибкой r.String undefined (type []int32 has no field or method String) .

Проблема заключается в том, что функция Scale возвращает значение типа []E, где E — это тип элемента среза аргумента. Когда мы вызываем Scale со значением типа Point, базовый тип которого — []int32, то получаем значение типа []int32, а не Point. Это обусловлено самим способом написания кода (дженериком). Но это не то, что нам здесь нужно.

Решим проблему, изменив функцию Scale (для типа среза используем тип-параметр:

// Scale returns a copy of s with each element multiplied by c. func Scale[S ~[]E, E constraints.Integer](s S, c E) S < r := make(S, len(s)) for i, v := range s < r[i] = v * c >return r >

Мы ввели новый тип-параметр среза S и ограничили его так, чтобы базовым типом стал S, а не []E, и типом результата также был S. Но E может быть только целым числом, поэтому эффект тот же, что и раньше: первый аргумент должен быть срезом целочисленного типа. Единственное изменение в теле функции: когда мы вызываем make — передаём S, а не []E.

Поведение новой функции — такое же, как и у прежней, если вызывать её с помощью обычного среза. Если же использовать тип Point, то получим значение типа Point. Это то, что нам нужно. В этой версии Scale более ранняя функция ScaleAndPrint будет компилироваться и запускаться, как мы ожидаем.

Но почему можно писать вызов к Scale без передачи аргументов с явно заданным типом? То есть почему, вместо того чтобы писать Scale[Point, int32](p, 2), мы можем написать Scale(p, 2) без типов-аргументов?

В новой функции Scale теперь два типа-параметра: S и E. Поскольку при вызове к Scale никаких типов-аргументов не передаётся, то описанный выше механизм выведения типа-аргумента функции позволяет компилятору в качестве типа-аргумента для S вывести Point.

Но у функции есть ещё тип-параметр E, — это тип множителя с . Соответствующий аргумент функции равен 2, а поскольку 2 — это нетипизированная константа, вывод типа аргумента функции не может вывести правильный тип для E: в лучшем случае он может вывести тип по умолчанию для 2, — int, что неверно.

Вместо этого происходит процесс, с помощью которого в компиляторе выводится, что тип-аргумент для E — это тип элемента среза. Этот процесс называется выведением типа ограничения. Типы-аргументы выводятся из ограничений параметров типа.

Выведение типа ограничения применяется, когда у одного типа-параметра есть ограничение, определяемое как другой тип-параметр. Когда тип-параметр одного из таких парметров известен, ограничение используется для выведения типа-аргумента другого типа-аргумента.

Обычный случай применения такого вывода — когда в одном ограничении используется форма ~type для типа, в свою очередь записываемого с помощью других типов-параметров. Мы видим это в примере со Scale.

Здесь S — это ~[]E, то есть ~, за которым идёт тип []E, написанный как другой тип-параметр. Если мы знаем тип-аргумент для S, то можем вывести и тип-аргумент для E. S — это тип среза, а E — тип элемента этого среза.

Мы рассмотрели лишь основы выведения типа ограничения. Подробности смотрите в документе с предложением или в спецификации языка.

Выведение типа на практике

Механизм выведения типа сложен, но применять его просто: выведение типа либо происходит, либо нет. Если тип выводится, типы-аргументы можно опустить — тогда вызов параметризованных функций ничем не отличается от вызова обычных функций. Если выведение типа не происходит, в компиляторе выдаётся сообщение об ошибке — тогда мы можем просто указать необходимые типы-аргументы.

Добавляя в язык выведение типа, мы стремились к оптимальному сочетанию возможностей и сложности — чтобы выводимые в компиляторе типы никогда не вызывали удивления. Мы старались исключить возможность выведения неверного типа, то лучше он не будет выведен.

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

Заключение

Дженерики — самое большое нововведение в Go 1.18. Языковым изменениям потребовалось много нового кода, который не проходил серьёзного тестирования в производственных условиях. Оно будет пройдено, только когда больше людей воспользуются дженериками.

Мы считаем, что их функционал хорошо реализован. Но это, в отличие от большинства аспектов Go, нельзя подкрепить реальным опытом. Поэтому, хотя мы и приветствуем использование дженериков там, где это имеет смысл, призываем быть осторожными при развёртывании такого кода на продакшене. Тем не менее надеемся, что с появлением дженериков программисты Go станут продуктивнее.

А мы поможем вам прокачать навыки или освоить профессию в IT, востребованную в любое время:

  • Профессия Backend-разработчик на Go
  • Профессия Fullstack-разработчик на Python

Краткий каталог профессий и курсов

Data Science и Machine Learning

  • Профессия Data Scientist
  • Профессия Data Analyst
  • Курс «Математика для Data Science»
  • Курс «Математика и Machine Learning для Data Science»
  • Курс по Data Engineering
  • Курс «Machine Learning и Deep Learning»
  • Курс по Machine Learning

Python, веб-разработка

  • Профессия Fullstack-разработчик на Python
  • Курс «Python для веб-разработки»
  • Профессия Frontend-разработчик
  • Профессия Веб-разработчик

Мобильная разработка

  • Профессия iOS-разработчик
  • Профессия Android-разработчик

Java и C#

  • Профессия Java-разработчик
  • Профессия QA-инженер на JAVA
  • Профессия C#-разработчик
  • Профессия Разработчик игр на Unity

От основ — в глубину

  • Курс «Алгоритмы и структуры данных»
  • Профессия C++ разработчик
  • Профессия Этичный хакер

А также

Теория дженериков в Java или как на практике ставить скобки

Java-университет

Дженерики (обобщения) — это особые средства языка Java для реализации обобщённого программирования: особого подхода к описанию данных и алгоритмов, позволяющего работать с различными типами данных без изменения их описания. На сайте Oracle дженерикам посвящён отдельный tutorial: «Lesson: Generics».

Во-первых, чтобы понять дженерики, нужно разобраться, зачем они вообще нужны и что они дают. В tutorial в разделе «Why Use Generics?» сказано, что одно из назначений — более сильная проверка типов во время компиляции и устранение необходимости явного приведения.

Теория дженериков в Java или как на практике ставить скобки - 2

Приготовим для опытов любимый tutorialspoint online java compiler. Представим себе такой вот код:

 import java.util.*; public class HelloWorld < public static void main(String []args)< List list = new ArrayList(); list.add("Hello"); String text = list.get(0) + ", world!"; System.out.print(text); >> 

Этот код выполнится хорошо. Но что если к нам пришли и сказали, что фраза «Hello, world!» избита и можно вернуть только Hello? Удалим из кода конкатенацию со строкой «, world!» . Казалось бы, что может быть безобиднее? Но на деле мы получим ошибку ПРИ КОМПИЛЯЦИИ: error: incompatible types: Object cannot be converted to String Всё дело в том, что в нашем случае List хранит список объектов типа Object. Так как String — наследник для Object (ибо все классы неявно наследуются в Java от Object), то требует явного приведения, чего мы не сделали. А при конкатенации для объекта будет вызван статический метод String.valueOf(obj), который в итоге вызовет метод toString для Object. То есть List у нас содержит Object. Выходит, там где нам нужен конкретный тип, а не Object, нам придётся самим делать приведение типов:

 import java.util.*; public class HelloWorld < public static void main(String []args)< List list = new ArrayList(); list.add("Hello!"); list.add(123); for (Object str : list) < System.out.println((String)str); >> > 

Однако, в данном случае, т.к. List принимает список объектов, он хранит не только String, но и Integer. Но самое плохое, в этом случае компилятор не увидит ничего плохого. И тут мы получим ошибку уже ВО ВРЕМЯ ВЫПОЛНЕНИЯ (ещё говорят, что ошибка получена «в Runtime»). Ошибка будет: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String Согласитесь, не самое приятное. И всё это потому, что компилятор — не искусcтвенный интеллект и он не может угадать всё, что подразумевает программист. Чтобы рассказать компилятору подробнее о своих намерениях, какие типы мы собираемся использовать, в Java SE 5 ввели дженерики. Исправим наш вариант, подсказав компилятору, что же мы хотим:

 import java.util.*; public class HelloWorld < public static void main(String []args)< Listlist = new ArrayList<>(); list.add("Hello!"); list.add(123); for (Object str : list) < System.out.println(str); >> > 

Как мы видим, нам больше не нужно приведение к String. Кроме того, у нас появились угловые скобки (angle brackets), которые обрамляют дженерики. Теперь компилятор не даст скомпилировать класс, пока мы не удалим добавление 123 в список, т.к. это Integer. Он нам так и скажет. Многие называют дженерики «синтаксическим сахаром». И они правы, так как дженерики действительно при компиляции станут теми самыми кастами. Посмотрим на байткод скомпилированных классов: с кастом вручную и с использованием дженериков:

Теория дженериков в Java или как на практике ставить скобки - 3

После компиляции какая-либо информация о дженериках стирается. Это называется «Стирание типов» или «Type Erasure». Стирание типов и дженерики сделаны так, чтобы обеспечить обратную совместимость со старыми версиями JDK, но при этом дать возможность помогать компилятору с определением типа в новых версиях Java.

Теория дженериков в Java или как на практике ставить скобки - 4

Raw Types или сырые типы

Говоря о дженериках мы всегда имеем две категории: типизированные типы (Generic Types) и «сырые» типы (Raw Types). Сырые типы — это типы без указания «уточненения» в фигурных скобках (angle brackets):

Теория дженериков в Java или как на практике ставить скобки - 5

Типизированные типы — наоборот, с указанием «уточнения»:

Теория дженериков в Java или как на практике ставить скобки - 6

Как мы видим, мы использовали необычную конструкцию, отмеченную стрелкой на скриншоте. Это особый синтаксис, который добавили в Java SE 7, и называется он «the diamond», что в переводе означает алмаз. Почему? Можно провести аналогию формы алмаза и формы фигурных скобок: <> Также Diamond синтаксис связан с понятием «Type Inference», или же выведение типов. Ведь компилятор, видя справа <> смотрит на левую часть, где расположено объявление типа переменной, в которую присваивается значение. И по этой части понимает, каким типом типизируется значение справа. На самом деле, если в левой части указан дженерик, а справа не указан, компилятор сможет вывести тип:

 import java.util.*; public class HelloWorld < public static void main(String []args) < Listlist = new ArrayList(); list.add("Hello World"); String data = list.get(0); System.out.println(data); > > 

Однако это будет смешиванием нового стиля с дженериками и старого стиля без них. И это крайне нежелательно. При компиляции кода выше мы получим сообщение: Note: HelloWorld.java uses unchecked or unsafe operations . На самом деле кажется непонятным, зачем вообще нужен тут diamond добавлять. Но вот пример:

 import java.util.*; public class HelloWorld < public static void main(String []args) < Listlist = Arrays.asList("Hello", "World"); List data = new ArrayList(list); Integer intNumber = data.get(0); System.out.println(data); > > 

Как мы помним, у ArrayList есть и второй конструктор, который принимает на вход коллекцию. И вот тут-то и кроется коварство. Без diamond синтаксиса компилятор не понимает, что его обманывают, а вот с diamond — понимает. Поэтому, правило #1: всегда использовать diamond синтаксис, если мы используем типизированные типы. В противном случае мы рискуем пропустить, где у нас используется raw type. Чтобы избежать предупреждений в логе о том, что «uses unchecked or unsafe operations» можно над используемым методом или классом указать особую аннотацию: @SuppressWarnings(«unchecked») Suppress переводится как подавлять, то есть дословно — подавить предупреждения. Но подумайте, почему вы решили её указать? Вспомните о правиле номер один и, возможно, вам нужно добавить типизацию.

Теория дженериков в Java или как на практике ставить скобки - 7

Типизированные методы (Generic Methods)

  • включает список типизированных параметров внутри угловых скобок;
  • список типизированных параметров идёт до возвращаемого метода.
 import java.util.*; public class HelloWorld < public static class Util < public static T getValue(Object obj, Class clazz) < return (T) obj; >public static T getValue(Object obj) < return (T) obj; >> public static void main(String []args) < List list = Arrays.asList("Author", "Book"); for (Object element : list) < String data = Util.getValue(element, String.class); System.out.println(data); System.out.println(Util.getValue(element)); > > > 

Если посмотреть на класс Util, видим в нём два типизированных метода. Благодаря возможности выведения типов мы можем предоставить определение типа непосредственно компилятору, а можем сами это указать. Оба варианта представлены в примере. Кстати, синтаксис весьма логичен, если подумать. При типизировании метода мы указываем дженерик ДО метода, потому что если мы будем использовать дженерик после метода, Java не сможет понять, какой тип использовать. Поэтому сначала объявляем, что будем использовать дженерик T, а потом уже говорим, что этот дженерик мы собираемся возвращать. Естественно, Util.getValue(element, String.class) упадёт с ошибкой incompatible types: Class cannot be converted to Class . При использовании типизированных методов стоит всегда помнить про стирание типов. Посмотрим на пример:

 import java.util.*; public class HelloWorld < public static class Util < public static T getValue(Object obj) < return (T) obj; >> public static void main(String []args) < List list = Arrays.asList(2, 3); for (Object element : list) < System.out.println(Util.getValue(element) + 1); > > > 

Он будет прекрасно работать. Но только до тех пор, пока компилятор будет понимать, что у вызываемого метода тип Integer. Заменим вывод на консоль на следующую строку: System.out.println(Util.getValue(element) + 1); И мы получим ошибку: bad operand types for binary operator ‘+’, first type: Object , second type: int То есть произошло стирание типов. Компилятор видит, что тип никто не указал, тип указывается как Object и выполнение кода падает с ошибкой.

Теория дженериков в Java или как на практике ставить скобки - 8

Типизированные классы (Generic Types)

Типизировать можно не только методы, но и сами классы. У Oracle в их гайде этому посвящён раздел «Generic Types». Рассмотрим пример:

 public static class SomeType  < public void test(Collection collection) < for (E element : collection) < System.out.println(element); >> public void test(List collection) < for (Integer element : collection) < System.out.println(element); >> > 

Тут всё просто. Если мы используем класс, дженерик указывается после имени класса. Давайте теперь в методе main создадим экземпляр этого класса:

 public static void main(String []args) < SomeTypest = new SomeType<>(); List list = Arrays.asList("test"); st.test(list); > 

Он отработает хорошо. Компилятор видит, что есть List из чисел и Collection типа String. Но что если мы сотрём дженерики и сделаем так:

 SomeType st = new SomeType(); List list = Arrays.asList("test"); st.test(list); 

Мы получим ошибку: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer Опять стирание типов. Поскольку у класса больше нет дженерика, компилятор решает: раз мы передали List, метод с List более подходящий. И мы падаем с ошибкой. Поэтому, правило #2: Если класс типизирован, всегда указывать тип в дженерике.

Ограничения

К типам, указываемым в дженериках мы можем применить ограничение. Например, мы хотим, чтобы контейнер принимал на вход только Number. Данная возможность описана в Oracle Tutorial в разделе Bounded Type Parameters. Посмотрим на пример:

 import java.util.*; public class HelloWorld < public static class NumberContainer < private T number; public NumberContainer(T number) < this.number = number; >public void print() < System.out.println(number); >> public static void main(String []args) < NumberContainer number1 = new NumberContainer(2L); NumberContainer number2 = new NumberContainer(1); NumberContainer number3 = new NumberContainer("f"); >> 
  • Upper Bounded Wildcards —
  • Unbounded Wildcards —
  • Lower Bounded Wildcards —

Теория дженериков в Java или как на практике ставить скобки - 9

Данный принцип ещё называют принципом PECS (Producer Extends Consumer Super). Подробнее можно прочитать на хабре в статье «Использование generic wildcards для повышения удобства Java API», а также в отличном обсуждении на stackoverflow: «Использование wildcard в Generics Java». Вот небольшой пример из исходников Java — метод Collections.copy:

Теория дженериков в Java или как на практике ставить скобки - 10

Ну и небольшой примерчик того, как НЕ будет работать:

 public static class TestClass < public static void print(Listlist) < list.add("Hello World!"); System.out.println(list.get(0)); >> public static void main(String []args) < Listlist = new ArrayList<>(); TestClass.print(list); > 

Но если заменить extends на super, всё станет хорошо. Так как мы наполняем список list значением перед выводом, он для нас является потребителем, то есть consumer’ом. Следовательно, используем super.

Наследование

Есть ещё одна необычная особенность дженериков — это их наследование. Наследование дженериков описано в tutorial от Oracle в разделе «Generics, Inheritance, and Subtypes». Главное это запомнить и осознать следующее. Мы не можем сделать так:

 List list1 = new ArrayList(); 

Потому что наследование работает с дженериками по-другому:

Теория дженериков в Java или как на практике ставить скобки - 11

И вот ещё хороший пример, который упадёт с ошибкой:

 List list1 = new ArrayList<>(); List list2 = list1; 

Тут тоже всё просто. List не является наследником List, хотя String является наследником Object.

Final

  • Юрий Ткач: Сырые типы — Generics #1 — Advanced Java
  • Наследование и расширители обобщений — Generics #2 — Advanced Java
  • Рекурсивное расширение типа — Generics #3 — Advanced Java
  • Александр Маторин — Неочевидные Дженерики
  • Введение в Java. Generics. Wildcards | Технострим
  • O’Reilly : Java Generics and Collections

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *