Дженерики или обобщения в Java
Положите в список без дженериков строку "42", а достаньте её как число: Integer n = (Integer) list.get(0); — компилятор промолчит, а программа упадёт во время выполнения с ClassCastException. Дженерики переносят эту ошибку туда, где её дешевле всего найти, — на этап компиляции.
Дженерики (generics, обобщения) в Java — это механизм параметризации типов: класс, интерфейс или метод объявляется с параметром типа (обычно T), а конкретный тип подставляется при использовании, например List<String> или Matrix<Integer>. Один и тот же код работает с разными типами данных, а компилятор проверяет типы и сам вставляет приведения. Дженерики появились в Java 5.
Классы, интерфейсы и методы с параметрами типа называют обобщёнными или параметризованными: обобщённый класс, обобщённый метод. Тип с подставленным аргументом, например Matrix<Integer>, называют параметризованным типом.
Зачем нужны дженерики
Дженерики нужны для безопасности типов: без них коллекция хранит Object, программист вручную приводит элементы к нужному типу, и ошибка приведения проявляется только во время выполнения. С дженериками компилятор знает тип элементов, не пропускает неподходящие значения и избавляет от явных приведений.
import java.util.ArrayList;
import java.util.List;
public class WhyGenerics {
public static void main(String[] args) {
// Без дженериков: сырой тип (raw type)
List rawList = new ArrayList();
rawList.add("42");
Integer n = (Integer) rawList.get(0); // компилируется, но падает при запуске
// С дженериками
List<Integer> numbers = new ArrayList<>();
// numbers.add("42"); // ошибка компиляции: incompatible types: String cannot be converted to Integer
numbers.add(42);
Integer m = numbers.get(0); // приведение не нужно
}
}
// Вывод:
// Exception in thread "main" java.lang.ClassCastException:
// class java.lang.String cannot be cast to class java.lang.Integer ... Тип без аргументов в угловых скобках (List вместо List<Integer>) называется сырым типом (raw type). Он оставлен только для совместимости с кодом, написанным до Java 5, — в новом коде его не используют.
Что означает T в Java: параметр типа
T в Java — это имя параметра типа, заполнитель, вместо которого при использовании класса или метода подставляется конкретный ссылочный тип. В объявлении class Matrix<T> буква T — параметр типа, а в Matrix<Integer> тип Integer — аргумент типа.
Параметр типа объявляется в угловых скобках после имени класса или перед типом возвращаемого значения метода. Внутри класса T используется как обычный тип: для полей, параметров, возвращаемых значений и локальных переменных. Имя может быть любым идентификатором, но по соглашению используют одну заглавную букву:
| Имя | Расшифровка | Где встречается | Пример из JDK |
|---|---|---|---|
T | Type — любой тип | Обобщённые классы и методы | Optional<T>, Comparable<T> |
E | Element — элемент | Коллекции | List<E>, Set<E> |
K, V | Key, Value — ключ и значение | Словари | Map<K, V> |
N | Number — число | Числовые параметры | <N extends Number> |
R | Result — результат | Функциональные интерфейсы | Function<T, R> |
S, U | Второй, третий тип | Несколько параметров | BiFunction<T, U, R> |
Аргументом типа может быть только ссылочный тип. Вместо примитивов используют классы-обёртки: Integer вместо int, Double вместо double; значения преобразуются автоматически благодаря автоупаковке. Начиная с Java 7 аргументы типа в правой части объявления можно не повторять — достаточно diamond-оператора <>: Matrix<Integer> m = new Matrix<>(...).
Параметризованные классы
Параметризованный (обобщённый) класс — это класс, объявленный с одним или несколькими параметрами типа в угловых скобках: class Matrix<T>. Его код пишется один раз, а при создании объекта указывается конкретный тип: Matrix<Double>, Matrix<Integer>.
Пример обобщённого класса, который хранит элементы матрицы в массиве:
public class Matrix<T> {
private final T[] array;
public Matrix(T[] array) {
this.array = array.clone();
}
public T get(int index) {
return array[index];
}
public int size() {
return array.length;
}
public static void main(String[] args) {
Matrix<Double> doubleMatrix = new Matrix<>(new Double[]{1.5, 2.5});
Matrix<Integer> integerMatrix = new Matrix<>(new Integer[]{1, 2, 3, 4});
Integer first = integerMatrix.get(0); // приведение не нужно
System.out.println(first + " " + doubleMatrix.size());
}
}
// Вывод: 1 2 В объявлении Matrix<Integer> integerMatrix тип Integer является аргументом типа. Java не создаёт отдельную версию класса для каждого аргумента: во время выполнения существует только один класс Matrix (почему так — в разделе Стирание типов).
Дженерики работают только с объектами, поэтому примитивный тип в качестве аргумента не скомпилируется:
Matrix<int> intMatrix = new Matrix<>(new int[3]);
// Ошибка компиляции: unexpected type; required: reference, found: int Параметризованные типы с разными аргументами несовместимы между собой. doubleMatrix и integerMatrix оба являются экземплярами Matrix, но для компилятора это разные типы:
doubleMatrix = integerMatrix;
// Ошибка компиляции: incompatible types: Matrix<Integer> cannot be converted to Matrix<Double> Обобщённый класс может иметь любое количество параметров типа — они перечисляются через запятую:
public class TwoGen<T, V> {
private final T obT;
private final 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) {
TwoGen<Integer, String> twoGen = new TwoGen<>(88, "Generics");
twoGen.showTypes();
System.out.println("Значение T: " + twoGen.getObT());
System.out.println("Значение V: " + twoGen.getObV());
}
}
// Вывод:
// Тип T: java.lang.Integer
// Тип V: java.lang.String
// Значение T: 88
// Значение V: Generics Ограниченные типы: <T extends Number>
Ограниченный тип (bounded type) — это параметр типа с верхней границей: запись <T extends Number> разрешает подставлять вместо T только Number и его подклассы. Взамен внутри класса можно вызывать методы границы, например doubleValue(), которых у произвольного Object нет.
Общий вид объявления — параметр типа, ключевое слово extends и суперкласс-граница:
class Gen<T extends Superclass> Пример: класс вычисляет среднее значение массива любых чисел.
public class Average<T extends Number> {
private final T[] array;
public Average(T[] array) {
this.array = array;
}
public double average() {
double sum = 0.0;
for (T value : array) {
sum += value.doubleValue(); // метод Number доступен благодаря границе
}
return sum / array.length;
}
}
public class AverageExample {
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
Average<Integer> integerAverage = new Average<>(intArray);
System.out.println("Среднее значение для Integer: " + integerAverage.average());
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
Average<Double> doubleAverage = new Average<>(doubleArray);
System.out.println("Среднее значение для Double: " + doubleAverage.average());
// Не скомпилируется, потому что String не является наследником Number:
// String[] strArray = {"1", "2", "3"};
// Average<String> strAverage = new Average<>(strArray);
// error: type argument String is not within bounds of type-variable T
}
}
// Вывод:
// Среднее значение для Integer: 3.0
// Среднее значение для Double: 3.3 Границей может быть не только класс, но и интерфейс — для интерфейса тоже пишется extends, а не implements:
public class MyClass<T extends Serializable> Ограничение может включать класс и один или несколько интерфейсов, объединённых знаком &. Класс указывается первым:
class Gen<T extends MyClass & MyInterface1 & MyInterface2> Здесь аргументом типа может быть только подкласс MyClass, который реализует оба интерфейса — MyInterface1 и MyInterface2.
Метасимвольный аргумент (wildcard ?)
Метасимвольный аргумент (wildcard) — это знак ? на месте аргумента типа, обозначающий неизвестный тип. Он нужен, когда метод должен принимать параметризованный тип с любым аргументом: Average<?> принимает и Average<Integer>, и Average<Double>.
Допустим, в класс Average из раздела Ограниченные типы нужно добавить метод sameAvg(), который сравнивает средние значения двух массивов, причём типы массивов могут различаться. Первая мысль — использовать параметр T:
boolean sameAvg(Average<T> ob) {
return average() == ob.average();
} Такой метод примет только объект с тем же аргументом типа: Average<Integer> можно сравнить с Average<Integer>, а вызов с Average<Double> не скомпилируется. Решение — метасимвольный аргумент:
boolean sameAvg(Average<?> ob) {
return average() == ob.average();
} Метасимвол не влияет на то, какие объекты Average можно создавать, — это по-прежнему определяет граница extends Number в объявлении класса. Он лишь совпадает с любым допустимым объектом Average:
public class AverageExample2 {
public static void main(String[] args) {
Average<Integer> iob = new Average<>(new Integer[]{1, 2, 3, 4, 5});
Average<Double> dob = new Average<>(new Double[]{1.1, 2.2, 3.3, 4.4, 5.5});
Average<Float> fob = new Average<>(new Float[]{1.0F, 2.0F, 3.0F, 4.0F, 5.0F});
System.out.println("iob и dob: " + (iob.sameAvg(dob) ? "одинаковые" : "разные"));
System.out.println("iob и fob: " + (iob.sameAvg(fob) ? "одинаковые" : "разные"));
}
}
// Вывод:
// iob и dob: разные
// iob и fob: одинаковые Метасимвол можно ограничить сверху или снизу. Это особенно важно, когда метод работает с иерархией классов: List<Integer> не является подтипом List<Number>, хотя Integer — подкласс Number, а List<? extends Number> принимает оба.
| Запись | Что принимает | Чтение элементов | Добавление элементов |
|---|---|---|---|
List<?> | Список с любым аргументом типа | Как Object | Нельзя (кроме null) |
List<? extends Number> | List<Number>, List<Integer>, List<Double>... | Как Number | Нельзя (кроме null) |
List<? super Integer> | List<Integer>, List<Number>, List<Object> | Только как Object | Можно добавлять Integer |
import java.util.ArrayList;
import java.util.List;
public class WildcardExample {
// ? extends Number: из списка можно читать элементы как Number
static double sum(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
// list.add(1); // ошибка компиляции: точный тип элементов неизвестен
return sum;
}
// ? super Integer: в список можно добавлять Integer
static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println(sum(ints));
System.out.println(sum(doubles));
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println(numbers);
// List<Number> wrong = ints;
// ошибка компиляции: incompatible types: List<Integer> cannot be converted to List<Number>
}
}
// Вывод:
// 6.0
// 4.0
// [1, 2, 3] Параметризованные методы и конструкторы
Параметризованный (обобщённый) метод — это метод, который объявляет собственные параметры типа в угловых скобках перед типом возвращаемого значения: public static <T> boolean isIn(T x, T[] array). Такой метод можно объявить и в обычном, необобщённом классе; аргумент типа компилятор обычно выводит сам по аргументам вызова.
Методы обобщённого класса, использующие параметр класса T, тоже работают с параметром типа, но собственных параметров не объявляют. Параметризованный метод независим от класса:
public class GenMethodExample {
/**
* Проверяет, является ли объект x элементом массива array.
*
* @param x искомый объект
* @param array массив, в котором выполняется поиск
* @param <T> тип искомого объекта и элементов массива
* @return true, если x равен одному из элементов массива
*/
public static <T> boolean isIn(T x, T[] array) {
for (T element : array) {
if (x.equals(element)) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
if (isIn(2, intArray)) {
System.out.println("2 входит в массив intArray");
}
if (!isIn(7, intArray)) {
System.out.println("7 не входит в массив intArray");
}
String[] strArray = {"one", "two", "three", "four", "five"};
if (isIn("two", strArray)) {
System.out.println("two входит в массив strArray");
}
if (!isIn("seven", strArray)) {
System.out.println("seven не входит в массив strArray");
}
}
}
// Вывод:
// 2 входит в массив intArray
// 7 не входит в массив intArray
// two входит в массив strArray
// seven не входит в массив strArray Неочевидный момент
Вызов isIn("2", intArray) скомпилируется и вернёт false: компилятор выведет T как общий супертип String и Integer (пересечение Serializable & Comparable<...>). Чтобы такая ошибка ловилась при компиляции, укажите аргумент типа явно: GenMethodExample.<Integer>isIn("2", intArray) — теперь это ошибка incompatible types: String cannot be converted to Integer.
Конструкторы тоже могут быть обобщёнными, даже если их класс таковым не является:
public class GenConstructor {
private final double value;
public <T extends Number> GenConstructor(T arg) {
value = arg.doubleValue();
}
public void showValue() {
System.out.println("value: " + value);
}
}
public class GenConstructorExample {
public static void main(String[] args) {
GenConstructor genConstructor1 = new GenConstructor(100);
GenConstructor genConstructor2 = new GenConstructor(123.5F);
genConstructor1.showValue();
genConstructor2.showValue();
}
}
// Вывод:
// value: 100.0
// value: 123.5 Параметризованные интерфейсы
Параметризованный (обобщённый) интерфейс объявляется так же, как обобщённый класс: interface MyInterface<T>. В JDK так устроены Comparable<T>, Comparator<T>, List<E>. Класс, реализующий такой интерфейс, либо сам становится обобщённым и передаёт свой параметр интерфейсу, либо подставляет конкретный тип.
public interface MyInterface<T> {
T someMethod(T t);
}
// 1. Обобщённый класс передаёт свой параметр T интерфейсу
public class Echo<T> implements MyInterface<T> {
@Override
public T someMethod(T t) {
return t;
}
}
// 2. Обычный класс фиксирует аргумент типа
public class Upper implements MyInterface<String> {
@Override
public String someMethod(String s) {
return s.toUpperCase();
}
}
public class InterfaceExample {
public static void main(String[] args) {
MyInterface<String> echo = new Echo<>();
MyInterface<String> upper = new Upper();
System.out.println(echo.someMethod("some string"));
System.out.println(upper.someMethod("some string"));
}
}
// Вывод:
// some string
// SOME STRING Если класс реализует обобщённый интерфейс, но сам объявлен без параметра и не указывает аргумент (class Echo implements MyInterface<T>), компилятор выдаст ошибку cannot find symbol: class T.
Иерархии параметризованных классов
Параметризованные классы участвуют в наследовании так же, как обычные: обобщённый класс может быть суперклассом или подклассом. Главное правило — подкласс обязан передать вверх по иерархии все аргументы типа, которые требует обобщённый суперкласс: либо своим параметром, либо конкретным типом.
public class GenericSuper<T> {
private final T ob;
public GenericSuper(T ob) {
this.ob = ob;
}
public T getOb() {
return ob;
}
}
// 1. Подкласс передаёт свой параметр T суперклассу
public class GenericSub<T> extends GenericSuper<T> {
public GenericSub(T ob) {
super(ob);
}
}
// 2. Необобщённый подкласс фиксирует аргумент типа
public class IntegerHolder extends GenericSuper<Integer> {
public IntegerHolder(int value) {
super(value);
}
}
// 3. Подкласс добавляет собственный параметр типа
public class LabeledHolder<T, V> extends GenericSuper<T> {
private final V label;
public LabeledHolder(T ob, V label) {
super(ob);
this.label = label;
}
public V getLabel() {
return label;
}
}
public class HierarchyExample {
public static void main(String[] args) {
LabeledHolder<Double, String> price = new LabeledHolder<>(9.99, "цена");
System.out.println(price.getLabel() + ": " + price.getOb());
int value = new IntegerHolder(5).getOb(); // приведение не нужно
System.out.println(value);
}
}
// Вывод:
// цена: 9.99
// 5 Подкласс обобщённого суперкласса не обязан быть обобщённым (вариант 2), а при необходимости может объявить и собственные параметры типа (вариант 3). Обратное тоже допустимо: суперклассом обобщённого класса может быть обычный необобщённый класс.
Оператор instanceof и дженерики
Оператор instanceof проверяет класс объекта во время выполнения, а аргументы типа к этому моменту стёрты. Поэтому проверка instanceof GenericSub<?> работает всегда, а проверка с конкретным аргументом, например instanceof GenericSub<Integer>, разрешена только тогда, когда компилятор может гарантировать её корректность по статическому типу переменной.
public class InstanceofExample {
public static void main(String[] args) {
GenericSuper<Integer> object = new GenericSub<>(88);
System.out.println(object instanceof GenericSuper<?>);
System.out.println(object instanceof GenericSub<?>);
// Java 16+: компилируется, приведение GenericSuper<Integer> -> GenericSub<Integer> проверяемое
System.out.println(object instanceof GenericSub<Integer>);
Object any = object;
// Ошибка компиляции в любой версии Java:
// System.out.println(any instanceof GenericSub<Integer>);
// Java 16+: Object cannot be safely cast to GenericSub<Integer>
// до Java 16: illegal generic type for instanceof
}
}
// Вывод:
// true
// true
// true Изменение в Java 16
До Java 16 любая проверка instanceof с конкретным аргументом типа была ошибкой компиляции, и в старых учебниках и тестах так и написано. Вместе с pattern matching для instanceof (JEP 394) правило смягчили: проверка разрешена, если приведение к целевому типу не является непроверяемым (unchecked). Удобно в паттерне: if (object instanceof GenericSub<Integer> sub) { int x = sub.getOb(); }.
Стирание типов (type erasure)
Стирание типов (type erasure) — это способ, которым в Java реализованы дженерики: компилятор проверяет аргументы типа, а затем удаляет их из байт-кода. Параметр T заменяется своей границей (Object, если границы нет, или Number для <T extends Number>), а в местах использования вставляются приведения типов. Во время выполнения JVM не знает, чем был T.
Так выглядит обобщённый класс до компиляции и (условно) после стирания:
// Исходный код
public class Box<T extends Number> {
private T value;
public T get() { return value; }
}
Box<Integer> box = new Box<>();
Integer i = box.get();
// Что остаётся после стирания
public class Box {
private Number value;
public Number get() { return value; }
}
Box box = new Box();
Integer i = (Integer) box.get(); // приведение вставил компилятор Поэтому параметризованные типы с разными аргументами во время выполнения неразличимы — это один и тот же класс:
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(strings.getClass().getName());
// Вывод:
// true
// java.util.ArrayList Стирание объясняет, почему нельзя написать new T(): в байт-коде на этом месте был бы new Object(), и JVM не знала бы, объект какого класса создать. Если тип нужен во время выполнения, его передают явно — объектом Class<T> (такой приём называют type token):
public class Factory<T> {
private final Class<T> type;
public Factory(Class<T> type) {
this.type = type;
}
public T create() throws ReflectiveOperationException {
return type.getDeclaredConstructor().newInstance(); // вместо недопустимого new T()
}
public static void main(String[] args) throws ReflectiveOperationException {
Factory<StringBuilder> factory = new Factory<>(StringBuilder.class);
StringBuilder sb = factory.create();
System.out.println(sb.append("создан через Class<T>"));
}
}
// Вывод: создан через Class<T> Где информация о типе всё-таки сохраняется: getGenericSuperclass()
Стираются аргументы типа у объектов, но не в объявлениях классов, полей и методов. Если класс наследует параметризованный суперкласс с конкретным аргументом, этот аргумент записан в class-файл, и его можно прочитать через рефлексию методом Class.getGenericSuperclass():
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class SuperclassTypeExample {
public static void main(String[] args) {
// IntegerHolder extends GenericSuper<Integer> (из раздела об иерархиях)
Type type = IntegerHolder.class.getGenericSuperclass();
System.out.println(type);
Type argument = ((ParameterizedType) type).getActualTypeArguments()[0];
System.out.println(argument);
}
}
// Вывод:
// GenericSuper<java.lang.Integer>
// class java.lang.Integer На этом построен приём super type token: создают анонимный подкласс абстрактного обобщённого класса, например new TypeReference<List<String>>() {}, и в его конструкторе вызывают getClass().getGenericSuperclass(). Так библиотеки Jackson (TypeReference) и Spring (ParameterizedTypeReference) узнают полный тип List<String>, который через List.class передать невозможно.
Ограничения дженериков
Почти все ограничения дженериков в Java — следствие стирания типов: то, что требует знать T во время выполнения (создание объекта или массива, проверка типа), запрещено, а к статическим членам параметр типа класса не относится вовсе. Сводная таблица с точными сообщениями компилятора javac:
| Что нельзя | Пример | Сообщение компилятора | Как обойти |
|---|---|---|---|
| Примитив как аргумент типа | Matrix<int> | unexpected type; required: reference, found: int | Класс-обёртка: Matrix<Integer> |
| Создать объект по параметру типа | new T() | unexpected type; required: class, found: type parameter T | Передать Class<T> или Supplier<T> |
| Создать массив по параметру типа | new T[10] | generic array creation | Получить готовый массив в конструкторе или использовать List<T> |
| Массив параметризованного типа | new GenericSub<Integer>[10] | generic array creation | GenericSub<?>[] или List<GenericSub<Integer>> |
Использовать T класса в статическом поле или методе | static T ob; | non-static type variable T cannot be referenced from a static context | Статический метод со своим параметром: static <V> void m(V v) |
| Перегрузка, различающаяся только аргументом типа | print(List<String>) и print(List<Integer>) | name clash: ... have the same erasure | Дать методам разные имена |
| Обобщённый класс исключения | class MyEx<T> extends Exception | a generic class may not extend java.lang.Throwable | Хранить данные в обычном поле исключения |
Те же ограничения в коде:
public class GenRestriction<T> {
private T ob;
private T[] array;
// Неверно: статическое поле не может иметь тип T
// public static T staticOb;
public GenRestriction(T ob, T[] array) {
// Недопустимо:
// this.ob = new T();
// this.array = new T[10];
this.ob = ob;
this.array = array; // массив создаёт вызывающий код, где тип известен
}
// Неверно: статический метод не может использовать T класса
// public static T getStaticOb() { return null; }
// А статический обобщённый метод со своим параметром типа объявить можно
public static <V> void print(V v) {
System.out.println(v);
}
public static void main(String[] args) {
// GenericSub<Integer>[] gens = new GenericSub<Integer>[10]; // generic array creation
GenericSub<?>[] gens = new GenericSub<?>[10];
gens[0] = new GenericSub<>(34);
print(gens[0].getOb());
}
}
// Вывод: 34 Часто задаваемые вопросы
Чем дженерики Java отличаются от шаблонов C++ и generics в C#?
Шаблоны C++ генерируют отдельный код для каждого аргумента типа, а в C# аргументы типа сохраняются во время выполнения (reified generics), поэтому там допустимы List<int> и new T(). В Java существует один класс на все аргументы, а сами аргументы стираются после проверки компилятором. Подробнее — в разделе Стирание типов.
Почему List<Integer> нельзя передать в метод с параметром List<Number>?
Параметризованные типы инвариантны. Если бы List<Integer> был подтипом List<Number>, метод мог бы добавить в него Double и сломать список целых чисел. Чтобы метод принимал списки любых чисел, объявите параметр как List<? extends Number> — см. раздел Метасимвольный аргумент.
Что такое правило PECS?
PECS — Producer Extends, Consumer Super. Если параметр только отдаёт элементы методу (производитель), используйте ? extends T; если только принимает их (потребитель) — ? super T. Классический пример из JDK — Collections.copy(List<? super T> dest, List<? extends T> src).
Что получится, если написать var list = new ArrayList<>()?
Типом переменной станет ArrayList<Object>: при var слева нет типа, из которого diamond-оператор мог бы вывести аргумент. В такой список можно положить что угодно, и проверка типов теряется. Пишите var list = new ArrayList<String>() или объявляйте тип явно.
Почему компилятор пишет «uses unchecked or unsafe operations»?
Это предупреждение о непроверяемых операциях: в коде используется сырой тип (например, List без угловых скобок) или приведение к параметризованному типу, которое нельзя проверить во время выполнения. Подробности покажет компиляция с ключом -Xlint:unchecked. Исправление — указать аргументы типа; @SuppressWarnings("unchecked") ставят только там, где безопасность доказана вручную.
Practice
Reinforce this lesson with hands-on tasks, each with a level and a time estimate.
- Обобщенный класс с тремя параметрами
- Обобщенная матрица
- Нахождение минимального и максимального элемента массива
- Калькулятор с обобщёнными статическими методами
- Wildcard: копирование списка и поиск максимума
Video Explanation
Prefer video format? Watch this lesson with examples and explanations.
Comments