【问题标题】:Why do we need bounded wilcard <? extends T> in Collections.max() method为什么我们需要有界通配符 <?在 Collections.max() 方法中扩展 T>
【发布时间】:2013-06-22 20:54:19
【问题描述】:

我读过 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&lt;? super T&gt; 类型约束中需要有界通配符,但是在参数的类型中真的有必要吗?在我看来,如果我们只留下List&lt;T&gt;Collection&lt;T&gt;,那将是一样的,不是吗?我的意思是这样的:

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

当然,对超类及其子类都重载方法不是很聪明,但它让我们看看实际推断出哪种类型的返回值(points 和以前一样是List&lt;ColoredPoint&gt;)。

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.&lt;type&gt;method

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

啊哈!这是答案。 List&lt;ColoredPoint&gt; 不能传递给期望 Collection&lt;Point&gt; 的函数,因为泛型不是协变的(与数组不同),但可以传递给期望 Collection&lt;? extends Point&gt; 的函数。

我不确定在这种情况下在哪里或谁可能更喜欢使用显式类型参数,但至少它显示了wrongMin 可能不合适的地方。

感谢@erickson 和@tom-hawtin-tackline 就T extends Object 约束的目的提供答案。

【问题讨论】:

  • T extends Object &amp; ... 导致擦除后方法的返回类型为Object 而不是Comparable。这对于保留方法签名是必要的,这样针对旧 API 编译的代码仍然可以运行。
  • 非常整洁的笔记。谢谢!
  • 不错的更新。感谢您提出一个很好的问题!

标签: java generics effective-java


【解决方案1】:

不同之处在于返回的类型,尤其是受推理的影响,因此该类型可能是 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)

【讨论】:

  • 使用其他实现也不会出现错误或警告,因为T 将被推断为Bottom,并且将Bottom 分配给Middle 没有问题,因为它是亚型
  • @Joni 不,T 被推断为Middle,并且列表的类型为? extends T,所以List&lt;Bottom&gt; 是可以的。如果有帮助,请参阅最后的编辑。
  • 我们正在比较两个不同的声明。我指的是您在答案中未提及的那个,它将推断TBottom
  • @Joni 但该方法仅用于比较,但可以
  • 正如@Joni 所说,通过List&lt;Bottom&gt; 调用maxwrongMax 的结果可以分配给Middle 类型的变量。即使wrongMax 将类型Bottom 推断为返回类型,它仍然可以隐式向下转换为Middle 而不会出现任何问题。
【解决方案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 中还不够强大,因此实际上这两个调用都无法编译。不同之处在于,第一次调用可以通过指定类型参数来修复,如Algorithms.&lt;Point&gt;min,而修复第二次调用需要显式转换。

【讨论】:

  • 虽然在第一个版本中它可以是T的超级类型你确定这里的super这个词的用法吗?
  • T extends Object 真的没有意义,注意最后你传递的不是接口而是可以实现接口的对象引用,所以每个T都会总是 最终成为Object
  • @LuiggiMendoza 并不是完全没有意义——就反射而言,超类型是交集中提到的first类型。跨度>
  • 之所以需要T extends Object &amp; 是因为二进制(和原始类型)兼容性。 max 返回 Object 而不是 Comparable 的原始类型(这在 1.5 之前的世界中通常没有用处)。
  • @LuiggiMendoza 哎呀,不是“超级类型的 T”。固定。
【解决方案3】:

这不是一件容易的事,但我会尽量具体:

T max(Collection&lt;? extends T&gt; coll) 你可以传递这样的参数List&lt;Animal&gt; or List&lt;Cat&gt; or List&lt;Dog&gt;, 在T wrongMax(Collection&lt;T&gt; xs) 其中 T 是 Animal 你不能作为参数传递 this List&lt;Dog&gt;, List&lt;Cat&gt; 当然在运行时你可以在List&lt;Animal&gt; 中添加 Cat 或 Dog 对象,但在编译时你将无法在列表的类型中传递 Animal 的子类作为参数在 wrongMax 中传递另一方面,在 max 方法中你可以。对不起我的英语,我还在学习它:),问候。

【讨论】:

  • 实际上在这种特殊情况下,由于泛型方法的类型推断,它们中的任何一个(List&lt;Animal&gt;List&lt;Cat&gt;List&lt;Dog&gt;)都可以成功传递给wronMax,如果它们的基类@987654332 @实现Comparable&lt;Animal&gt;或更通用的Comparable&lt;Object&gt;
  • @east825 你确定吗?泛型不是协变的。我们正在讨论 Collection 上的类型。
  • 是的,请仔细阅读:即使在wrongMax 中其参数的类型被声明为Collection&lt;T&gt;,仍然存在附加约束 在方法 T extends Comparable&lt;? super T&gt; 前面。这意味着可以传入这些类直接或通过子类实现Comparable&lt;? super T&gt; 的任何元素的集合。
  • 我认为在这种情况下 &lt;? extends T&gt; 它的参数类型变得多余(将其读作 class,它扩展了一些扩展了实现 Comparable 的东西)。这就是我问这个问题的原因。
  • 不知道我们说的是不是同一件事,但是当我们在返回类型之前定义T extends Comparable&lt;? super T&gt; 时,如果参数定义为@,List&lt;Dog&gt; 将不会作为参数传递987654343@ 因为? super T 表示 T 的任何超类型,包含 T,DogComparable 是的,但它是 Comparable 因为它是 Animal 的子类,它不会通过 ? super T
猜你喜欢
  • 1970-01-01
  • 2018-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-29
相关资源
最近更新 更多