Generics
Дженерики (generics) в языке программирования Java — это сущности, которые могут хранить в себе данные только определенного типа. Например, список элементов, в котором могут быть одни числа. Но не только: дженерик — обобщенный термин для разных структур.

Освойте профессию «Java-разработчик»
Можно представить дженерик как папку для бумаг, куда нельзя положить ничего, кроме документов определенного формата. Это удобно: помогает разделить разные данные и не допустить ситуаций, когда в сущность передается что-то не то.
Дженерик-сущности еще иногда называют параметризованными, общими или обобщенными. Такая сущность создается со специальным параметром. Параметр позволяет указать, с каким типом данных она будет работать. Отсюда и название.
В разных источниках можно услышать про «тип-дженерик», «класс-дженерик» или «метод-дженерик». Это нормально, ведь обобщение и параметризация касаются всех этих сущностей, а generics — общий термин.
Для чего нужны дженерики
С дженериками работают программисты на Java. Без этой возможности писать код, который работает только с определенным видом данных, было сложнее. Существовало два способа, и оба неоптимальные:
- указывать проверку типа вкоде. Например, получать данные — и сразу проверять, а если они не те, выдавать ошибку. Это помогло бы отсеять ненужные элементы. Но если бы класс понадобилось сделать более гибким, например, создать его вариацию для другого типа, его пришлось бы переписывать или копировать. Не получилось бы просто передать другой специальный параметр, чтобы тот же класс смог работать еще с каким-то типом;
- полагаться на разработчиков. Например, оставлять в коде комментарий «Этот класс работает только с числами». Слишком велик риск, что кто-то не заметит комментарий и передаст в объект класса не те данные. И хорошо, если ошибка будет заметна сразу, а не уже на этапе тестирования.
Поэтому появились дженерики: они решают эту проблему, делают написание кода проще, а защиту от ошибок надежнее.
Профессия / 14 месяцев
Java-разработчик
Освойте востребованный язык

Как работают дженерики
Чтобы вернее понять принцип работы, нужно представлять, как устроены сущности в Java. Есть классы — это как бы «чертежи» будущих сущностей, описывающие, что они делают. И есть объекты — экземпляры классов, непосредственно существующие и работающие. Класс — как схема машины, объект — как машина.
Когда разработчик создает дженерик-класс, он приписывает к нему параметр в треугольных скобках — метку. К примеру, так:
Теперь при создании объекта этого класса нужно будет указать на месте T название типа, с которым будет работать объект. Например, myClass для целых чисел или myClass для строк. Сам класс остается универсальным, то есть общим. А вот каждый его объект специфичен для своего типа.
С помощью дженериков можно создать один класс, а потом на основе него — несколько объектов этого класса для разных типов. Не понадобится дублировать код и усложнять программу. Поэтому дженерики лучше и удобнее, чем проверка типа прямо в коде — тогда для каждого типа данных понадобился бы свой класс.
Что такое raw types
В Java есть понятие raw types. Так называют дженерик-классы, из которых удалили параметр. То есть изначально класс описали как дженерик, но при создании объекта этого класса тип ему не передали. То есть что-то вроде myClass<> — тип не указан.
Дословно это название переводится как «сырые типы». Пользоваться ими сейчас в коммерческой разработке — чаще всего плохая практика. Но в мире все еще много старого кода, который написали до появления дженериков. Если такой код еще не успели переписать, в нем может быть очень много «сырых типов». Это надо учитывать, чтобы не возникало проблем с совместимостью.
Дженерики-классы и дженерики-методы
Выше мы говорили, что дженериками могут быть разные сущности. Разберемся подробнее:
- дженерик-классы (generic classes)— это классы, «схемы» объектов с параметром. При создании объекта ему передается тип, с которым он будет работать;
- дженерик-методы (generics methods)— это методы, работающие по такому же принципу. Метод — это функция внутри объекта, то, что он может делать. Методу тип передается при вызове, сразу перед аргументами. Так можно создавать более универсальные функции и применять одну и ту же логику к данным разного типа.
Кстати, дженериками могут быть и встроенные классы или методы, и те, которые разработчик пишет самостоятельно. Например, встроенный ArrayList — список-массив — работает как дженерик.

Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке
Что будет, если передать дженерику не тот тип
Если объекту класса-дженерика передать не тот тип, который указали при его объявлении, он выдаст ошибку. Например, если в ходе работы экземпляра myClass в нем попытаются сохранить дробное число или даже строку, программа не скомпилируется. Вместо этого разработчик увидит ошибку: неверный тип.
Эта ошибка отличается от тех, которые возникнут, если не пользоваться дженериками. По ней сразу ясно, из-за чего она возникла и как можно ее исправить. Кроме того, она появляется сразу. Поэтому код становится легче отлаживать.
А если отправить «не тот» тип объекту без дженерика, действия с ним выполнятся с ошибкой. Но по этой ошибке не всегда очевидно, чем она вызвана. Худший вариант — код успешно запустится, но сработает неправильно: так ошибку будет найти еще сложнее.
Особенности дженериков
У дженериков есть несколько особенностей, о которых стоит знать при работе с ними. Если не учитывать эти детали, программировать будет как минимум менее удобно. А как максимум можно допустить ошибку и не понять, куда она закралась.
Выведение типа. Эта особенность касается объявления экземпляра класса, то есть создания объекта. Полная запись создания будет выглядеть так:
myClass objectForIntegers = new myClass();
objectForIntegers — это название объекта, оно может быть любым. То, что находится после знака «равно», — непосредственно команда «создать новый экземпляр класса».
Но полная запись очень громоздкая. Поэтому современные компиляторы Java способны на выведение типа — автоматическую его подстановку в записи после первого упоминания. То есть конструкцию myClass понадобится написать только один раз.
Запись, в которой программист пользуется возможностью выведения типа, будет выглядеть так:
myClass objectForIntegers = new myClass<>();
Повторное упоминание типа опускается. Запись становится короче. Кажется, что это мелочь, но таких конструкций в коде могут быть десятки и писать полную запись всегда было бы не очень удобно.
Стирание типов. Важная деталь, которая касается работы дженериков, — они существуют только на этапе компиляции. В этом их суть: «не пропускать» данные ненужного типа в объект, а такие вещи определяет компилятор.
После компиляции код на Java превращается в байт-код. И на этом уровне никаких дженериков нет. myClass и myClass в байт-коде будут идентичны, просто с разными данными внутри.
Это называется стиранием типов. Суть в том, что внутри дженерик-класса нет информации о его параметре и после компиляции эти сведения просто исчезают. Так сделали, потому что дженерики появились в Java не сразу. Если бы информацию о параметре добавили в байт-код, это сломало бы совместимость с более старыми версиями.
О стирании типов важно помнить. Для запущенной программы в байт-коде дженериков не существует, и это может вызвать ошибки. Например, при сравнении myClass и myClass программа скажет, что они одинаковые. А иногда в объект в запущенном коде и вовсе получается передать данные другого типа.
«Дикие карты». Еще одна интересная и полезная особенность дженериков — так называемые wildcards, или «дикие карты». Это термин из спорта, означающий особое приглашение спортсмена на соревнование в обход правил. А в карточных играх так называют карты, которые можно играть вместо других, например джокера.
В основе wildcards в Java лежит такая же идея: изменить предустановленное поведение и сделать что-то в обход установленных рамок. Когда объявляется «дикая карта», в треугольных скобках вместо названия типа ставится вопросительный знак. Это означает, что сюда можно подставить любой тип.
Подставить wildcard можно не везде. Например, при создании класса это сделать не получится, а при объявлении объекта этого класса — получится. Чаще всего «дикую карту» используют при работе с переменными и с коллекциями.
Ограниченные «дикие карты». Кроме стандартной wildcard, существует еще несколько типов — ограниченные «дикие карты». С их помощью можно передать в объект данные не только конкретного типа, но и унаследованных от него — «потомков». Или же «предков» — типов, от которых был унаследован упомянутый.
Ограниченный wildcard описывается как вопросительный знак, за которым следует правило.
Есть два вида ограничений:
- upper bounding — ограничение сверху. За вопросительным знаком следует слово extends и название типа. В такой дженерик можно передавать названный тип и его потомков;
- lower bounding — ограничение снизу. Ситуация наоборот: за вопросительным знаком слово super и тип, а подставлять можно элементы этого типа и его предков.
Скорее всего, впервые столкнуться с дженериками придется еще в начале изучения Java, просто новичку не сразу понятно, что это такое. Со временем появляется понимание, как работает эта конструкция, и становится легче решать более сложные задачи.
Java-разработчик
Java уже 20 лет в мировом топе языков программирования. На нем создают сложные финансовые сервисы, стриминги и маркетплейсы. Освойте технологии, которые нужны для backend-разработки, за 14 месяцев.

Статьи по теме:
Введение в дженерики – обобщенные классы и функции
В программировании иногда приходится обрабатывать разные типы данных похожим образом. С другой стороны, в языках со статической типизацией, к которым относится 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) >
Дженерики Java
Дженерики (или обобщения) — это параметризованные типы.
Параметризованные типы позволяют объявлять классы, интерфейсы и методы, где тип данных, которыми они оперируют, указан в виде параметра. Используя дженерики, можно создать единственный класс, например, который будет автоматически работать с разными типами данных.
Классы, интерфейсы или методы, имеющие дело с параметризованными типами, называются параметризованными или обобщениями, параметризованными (обобщенными) классами или параметризованными (обобщёнными) методами.
Обобщения добавили в язык безопасность типов.
- Параметризованные классы
- Ограниченные типы
- Применение метасимвольных аргументов
- Параметризованные методы и конструкторы
- Параметризованные интерфейсы
- Иерархии параметризованных классов
- Использование оператора instanceof c параметризованными классами
- Ограничения присущие обобщениям
1. Параметризованные классы
Следующий пример демонстрирует использование параметризованного класса, который описывает матрицу:
public class Matrix < private T[] array; public Matrix(T[] array) < this.array = array.clone(); >public static void main(String[] args) < MatrixdoubleMatrix = new Matrix<>(new Double[2]); Matrix integerMatrix = new Matrix<>(new Integer[4]); Matrix byteMatrix = new Matrix<>(new Byte[7]); > >
В объявлении Matrix integerMatrix Integer является аргументом типа.
Java не создает разные версии класса Matrix или любого другого параметризованного класса. Имеется только одна версия класса Matrix , которая существует в прикладной программе.
Дженерики работают только с объектами! Следующий код является неправильным:
Gen strOb = new Gen (53); // Ошибка, нельзя использовать примитивные типы
Т обозначает имя параметра типа. Это имя используется в качестве заполнителя вместо которого в дальнейшем подставляется имя конкретного типа, передаваемого классу Matrix при создании объекта. Это означает, что обозначение Т применяется в классе Matrix всякий раз, когда требуется параметр типа. Всякий раз, когда объявляется параметр типа, он указывается в угловых скобках.
Обобщенные типы отличаются в зависимости от типов-аргументов. Следующий код не допустим:
doubleMatrix = integerMatrix; // Не верно!
Несмотря на то, что doubleMatrix и integerMatrix имеют тип Matrix , они являются ссылками на разные типы, потому что типы их параметров отличаются.
Обобщенный класс может быть объявлен с любым количеством параметров типа. Например:
public class TwoGen < private T obT; private V obV; public TwoGen(T obT, V obV) < this.obT = obT; this.obV = obV; >public void showTypes() < System.out.println("Тип T: " + obT.getClass().getName()); System.out.println("Тип V: " + obV.getClass().getName()); >public T getObT() < return obT; >public V getObV() < return obV; >> public class SimpleGen < public static void main(String[] args) < TwoGentwoGen = new TwoGen<>(88, "Generics"); twoGen.showTypes(); System.out.println("Значение T: " + twoGen.getObT()); System.out.println("Значение V: " + twoGen.getObV()); > >
2. Ограниченные типы
Указывая параметр типа, можно наложить ограничение сверху в виде верхней границы, где объявляется супер класс, от которого должны быть унаследованы все аргументы типов. С этой целью вместе с параметром указывается ключевое слово extends :
class Gen
Параметр типа Т может быть заменен только указанным супер классом или его подклассами.
Рассмотрим пример использования ограниченного типа:
public class Average < private T[] array; public Average(T[] array) < this.array = array; >public double average() < double sum = 0.0; for (T value : array) < sum += value.doubleValue(); >return sum / array.length; > > public class AverageDemo < public static void main(String[] args) < Integer[] intArray = ; Average integerAverage = new Average<>(intArray); System.out.println("Среднее значения для Integer " + integerAverage.average()); Double[] doubleArray = ; Average doubleAverage = new Average<>(doubleArray); System.out.println("Среднее значения для Double " + doubleAverage.average()); // Не откомпилируется, // потому что String не является наследником Number /* String[] strArray = ; Average strAverage = new Average<>(strArray); System.out.println("Среднее значения для String " + strAverage.average());*/ > >
В виде ограничения можно накладывать не только тип класса, но и тип интерфейса:
public class MyClass
Ограничение может включать в себя как тип класса, так и типы одного или нескольких интерфейсов:
class Gen
Тип класса должен быть задан первым. Накладывая на обобщенный тип ограничение, состоящее из класса и одного или нескольких интерфейсов, для их объединения следует воспользоваться логической операцией &: Таким образом, любой тип, передаваемый параметру Т , должен быть подклассом, производным от класса MyClass и реализующим интерфейсы MyInterface1 и MyInterface2 .
3. Применение метасимвольных аргументов
Представьте, что мы хотим добавить метод для сравнения средних значений массивов в класс Average из примера 3. Причем типы массивов могут быть разные:
Integer intArray[] = ; Double doubleArray[] = ; Average iob = new Average<>(intArray); Average dob = new Average<>(doubleArray); if (iob.sameAvg(dob)) < System.out.println("are the same.");>else
Так как Average параметризованный тип, какой тип параметра вы укажете для Average , когда создадите параметр метода типа Average ? Напрашивается следующий вариант:
boolean sameAvg(Average ob)
Но это не сработает, так как в этом случае метод sameAvg будет принимать аргументы только того же типа, что и существующий объект:
if (iob.sameAvg(iob)) < System.out.println("are the same.");>else
Чтобы создать обобщенную версию метода sameAvg() , следует воспользоваться другим средством обобщений Jаvа – метасимвольным аргументом. Метасимвольный аргумент обозначается знаком ? и представляет неизвестный тип.
boolean sameAvg(Average ob)
Мета символ не оказывает никакого влияния на тип создаваемых объектов класса Average . Это определяется оператором extends в объявлении класса Average. Мета символ просто совпадает с любым достоверным объектом класса Average .
Метасимвольные аргументы могут быть ограничены почти таким же образом, как и параметры типов. Ограничивать метасимвольный аргумент особенно важно при создании обобщенного типа, оперирующего иерархией классов. Например:
public class Average2 < private T[] array; public Average2(T[] array) < this.array = array.clone(); >public double average() < double sum = 0.0; for (T value : array) < sum += value.doubleValue(); >return sum / array.length; > boolean sameAvg(Average2 >ob) < return average() == ob.average(); >> public class AverageDemo2 < public static void main(String[] args) < Integer[] intArray = ; Average2 iob = new Average2<>(intArray); System.out.println("Среднее значения для Integer " + iob.average()); Double[] doubleArray = ; Average2 dob = new Average2<>(doubleArray); System.out.println("Среднее значения для Double " + dob.average()); Float[] floatArray = ; Average2 fob = new Average2<>(floatArray); System.out.println("Среднее значения для Float " + fob.average()); System.out.print("Средние значения для iob и dob "); if (iob.sameAvg(dob)) < System.out.println("одинаковые."); >else < System.out.println("разные."); >System.out.print("Средние значения для iob и fob "); if (iob.sameAvg(fob)) < System.out.println("одинаковые."); >else < System.out.println("разные."); >> >
4. Параметризованные методы и конструкторы
В методах параметризованного класса можно использовать параметр типа, а следовательно, они становятся параметризованными относительно параметра типа.
Но можно объявить параметризованный метод, в котором непосредственно используется один или несколько параметров типа. Более того, можно объявить параметризованный метод, входящий в не параметризованный класс. Например:
public class GenMethodDemo < /** * Является ли объект x элементом массива array * * @param x * @param array * @param * @param * @return */ public static boolean isIn(T x, V[] array) < for (V element : array) < if (x.equals(element)) < return true; >> return false; > public static void main(String[] args) < Integer[] intArray = ; if (isIn(2, intArray)) < System.out.println("2 входит в массив intArray"); >if (!isIn(7, intArray)) < System.out.println("7 не входит в intArray"); >System.out.println(); String[] strArray = ; if (isIn("two", strArray)) < System.out.println("two входит в массив strArray"); >if (!isIn("seven", strArray)) < System.out.println("seven не входит в массив strArray"); >> >
Конструкторы также могут быть обобщенными, даже если их классы таковыми не являются. Например:
public class GenConstructor < private double value; public GenConstructor(T arg) < value = arg.doubleValue(); >public void showValue() < System.out.println("value: " + value); >> public class GenConstructorDemo < public static void main(String[] args) < GenConstructor genConstructor1 = new GenConstructor(100); GenConstructor genConstructor2 = new GenConstructor(123.5F); genConstructor1.showValue(); genConstructor2.showValue(); >>
5. Параметризованные интерфейсы
В дополнение к обобщенным классам и методам вы можете объявлять параметризованные интерфейсы. Параметризованные интерфейсы специфицируются так же, как и обобщенные классы:
public interface MyInterface < T someMethod(T t); >public class MyClass implements MyInterface < @Override public T someMethod(T t) < return t; >public static void main(String[] args) < MyInterfaceobject = new MyClass<>(); String str = object.someMethod("some string"); > >
6. Иерархии параметризованных классов
Параметризованные классы могут быть частью иерархии классов так же, как и любые другие не параметризованные классы. То есть параметризованный класс может выступать в качестве супер класса или подкласса.
Ключевое отличие между параметризованными и не параметризованными иерархиями состоит в том, что в параметризованной иерархии любые аргументы типов, необходимые параметризованному супер классу, всеми подклассами должны передаваться по иерархии вверх.
public class GenericSuper < private T ob; public GenericSuper(T ob) < this.ob = ob; >private T getOb() < return ob; >> public class GenericSub extends GenericSuper < public GenericSub(T ob) < super(ob); >>
Подкласс параметризованного супер класса необязательно должен быть параметризованным, но в нем все же должны быть, указаны параметры типа, требующиеся его параметризованному супер классу. Подкласс может, если требуется, быть, дополнен и своими параметрами типа. Супер классом для параметризованного класса может быть класс не параметризованный.
7. Использование оператора instanceof c параметризованными классами
public class HierarchyDemo < public static void main(String[] args) < GenericSuperobject = new GenericSub<>(88); if (object instanceof GenericSuper>) < System.out.println("object is instance of GenericSuper"); >if (object instanceof GenericSub>) < System.out.println("object is instance of GenericSub"); >// Ошибка компиляции - информация об обобщенном типе недоступна во время выполнения /* if (object instanceof GenericSub) < System.out.println("object is instance of GenericSub"); >*/ > >
8. Ограничения присущие обобщениям
Обобщениям присущи некоторые ограничения. Рассмотрим их:
1. Нельзя создавать экземпляр по параметру типа. Ни обычный объект, ни массив:
public class GenRestriction < private T ob; private T[] array; public GenRestriction(T ob, T[] array) < // Недопустимо. //оb = new Т(); //array = new Т[10]; this.ob = ob; this.array = array; >>
2. Нельзя создать массив специфических для типа обобщенных ссылок:
public class GenArrays < public static void main(String[] args) < // Нельзя создать массив специфичных для типа обобщенных ссылок. // GenericSub[] gens = new GenericSub[10]; GenericSub>[] gens = new GenericSub>[10]; gens[0] = new GenericSub<>(34); > >
3. Нельзя создавать обобщенные статические переменные и методы. Но объявить статические обобщенные методы со своими параметрами типа все же можно:
public class GenericWrongStatic < // Неверно, нельзя создать статические переменные типа Т. //public static Т оb; // Неверно, ни один статический метод не может использовать Т. /* public static T getOb() < return оb; >*/ //Но объявить статические обобщенные методы со своими параметрами типа можно public static void getOb(V v) < System.out.println(v); >>
- Вложенные классы
- Задания
Для чего использовать дженерики в TypeScript
Дженерики (generic) помогают писать универсальный, переиспользуемый код, а также в некоторых случаях позволяют отказаться от any . Главная задача дженериков — помочь разработчику писать код, который одинаково будет работать со значениями разных типов.
Посмотрим на примере из реального мира.

Представьте завод по изготовлению автомобилей. Старый завод, который проектировался для сборки автомобиля определённой модели. На нём могут собирать только такую модель автомобиля, а если потребуется выпустить машину с немного другим кузовом, то придётся строить новый завод. Это неоптимальное решение. Если разные машины собираются одинаково, то лучше научиться собирать разные машины на одном заводе.
Узнайте больше о дженериках, научитесь на практике использовать аннотацию типов и обобщённое программирование на профессиональном курсе по TypeScript.
Суть дженериков
С дженериками тоже примерно так. Если мы напишем функцию и жёстко зададим тип, то она сможет работать только со значениями этого типа. Значения других типов передать не получится. Есть два способа это поправить.
Первый способ. Написать несколько одинаковых функций, которые работают с разными типами. Например, такая функция проверяет, есть ли в массиве конкретный элемент.
function includeStr(array: string[], query: string): boolean < // на входе массив и строка для поиска for (const value of array) < // перебираем массив if (value === query) < // если в массиве есть элемент — возвращаем true return true; >> // если ничего не нашлось, возвращаем false return false; >
Функция будет отлично работать на массивах из строк. Но для поиска в массиве из чисел придётся дублировать функцию, менять типы, но сам код функции останется неизменным. Например:
function includeNumber(array: number[], query: number): boolean < // всё то же самое, только на входе числа for (const value of array) < if (value === query) < return true; >> return false; >
Вот тут на помощь и приходят дженерики. Они помогают написать код, который одинаково работает с данными разных типов.
❌ Пишем много функций для разных типов
✅ Объявляем в функции параметр типа, а потом передаём через него нужный тип
Вместо конкретного типа, мы как будто объявляем «переменную», а затем передаём в неё нужный тип. Таким образом, получается код, который может работать с разными типами:
function include < T >(array: T[], query: T): boolean < for (const value of array) < if (value === query) < return true; >> return false; >
Код функции не поменялся, но теперь мы не указываем конкретный тип. Мы заводим переменную T и говорим, что тип параметра array — это тип, который будет передан в переменную T . А тип параметра query — это тип, который будет передан через переменную T .
Когда мы захотим воспользоваться этой функцией, то помимо данных для параметров array и query мы ещё должны передать информацию о типах (для переменной T ). В первом примере мы передаём тип string , а во втором — number .
// передаём string в качестве типа include < string >(['igor', 'sasha', 'ira'], 'ira'); // true // передаём number в качестве типа include < number >([1, 3, 5], 7); // false
Получается, что с помощью дженериков мы смогли написать код, который работает с разными типами значений. То есть, если коротко.
Дженерики — переменные, через которые мы можем передавать тип.
Ещё о JavaScript
- Type predicates в TypeScript на примере
- Типы данных в JavaScript. Инструкция для начинающих
- Живые и неживые коллекции в JavaScript
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.