何时使用通用方法以及何时使用通配符?


122

我正在从OracleDocGenericMethod中阅读有关泛型方法的信息。当比较指出何时使用通配符以及何时使用通用方法时,我对此感到非常困惑。引用文档。

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

我们可以在这里使用通用方法:

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!
}

[…]这告诉我们类型参数正在用于多态。它的唯一作用是允许在不同的调用站点使用各种实际的参数类型。在这种情况下,应使用通配符。通配符旨在支持灵活的子类型化,这就是我们试图在此处表达的内容。

我们难道不认为像通配符一样(Collection<? extends E> c);也支持某种多态性吗?那么为什么在这种情况下通用方法的使用被认为不好呢?

它继续说,

通用方法允许使用类型参数来表示方法的一个或多个参数的类型和/或其返回类型之间的依赖性。如果没有这种依赖性,则不应使用通用方法。

这是什么意思?

他们举了例子

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

[…]

我们可以以另一种方式编写此方法的签名,而根本不使用通配符:

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

该文档不鼓励使用第二种声明,而是提倡使用第一种语法?第一个和第二个声明之间有什么区别?两者似乎都在做同一件事?

有人可以照亮这个区域吗?

Answers:


173

在某些地方,通配符和类型参数会做同样的事情。但是在某些地方,您必须使用类型参数。

  1. 如果要对不同类型的方法参数强制执行某种关系,则不能使用通配符,而必须使用类型参数。

以您的方法为例,假设您要确保传递给方法的srcand dest列表copy()应为相同的参数化类型,则可以使用如下类型的参数来实现:

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

参考文献:


1
这是一个奇怪的答案。它并没有解释为什么您需要使用它?。您可以将其重写为公共静态<T1扩展Number,T2扩展Number> void copy(List <T1> dest,List <T2> src),在这种情况下,很明显发生了什么事。
2013年

@kan。那才是真正的问题。您可以使用type参数来强制执行相同的类型,但不能使用通配符来实现。为类型形参使用两种不同类型是不同的事情。
罗希特·贾因

1
@benz。您不能在Listusing类型参数中定义下限。List<T super Integer>无效,也不会编译。
罗希特·贾因

2
@benz。不客气:)我强烈建议您浏览我在结尾处发布的链接。那是您将获得的有关泛型的最佳资源。
罗希特·贾因

3
@ jorgen.ringen- <T extends X & Y>>多个界限。
罗希特·贾因

12

考虑下面的James Gosling的Java编程第4版中的以下示例,在该示例中我们要合并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'。书中没有第一种方法。


我投票支持一本书的报价,内容简洁明了
Kurapika

9

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

例如:

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. */

1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();不是有效的语法。您必须无限制地实例化ArrayList。
阿诺德·皮斯托里乌斯

在上面的示例中,无法将苹果添加到购物篮中,因为购物篮可能是梨子的列表。示例AFAIK错误。并且不编译。
Khanna111 '16

1
@ArnoldPistorius确实让我感到困惑。我检查了ArrayList的API文档,并签名了一个构造函数ArrayList(Collection<? extends E> c)。您能解释一下为什么这么说吗?
Kurapika

@Kurapika可能是我使用的是旧Java版本吗?评论发表于大约3年前。
阿诺·皮斯托里乌斯

2

通配符方法也是通用的-您可以使用一定范围的类型来调用它。

<T>语法定义了一个类型变量名。如果类型变量有任何用处(例如,在方法实现中或作为其他类型的约束),则将其命名是有意义的,否则可以将?用作匿名变量。因此,看起来就像一条捷径。

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

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

3
这不应该是评论吗?
Buhake Sindi

@BuhakeSindi对不起,还不清楚什么?为什么是-1?我认为它回答了这个问题。
2013年

2

我将一一解答。

我们难道不认为像通配符一样(Collection<? extends E> c);也支持某种多态性吗?

否。原因是有界通配符没有定义的参数类型。这是一个未知数。它“知道”的全部就是“包含”是一种类型E(无论定义如何)。因此,它无法验证和证明提供的值是否与边界类型匹配。

因此,在通配符上具有多态行为是不明智的。

该文档不鼓励使用第二种声明,并提倡使用第一种语法?第一个和第二个声明之间有什么区别?两者似乎都在做同一件事?

在这种情况下,第一个选项更好,因为T它总是有界的,并且source肯定会具有(unknown)子类的值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需要绑定的参数化类型。

我希望这有帮助。


您能解释一下您的最后一段是什么意思吗
benz 2013年

,指出第二个选项将迫使你绑定一个......的人能找到你阐述了它
奔驰

<S extends T>声明S是属于的子类的参数化类型T,因此它需要作为的子类的参数化类型(无通配符)T
Buhake Sindi

2

另一个未在此处列出的差异。

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本身。

封装泛型;

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>{

}

因此,通配符具有其特定的用例,例如这样。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.