Java核心技术梳理-类加载机制与反射

1、引言

反射机制是一个很是好用的机制,C#和Java中都有反射,反射机制简单来讲就是在程序运行状态时,对于任意一个类,可以知道这个类的全部属性和方法,对于任意一个对象,可以调用它的任意属性和方法,其实初听就知道反射是一个比较暴力的机制,它可能会破坏封装性。java

经过反射的定义咱们能够想到反射的好处:能够灵活的编写代码,代码能够在运行时装配,下降代码的耦合度,动态代理的实现也离不开反射。api

为了更好的理解反射,咱们先了解下JVM中的类加载机制。数组

2、类加载机制

当程序要使用某个类时,若是这个类还未加载到内存,则须要将其加载到内存中,JVM会经过加载、链接、初始化三个步骤来对该类进行初始化。缓存

2.1 类加载

类的加载由类加载器完成,类加载器一般由JVM提供的,JVM提供的类加载器一般称为系统加载器,开发者能够经过继承ClassLoader基类来建立本身的类加载器。安全

类加载加载的是一个二进制数据,这些数据的来源有几种:网络

  • 本地文件系统加载class文件。函数

  • 从JAR包中记载class文件。spa

  • 经过网络加载class文件。代理

  • 把一个java文件动态编译,并执行加载code

类加载不必定是要等到首次使用时才加载,虚拟机容许系统预先加载某些类

2.2 类链接

在类被加载后,系统会为之生成一个对应的Class对象,接着进入链接阶段,链接阶段是把类的二进制数据合并到JRE中,类链接分为三个阶段:

  1. 验证:检验被加载的类是否有正确的内部结构,并和其余的类协调一致。

  2. 准备:负责为类变量分配内存,并设置默认初始化值。

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

2.3 类的初始化

虚拟机负责对类进行初始化,主要是对变量进行初始化,对类变量指定初始值有两种方式:

  • 声明类变量时指定初始值。

  • 使用静态初始化块为类变量指定初始值。

JVM初始化的几个步骤:

  1. 假如该类尚未被加载和链接,则程序先加载或链接该类。

  2. 假如该类的直接父类没有初始化,则先初始化这该类的父类。

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

能够看出当程序主动使用某个类时,必定会保证该类及其全部父类都被初始化。那么在什么状况下系统会初始化一个类活着接口呢?

  • 建立类的实例,既包括使用new来建立,也包括经过反射来建立和反序列化来建立。

  • 调用某个类的静态方法。

  • 访问某个类或接口的类变量。

  • 使用反射方式来强制建立某个类或接口对应的java.jang.class对象。

  • 初始化某个类的子类。

  • 使用java.exe命令来运行某个主类。

3、 类加载器

类加载器是负责将.class文件加载到内存中,并生成对应的java.lang.Class实例,一旦一个类被载入到JVM中,就不会被载入了,这里就存在一个惟一标识的问题,JVM是经过其全限定类名和其加载器来作惟一标识的,便是经过包名,类名,及加载器名。这也意味着不一样类加载器加载的同一个类是不一样的。

3.1 类加载机制

当JVM启动时,会造成三个类加载器组成的初始类加载器层次结构:

  • Bootstrap ClassLoader:根类加载器,负责加载Java的核心类,是由JVM自身提供的。

  • Extension ClassLoader:扩展类记载器,负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext)中JAR包中的类。

  • System ClassLoader:系统类加载器,负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量指定的JAR包和类路径。

JVM的类加载机制主要有三种:

  • 全盘负责:就是当一个类加载器负责加载某个Class时,该class所依赖的和引用的其余Class也将由该类加载器负责载入,除非显示使用另一个类加载器载入。

  • 父类委托:先让父类加载器试图加载该Class,只有当父类加载器没法加载该类时才尝试从本身的类路径中加载该类。

  • 缓存机制:全部加载过的Class都会被缓存,这就是为何修改代码后必须从新启动JVM修改才会生效的缘由。

类加载器加载Class大体以下:

  1. 检测此Class是否载入过(经过缓存查询),跳至第8步。

  2. 若是父类加载器不存在,则跳至第4步,若是父类加载器存在,则执行第3步。

  3. 请求使用父类加载器去载入目标类,成功则跳到第8步,不然跳到5步。

  4. 请求使用根类加载器来载入目标类,成功则调到第8步,不然跳至7步。

  5. 当前类加载器尝试寻找Class文件,找到执行第6步,找不到则跳入7步。

  6. 从文件中载入Class,成功跳入第8步。

  7. 抛出ClassNotFoundException异常。

  8. 返回对应的java.lang.Class对象。

3.2 自定义类加载器

JVM中除了根类加载器外的全部类加载器都是ClassLoader子类的实例,咱们能够扩展ClassLoader的子类,并重写其中的方法来实现自定义加载器。ClassLoader有两个关键方法:

  • loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类

  • findClass(String name):根据指定名称来查找类。

一般推荐findClass(String name),重写findClass()方法能够避免覆盖默认类加载器的父类委托和缓存机制两种策略。ClassLoader中还有一个核心方法Class<?> defineClass(String name, byte[] b, int off, int len),该方法负责将指定类的字节码文件读入到数组中,并把它转换成Class对象,不过这个方法是final,不须要咱们重写。

4、反射

前面的经过类加载机制咱们知道,每一个类被加载后,系统会为该类生成一个对应的Class对象,经过这个对象就能够访问到JVM中的这个类,在程序中获取到Class的方式有三种:

  1. 使用Class类中的forName(String name)静态方法,传入的参数是某个类的全限定名。

  2. 调用某个类的class属性来获取该类对应的的Class对象。

  3. 调用某个对象的getClass()方法。

在第一个和第二个方法中,都是经过类来获取到Class对象,可是很明显第二种更安全也效率更高。

4.1 获取Class信息

当咱们获取到Class对象后咱们能够根据Class来获取信息,Class类提供了大量的方法来获取Class对应类的详细信息,类中主要的信息包括:构造函数、方法、属性、注解,另外还有一些基本属性如:类的修饰符、类名、所在包等。

构造函数

  • Constructor<?>[] getConstructors():返回此Class对象对应类的全部public构造函数。

  • Constructor<?>[] getDeclaredConstructors():返回此Class对象对应类的全部构造函数,无权限限制

  • Constructor<T> getConstructor(Class<?>... parameterTypes):返回此Class对象对应类的、带指定形参的列表的public构造函数。

  • Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes):返回此Class对象对应类的、带指定形参的列表的构造函数,无权限限制。

方法中存在Declared表示无权限限制,后面的也与此相同,后面就不列出

方法:

  • Method getMethod(String name, Class<?>... parameterTypes):返回此Class对象对应类的、带指定形参的列表的public方法。

  • Method[] getMethods():返回此Class对象对应类的全部public方法

成员变量:

  • Field[] getFields():返回此Class对象对应类的全部public成员变量。

  • Field[] getField(String name):返回此Class对象对应类的、指定名称的public成员变量

注解:

  • Annotation[] getAnnotations():返回修饰该Class对象对应类的全部注解

  • <A extends Annotation> A getAnnotation(Class<A> annotationClass):获取该Class对象对应类存在的、指定类型的注解,若是不存在,则返回 null。

内部类:

  • Class<?>[] getDeclaredClasses():获取该Class对象对应类里包含的所有内部类。

外部类:

  • Class<?> getDeclaringClass():获取该Class对象对应类里包含的所在的外部类

接口:

  • Class<?>[] getInterfaces():获取该Class对象对应类里所实现的所有接口

基本信息:

  • int getModifiers():返回此类或接口的全部修饰符,返回的int类型须要解码。

  • Package getPackage():获取此类包名。

  • String getName():获取该Class对象对应类的名称。

  • String getSimpleName():获取该Class对象对应类的简称。

  • boolean isAnnotation():返回Class对象是否表示一个注解类型。

  • boolean isArray():Class对象是不是一个数组。

这里将大致可以获取的类信息列出来了:

public class ClassTest {

    private ClassTest() {
    }

    public ClassTest(String name) {
        System.out.println("有参数的构造函数");
    }

    public void info() {
        System.out.println("无参数的方法");
    }

    public void info(String name) {
        System.out.println("有参数的方法");
    }

    //内部类
    class inner {
    }

    public static void main(String[] args) throws NoSuchMethodException {
        Class<ClassTest> clazz = ClassTest.class;
        Constructor<?>[] constructors = clazz.getConstructors();
        System.out.println("所有public构造器以下:");
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }
        Constructor<?>[] pubConstructors = clazz.getDeclaredConstructors();
        System.out.println("所有构造器以下:");
        for (Constructor constructor : pubConstructors) {
            System.out.println(constructor);
        }
        Method[] methods = clazz.getMethods();
        System.out.println("所有public方法以下");
        for (Method method : methods) {
            System.out.println(method);
        }
        System.out.println("名称为info,而且入参为String类型的方法:" + clazz.getMethod("info", String.class));

    }
}

4.2 生成并操做对象

4.2.1 生成对象

生成对象的方式有两种,一种是直接调用newInstance()方法,这种方式是用默认构造器来建立对象,还有一种方式是先得到Constructor对象,而后用Constructor调用newInstance()来建立对象。

Class<ClassTest> clazz = ClassTest.class;
ClassTest classTest = clazz.newInstance();
classTest.info();
Constructor<ClassTest> constructor = clazz.getConstructor(String.class);
ClassTest class2 = constructor.newInstance("你好");
class2.info();

4.2.2 调用方法

上面的例子中,咱们能够明确的知道返回的是哪一个类,全部调用的方法也和以前对象调用方法没有区别,可是通常在用反射机制时,咱们是不知道具体类的,这个时候咱们可使用getMethod获取方法,而后使用invoke来进行方法调用:

Class<?> aClass = Class.forName("com.yuanqinnan.api.reflect.ClassTest");
//建立了对象
Object object = aClass.newInstance();
//获取到方法
Method info = aClass.getMethod("info", String.class);
//调用方法
info.invoke(object, "你好");

4.2.3 访问成员变量

通常状况下,咱们会使用getXXX()方法和setXXX(XXX)方法来设置或者获取成员变量,可是有了反射后,咱们能够直接对成员变量进行操做:

public class Person {

    private int age;
    private String name;

    public String toString() {
        return name + ":" + age;
    }

    public static void main(String[] args) throws Exception {
        Person p = new Person();
        Class<Person> personClass = Person.class;
        Field name = personClass.getDeclaredField("name");
        //去掉访问限制
        name.setAccessible(true);
        name.set(p, "张三");

        Field age = personClass.getDeclaredField("age");
        age.setAccessible(true);
        age.set(p, 20);
        System.out.println(p.toString());
    }
}

从这里能够看出来,反射破坏了封装性。

相关文章
相关标签/搜索