1. Java类的生命周期
到目前为止,相信同学们已经掌握了一套Java开发的最基本套路,那就是︰先定义一个Java类,然后定义它内部的属性/行为,最后在需要用的时候产生该类的对象,调用该对象的方法完成功能或操作属性值。 由此可见“类-class”这个概念在整个Java的语法和运行机制中都是非常核心的。那么一个Java类从被加载到虚拟机内存开始,到卸载出内存为止,它经过了哪些步骤呢?那这些步骤又被我们称作类的生命周期。
1.1 类的生命周期图
一个Java类从开始到结束,它的整个生命周期一共会经历7个阶段:加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备、解析三个部分又统称为连接。
在这七个步骤中,我们把前5个步骤统称为“类加载”,包括:加载、连接(验证、准备、解析)、初始化。
1.2 类加载的任务
那我们所谓的类加载,它的任务是啥呢?
-
加载:由类加载器完成,类的class文件读入内存后,就会创建一个java.lang.Class对象。一旦某个类被载入JVM中,同一个类就不会再次被载入。
-
连接:把类的二进制数据合并到JRE中。
-
初始化:JVM负责对类进行初始化,也就是对静态属性进行初始化。
在Java类中,对静态属性指定初始值的方式有两种:
-
声明静态属性时指定初始值;
-
使用静态初始化块为静态属性指定初始值。
-
1.3 双亲委派机制
在说明类加载任务时,有提到一个点:一旦某个类被载入JVM中,同一个类就不会被再次载入。那这一要点是如何实现的呢?答案就是--双亲委派机制。
在Java中默认提供了三种类加载器:
-
启动类加载器 ,或者叫根加载器。这个类加载器主要是去加载你在本机配置的环境变量Java_Home/jre/lib目录下的核心API,如rt.jar,其底层采用C++代码加载;
-
扩展类加载器。这个加载器负责加Java_Home/jre/lib/ext目录下的所有jar包;
-
应用程序类加载器。这个加载器加载的是你的项目工程的 ClassPath目录下的类库。另外,用户也可以根据自身需要实现自己的类加载器,继承ClassLoader即可。如果用户没有自定义自己的类加载器,这个就是程序默认的类加载器。
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class 文件时,Java虚拟机采用的是双亲委派模式,即把请求交由上级加载器处理,是—种任务委派模式。双亲委派机制模型图如下:
其工作原理如下:
-
如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行;
-
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的根类加载器;
-
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去 加载,这就是双亲委派机制;
-
父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至最底层类加载器也无法加载此类,则抛出异常。
由于启动类加载器是用C++开发实现的,所以打印出来的Java对象是null。
思考:为什么要设计双亲委派机制呢?
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。 相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object 的类,并放在程序的Class Path中,那系统中将会出现多个不同的 Object类,Java类型体系中最基础的行为也就无法保 证,应用程序也将会变得一片混乱。
2. 反射
前面讲了那么久,那到底什么是反射呢?
Java的反射技术是java程序的特征之一,它允许运行中的Java程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。
使用反射可以获得Java类中各个成员的名称并显示出来。简单的说,反射就是让你可以通过名称来得到对象(类,属性,方法)的技术。
总结为一句话:运行时探究和使用编译时未知的类。
在这一句话中,有两个时间节点需要大家特别注意:运行时和编译时。
在正常情况下,我们的开发实现都是在运行时使用编译时已知的类,就我们Java开发的最基本套路:要先定义好一个类,然后再用它。在定义过程中,我们已经确定了这个类有哪些属性方法,然后在运行中产生该类对象、操作该对象的属性和调用该对象的方法。
但是呀,反射让我们把这个过程给反过来了,即在编译时我们可能并不能确定要使用的到底是哪个类的对象,要调用它的哪个方法,而是在“运行期”的时候给予确定。这就是所谓的编译时未知,运行时探究和使用。
那么,这样的反射在Java中又是如何实现的呢?
在讲解类加载时,我们讲过,JVM会把程序中所要用到的类的class文件加载到内存当中,而这个类的所有基本信息就会被封装到一个java.lang.Class类型的对象里面;因此,我们要用到的每个类都会在内存中有一个Class对象,该对象就被称之为这个类的“类模板对象”。
这个类模板对象--- Class对象,本意是交给JVM自己使用的,用于记录和查找这个类的信息的。但是反射机制允许我们在我们的程序运行过程中得到这个对象,从而实现“运行时探究和使用编译时未知的类”;例如︰我们用的Eclipse或IDEA这些IDE工具。它们在当初开发的时候,并不知道程序员要写的类叫什么,有什么。但是,当它们在运行起来以后通过它的内部的反射实现可以探究到我们书写的类的信息。所以,当程序员在这些IDE中用“.”操作符的时候,会弹出这个类的方法和属性,这就是反射的效果。
3. 必须掌握的反射API
每个Java类被加载后,JVM都会为该类生成一个对应的Class对象。通过Class对象,我们就可以探究这个类的信息,然后完成操作。所以,反射操作的步骤就分为了以下三步:
-
获得你想操作的类的java.lang.Class对象;
-
探究这个类的信息,包括但不限于:所属包、父类、实现的接口、内部类、外部类、属性、构造、方法、修饰符等,当然最常用的还是属性、构造和方法。
-
使用探究到的信息完成功能实现,比如:探究到构造就可以产生类的实例对象;探究到属性就可以给属性进行赋值取值;探究到方法就可以进行方法调用。
接下来我们就分别来进行学习。为了便于理解,我们首先创建两个自定义类Person和Student,其代码如下:
package com.project.bean; public class Person { private String name; public Person() {} public Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
package com.project.bean; import java.io.Serializable; public class Student extends Person implements Serializable { private String gender; private Integer age; private String address; public Student(){} public Student(String name){ super(name); } public Student(String name,String gender,Integer age,String address){ super(name); this.gender = gender; this.address = address; this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } private void study() { System.out.println(this.getName() + "说:Good Good Study,Day Day Up!"); } public void study(String teacherName) { System.out.println(teacherName + "老师说:" + this.getName() + "格老子好好学习!"); } @Override public String toString() { return "Student{" + "name='" + this.getName() + '\'' + ",gender='" + gender + '\'' + ", age=" + age + ", address='" + address + '\'' + '}'; } }
3.1 获取Class对象
Class类,只提供了私有构造方法,所以不能通过new的方式创建。获取一个类的Class对象主要有三种方式:
-
利用类型.class获取
// String类的Class对象 Class strClass = String.class; //自定义Student类的Class对象 Class stuClass = Student.class; //数组的Class对象 Class intArrayClass = int[].class; //枚举的Class对象 Class enumClass = RetentionPolicy.class; //注解的Class对象 Class annotationClass = Override.class; //接口的Class对象 Class serializableClass = Serializable.class; //基本数据类型获取Class对象 Class intClass = int.class; // 其中基本数据类型的Class对象在JDK1.5前,使用包装类.TYPE Class intClass2 = Integer.TYPE; // 包装类型获取Class对象 Class IntegerClass = Integer.class; //返回类型void也能获取Class对象 Class voidClass = void.class;
-
利用对象.getClass()获取
//String类的Class对象 Class strClass = "hello".getClass(); //自定义Student类的Class对象 Student stu = new Student("张三"); Class stuClass = stu.getClass(); //数组的Class对象 double[] array = {1.2, 2.3, 3.4}; Class intArrayClass = array.getClass(); //枚举的Class对象 Class enumClass = RetentionPolicy.SOURCE.getClass();
由于getClass()方法是来自于根类Оbject ,所以只有继承自Object的子类型的对象才能调用。因此该方式不能获取到基本数据类型以及void的Class对象;另外,接口的实现类虽然能调用到getClass()方法,但是获取到的是该实现类的Class对象而不是该接口的,注解Annotation是一种特殊的接口,同理也不能用本方式获取。
思考:如果父类引用指向子类对象,调用该引用的getClass()方法得到的是父类还是子类的Class对象? 回答∶子类。此时,真正被调用的是子类对象身上的getClass()方法,得到的就应该是该子类对象的类模板对象,父类引用只是帮助找到这个子类对象。
-
利用Class.forName()获取
try { // String类的Class对象 Class strClass = Class.forName("java.lang.String"); //自定义Student类的Class对象 Class stuClass = Class.forName("com.project.bean.Student"); //接口的Class对象 Class serializableClass = Class.forName("java.io.Serializable"); //枚举的Class对象 Class enumClass = Class.forName("java.lang.annotation.RetentionPolicy"); //注解的Class对象 Class annotationClass = Class.forName("java.lang.Override"); //int[]的Class对象 Class intArrayClass = Class.forName("[I"); // Student[]的Class对象 Class stuArrayClass = Class.forName("[Lcom.project.bean.Student;"); System.out.println(stuArrayClass); } catch (ClassNotFoundException e) { e.printStackTrace(); }
注意:
-
Class.forName()的参数必须是类的全限定名(即包名+类名);
-
由于该方法接受的参数是字符串值,有可能出现参数值不正确找不到类的情况,所以该方法有编译时异常ClassNotFoundException,必须用try-catch或throws处理;
-
该方法支持所有的引用数据类型,所以基本数据类型不能通过该方法获取Class对象;
-
.数组类型的字符串值非常特殊,总结如下:
-
byte[]---“[B"
-
short[]---“[s"
-
int[]--- "[I"
-
long[]---“[J”---需要特殊记忆
-
float[]---“[F”
-
double[]---“[D"
-
char[]--- "[C"
-
boolean[]---“[Z"---需要特殊记忆
-
引用类型[]---“[L引用类型限定名;"
-
-
-
三种获取方式的对比
方式 区别 类型.class 支持获取所有类型的Class对象;没有动态性,因为在代码中已经写定了要探究的具体类型,无法做到编译时未知。 对象.getClass() 不支持接口、注解和基本数据类型;有一定的动态性,可以利用父类引用指向子类对象这一特性,通过调用该父类引用的getClass()从而获取到所指子类的Class对象。 Class.forName() 支持除基本数据类型之外的所有类型;由于类的字符串名称可以通过外部输入手段(配置文件,外部参数,网络传输等等)获取,不用在代码中写定,所以可以动态的产生“编译时未知,运行时探究和使用”的效果。本方式是框架设计中最常采用的方式。
注意:三种方式无论采用哪一种,获取到的某类型Class对象都是同一个。
3.2 探究类的信息
获取到某个类型的Class对象之后,我们就可以通过它获取到这个类当中的信息。
3.2.1 探究类的基本信息
// 获取class对象 Class stuClass = Student.class; //获取类型名---全限定名 String className = stuClass.getName(); //获取类型名---简单名 className = stuClass.getSimpleName(); //获取类型所在包的包名 String packageName = stuClass.getPackage().getName(); //先获取父类的Class对象,再获取父类的简单类名 String superClassName = stuClass.getSuperclass().getSimpleName();//获取本类实现的所有接口的Class对象 Class[] interClasses = stuClass.getInterfaces(); //获取类的修饰符 int modClass = stuClass.getModifiers(); String modClassStr = Modifier.toString(modClass);
注意:
-
由于Java中的修饰符既包括访问修饰符(private/同包/protected/public),又包括可选修饰符(abstract/final/static等等),还可以根据情况任意组合且不限制书写顺序,所以在设计时Class对象里面存放的修饰符是把各种修饰符组合定义为int常量。
如果需要打印输出成String,需要使用专用的工具类Modifier 的 toString()方法进行一次转换。
3.2.2 探究类当中的属性
在反射API中提供了一个java.lang.reflect.Field的类型来表示类当中定义的属性。我们可以通过一个类的Class 对象获取到定义在该类上的Field属性,一共有4个方法:
Class stuClass = Student.class; //获取本类的公共属性(无论是自己定义的,还是继承而来的) Field[] allPublicFields = stuClass.getFields(); //获取本类中声明的所有属性(不限制访问修饰符,但只能是本类中定义的,不包括继承而来的) Field[] allFields = stuClass.getDeclaredFields(); try { //获取本类中某个指定的公共属性(必须是公共的,可以是继承而来的) Field thePublicField = stuClass.getField("name"); //获取本类中声明的某个指定属性(不限制访问修饰符,不包括继承而来的) Field theField = stuClass.getDeclaredField("age"); } catch (NoSuchFieldException e) { e.printStackTrace(); }
注意:
-
getField()和 getDeclaredField()方法由于获取的是指定属性,所以需要传入这个属性的名字。如果传入的字符串参数有可能找不到,所以这两个方法都要求处理编译时异常NoSuchFieldException --没有该属性异常。
获取到某个Field对象后,我们就能调用Field对象上的方法,获取该属性的信息。
Class stuClass = Student.class; //获取本类中声明的所有属性 Field[] allFields = stuClass.getDeclaredFields(); //在遍历中获取每个属性的基本信息 for (Field f : allFields) { //获取属性名 String fName = f.getName(); /* *获取属性的类型 * getType()返回的是该属性的类型(Class对象形式) * 然后通过getName获取该类型的字符串名称 */ String fType = f.getType().getName(); /* *获取属性的修饰符 * getModifiers()同样返回的是所有修饰符的组合后int常量(包括访问修饰符和可选修饰符) * 然后用Modifier工具类转化成字符串 */ String fMod = Modifier.toString(f.getModifiers()); System.out.println("\t" + fMod + " " + fType + " " + fName + "; "); }
3.2.3 探究类当中定义的构造
类当中的构造,在反射API中是使用另一个同样来自于java.lang.reflect包的类Constructor来表示的。与Field一样,它同样是通过Class对象获取的,且也有4个方法。
Class stuClass = Student.class; //获取本类的所有公共构造 Constructor[] publicCons = stuClass.getConstructors(); //获取本类中的所有构造 Constructor[] allCons = stuClass.getDeclaredConstructors(); try { //获取本类中指定的某个公共构造 Constructor<Student> thePublicCon = stuClass.getConstructor(String.class); //获取本类中指定的某个构造 Constructor theCon = stuClass.getDeclaredConstructor(); } catch (NoSuchMethodException e) { e.printStackTrace(); }
那获取到构造的Constructor对象后,我们能进一步得到构造的那些信息呢?
Class stuClass = Student.class; //获取本类中的所有构造 Constructor[] allCons = stuClass.getDeclaredConstructors(); //在遍历中获取各个构造的信息 for (Constructor con : allCons) { //构造方法名 String conName = con.getName(); //构造方法的修饰符 String conMod = Modifier.toString(con.getModifiers()); //JDK1.8之前,参数只能得到类型的Class对象数组,无法获取参数名 Class[] paramTypes = con.getParameterTypes(); //JDK1.8之后,参数有专门的Parameter类型描述,可以获取参数名 Parameter[] params = con.getParameters(); //把当前构造的所有参数构造成一个字符串 String paramStr = ""; for (int i = 0; i < params.length; i++) { //获取参数的类型名称 String paramType = params[i].getType().getSimpleName(); //获取参数的名称 String paramName = params[i].getName(); paramStr += paramType + " " + paramName; //除最后一个参数外,每个参数的后面用“,”分隔 if (i != params.length - 1) { paramStr += ", "; } } System.out.println(conMod + " " + conName + "(" + paramStr + "){...}"); }
注意:
-
Parameter类型是JDK1.8当中设计的,专门用来封装参数对象的信息;
-
如果没有进行专门的配置,默认情况下获取到的参数名称是arg0,arg...,若想要得到程序员定义的参数名称,需要在编译( javac命令)的时候增加一个编辑参数“-parameters";
-
反射不是反编译,Class对象中只封装了类的声明信息(代码是被载入到内存的代码段当中的),没有方法的实现部分代码,所以我们不能够通过反射获取到方法的实现部分代码。
3.2.4 探究类当中定义的方法
java.lang.reflect.Method类是专门用来在反射中封装方法信息的。和Field和Constructor类似,有4个方法。
Class stuClass = Student.class; //获取本类的所有公共方法,包括继承而来的 Method[] allPublicMethods = stuClass.getMethods(); //获取本类中声明的所有方法,只能是本类中写的(包括重写方法) Method[] allMethods = stuClass.getDeclaredMethods(); try { // 获取本类中某个指定的公共方法,包括继承而来的 Method thePublicMethod = stuClass.getMethod("study"); //获取本类声明中的某个指定方法,只能是本类的 Method theMethod = stuClass.getMethod("study",String.class); } catch (NoSuchMethodException e) { e.printStackTrace(); }
注意:
-
获取指定方法的时候,相对于获取指定构造要多传递一个方法名,然后再传入参数列表的Class对象〈方法是允许重 载的,所以必须用方法名和参数列表共同指定)
获取Method的内部信息:
Class stuClass = Student.class; //获取本类中的所有公共方法 Method[] allPublicMethods = stuClass.getMethods(); //在遍历中获取各个方法的信息 for (Method m : allPublicMethods) { //获取方法的修饰符 String mMod = Modifier.toString(m.getModifiers()); //获取方法的返回类型 String mReturnType = m.getReturnType().getName(); //获取方法名 String mName = m.getName(); //获取方法的所有参数 Parameter[] params = m.getParameters(); //把参数类表构造为一个字符串 String mParatmerStr = ""; for (int i = 0; i < params.length; i++) { Parameter p = params[i]; String pType = p.getType().getName(); String pName = p.getName(); mParatmerStr += pType + " " + pName; if (i != params.length - 1) { mParatmerStr += " , "; } System.out.println("\t" + mMod + " " + mReturnType + " " + mName + "(" + mParatmerStr + "){...}"); } }
3.3 操作类的信息
我们通过反射获取到构造Constructor、方法Method、属性之后,除开能够获取它们声明的信息以外,我们还可以操作它们。构造Constructor可以用来产生这个类的实例对象;方法Method可以用来调用该方法;而属性Field可以给该属性进行赋值或取值。
3.3.1 通过反射创建实例对象 - newInstance
Constructor对象有一个叫做newInstance方法,用于使用该构造方法产生实例对象。
try { Class stuClass = Class.forName("com.project.bean.Student"); Constructor con = stuClass.getDeclaredConstructor(String.class); Student stu = (Student) con.newInstance("张三"); System. out.println(stu. getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); }catch (NoSuchMethodException e) { e.printStackTrace(); }catch (IllegalAccessException e) {// 非法访问异常 e.printStackTrace(); } catch (InstantiationException e) {// 实例化异常 e.printStackTrace(); } catch (InvocationTargetException e) {// 调用目标异常 e.printStackTrace(); }
Constructor的newInstance会抛出3个编译时异常︰
-
IllegalAccessException---非法访问异常,当构造方法的访问修饰符限制该构造不可被访问时抛出;
-
InstantiationException---实例化异常,当该构造方法所属类型不是一个可实例化的类型时抛出,比如︰抽象类;
-
InvocationTargetException---调用目标异常,当被调用的方法的内部抛出了异常而没有被捕获时,将由此异常接收。
-
另外在运行期该方法还有可能抛出1个运行时异常:
IllegalArgumentException---非法参数异常,当调用newInstance()的时候传递的实参与获取Constructor对象时指定的形参不匹配的时候会报该异常。
3.3.2 通过反射调用方法 - invoke
通过Method对象完成方法的调用:
try { //获取类模板对象 Class stuClass = Class.forName("com.project.bean.Student"); //通过反射获取类实例对象 Student stu = (Student) stuClass.getDeclaredConstructor().newInstance(); //通过类模板对象获取指定的study方法的Method对象 Method method = stuClass.getMethod("study", String.class); //调用Method对象的invoke方法,完成study方法的调用 method.invoke(stu, "Miss Zhang"); } catch (Exception e) { e.printStackTrace(); }
注意:invoke()方法接收两个参数:
参数1:类的实例对象;
参数2:方法的被调用时具体要传入的实参数据,需要与寻找该Method对象时指定的形参匹配。
思考:为什么invoke()方法要传入实例对象? 回答:在面向对象语法中,类当中的普通方法的调用都必须指定是哪个对象的行为。反射也不能例外,反射只是一种程序机制而不没有改变面向对象编程理念,所以 invoke()也必须要通过传入实例对象参数来确认,到底是哪个对象在执行这个方法。除非这个被调用的方法是static静态的,因为类当中的静态方法与实例对象无关,能够通过类名直接访问,那么这个时候invoke()方法的第一个参数可以传入null。
3.3.3 通过反射操作属性 - get/set
当我们获取到属性Field对象之后,当然也可以在该属性访问修饰符允许的情况下给这个属性赋值/取值。
try { //获取类模板对象 Class stuClass = Class.forName("com.project.bean.Student"); // 通过反射获取类实例对象 Student stu = (Student) stuClass.getDeclaredConstructor().newInstance(); //通过类模板对象获取指定的score属性的 Field对象 Field field = stuClass.getDeclaredField("gender"); //调用Field 对象的set() 方法,给score属性赋值 field.set(stu, "男"); //调用Field对象的get()方法,取出score属性的值 String gender = (String) field.get(stu); System.out.println(gender); } catch (Exception e) { e.printStackTrace(); }
注意:由于大部分属性在定义的时候都是私有的,所以我们在反射操作的时候虽然能够获取到私有属性的Field对象,但是一调用get/set方法就会报lllegalAccessException非法的访问异常。 这个问题虽然能够解决,但是我们还是推荐大家不要直接操作这个属性的 Field对象(特别是访问修饰符被限制的情况),而是去操作这个属性的访问器/修改器对应的Method对象。如果类的设计者没有给这个属性提供对应访问权限的访问器/修改器,那么说明他的设计意图本身就不希望操作人员去操作这个属性。
3.3.4 不安全的访问设置
当一个类的属性、构造或行为被该类的设计人员用访问修饰符修饰之后,那么我们就只能够在设计允许的范围内操作这些内容了。但是反射却可以让我们突破这种限制,这也是反射的强大之处的体现。 再次强调,我们不建议这么做,这种做法破坏了面向对象的封装性
try { Class stuClass = Class.forName("com.project.bean.Student"); Student stu = (Student) stuClass.getDeclaredConstructor().newInstance(); Field field = stuClass.getDeclaredField("gender"); //先设置该属性对象的访问权限为true field.setAccessible(true); //然后再操作该field field.set(stu, "男"); //赋值 String gender = (String) field.get(stu);//取值 System.out.println(gender); } catch (Exception e) { e.printStackTrace(); }
这样就不会报 IllegalAccessException异常了。除了Field对象,Method对象和Constructor对象都有这个方法,都可以突破它们的访问修饰符限制。