0. Introduction
Java 运行时类型信息,简称RTTI;RTTI能够让我们在程序正在运行时发现并使用类型信息; RTTI能够让我们放开编译时面向类型的约束,编写更强大的代码
RTTI可以分为两种:
传统的RTTI
。这是在编译时
所有的类型就可用。反射机制(reflection machanism)
。它能够帮助我们在运行时
发现类型信息。
1. RTTI的必要性
多态是面向对象编程语言的基本特点,而Java要利用RTTI来实现多态
先来一段代码:
abstract class Shape {
void draw(){
System.out.println(this + ".draw()");
}
abstract public String toString();
}
class Circle extends Shape {
@Override
public String toString() {
return "Circle";
}
}
class Square extends Shape {
@Override
public String toString() {
return "Square";
}
}
class Triangle extends Shape {
@Override
public String toString() {
return "Triangle";
}
}
public class Main {
public static void main(String[] args) {
List<Shape> shapeList = Arrays.asList(
new Circle(), new Square(), new Triangle()
);
for(Shape shape : shapeList){
shape.draw();
}
}
}
很显然,输出结果:
Circle.draw()
Square.draw()
Triangle.draw()
现在分析RTTI在这个过程中怎么体现的。
首先,三个对象放入List<Shap>中,编译时,由于泛型系统的类型擦除机制,List<Shap>中实际上存放的全是Object对象。所以此时失去了具体Shape类的特性。
new Circle(), new Square(), new Triangle()
for-each遍历时,我们从List<Shap>中取出Shap类型的对象,那么Object类型会转化成Shap类型。
这是最基本的RTTI形式:所有在运行时的强制类型转换都会被检查正确性,也就是说,在运行时,对象的类型才被完全确定
上面说的RTTI强制类型转换只是一部分,因为我们上面只讨论到Object强制转化成Shap类型
。
接下来的代码shape.draw();
才是多态在起作用。多态决定了shape对象的引用到底是指向Cicle还是Square,或者Triangle
。RTTI会知道shape对象的具体类型(specific type)
2. Class对象与类加载器
2.1. 类加载器概述
要理解Java中的RTTI,我们必须要知道运行时的类型信息到底是怎么表示的,以什么方式表示的。
而我们这里要介绍的Class对象,正是存放了类的信息。
所以运行时的类型信息是保存在一个Class对象中。Java使用Class对象来创建所有的与此Class对象对应的一般对象(regular objects),Java使用Class对象来实现RTTI功能的(例如强制类型转换等)
在任何Java程序中,每个类最多只能有一个对应的Class对象(如果类还没加载到内存,那么就没有对应的Class对象)。
为了让程序在内存中生成类的Class对象,我们有必要了解一下JVM的类加载器ClassLoader
虚拟机把描述类的信息数据从
class文件
加载到内存,并对数据进行校验,转换解析,初始化,最终形成可用被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制
与那些编译时需要进行连接工作的语言不同,Java语言中,类的加载,连接,初始化过程都是在程序运行时期间完成的。
有关Class文件的结构,可以查看另一篇文章:Java虚拟机实践(1)——逐个字节分析.class文件字节码二进制内容,这里不详细介绍,只要知道,我们的.java源代码经过编译后会形成.class文件,它是二进制内容,描述了类的各种信息。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个声明周期分为7个阶段:加载,连接(验证,准备,解析),初始化,使用,卸载。
--------- -------------------Linking------------------------
| Loading | --> | Verification --> Preparation --> Resolution | -->
---------- --------------------------------------------------
--------------- --------- -----------
--> | Initialization | --> | Using | --> | Unloading |
--------------- --------- -----------
之前说过,程序运行的过程中才会加载class文件到内存中,那么什么情况下会开始加载class文件呢?JVM规范规定,有且仅有下面的5中情况发生时,必须对类进行初始化(加载自然在初始化之前开始)
1.
程序运行时,字节码指令为new,getstatic,putstatic,invokestatic
时,如果类没有进行过初始化,则需要先触发其初始化。体现到源代码中就是:new一个对象,访问/赋值类的静态字段,调用类的静态方法
。值得注意的是,这里的静态字段,不包括被final修饰的,已经在编译期把结果放入常量池的静态字段
2.
使用java.lang.reflect包对类进行反射调用的时候,
如果类没有进行过初始化,则需先触发其初始化。
3.
当初始化一个类时,发现其父类还没有进行过初始化
,则需要先触发其父类的初始化。
4.
虚拟机启动时main方法所在的类
会被虚拟机先进行初始化。
5.
java.lang.invoke.MethodHandle
实例解析的结果为REF_getStatic,REF_putStatic,REF_invokeStatic
的方法句柄时,并且此方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
这五种情况我们称之为对类的一个主动引用
。
Java程序在运行时,任何对类的
首次主动使用
,JVM的类加载器都会动态地将类加载到内存中。
所以,我们能够知道:Java程序只会在必要的时候才会加载特定的类,而这个必要,就是对类的首次主动使用
那么类加载阶段,到底做了什么呢?
- 通过类的
全限定名(Fully Qualified Name)
获取到此类的二进制字节流。 - 将字节流所代表的
静态存储结构转化为方法去的运行时数据结构
- 在
内存中生成一个代表这个类的java.lang.class对象,作为方法去这个类的各种信息数据的访问入口
2.2. Class对象
现在我们已经知道,类加载阶段会在内存中生成该类的Class对象。现在我们来简单探究一下Class对象。
在了解Class对象之前,我们有必要知道每个Class对象都是java.lang.Class类的实例
,我们可以使用下面的方法来手动硬编码加载类:
Class.forName("com.mysql.jdbc.Driver");
上面的代码应该在学Java JDBC时很常见的代码,我们通常称之为加载数据库驱动
,那么现在我们学了之前的知识,我们知道,这是加载com.mysql.jdbc.Driver.class类
。
再看看下面的代码:
class Candy {
static {
System.out.println("Loading Candy");
}
}
class Gum {
static {
System.out.println("Loading Gum");
}
}
class Cookie {
static {
System.out.println("Loading Cookie");
}
}
public class SweetShop {
public static void main(String[] args) {
System.out.println("inside main");
new Candy();
System.out.println("After creating Candy");
try {
// 这里的Gum注意要在之前添加当前的java文件的包名
Class.forName("Gum");
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("After Class.forName(\"Gum\")");
new Cookie();
System.out.println("After creating Cookie");
}
}
输出如下:
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie
可以看到,new算对类的主动使用,所以会加载类并初始化类
。
而Class.forName("Full qualified name");并不在上面所说的主动引用的5种情况中(java.lang.Class并不在java.lang.reflect包下,所以也不是反射调用)
,而我们知道我们通常说Class.forName用来加载驱动,这里只说加载,摸棱两可,而上面的代码证明,Class.forName显然是触发了类的初始化操作了。
那看看官方文档怎么说的吧,下面是Java的Class.forName(name)函数的注释吧
/**
* 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>
*
* @param className the fully qualified name of the desired class.
* @return the {@code Class} object for the class with the
* specified name.
**/
意思是说:Class.forName(name)返回一个给定name的类或接口对应的Class对象,此方法等效于调用
Class.forName(className, true, currentLoader)
那么我们再看看Class.forName(className, true, currentLoader)的源码注释吧
/**
* 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.
*
* ... 省略部分
* /
Class.forName(className, initialize, currentLoader)返回给定className对应类的Class对象,
它会使用传入的currentLoader加载器来加载className表示的类,并且会完成连接阶段。....
类是否初始化,取决于传入的initialize布尔变量
而我们知道:
Class.forName(name) 等效于Class.forName(className, true, currentLoader),
所以Class.forName(name)能够完成类的 加载,连接,初始化
现在能够通过硬编码的方式来手动加载类,并且获取类的Class对象。实际上,有两种方法来获取类的Class对象。
Class.forName(name)
obj.getClass();
其中obj为已经实例化的一个类的对象,通过调用其getClass方法,能够获取此类的Class对象。
既然能够获取到类的Class对象,那么我们说过,Class对象保存着这个类相关的信息,我们现在简单研究一下Class对象的一些方法。
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
class Toy {
Toy() {} // 注释掉这行,调用 newInstance()方法就会报异常。
Toy(int i) {}
}
class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {
FancyToy() { super(1); }
}
public class ToyTest {
static void printInfo(Class cc) {
System.out.println("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");
System.out.println("Simple name: " + cc.getSimpleName());
System.out.println("Canonical name : " + cc.getCanonicalName() + "\n");
}
public static void main(String[] args) {
Class c = null;
try {
// 这里的参数要填当前文件所在的包,否则会报错
c = Class.forName("thinkinginjava.rtti.FancyToy");
} catch(ClassNotFoundException e) {
System.out.println("Can't find FancyToy");
System.exit(1);
}
printInfo(c);
// 获取类实现的接口类对象 public Class<?>[] getInterfaces()
for(Class face : c.getInterfaces()) {
printInfo(face);
}
// 获取类的父类对象, native Class<? super T> getSuperclass();
Class up = c.getSuperclass();
Object obj = null;
try {
// 实例化对象,必须要一个默认的构造器(无参构造器)
obj = up.newInstance();
} catch(InstantiationException e) {
System.out.println("Cannot instantiate");
System.exit(1);
} catch(IllegalAccessException e) {
System.out.println("Cannot access");
System.exit(1);
}
printInfo(obj.getClass());
}
}
代码很简单,输出:
Class name: thinkinginjava.rtti.FancyToy is interface? [false]
Simple name: FancyToy
Canonical name : thinkinginjava.rtti.FancyToy
Class name: thinkinginjava.rtti.HasBatteries is interface? [true]
Simple name: HasBatteries
Canonical name : thinkinginjava.rtti.HasBatteries
Class name: thinkinginjava.rtti.Waterproof is interface? [true]
Simple name: Waterproof
Canonical name : thinkinginjava.rtti.Waterproof
Class name: thinkinginjava.rtti.Shoots is interface? [true]
Simple name: Shoots
Canonical name : thinkinginjava.rtti.Shoots
Class name: thinkinginjava.rtti.Toy is interface? [false]
Simple name: Toy
Canonical name : thinkinginjava.rtti.Toy
Class对象的方法:
// 获取简单的类名,如 String
public String getSimpleName() { ... }
// 一下两个获取全限定名,如java.lang.String
public String getName(){ ... }
public String getCanonicalName() { ... }
// 获取类实现的接口,返回Class对象数组
public Class<?>[] getInterfaces() { ... }
// 获取继承的父类,如果没有显示继承,则默认为Object类
public native Class<? super T> getSuperclass();
// 主动实例化类,获取类的实例化对象,必须给类手动添加一个无参构造器,否则会报异常。
public T newInstance() throws InstantiationException, IllegalAccessException { ... }
3. Class字面量(Class literals)
相比Class.forName(...)
方式,Class字面量更加简单,安全,高效
String.class
下面表格左右是等效的
Class字面量 | 包装类字段 |
---|---|
boolean.class | Boolean.TYPE |
char.class | Character.TYPE |
byte.class | Byte.TYPE |
short.class | Short.TYPE |
int.class | Integer.TYPE |
long.class | Long.TYPE |
float.class | Float.TYPE |
double.class | Double.TYPE |
void.class | Void.TYPE |
注意,单纯地使用.class形式得到的Class对象引用,并不会初始化Class类。直到首次引用,也就是我们前面提到的,引用其
静态非final字段,或者调用其静态方法
下面是一个例子:
class Initable {
static final int staticFinal = 47;
static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random rand = new Random(47);
public static void main(String[] args) throws Exception {
Class initable = Initable.class;
System.out.println("After creating Initable ref");
// Does not trigger initialization:
System.out.println(Initable.staticFinal);
// Does trigger initialization:
System.out.println(Initable.staticFinal2);
// Does trigger initialization:
System.out.println(Initable2.staticNonFinal);
// 这里forName方法传参一定要把Initable3所在包名添加进去
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
输出如下:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
这里我们将static变量分为non-final static 和 final static
,对于非final的static变量,我们访问时就算一次主动引用。而对于final static的字面量,在编译时放入常量池,称之为编译时常量(compile-time constant),当我们访问这种常量字段时,不算主动引用。而对于要在运行时确定的常量,必须先对类进行初始化。