泛型 - 什么用途?

3
JDK文档中的java.util.Stream接口,有以下代码片段作为Collector构造的示例。
Collector<Widget, ?, TreeSet<Widget>> intoSet =
         Collector.of(TreeSet::new, TreeSet::add,
                      (left, right) -> { left.addAll(right); return left; });

Collector.of方法的返回类型为static <T,R> Collector<T,R,R>

在示例代码的返回类型中,问号是否只是一种方便地引用下一个泛型类型的方式,因为这两个类型在方法签名中被声明为相同的。以下三个语句是否完全相同:

Collector<Widget, ?, TreeSet<Widget>> intoSet =
             Collector.of(TreeSet::new, TreeSet::add,
                          (left, right) -> { left.addAll(right); return left; });
Collector<Widget, TreeSet<Widget>, TreeSet<Widget>> intoSet =
             Collector.of(TreeSet::new, TreeSet::add,
                          (left, right) -> { left.addAll(right); return left; });
Collector<Widget, TreeSet<Widget>, ?> intoSet =
             Collector.of(TreeSet::new, TreeSet::add,
                          (left, right) -> { left.addAll(right); return left; });

我猜这里使用了?通配符,因为编译器能够推断所有的Collector类型变量,而不需要显式声明为TreeSet。这三个语句并不完全相同,但是如果意图是使用Collector执行Stream.collect(Collector),那么它们都可以正常工作。 - M A
3个回答

6

什么是wildcard?

在通用代码中,问号 (?) 被称为 wildcard,表示未知类型。通配符可以用于各种情况:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更好的编程实践是更具体地指定)。通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数。

为什么在Collector.of中使用它?

正如您已经注意到的,Collector.of(Supplier<R> supplier, BiConsumer<R, T> accumulator, BinaryOperator<R> combiner, Characteristics... characteristics) 返回一个Collector<T, R, R>类型的对象,而类Collector有三个类型参数:TAR,其中:

  • <T>:要缩减操作的输入元素的类型。
  • <A>:缩减操作的可变累加类型(通常作为实现细节隐藏)
  • <R>:缩减操作的结果类型

因此,如 javadoc 中所述,我们通常对类型参数 <A> 使用wildcard,因为它被认为是一个实现细节,因为它只是一个中间类型,真正重要的是输入和输出类型参数分别是TR,因此为了简单性/可读性,在这种情况下更倾向于使用 ? 而不是理论上应该使用的 TreeSet<Widget>


<?> 在这里是否被允许是因为这是实现细节,还是因为编译器可以猜测返回类型?如果<A>不是实现细节,并且作为Collector其中一个方法的返回类型而公开,是否会对编译器造成任何影响? - pawinder gupta
不,这对编译器没有任何影响。最终由于类型擦除,你将得到相同的字节码。 - Nicolas Filotto
感谢提及类型擦除,这解决了我的疑惑。 - pawinder gupta

1

在示例中,返回类型中的 ? 只是一种方便的方式,用于指代在方法签名中已声明为相同的下一个泛型类型。

不,? 与下一个泛型类型无关。在这个特定的示例中,我们知道 ? 恰好是 R。这被称为类型推断,并且是从 '=' 后面的语句中推断出来的。您肯定可以定义一个 Collector,在其中 ? 引用与最后一个泛型类型不同的类型。

以下三个语句是否完全相同:

是的,就 intoSet 的实际含义而言。 不是的,就 intoSet 如何进一步使用而言。

不要让语法困扰您。这只是类似于

String str = "hello";

vs.

Object str = "hello";

在这两种情况下,str都是运行时的字符串,这与其如何声明无关。但在第二种情况下,您不能将str用作字符串,编译器不允许,因为str仅被声明为Object。
泛型也是同样的道理。Java没有声明位置的变化,而是使用位置的变化。通配符“?”与Collector.of的定义无关,而是与您想要使用它的方式有关。
在这里,Collector<Widget,?,TreeSet<Widget>> intoSet = ...告诉您它是一个收集器,可以将Widget收集到TreeSet中。但是它如何将Widget收集到TreeSet中呢?更具体地说,它是直接将每个Widget放入集合中,还是使用中间累加器(例如Stack<Widget>),最终将结果转换为TreeSet?鉴于Collector接口被定义为Collection<T,A,R>,这两者都是可能的。然而,intoSet声明只是说“我不在乎,这完全取决于您如何初始化我”。
您甚至可以将其定义为Collector<?,?,?>,完全没问题。
至于区别,可以参考
Collector<Widget, TreeSet<Widget>, TreeSet<Widget>> intoSet =
             Collector.of(TreeSet::new, TreeSet::add,
                          (left, right) -> { left.addAll(right); return left; });
Collector<Widget, TreeSet<Widget>, ?> intoSet =
             Collector.of(TreeSet::new, TreeSet::add,
                          (left, right) -> { left.addAll(right); return left; });

这再次涉及到您想如何使用intoSet。大多数情况下,我们只关心输入和输出 - 输入是我们需要传递的类型,输出是我们最终将要利用的类型。这两种类型告诉了我们一个关键信息,即收集器将如何与代码的其余部分交互。因此,您可能需要/必须指定它们。 Collector<Widget, TreeSet<Widget>, ?> intoSet是合法的,但在实践中可能不太有用。这是因为当您想要使用intoSet时,您不知道结果容器的类型(因此您无法有意义地使用它)仅通过声明就无法确定
类似的例子:
 Map<?, ?> map = new HashMap<String, Integer>();
 map.put("a", 1); //compiler complains here when you try to use map, as map is declared as <?,?>, not <String, Integer>

1
在Java泛型中,“?”是一个纯通配符,可以匹配任何类型,而不考虑签名中的其他类型(也没有“下一个泛型类型”的语法)。
请记住,由于类型擦除,在签名中指定的类型仅用于编译时检查,而不用于运行时。这意味着这三个语句将完全相同地执行。它们之间唯一的区别在于你是否想要为不同的类型进行泛型(编译时)类型检查。
最后,请注意还有一个“:”。
static <T,A,R> Collector<T,A,R> of

方法,因此可能有各种不同类型。


静态方法 <T,A,R> Collector<T,A,R> of 可以接受不同数量的参数。与参数匹配的是静态方法 <T,R> Collector<T,R,R>。如果我将 intoset 类型声明为 Collector<Widget, ?, ?>,它将无法编译。 - pawinder gupta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接