引言

上一篇文章,我们学习了Class类的继承关系,它实现了几个重要的接口(GenericDeclaration、Type和AnnotatedElement),拥有这几个接口的特性。
Class在java中扮演着十分重要的角色,它表示了运行时的类型信息,java的RTTI(运行时类型信息)就是通过Class来实现的,RTTI能让我们在运行时获取对象和类信息,除此之外,Class还跟反射息息相关,它可以看做是反射的入口。这篇文章,我们来看一下Class的创建方式和不同类型对应的Class的输出格式。

获取Class对象

首先需要说明的一点是:Class是java.lang包下的类,不是java.lang.reflect下面的类。

使用instance.getClass()方法获取

getClass()是Object提供的native方法,所有的Object都能使用该方法来获取对象所属的Class。但是,前提是我们必须有一个类的实例,才能执行该方法的调用:

  1. public static void main(String[] args) {
  2. String s = new String("aClassTestString");
  3. Class<? extends String> aClass = s.getClass();
  4. }

我们需要注意getClass方法的返回值,是Class<? extends String>,准确的说,Object提供的这个方法的返回值中的泛型类型是调用getClass的对象的类型的擦除, 看下面的例子:

  1. public static void main(String[] args) throws ClassNotFoundException {
  2. Number n = 2;
  3. Class<? extends Number> aClass = n.getClass();
  4. aClass = Integer.class;
  5. System.out.println(aClass);
  6. }

我们可以将得到的Class变量指向任意的Number的子类的Class对象。Class中泛型的使用,使得在编译期就能保证类型安全,如果我们使用Class原始类型而不是Class<? extends Number>这种泛型写法,就有可能在运行期间出现问题,具体原因可参照这里
由于调用getClass()方法必须得有类的实例,所以,对于原始数据类型、void,就不能使用这种方式来获取Class对象了,而对于类、接口和数组没有问题。

使用Class.class方法获取(类字面常量)

类字面常量提供了获取Class对象更方便的方式,类字面常量就是我们经常所说的使用类名的方式,例如:

  1. public static void main(String[] args) {
  2. Class<Number> numberClass = Number.class;
  3. }

《java语言规范》15.8.2对类字面量有这样的描述:
类字面量是由类、接口、数组或简单类型的名字,或伪类型void,以及后面跟着的 ‘.’和class符号构成的表达式。
如果C是类、接口或数组类型的名字,那么C.class的类型就是Class
如果p是简单类型的名字,那么p.class的类型就是Class,其中B是类型p的表达式在装箱转换之后的类型。
void.class的类型是Class
所以对于不能采用getClass()方法的原始数据类型和void,可以采用这种方式来获取Class对象。
注意返回值与getClass()方法有不同:并不是通配符形式,而直接就是精确的Class
当然我们也可以这样:

  1. public static void main(String[] args) {
  2. Class<? extends Number> numberClass = Number.class;
  3. }

这就是泛型带来的特性了。
与instance.getClass()获取Class一样,类字面常量方式也在编译期就保证了类型安全。使用这两种方式获取Class对象时,我们需要遵循它们的返回值规范,尽量不要自己去掉泛型类型信息而使用原始类型。

使用Class.forName()获取

这个Class类提供的静态方法同样可以用来获取Class对象。forName()涉及到三个方法,我们先看forName(String className):

  1. /**
  2. * Returns the {@code Class} object associated with the class or
  3. * interface with the given string name. Invoking this method is
  4. * equivalent to:
  5. *
  6. * <blockquote>
  7. * {@code Class.forName(className, true, currentLoader)}
  8. * </blockquote>
  9. *
  10. * where {@code currentLoader} denotes the defining class loader of
  11. * the current class.
  12. *
  13. */
  14. @CallerSensitive
  15. public static Class<?> forName(String className)
  16. throws ClassNotFoundException {
  17. Class<?> caller = Reflection.getCallerClass();
  18. return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
  19. }
  20. /** Called after security check for system loader access checks have been made. */
  21. private static native Class<?> forName0(String name, boolean initialize,
  22. ClassLoader loader,
  23. Class<?> caller)
  24. throws ClassNotFoundException;

这个方法只有一个参数,就是要获取Class对象的类的全限定名称。然后调用了forName0这个native方法,传递给forName0的ClassLoader是当前类的类定义加载器,initialize是true,那么ClassLoader和initialize这两个参数的作用是什么?我们需要看forName(String name, boolean initialize, ClassLoader loader)这个方法:

  1. /**
  2. * Returns the {@code Class} object associated with the class or
  3. * interface with the given string name, using the given class loader.
  4. * Given the fully qualified name for a class or interface (in the same
  5. * format returned by {@code getName}) this method attempts to
  6. * locate, load, and link the class or interface. The specified class
  7. * loader is used to load the class or interface. If the parameter
  8. * {@code loader} is null, the class is loaded through the bootstrap
  9. * class loader. The class is initialized only if the
  10. * {@code initialize} parameter is {@code true} and if it has
  11. * not been initialized earlier.
  12. */
  13. @CallerSensitive
  14. public static Class<?> forName(String name, boolean initialize,
  15. ClassLoader loader)
  16. throws ClassNotFoundException
  17. {
  18. Class<?> caller = null;
  19. SecurityManager sm = System.getSecurityManager();
  20. if (sm != null) {
  21. // Reflective call to get caller class is only needed if a security manager
  22. // is present. Avoid the overhead of making this call otherwise.
  23. caller = Reflection.getCallerClass();
  24. if (sun.misc.VM.isSystemDomainLoader(loader)) {
  25. ClassLoader ccl = ClassLoader.getClassLoader(caller);
  26. if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
  27. sm.checkPermission(
  28. SecurityConstants.GET_CLASSLOADER_PERMISSION);
  29. }
  30. }
  31. }
  32. return forName0(name, initialize, loader, caller);
  33. }

根据注释,我们知道loader参数是用来指定使用哪个类加载器来加载这个类,initialize参数用来指定是否要对这个类进行初始化(注意是类加载过程中的初始化阶段),只有当initialize为true并且这个类之前没有被初始化过,才会去初始化这个类。
所以上面forName(String name)总是指定用当前类(调用forName方法的类)的类加载器会去初始化这个类并且总是会初始化这个类,而forName(String name, boolean initialize, ClassLoader loader)方法可以让我们自己指定使用哪个ClassLoader来加载类和是否初始化这个类。因为要使用类加载器来加载类,所以就会有ClassNotFoundException,看下面的例子:

  1. public class ClassUsages {
  2. public static void main(String[] args) throws ClassNotFoundException {
  3. System.out.println(Class.forName("person.andy.concurrency.reflect.ForNameTest",true,String.class.getClassLoader()));
  4. }
  5. }
  6. public class ForNameTest {
  7. }

我们用String的类加载器(bootstrap类加载器)来加载应用程序中的一个类,就会抛出ClassNotFoundException:

  1. Exception in thread "main" java.lang.ClassNotFoundException: person/andy/concurrency/reflect/ForNameTest
  2. at java.lang.Class.forName0(Native Method)
  3. at java.lang.Class.forName(Class.java:348)
  4. at person.andy.concurrency.reflect.ClassUsages.main(ClassUsages.java:6)

三种创建方式的区别

编译期的类型安全

instance.getClass()和Class.class能保证编译期间的类型安全,这个我们从返回值就可以看出来:

  1. public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
  2. Class<ForNameTest> forNameTestClass = ForNameTest.class;
  3. ForNameTest forNameTest = new ForNameTest();
  4. Class<? extends ForNameTest> aClass1 = forNameTest.getClass();
  5. }

类字面量方式的Class泛型类型直接就是ForNameTest,getClass()返回的Class泛型是? extends ForNameTest,这样,我们就无法进行不安全的类型转换了。
而Class.forName()则不能保证类型安全,它的返回值是Class<?>,我们可以将返回值指向任意的Class,看下面的例子:

  1. public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
  2. Class<?> aClass = Class.forName("person.andy.concurrency.reflect.ForNameTest");
  3. String s = (String) aClass.newInstance();
  4. }

由于没有编译期的类型安全,我可以对newInstance进行强制类型转换,结果就是运行期报错:

  1. Exception in thread "main" java.lang.ClassCastException: person.andy.concurrency.reflect.ForNameTest cannot be cast to java.lang.String
  2. at person.andy.concurrency.reflect.ClassUsages.main(ClassUsages.java:7)

所以从类型安全的方面考虑的话,不推荐使用Class.forName()方法。

对类初始化的影响

我们先来看一下类的初始化过程做了什么事情:
类初始化阶段是整个类加载过程的一部分,初始化阶段会初始化类变量和其他资源,或者说:初始化阶段就是执行类构造器方法的过程,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。这里说的初始化就是初始化成代码中指定的值而不是数据类型的0值或者null(这些都认为是原始值),初始化为原始值是准备阶段完成的,例如下面的字段:

  1. public static int i = 100;

这个会在准备阶段被设置为0,然后在初始化阶段被设置为100。
另外还有一点需要注意,准备阶段有一些特殊情况:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量就会被初始化为ConstantValue属性所指定的值,一般就是static final修饰的原始类型和String类型,例如:

  1. public static final int i = 100;
  2. public static final String s = "aString";

i和s在准备阶段直接就被设置为100和aString。
所以,如果一个类的静态块被执行了,我们就可以认为发生了类初始化过程。
再看一下什么时候会进行类的初始化(参考《深入理解java虚拟机》第三版 和《java虚拟机规范》第八版):

  • 遇到new、getstatic、putstatic和invokestatic这四条字节码指令时,如果类没有进行过初始化,则首先要触发其初始化。生成这四条指令的最常见的场景是:使用new关键字实例化对象的时候、读取或者设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 在调用类库中的某些反射方法时,例如,Class类或者java.lang.reflect包中的反射方法。
  • 当初始化一个类的时候,如果发现其父类没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

下面我们来看这三种获取Class对象的方式是否会导致类的初始化。
首先,getClass()方法调用的前提是有一个对应类型的实例,所以getClass()肯定会导致类的初始化(对象的创建会导致初始化)。
Class.forName()上面我们已经介绍了,initialize参数指定是否要对类进行初始化,forName(String className)默认会初始化,而forName(String name, boolean initialize,ClassLoader loader)是否初始化就取决于我们传的参数了。看下面的例子:

  1. public class ForNameTest {
  2. static {
  3. System.out.println("initializing ForNameTest");
  4. }
  5. }
  6. public class ForNameTest1 {
  7. static {
  8. System.out.println("initializing ForNameTest1");
  9. }
  10. }
  11. public class ForNameTest2 {
  12. static {
  13. System.out.println("initialing ForNameTest2");
  14. }
  15. }
  16. public class ClassUsages {
  17. public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
  18. Class<?> aClass = Class.forName("person.andy.concurrency.reflect.ForNameTest",true,ClassUsages.class.getClassLoader());
  19. Class<?> aClass1 = Class.forName("person.andy.concurrency.reflect.ForNameTest1",false,ClassUsages.class.getClassLoader());
  20. Class<?> aClass2 = Class.forName("person.andy.concurrency.reflect.ForNameTest2");
  21. }
  22. }

输出如下:

  1. initializing ForNameTest
  2. initialing ForNameTest2

字面量获取Class的方式不会自动的进行类的初始化,看下面的例子:

  1. public class ClassUsages {
  2. public static void main(String[] args) {
  3. Class<ForNameTest> forNameTestClass = ForNameTest.class;
  4. }
  5. }

执行没有任何的输出,所以没有进行类的初始化。

接下来,我们来看一个简单的问题:不同类型对象的Class表示。

不同类型对象的Class和输出格式

这里先整体描述一下:java中的原始数据类型、数组、类和接口(注意枚举是类、注解是接口)都有对应的Class,并且对于这些类型,我们输出它们的Class,格式都是不同的,下面我们来看一下:

原始数据类型和void

原始数据类型的Class可以通过上面说的类字面常量的方式获取:

  1. public static void main(String[] args) {
  2. System.out.println(int.class);
  3. System.out.println(boolean.class);
  4. System.out.println(void.class);
  5. }

即原始类型关键字或者void加.class的方式,输出如下:

  1. int
  2. boolean
  3. void

注意并不是原始类型对应的包装类,而是该原始类型的关键字。实际上,当我们用这种方式获取Class对象,如果给定变量,变量的类型是这样的:

  1. public static void main(String[] args) {
  2. Class<Integer> integerClass = int.class;
  3. Class<Boolean> booleanClass = boolean.class;
  4. }

Class中的泛型信息就是对应的包装类,那为什么会返回int和boolean呢?这个问题我们暂且放着。

非数组的引用类型

我们可以使用前面介绍的创建Class的任意一种方式类获取非数组的引用类型的Class对象,这里以String为例:

  1. public static void main(String[] args) throws ClassNotFoundException {
  2. Class<String> stringClass = String.class;
  3. Class<? extends String> classString = new String("classString").getClass();
  4. Class<?> aClass = Class.forName("java.lang.String");
  5. }

看输出结果:

  1. class java.lang.String
  2. class java.lang.String
  3. class java.lang.String

输出的格式是字符串class后面加上类的二进制名称。
再看一个包装类Integer的例子:

  1. public static void main(String[] args) throws ClassNotFoundException {
  2. Class<Integer> integerClass = Integer.class;
  3. Class<? extends Integer> aClass = new Integer(2).getClass();
  4. Class<?> aClass1 = Class.forName("java.lang.Integer");
  5. System.out.println(integerClass);
  6. System.out.println(aClass);
  7. System.out.println(aClass1);
  8. }

输出结果如下:

  1. class java.lang.Integer
  2. class java.lang.Integer
  3. class java.lang.Integer

数组类型

数组类型可以通过数组对象的getClass方法来获取Class对象。

  1. public static void main(String[] args) throws ClassNotFoundException {
  2. int[] ints = new int[10];
  3. System.out.println(ints.getClass());
  4. int[][][] ints1 = new int[3][3][3];
  5. System.out.println(ints1.getClass());
  6. Integer[] integers = new Integer[9];
  7. System.out.println(integers.getClass());
  8. String[] strings = new String[8];
  9. System.out.println(strings.getClass());
  10. }

看输出格式:

  1. class [I
  2. class [[[I
  3. class [Ljava.lang.Integer;
  4. class [Ljava.lang.String;

格式的输出规则是:首先class字符串,然后一个或多个[,[的数量与数组的维度保持一致,一维数组一个[,n维数组n个[,然后加上数组元素的固定输出格式。当元素类型是引用类型(类或者接口)时,输出格式是字符L加上类名,如上面的String。当元素的类型是原始类型时,不同的类型有不同的表示 ,上面例子中的int就用I来表示。
不同的原始类型与输出表示对应如下:

原始类型 输出表示
boolean Z
byte B
char C
double D
float F
int I
long J
short S

数组还有一点需要注意,就是维度相同、数组元素相同的数组共享相同的Class,看下面的例子:

  1. public static void main(String[] args) {
  2. String[] strings = new String[8];
  3. String[] strings1 = new String[9];
  4. System.out.println(strings.getClass() == strings1.getClass());
  5. }

输出结果是:

  1. true

接口

我们用Cloneable这个接口来作为示例:

  1. public static void main(String[] args) {
  2. Class<Cloneable> cloneableClass = Cloneable.class;
  3. System.out.println(cloneableClass);
  4. }

输出如下:

  1. interface java.lang.Cloneable

接口的输出格式是interface字符串后面加上该接口的名称。

输出格式的来源

知道了每种类型的Class的输出格式之后,我们来看这种格式是在哪定义的,因为System.out.print()会调用toString()方法,所以,我们看toString方法的实现:

  1. public String toString() {
  2. return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
  3. + getName();
  4. }

方法里面做了判断,如果是interface,会首先输出一个interface字符串,如果是class,会首先输出一个class字符串,如果是原始类型,就输出空字符串,这与我们看到的相符的,之后调用了getName()方法:

  1. public String getName() {
  2. String name = this.name;
  3. if (name == null)
  4. this.name = name = getName0();
  5. return name;
  6. }

返回的是this.name,这个name是通过getName0来获取的,而getName0是一个native方法:

  1. private native String getName0();

但我们可以从getName的注释中找到关于输出规则的描述,这里不再罗列这些注释。

小结

这篇文章,我们学习了Class对象三种不同的获取方式,并分析了这三种获取方式的区别,从编译期类型安全和对类初始化方面,我们应该能知道如何在开发过程中选择合适的获取方式。之后,我们举例分析了不同类型的Class对象的输出格式,实际上就是Class的toString()方法的逻辑。
下一篇文章,我们来学习Class类提供的api,来看我们能用Class做什么。