通配符泛型真的有必要吗?

13
例如:

举个例子:

    public String add(Set<?> t){
    ...;
    }



    public <T> String add(Set<T> t){
    ...;
    }

第一种使用通配符泛型;第二种是泛型方法的正常形式。 有什么区别呢?

在什么情况下需要使用通配符泛型,而不是泛型的正常形式?


这取决于你根据情况的具体实现。 - Braj
不,我们实际上从来不需要通配符。这只是为了可读性,以防止我们有一堆类型参数,每个参数仅使用一次。 - Dawood ibn Kareem
4
@DavidWallace: 我们绝不需要通配符是不正确的。例如,List<List<?>> 是一个列表,可以同时容纳其中不同类型的列表(您可以将 List<A>List<B> 添加到其中,但不能添加非 List 类型的元素)。除非使用原始类型,否则您无法表达这种情况。对于任何 EList<List<E>> 只能容纳一种类型的列表,即 List<E> - newacct
哇。经过深思熟虑,我同意了。@newacct,你是对的,我错了。我收回之前的评论。 - Dawood ibn Kareem
4个回答

12

这里有一个需要使用通配符的情况。该方法接受一个 List<List<?>>,它是一个列表的列表。该方法可以添加不同组件类型的列表:

public void foo(List<List<?>> t) {
    t.add(new ArrayList<String>());
    t.add(new ArrayList<Integer>());
}

如果不使用通配符,你无法使用普通类型参数来实现这一点。例如,以下代码将无法工作:

public <T> void foo(List<List<T>> t) {
    t.add(new ArrayList<String>()); // does not compile
    t.add(new ArrayList<Integer>()); // does not compile
}

在我看来,嵌套通配符应该有一个不同于 * 的符号,因为它们的含义是不同的。但是像 Class.forName 中顶层通配符的返回类型则是它们必要性的一个很好的例子。 - Paul Bellora

11
自从支持泛型后,使用未提供类型参数的参数化类型通常会导致编译器警告。另一方面,有些情况下您可能根本不关心类型参数是什么(即您在任何地方都不使用该类型),甚至更糟糕的是,您可能根本不知道 T 是什么,并且使用 使您可以表达这一点而不会导致编译器警告。
“不关心”情况的可能用例(为简洁起见非常简单,但您可以了解思路):
public void clearList(List<?> list) {
    list.clear();
}

“不知道”情况的示例:来自Class类的实际方法签名:

static Class<?> forName(String className);

这里的方法返回一个某种Class类型的对象。 Class是泛型的,但当然你不知道具体的类型,因为它取决于在运行时解析的className参数。因此,你不能在这里放置T,因为T在编译时(甚至针对特定的调用站点)是未知的,并且仅使用没有类型参数的Class会导致编译器警告,这是一种不好的做法。


但在最后一种情况下,您可以编写 static <T> Class<T> forName(String className),这样就可以了。实际上没有必要使用 <?> - Dawood ibn Kareem
6
@DavidWallace:这两个是完全不同的。Class<?> forName(String className); 返回一个未知参数的 Class<T> Class<T> forName(String className); 返回调用者想要的任何参数的 Class(而不知道该参数是什么)。显然,除非它始终返回 null,否则无法安全实现这一点。 - newacct

1
通配符形式是当您不介意处理哪些类型的对象时使用的。
泛型形式允许您对处理的对象类型添加约束。
一个示例用例可能是以下内容: 具有添加/更新/删除方法的通用存储库,您可以使用泛型类型定义通用行为:
public class Repository<T>{
 public void add(T t){...}
 public void update(T t){...}
 public void remove(T t){...}
}

然后,要为AppleBanana创建一个存储库,您只需要扩展此类并将T替换为实际类型:

public class AppleRepo extends Repository<Apple> {}
public class BananaRepo extends Repository<Banana> {}

如果通用存储库被声明为Repository<?>,那么它是不好的,因为它没有限制在香蕉上,并且您将无法在其中使用Banana特定方法而不需要转换对象;
此外,泛型允许您表达进一步的约束,例如:
Repository<T extends Fruit>

这允许您将通用仓库类限制为水果。并且您将能够在其代码中调用方法。


4
好的,但这并没有回答问题。问题是什么情况下会“需要”通配符;换句话说,我们是否可以使用通配符做一些其他语法无法完成的事情。 - Dawood ibn Kareem

0

调用这些方法没有区别。

在第二个方法(add(Set<T>))中,您可以创建类型为T的变量:

public <T> String add(Set<T> t){
    T item = t.iterator().next();
    //....
}

这可以为您提供一些额外的类型检查。在第一种方法中,您只能使用Object


3
好的,但这并没有回答问题。问题是什么情况下需要使用通配符;换句话说,有没有什么我们可以用通配符做的事情,其他语法不能实现。 - Dawood ibn Kareem

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