Java RTTI(RunTime Type Information,运行时类型信息)

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,或者TriangleRTTI会知道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程序只会在必要的时候才会加载特定的类,而这个必要,就是对类的首次主动使用

那么类加载阶段,到底做了什么呢?

  1. 通过类的全限定名(Fully Qualified Name)获取到此类的二进制字节流。
  2. 将字节流所代表的静态存储结构转化为方法去的运行时数据结构
  3. 内存中生成一个代表这个类的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.classBoolean.TYPE
char.classCharacter.TYPE
byte.classByte.TYPE
short.classShort.TYPE
int.classInteger.TYPE
long.classLong.TYPE
float.classFloat.TYPE
double.classDouble.TYPE
void.classVoid.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),当我们访问这种常量字段时,不算主动引用。而对于要在运行时确定的常量,必须先对类进行初始化。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值