为什么我们需要有界威尔卡<?在集合.max() 方法中扩展 T>答

2022-09-03 02:53:02

我读过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>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);
    }

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

附言:为什么在官方实施中会有这种情况?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");
}

当然,为超类及其子类重载方法不是很聪明,但它让我们看看实际上推断出的返回值类型(和以前一样)。pointsList<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


答案 1

区别在于返回的类型,特别是受推理的影响,因此该类型可能是可比较类型和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

如果您需要一个结果,无需推理,则可以显式键入对以下各项的调用:MiddleMiddle

 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)

答案 2

区别

T max(Collection<? extends T> coll)

T wrongMax(Collection<T> xs)

是第二个版本的返回类型与集合的元素类型完全相同,而在第一个版本中可以是元素类型的超类型。TT

第二个问题:确保它是一个类而不是一个接口的原因。T extends ObjectT


更新:稍微“自然”地演示了这种差异:假设您定义了这两种方法:

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

并像这样称呼第一个他们:

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

类型推理引擎可以推断出在第一次调用中,返回类型应该是,代码将编译。第二个调用将无法编译,因为对 的调用是不明确的。minPointhappy

不幸的是,至少在Java 7中,类型推理引擎还不够强大,所以实际上两个调用都无法编译。不同之处在于,第一个调用可以通过指定 type 参数来修复,如 中所示,而修复第二个调用需要显式强制转换。Algorithms.<Point>min


推荐