18.类加载和反射

本章要点

  • 类加载
  • 类连接的过程
  • 类初始化的过程
  • 类加载器以及实现机制
  • 继承ClassLoader实现自定义类加载器
  • 使用URLClassLoader
  • 使用Class对象
  • 动态创建java对象
  • 动态调用方法
  • 访问,并修改java对象的属性值
  • 使用反射操作数组
  • 使用Proxy和InvacatonHandler创建动态代理
  • AOP入门
  • Class类的泛型
  • 通过反射获取泛型类型

18.1 类的加载,连接和初始化

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

18.1.1 JVM和类

当我们调用java命令运行某个java程序时,该命令将会启动一条java虚拟机进程,不管该java程序有多么复杂,该程序启动了多少个线程,他们都处于该java虚拟机进程里。正如前面介绍的,同一个JVM的所有线程,所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止:

  • 程序运行到最后正常结束。
  • 程序运行到使用System.exit()或Runtime.getRuntime().exit()代码结束程序。
  • 程序执行过程中遇到未捕获的异常或错误而结束。
  • 程序所在平台强制结束了JVM的进程。

18.1.2 类的加载

类是某一类对象的抽象,类是概念层次的东西,但不知道读者有没有想过:类也是一种对象,就像我们说概念主要用于定义,描述其他事物的,但概念本身也是一种事物,那么概念本身也需要被描述--呵呵,这有点像一个哲学命题,但事实就是这样,每个类是一批具有相同特征的对象的抽象(或者说概念),而系统中所有的类,他们实际上也是对象,他们都是java.lang.Class的实例。

18.1.3 类的连接

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

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

18.1.4 类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态属性进行初始化。在java类中对静态属性指定初始值有两种方式:

  1. 声明静态属性时指定初始值;
  2. 使用静态初始化块为静态属性指定初始值。

声明变量时指定初始值,静态初始化块都将被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行他们。

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

  • 假如这个类还没有被加载和连接,程序先加载并连接该类。
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
  • 假如类中有初始化语句,则系统依次执行这些初始化语句。

当执行第二步骤时,系统对直接父类的初始化步骤也遵循此3个步骤,如果该直接父类又有直接父类,系统再次重复这三个步骤来先初始化这个父类.....依次类推,所以JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。

18.1.5 类初始化的时机

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

  • 创建类的实例。为某个类创建实例的方式包括使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
  • 调用某个类的静态方法。
  • 访问某个类或接口的静态属性,或为该静态属性赋值。
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
  • 初始化某个类的子类,当初始化某个类的子类时,该子类的所有父类都会被初始化。
  • 直接使用java.exe命令来运行某个主类,当运行某个主类时,程序会先初始化该主类。

除此之外,下面有几种情形需要特别指出:

对于一个final型的静态属性,如果该属性可以在编译时就得到属性值,则可认为该属性可被当成编译时常量。当程序使用编译时常量时,系统会认为这是对该类的被动调用,所以不会导致该类的初始化。

反之,如果final类型的静态属性的值不能在编译时得到,必须等到运行时才可以确定该属性的值,如果通过该类来访问该静态属性,则可以认为是主动访问使用该类,将会导致该类被初始化。

18.2 类加载器

类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。尽管java开发中无须过分关心类加载机制,但所有的编程人员都应该了解其工作机制,明白如何做才能让其更好地满足我们的需要。

18.2.1 类加载器简介

类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。现在的问题是怎么样才算“同一个类”?正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。

同理,载入JVM的类也有一个唯一的标识,在java中,一个类用其全限定类名(包括包名和类名)作为标识。但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。因此,如果在pg的包中,有一个名为Person的类,被类加载器KlassLoader的实例kl负责加载。则该Person类对应的Class对象在JVM中表示为(Person,pg,kl)。这意味着两个类加载器加载的同名类:(Person,pg,kl)和(Person,pg,kl2)是不同的,它们所加载的类也是完全不同,互不兼容的。

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

  • Bootstrap ClassLoader:根类加载器
  • Extension ClassLoader:扩展类加载器
  • System ClassLoader:系统类加载器

Bootstrap ClassLoader,被称为引导(也称为原始或根)类加载器。它负责加载java的核心类。在Sun的JVM中,当执行java.exe的命令时使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。

根类加载器非常特殊,它并不是java.lang.ClasLoader的子类,而是由JVM自身实现的。

Extension Classloader,被称为扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR的类包。

通过这种方式,我们就可以为java扩展核心类以外的新功能,只要我们把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。

System Classloader,被称为系统(也称为应用)类加载器,它负责在JVM启动时,加载来自命令java中的-classpath选项或java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。

程序可以通过ClassLoader.的静态方法getSystemClassLoader()获取该类加载器。如果没有特别指定,则用户自定义的类加载器都以该类加载器作为它的父类加载器。

18.2.2 类加载机制

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

  • 全盘负责:所谓全盘负责,就是说当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
  • 父类委托:所谓父类委托则是先让parent(父)类加载器试图加载该class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
  • 缓存机制:缓存机制将会保证所有被加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜寻该Class,只有当缓存中不存在该Class对象时,系统才会重读该类对应的二进制数据,并将其转换成Class对象,并存入cache。这就是为什么我们修改了Class后,程序必须重新启动JVM,程序所作的修改才会生效的原因。

提示:

类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。

18.2.3 创建并使用自定义的类加载器

JVM中除根加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。

ClassLoader类有如下三个关键方法:

  • loadClass(String name,boolean resolve):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的class对象。
  • findClass(String name):根据二进制名称来查找类。
  • Class defineClass(Strinng name,byte[] b,int off,int len),该方法负责将指定类的字节码文件读入字节数组,并把它转化为Class对象,该字节码文件可以来源于文件,网络等。

18.2.4 URLClassLoader 类

java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处是父类,而不是父类加载器,这里是类与类之间的继承关系),URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。

实际上应用程序中可以直接使用URLClassLoader来加载类,提供了如下两个构造器:

  • URLClassLoader(URL【】urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。
  • URLClassLoader(URL[] urls,ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同。

一旦得到了URLClassLoader对象之后,就可以调用该对象的loadClass方法来加载指定类。

18.3 通过反射查看类信息

java程序中许多对象在运行时都会出现两种类型:编译时类型和运行时类型。

除此之外,还有更极端的情形,程序在运行时接收到外部传入的一个对象,该对象的编译类型是Object,但程序又需要调用该对象运行时类型的方法。

为了解决这些问题,程序需要在运行时发现对象和类的真实信息,为了解决这个问题,我们有两种做法:

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

18.3.1 获得Class对象

前面已经介绍过了,每个类被加载之后,系统就会为该类生成一个对应的Class对象,通过该Class对象就可以访问到JVM中的这个类,java程序中获得Class对象通常有如下三种方式:

  • 使用Class类的forName()静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。
  • 调用某个类的class属性来获取该类对应类Class对象。
  • 调用某个对象的getClass()方法,该方法是Object类中的一个方法,所以所有java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。

对于第一种方式和第二种方式都是直接根据类来取得该类的Class对象,但相比之下,第二种方式有如下两种优势:

  • 代码更安全,程序在编译阶段就可以检查需要访问的Class对象是否存在。
  • 程序性能更高,因为这种方式无须调用方法,所以性能更好。

一旦获得了某个类所对应的Class对象之后,程序就可以调用Class对象的方法来获得该对象和该类的真实信息了。

18.3.2 从Class中获取信息

对于只能在源代码上保留的注释,使用运行时获得的Class对象无法访问到该注释对象。

通过Class对象可以得到了大量的Method,Constructor,Field等对象,这些对象分别代表该类所包括的方法,构造器和属性等,程序还可以通过这些对象来执行实际的功能:例如调用方法,创建实例。

18.4 使用反射生成并操作对象

18.4.1 创建对象

通过反射来生成对象有如下两种方式:

  • 使用Class对象的newInstance()方法来创建该对象对应类的实例,这种方式要求该Class对象的对应类有默认构造器,而执行newInstance()方法时实际上是利用默认构造器来创建该类的实例。
  • 先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。通过这种方式可以选择使用某个类的指定构造器来创建实例。

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

18.4.2 调用方法

getMethods()

invoke()

18.4.3 访问属性值

getFields()

18.4.4 操作数组

18.5 使用反射生成JDK动态代理

18.6 反射和泛型

猜你喜欢

转载自blog.csdn.net/qq_21874145/article/details/82107670
18.