为什么Class<?>比Class更受欢迎?

41
如果我将一个类声明为一个字段:
Class fooClass;

Eclipse给出了以下警告:

类是一个原始类型。对泛型类型Class的引用应该被参数化

这在实践中意味着什么?为什么我要这么做?如果我请求Eclipse提供“快速修复”,它会给我:

Class<?> fooClass;

这似乎没有增加太多价值,但不再会出现警告。

编辑:为什么类是泛型的?您能否举一个参数化的示例,即除了 <?> 之外还有其他有效用途吗?

编辑:哇!我没有意识到其中的深度。我也看过Java难题,它确实让我对陷阱感到害怕。所以我将永远使用

Class<MyString> myStringClass = MyString.class;

相比之下

Class myStringClass = MyString.class;

(但是从一开始就使用Java,当Class变成泛型时我并没有真正注意到);

注意:我接受了@oxbow_lakes的意见,因为这对我很有意义,但显然这是一个非常复杂的领域。我敦促所有程序员使用特定的Class<MyString>而不是Class。而Class<?>Class更安全。


Eclipse没有特殊对待Class。当您使用任何原始类型(例如List)时,就会发生这种情况。 - Mark Peters
5个回答

61

原始类型和无限制通配符

之前的回答没有真正解释为什么你应该优先选择 Class<?> 而不是 Class,因为表面上看,前者似乎并没有提供比后者更多的信息。

原因在于,原始类型,即 Class,会防止编译器进行泛型类型检查。也就是说,如果你使用原始类型,你会破坏类型系统。例如:

public void foo(Class<String> c) { System.out.println(c); }

以下代码可以这样调用(它将编译并运行):

Class r = Integer.class
foo(r); //THIS IS OK (BUT SHOULDN'T BE)

但不能通过以下方式:

Class<?> w = Integer.class
foo(w); //WILL NOT COMPILE (RIGHTLY SO!)

始终使用非原始形式,即使您必须使用?因为您不知道类型参数是什么(或者受到限制),也比使用原始类型更能允许编译器更充分地推断程序的正确性。

为什么要使用原始类型?

Java语言规范说:

允许使用原始类型只是为了兼容旧代码

您应该尽可能避免使用它们。未限定通配符?可能最好在其他地方描述,但基本上意味着"此参数化基于某些类型,但我不知道(或不关心)它是什么"。这与原始类型并不相同,原始类型是一种丑恶的类型,在其他具有泛型的语言中不存在,如Scala


为什么类需要参数化?

下面是一个用例。假设我有一些服务接口:

public interface FooService

我希望注入其实现,使用系统属性来定义要使用的类。

Class<?> c = Class.forName(System.getProperty("foo.service"));

目前我不确定我的类是否属于正确的类型

//next line throws ClassCastException if c is not of a compatible type
Class<? extends FooService> f = c.asSubclass(FooService.class); 

现在我可以实例化一个FooService
FooService s = f.newInstance(); //no cast

4
目前为止唯一一个真正回答问题的答案!+1 - Daniel Earwicker
1
@Poindexter - 试试Scala吧。你会意识到Java的类型系统无论有没有泛型都是个笑话。 - oxbow_lakes
@oxbow_lakes 我完全同意Java的类型系统是个笑话。可惜我的工作不知道这一点。 - Poindexter
1
@Mark - 从我上面的例子可以明显看出这是错误的!如果我使用 Class<Object> 调用 foo,那么会导致编译错误。 - oxbow_lakes
@Mark - 我明白了。这样的替换既有效又毫无意义! - oxbow_lakes
显示剩余3条评论

3

这似乎并没有增加太多价值,但不再出现警告。

你说得对。但这可能会增加价值:

Class<FooClass> fooClass;

或者,如果更合适:
Class<? extends FooClass> fooClass;

或者
Class<FooInterface> fooClass;

与泛型一般一样,您可以通过指定您想要变量持有的类别来提高类型安全性。对于原始类型的警告只是为了捕获这种潜力未被使用的代码片段。通过声明Class<?>,您基本上是在说“这应该持有任何一种类”。

这个问题是关于为什么要优先使用无限制通配符而不是原始类型。我不确定这是否是一个答案。 - oxbow_lakes
@oxbow_lakes:我会说我的最后两句话已经解释了这个问题。当然,你的回答更加详细。 - Michael Borgwardt

2
因为自JDK 5以来,Class现在具有参数化类型,使得Class成为通用对象。这是必要的(自引入泛型以来)编译器可以在编译时进行类型检查。 Class<?>表示一个“未知类”,其中?是泛型通配符。这意味着fooClass类型的Class<?>接受与任何东西匹配的Class
典型的例子:
Class<?> classUnknown = null;
classUnknown = ArrayList.class; //This compiles.

您可以使用参数化类型来更具体地指定,例如:
Class<ArrayList> = ArrayList.class;

注意:虽然ArrayList是List的一种,但Class<List> listClass = ArrayList.class;无法编译。但是(正如Mark Peters在评论中提到的那样)Class<? extends List> listClass = ArrayList.class;可以编译(感谢通配符)。


你最后的评论源于一个事实,在Java中,泛型类型在其类型参数上不是协变的。 - oxbow_lakes
而正是通配符解决了这个问题...Class<? extends List> clazz = ArrayList.class. - Mark Peters

1

Class类的Javadoc确实提供了一些关于为什么该类存在类型参数的想法:

T - 由此Class对象建模的类的类型。例如,String.class的类型是Class<String>。如果被建模的类未知,则使用Class<?>

这种类型参数的使用并不那么明显,但是对该类的源代码进行粗略查看可以发现,有时候需要类型参数。考虑newInstance方法的实现:

    public T newInstance() 
        throws InstantiationException, IllegalAccessException
    {
    if (System.getSecurityManager() != null) {
        checkMemberAccess(Member.PUBLIC, ClassLoader.getCallerClassLoader());
    }
    return newInstance0();
    }

如果有人没有注意到,此方法返回的对象类型是类型参数的类型。这在使用大量反射的代码中非常有用,而且希望特别小心确保实例化正确类型的对象。

考虑问题中的示例,如果类实例改为声明为:

Class<String> fooClass;
Class<Integer> barClass;
String aString;

因此,以下代码几乎不可能编译:

aString = barClass.newInstance();

简而言之,如果您将要使用类层次结构,并希望强制进行严格的编译时检查以确保您的代码不需要执行大量的instanceof检查,则最好指定您希望使用的类的类型。指定?允许所有类型,但在某些情况下,您需要更具体的类型。

0

因为使用原始类型而不是参数化类型有许多陷阱。

其中之一是,如果使用原始类型,则类上的所有泛型都会丢失。即使是每个方法定义的泛型也是如此。


"Erased" 可能是一个误导性的词语......在运行时,所有的泛型都会被擦除,无论是否使用原始类型。 - Mark Peters
已经进行了更正。但是一些参数化信息在运行时保留,不是全部被擦除 ;) - Bozho

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