Зачем нужны Generics?
Generics
- это механизм, который позволяет писать безопасный для типов код. С помощью Generics в Java можно создавать классы, интерфейсы и методы, которые могут работать с различными типами данных, без необходимости создавать отдельные версии этих элементов для каждого типа.
Для указания параметризованного типа используют угловые скобки (<>)
. Например, можно создать обобщенный класс, который будет хранить массив элементов любого типа:
public class Test<T> {
private T[] elements;
public Test(T elements) {
this.elements = elements;
}
public void printElems() {
for (T element : elements) {
System.out.print(element+" ");
}
}
public T getFirstElem() {
return elements[0];
}
}
Здесь означает, что класс Test параметризован типом T. При создании экземпляра класса Test, тип T будет заменен на конкретный тип данных. Например:
Test<String> test = new Test<>(new String[]{"Hello", "World"});
test.printElems(); //Hello World
Test<Integer> test = new Test<>(new Integer[]{1, 2, 3});
test.printElems(); //1 2 3
Также, если вы заметили, в нашем классе есть метод, который возвращает первый элемент массива. Но ведь мы не знаем, какой тип данных будет у массива. Поэтому, чтобы избежать ошибок, мы можем указать, что метод должен возвращать объект типа T:
test.getFirstElem(); //Hello
Но стоит отметить, что при указании типа T, мы не можем использовать примитивные типы. Например, следующий код не скомпилируется:
Test<int> test = new Test<>(new int[]{1, 2, 3});
Поэтому, если вы хотите использовать примитивные типы, то вам нужно использовать их обертки:
Test<Integer> test = new Test<>(new Integer[]{1, 2, 3});
Мы можем использовать несколько параметров типа. Например, можно создать класс, который будет хранить пару элементов любого типа:
public class Pair<T, V> {
private T first;
private V second;
public Pair(T first, V second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public V getSecond() {
return second;
}
}
Теперь, при создании экземпляра класса Pair, мы можем указать типы данных для параметров T и V:
Pair<String, Integer> pair = new Pair<>("Hello", 1);
System.out.println(pair.getFirst()); //Hello
Так как мы не можем указать несколько классов в качестве параметров типа, то нам нужно использовать ограничения. Для этого используется ключевое слово extends
. Например, мы можем создать класс, который будет принимать в качестве параметров типа Cat
и Dog
, которые наследуются от класса Animal
:
// Родительский класс Animal
public class Animal {
public void say() {
System.out.println("Hello");
}
}
// Классы Cat и Dog, которые наследуются от класса Animal
public class Cat extends Animal {
@Override
public void say() {
System.out.println("Meow");
}
}
public class Dog extends Animal {
@Override
public void say() {
System.out.println("Woof");
}
}
// Класс Test, который принимает в качестве параметров типа Cat и Dog
public class Test<T extends Animal> {
private T[] elements;
public Test(T elements) {
this.elements = elements;
}
public void printElems() {
for (T element : elements) {
element.say();
}
}
}
Но, если мы попытаемся создать экземпляр класса Test, указав в качестве параметра типа класс String
, то код не скомпилируется
-
Инвариантность
Это когда можно подставлять только определенный тип.
-
Ковариантность
Это когда можно подставлять более конкретный тип, вместо более обобщенного.
Это у нас
extends
. -
Контрвариантность
Это когда можно подставлять более общий тип, вместо более конкретного.
Это у нас
super
.
List
в Java
инвариантен, т.е если у меня есть два класса, один из которых наследник другого, например, List<String>
и List<Object>
, то эти коллекции - не являются наследниками друг друга и подставить одну вместо другой мы не можем. Они инвариантны.
List<String> strings = new ArrayList<>();
List<Object> objects = strings; //wrong!
Так делать запрещено! Почему? Потому что если бы такое было разрешено, то мы бы получали ошибки в рантайме, которые сложно отследить. Когда я бы у коллекции objects
какой-нибудь элемент кастовал бы в String
, а он был бы не String
.
Чтобы это работало - надо использовать ограничения.
Так как у нас тут list
- это producer
данных, то использовать надо extends
.
List<String> strings = new ArrayList<>();
List<? extends String> objects = strings; //right!
Есть еще ограничения super
. Если у нас коллекция - это consumer
.
Коллекция потребляет данные, т.е мы туда что-то записываем.
Тогда можно написать так:
static void putAnimalToCollection(List<? super Animal> list) {
list.add(new Cat("Kitty"));
list.add(new Dog("Doggerman"));
}
Мы можем только писать туда, но не забирать оттуда данные, так как мы не знаем, что конкретно к нам придет из такой коллекции. Т.е компилятор считает, что там Object
, поэтому мы не знаем к чему кастовать.
Это еще называется PECS
- Producer extends Consumer super.
Еще раз: Producer
- может работать с типом T
и его наследниками, Consumer
- может принимать T
и его предков.