Что означает "Связанный с рекурсивным типом" в 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 сравнивает любые два Objects. С привязкой типа, компилятор может гарантировать, что только два объекта типа 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

Другие вопросы по тегам