引言
上一篇文章,我们学习了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。但是,前提是我们必须有一个类的实例,才能执行该方法的调用:
public static void main(String[] args) {
String s = new String("aClassTestString");
Class<? extends String> aClass = s.getClass();
}
我们需要注意getClass方法的返回值,是Class<? extends String>,准确的说,Object提供的这个方法的返回值中的泛型类型是调用getClass的对象的类型的擦除, 看下面的例子:
public static void main(String[] args) throws ClassNotFoundException {
Number n = 2;
Class<? extends Number> aClass = n.getClass();
aClass = Integer.class;
System.out.println(aClass);
}
我们可以将得到的Class变量指向任意的Number的子类的Class对象。Class中泛型的使用,使得在编译期就能保证类型安全,如果我们使用Class原始类型而不是Class<? extends Number>这种泛型写法,就有可能在运行期间出现问题,具体原因可参照这里。
由于调用getClass()方法必须得有类的实例,所以,对于原始数据类型、void,就不能使用这种方式来获取Class对象了,而对于类、接口和数组没有问题。
使用Class.class方法获取(类字面常量)
类字面常量提供了获取Class对象更方便的方式,类字面常量就是我们经常所说的使用类名的方式,例如:
public static void main(String[] args) {
Class<Number> numberClass = Number.class;
}
《java语言规范》15.8.2对类字面量有这样的描述:
类字面量是由类、接口、数组或简单类型的名字,或伪类型void,以及后面跟着的 ‘.’和class符号构成的表达式。
如果C是类、接口或数组类型的名字,那么C.class的类型就是Class
如果p是简单类型的名字,那么p.class的类型就是Class,其中B是类型p的表达式在装箱转换之后的类型。
void.class的类型是Class
所以对于不能采用getClass()方法的原始数据类型和void,可以采用这种方式来获取Class对象。
注意返回值与getClass()方法有不同:并不是通配符形式,而直接就是精确的Class
当然我们也可以这样:
public static void main(String[] args) {
Class<? extends Number> numberClass = Number.class;
}
这就是泛型带来的特性了。
与instance.getClass()获取Class一样,类字面常量方式也在编译期就保证了类型安全。使用这两种方式获取Class对象时,我们需要遵循它们的返回值规范,尽量不要自己去掉泛型类型信息而使用原始类型。
使用Class.forName()获取
这个Class类提供的静态方法同样可以用来获取Class对象。forName()涉及到三个方法,我们先看forName(String className):
/**
* Returns the {@code Class} object associated with the class or
* interface with the given string name. Invoking this method is
* equivalent to:
*
* <blockquote>
* {@code Class.forName(className, true, currentLoader)}
* </blockquote>
*
* where {@code currentLoader} denotes the defining class loader of
* the current class.
*
*/
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
/** Called after security check for system loader access checks have been made. */
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
throws ClassNotFoundException;
这个方法只有一个参数,就是要获取Class对象的类的全限定名称。然后调用了forName0这个native方法,传递给forName0的ClassLoader是当前类的类定义加载器,initialize是true,那么ClassLoader和initialize这两个参数的作用是什么?我们需要看forName(String name, boolean initialize, ClassLoader loader)这个方法:
/**
* Returns the {@code Class} object associated with the class or
* interface with the given string name, using the given class loader.
* Given the fully qualified name for a class or interface (in the same
* format returned by {@code getName}) this method attempts to
* locate, load, and link the class or interface. The specified class
* loader is used to load the class or interface. If the parameter
* {@code loader} is null, the class is loaded through the bootstrap
* class loader. The class is initialized only if the
* {@code initialize} parameter is {@code true} and if it has
* not been initialized earlier.
*/
@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
根据注释,我们知道loader参数是用来指定使用哪个类加载器来加载这个类,initialize参数用来指定是否要对这个类进行初始化(注意是类加载过程中的初始化阶段),只有当initialize为true并且这个类之前没有被初始化过,才会去初始化这个类。
所以上面forName(String name)总是指定用当前类(调用forName方法的类)的类加载器会去初始化这个类并且总是会初始化这个类,而forName(String name, boolean initialize, ClassLoader loader)方法可以让我们自己指定使用哪个ClassLoader来加载类和是否初始化这个类。因为要使用类加载器来加载类,所以就会有ClassNotFoundException,看下面的例子:
public class ClassUsages {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(Class.forName("person.andy.concurrency.reflect.ForNameTest",true,String.class.getClassLoader()));
}
}
public class ForNameTest {
}
我们用String的类加载器(bootstrap类加载器)来加载应用程序中的一个类,就会抛出ClassNotFoundException:
Exception in thread "main" java.lang.ClassNotFoundException: person/andy/concurrency/reflect/ForNameTest
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at person.andy.concurrency.reflect.ClassUsages.main(ClassUsages.java:6)
三种创建方式的区别
编译期的类型安全
instance.getClass()和Class.class能保证编译期间的类型安全,这个我们从返回值就可以看出来:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<ForNameTest> forNameTestClass = ForNameTest.class;
ForNameTest forNameTest = new ForNameTest();
Class<? extends ForNameTest> aClass1 = forNameTest.getClass();
}
类字面量方式的Class泛型类型直接就是ForNameTest,getClass()返回的Class泛型是? extends ForNameTest,这样,我们就无法进行不安全的类型转换了。
而Class.forName()则不能保证类型安全,它的返回值是Class<?>,我们可以将返回值指向任意的Class,看下面的例子:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> aClass = Class.forName("person.andy.concurrency.reflect.ForNameTest");
String s = (String) aClass.newInstance();
}
由于没有编译期的类型安全,我可以对newInstance进行强制类型转换,结果就是运行期报错:
Exception in thread "main" java.lang.ClassCastException: person.andy.concurrency.reflect.ForNameTest cannot be cast to java.lang.String
at person.andy.concurrency.reflect.ClassUsages.main(ClassUsages.java:7)
所以从类型安全的方面考虑的话,不推荐使用Class.forName()方法。
对类初始化的影响
我们先来看一下类的初始化过程做了什么事情:
类初始化阶段是整个类加载过程的一部分,初始化阶段会初始化类变量和其他资源,或者说:初始化阶段就是执行类构造器
public static int i = 100;
这个会在准备阶段被设置为0,然后在初始化阶段被设置为100。
另外还有一点需要注意,准备阶段有一些特殊情况:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量就会被初始化为ConstantValue属性所指定的值,一般就是static final修饰的原始类型和String类型,例如:
public static final int i = 100;
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)是否初始化就取决于我们传的参数了。看下面的例子:
public class ForNameTest {
static {
System.out.println("initializing ForNameTest");
}
}
public class ForNameTest1 {
static {
System.out.println("initializing ForNameTest1");
}
}
public class ForNameTest2 {
static {
System.out.println("initialing ForNameTest2");
}
}
public class ClassUsages {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> aClass = Class.forName("person.andy.concurrency.reflect.ForNameTest",true,ClassUsages.class.getClassLoader());
Class<?> aClass1 = Class.forName("person.andy.concurrency.reflect.ForNameTest1",false,ClassUsages.class.getClassLoader());
Class<?> aClass2 = Class.forName("person.andy.concurrency.reflect.ForNameTest2");
}
}
输出如下:
initializing ForNameTest
initialing ForNameTest2
字面量获取Class的方式不会自动的进行类的初始化,看下面的例子:
public class ClassUsages {
public static void main(String[] args) {
Class<ForNameTest> forNameTestClass = ForNameTest.class;
}
}
执行没有任何的输出,所以没有进行类的初始化。
接下来,我们来看一个简单的问题:不同类型对象的Class表示。
不同类型对象的Class和输出格式
这里先整体描述一下:java中的原始数据类型、数组、类和接口(注意枚举是类、注解是接口)都有对应的Class,并且对于这些类型,我们输出它们的Class,格式都是不同的,下面我们来看一下:
原始数据类型和void
原始数据类型的Class可以通过上面说的类字面常量的方式获取:
public static void main(String[] args) {
System.out.println(int.class);
System.out.println(boolean.class);
System.out.println(void.class);
}
即原始类型关键字或者void加.class的方式,输出如下:
int
boolean
void
注意并不是原始类型对应的包装类,而是该原始类型的关键字。实际上,当我们用这种方式获取Class对象,如果给定变量,变量的类型是这样的:
public static void main(String[] args) {
Class<Integer> integerClass = int.class;
Class<Boolean> booleanClass = boolean.class;
}
Class中的泛型信息就是对应的包装类,那为什么会返回int和boolean呢?这个问题我们暂且放着。
非数组的引用类型
我们可以使用前面介绍的创建Class的任意一种方式类获取非数组的引用类型的Class对象,这里以String为例:
public static void main(String[] args) throws ClassNotFoundException {
Class<String> stringClass = String.class;
Class<? extends String> classString = new String("classString").getClass();
Class<?> aClass = Class.forName("java.lang.String");
}
看输出结果:
class java.lang.String
class java.lang.String
class java.lang.String
输出的格式是字符串class后面加上类的二进制名称。
再看一个包装类Integer的例子:
public static void main(String[] args) throws ClassNotFoundException {
Class<Integer> integerClass = Integer.class;
Class<? extends Integer> aClass = new Integer(2).getClass();
Class<?> aClass1 = Class.forName("java.lang.Integer");
System.out.println(integerClass);
System.out.println(aClass);
System.out.println(aClass1);
}
输出结果如下:
class java.lang.Integer
class java.lang.Integer
class java.lang.Integer
数组类型
数组类型可以通过数组对象的getClass方法来获取Class对象。
public static void main(String[] args) throws ClassNotFoundException {
int[] ints = new int[10];
System.out.println(ints.getClass());
int[][][] ints1 = new int[3][3][3];
System.out.println(ints1.getClass());
Integer[] integers = new Integer[9];
System.out.println(integers.getClass());
String[] strings = new String[8];
System.out.println(strings.getClass());
}
看输出格式:
class [I
class [[[I
class [Ljava.lang.Integer;
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,看下面的例子:
public static void main(String[] args) {
String[] strings = new String[8];
String[] strings1 = new String[9];
System.out.println(strings.getClass() == strings1.getClass());
}
输出结果是:
true
接口
我们用Cloneable这个接口来作为示例:
public static void main(String[] args) {
Class<Cloneable> cloneableClass = Cloneable.class;
System.out.println(cloneableClass);
}
输出如下:
interface java.lang.Cloneable
接口的输出格式是interface字符串后面加上该接口的名称。
输出格式的来源
知道了每种类型的Class的输出格式之后,我们来看这种格式是在哪定义的,因为System.out.print()会调用toString()方法,所以,我们看toString方法的实现:
public String toString() {
return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
+ getName();
}
方法里面做了判断,如果是interface,会首先输出一个interface字符串,如果是class,会首先输出一个class字符串,如果是原始类型,就输出空字符串,这与我们看到的相符的,之后调用了getName()方法:
public String getName() {
String name = this.name;
if (name == null)
this.name = name = getName0();
return name;
}
返回的是this.name,这个name是通过getName0来获取的,而getName0是一个native方法:
private native String getName0();
但我们可以从getName的注释中找到关于输出规则的描述,这里不再罗列这些注释。
小结
这篇文章,我们学习了Class对象三种不同的获取方式,并分析了这三种获取方式的区别,从编译期类型安全和对类初始化方面,我们应该能知道如何在开发过程中选择合适的获取方式。之后,我们举例分析了不同类型的Class对象的输出格式,实际上就是Class的toString()方法的逻辑。
下一篇文章,我们来学习Class类提供的api,来看我们能用Class做什么。