Что означает "Связанный с рекурсивным типом" в Generics?
Я читаю главу "Обобщения с эффективной Java" [пункт 27].
В книге есть этот параграф:
Допустимо, хотя и относительно редко, чтобы параметр типа был ограничен каким-либо выражением, включающим сам параметр этого типа. Это то, что известно как рекурсивная привязка типа.
и это:
// Using a recursive type bound to express mutual comparability
public static <T extends Comparable<T>> T max(List<T> list) {...}
Что такое рекурсивная привязка типов и как приведенный выше фрагмент кода помогает достичь взаимной сопоставимости?
2 ответа
Что такое рекурсивный тип
Это: <T extends Comparable<T>>
Обратите внимание, что параметр типа T
также является частью подписи суперинтерфейса Comparable<T>
,
и как приведенный выше фрагмент кода помогает достичь взаимной сопоставимости?
Это гарантирует, что вы можете сравнивать только объекты типа T
, Без привязки типа, Comparable
сравнивает любые два Object
s. С привязкой типа, компилятор может гарантировать, что только два объекта типа T
сравниваются.
Чтобы понять концепцию рекурсивных границ типов, давайте решим простую задачу. Эту концепцию легче понять, решив реальную проблему. Я дам определение привязанного рекурсивного типа в конце, потому что оно становится более понятным после понимания концепции.
Проблема
Предположим, что нам нужно отсортировать фрукты по размеру. Нам говорят, что сравнивать можно только фрукты одного и того же типа. Например, мы не можем сравнивать яблоки с апельсинами (каламбур).
Итак, мы создаем простую иерархию типов, как показано ниже,
Fruit.java
interface Fruit {
Integer getSize();
}
Apple.java
class Apple implements Fruit, Comparable<Apple> {
private final Integer size;
public Apple(Integer size) {
this.size = size;
}
@Override public Integer getSize() {
return size;
}
@Override public int compareTo(Apple other) {
return size.compareTo(other.size);
}
}
Orange.java
class Orange implements Fruit, Comparable<Orange> {
private final Integer size;
public Orange(Integer size) {
this.size = size;
}
@Override public Integer getSize() {
return size;
}
@Override public int compareTo(Orange other) {
return size.compareTo(other.size);
}
}
Main.java
class Main {
public static void main(String[] args) {
Apple apple1 = new Apple(3);
Apple apple2 = new Apple(4);
apple1.compareTo(apple2);
Orange orange1 = new Orange(3);
Orange orange2 = new Orange(4);
orange1.compareTo(orange2);
apple1.compareTo(orange1); // Error: different types
}
}
Решение
В этом коде мы можем достичь нашей цели - сравнить одни и те же типы, то есть яблоки с яблоками и апельсины с апельсинами. Когда мы сравниваем яблоко с апельсином, мы получаем ошибку, которая нам и нужна.
Проблема
Проблема здесь в том, что код для реализации
compareTo()
метод дублируется для
Apple
и
Orange
класс. И будет дублироваться больше во всех классах, которые мы расширяем от
Fruit
, для создания новых плодов в будущем. Количество повторяющегося кода в нашем примере меньше, но в реальном мире повторяющийся код может состоять из сотен строк в каждом классе.
Перемещение повторяющегося кода в общий класс
Fruit.java
class Fruit implements Comparable<Fruit> {
private final Integer size;
public Fruit(Integer size) {
this.size = size;
}
public Integer getSize() {
return size;
}
@Override public int compareTo(Fruit other) {
return size.compareTo(other.getSize());
}
}
Apple.java
class Apple extends Fruit {
public Apple(Integer size) {
super(size);
}
}
Orange.java
class Orange extends Fruit {
public Orange(Integer size) {
super(size);
}
}
Решение
На этом этапе мы избавляемся от повторяющегося кода
compareTo()
метод, переместив его в суперкласс. Наши расширенные классы
Apple
и
Orange
больше не загрязняются общим кодом.
Проблема
Проблема здесь в том, что теперь мы можем сравнивать разные типы, сравнение яблок с апельсинами больше не дает нам ошибки:
apple1.compareTo(orange1); // No error
Введение в параметр типа
Fruit.java
class Fruit<T> implements Comparable<T> {
private final Integer size;
public Fruit(Integer size) {
this.size = size;
}
public Integer getSize() {
return size;
}
@Override public int compareTo(T other) {
return size.compareTo(other.getSize()); // Error: getSize() not available.
}
}
Apple.java
class Apple extends Fruit<Apple> {
public Apple(Integer size) {
super(size);
}
}
Orange.java
class Orange extends Fruit<Orange> {
public Orange(Integer size) {
super(size);
}
}
Решение
Чтобы ограничить сравнение разных типов, мы вводим параметр типа
T
. Так что сопоставимые
Fruit<Apple>
нельзя сравнивать с сопоставимыми
Fruit<Orange>
. Обратите внимание на наши
Apple
и
Orange
классы; теперь они наследуются от типов
Fruit<Apple>
и
Fruit<Orange>
соответственно. Теперь, если мы попытаемся сравнить разные типы, IDE покажет ошибку, наше желаемое поведение:
apple1.compareTo(orange1); // Error: different types
Проблема
Но на этом этапе наши
Fruit
класс не компилируется. В
getSize()
метод
T
компилятору неизвестно. Это потому, что параметр типа
T
из нашегоFruit
класс не имеет никаких ограничений. Так что
T
может быть любым классом, невозможно, чтобы каждый класс имел
getSize()
метод. Итак, компилятор прав, не распознав
getSize()
метод
T
.
Введение в рекурсивную привязку типов
Fruit.java
class Fruit<T extends Fruit<T>> implements Comparable<T> {
private final Integer size;
public Fruit(Integer size) {
this.size = size;
}
public Integer getSize() {
return size;
}
@Override public int compareTo(T other) {
return size.compareTo(other.getSize()); // Now getSize() is available.
}
}
Apple.java
class Apple extends Fruit<Apple> {
public Apple(Integer size) {
super(size);
}
}
Orange.java
class Orange extends Fruit<Orange> {
public Orange(Integer size) {
super(size);
}
}
Окончательное решение
Итак, мы говорим компилятору, что наш
T
это подтип
Fruit
. Другими словами, мы указываем верхнюю границу
T extends Fruit<T>
. Это гарантирует, что только подтипы
Fruit
разрешены как аргументы типа. Теперь компилятор знает, что
getSize()
метод можно найти в подтипе
Fruit
класс (Apple
,
Orange
и т. д.), потому что
Comparable<T>
также получает наш тип (Fruit<T>
), который содержит
getSize()
метод.
Это позволяет нам избавиться от повторяющегося кода
compareTo()
метод, а также позволяет сравнивать однотипные плоды, яблоки с яблоками и апельсины с апельсинами.
Теперь
compareTo()
метод может использоваться внутри
max()
функция, указанная в вопросе.
Определение границы рекурсивного типа
В дженериках, когда ссылочный тип имеет параметр типа, который ограничен самим ссылочным типом, то говорят, что этот параметр типа имеет привязанный рекурсивный тип.
В нашем примере универсальный тип
Fruit<T extends Fruit<T>>
,
Fruit
это наш ссылочный тип, его параметр типа
T
ограничен
Fruit
сам, поэтому параметр типа
T
имеет рекурсивную привязку типа
Fruit<T>
.
Рекурсивный тип - это тот, который включает функцию, которая использует сам этот тип как тип для некоторого аргумента или его возвращаемого значения. В нашем примере
compareTo(T other)
- функция рекурсивного типа, которая принимает тот же рекурсивный тип в качестве аргумента.
Предостережение
В этом шаблоне есть предостережение. Компилятор не мешает нам создать класс с аргументом типа другого подтипа:
class Orange extends Fruit<Orange> {...}
class Apple extends Fruit<Orange> {...} // No error
Примечание в
Apple
класс выше, по ошибке мы прошли
Orange
вместо
Apple
как аргумент типа. Это приводит к
compareTo(T other)
способ взять
Orange
вместо
Apple
. Теперь мы больше не получаем ошибку при сравнении разных типов и внезапно не можем сравнивать яблоки с яблоками:
apple1.compareTo(apple2); // Error
apple1.compareTo(orange1); // No error
Итак, разработчик должен быть осторожен при расширении классов.
Это оно! Надеюсь, это поможет.
В разделе часто задаваемых вопросов по Java Generics написана статья Анджелики Лангер, в которой объясняются подробности такой декларации: http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html