Generics - DmitryGontarenko/usefultricks GitHub Wiki
Generics (обобщения) - обобщения позволяют типам (классам и интерфейсам) быть параметрами при определении класов, интерфейсов и методов. Передача разных параметров дает нам возможность повторно использовать один и тот же код.
Еще одно преимущеста обобщений в том, что они позволяют предотвращать ошибки времени выполнения (runtime) при приобразовании типов, переводя такие ошибки в ошибки компиляции (compile exception). Это намного упрощает поиск ошибок и делает код безопаснее.
Рассмотрми небольшой пример:
public class GenericsApplication {
public static void main(String[] args) {
GenericTest<String> t1 = new GenericTest<>("Paul");
GenericTest<Integer> t2 = new GenericTest<>(17);
t1.display(); // Paul
t2.display(); // 17
}
}
class GenericTest<T> {
private T obj;
/**
* Пример обобщенного типа в качестве возвращаемого значения
*/
public T getObj() {
return obj;
}
public GenericTest(T obj) {
this.obj = obj;
}
void display() {
System.out.println(obj);
}
}
При создании экземпляра мы параметризировали класс типами String
и Integer
.
Таким образом, если мы захотим присвоить одну переменную другой - это приведет к ошибки компиляции:
t1 = t2; // Incompatible types. Required: String, Found: Integer
А если не использовать обобщения при создании экземпляров или заменить тип T
на тип Object
, то такой код не вызовет ошибок компиляции, но при запуске программы вызовет ошибку времени выполнения:
GenericTest t1 = new GenericTest("Paul");
GenericTest t2 = new GenericTest(17);
t1 = t2;
String text = (String) t1.obj; // ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Generic-класс может иметь несколько параметров типа.
Например, создадим класс, который будет хранить пару ключ-значение:
public class GenericsApplication {
public static void main(String[] args) {
Pair<String, Integer> pair1 = new Pair<>("Sarah", 18);
Pair<String, String> pair2 = new Pair<>("Russia", "st. Petersburg");
System.out.println("Name: " + pair1.getKey() + ", age: " + pair1.getValue());
// Name: Sarah, age: 18
System.out.println("Country: " + pair2.getKey() + ", city: " + pair2.getValue());
// Country: Russia, city: st. Petersburg
}
}
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
Обобщенные методы - это методы, которые могут использовать свои собственные параметры типа. Область действия таких параметров будет ограничена методом, в котором они объявлены.
Параметры указываются внутри угловых скобок, перед типом возвращаемого значения, например:
public static <V> int method(V value);
Рассмотрим на примере:
public class GenericsApp {
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair<>(1, "Sarah");
Pair<Integer, String> p2 = new Pair<>(1, "Sarah");
boolean result = Service.compare(p1, p2);
System.out.println(result); // true
}
}
@AllArgsConstructor
class Pait<K, V> {
private K key;
private V value;
}
class Service {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p2.getValue().equals(p2.getValue());
}
}
Полный синтаксис для вызова метода будет с использованием ковычек перед именем самого метода, но этого указывать не обязательно:
boolean result2 = Service.<Integer, String>compare(p1, p2);
Все Generics в коде существуют только в момент компиляции, а в момент выполнения (запуска программы) заменяются на тип Object
. Тем самым Generic работают и помогают в поиске ошибок именно в моменте компиляции.
Исходя из этого, мы не можем вызвать у Обобщенного типа какой-либо кастомный метод (кроме методов самого класса Object).
Рассмотрим на примере - мы написали обобщенный класс и хотим вызвать в цикле метод из этого класса:
class Service<T> {
T[] array;
void display() {
for (T temp : array) {
temp.someMethod(); // Error: Cannot resolve method 'someMethod()'
}
}
}
В таком случае мы получим ошибку компиляции, т.к. у класса Object отсутствует такой метод.
На помощью нам придет возможность наследования в Generics - <T extends Class>
.
Рассмотрим готовый пример с использованием наследования:
/**
* Клиентский код
*/
public class GenericsApplication {
public static void main(String[] args) {
Backend[] backends = {
new Backend("Java"),
new Backend("Python")
};
Service<Developer> service = new Service<>(backends);
service.display(); // Developer writes Java code
// Developer writes Python code
}
}
interface Developer {
void write();
}
@AllArgsConstructor
class Backend implements Developer {
private String language;
@Override
public void write() {
System.out.println("Developer writes " + language + " code");
}
}
@AllArgsConstructor
class Service<T extends Developer> {
private T[] array;
void display() {
for (T temp : array) {
temp.write();
}
}
}
В данном случае мы указали, что обобщенный тип T
наследуется от типа Developer
и может вызывать его методы.
В Generics есть возможность наследоваться сразу от нескольких классов или интерфейсов.
Рассмотрим синтаксис:
class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
class F <T extends A & B, U extends C> { /* ... */ }
Наследование нескольких классов или интерйесов для одного параметра осуществляется с помощью символа &
.
Если же нужно указать несколько параметров, используется запятая.
ВАЖНО! При множественном наследовании первым должен стоять класс (если он пристутствует), а затем уже интерфесы. Иначе будет получена ошибка компиляции.
- можно использовать только один класс
В Generics коде символ знак вопроса ?
называется wildcard и представляет неизвестный тип. Это значит
Wildcard можно использовать в качестве типа параметра, поля или локальной переменной.
Например, когда в одном методе, в качестве параметров передаются экземпляры Generics-классов с разными generics-типами - необходимо использовать Wildcard:
public class WildcardApp {
public static void main(String[] args) {
Employee[] arrays1 = { new Fullday(16), new Fullday(43), new Fullday(25) };
Employee[] arrays2 = { new Partday(24), new Partday(21), new Partday(19) };
Util<Employee> u1 = new Util<>(arrays1);
Util<Employee> u2 = new Util<>(arrays2);
Util.compare(u1, u2); // 20
}
}
interface Employee {
int getAge();
}
@AllArgsConstructor
class Fullday implements Employee {
private int age;
@Override
public int getAge() {
return age;
}
}
@AllArgsConstructor
class Partday implements Employee {
private int age;
@Override
public int getAge() {
return age;
}
}
@AllArgsConstructor
class Util<T extends Employee> {
private T[] ages;
static void compare(Util<?> a1, Util<?> a2) {
System.out.println(a1.countAge() - a2.countAge());
}
private int countAge() {
int sum = 0;
for (T temp : ages) {
sum += temp.getAge();
}
return sum;
}
}
Есть несколько типов Wildcard:
- Unbounded - тип класса для подстановки ничем не ограничивается, например
List<?>
; - Upper-bounded - с помощью ключевого слова
extends
, для типа класса при подстановке указывается верхняя граница, напримерList<? extends Class>
; - Lower-bounded - с помощью ключевого слова
super
, для типа класса при подстановке указывается нижняя границы, напримерList<? super Class>
.
Подробнее можно увидить на примере:
public class WildcardApp {
public static void main(String[] args) {
List<A> aList = new ArrayList<>();
List<B> bList = new ArrayList<>();
List<C> cList = new ArrayList<>();
// List<? extends A> a
Service.upper(aList);
Service.upper(bList);
Service.upper(cList);
// List<? super B> c
Service.lower(aList);
Service.lower(bList);
Service.lower(cList); // Compile Error - метод не может принять тип класса С
// List<?> list
Service.unbounded(aList);
Service.unbounded(bList);
Service.unbounded(cList);
Service.unbounded(new ArrayList<String>()); // Ошибки нет, метод может принять даже тип класса String
}
}
interface A { /* ... */ }
class B implements A { /* ... */ }
class C extends B { /* ... */ }
class Service {
public static void upper(List<? extends A> a) {
/*
Данный метод в качестве параметра может принимать
интерфейс А и все его реализации
*/
}
public static void lower(List<? super B> c) {
/*
Данный метод в качестве параметра может принимать
только класс B и его суперкласс
*/
}
public static void unbounded(List<?> list) {
/*
Данный метод в качестве параметра может принимать
любой тип
*/
}
}
Полиморфизм в Обобщениях по разному может работать с массивами и коллекциями.
Например, у нас есть следующая структура классов:
interface A { /* ... */ }
class B implements A { /* ... */ }
class C implements A { /* ... */ }
И мы создаем метод, который принимает массив типа А:
public static void changeArray(A[] array) {
array[0] = new B();
array[1] = new C();
}
При попытке передать в этот метод массив типа B, ошибки на уровне комптляции не возникнет, но после запуска программы появится ошибка времени выполнения. Т.к. мы пытаемся добавить в массив типа B
, элемент типа С
:
B[] arr = { new B(), new B() };
Service.changeArray(arr); // Runtime Error: в методе arrays попытка записать в массив типа B элемент типа С
В том случае, если мы создадим метод, который будет принимать в качесве параметра коллекцию типа A
, мы не сможем передать в этот метод коллекцию какого-либо другого типа, даже если этот тип будет наследником типа A
:
public static void concrete(List<A> a) {
/*
В этом методе мы указали в качестве параметра тип А.
Хотя этот интерфейс и имеет реализацию, передать в этот метод
в качестве параметра можно будет только коллекцию типа A.
*/
}
List<B> bList = new ArrayList<>();
Service.concrete(bList); // Compile Error: метод принимает коллекцию типа А, а мы пытаемся передать тип В
Решить проблему нам может помочь использование Wildcard.
Создадим метод, который будет принимать Uppder-Wildcard:
public static void upper(List<? extends A> a) {
/*
Этот метод в качестве параметра может принять коллекцию типа А
и всех его реализаций.
Но такую коллекцию нельзя будет изменить, а только просматривать.
*/
a.add(new B()); // Compile Error - изменить коллекцию нельзя
System.out.println(a.size());
}
List<B> bList = new ArrayList<>();
Service.upper(bList);
Что бы создать метод, который сможет принимать коллекцию и работать с ней, необходимо использовать Lower-Wildcard:
public static void lower(List<? super B> a) {
/*
Этот метод принимает класс B и его суперкласс.
Такую коллекцию можно будет редактировать
*/
a.add(new B());
System.out.println(a.size());
}
List<B> bList = new ArrayList<>();
Service.lower(bList); // 1