理解Java泛型中?的上限和下限

39
我真的很难理解通配符参数。我有几个关于它的问题。
1. ?作为类型参数只能用在方法中,例如:printAll(MyList<? extends Serializable>)。我不能定义带有?作为类型参数的类。
2. 我理解?的上限。例如:printAll(MyList<? extends Serializable>)的意思是:“如果MyList中包含实现Serializable接口的对象,则printAll将打印MyList。” 我对super有点困惑。例如:printAll(MyList<? super MyClass>)的意思是:“如果MyList中包含MyClass或任何扩展MyClassMyClass的后代)的类的对象,则printAll将打印MyList。”
请纠正我错误的地方。
简而言之,只有TEKVN可以用作定义泛型类的类型参数。?只能用在方法中。
更新1:
public void printAll(MyList<? super MyClass>){
    // code code code
}

根据Ivor Horton的书所述,MyList<? super MyClass>表示如果MyListMyClass对象或其实现的任何接口或类,则可以打印MyList。也就是说,MyClass下限。它是继承层次结构中的最后一个类。这意味着我的初始假设是错误的。
因此,假设MyClass如下:
public class MyClass extends Thread implements ActionListener{
    // whatever
}

然后,printAll()将会打印以下内容:
1. 列表中有MyClass的对象
2. 列表中有ThreadActionListener的对象

更新2: 所以,在阅读了许多问题的答案之后,这是我的理解:
  1. ? extends T意味着任何继承T的类。因此,我们指的是T的子类。因此,T是上限。继承层次结构中最高的类

  2. ? super T意味着任何T的父类/接口。因此,我们指的是所有T的父类T因此是下限。继承层次结构中最低的类


我非常确定你已经查看了这个链接,但是如果你还没有的话,它可以为你解释一下通配符的使用方式。链接 - Fritz
一个 MyList<? super MyClass> 也会接受 MyList<java.lang.Object>。不要局限于“列表中的内容”。声明是关于可以传递哪些类型的。由于 printAll(MyList<? super MyClass>) 也会接受 MyList<Object>,因此 MyList<Object> 可能甚至包含一个 JButton,因为 JButton 实例也是 Object 的实例。 - Holger
@Holger MyClass的所有超类/接口 开始有点明白了。 - An SO User
可能是为什么Java类型参数不能有下限?的重复问题。 - Rob
6个回答

21

?作为类型参数只能用于方法中。例如:printAll(MyList<? extends Serializable>),我不能定义带有?作为类型参数的类。

通配符(?)不是正式的类型参数,而是可以用作类型参数。在您提供的示例中,? extends Serializable被赋予了泛型类型MyListprintAll方法参数的类型参数。

方法也可以像类一样声明类型参数,例如:

static <T extends Serializable> void printAll(MyList<T> myList)

我理解对于 ? 的上限。 printAll(MyList<? extends Serializable>) 意味着如果 MyList 中有实现 Serialzable 接口的对象,则 printAll 将打印 MyList。
更准确地说,这意味着只有在传递了一个具有某些泛型类型的 MyList,该类型是 Serializable 或实现 Serializable 时,调用 printAll 才会编译。在这种情况下,它将接受 MyList<Serializable>、MyList<Integer> 等。 super 有点问题。 printAll(MyList<? super MyClass>) 意味着如果 MyList 中有 MyClass 或任何扩展 MyClass(即 MyClass 的后代)的类的对象,则 printAll 将打印 MyList。
使用 super 限定符的通配符是一个下限。因此,我们可以说只有在传递了具有 MyClassMyClass 的某个超类型的某些泛型类型的 MyList 时,调用 printAll 才会编译。因此,在这种情况下,它将接受 MyList<MyClass>,例如 MyList<MyParentClass> 或 MyList<Object>。

So, say if MyClass looks like:

public class MyClass extends Thread implements ActionListener{
    // whatever
}

then, printAll() will print if

  1. There are objects of MyClass in the list
  2. There are objects of Thread or ActionListener in the list
你已经在正确的道路上了。但我认为说例如“如果列表中有MyClass对象,它将会打印”是有问题的。这让它听起来像是你在定义运行时行为——泛型全部关于编译时检查。例如,在MyList<? super MyClass>的参数中,你将不能够传递MyList<MySubclass>,即使它可能包含MyClass的实例,因为继承。我会重新措辞:

只有当它被传递以下内容时,对printAll(MyList<? super MyClass>)的调用才会编译:

  1. MyList<MyClass>
  2. MyList<Thread>
  3. MyList<Runnable>
  4. MyList<ActionListener>
  5. MyList<EventListener>
  6. MyList<Object>
  7. MyList<? super X>其中XMyClassThreadRunnableActionListenerEventListenerObject

所以,在阅读了这个问题的许多答案之后,这是我的理解:

? extends T表示任何扩展T的类。因此,我们正在引用T的子类。因此,T是上限。继承层次结构中最高的类

? super T表示任何是T的super的类/接口。因此,我们正在引用T的所有父项。因此,T是下限。在继承层次结构中最低的类

接近了,但我不会说“T的子类”或“T的父类”,因为这些边界是包容性的——更准确地说应该是“T或其子类型”,以及"T或其超类型"。


16
首先,TEK等都不是固定的名称。它们只是类型变量,您可以为它们决定名称。 TEK只是示例,但您也可以称其为Foo或其他任何内容。
现在回答您的第一个问题:由于通配符?表示“任何和未知”的类型,未指定的类型,因此声明一个类为通用类型没有任何意义。当您不关心类型时,在方法参数或变量中使用通配符是有用的。
现在关于您的第二个问题:下限使您的通用方法更加灵活。 both extendssuper是相反的:
  • ? extends T:一个未知类型,它是T的子类型
  • ? super T:一个未知类型,它是T的超级类型
后者在您想要接受与T兼容的类型(以便T是该类型)时非常有用。可以在这里找到一个实际的例子。

1
经过反复多次阅读您的回答,我想出了更新2。 - An SO User

3

让我们从头开始。

严格来说,任何有效的Java标识符都可以用作泛型类型参数 - 它只是一种特殊类型的变量:

public static final class MyGenericClass<MyGenericType> {

}

这是完全有效的Java代码。

接下来,您可以在任何声明变量的地方使用?。您可以在声明变量时使用通配符,但不能在实例化变量时使用:

public static final class MyGenericClass {
    private final Collection<? extends String> myThings;

    public MyGenericClass(Collection<? extends String> myThings) {
        this.myThings = myThings;
    }  

    public void doStuff(final Collection<? extends String> myThings) {

    }
}

所有的内容都是有效的,你不能这样做:

final Collection<? extends String> myThings = new ArrayList<? extends String>();

当涉及到extendssuper之间的区别时,这被称为协变性与逆变性。它决定了沿着类层次结构提供类型所允许的方向:

final Collection<? extends Runnable> example1 = new ArrayList<Runnable>();
final Collection<? extends Runnable> example2 = new ArrayList<TimerTask>();
final Collection<? super Runnable> example3 = new ArrayList<Runnable>();
final Collection<? super Runnable> example4 = new ArrayList<Object>();

第一个和第二个示例演示了extends——你可以从Collection中假设的最紧密限制是Runnable,因为用户可以传递任何继承层次结构中具有Runnable的东西的Collection
第二个和第三个示例演示了super——你可以从Collection中假设的最紧密限制是Object,因为我们允许任何在Runnable的继承层次结构中的东西。

1
对于第一个问题:你不能将?作为类型参数定义一个方法。以下代码将无法编译:
void <?> foo() {}

?用于将另一个泛型绑定到当前泛型,而不提供类型参数。您可以在方法中这样写:

void foo(List<?> e) {}

你也可以为类编写代码:

public class Bar<E extends List<?>> { }

关于使用super
public void printAll(MyList<? super MyClass>){
    // code code code
}

这段代码不会像你所说的那样只打印"MyClass"类的列表。它可以包含任何一个是"MyClass"类的父类子类的对象。编译器在编译时并不知道列表中将有哪些对象。
为了更好地理解,考虑一个简单的例子,使用"Number"类层次结构。"Float"和"Integer"都是"Number"的子类。您可以像这样编写您的方法:
public void printAll(List<? super Float>){
    // code code code
}

然后,您可以使用 List<Number> 调用该方法:

List<Number> numbers = new ArrayList<>();
numbers.add(1); // actually only add an Integer
printAll(numbers); // compiles.

这种情况下可能并不是非常有用。但它的用处在于,例如当您想将Float添加到集合中而又不希望它只是一个列表时,它就很有用了。
public void addFloat(List<? super Float> list){
    list.add(2.5);
}

嗨@cyrille-ka,你确定 public void addFloat(List<? super Float> list){ list.add(2.5); } 吗?它无法编译,但是 public void addFloat(List<? super Number> list){ list.add(2.5); } 可以,因为它保证了列表项的一致性。 - notsoux

1
下限说:您可以在'super'关键字后面使用提到的类或其任何超类型。这可能会变得棘手。那些超类型可能有其他(完全不同)的类继承自它们,如下面的示例所示。
例如,
List<? super Car> cars = new ArrayList <Vehicle>();

程序员是否被允许编写以下内容:

cars.add(new Helicopter()); //Helicopter is a kind of Vehicle

这显然不应该被允许,它反映了在使用下限时的危险。
程序员应该被允许将车辆添加到列表中,但不是任何车辆。他必须进行强制类型转换,让Java知道他只是添加了一辆汽车,就像这样:
cars.add((Car) new Vehicle()); 

0

嗯,你关于superprintAll(MyList<? super MyClass>))的陈述不够清晰。假设Myclass扩展了Object,这意味着你可以printAll(MyList<MyClass>)printAll(MyList<Object>),但不能使用其他任何类型... 这意味着MyList的泛型类型必须是MyClass的超类(而不是子类)。这与你所说的不同。

至于T、E、K、V或N,它们本身都是没有意义的名称。你可以使用任何你想要的东西。惯例建议使用单个大写字母值,T通常用于泛型方法,而E用于类....


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