17

我读过 Joshua Bloch 写的很棒的“Effective 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>or 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);
    }

那么你能举一个例子,说明这种简化的签名是不可接受的吗?

PS 以及为什么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约束目的的答案。

4

3 回答 3

5

不同之处在于返回的类型,尤其是受inference的影响,因此该类型可能是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)
于 2013-06-22T21:22:56.273 回答
2

和...之间的不同

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 中还不够强大,因此实际上这两个调用都无法编译。不同之处在于,可以通过将类型参数指定为 in 来修复第一次调用Algorithms.<Point>min,而修复第二次调用则需要显式转换。

于 2013-06-22T21:05:59.337 回答
1

Not an easy one, but i'll try to be as specific as possible:

in T max(Collection<? extends T> coll) you could pass an argument like this List<Animal> or List<Cat> or List<Dog>, and in T wrongMax(Collection<T> xs) where T is Animal you can't pass as an Argument this List<Dog>, List<Cat> of course in Runtime you could add Cat or Dog objects in List<Animal> but in compilation time you wouldn't be able to pass a subclass of Animal in the Type of the List being passed as an argument in the wrongMax method, in the other hand, in the max method you could. Sorry for my english, i still learning it :), Regards.

于 2013-06-22T21:32:07.733 回答