类加载机制

java.lang.reflect 包下的接口和类,包括 Class、Method、Field、Constructor、Array。这类分别代表类、方法、Field、构造器和数组。

Java 程序可以通过这些类动态地获取某个对象、某个类的运行时信息,并可以动态地创建 Java 对象,动态地调用 Java 方法,访问并修改指定对象的Field 值。

当程序主动使用某个类的时候,如果该类还没有被加载到内存中,则系统会通过加载、连接、初始化 3 个步骤对该类进行初始化。

类的加载

类的加载指的是将类的 class 文件加载到内存中,并为之创造一个 java.lang.Class 对象。

类的加载一般是由类加载器来完成,通常由 JVM 来提供,称为系统类加载器。

类加载器

Bootstrap ClassLoader: 根加载器

Extension ClassLoader: 扩展类加载器

System ClassLoader: 系统类加载器

类加载机制

1.全盘负责:

当一个类加载器负责加载某个 Class 时,该CLass所依赖的和引用的其他 Class 也将由该加载器负责载入,除非显式使用另外一个 类加载 载入;

2.父类委托:

先让 parent 加载器试图加载该 Class,只有在父类加载器无法加载该类时才尝试自己的类路径中加载该类。

3.缓存机制

保证所有被加载的 Class 都会被缓存,当程序需要使用 某个 Class 时,类加载先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取类的对应的二进制数据,并将其转换成 Class 对象,存入缓存区。

类的连接

分为三个阶段:

1.验证:验证阶段用于检验被加载的类是否有正确的内部结构。

2.准备:类准备阶段则负责为类的静态 Field 分配内存,并设置默认初始值。

3.解析:将类的二进制数据中的符号引用替换成直接引用。

类的初始化

类的初始化阶段,虚拟机负责对类进行初始化,主要是静态 Field 进行初始化。类的初始化一般包含以下几个步骤:

1.假如这个类还没有被加载,则程序先加载并连接该类;

2.假如该类的直接父类还没有被初始化,则初始化其父类;

3.假如类中有初始化语句,则系统依次执行这些初始化语句。

什么是反射?

在官方的API文档里面是对于反射所用到的包是这样说的

提供类和接口,以获得关于类和对象的反射信息。在安全限制内,反射允许编程访问关于加载类的字段、方法和构造方法的信息,并允许使用反射字段、方法和构造方法对其底层对等项进行操作。

从这个概念也可以知道反射大概是做什么的:即通过类或者接口的名字,来获得诸如已经加载类的字段、方法和构造方法等信息。

需要注意的是:默认只能获取原来类的 public 方法、字段和构造方法等信息。如果要获取 private 字段方法等,则需要设置 AccessibleObject 为true 来允许抑制访问检查。

反射的使用场景是什么?

1.基础框架

经典的就是在xml文件或者properties里面写好了配置,然后在Java类里面解析xml或properties里面的内容,得到一个字符串,然后用反射机制,根据这个字符串获得某个类的Class实例,这样就可以动态配置一些东西,不用每一次都要在代码里面去new或者做其他的事情,以后要改的话直接改配置文件,代码维护起来就很方便了,同时有时候要适应某些需求,Java类里面不一定能直接调用另外的方法,这时候也可以通过反射机制来实现。

2.软件的第三方插件

当你做一个软件可以安装插件的功能,你连插件的类型名称都不知道,你怎么实例化这个对象呢?因为程序是支持插件的(第三方的),在开发的时候并不知道 。所以无法在代码中 New出来 ,但反射可以,通过反射,动态加载程序集,然后读出类,检查标记之后再实例化对象,就可以获得正确的类实例。

3.扩展第三方应用

在编码阶段不知道那个类名,要在运行期从配置文件读取类名, 这时候就没有办法硬编码new ClassName(),而必须用到反射才能创建这个对象.反射的目的就是为了扩展未知的应用。比如你写了一个程序,这个程序定义了一些接口,只要实现了这些接口的dll都可以作为插件来插入到这个程序中。那么怎么实现呢?就可以通过反射来实现。就是把dll加载进内存,然后通过反射的方式来调用dll中的方法。很多工厂模式就是使用的反射。

一个反射的小例子

package study.ref;        
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

class TestObject {
    public int status;
    private int states = 12;
    public TestObject(int status) {
        this.status = status;
    }

    public void increase() {
        status++;
    }

    public void PrintSomething(String className) {
        System.out.println("Something to be printed!" + className);
    }
}

public class ReflectionTest {
    public static void TestFunction() {
        try {
            Constructor<TestObject> cons = TestObject.class
                    .getConstructor(int.class); // 获取构造方法
            TestObject test = cons.newInstance(10); // 调用Class对象的构造器的                                            //newInstance方法可以创建对象
            Method method = test.getClass().getMethod("increase"); // 得到方法
            method.invoke(test); // 调用方法
            Field field = test.getClass().getField("status"); // 得到变量
            field.set(test, 3);
            //注意此处因为在类中使用的变量是非公有的,故不能使用getField方法
            Field states = test.getClass().getDeclaredField("states");
            //设置其访问属性为true
            states.setAccessible(true);
            System.out.println(field.getInt(test));
            System.out.println(states.getInt(test));
            Method method2 = test.getClass().getMethod("PrintSomething",
                    String.class);
            method2.invoke(test, "MyOwnClass");
            System.out.println(method2.getParameterTypes());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        TestFunction();
        //System.out.println(int.class);
    }
}

运行结果:

3
12
Something to be printed!MyOwnClass
[Ljava.lang.Class;@139a55

使用反射的利弊

不足:

首先是编译期的安全保证问题。就像上面这个示例一样,你会发现如果getDeclaredField()方法调用的参数输错了,那么只有在运行期才能发现。要知道的是,寻找运行期Bug的难度要远远超过编译期的Bug。

然后是代价问题。JIT对反射的优化程度是不同的,有些优化时间会更长一些,而有些甚至是无法应用优化。因此,有时反射的性能损失可以达到几个数量级的差别。通知jvm要做的事情,性能比直接的java代码要慢很多。

优点:

可以用来做运行期类型的判断,动态类加载,动态代理使用反射,在AOP方面反射有着很大用处。

关于动态代理,请看下一篇文章。