何时使用通用方法,何时使用通配符?

160

我正在阅读来自OracleDocGenericMethod的通用方法,但是当文档中提到何时使用通配符和何时使用通用方法进行比较时,我感到相当困惑。

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

We could have used generic methods here instead:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

[…] This tells us that the type argument is being used for polymorphism; its only effect is to allow a variety of actual argument types to be used at different invocation sites. If that is the case, one should use wildcards. Wildcards are designed to support flexible subtyping, which is what we're trying to express here.

我们是否认为像 (Collection<? extends E> c); 这样的通配符也支持多态性呢?那么为什么泛型方法的使用被认为不好呢?

接下来,文中进一步说明:

泛型方法允许类型参数用于表示一个或多个参数及/或其返回类型之间的依赖关系。如果没有这样的依赖关系,则不应使用泛型方法。

这是什么意思?

他们提供了以下示例:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

[…]

We could have written the signature for this method another way, without using wildcards at all:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

这篇文档不鼓励使用第二种声明方式,而是推荐使用第一种语法?第一种和第二种声明有什么区别?它们似乎都在做同样的事情?

能有人解释一下吗?

9个回答

226
有一些地方通配符和类型参数可以做相同的事情。但也有一些地方你必须使用类型参数。
如果你想要在不同类型的方法参数之间强制建立某种关系,你不能使用通配符,必须使用类型参数。
以你的方法为例,假设你想要确保传递给copy()方法的src和dest列表应该是相同参数化类型,你可以使用类型参数来实现:
public static <T extends Number> void copy(List<T> dest, List<T> src)

在这里,你可以确保destsrc都具有相同的参数化类型List。因此,从src复制元素到dest是安全的。
但是,如果你继续改变方法使用通配符:
public static void copy(List<? extends Number> dest, List<? extends Number> src)

它不会按预期工作。在第二种情况下,您可以将List<Integer>List<Float>作为destsrc传递。因此,从src移动元素到dest将不再是类型安全的。 如果您不需要这种关系,那么您可以完全不使用类型参数。

使用通配符和类型参数的其他区别包括:

  • 如果你只有一个参数化类型的参数,你可以使用通配符,尽管类型参数也可以工作。

  • 类型参数支持多个边界,通配符不支持。

  • 通配符支持上界和下界,类型参数只支持上界。所以,如果你想定义一个接受List类型为Integer或其超类的方法,你可以这样做:

     public void print(List<? super Integer> list)  // OK
    
但是你不能使用类型参数:
     public <T super Integer> void print(List<T> list)  // Won't compile

参考资料:


5
这是一个奇怪的回答。它没有解释为什么你需要使用 ?。你可以将其重写为 public static <T1 extends Number, T2 extends Number> void copy(List<T1> dest, List<T2> src),在这种情况下,就会变得很明显发生了什么。 - kan
1
@benz。你不能使用类型参数在List中定义下限。List<T super Integer>是无效的,也不会编译。 - Rohit Jain
2
@benz。不用谢 :) 我强烈建议你仔细阅读我在结尾处发布的链接。那是你能找到的最好的关于泛型的资源。 - Rohit Jain
6
<T extends X & Y>表示多重限定(multiple bounds),即T必须是X和Y的子类。 - Rohit Jain
1
那么为什么Java被定义成不能使用super关键字作为类型参数呢? - user3509406
显示剩余7条评论

32
考虑以下来自James Gosling第4版《Java编程》的示例,我们要合并2个SinglyLinkQueue:
public static <T1, T2 extends T1> void merge(SinglyLinkQueue<T1> d, SinglyLinkQueue<T2> s){
    // merge s element into d
}

public static <T> void merge(SinglyLinkQueue<T> d, SinglyLinkQueue<? extends T> s){
        // merge s element into d
}

以上两种方法的功能相同。那么哪一个更好呢?答案是第二个。按照作者自己的话来说:

"一般规则是尽可能使用通配符,因为带有通配符的代码通常比带有多个类型参数的代码更易读。在决定是否需要类型变量时,请问自己该类型变量是否用于关联两个或多个参数,或者将参数类型与返回类型相关联。如果答案是否定的,则应该使用通配符。"

注意:书中仅提供了第二种方法,并且类型参数名称为S而不是'T'。书中没有第一种方法。


2
我为一本书中的引语点赞,它直接而简洁。 - TomasMolina

12

在你的第一个问题中:它意味着如果参数类型和方法返回类型之间存在关系,则使用泛型。

例如:

public <T> T giveMeMaximum(Collection<T> items);
public <T> Collection<T> applyFilter(Collection<T> items);

在这里,您正在根据某些标准提取T的一部分。如果T是Long,则您的方法将返回LongCollection<Long>; 实际返回类型取决于参数类型,因此使用泛型类型是有用的并且是建议的。

当不是这种情况时,您可以使用通配符类型:

public int count(Collection<?> items);
public boolean containsDuplicate(Collection<?> items);

在这两个例子中,无论集合中的项的类型是什么,返回类型都将是intboolean
在您的示例中:
interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

这两个函数将返回一个布尔值,无论集合中的项目类型是什么。在第二种情况下,它仅限于E的子类实例。

第二个问题:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

这段代码可以让您将一个异构的List<? extends T> src作为参数传递。只要它们都扩展于基类T,此列表可以包含多个不同类别的元素。

如果您有:

interface Fruit{}

并且

class Apple implements Fruit{}
class Pear implements Fruit{}
class Tomato implements Fruit{}

您可以做到

List<? extends Fruit> basket = new ArrayList<? extends Fruit>();
basket.add(new Apple());
basket.add(new Pear());
basket.add(new Tomato());
List<Fruit> fridge = new ArrayList<Fruit>(); 

Collections.copy(fridge, basket);// works 

另一方面

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

List<S> src 限制为 T 的子类 S 中的一个特定类。该列表只能包含一个类(在此情况下为 S)的元素,而不是其他类,即使它们也实现了 T。您不能使用之前的示例,但可以执行以下操作:

List<Apple> basket = new ArrayList<Apple>();
basket.add(new Apple());
basket.add(new Apple());
basket.add(new Apple());
List<Fruit> fridge = new ArrayList<Fruit>();

Collections.copy(fridge, basket); /* works since the basket is defined as a List of apples and not a list of some fruits. */

2
List<? extends Fruit> basket = new ArrayList<? extends Fruit>(); 不是有效的语法。你必须实例化没有边界的ArrayList。 - Arnold Pistorius
在上面的例子中无法将苹果添加到篮子中,因为篮子可能是梨子的列表。据我所知,这个例子是错误的,并且也无法编译。 - Khanna111
1
@ArnoldPistorius,这让我感到困惑。我查看了ArrayList的API文档,它有一个签名为ArrayList(Collection<? extends E> c)的构造函数。你能解释一下你为什么这样说吗? - TomasMolina
@Kurapika 可能是我使用的 Java 版本太旧了?该评论发布于近3年前。 - Arnold Pistorius

4

通配符方法也是一般化的-您可以使用某些类型范围调用它。

<T>语法定义了一个类型变量名称。如果类型变量有任何用途(例如在方法实现中或作为其他类型的约束),则对其进行命名是有意义的,否则可以使用?作为匿名变量。因此,看起来只是一种简便方式。

此外,在声明字段时,?语法是不可避免的:

class NumberContainer
{
 Set<? extends Number> numbers;
}

3
这不应该是一个评论吗? - Buhake Sindi
2
@BuhakeSindi 对不起,有什么不清楚的吗?为什么是-1?我认为它回答了这个问题。 - kan

3
我会逐一回答你的问题。
我们不认为 Collection 这样的通配符支持多态。原因是有界通配符没有定义参数类型,它是未知的。它“知道”的只是“包含”一个类型为 E 的元素(无论如何定义)。所以它无法验证和证明所提供的值是否匹配有界类型。
因此,在通配符上实现多态行为是没有意义的。
在这种情况下,文档不鼓励第二种声明方式,而推荐使用第一种语法。第一种选项更好,因为 T 总是有界的,而 source 肯定会有子类 T 的值(未知值)。
所以,假设您想要复制所有数字列表,第一种选项将是:
Collections.copy(List<Number> dest, List<? extends Number> src);

src基本上可以接受List<Double>List<Float>等,因为在dest中找到了参数化类型的上限。

第二个选项将强制你绑定每种要复制的类型的S,如下所示

//For double 
Collections.copy(List<Number> dest, List<Double> src); //Double extends Number.

//For int
Collections.copy(List<Number> dest, List<Integer> src); //Integer extends Number.

作为一个需要绑定的参数化类型,S。希望这有所帮助。

1
你能解释一下你在最后一段中的意思吗? - benz
1
第二个选项会强制你绑定一个......你能详细说明一下吗? - benz
<S extends T> 表示 S 是一个参数化类型,它是 T 的子类,因此需要一个参数化类型(无通配符),它是 T 的子类。 - Buhake Sindi

3

? 表示未知

一般规则适用: 您可以从中读取,但不能写入

给定简单的POJO汽车

class Car {
    void display(){

    }
}

这将会编译。

private static <T extends Car> void addExtractedAgain1(List<T> cars) {
    T t = cars.get(1);
    t.display();
    cars.add(t);
}

这个方法无法编译。
private static void addExtractedAgain2(List<? extends Car> cars) {
    Car car = cars.get(1);
    car.display();
    cars.add(car); // will not compile
}

另一个例子
List<?> hi = Arrays.asList("Hi", new Exception(), 0);

hi.forEach(o -> {
   o.toString() // it's ok to call Object methods and methods that don't need the contained type
});

hi.add(...) // nothing can be add here won't compile, we need to tell compiler what the data type is but we do not know

2
这里没有列出的另一个区别是,HTML标签需要保留。
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // correct
    }
}

但是以下代码将导致编译时错误。
static <T> void fromArrayToCollection(T[] a, Collection<?> c) {
    for (T o : a) {
        c.add(o); // compile time error
    }
}

0
据我所知,只有一个使用情况需要通配符(即可以表达一些无法使用显式类型参数表达的内容),那就是当您需要指定下限时。
除此之外,通配符还可以用于编写更简洁的代码,正如您提到的文档中所描述的那样:
“通用方法允许使用类型参数来表示一个或多个参数的类型和/或其返回类型之间的依赖关系。如果没有这样的依赖关系,则不应使用通用方法。”
“使用通配符比声明显式类型参数更清晰、更简洁,因此应尽可能使用。”
“通配符还具有一个优点,就是它们可以用于方法签名之外,例如字段、局部变量和数组的类型。”

0
主要 -> 通配符在非泛型方法的参数/参数级别上强制执行泛型。 注意。它也可以在默认情况下在genericMethod中执行,但是这里我们可以使用T本身而不是?。
包generics;
public class DemoWildCard {


    public static void main(String[] args) {
        DemoWildCard obj = new DemoWildCard();

        obj.display(new Person<Integer>());
        obj.display(new Person<String>());

    }

    void display(Person<?> person) {
        //allows person of Integer,String or anything
        //This cannnot be done if we use T, because in that case we have to make this method itself generic
        System.out.println(person);
    }

}

class Person<T>{

}

SO通配符有其特定的用例,例如这样。


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