类型信息
运行时类型信息可以帮助程序员在程序运行时发现和使用类型信息——这将程序员从在编译期面向类型进行操作的禁锢中解救出来。下面我们将会讨论在Java中,是如何发现和使用类型信息的:
- 传统的RTTI:它假定在程序编译期就已经知道了所有的类型
- 反射机制:允许我们在运行时发现和使用类型信息
为什么需要RTTI
public class RTTITest {
public static void main(String[] args) {
ArrayList<Tee> list = new ArrayList<>();
list.add(new GreenTee());
list.add(new RedTee());
list.add(new BruceLee());
for (Tee t:list) {
t.saySomething();
}
}
}
abstract class Tee{
Tee(){}
abstract void saySomething();
}
class GreenTee extends Tee{
GreenTee(){}
@Override
void saySomething() {
System.out.println("i am Green Tee!");
}
}
class RedTee extends Tee{
RedTee(){}
@Override
void saySomething() {
System.out.println("i am Red Tee!");
}
}
class BruceLee extends Tee{
BruceLee(){}
@Override
void saySomething() {
System.out.println("i am Bruce Lee!");
}
}
//output:
i am Green Tee!
i am Red Tee!
i am Bruce Lee!
上面的代码中,BruceLee等类的对象被向上转型后传入了带有泛型标记Tee的列表中,但同时也失去了它的具体类型。在取出时,对于容器而言,他会被认为是Tee的一个对象而并非是BruceLee的对象。在Java中,容器会将所有内容当做Object持有,所有的类型转换都是在程序运行时进行正确性检查的,即使这个转换有时会并不彻底(比如BruceLee的对象被当做Lee处理)。这是RTTI——RunTime Type Identification的最基本使用形式,即在运行时识别一个对象的类型。在编译期,由容器和java的泛型系统来强制确保这一点,而在运行时,由强制类型转换来确保这一点。
而接下来的执行则由多态来完成——具体执行什么样的操作是Tee引用指向的对象来决定的,当然这也是我们在coding时的初衷:即尽量少的去了解对象的具体类型,而通过执行通用的操作来完成目标,这样的设计更加优雅灵活。
然而,你还是会不可避免的遇到这样的问题:如果能够知道一个泛化引用的具体类型,就可以更直接的去操作它,那么如何得知一个引用的具体类型呢?
Class类型
在java中类型信息的表示是由被称为Class的特殊类的对象实现的,它包含了与类有关的信息。当然事实上,Java是使用Class对象来完成所有"常规"对象的创建的,Class对象被用来执行其RTTI,即使是执行类型转换这样的操作。同时Class类还拥有很多其他用于执行RTTI的方法。
类是程序的一部分,每个类都拥有一个Class对象,每当编写并编译了一个新的类,都会获得一个对应的Class对象,更确切的说,是会获得一个同名的.class
文件。JVM将使用被称为类加载器的子系统来生成这个Class对象。
所有的类都是在对其第一次使用时加载到JVM中的,当创建第一个对类的静态成员的引用时,就会加载这个类——因此构造器也被看做类的静态成员,即使它并没有被static修饰。因此在Java中并不是程序一开始就会加载所有的类,而是按照需要动态加载。动态加载使能的行为,是在如C++这样的静态加载语言中很难复制的。(在编译时类加载器会将所有的类生成同名的.class
文件,但是只有在使用时才会将必要的class对象生成并加载到内存中。不过基于jvm使用需要的类会在程序一开始就被加载,如Object等)
类加载器首先会检查某个类的Class对象是否被加载,如果尚未加载,默认的类加载器就会根据类名查找.class
文件。一旦一个类的Class对象被载入内存,他就被用来创建这个类的所有对象。
package com.jarvis;
public class ClassLoaderTest {
public static void main(String[] args) {
new Jarvis();
System.out.println("======After new Jarvis======");
try{
Class.forName("com.jarvis.IronMan");
}catch(Throwable t){
System.out.println("Error");
}
System.out.println("======After forName IronMan======");
new BlackWidow();
System.out.println("======After new Black Widow======");
}
}
class Jarvis{
static{
System.out.println("i am Jarvis!");
}
}
class IronMan{
static{
System.out.println("i am IronMan!");
}
}
class BlackWidow{
static{
System.out.println("i am Black Widow!");
}
}
/**
output:
i am Jarvis!
======After new Jarvis======
i am IronMan!
======After forName IronMan======
i am Black Widow!
======After new Black Widow======
**/
从上面的代码中我们可以发现,Class对象只有在需要的情况下才会加载,static初始化是在加载过程中完成的。Class.forName()
是获取Class对象引用的一个方法,同时他会产生一定的副作用——如果该类还没有加载那就加载它——而我们经常为了这样的副作用来使用该方法。无论何时,如果想要获取运行时类型信息,就必须首先获取对应的Class对象的引用,Class.forName()
使你不需要为了获取到Class对象的引用而首先持有对应类的对象——当然在你已经持有某个类型的对象时,可以使用getClass()
方法来获取该类型对应的Class对象引用,该方法属于Object。
Class类拥有很多可以获取目标类型信息的方法,我们甚至可以通过Class的引用来创建对应类型的实例对象——newInstance()
。在上面的代码中,通过Class.forName()
获取到的Class引用在使用newInstance()
创建类的对象时,由于在编译期无法获取到更具体的类型信息,因此只能得到一个Object的引用,但是这个引用指向的是目标类的对象。在使用该引用向对象发送任何Object可以接受的以外的消息时,必须执行某种转型。此外,使用newInstance()
方法来创建对象时,需要确保目标类带有默认的构造器。
类字面常量
Java还提供了类字面常量的方式来获取Class对象的引用:Jarvis.class
。这样做不仅更加高效,甚至更安全——因为在编译时就可以对其进行安全性检查,并且它根除了对forName()
方法的调用。
类字面常量不单单可以对类使用,对接口、数组甚至基本数据类型都适用。另外,对于基本数据类型对应的包装类还可以使用该类中的TYPE
字段,该字段是一个引用,指向对应的基本数据类型的Class对象。即int.class
等价于Integer.TYPE
。
为了使用类而做的准备工作包括三个步骤:
1. 加载:类加载器查询字节码文件,并从字节码中创建一个Class对象。(注意,即使在编译期会将所有的类生成对应的字节码文件,但是这并不意味着在程序开始时会将他们全部加载——Java的动态加载使能)
2. 链接:在链接阶段将验证类的字节码,为静态域分配空间,如果必须的话(指该类中存在其他类的引用作为成员),将解析这个类创建的对其他类的所有引用。
3. 初始化:对该类以及该类的超类进行初始化,执行静态初始化器和静态初始化块。初始化被延迟到了对静态方法或者非常数静态域(编译期常量,如果在编译期能够得知该域中的所有信息,也就意味着没有必要为了已知的信息去做不必要的操作)进行首次引用时进行。注意,在使用.class
来获取Class引用时,不会对该类型进行自动初始化(使用.class
方式获取引用时是编译器加载方式,属于静态加载——可以理解为使用.class
方式会去读取对应的字节码文件,由于字节码文件——即Class对象中包含了足够的类型信息,因此无初始化必要)
public class ClassLifeCycleTest {
public static void main(String[] args) throws ClassNotFoundException {
Class A = J.class;
System.out.println("After...");
Class B = Class.forName("com.jarvis.J");
System.out.println(A == B);
System.out.println(K.a);
System.out.println("After ...");
System.out.println(K.b);
}
}
class J{
static{
System.out.println("Hello");
}
}
class K{
public static final int a = 15;
public static int b = 25;
static{
System.out.println("执行初始化...");
}
}
/*
output:
After...
Hello
true
15
After ...
执行初始化...
25
*/
如上面代码表现出来的,初始化有效的实现了尽可能的“惰性”——只有在确保必要的情况下才去进行初始化。对于类来说,如果一个static域不是final的,那么对他访问时,在对其进行读取之前,要先进行链接(为该域分配存储空间)以及初始化(初始化该存储空间)
泛化的Class引用
Class引用总是指向一个Class对象,在Java SE5之后,允许使用泛型语法,用来对Class引用指向Class对象类型进行更加具体的限定:Class<Integer> clazz = int.class;
。通过使用泛型语法,可以使编译器强制执行额外的类型检查(再次强调,泛型语法与编译息息相关,如果不是为了在编译期更加优雅的确定类型或者为了在运行时安全的进行类型转换,那么泛型将没有意义)。
当然,这样的错误可能会使你难以理解——Class<Number> clazz = int.class;
——这是因为Number Class类并不是Integer Class的父类,关于泛型更详细的解读我们将在后面进行。为了更加灵活的使用泛型,我们可以使用泛型通配符?
,这样的语法是可以被接受的——Class<?> clazz = int.class;
以及Class<? extends Number> clazz = int.class;
。(同样的,再次强调,为Class添加泛型语法仅仅是为了在编译时进行类型检查。否则在直接使用Class引用时,如果真的犯了错误,那么你将只能在运行时才会发现它,这会非常麻烦)
public class GenericClassTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
(new TestClass(J.class)).saySomething();
System.out.println("====");
(new TestClass(Jarvis.class)).saySomething();
}
}
class TestClass<T> {
private Class<T> type;
TestClass(Class<T> type){
this.type = type;
}
public void saySomething() throws IllegalAccessException, InstantiationException {
System.out.println(type.getSimpleName());
System.out.println(type.newInstance());
}
}
/*
output:
J
Hello
com.jarvis.J@2d98a335
====
Jarvis
i am Jarvis!
com.jarvis.Jarvis@7ef20235
从上面的代码中我们会发现,在添加了泛型语法后的Class引用,在使用newInstance
实例化对象时,会得到确切类型而不是Object——这归功于编译时的泛型检查。当然这也有局限性,如添加了<? extends A>
或者<? super A>
时,就只能得到Object了。
public class GenericClassTest2 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<C> clazz = C.class;
C c = clazz.newInstance();
Class<? super C> superclass = clazz.getSuperclass();
Object object = superclass.newInstance();
}
}
class A{}
class B extends A{}
class C extends A{}
cast()转型语法
cast()方法可以接受参数对象,并将其转出为Class引用。当然,这种语法稍显怪异,你其实可以直接使用强制转型来完成这件事情。当然,在你存储了Class引用并且遇到一些无法使用普通转型的场景时,这个语法可能会对你有些帮助——理论上。
public class CastTest {
public static void main(String[] args) {
NewHuman man = new NewMan();
Class<NewMan> clazz = NewMan.class;
NewMan cast = clazz.cast(man);
//or just do this...
NewMan cast2 = (NewMan)man;
}
}
class NewMan extends NewHuman{}
class NewHuman{}