什么时候使用泛型方法,什么时候使用通配符?

我正在阅读关于 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) {
...
}

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

有人能照亮这个区域吗。

73193 次浏览

通配符方法也是泛型的——您可以使用某些类型调用它。

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

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

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

在某些地方,通配符和类型参数执行相同的操作。但是也有一些地方必须使用类型参数。

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

以您的方法为例,假设您想确保传递给 copy()方法的 srcdest列表应该是相同的参数化类型,您可以使用类型参数这样做:

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将不再是类型安全的。 如果您不需要这种关系,那么您完全可以不使用类型参数。

使用通配符和类型参数之间的其他一些区别是:

  • 如果只有一个参数化类型参数,那么可以使用通配符,不过类型参数也可以工作。
  • 类型参数支持多个边界,通配符不支持。
  • 通配符支持上界和下界,类型参数只支持上界。所以,如果你想定义一个取 Integer类型的 List或者超类的方法,你可以这样做:

    public void print(List<? super Integer> list)  // OK
    

    但是不能使用类型参数:

     public <T super Integer> void print(List<T> list)  // Won't compile
    

References:

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

例如:

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为一个特定的 S 类,即 T 的子类。列表只能包含一个类的元素(在这个实例中是 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. */

我会试着一个一个回答你的问题。

难道我们不认为像 (Collection<? extends E> c);这样的外卡也是 支持多态性?

没有。原因是 有界通配符有界通配符没有定义的参数类型。这是个未知数。它所“知道”的只是“容器”是 E类型(无论定义如何)。因此,它无法验证和判断提供的值是否匹配有界类型。

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

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

在这种情况下,第一个选项更好,因为 T总是有界的,而且 source肯定有(未知的)子类 T的值。

因此,假设您想要复制所有数字列表,第一个选项将是

Collections.copy(List<Number> dest, List<? extends Number> src);

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

第2个选项将强制您为每个要复制的类型绑定 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是需要绑定的参数化类型。

希望这个能帮上忙。

考虑下面的例子,由 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”。第一种方法在书里没有。

这里没有列出的另一个差异。

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

据我所知,当严格需要通配符时,只有一种用例(即可以使用显式类型参数表示您无法表示的内容)。这时需要指定一个下限。

除此之外,通配符用于编写更简洁的代码,正如您在文档中提到的以下语句所描述的:

泛型方法允许使用类型参数来表示 方法的一个或多个参数的类型之间的依赖项 和/或其返回类型 方法。

[...]

使用通配符比声明显式通配符更清晰、更简洁 类型参数,因此应该尽可能首选。

[...]

通配符还有一个优点,它们可以在 方法签名,如字段、局部变量和数组的类型。

通配符主要在 Non-Generic 方法的参数/参数级别强制使用泛型。 注意: 默认情况下,它也可以在 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>{


}

通配符具有如下特定用例。

意思是未知

一般规则适用于: 你从它 识字,但 不写

给我一辆简单的破车

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