一、类的加载机制
Java程序运行需要使用某个类时,如果该类还没有加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化。
1.类加载
当我们运行java.exe命令执行某个Java程序时,由于Java程序本身以.class字节码的形式存在,它不是一个可执行文件,所以需要JVM将类文件加载到内存中。
类的加载由类加载器完成。JVM本身包含了一个类加载器,称为根类加载器。和JVM一样,根类加载器是用本地代码实现的,它负责加载核心Java类(即所有java.*开头的类)。
另外,JVM还会提供两个类加载器,它们都是用Java语言编写的,由根类加载器加载。其中,扩展类加载器负责加载扩展的Java类,包括所有java.*开头的类和存放在JRE的扩展目录下中JAR的类包;系统类加载器负责加载应用程序自身的类。
此外,Java API中还提供了一个ClassLoader抽象类,开发者还可以通过继承ClassLoader基类来创建自定义的类加载器。
当我们运行Java.exe命令执行一个Java程序时,程序最基本的加载流程如下:
(1)java.exe程序搜索jre目录,寻找JVM.dll,并启动JVM。
(2)JVM运行根类加载器,该根类加载器加载Java核心API。
(3)根类加载器运行后,它会自动加载扩展类加载器和系统类加载器,并将扩展类加载器的父类设置为根类加载器,将应用加载器的父类设置为扩展类加载器。
(4)扩展类加载器加载搜索JAVA_HOME/jre/lib/ext目录,加载扩展API。
(5)应用加载器搜索CLASSPATH目录,加载我们要运行的类。
(6)类的class文件读入内存后,就会创建一个java.lang .Class对象。一旦某个类载入JVM中,同一个类就不会再次被载入。
一个类加载后,对应的Class对象,可以通过该类的实例的getClass()方法得到。Class对象有一个getClassLoader()方法,可以得到加载该类所用到的类加载器。
2.连接
当类被加载后,系统就为之创建一个对应的Class对象,接着就会进入连接阶段。连接阶段会负责吧类的二进制数据合并到JRE中。类连接又可以分为如下三个阶段:
(1)验证:检验被加载的类是否有正确的内部结构,并和其它类协调一致。
(2)准备:负责为类的静态属性分配内存,并设置默认初始值。
(3)解析:将类的二进制数据中的符号引用替换成直接引用。
3.初始化
随后,JVM负责对类进行初始化,也就是对静态属性进行初始化。在Java类中,对静态属性指定初始值的方式有两种:(1)声明静态属性时指定初始值;(2)使用静态初始化块为静态属性指定初始值。
JVM 初始化一个类,一般包含如下几个步骤:
(1)加入这个类还没有被加载和连接,程序先加载并连接该类。
(2)加入该类的直接父类还没有被初始化,则先初始化其直接父类。
(3)加入该类中有初始化语句,则系统一次执行这些初始化语句。
当执行第二步时,系统对直接父类的初始化步骤也遵循这三个步骤。如果该直接父类又有直接父类,系统再次重复着三个步骤,一次类推。所以,JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及它的所有父类都会被初始化。
当java程序首次通过下面六种方式使用某个类或者接口时,系统就会初始化该类或者接口:
(1)创建类的实例。
(2)调用某个类的静态方法。
(3)访问某个类或接口的静态属性,或者为静态属性赋值。
(4)使用反射方式强制创建某个类或接口对应的java.lang.Class对象。
(5)初始化某个类的子类。
(6)直接使用java.exe命令运行某个主类。
二、反射
Java程序中的对象在运行时会出现两种类型:编译时类型和运行时类型。
反射是指在Java中,可以在运行期载入、探知和使用编译期完全未知的类。换句话说,Java程序可以装载一个运行期猜得到名称的类,获取其完整结构,并创建对象、或者对类的成员变量设定值、调用方法以及动态创建和访问数组。
1.使用反射查看类信息
每个Java被加载后,系统都会为该类生产一个对应的java.lang.Class对象。通过Class对象,我们就可以访问JVM中的这个类。在java程序中获得Class对象的方法有三种:
(1)在编译期不知道类名,但是在运行期可以获得该类名的时候,使用Class类的forName()静态方法可以获得Class对象。
(2)如果在编译期知道类名的情况下,可以调用该类的class属性来获得该类对象的Class对象。
(3)如果一个类的实例对象已经得到,则调用该对象的getclass()方法返回该对象所属类对应的Class对象。getClass()方法是java.lang.Object类的方法之一,所以所有对象都可以调用该方法。
一旦获得了某个类对应的Class对象,程序就可以调用Class对象的方法来获取该对象和该类的真实信息。
2.使用反射生成并操纵对象
(1)创建对象
第一种:使用Class对象的newInstance()方法来创建该Class对象对应类的实例。这种方式要求Class对象的对应类有默认构造器,执行newInstance()方法时,实际上是调用默认的构造器来创建实例。用这种方式创建对象比较常见,后面我们要学习到很多JavaEE开源框架都是采用这种方式创建对象。
第二种:先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。通过这种方式可以选择使用某个类的指定构造器来创建实例。
(2)调用方法
当获得某个类对应的Class对象后,就可以通过该Class对象的getMethods()方法或getMethod()方法获取全部方法或者指定方法。getMethods()和getMethod()方法返回值是一个Method对象数组或者Method对象。每个Method对象对应一个方法,获得Method对象后,程序就可以通过该Method对象的invoke()方法来调用对应的方法。
步骤:
第一:通过Class.forName()或者类名.class或者对象名.getClass()任意一种方法,获得Class对象;
第二:通过Class对象的getMethod()方法获取要调用的方法的Method对象;
第三:调用获取的Method对象的invoke()方法,来调用对应的类方法。
(3)访问属性值
通过Class对象的getFields()方法可以获取一个类全部属性或指定属性。这两个方法 返回值是一个Field对象数组或Field对象提供了如下两组方法来访问属性:
第一种:getXXX(Object o):获取o对象该Field的属性值。这里的XXX对应八种基本数据类型,如果是引用型,则取消get后面的xxx.
第二种:setXXX(Object o,XXX val):将o对象的该Field的值设置为val。这里的XXX对应八种基本数据类型,如果是引用类型,则取消get后面的XXX。
使用这两种方法可以随意访问指定对象的所有属性。
(4)动态创建和访问数组
当我们不能确定数组的大小时,比如公司的员工,如果我们声明一个固定大小200 数组来存储员工,可能会存在浪费空间或者数组大小不够的情况。正确的做法是应该根据运行时员工的人数,确定数组的大小。利用反射机制,我们可以动态创建数组并访问数组元素。
java.lang.reflect包中的Array类提供了动态创建及访问数组元素数组的方法,这些方法都是静态方法。