第18章 类加载和反射

类的加载、连接和初始化

系统可能在第一次使用某个类时加载该类,也可能采用预先加载机制来预加载某个类。

类的加载

类的加载指的是类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象,也就是说当程序中使用任何类时,系统都会为之建立一个 java.lang.Class 对象。类的加载是由类加载器完成的,类加载器通常由 JVM 提供。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据:

  • 从本地文件系统来加载 class 文件
  • 从 JAR 包中加载 class 文件
  • 通过网络加载 class 文件
  • 把一个 Java 源文件动态编译、并执行加载
类的连接

当类被加载之后,系统为之生成一个对应的 Class 对象,接着将会进入连接阶段,连接阶段将会负责把类的二进制数据合并到 JRE 中。类连接又可分为如下三个阶段:

  • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
  • 准备:类准备阶段负责为类的静态属性分配内存,并设置默认初始值
  • 解析:将类的二进制数据中的符号引用替换成直接引用
类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态属性进行初始化。在 Java 类中对静态属性指定初始值有两种方式:(1)声明静态属性时指定初始值;(2)使用静态初始化块为静态属性指定初始值。

public class Test{
    //声明a变量时指定初始值
    static int a = 5;
    static int b;
    static int c;
}
static{
    //使用静态初始化块为b变量指定初始值
    b = 6}

静态属性 c 没有指定初始值,它将采用默认初始值 0。

JVM 初始化一个类包括如下几个步骤:

  1. 加入这个类还没有被加载和连接,程序先加载并连接该类
  2. 加入该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 加入类中有初始化语句,则系统依次执行这些初始化语句
    当执行到第二步时,系统对直接父类的初始化也遵循上面的三个步骤,如果该直接父类又有直接父类,系统将再次重复这三个步骤来初始化这个父类…。所以,JVM 最先初始化的总是 java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及其所有的父类都会被初始化。
类初始化的时机

当 Java 程序首次通过下面 6 种方式来使用某个类或接口时,系统会初始化该类或接口:

  1. 创建类的实例。为某个类创建实例的方式包括使用 new 操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
  2. 调用某个类的静态方法。
  3. 访问某个类或接口的静态属性,或为该静态属性赋值。
  4. 使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象。
  5. 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
  6. 直接使用 java.exe 命令来运行某个主类,当运行某个主类时,程序会先初始化该主类。
    下面几种情形需要特别指出:
    对于一个 final 型的静态属性,如果该属性在编译时就可以得到属性值,则可认为该属性可被当成编译时常量。当程序使用编译时常量时,系统会认为这是对该类的被动使用,所以不会导致该类的初始化。
class Tester{
    static{
        System.out.println("Tester类的静态初始化块...");
    }
}

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException{
        ClassLoader c1 = ClassLoader.getSystemClassLoader();
        //下面语句仅仅是加载Tester类
        c1.loadClass("Tester");
        //下面语句才会初始化Tester类
        Class.forName("Tester");
    }
}

类加载器

通过反射查看类的信息

Java 程序种许多对象在运行时会出现两种类型:编译时类型和运行时类型,例如:Person p = new Student();,这行代码将会生成一个 p 变量,该变量的编译类型为 Person,运行时类型为 Student。除此之外,还有程序在运行时接收外部传入的一个对象,该对象的编译类型是 Object,但程序又需要调用该对象运行时类型的方法。
为了解决这些问题,程序需要在运行时发现对象和类的真是信息,有两种做法:

  1. 假设在编译和运行时完全直到类型的具体信息,我们可以利用 instanceof 运算符进行判断,再利用强制类型转换将其转换为运行时变量。
  2. 第二种是编译时根本无法预知该对象和类可能属于哪些类,程序只能依靠运行时信息来发现该类和对象的真实信息,这就必须使用反射。
获取 Class 对象

Java 程序中获取 Class 对象的三种方法:

  1. 使用 Class 类的 forName() 静态方法。该方法需要传入字符串参数,参数值是某个类的全限定名。
  2. 调用某个类的 class 属性来获取该类的 Class 对象。例如 Person.class 将会返回 Person 类对应的 Class 对象。
  3. 调用某个对象的 getClass() 方法,该方法是 java.lang.Object 类中的一个方法,该方法将会返回该对象所属类对应的 Class 对象。
从 Class 中获取信息

Class 类提供大量实例方法来获取该 Class 对象所对应类的详细信息
四种方法用于访问 Class 对应的类所包含的构造器:

  • Constructor getConstructor(Class<?>… parameterTypes):返回此 Class 对象所表示的类的指定的 public 构造器
  • Constructor<?>[] getConstructors():返回此 Class 对象所表示的类的所有 public 构造器

四种方法用于访问 Class 对应的类所包含的方法:

  • Method getMethod(String name, Class<?>… parameterTypes):返回此 Class 对象所表示的类的指定 public 方法
  • Method[] getMethods():返回此 Class 对象所表示的类的所有 public 方法

如下四种方法用于访问 Class 对应的类所包含的属性:

  • Field getField(String name):返回此 Class 对象所表示的类的指定 public 属性
  • Field[] getField():返回此 Class 对象所表示的类的所有 public 属性

如下方法用于访问该 Class 对象对应类包含的内部类:

  • Class<?>[] getDeclaredClasses():返回此 Class 对象对应类里包含的全部内部类

如下方法用于访问该 Class 对象所对应类所继承的父类、所实现的接口等:

  • Class<?>[] getInterfaces():返回 Class 对象所对应的类所实现的全部接口
public class ClassTest {
    private ClassTest(){}

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

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

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

    //定义一个测试用的内部类
    class inner{

    }

    public static void main(String[] args) throws Exception{
        //获取ClassTest对应的Class(调用某个类的class属性来获取该类的Class对象)
        Class<ClassTest> clazz = ClassTest.class;
        //获取该Class对象所对应类的全部构造器
        Constructor[] ctors = clazz.getDeclaredConstructors();
        System.out.println("ClassTest的全部构造器如下:");
        for(Constructor c : ctors){
            System.out.println(c);
        }

        //获取该Class对象所对应类的全部public构造器
        Constructor[] publicCtors = clazz.getConstructors();
        System.out.println("ClassTest的全部public构造器如下:");
        for(Constructor c : publicCtors){
            System.out.println(c);
        }

        //获取该Class对象的全部public方法
        Method[] mtds = clazz.getMethods();
        System.out.println("ClassTest的全部public方法如下:");
        for(Method m : mtds){
            System.out.println(m);
        }

        //获取该Class对象对应类的指定方法
        System.out.println("ClassTest里带一个字符串参数的info方法 " + clazz.getMethod("info", String.class));

        
    }

}

通过反射生成并操作对象

创建对象

通过反射创建对象有如下两种方式:

  • 使用 Class 对象的 newInstance() 方法来创建 Class 对象对应类的实例
  • 先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法来创建该 Class 对象对应类的实例。
    通过第一种方式来创建实例是比较常见的情形,因为在很多 Java EE 框架中都需要根据配置文件信息来创建 Java 对象,从配置文件读取的只是某个类的字符串类名,程序需要根据该字符串创建对应类的实例,就必须使用反射。
调用方法

当获取某个类对应的 Class 对象后,就可以通过该 Class 对象的 getMethods() 方法或 getMethod() 方法来获取全部方法或指定方法——这两个方法的返回值是 Method 对象数组,或者Method 对象。
每个 Method 对象对应一个方法,获取 Method 对象后,程序就可以通过该 Method 来调用对应的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值