原始类型、无限制通配符和在泛型中使用 Object 有什么区别?

25

我正在阅读《Effective Java》中关于泛型的章节。

请帮我理解SetSet<?>Set<Object>之间的区别?

以下段落摘自该书:

Set<Object>是一个参数化类型,表示可以包含任何类型对象的集合;Set<?>是通配符类型,表示只能包含某种未知类型的对象的集合;而Set则是原始类型,不使用泛型类型系统。

“某种未知类型”是什么意思?所有未知类型是否都是Object类型?那么,Set<?>Set<Object>之间的具体区别是什么?

8个回答

20
  • 原始类型(Set)将该类型视为根本没有任何泛型类型信息。请注意,不仅类型参数T将被忽略,而且该类型的方法可能具有的所有其他类型参数也将被忽略。您可以向其添加任何值,并始终返回Object
  • Set<Object>是一个接受所有Object对象(即所有对象)的Set,并将返回类型为Object的对象。
  • Set<?>是一个接受某些特定但未知类型的所有对象的Set,并将返回该类型的对象。由于对此类型一无所知,因此您无法向该集合中添加任何内容(除了null),并且您所知道的关于它返回的值的唯一事情是它们是Object的某个子类型。

那么原始类型(set)和接受Object类型的集合(Set<Object>)是一样的吗? - orrymr
@orrymr:它的工作方式非常相似,但明确不兼容赋值(某些边角情况实际上是不同的)。此外,从技术上讲,它并不总是“Object”,而是泛型类型的下限(即如果泛型类型只是<T>,则为“Object”,但如果泛型类型是<T extends Number>,则下限为“Number”)。 - Joachim Sauer
不兼容赋值,因此您不能说: Set <Object> mySet = new Set <Integer>; 这是因为 Set <Integer> 不继承自 Set <Object> 吗? - orrymr

3
在运行时,由于类型擦除,JVM 只会看到“Set”。
在编译时,有所不同:
“Set”对一个类型“E”进行了参数化,将“Object”作为参数,因此,“Set.add(E element)”将被参数化为“Set.add(Object element)”。
另一方面,“Set”在类型“E”上添加了通配符,因此,“Set.add(E element)”被翻译为“Set.add(? element)”。由于这是无法编译的,Java 将其“翻译”为“Set.add(null element)”。这意味着您不能向该集合中添加任何内容(除了 null)。原因是通配符引用了未知类型。

虽然Set<Object>被参数化为Set.add(Object element),但是不可能添加Object的子类型(例如String),对吗? - Vinoth Kumar C M
这是可能的,因为所有具体类都隐式/显式地扩展了Object - Buhake Sindi
那么为什么这段代码无法编译?Set<Object> set = new HashSet<Integer>(); - Vinoth Kumar C M
因为您错误地实例化了集合。实例set必须具有与您分配的相同类型T。正确的做法是Set<Object> set = new HashSet<Object>();。您的代码可能被解释为Set<T> set = new HashSet<U>()(在泛型术语中,T != U)。 - Buhake Sindi

3
“some unknown type”是什么意思?
这就意味着——Set有一些泛型参数,但我们不知道具体是什么。因此,赋值给“Set<?>”变量的Set可以是一个“Set<String>”,或一个“Set<Integer>”,或一个“Set<Map<Integer, Employee>>”,或者包含任何其他特定类型的集合。
那么这对你如何使用它有什么影响呢?那么,从中获取到的任何东西都将是“?”的实例,无论它是什么。由于我们不知道类型参数是什么,所以你只能比较笼统地说,集合中的元素可分配给Object(仅因为所有类都继承自它)。
如果你想往集合中添加一些内容,那么“add”方法需要一个“?”(很合理,因为这是集合内对象的类型)。但是,如果你尝试添加任何特定的对象,怎么能确定这是类型安全的呢?你不能——如果你插入一个字符串,你可能会把它放进一个“Set<Integer>”,这会破坏泛型提供的类型安全性。因此,虽然你不知道泛型参数的类型,但你不能提供任何这种类型的参数(唯一的例外是null,因为它是任何类型的“实例”)。
与大多数与泛型相关的答案一样,这个问题集中在集合上,因为它们更容易直观地理解。但是这些讨论适用于任何需要泛型参数的类——如果它使用未限定的通配符参数“?”来声明,那么你就不能提供任何参数,而且你接收到的任何值也只能分配给Object。

2

Set: 这里没有泛型,不安全。可以添加任何想要的内容。

Set<?>: 一组我们不知道其类型的集合。与Set<? extends Object>相同。可以引用任何类型的集合,但该类型必须在实例化集合时定义。使用通配符引用,我们无法修改集合(不能添加或删除除null以外的任何内容)。就像一个视图。

Set<Object>: 包含对象(仅基类,不包括子类)的集合。我的意思是,您可以使用Object类型的集合实例化集合,例如HashSet<Object>,但不能使用HashSet<String>。当然,您可以向集合中添加任何类型的元素,但这只是因为所有内容都是对象或对象的子类。如果将集合定义为Set,则只能添加数字和数字的子类,而不能添加其他内容。


3
一个 Set<Object> 绝对可以包含任何 Object 的子类,你可以添加 IntegerString 和其他所有对象进去。 - Joachim Sauer
那么为什么这段代码无法编译?Set<Object> set = new HashSet<Integer>() - Vinoth Kumar C M
@cmv 因为你将集合引用定义为类型为 Object 的 Set,没有更多的信息。你应该写成 Set<? extends Object> 才能编译通过。请注意,使用数组时可以写成 Object[] arr = new Integer[3]; - Mister Smith
2
@cmv 编译器阻止您将 HashSet<Integer> 分配给 Set<Object> 或甚至 HashSet<Object> 的原因是因为在持有该引用时,您会添加一个不是整数的元素。 - Mister Smith

1

Set<Object>Set<?>之间的区别在于,类型为Set<?>的变量可以分配更具体的泛型类型,例如:

Set<?> set = new HashSet<Integer>();

Set<Object> 只能被赋值为 Set<Object> 时:

Set<Object> set = new HashSet<Integer>(); // won't compile

Set<Object>仍然很有用,因为任何对象都可以放入其中。在这个意义上,它很像原始的Set,但与通用类型系统配合得更好。


1
我向朋友解释了这个项目,并具体要求使用"safeAdd"方法作为不安全添加示例的对策。因此,这就是它。
public static void main(String[] args) {
    List<String> strings = new ArrayList<String>();

    unsafeAdd(strings, new Integer(42)); // No compile time exceptions

    // New 
    safeAdd(strings, new Integer(42)); // Throwing an exception at compile time


    String s = strings.get(0); // Compiler-generated cast

}

private static void unsafeAdd(List list, Object o) {
    list.add(o);
}


private static <E> void safeAdd(List<E> list, E o) {
    list.add(o);
}

1
Set<?> set = new HashSet<String>();
set.add(new Object()); // compile time error

由于我们不知道集合的元素类型,因此无法向其中添加对象。 add() 方法接受类型为 E 的参数,即 Set 的元素类型。 当实际类型参数为 ? 时,它代表某种未知类型。我们传递给 add 的任何参数都必须是该未知类型的子类型。由于我们不知道该类型是什么,因此无法传递任何内容。唯一的例外是 null,它是每种类型的成员。

给定一个 Set<?>,我们可以调用 get() 并利用结果。结果类型是未知的,但我们总是知道它是一个对象。因此,将 get() 的结果分配给类型为 Object 的变量或将其作为参数传递到期望类型为 Object 的位置是安全的。


-1
假设你正在编写一个通用方法,以打印出List中出现的元素。现在,这个方法可以用于打印类型为Integer、Double、Object或其他任何类型的Lists。你会选择哪一个?
  • List<Object> :如果我们使用这个,这将帮助我们仅打印类型为 Object 的元素。它不适用于打印属于其他类别(例如 Double)的元素。这是因为泛型默认不支持继承,需要使用“super”关键字明确指定。

    // 仅打印类型为 'Object' 的对象
    
    public static void printList(List<Object> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }
    
  • List<?> :这可以帮助我们拥有一个通用的方法来打印任何数据类型。我们可以使用此方法来打印任何类型的实例。

    // 类型将取决于传递的内容
    public static void printList(List<?> list) {
        for (Object elem: list)
            System.out.print(elem + " ");
        System.out.println();
    }
    

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