Тип данных определяет набор операций, который допустим для данного типа. Например, мы можем складывать числа, но не можем складывать булевы значения.
jshell> 3 + 4
$1 ==> 7
jshell> true + false
| Error:
| bad operand types for binary operator '+'
| first type: boolean
| second type: boolean
| true + false
| ^----------^
Однако, логика кода не всегда зависит от того, с каким типом данных происходит работа. Ярким примером служат коллекции, где большая часть операций никак не связана с типом данных, который находится внутри. Например, операция добавления элемента в список никак не затрагивает сам элемент. То же самое касается изменения, удаления и большей части остальных операций. Эти операции производятся над самой коллекцией, но сами элементы никак не обрабатываются.
Представьте если бы коллекций в том виде, в котором мы с ними знакомились в Java не существовало. Как бы мы их реализовали? У нас было бы два способа:
Создать свой вариант коллекции для каждого типа данных
public class ArrayListOfInts {
private Integer[] data;
private int size;
public ArrayListOfInts() {
data = new Integer[10]; // initial capacity
size = 0;
}
public void add(Integer value) {
// Увеличивает размер внутреннего массива если место закончилось
ensureCapacity();
data[size++] = value;
}
public Integer get(int index) {
return data[index];
}
// Остальные методы
}
Ровно такой же класс мы сделаем и для остальных типов. Разница в этих классах будет исключительно в типе данных, который указан в описании класса:
public class ArrayListOfStrings {
private String[] data;
private int size;
public ArrayListOfStrings() {
data = new String[10]; // initial capacity
size = 0;
}
public void add(String value) {
// Увеличивает размер внутреннего массива если место закончилось
ensureCapacity();
data[size++] = value;
}
public String get(int index) {
return data[index];
}
// Остальные методы
}
Представьте, что, то же самое придется делать для каждого нового класса или интерфейса, которые добавляются в код. Никто не захочет тратить на это время. Поэтому обычно идут другим способом.
Приведение к Object
В Java все классы неявно наследуют класс Object
. Тему наследования мы еще не проходили, но для данной задачи нам не нужны глубокие знания. Достаточно увидеть, что любой объект можно привести к типу Object
, а можно выполнить обратное преобразование.
Object value = "string";
// Методы строки работать не будут
// Error: cannot find symbol symbol: method toUpperCase()
value.toUpperCase();
// Здесь у нас снова обычная строка
var value2 = (String) value;
// Этот код работает
value2.toUpperCase();
Таким образом мы можем создать ровно один класс, хранящий в себе все данные в виде Object
.
public class ArrayListOfObjects {
private Object[] data;
private int size;
public ArrayListOfObjects() {
data = new Object[10]; // initial capacity
size = 0;
}
public void add(Object value) {
// Увеличивает размер внутреннего массива если место закончилось
ensureCapacity();
data[size++] = value;
}
// Данные придется преобразовывать в нужный тип снаружи
public Object get(int index) {
return data[index];
}
// Остальные методы
}
Использование:
var items = new ArrayListOfObjects();
items.add("Sun");
// Требуется ручное преобразование
var value = (String) items.get(0);
У этого способа есть серьезный недостаток, это необходимость вручную следить за типами и как следствие, отсутствие типобезопасности. В такую коллекцию можно добавить любые данные, так как все типы в Java являются подтипами Object
.
var items = new ArrayListOfObjects();
items.add("Sun");
items.add(234);
items.add(true);
var value = (String) items.get(0);
var value = (Integer) items.get(1);
var value = (Boolean) items.get(2);
System.out.println(value);
Все это привело к тому, что в языке появились дженерики, которые с одной стороны убирают дублирование кода, с другой обеспечивают типобезопасность. Концепция дженериков основана на понятии "параметр типа". То есть у типа (класса или интерфейса) появляется параметр, который тоже является типом. Этот параметр определяет то, с каким типом будет работать дженерик для конкретной ситуации, например, созданного объекта. Синтаксически, параметр типа указывается в угловых скобках во время создания объекта из дженерика.
// items1 работает только с числами
var items1 = new MyArrayList<Integer>();
// items2 работает только со строками
var items2 = new MyArrayList<String>();
Внутри это выглядит примерно так:
public class MyArrayList<T> {
private Object[] data;
private int size;
public MyArrayList() {
data = new Object[10];
size = 0;
}
public void add(T value) {
data[size++] = value;
}
public T get(int index) {
// Приведение типа к T
return (T) data[index];
}
// Остальные методы
}
После названия класса ставятся угловые скобки, внутри которых используется имя для параметра типа. Обычно пишут T
, но это не обязательно. Внутри класса параметр типа используется там, где бы использовался обычный тип. Единственное исключение в случае коллекций заключается в том, что данные все равно надо хранить как объекты. Преобразование делается во время получения данных, внутри дженерика.
Программирование с использованием дженериков часто называют обобщенным программированием, а сами дженерики параметризуемыми типами. Так как дженериками выступают классы и интерфейсы, в которые как в методы передается параметр, только в отличие от методов, параметром является не значение какого-то типа, а сам тип.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.