Dlaczego potrzebujemy ograniczonej karty wilcard <? extends T> w kolekcji Collections.max ()

Czytałem niesamowitą „Skuteczną Javę” Joshua Blocha. Ale jeden przykład w książkach jest dla mnie niejasny. Został zaczerpnięty z rozdziału o lekach generycznych, dokładna pozycja to„Punkt 28: Użyj ograniczonych symboli wieloznacznych, aby zwiększyć elastyczność interfejsu API”.

W tym punkcie pokazano, jak napisać najbardziej uniwersalną i kuloodporną (w typie systemu punkt widzenia) wersję algorytmu wyboru elementu maksymalnego z kolekcji przy użyciu parametrów typu ograniczonego i ograniczonych typów wieloznacznych.

Ostateczna sygnatura napisanej metody statycznej wygląda następująco:

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

I w większości jest taki sam jak wCollections#max funkcja z biblioteki standardowej.

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

Rozumiem, dlaczego potrzebujemy ograniczonego znaku wieloznacznegoT extends Comparable<? super T> ograniczenie typu, ale czy jest to naprawdę konieczne w typie argumentu? Wydaje mi się, że będzie tak samo, jeśli tylko odejdziemyList<T> lubCollection<T>, prawda? Mam na myśli coś takiego:

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

Napisałem następujący głupi przykład używania obu podpisów i nie widzę żadnej różnicy:

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);
    }

Czy możesz zaproponować przykład, w którym taki uproszczony podpis będzie niedopuszczalny?

P.S. I dlaczego do cholery jestT extends Object w oficjalnej implementacji?

Odpowiedź

Cóż, dzięki @Bohemian udało mi się ustalić, jaka jest różnica między nimi.

Rozważ następujące dwie metody pomocnicze

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

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

Jasne, przeciążanie metody zarówno dla superklasy, jak i dla jej podklasy nie jest zbyt inteligentne, ale pozwala nam zobaczyć, jaki typ wartości zwracanej został faktycznie wywnioskowany (points jestList<ColoredPoint> jak wcześniej).

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

Dla obu metod wywnioskowano typColoredPoint.

Czasami chcesz mieć wyraźne informacje o typie przekazywanym do funkcji przeciążonej. Możesz to zrobić na kilka sposobów:

Możesz rzucać:

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

Nadal nie ma różnicy ...

Lub możesz powiedzieć kompilatorowi, jaki typ należy wywnioskować za pomocą składniclass.<type>method:

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

Aha! Oto odpowiedź.List<ColoredPoint> nie można przekazać do funkcji oczekiwanejCollection<Point> ponieważ typy generyczne nie są kowariantne (w przeciwieństwie do tablic), ale mogą być przekazywane do funkcji oczekiwanejCollection<? extends Point>.

Nie jestem pewien, gdzie lub kto może w takim przypadku użyć jawnego parametru typu, ale przynajmniej pokazuje, gdziewrongMin może być niewłaściwe.

Dzięki @erickson i @ tom-hawtin-tackline za odpowiedzi na temat celuT extends Object przymus.

questionAnswers(3)

yourAnswerToTheQuestion