Зачем нам нужен ограниченный подстановочный знак ‹? расширяет T› в методе Collections.max()

Я прочитал потрясающую книгу Джошуа Блоха «Эффективная Java». Но один пример в книгах остался для меня неясным. Это взято из главы о дженериках, точный пункт - "Пункт 28. Используйте ограниченные подстановочные знаки для повышения гибкости API".

В этом пункте показано, как написать наиболее универсальный и пуленепробиваемый (с точки зрения системы типов) вариант алгоритма выбора максимального элемента из коллекции с использованием параметров ограниченного типа и ограниченных подстановочных типов.

Окончательная сигнатура написанного статического метода выглядит так:

public static <T extends Comparable<? super T>> T max(List<? extends T> list)

И это в основном то же самое, что и функция Collections#max из стандартной библиотеки.

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) 

Я понимаю, почему нам нужен ограниченный подстановочный знак в ограничении типа T extends Comparable<? super T>, но действительно ли он необходим в типе аргумента? Мне кажется, будет то же самое, если оставить только List<T> или Collection<T>, не так ли? Я имею в виду что-то вроде этого:

public static <T extends Comparable<? super T>> T wrongMin(Collection<T> xs)

Я написал следующий глупый пример использования обеих подписей и не вижу никакой разницы:

public class Algorithms {
    public static class ColoredPoint extends Point {
        public final Color color;

        public ColoredPoint(int x, int y, Color color) {
            super(x, y);
            this.color = color;
        }
        @Override
        public String toString() {
            return String.format("ColoredPoint(x=%d, y=%d, color=%s)", x, y, color);
        }
    }

    public static class Point implements Comparable<Point> {
        public final int x, y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
        @Override
        public String toString() {
            return String.format("Point(x=%d, y=%d)", x, y);
        }
        @Override
        public int compareTo(Point p) {
            return x != p.x ? x - p.x : y - p.y;
        }
    }

    public static <T extends Comparable<? super T>> T min(Collection<? extends T> xs) {
        Iterator<? extends T> iter = xs.iterator();
        if (!iter.hasNext()) {
            throw new IllegalArgumentException("Collection is empty");
        }
        T minElem = iter.next();
        while (iter.hasNext()) {
            T elem = iter.next();
            if (elem.compareTo(minElem) < 0) {
                minElem = elem;
            }
        }
        return minElem;
    }

    public static <T extends Comparable<? super T>> T wrongMin(Collection<T> xs) {
        return min(xs);
    }

    public static void main(String[] args) {
        List<ColoredPoint> points = Arrays.asList(
                new ColoredPoint(1, 2, Color.BLACK),
                new ColoredPoint(0, 2, Color.BLUE),
                new ColoredPoint(0, -1, Color.RED)
        );
        Point p1 = wrongMin(points);
        Point p2 = min(points);
        System.out.println("Minimum element is " + p1);
    }

Можете ли вы предложить пример, когда такая упрощенная подпись будет неприемлема?

P.S. И на кой черт есть T extends Object в официальной реализации?

Отвечать

Что ж, благодаря @Bohemian мне удалось выяснить, в чем между ними разница.

Рассмотрим следующие два вспомогательных метода

private static void expectsPointOrColoredPoint(Point p) {
    System.out.println("Overloaded for Point");
}

private static void expectsPointOrColoredPoint(ColoredPoint p) {
    System.out.println("Overloaded for ColoredPoint");
}

Конечно, не очень разумно перегружать метод как для суперкласса, так и для его подкласса, но это позволяет нам увидеть, какой тип возвращаемого значения был фактически выведен (points равно List<ColoredPoint>, как и раньше).

expectsPointOrColoredPoint(min(points));     // print "Overloaded for ColoredPoint"
expectsPointOrColoredPoint(wrongMin(points)); // print "Overloaded for ColoredPoint"

Для обоих методов предполагаемый тип был ColoredPoint.

Иногда вам нужно явно указывать тип, передаваемый в перегруженную функцию. Вы можете сделать это несколькими способами:

Вы можете разыгрывать:

expectsPointOrColoredPoint((Point) min(points));     // print "Overloaded for Point"
expectsPointOrColoredPoint((Point) wrongMin(points)); // print "Overloaded for Point"

Все равно никакой разницы...

Или вы можете указать компилятору, какой тип следует вывести, используя синтаксис class.<type>method:

expectsPointOrColoredPoint(Algorithms.<Point>min(points));     // print "Overloaded for Point"
expectsPointOrColoredPoint(Algorithms.<Point>wrongMin(points)); // will not compile

Ага! Вот ответ. List<ColoredPoint> нельзя передать функции, ожидающей Collection<Point>, поскольку дженерики не являются ковариантными (в отличие от массивов), но их можно передать функции, ожидающей Collection<? extends Point>.

Я не уверен, где или кто может предпочесть использовать явный параметр типа в таком случае, но, по крайней мере, он показывает, где wrongMin может быть неуместным.

И спасибо @erickson и @tom-hawtin-tackline за ответы о цели ограничения T extends Object.


person east825    schedule 22.06.2013    source источник
comment
T extends Object & ... приводит к тому, что возвращаемый тип метода после стирания будет Object вместо Comparable. Это было необходимо для сохранения сигнатуры метода, чтобы код, скомпилированный для старого API, продолжал работать.   -  person erickson    schedule 23.06.2013
comment
Действительно аккуратная заметка. Спасибо!   -  person east825    schedule 23.06.2013
comment
Хорошее обновление. Спасибо за отличный вопрос!   -  person Paul Bellora    schedule 23.06.2013


Ответы (3)


Разница заключается в возвращаемом типе, на который особенно влияет вывод, при котором тип может быть типом, иерархически между типом Comparable и типом List. Позвольте мне привести пример:

class Top {
}
class Middle extends Top implements Comparable<Top> {
    @Override
    public int compareTo(Top o) {
        // 
    }
}
class Bottom extends Middle {
}

Используя предоставленную вами подпись:

public static <T extends Comparable<? super T>> T max(List<? extends T> list)

мы можем закодировать это без ошибок, предупреждений или (что важно) приведения:

List<Bottom> list;
Middle max = max(list); // T inferred to be Middle

И если вам нужен результат Middle без вывода, вы можете явно ввести вызов Middle:

 Comparable<Top> max = MyClass.<Middle>max(list); // No cast

или перейти к методу, который принимает Middle (где вывод не будет работать)

someGenericMethodThatExpectsGenericBoundedToMiddle(MyClass.<Middle>max(list));

Я не знаю, поможет ли это, но чтобы проиллюстрировать допустимые/выведенные компилятором типы, сигнатура будет выглядеть так (конечно, это не компилируется):

public static <Middle extends Comparable<Top>> Middle max(List<Bottom> list)
person Bohemian♦    schedule 22.06.2013
comment
С другой реализацией также не было бы ошибок или предупреждений, поскольку T предполагалось бы как Bottom, и нет проблем с назначением Bottom на Middle, потому что это подтип - person Joni; 23.06.2013
comment
@Joni Нет, T подразумевается как Middle, а список имеет тип ? extends T, поэтому List<Bottom> в порядке. См. редактирование в конце, если это поможет. - person Bohemian♦; 23.06.2013
comment
Мы сравниваем две разные декларации. Я имею в виду тот, который вы не упомянули в ответе, из которого T следует Bottom. - person Joni; 23.06.2013
comment
@Joni, но этот метод был дан только для сравнения, но ладно - person Bohemian♦; 23.06.2013
comment
Как сказал @Joni, результаты обоих вызовов либо max, либо wrongMax с переданным List<Bottom> могут быть присвоены переменной типа Middle. Даже если wrongMax выведет тип Bottom как возвращаемый тип, его все равно можно неявно понизить до Middle без каких-либо проблем. - person east825; 23.06.2013
comment
@ east825 Но вам не нужны приведения с этой подписью. Слепки уродливы и (как правило) небезопасны. См. правки ближе к концу, касающиеся приведения - person Bohemian♦; 23.06.2013
comment
@Bohemian Я написал неявно, конечно, без приведения типов. Я закодировал ваш пример (на самом деле он более понятен, чем мой, спасибо), и, как я уже сказал, результат обоих вызовов может быть назначен переменной типа Middle без каких-либо жалоб со стороны компилятора (как и ожидалось на самом деле). Проверьте это: gist.github.com/east825/5842803. - person east825; 23.06.2013
comment
@east825 east825 Я добавил еще один пример, когда логического вывода недостаточно. - person Bohemian♦; 23.06.2013
comment
@Bohemian О, теперь я вижу: это компилируется: Comparable<Top> result = Algorithms.<Middle>min(bottoms), и это не Comparable<Top> result2 = Algorithms.<Middle>wrongMin(bottoms), потому что Collection<Bottom> не является ковариантным для Collection<Middle>. Однако, как писал @Joni, я не могу представить себе ни одного случая, когда необходим такой явный параметр типа для универсального метода, возвращающего значение самого типа T, а не какой-либо общий тип, параметризованный из T (например, Colections.emptyList()). - person east825; 23.06.2013
comment
@Bohemian относительно someMethodThatExpectsMiddle. Он должен принимать значение типа Bottom , поэтому он также будет принимать значение, возвращаемое wrongMin(list) - нет необходимости в явном параметре типа, только если он также перегружен для Bottom (что, безусловно, является плохой практикой). Во всяком случае, теперь я вижу, в чем смысл, спасибо. - person east825; 23.06.2013
comment
Я не понимаю, что ты говоришь. С public static <T extends Comparable<? super T>> T max(List<T> list) и List<Bottom> list; Middle max = max(list) это будет работать без ошибок, предупреждений или приведений. - person newacct; 24.06.2013
comment
Мне кажется, или javac лучше выводит типы, которые предотвращают ошибки, чем раньше? - person Andy; 01.11.2017
comment
В прошлом я определенно видел случаи, когда инкрементный компилятор Java Eclipse принимает сумасшедшие дженерики, но javac нет... - person Andy; 01.11.2017

Разница между

T max(Collection<? extends T> coll)

а также

T wrongMax(Collection<T> xs)

заключается в том, что возвращаемый тип второй версии точно такой же, как тип элемента коллекции T, тогда как в первой версии T может быть супертипом типа элемента.

Второй вопрос: причина T extends Object гарантирует, что T является классом, а не интерфейсом.


Обновление: чуть более "естественная" демонстрация разницы: предположим, вы определили эти два метода:

static void happy(ColoredPoint p, Point q) {}
static void happy(Point p, ColoredPoint q) {}

И назовите первый из них так:

happy(coloredPoint, min(points));
happy(coloredPoint, wrongMin(points));

Механизм вывода типов может сделать вывод, что при первом вызове возвращаемый тип min должен быть Point, и код будет скомпилирован. Второй вызов не сможет скомпилироваться, поскольку вызов happy неоднозначен.

К сожалению, механизм вывода типов недостаточно мощный, по крайней мере, в Java 7, поэтому на самом деле оба вызова не компилируются. Разница в том, что первый вызов можно исправить, указав параметр типа, как в Algorithms.<Point>min, а для исправления второго вызова потребуется явное приведение.

person Joni    schedule 22.06.2013
comment
в то время как в первой версии это может быть супер тип T Вы уверены в употреблении слова супер здесь? - person Luiggi Mendoza; 23.06.2013
comment
T extends Object на самом деле бессмысленно, обратите внимание, что в конце вы передаете не интерфейс, а ссылку на объект, который может реализовать интерфейс, поэтому каждый T всегда будет Object в конце. - person Luiggi Mendoza; 23.06.2013
comment
@LuiggiMendoza Это не полностью бессмысленно - что касается отражения, супертип - это первый тип, упомянутый на пересечении. - person Bohemian♦; 23.06.2013
comment
Причина, по которой T extends Object & необходима, заключается в совместимости двоичных файлов (и необработанных типов). max возвращает необработанный тип Object, а не Comparable (что обычно не было бы полезно в мире до версии 1.5). - person Tom Hawtin - tackline; 23.06.2013
comment
@LuiggiMendoza упс, не супер тип T. Исправлено. - person Joni; 23.06.2013
comment
Итак, на первый вопрос, по сути, вы говорите, что не знаете никакой разницы? - person newacct; 24.06.2013
comment
@newacct, я говорю, что разница очевидна, но я не мог придумать практической демонстрации. Bohemian нашел один: Algorithms.<Point>wrongMax не компилируется, а Algorithms.<Point>max компилируется. - person Joni; 24.06.2013
comment
@Joni: Но это потому, что вы явно указываете неправильный параметр. Существует правильный выбор T. Algorithms.<ColoredPoint>wrongMax будет компилироваться. Дело в том, что в любое время, когда есть допустимый выбор T для max, есть правильный выбор T для wrongMax. - person newacct; 25.06.2013
comment
Вы можете доказать это математически? Думаю, я нашел контрпример; смотрите обновление. - person Joni; 25.06.2013

Не простой, но я постараюсь быть как можно более конкретным:

в T max(Collection<? extends T> coll) вы можете передать аргумент, подобный этому List<Animal> or List<Cat> or List<Dog>, а в T wrongMax(Collection<T> xs), где T — это животное, вы не можете передать в качестве аргумента это List<Dog>, List<Cat>, конечно, во время выполнения вы можете добавить объекты Cat или Dog в List<Animal>, но во время компиляции вы не будете можно передать подкласс Animal в типе списка, передаваемого в качестве аргумента в методе wrongMax, с другой стороны, в методе max вы могли бы. Извините за мой английский, я все еще учу его :), С уважением.

person jac1013    schedule 22.06.2013
comment
На самом деле, в этом конкретном случае благодаря выводу типов универсальных методов любой из них (List<Animal>, List<Cat> или List<Dog>) может быть успешно передан в wronMax, если их базовый класс Animal реализует Comparable<Animal> или даже более общий Comparable<Object>. - person east825; 23.06.2013
comment
@east825 ты уверен в этом? дженерики не являются ковариантными. а речь идет о типе на Коллекции. - person jac1013; 23.06.2013
comment
Да, читайте внимательнее: хотя в wrongMax тип его аргумента объявлен как Collection<T>, все равно есть дополнительное ограничение перед методом T extends Comparable<? super T>. Это означает, что коллекция любых элементов, которые реализует этот класс Comparable<? super T> напрямую или через подклассы, может быть передана. - person east825; 23.06.2013
comment
Я подумал, что в данном случае <? extends T> тип аргумента стал излишним (прочитайте его как класс, который расширяет что-то, что расширяет что-то, что реализует Comparable). Вот почему я задал вопрос. - person east825; 23.06.2013
comment
не знаю, говорим ли мы об одном и том же, но когда мы определяем T extends Comparable<? super T> перед возвращаемым типом, все равно List<Dog> не будет передаваться в качестве аргумента, если аргумент определен как Collection<T>, потому что ? super T означает любой супертип T с T включительно, Dog Comparable да, но это Comparable, потому что это подкласс Animal и он не пройдет ? super T - person jac1013; 23.06.2013
comment
@ user1322142: Вы ошибаетесь. Если вы передаете List<Dog>, то работает T = Dog. - person newacct; 24.06.2013
comment
Да, я понимаю, вы правы @east825, спасибо за уточнение ответа. - person jac1013; 24.06.2013