Generics
Generics — набор свойств языка позволяющих определять и использовать обобщенные типы и методы.
Обобщенные типы или методы отличаются от обычных тем, что имеют типизированные параметры (T – параметр в котором могут быть разные объекты).
ArrayList<Integer> list = new ArrayList<Integer>();
- Строгая типизация.
- Единая реализация.
- Отсутствие информации о типе.
В <> могут быть только ссылочные типы: можно: String, Integer, int[] , нельзя: int, double .
Пусть у нас есть тип Foo, который является подтипом Bar, и еще G — наследник Коллекции.
То G<Foo> не является наследником G<Bar>.
public class Example <
public void print(Set<String> strSet)
public void print(Set<Integer> intSet)
Примером использования обобщенных типов может служить Java Collection Framework.
Так, класс LinkedList<E> — типичный обобщенный тип. Он содержит параметр E, который представляет тип элементов, которые будут храниться в коллекции.
Создание объектов обобщенных типов происходит посредством замены параметризированных типов реальными типами данных.
Вместо того, чтобы просто использовать LinkedList, ничего не говоря о типе элемента в списке, предлагается использовать точное указание типа LinkedList<String>, LinkedList<Integer> и т.п.
Для чего нужны дженерики?
Generics (обобщенные типы и методы) позволяют нам уйти от жесткого определения используемых типов.
С их помощью можно объявлять классы, интерфейсы и методы, где тип данных указан в виде параметра.
Возможность создавать универсальные алгоритмы и структуры данных.
Позволяет осуществлять проверку на правильность написания кода во время компиляции, а не в Runtime.
Компилятор стирает все дженерики. В Runtime дженериков практически нет. Компилятор использует кастование.
Хоть бы слово про стирание типов.
Сделали использование Java Collection Framework проще, удобнее и безопаснее.
Стирание типов
Это когда при компиляции из обобщенных классов с параметрами генерируется код обычных сырых классов с расстановкой, где нужно, приведения типов, генерацией дополнительных методов и т.д.
Дженерики существуют только на этапе компиляции и только для компилятора.
Стираются вверх (т.е. если не указано extends, то стирается в Object)
Что такое сырые типы (raw type)?
Raw type — это имя интерфейса без указания параметризованного типа
Сырые типы — это типы без указания «уточненения» в фигурных скобках.
Нужны чтобы поддерживать старый код (обратная совместимость).
Рекомендуется использовать хоть какие-то параметризованные типы.
Также Diamond синтаксис связан с понятием «Type Inference», или же выведение типов. Ведь компилятор, видя справа <> смотрит на левую часть, где расположено объявление типа переменной, в которую присваивается значение. И по этой части понимает, каким типом типизируется значение справа.
На самом деле, если в левой части указан дженерик, а справа не указан <> , компилятор сможет вывести тип. Однако это будет смешиванием нового стиля с дженериками и старого стиля без них — вы теряете безопасность (типобезопасность) типов (может добавляться какой угодно тип в список, а не то что надо. Когда будет доставаться – то может быть сюрприз)))
ArrayList <String> strings = new ArrayList<>(); // parameterized type
ArrayList arrayList = new ArrayList(); // raw type
arrayList = strings; // Ok
strings = arrayList; // Unchecked assignment (назначение)
arrayList.add(1); //unchecked call
Что такое вайлдкарды (Маски)
Языковая конструкция внутри diamond-оператора, позволяющая сделать код более универсальным.
Решает проблему наследования типов в дженериках.
Может быть 3-х типов: инвариантность, upper и lower.
Принцип PECS
The Get and Put Principle или PECS (Producer Extends Consumer Super) Get and Put Principe
Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super).
Запись вида Collection<?> равносильна Collection<? extends Object> , а значит — коллекция может содержать объекты любого класса, так как все классы в Java наследуются от Object – поэтому подстановка называется неограниченной.
Если мы объявили wildcard с extends, то это producer. Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает.
Если же мы объявили wildcard с super — то это consumer. Он только принимает, а предоставить ничего не может.
Если метод имеет аргументы с параметризованным типом (например, Collection или Predicate), то в случае, если аргумент — производитель (producer), нужно использовать ? extends T, а если аргумент — потребитель (consumer), нужно использовать ? super T.
Eсли метод читает данные из аргумента, то этот аргумент — производитель, Метод передаёт данные в аргумент, то аргумент является потребителем. Важно заметить, что определяя производителя или потребителя, мы рассматриваем только данные типа T.
Пришел, увидел, обобщил: погружаемся в Java Generics
Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.
Работа с коллекциями
Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:
Мы итерировались, пробегались по списку аккаунтов и проверяли, действительно ли элемент из этого списка является экземпляром класса Account — то есть счетом пользователя. Выполняли приведение типа нашего объекта класса Account и метод getAmount , который возвращал сумму на этом счете. Дальше все это суммировали и возвращали итоговую сумму. Требовалось выполнить два действия:
Если не сделать проверку ( instanceof ) на принадлежность к классу Account , то на втором этапе возможен ClassCastException – то есть аварийное завершение программы. Поэтому такая проверка была обязательной.
С появлением Generics необходимость в проверке и приведении типа отпала:
Теперь метод принимает в качестве аргументов только список объектов класса Account . Это ограничение указано в самом методе, в его сигнатуре, программист просто не может передать никакой другой список — только список клиентских счетов.
Нам не нужно выполнять проверку типа элементов из этого списка: она подразумевается описанием типа у параметра метода (можно прочитать как список объектов класса Account ). И компилятор выдаст ошибку, если что-то пойдет не так — то есть если кто-то попробует передать в этот метод список объектов, отличных от класса Account .
Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.
Принцип подстановки
Принцип подстановки Барбары Лисков – специфичное определение подтипа в объектно-ориентированном программировании. Идея Лисков о «подтипе» дает определение понятия замещения: если S является подтипом T , тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы.
| Тип | Подтип |
| Number | Integer |
| List<E> | ArrayList<E> |
| Collection<E> | List<E> |
| Iterable<E> | Collection<E> |
Примеры отношения тип/подтип
Вот пример использования принципа подстановки в Java:
Integer является подтипом Number , следовательно, переменной n типа Number можно присвоить значение, которое возвращает метод Integer.valueOf(42) .
Ковариантность, контравариантность и инвариантность
Сначала немного теории. Ковариантность — это сохранение иерархии наследования исходных типов в производных типах в том же порядке. Например, если Кошка — это подтип Животные, то Множество<Кошки> — это подтип Множество<Животные>. Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:
Множество<Животные> = Множество<Кошки>
Контравариантность — это обращение иерархии исходных типов на противоположную в производных типах. Например, если Кошка — это подтип Животные , то Множество<Животные> — это подтип Множество<Кошки>. Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:
Множество<Кошки> = Множество<Животные>
Инвариантность — отсутствие наследования между производными типами. Если Кошка — это подтип Животные, то Множество<Кошки> не является подтипом Множество<Животные> и Множество<Животные> не является подтипом Множество<Кошки>.
Массивы в Java ковариантны. Тип S[] является подтипом T[] , если S — подтип T . Пример присваивания:
Мы присвоили ссылку на массив строк переменной arr , тип которой – «массив объектов» . Если бы массивы не были ковариантными, нам бы это сделать не удалось. Java позволяет это сделать, программа скомпилируется и выполнится без ошибок.
Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.
«Дженерики» инвариантны. Приведем пример:
Если взять список целых чисел, то он не будет являться ни подтипом типа Number , ни каким-либо другим подтипом. Он является только подтипом самого себя. То есть List <Integer> — это List<Integer> и ничего больше. Компилятор позаботится о том, чтобы переменная ints , объявленная как список объектов класса Integer, содержала только объекты класса Integer и ничего кроме них. На этапе компиляции производится проверка, и у нас в рантайме уже ничего не упадет.
Wildcards
Всегда ли Generics инварианты? Нет. Приведу примеры:
Это ковариантность. List<Integer> — подтип List<? extends Number>
Это контравариантность. List<Number> является подтипом List<? super Integer> .
Запись вида «? extends . » или «? super . » — называется wildcard или символом подстановки, с верхней границей ( extends ) или с нижней границей ( super ). List<? extends Number> может содержать объекты, класс которых является Number или наследуется от Number . List<? super Number> может содержать объекты, класс которых Number или у которых Number является наследником (супертип от Number ).
Запись вида T2 <= T1 означает, что набор типов описываемых T2 является подмножеством набора типов описываемых T1
т.е.
Number <=? extends Object
? extends Number <=? extends Object
и
? super Object <=? super Number
Пара задачек для проверки знаний:
1. Почему в примере ниже compile-time error? Какое значение можно добавить в список nums ?
2. Почему нельзя получить элемент из списка ниже?
Нельзя прочитать элемент из контейнера с wildcard ? super , кроме объекта класса Object
The Get and Put Principle или PECS (Producer Extends Consumer Super)
Особенность wildcard с верхней и нижней границей дает дополнительные фичи, связанные с безопасным использованием типов. Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super ). Чтобы было легче запомнить, когда какой wildcard использовать, существует принцип PECS — Producer Extends Consumer Super.
- Если мы объявили wildcard с extends, то это producer. Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает.
- Если же мы объявили wildcard с super — то это consumer. Он только принимает, а предоставить ничего не может.
Метод осуществляет копирование элементов из исходного списка src в список dest . src — объявлен с wildcard ? extends и является продюсером, а dest — объявлен с wildcard ? super и является потребителем. Учитывая ковариантность и контравариантность wildcard, можно скопировать элементы из списка ints в список nums :
Если же мы по ошибке перепутаем параметры метода copy и попытаемся выполнить копирование из списка nums в список ints , то компилятор не позволит нам это сделать:
<?> и Raw типы
Ниже приведен wildcard с неограниченным символом подстановки. Мы просто ставим <?> , без ключевых слов super или extends :
На самом деле такой «неограниченный» wildcard все-таки ограничен, сверху. Collection<?> — это тоже символ подстановки, как и » ? extends Object «. Запись вида Collection<?> равносильна Collection<? extends Object> , а значит — коллекция может содержать объекты любого класса, так как все классы в Java наследуются от Object – поэтому подстановка называется неограниченной.
Если мы опустим указание типа, например, как здесь:
то, говорят, что ArrayList — это Raw тип параметризованного ArrayList<T>. Используя Raw типы, мы возвращаемся в эру до дженериков и сознательно отказываемся от всех фич, присущих параметризованным типам.
Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения.
Wildcard Capture
Попробуем теперь реализовать метод, выполняющий перестановку элементов списка в обратном порядке.
Ошибка компиляции возникла, потому что в методе reverse в качестве аргумента принимается список с неограниченным символом подстановки <?> .
<?> означает то же что и <? extends Object> . Следовательно, согласно принципу PECS, list – это producer . А producer только продюсирует элементы. А мы в цикле for вызываем метод set() , т.е. пытаемся записать в list . И поэтому упираемся в защиту Java, что не позволяет установить какое-то значение по индексу.
Что делать? Нам поможет паттерн Wildcard Capture . Здесь мы создаем обобщенный метод rev . Он объявлен с переменной типа T . Этот метод принимает список типов T , и мы можем сделать сет.
Теперь у нас все скомпилируется. Здесь произошел захват символа подстановки (wildcard capture). При вызове метода reverse(List<?> list) в качестве аргумента передается список каких-то объектов (например, строк или целых чисел). Если мы можем захватить тип этих объектов и присвоить его переменной типа X , то можем заключить, что T является X .
Более подробно о Wildcard Capture можно прочитать здесь и здесь.
Вывод
Если необходимо читать из контейнера, то используйте wildcard с верхней границей » ? extends «. Если необходимо писать в контейнер, то используйте wildcard с нижней границей » ? super «. Не используйте wildcard, если нужно производить и запись, и чтение.
Не используйте Raw типы! Если аргумент типа не определен, то используйте wildcard <?> .
Переменные типа
Когда мы записываем при объявлении класса или метода идентификатор в угловых скобках, например <T> или <E> , то создаем переменную типа. Переменная типа — это неквалифицированный идентификатор, который можно использовать в качестве типа в теле класса или метода. Переменная типа может быть ограничена сверху.
В этом примере выражение T extends Comparable<T> определяет T (переменную типа), ограниченную сверху типом Comparable<T> . В отличие от wildcard, переменные типа могут быть ограничены только сверху (только extends ). Нельзя записать super . Кроме того, в этом примере T зависит от самого себя, это называется recursive bound — рекурсивная граница.
Вот еще пример из класса Enum:
Здесь класс Enum параметризован типом E, который является подтипом от Enum<E> .
Multiple bounds (множественные ограничения)
Multiple Bounds – множественные ограничения. Записывается через символ » & «, то есть мы говорим, что тип, представленный переменной типа T , должен быть ограничен сверху классом Object и интерфейсом Comparable .
Запись Object & Comparable<? super T> образует тип пересечения Multiple Bounds . Первое ограничение — в данном случае Object — используется для erasure , процесса затирания типов. Его выполняет компилятор на этапе компиляции.
Вывод
Переменная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure).
Type Erasure
Type Erasure представляет собой отображение типов (возможно, включая параметризованные типы и переменные типа) на типы, которые никогда не являются параметризованными типами или переменными типами. Мы записываем затирание типа T как |T| .
- Затиранием параметризованного типа G<T1. Tn> является |G|
- Затиранием вложенного типа T.C является |T|.C
- Затиранием типа массива T[] является |T|[]
- Затиранием переменной типа является затирание ее левой границы
- Затиранием любого иного типа является сам этот тип
- добавляет приведение типов для обеспечения type safety, если это необходимо
- генерирует Bridge методы для сохранения полиморфизма
| T (Тип) | |T| (Затирание типа) |
| List< Integer>, List< String>, List< List< String>> | List |
| List< Integer>[] | List[] |
| List | List |
| int | int |
| Integer | Integer |
| <T extends Comparable<T>> | Comparable |
| <T extends Object & Comparable<? super T>> | Object |
| LinkedCollection<E>.Node | LinkedCollection.Node |
Эта таблица показывает, во что превращаются разные типы в процессе затирания, Type Erasure.
На скриншоте ниже два примера программы: 
Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему?
В Java два разных метода не могут иметь одну и ту же сигнатуру. В процессе Type Erasure компилятор добавит bridge-метод public int compareTo(Object o) . Но в классе уже содержится метод с такой сигнатурой, что и вызовет ошибку во время компиляции.
Скомпилируем класс Name, удалив метод compareTo(Object o) , и посмотрим на получившийся байткод с помощью javap:
Видим, что класс содержит метод int compareTo(java.lang.Object) , хотя мы его удалили из исходного кода. Это и есть bridge метод, который добавил компилятор.
Reifiable типы
- Примитивные типы (int, long, boolean)
- Непараметризованные (необобщенные) типы (String, Integer)
- Параметризованные типы, параметры которых представлены в виде unbounded wildcard (неограниченных символов подстановки) (List<?>, Collection<?>)
- Raw (несформированные) типы (List, ArrayList)
- Массивы, компоненты которых — Reifiable типы (int[], Number[], List<?>[], List[)
Почему информация об одних типах доступна, а о других нет? Дело в том, что из-за процесса затирания типов компилятором информация о некоторых типах может быть потеряна. Если она потерялась, то такой тип будет уже не reifiable. То есть она во время выполнения недоступна. Если доступна – соответственно, reifiable.
Решение не делать все обобщенные типы доступными во время выполнения — это одно из наиболее важных и противоречивых проектных решений в системе типов Java. Так сделали, в первую очередь, для совместимости с существующим кодом. За миграционную совместимость пришлось платить — полная доступность системы обобщенных типов во время выполнения невозможна.
- Переменная типа (T)
- Параметризованный тип с указанным типом параметра (List<Number>ArrayList<String>, List<List<String>>)
- Параметризованный тип с указанной верхней или нижней границей (List<? extends Number>, Comparable<? super String>). Но здесь стоит оговориться: List<? extends Object> — не reifiable, а List<?> — reifiable
И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception?
Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы.
Unchecked Warnings
Компиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем.
Heap Pollution
Как мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием » Heap Pollution » (загрязнение кучи). Вот пример:
В строке (1) компилятор предупреждает об «Unchecked assignment».
Нужно привести и другой пример «загрязнения кучи» — когда у нас используются параметризованные объекты. Кусок кода ниже наглядно показывает, что недопустимо использовать параметризованные типы в качестве аргументов метода с использованием Varargs . В данном случае параметр метода m – это List<String>… , т.е. фактически, массив элементов типа List<String> . Учитывая правило отображения типов при затирании, тип stringLists превращается в массив raw списков ( List[] ), т.е. можно выполнить присваивание Object[] array = stringLists; и после записать в array объект, отличный от списка строк (1), что вызовет ClassCastException в строке (2).
Рассмотрим еще один пример:
Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число.
Reflection
Хотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection.
- Все reifiable доступны через механизм Reflection
- Информация о типе полей класса, параметров методов и возвращаемых ими значений доступна через Reflection.
С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код:
Переменная ints имеет тип List<Integer> и она содержит ссылку на объект типа ArrayList< Integer> . Тогда ints.getClass() вернёт объект типа Class<ArrayLis> , так как List<Integer> затирается в List . Объект типа Class<ArrayList> можно присвоить переменной k типа Class<? extends List> , согласно ковариантности символов подстановки? extends . А ArrayList.class возвращает объект типа Class<ArrayList> .
Вывод
Если информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable.
Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы.
Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса.
Type Inference
Термин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода:
С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList :
Компилятор выведет тип ArrayList из контекста – List<Integer> . Этот процесс и называется type inference .
- Приведение (reduction)
- Объединение (incorporation)
- Разрешение (resolution)
Предположим у нас есть вот такой класс, который описывает связный список:
Результат обобщенного метода List.nil() может быть выведен из правой части:
Механизм выбора типа компилятором показывает, что аргумент типа для вызова List.nil() действительно String — это работает в JDK 7, все хорошо.
Выглядит разумно, что компилятор также должен иметь возможность вывести тип, когда результат такого вызова обобщенного метода передается другому методу в качестве аргумента, например:
В JDK 7 мы получили бы compile-time error. А в JDK 8 скомпилируется. Это и есть первая часть JEP-101, его первая цель — вывод типа в позиции аргумента. Единственная возможность осуществить это в версиях до JDK 8 — использовать явный аргумент типа при вызове обобщенного метода:
Вторая часть JEP-101 говорит о том, что неплохо бы выводить тип в цепочке вызовов обобщенных методов, например:
Но данная задача не решена до сих пор, и вряд ли в ближайшее время появится такая функция. Возможно, в будущих версиях JDK необходимость в этом исчезнет, но пока нужно указывать аргументы вручную:
После выхода JEP 101 на StackOverflow появилось множество вопросов по теме. Программисты спрашивают, почему код, который выполнялся на 7-й версии, на 8-й выполняется иначе – или вообще не компилируется? Вот пример такого кода:
Посмотрим на байт-код после компиляции на JDK1.8:
Инструкция под номером 0 выполняет вызов метода g:()Ljava/lang/Object; Метод возвращает java.lang.Object . Далее, инструкция 3 производит приведение типа («кастинг») объекта, полученного на предыдущем шаге к типу массива java.lang.String , и инструкция 6 выполняет метод m:([Ljava/lang/String;) , что и напечатает в консоли «two».
А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7:
Мы видим, что здесь нет инструкции checkcast , которую добавила Java 8, так что вызовется метод m:(Ljava/lang/Object;) , а в консоли напечатается «one». Checkcast – результат нового выведения типа, который был усовершенствован в Java 8.
Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить.
Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную:
Заключение
На этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы:
Полезности Java: что должен знать программист о дженериках

Java – перспективный и относительно простой в освоении язык программирования. Его может освоить даже новичок в соответствующей сфере. Несмотря на то, что Джава – это «способ общения» с программным обеспечением и «железом» устройства типа ООП, работать с ним не составляет никакого труда.
Данный вариант идеально подходит для:
- игрового софта;
- «серьезных» программ и утилит;
- веб-программирования.
Последнее направление является наиболее популярным. Web-programming – основная стезя Джава-семейства. Тип языка, при помощи которого можно писать браузерные расширения. Обладает разнообразными функциями и возможностями. Некоторые элементы семейства требуют отдельного внимания. Пример – дженерики (generic). О них мы расскажем далее в статье.
Преимущества Джавы: что учесть новичку
Но сначала стоит обратить внимание на некоторые особенности выбранного типа «способа общения» с программным обеспечением. Джава обладает существенными преимуществами перед остальными вариантами:
- простой и понятный синтаксис;
- быстрое осваивание «с нуля»;
- не требует никаких навыков программирования – подойдет даже тем, кто далек от информационных технологий;
- наличие ООП;
- дополнительные библиотеки;
- собственный движок, а также наличие множества платформ для создания игрового контента;
- относительно небольшие исходные коды получаемых приложений;
- свободное распространение.
Пользователи, решившие работать с соответствующим типом language, будут сталкиваться с принципом «меньше писать, больше выполнять». Посредством циклов и других функций удается значительно уменьшить размер исходной кодификации. Проверять ее работоспособность тоже будет не слишком трудно.
Терминологический вопрос – основные понятия
Перед тем, как рассмотрим дженерики в Джаве, необходимо разобраться с некоторыми понятиями в программировании. Соответствующие данные пригодятся как новичкам, так и тем, кто уже пытался создавать собственные утилиты.
Запомнить рекомендуется следующую информацию:
- алгоритм – правила и инструкции, их сочетания, предназначенные для решения поставленной задачи;
- API (интерфейс программирования) – правила, процедуры, протоколы, необходимые при создании программного софта;
- аргументы – значения, передаваемые в функции и команды;
- символы – минимальные единицы отображения данных, равные одной букве или символу;
- класс – шаблон, описывающий тот или иной объект в программировании, набор элементов с общими свойствами;
- константы – тип значений, не изменяемых в процессе выполнения программной кодификации;
- типы данных – классификация информации того или иного характера;
- массивы – группы и списки (пример — list integer) схожих типов значений информации, подлежащие группировке;
- декларация – оператор, описывающий переменные, функции и иные идентификаторы;
- фреймворк – готовый пример кода, используемый при создании собственных приложений;
- цикл – последовательность инструкций, отвечающих за выполнение одних и тех же манипуляций несколько раз;
- операнд – объекты, которыми можно манипулировать;
- оператор – элемент кода, управляющий теми или иными операндами;
- переменная – элементарное хранилище информации при создании кодификаций;
- методы – функции и процедуры, которые относятся к тому или иному типу/классу объектов, своеобразные действия, которые «умеет» выполнять элемент кода.
Отдельное внимание необходимо уделить понятию дженерика. С его использованием программирование на Джаве стало значительно проще. И разобраться с соответствующим элементом при грамотном подходе не составит никакого труда. Для лучшего понимания рекомендуется обладать хотя бы первоначальными навыками программирования.
Дженерик – что это такое
При использовании string и других объектов в процессе коддинга, разработчики задействуют всевозможные классы, методы и типы информации. Для каждого варианта разрабатывались собственные алгоритмы и решения.
Java, начиная с 1995 года, активно развивался, дорабатывался, совершенствовался. Так появились дженерики. Впервые о них услышали в Java 5. С тех пор подобные элементы весьма активно применяются на практике.
Generic – понятие обширное. Обозначает языковые возможности (функции), позволяющие применять на практике универсальные методы и типы. Своеобразное обобщение.
Общие типы отличаются от обычных:
- до появления дженериков программеры использовали коллекции для хранения всех типов объектов;
- начиная с Java 5, за счет generics объекты делятся и хранятся «обособлено» друг от друга.
Иногда тип данных ввода не выступает в качестве фиксированного. Указываемыми единицами информации могут служить string, числа (с плавающими запятыми в том числе), буквенные записи. Для ввода переменной правильного (нужного) типа требуется внедрить предварительные проверки. Если же воспользоваться дженериками, соответствующая операция проведется автоматически в процессе компиляции. Тип данных будет установлен по умолчанию. Используется эта «функция» весьма активно, особенно продвинутыми разработчиками.
Кратко о главном в дженериках: наглядное пособие
Generic носит название родовых типов. Соответствующие элементы позволяют безопасно пользоваться классами коллекций. Лучше всего изучать их на наглядных образцах кодификаций. Вот пример кода без дженериков:

Здесь происходит следующее:
- компиляция при запуске утилиты осуществляется нормально;
- ClassCastException будет брошен в процессе исполнения кода;
- один из объектов выступает как integer, а должен являться string.
В Java 5 и более поздних версиях стал актуален следующий код:

В процессе создания списка (list string, new arraylist) указано, что тип элементов, задействованных в нем – это string. Если попытаться добавить иные варианты, при компиляции пользователь увидит на экране сообщение об ошибке.
В цикле for приведение типов не используется. О ClassCastException можно не беспокоиться. Программа заработает исправно.
Способы применения
Что собой представляют дженерики в Джаве, понятно. Но для того, чтобы лучше в них разбираться, требуется обозначить сферы применения оных. Существуют различные варианты развития событий. Наглядные примеры помогут лучше усвоить соответствующий материал даже новичкам.
На данный момент generic может использоваться следующими способами:
- типовыми классами;
- интерфейсами;
- методами;
- конструкторами.
Также имеют место при наследовании. Далее каждый вариант будет рассмотрен более подробно. Приведенные примеры – это лишь шаблоны, на которые рекомендуется опираться при самостоятельном изучении выбранного направления.
Типовые классы
Класс будет называться generic, если он отвечает за объявления одной или нескольких переменных типа (string, int и так далее). Соответствующие виды принято называть параметрами типа класса Джава. Чтобы лучше понимать их, стоит рассмотреть пример.
Здесь происходит следующее:
- создается класс со свойством X;
- тип свойства – это объект;
- после инициализации класс применяется только этим конкретным типом.
Так, если хочется, чтобы экземпляр класса имел значение типа string, программисту предстоит установить и получить единственный string. В предложенном примере соответствующих ограничений нет. Связано это с тем, что произошло объявление типа свойства для объекта. Разработчики способны устанавливать любые элементы и ждать любой «вид» возвращаемого значения при активации метода get.

Приведенный выше пример позволяет применять обозначенное ранее ограничение. При подобных обстоятельствах разработчик может быть уверен в том, что класс задействуют максимально грамотно и правильно.
Вот самый простой пример Genericclass:
Подобный вариант актуален и для интерфейсов.
Интерфейсы и классы – как используют generic
Для того, чтобы облегчить коддинг, можно использовать при описании интерфейсов и classes generics. Для этого применяются угловые скобочки (<>). Они помогают указывать «разновидность» параметра задействованного объекта.
Вот вариант создания class без generics:
Здесь при применении class не нужно приводить «вид» объекта. В случае с generics кодификация получит такую интерпретацию:
- за classcastexception в main() беспокоиться не нужно – «мейн» выступает методов GenericsType;
- если отказаться от соответствующей прописи, компилятор сообщит об этом.
В последнем случае имеет место следующий расклад:
- type не прописывается при создании экземпляра class;
- автоматически type получает «значение» object;
- string можно использовать в качестве objects, как и иные элементы new integer.
В конечном итоге придется прибегнуть к приведению types. Им выступит OldSchool из предложенного примера.
Только интерфейс
Интерфейс Comparable – хороший пример применения рассматриваемого элемента в программировании. Выглядит код так:

На основе этого варианта можно освоить принципы функционирования generics в Джаве относительно интерфейсов, без дополнительных составляющих. Это – база, помогающая применять рассматриваемый элемент в classes и interface. Еще и при помощи задействования нескольких параметров. Пример – интерфейс map. Подойдет string вида:
New hashMap<String, list<string>>().
Методы-конструкторы
Еще один вариант развития событий актуален, когда нет необходимости в параметризации всего класса. При подобных обстоятельствах рассмотренные ранее примеры теряют свою актуальность. Можно значительно упростить процесс написания исходного кода за счет применения generics исключительно в методах или конструкторах.
Вот пример, который поможет разобраться, как реализовывается соответствующий подход:

Как можно заметить, приведенная кодификация предусматривает два способа применения. Один – обычный, другой – максимально простой. Остановиться программист может на том, который кажется ему наиболее комфортным, удобным.
Внимание: не всегда упрощенный вариант подходит при написании контента. В основном он встречается в элементарных приложениях. Для сложных проектов рекомендуется пользоваться «обычным» способом представления generics в методах/конструкторах.
Вопрос наследования
Джава – современный, удобный, продвинутый язык, оснащенный огромным функционалом. Здесь дозволено назначать переменные других переменных. Такой расклад возможен, когда первый элемент выступает в качестве подкласса второго.
Для реализации подобных приемов используется наследование. Код имеет примерно следующую структуру:
Дженерики в Java – это удобно и практично. Но разработчикам требуется выяснить, как происходит понимание программного кода и его унификация. Соответствующий спектр знаний поможет отслеживать ошибки, если они имеются, а также избегать оные.
Соглашение об именовании – в помощь разработчику
Generics имеют собственное соглашение об именовании. Это – некий «свод правил», который применяется при внедрении рассматриваемого элемента в кодификацию. Выражаются преимущественно прописными буквами, за счет чего удается без особых затруднений отделить оные от переменных в Джаве.
Соглашение об именовании весьма обширное. Там нет «заумных» фраз типа «extends number, int I, public static t» и так далее. В основном дело предстоит иметь с «самостоятельными буквами». Чаще всего на практике встречаются следующие варианты:
- K – ключ (имеет место в map);
- T – тип;
- E – элемент (имеет широкое распространение в Java Collection Framework);
- B – значение (тоже встречается в map);
- S, U, V и так далее – 2-ой, 3-ий, 4-ый…тип.
Больше информации можно узнать из сопутствующей документации. У Java с ней проблем нет, как и у generic. Также помощи всегда можно попросить у продвинутых разработчиков – комьюнити Джавы весьма лояльно и дружелюбно к новичкам.
Курсы – лучший подход к изучению
Что такое generic в Java, понятно. Это – метод обобщения, который упрощает коддинг. Иногда «с нуля» освоить его бывает проблематично. В таком случае целесообразно посетить специализированные компьютерные курсы.
Опытные специалисты и современные программисты расскажут, что такое public static void, interfaces, generic, t extends и не только. Материал подается в понятной и удобной форме. Возможно дистанционное обучение. Можно выбрать узкую специализацию или рассмотреть выбранное направление всесторонне. Имеются курсы как для новичков, так и для более опытных программеров.
Дженерики Java – это не так трудно, если грамотно подойти к изучению темы. Курсы доступны всем желающим. В конце обучения выдаются сертификаты установленного образца.
Зачем Go нужны дженерики
Это статья о том, как введение дженериков может изменить Go и почему это будет целесообразным шагом. Здесь также будут затронуты изменения, которые придётся внести в язык для выполнения задуманного.
Go выпустили в свет 10 ноября 2009 года, и меньше чем через 24 часа появился первый комментарий про использование дженериков. В комментарии упоминались исключения, которые и были добавлены в начале 2010 в виде panic и recover .
По отзывам пользователей, именно отсутствие дженериков — одна из главных проблем Go.
Зачем нужны дженерики?
Что же такое «добавление дженериков» и зачем это вообще нужно?
Программирование с использованием дженериков позволяет представить функции и структуры данных в обобщённой форме, с факторизацией типов.
В качестве простейшего примера предположим, что нам нужно перераспределить в обратном порядке элементы среза. Это, разумеется, не та задача, с которой часто сталкиваются программисты, но и ничего из ряда вон выходящего.
Предположим, что это целочисленный массив.
Простейшая функция, но и в ней можно обнаружить ошибку.
При определении переменной, содержащей индекс последнего элемента, надо уменьшить размер среза на 1.
Теперь создадим функцию для осуществления такой же операции с массивом строк.
Сравнив функции ReverseInts и ReverseStrings , вы увидите, что они различаются только типом параметра.
Тем, кто начинает изучать Go, может показаться странным, что невозможно написать простую функцию Reverse , которая будет работать с данными любого типа. Ведь в большинстве других языков такая возможность есть.
В языках с динамической типизацией, таких как Python или JavaScript, функцию можно написать, не обращая внимания на определение типа элементов. На Go такой способ не сработает, поскольку это язык со статической типизацией и требует точного указания типа среза и типа его элементов. Большая часть других языков со статической типизацией, таких как C++, Java, Rust или Swift, поддерживает дженерики, чтобы устранить это затруднение.
Как Go обходится без дженериков сейчас
Интерфейсы
Создать функцию, которая может обрабатывать различные типы данных в Go можно, используя интерфейсный тип. Для этого требуется определить методы для тех типов срезов, которые вы намереваетесь передавать функции. Именно так работает функция sort.Sort из стандартной библиотеки.
Другими словами, отсутствие дженериков в Go можно компенсировать с помощью интерфейсов. Они помогают выделить общие аспекты различных типов и выразить их в качестве методов. Таким образом мы можем создавать функции, которые будут обрабатывать любые типы, поддерживающие эти методы.
Но при таком подходе приходится писать методы для интерфейсов вручную. Довольно неудобно определять отдельный тип с парой методов просто для того, чтобы перераспределить элементы среза. А поскольку методы для каждого типа будут идентичны, мы не избавились от дублирующегося кода, просто передвинули его. И хотя интерфейсы позволяют реализовать некоторые элементы дженериков, полной функциональности они не дают.
Методы по умолчанию
Ещё один способ, при котором нам не придётся самостоятельно писать методы — определение в самом языке методов по умолчанию для некоторых типов. В настоящее время такой подход не поддерживается в Go, но, к примеру, в языке можно было бы определить для каждого массива метод Index, возвращающий элемент. Однако, чтобы использовать этот способ на практике, метод должен возвращать пустой интерфейсный тип, а в таком случае мы потеряем все преимущества статического типирования. Более того, мы не смогли бы создать функцию, которая принимает два среза с элементами одного типа или map с элементами определённого типа и возвращает срез. Статическая типизация облегчает Go создание больших программ. И не хотелось бы терять это преимущество ради плюсов, которые дают дженерики.
Пакет reflect
Ещё один вариант — написать функцию Reverse с помощью пакета reflect, но это настолько неудобно и непрактично, что почти никто так не делает. Кроме того, этот подход требует чёткого определения типов и не допускает их статической проверки.
Генераторы кода
Можно также написать генератор кода, который принимает тип и генерирует функцию Reverse для срезов этого типа. Существует несколько подходящих генераторов кода. Но это добавляет лишний шаг в любой пакет, включающий функцию Reverse , что в свою очередь усложняет сборку, поскольку требует компиляцию всех копий. Также исправление бага в источнике требует перегенерации всех инстансов, некоторые из которых могут использоваться даже в других проектах.
Все описанные подходы столь неудобны, что в большинстве случаев при необходимости перераспределить элементы среза просто пишут отдельные функции для каждого типа, который планируют использовать. Затем для каждой функции нужно написать и провести тесты, чтобы отловить все ошибки вроде той, что в первом примере.
Не слишком ли много лишней работы ради функций, которые отличаются только типом элемента? Должен быть способ намного лучше!
Для статически типизированных языков этот способ — дженерики. Как было сказано в самом начале, использование дженериков позволяет исключать типы, а это именно то, что нам нужно.
Что могут привнести в Go дженерики
Первая и самая главная вещь, которую мы хотим получить от дженериков в Go — возможность писать такие функции, как Reverse , не задумываясь о типах элементов среза. Мы хотим вывести тип элемента за скобки. Тогда мы сможем написать одну функцию, один набор тестов, поместить их в пользовательский пакет и вызывать их, когда захотим.
Более того, раз уж мы работаем в окружении открытого исходного кода, кто-то другой может написать Reverse , а мы просто используем эту функцию.
Необходимо отметить, что под «дженериками» можно подразумевать кучу разных вещей. Например, в C++ этим термином обозначают шаблоны, которые обладают гораздо большим спектром возможностей. Однако в данной статье под дженериками подразумевается ровно то, что описано выше.
Мы сосредоточились на функции Reverse , однако с помощью дженериков можно сделать гораздо больше:
- найти самый маленький/самый большой элемент среза;
- найти среднее/стандартное отклонение среза;
- рассчитать слияние/пересечение в map ;
- найти кратчайший путь на графе с узлами/рёбрами;
- применить функцию трансформации к срезу/ map ‘у, возвращая новый срез/ map .
В большинстве других языков это легко реализуемо, более того, сам этот список написан с оглядкой на стандартную библиотеку шаблонов C++.
А вот список тех возможностей, которые можно реализовать именно в Go благодаря усиленной поддержке многопоточности:
- читать канал без задержек;
- выполнять слияние двух каналов в один;
- вызывать список параллельно выполняющихся функций, возвращая срез в качестве результата;
- вызывать список функций, используя Context, возвращая результат первой функции чтобы завершить, отменить или очистить дополнительные горутины.
Эти функции можно довольно часто встретить, при этом для каждого типа используется отдельная функция. Написать их на Go не так сложно. Но куда лучше было бы получить возможность использовать единожды написанную функцию для любых типов.
Уточним, что это всего лишь примеры, дженерики позволят проще и безопаснее создать код для куда большего количества функций.
Кроме того, как упоминалось ранее, применение дженериков затронет не только функции, но и структуры данных.
В Go встроены две основных структуры данных общего назначения: срезы и map ‘ы. Эти структуры могут содержать значения любого типа данных, со статической проверкой типа для хранящихся и запрашиваемых значений. Значения данных в этих структурах хранятся без опосредования через интерфейсы. Таким образом в []int хранятся именно целые числа, а не целые числа, преобразованные через интерфейс.
Срезы и карты — наиболее часто встречающиеся структуры данных общего назначения, но не единственные. Вот примеры других образований:
- наборы;
- самобалансирующиеся деревья с эффективной вставкой элементов и прохождением в порядке сортировки;
- мультикарты, допускающие несколько значений ключа;
- параллельные хеш-карты, поддерживающие параллельные вставки и вызовы без блокировок.
Получив возможность создавать обобщённые типы, мы сможем определять новые структуры данных, сохраняя преимущества срезов и map ‘ов: компилятор сможет проверять тип содержащихся в них значений, а сами значения можно будет хранить без преобразования с помощью интерфейсов. И к этим структурам данных можно будет применить упомянутые раньше алгоритмы.
Все эти примеры будут реализованы подобно Reverse : обобщённые функции и структуры данных единожды написанные, размещённые в пакетах и вызываемые в случае необходимости. Они должны работать как срезы и карты, то есть хранить не тип пустого интерфейса, а вполне определённые типы, которые можно проверить в процессе компиляции.
Итак, теперь вы знаете, какие преимущества получит Go от применения дженериков. Они могут предоставить нам отличную возможность унифицировать код, делиться им, упростят создание программ.
Чем придётся пожертвовать
Справедливости ради стоит сказать, что за каждое изменение в языке нужно заплатить определённую цену. И добавление дженериков в Go определённо усложнит язык. Как и в случае с любыми другими изменениями, нужно подумать о том, как максимизировать выгоду и минимизировать потери.
При разработке Go создатели ставили целью уменьшить сложность языка с помощью независимых, свободно сочетаемых особенностей. С этой целью каждая отдельная функциональность максимально упрощалась, а преимущества достигались с помощью их сочетаний. К дженерикам нужен тот же подход.
Ниже приведён список правил, которых стоит придерживаться при модификации языка:
- Минимизация новых концепций: в язык нужно вносить только самый необходимый минимум изменений. Это относится к синтаксису, новым ключевым словам и другим аспектам;
- Затруднения должен разрешать создатель дженерик-кода, а не пользователь: насколько это возможно, разрешение сложных моментов следует оставить за разработчиком дженерик-пакетов. Пользователь, вызывающий эти пакеты, не должен заботиться о реализации дженериков. Это значит, что вызов функций и обработку ошибок нужно сделать интуитивно понятными. Отладка вызовов в дженерик-коде должна остаться максимально лёгкой.
- Создатель дженерик-кода и пользователь должны иметь возможность работать независимо друг от друга: это значит, что каждый из них может не задумываться о том, что делает другой, также как и при разработке пакетов обычных функций. Выглядит очевидным, но в других языках не всегда реализовано.
- Быстрая сборка, быстрое выполнение: это особенность языка Go, и её надо постараться сохранить. Дженерики позволяют сохранить компромисс между скоростью сборки и исполнения.
- Сохранение простоты и ясности Go: сейчас это простой язык, программы на котором легко читать и понимать. Создатели языка большое внимание уделяют тому, как добавить в Go дженерики, сохранив при этом ясность языка. Новая концепция должна вписаться в Go как очевидная его часть, минимально изменив структуру.
К любым попыткам реализовать дженерики в Go нужно применять эти правила. Основная идея состоит в том, чтобы привнести в язык максимум улучшений, при этом сохранив его идентичность.
Наброски изменений
К счастью, есть способ сделать это. Во второй половине статьи мы перейдём к тому, как можно реализовать дженерики в Go.
На Gophercon 2019 Ян Лэнс Тэйлор и Роберт Гризмер обнародовали наброски предполагаемых изменений. В этом документе содержится полная информация, здесь же мы осветим основные аспекты.
Вот как будет выглядеть функция Reverse с применением дженериков:
Как видите, тело функции осталось то же, изменилась лишь сигнатура.
Тип элементов среза выделен. Теперь он называется Element и является параметром типа. Теперь это не часть параметра среза.
Для вызова функции с параметром типа в базовом случае требуется передать аргумент типа так же, как мы передаём любой другой аргумент.
В данном примере это (int) .
К счастью, в большинстве случаев компилятор может самостоятельно вычислить аргумент типа, основываясь на типах стандартных аргументов, поэтому аргумент типа можно вообще опустить.
Вызов функции с дженериком будет выглядеть так же, как вызов любой другой.
Другими словами, хотя функция с дженериком Reverse чуть сложнее, чем ReverseInts и ReverseStrings , эта сложность остаётся на разрешение создателя функции, а не того, кто её вызывает.
Контракты
Поскольку Go — статически типизированный язык, требуется подробнее рассмотреть тип параметра типа. Этот мета-тип сообщает компилятору, какие разновидности типов аргументов можно передавать при вызове функции с дженериками, а также какие операции функция может производить со значениями таких типов.
Функция Reverse может работать со срезами любого типа. Единственная операция, которую эта функция осуществляет с типом Element — присвоение, а эту операцию на Go можно провести с любым типом. Для подобных распространённых функций с дженериками нам не нужно определять каких-то специфических условий для параметров.
Рассмотрим другую функцию.
В настоящий момент пакеты для обработки байт и строк из стандартной библиотеки содержат функцию IndexByte . Она возвращает индекс b в последовательности s , где s либо string , либо []byte . Мы можем использовать указанную выше функцию с дженериками, заменив ею соответствующие функции в указанных пакетах. Вероятно, при обновлении языка этого не сделают, но пример полезный.
Здесь нам нужно знать, что параметр типа T работает как string или []byte . Мы можем вызвать len для этого типа, индексировать, сравнить результат индексирования со значением типа byte.
Чтобы скомпилировать эту функцию, параметр типа T сам требует тип. Это мета-тип, но поскольку нам иногда нужно описать множество относящихся типов и речь идёт об связях функции со способами её вызова, мы будем называть тип T контрактом. Здесь контракт назван Sequence . Он появляется после списка параметров типа.
Вот как для этого примера определяется контракт Sequence :
Поскольку пример довольно простой, то и определение контракта простое: параметр типа T может быть либо string , либо []byte . А contract может быть новым ключевым словом или особым идентификатором, воспринимаемым при рассмотрении пакета. Подробности отражены в набросках разработки.
Если вы вспомните, о чём говорилось на Gophercon’е 2018, то увидите, что способ определения контрактов сильно упростился. Разработчики учли отзывы участников конвента, которым контракты образца 2018 года показались излишне сложными. Новые контракты куда проще писать, читать и понимать.
Контракты позволят определить подлежащие типы параметра типов, а также список методов параметра типов. Кроме того, они помогут описать отношения разных параметров типов.
Контракты с методами
Вот ещё один простой пример функции, которая использует метод String , возвращая []string строковых представлений всех элементов в s .
Всё довольно просто: пройти через срез, вызвать метод String для каждого элемента и вернуть срез строк в качестве результата.
В этой функции необходимо, чтобы тип элемента включал метод String . Для этого служит контракт Stringer.
Этот контракт просто говорит, что T должен включать метод String .
Вы могли заметить, что указанный выше контракт похож на интерфейс fmt.Stringer . Поэтому стоит отметить, что аргумент функции ToStrings не является срезом fmt.Stringer . Это срез типа определённого элемента, где тип элемента реализует fmt.Stringer . Представление среза типа элемента и среза fmt.Stringer обычно отличаются, и Go не поддерживает прямую конверсию между ними. Поэтому несмотря на существование fmt.Stringer , создание функции ToStrings имеет смысл.
Контракты с множественными типами
Вот пример контракта со множеством параметров типа:
Здесь мы описываем граф, построенный из узлов и рёбер. Нам не требуется определённая структура данных для графа. Вместо этого мы говорим, что тип Node должен включать метод Edges , который возвращает список рёбер, соединённых с Node . А тип Edge должен включать метод Nodes , возвращающий два Node , соединённых этой Edge .
Реализация функции опущена, но показана сигнатура функции New , которая возвращает Graph , и сигнатура метода ShortestPath для Graph .
Важный вывод тут в том, что контракты работают не только с единичными типами, но и описывают отношения нескольких типов.
Упорядоченные типы
На удивление часто программисты сетуют на отсутствие в Go функций Min и Max . Причина этого в том, что такие функции должны работать с любым упорядоченным типом, то есть использовать дженерики.
Хотя функцию Min легко написать самостоятельно, использование дженериков позволит разработчикам языка просто внести её в стандартную библиотеку. Вот как это может выглядеть:
Контракт Ordered говорит, что тип T должен быть упорядоченным типом, что означает поддержку таких операторов, как «меньше чем», «больше чем» и т.д.
Контракт Ordered — просто список всех упорядоченных типов, вшитых в язык. Этот контракт принимает любой из перечисленных типов, а также любой пользовательский тип, основанный на одном из перечисленных. Фактически это любой тип, к которому можно применить оператор «меньше чем».
Выходит, что гораздо проще просто перечислить все типы, поддерживающие оператор «меньше чем», чем изобретать новый параметр, подходящий для всех операторов. В конце концов в Go только вшитые типы поддерживают операторы.
Такой же подход можно использовать для любого оператора. Более того, можно написать контракт для любой функции с дженериками, работающей со встроенными типами. Это позволит создателю функции явно объявить, с какими типами сможет работать его код, а пользователю функции — определить, подходит ли она для его типов данных.
На практике этот контракт вероятнее всего попадёт в будущем в стандартную библиотеку, так же как и функция Min . Здесь мы просто ссылаемся на контракт Ordered , определённый в контрактах пакета.
Дженерик структуры данных
Теперь давайте рассмотрим простую структуру данных на основе дженериков — двоичное дерево. В данном примере мы реализуем дерево с функцией сравнения, поэтому требований по типу элементов не будет.
Вот как создать новое бинарное дерево. Функция сравнения передаётся функции New .
Неэкспортированный метод возвращает указатель либо на слот, содержащий v, либо на то место в дереве, где она должна быть.
Детали тут не слишком существенны, это лишь простой пример для демонстрации того, как можно создать структуру данных с использованием дженериков.
Вот код, предназначенный для проверки того, содержит ли дерево значение:
А этот код добавляет новое значение:
Обратите внимание на тип аргумента E в аргументе node . Вот как выглядит код структуры данных с использованием дженериков. Как видите, мало чем отличается от обычного кода на Go, просто местами появляются типы в качестве аргументов.
Использовать дерево довольно просто.
Так и должно быть. Разрабатывать структуры данных с дженериками чуть сложнее, поскольку вам зачастую нужно чётко определить аргументы с типами, однако использование этого кода обычно не сложнее, чем работа с традиционными структурами данных.
Дальнейшие шаги
В настоящее время создатели языка работают над реализацией дженериков, проверяя свои идеи на практике. Процесс идёт не так быстро, как они надеялись, однако эксперименты позволяют понять, какие программы можно будет создавать с помощью их разработок.
Роберт Гризмер подготовил раннюю версию изменений пакетов Go. С ней можно потестировать проверку типов в коде с использованием дженериков и контрактов. Версия неполная и постоянно дорабатывается, но с одним пакетом работает неплохо.
Разработчики хотели бы, чтобы люди больше экспериментировали с кодом, использующим дженерики. Естественно, сначала не всё будет работать идеально, поэтому разработчики ждут отзывов и больше заинтересованы в комментариях семантики, чем деталей синтаксиса.
Цель создателей Go — добавить в язык дженерики, не усложняя языка и сохраняя его идентичность.