《疯狂java讲义》第18章 类加载机制与反射

第18章 类加载机制与反射

本章将重点介绍java.lang.reflect包下的接口和类,包括Class、Method、Field、Constructor和Array等,这些类分别代表类、方法、成员变量、构造器和数组,Java程序可以使用这些类动态地获取某个对象、某个类的运行时信息,并可以动态地创建Java对象,动态地调用Java方法,访问并修改指定对象的成员变量值。本章还将介绍该包下的Type和ParameterizedType两个接口,其中Type是Class类所实现的接口,而ParameterizedType则代表一个带泛型参数的类型。

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

18.1.1 JVM和类

  • 当调用java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。
  • 同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。
  • JVM进程将被终止的几种情况:

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

  • 当Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。

18.1.2 类的加载

  • 当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载连接初始化三个步骤来对该类进行初始化。JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。
  • 类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
  • 类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类创建自己的类加载器
  • 通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
    (1) 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
    (2) 从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
    (3)通过网络加载class文件。
    (4)把一个Java源文件动态编译,并执行加载。
  • 类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类

18.1.3 类的连接

  • 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中
  • 类连接又可分为如下三个阶段。
    (1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
    (2)准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值。
    (3)解析:将类的二进制数据中的符号引用替换成直接引用。

18.1.4 类的初始化

  • 类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化。在Java类中对类变量指定初始值有两种方式:(1)声明类变量时指定初始值;(2)使用静态初始化块为类变量指定初始值。
  • JVM初始化一个类包含如下几个步骤:
    (1)假如这个没有被加载和连接,则程序先加载并连接该类。
    (2)假如该类的直接父类没有被初始化,则先初始化其直接父类。
    (3)假如类中有初始化语句,则系统依次执行这些初始化语句。
  • 当执行第2个步骤时,系统对直接父类的初始化步骤也遵循此步骤1~3;如果该直接父类有直接父类,则系统再次重复这三个步骤来先初始化这个父类……依此类推,所以JVM最先初始化的总是java.lang.Object类。

18.1.5 类初始化的时机

  • 当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口
    (1) 创建类的实例。为某个类创建实例的方式包括:使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
    (2)调用某个类的类方法(静态方法)。
    (3)访问某个类或接口的类变量,或为该类变量赋值。
    (4)使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如代码:Class.forName(“Person”),如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象。
    (5)初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
    (6)直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。
  • 对于final型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。Java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。
  • 如果final修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化

18.2 类加载器

类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。

18.2.1 类加载机制

  • 一个载入JVM中的类有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识。
  • 当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。

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

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

  • 类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
    在这里插入图片描述

  • 类加载器加载Class大致要经过如下8个步骤:
    (1) 检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则接着执行第2步。
    (2)如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步。
    (3)请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步。
    (4)请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步。
    (5)当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。
    (6)从文件中载入Class,成功载入后跳到第8步。
    (7)抛出ClassNotFoundException异常。
    (8)返回对应的java.lang.Class对象。
    其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。

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

  • JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。
  • ClassLoader中包含了大量的protected方法——这些方法都可被子类重写
  • ClassLoader类有如下两个关键方法:

loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。
findClass(String name):根据指定名称来查找类。

  • 如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写findClass()方法,而不是重写loadClass()方法。
  • loadClass()方法的执行步骤如下。
    (1) 用findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回。
    (2)在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载。
    (3)调用findClass(String)方法查找类。
  • 从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
  • 在ClassLoader里还有一个核心方法:Class defineClass(String name,byte [] b,int off,int len),该方法负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等。

18.2.3 URLClassLoader类

  • Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处的父类,就是指类与类之间的继承关系)。URLClassLoader既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。
  • 在应用程序中可以直接使用URLClassLoader加载类,URLClassLoader类提供了如下两个构造器。

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

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

18.3 通过反射查看类信息

18.3.1 获得Class对象

  • 每个类被加载之后,系统就会为该类生成一个对应的Class对象,通过该Class对象就可以访问到JVM中的这个类。
  • 在Java程序中获得Class对象通常有如下三种方式:
    (1) 使用Class类的forName(String clazzName)静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。
    (2)调用某个类的class属性来获取该类对应的Class对象。例如,Person.class将会返回Person类对应的Class对象。
    (3)调用某个对象的getClass()方法。该方法是java.lang.Object类中的一个方法,所以所有的Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。
  • 一旦获得了某个类所对应的Class对象之后,程序就可以调用Class对象的方法来获得该对争和该类的真实信息了。

18.3.2 从Class中获取信息

  • Class类提供了大量的实例方法来获取该Class对象所对应类的详细信息,Class类大致包含如下方法,下面每个方法都可能包括多个重载的版本,读者应该查阅API文档来掌握它们。
  • 通过Class对象可以得到大量的Method、Constructor、Field等对象,这些对象分别代表该类所包括的方法、构造器和成员变量等,程序还可以通过这些对象来执行实际的功能,例如调用方法、创建实例。

18.3.3 方法参数反射

  • Java 8在java.lang.reflect包下新增了一个Executable抽象基类,该对象代表可执行的类成员,该类派生了Constructor、Method两个子类。
  • Executable基类提供了大量方法来获取修饰该方法或构造器的注解信息;还提供了isVarArgs()方法用于判断该方法或构造器是否包含数量可变的形参,以及通过getModifiers()方法来获取该方法或构造器的修饰符。
  • Executable提供了如下两个方法来获取该方法或参数的形参个数及形参名。

int getParameterCount():获取该构造器或方法的形参个数。
Parameter[] getParameters():获取该构造器或方法的所有形参。

  • 上面第二个方法返回了一个Parameter[] 数组,Parameter也是Java 8新增的API,每个Parameter对象代表方法或构造器的一个参数。
  • Parameter也提供了大量方法来获取声明该参数的泛型信息,还提供了如下常用方法来获取参数信息。

getModifiers():获取修饰该形参的修饰符。
String getName():获取形参名。
Type getParameterizedType():获取带泛型的形参类型。
Class<?> getType():获取形参类型。
boolean isNamePresent():该方法返回该类的class文件中是否包含了方法的形参名信息。
boolean isVarArgs():该方法用于判断该参数是否为个数可变的形参。

  • 需要指出的是,使用javac命令编译Java源文件时,默认生成的class文件并不包含方法的形参名信息,因此调用isNamePresent()方法将会返回false,调用getName()方法也不能得到该参数的形参名。如果希望javac命令编译Java源文件时可以保留形参信息,则需要为该命令指定-parameters选项。

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

Class对象可以获得该类里的方法(由Method对象表示)、构造器(由Constructor对象表示)、成员变量(由Field对象表示),这三个类都位于java.lang.reflect包下,并实现了java.lang.reflect.Member接口。程序可以通过Method对象来执行对应的方法,通过Constructor对象来调用对应的构造器创建实例,能通过Field对象直接访问并修改对象的成员变量值。

18.4.1 创建对象

  • 通过反射来生成对象需要先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。
  • 如果不想利用默认构造器来创建Java对象,而想利用指定的构造器来创建Java对象,则需要利用Constructor对象,每个Constructor对应一个构造器。
  • 为了利用指定的构造器来创建Java对象,需要如下三个步骤。
    (1)获取该类的Class对象。
    (2)利用Class对象的getConstructor)方法来获取指定的构造器。
    (3)调用Constructor的newInstance)方法来创建Java对象。

18.4.2 调用方法

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

Object invoke(Object obj, Object...args):该方法中的obj是执行该方法的主调,后面的args是执行该方法时传入该方法的实参。

  • 当通过Method的invoke()方法来调用对应的方法时,Java会要求程序必须有调用该方法的权限
  • 如果程序确实需要调用某个对象的private方法,则可以先调用Method对象的如下方法。

setAccessible(boolean flag):将Method对象的accessible设置为指定的布尔值。值为true,指示该Method在使用时应该取消Java语言的访问权限检查;值为false,则指示该Method在使用时要实施Java语言的访问权限检查。

18.4.3 访问成员变量值

  • 通过Class对象的getFields()getField()方法可以获取该类所包括的全部成员变量或指定成员变量。
  • Field提供了如下两组方法来读取或设置成员变量值。

getXxx(Object obj):获取obj对象的该成员变量的值。此处的Xxx对应8种基本类型,如果该成员变量的类型是引用类型,则取消get后面的Xxx。
setXxx(Object obj, Xxx val):将obj对象的该成员变量设置成val值。此处的Xxx对应8种基本类型,如果该成员变量的类型是引用类型,则取消set后面的Xxx。

  • 使用这两个方法可以随意地访问指定对象的所有成员变量包括private修饰的成员变量。

18.4.4 操作数组

  • 在java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有的数组。程序可以通过使用Array来动态地创建数组,操作数组元素等。
  • Array提供了如下几类方法。

static Object newInstance(Class<?>componentType, int…length):创建一个具有指定的元素类型、指定维度的新数组。
static xxx getXxx(Object array, int index):返回array数组中第index个元素。其中xxx是各种基本数据类型,如果数组元素是引用类型,则该方法变为get(Object array,intindex)。
static void setXxx(Object array, int index, xxx val):将array数组中第index个元素的值设为val。其中xxx是各种基本数据类型,如果数组元素是引用类型,则该方法变成set(Object array,int index,Object val)。

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

18.5.1 使用Proxy和InvocationHandler创建动态代理

  • Proxy提供了用于创建动态代理类和代理对象的静态方法,它也是所有动态代理类的父类。如果在程序中为一个或多个接口动态地生成实现类,就可以使用Proxy来创建动态代理类;如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。
  • Proxy提供了如下两个方法来创建动态代理类和动态代理实例。

static Class<?> getProxyClass(ClassLoader loader, Class<?>…interfaces):创建一个动态代理类所对应的Class对象,该代理类将实现interfaces所指定的多个接口。第一个ClassLoader参数指定生成动态代理类的类加载器。
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h):直接创建一个动态代理对象,该代理对象的实现类实现了interfaces指定的系列接口,执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法。

  • 实际上,即使采用第一个方法生成动态代理类之后,如果程序需要通过该代理类来创建对象,依然需要传入一个InvocationHandler对象。也就是说,系统生成的每个代理对象都有一个与之关联的InvocationHandler对象。
  • 当程序使用反射方式为指定接口生成系列动态代理对象时,这些动态代理对象的实现类实现了一个或多个接口。动态代理对象就需要实现一个或多个接口里定义的所有方法,当执行动态代理对象里的方法时,实际上会替换成调用InvocationHandler对象invoke方法。

18.5.2 动态代理和AOP

  • 这种动态代理在AOP(Aspect Orient Programming,面向切面编程)中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。
  • 但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理。
    在这里插入图片描述

18.6 反射和泛型

从JDK 5以后,Java的Class类增加了泛型功能,从而允许使用泛型来限制Class类,例如,String.class的类型实际上是Class<String>。如果Class对应的类暂时未知,则使用Class<?>。通过在反射中使用泛型,可以避免使用反射生成的对象需要强制类型转换

18.6.1 泛型和Class类

  • 使用Class<T>泛型可以避免强制类型转换。

18.6.2 使用反射来获取泛型信息

  • 普通类型成员变量:通过指定类对应的Class对象,可以获得该类里包含的所有成员变量,不管该成员变量是使用private修饰,还是使用public修饰。获得了成员变量对应的Field对象后,就可以很容易地获得该成员变量的数据类型
  • 对成员变量有泛型类型的的类型:应先使用如下方法来获取成员变量的泛型类型;
//获得成员变量f的泛型类型
Type gType = f.getGenericType();

然后将Type对象强制类型转换ParameterizedType对象,ParameterizedType代表被参数化的类型,也就是增加了泛型限制的类型。

  • ParameterizedType类提供了如下两个方法:

getRawType():返回没有泛型信息的原始类型。
getActualTypeArguments():返回泛型参数的类型。

  • 使用getType()方法只能获取普通类型的成员变量的数据类型;对于增加了泛型的成员变量,应该使用getGenericType()方法来取得其类型。
  • Type也是java.lang.reflect包下的一个接口,该接口代表所有类型的公共高级接口,Class是Type接口的实现类。Type包括原始类型、参数化类型、数组类型、类型变量和基本类型等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值