在说反射的概念之前,我们先回忆一下,在我们Java编程的过程中,是不是一直都在用某个类的构造方法 new一个对象,用某个类的成员方法实现某些功能,或者往某个类的成员变量中存储数据,相信这个答案的肯定的!
那么,今天我们所学习的这个反射,跟类有很大的关系,或者说就是围绕类来开展,下面就来了解一下正式的反射含义:Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制
注意到任意了吗?没错,反射就是这么强大!强大归强大,相信很多同学对这么抽象的定义都不太能够理解,他的是怎么实现的呢?为什么要这样做呢?让我们带着这些疑问继续看下去!
在前文我们说到,反射就是围绕类来开展,那么下面将介绍一个类(或者说任意Java代码)在计算机中的三个阶段
相信认真学习的同学们,都还记得写好Java代码后,要先编译才能运行,看起来就是两步的样子,但在计算机底层是没有那么简单的,下面将用自定义的Person类进行介绍
首先,我们编写好的代码是一个Person.java
文件,在经过javac的编译之后,得到Person.class
文件,这两个文件,都是保存在我们的硬盘当中,称为Source源代码阶段,这个阶段是Java代码在计算机的第一个阶段
然后,JVM中有一个类加载器,它会把Person.class
文件加载进内存当中,但此加载过程并不是简单的加载过程,我们知道,一个类通常有很多个成员变量、成员方法和构造方法,那在内存中是什么形式存在呢?类加载器会把所有的成员变量存在一个数组当中,把所有的构造方法和成员方法都分别存在一个数组当中,当然还会有其他类型的数组,但我们这里只介绍这三个作为例子,这个阶段称为Class类对象阶段,这是Java代码在计算机的第二个阶段
最后,当我们需要使用到Person对象的时候,我们会创建一个对象,new这个动作是在程序运行到这个语句的时候执行的,因此这个阶段称为Runtime运行时阶段,这是Java代码在计算机的第三个阶段
上面介绍了Java程序在计算机中的三个阶段,那么,对应的,我想要获取一个Class对象,也有三种不同的获取方式:
下面来演示一下三种方法的使用:
其运行结果如下,可见均打印了Person类的全类名:
这时候可能有同学会有疑问,既然有三种获取方式,那么三种方式所获取出来的对象是不是同一个呢?下面我们来验证一下:
要注意,==
的比较方式是比较内存地址,换言之,内存地址相同代表这三种方法所获取到的Class对象均是同一个
既然三种方法获取出来的对象都一样,那么哪种方法比较常用呢?forName
方法!为什么?仔细观察一下,方法二和方法三都有Person,意味着我需要import
这个包才能够使用,不够灵活,不能契合Java反射机制的灵活性,因此在反射机制当中forName
方法是最常用的获取Class对象的方法,随后的讲解也是基于该方法进行
第一章中反射的定义说到可以操作任意类的任意变量和方法,现在我有了一个Class对象了,那我怎么去用和操作呢?为了方便讲解,这里先给出两个类的定义(要注意,Class本身就是一个类)
Person类:
public class Person {
public String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public void eat(){
System.out.println("eat something");
}
private void sleep(int time){
System.out.println("sleep " + time + " hours");
}
}
Student类:
public class Student {
public void study(){
System.out.println("studying...");
}
}
想要有一个对象,首先要知道他的构造方法,我们才能把这个对象给创建出来,在Class类中,关于构造方法的获取有四种方法:
Constructor<T> getConstructor(Class<?>... parameterTypes):返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。
Constructor<?>[] getConstructors():返回一个包含某些 Constructor 对象的数组,这些对象反映此 Class 对象所表示的类的所有公共构造方法。
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes):返回一个 Constructor 对象,该对象反映此 Class 对象所表示的类或接口的指定构造方法。
Constructor<?>[] getDeclaredConstructors():返回 Constructor 对象的一个数组,这些对象反映此 Class 对象表示的类声明的所有构造方法。
国际惯例,简单介绍完之后下面直接来给出获取例子:
public class DemoConstructor {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("practice.java03.coding.day02Reflection.Person");
System.out.println(cls);
System.out.println("1============");
getC1(cls);
System.out.println("2============");
getC2(cls);
System.out.println("3============");
getC3(cls);
System.out.println("4============");
getC4(cls);
}
private static void getC1(Class cls) {
Constructor[] constructors = cls.getConstructors(); //获取所有public修饰的构造方法
for (Constructor constructor : constructors) {
System.out.println(constructor);
}
}
private static void getC2(Class cls) throws NoSuchMethodException {
System.out.println(cls.getConstructor()); //获取指定的public修饰的构造方法(以参数类型为区分) 空参则什么都不写
System.out.println(cls.getConstructor(String.class, int.class)); //有参构造则写 参数类型.class,.....
}
private static void getC3(Class cls) {
Constructor[] declaredConstructors = cls.getDeclaredConstructors(); //获取所有构造方法 不考虑修饰符
for (Constructor declaredConstructor : declaredConstructors) {
System.out.println(declaredConstructor);
}
}
private static void getC4(Class cls) throws NoSuchMethodException {
System.out.println(cls.getDeclaredConstructor()); //获取指定的构造方法(以参数类型为区分) 不考虑修饰符
System.out.println(cls.getDeclaredConstructor(String.class, int.class));
}
}
该代码的运行结果:
现在我已经能获取到一个类的任意构造方法了,那么怎样可以通过这个构造方法来创建对象呢?首先我们再来学习一个方法,在Constructor类中有一个newInstance
方法(没错Constructor又是一个类):
T newInstance(Object... initargs):使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。
那下面就来示范一下利用这个构造方法来创建对象的过程:
细心的同学会发现在程序的最后我们写了一个简化方法,在使用空参数构造时,Class类中为了方便我们使用也定义了一个newInstance
方法,所以空参数构造的情况下我们可以直接使用Class对象的方法来创建对象,省去了获取构造方法的麻烦,下面来看看执行结果:
介绍完构造方法后,我们来介绍一下成员方法在Class类中,关于成员方法的获取也有四种方法:
Method getMethod(String name, Class<?>... parameterTypes):返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。
Method[] getMethods():返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。
Method getDeclaredMethod(String name, Class<?>... parameterTypes):返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Method[] getDeclaredMethods():返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
国际惯例,简单介绍完之后下面直接来给出获取例子:
public class DemoMethod {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("practice.java03.coding.day02Reflection.Person");
System.out.println(cls);
System.out.println("1============");
getM1(cls);
System.out.println("2============");
getM2(cls);
System.out.println("3============");
getM3(cls);
System.out.println("4============");
getM4(cls);
}
private static void getM1(Class cls) {
Method[] methods = cls.getMethods(); //获取所有public修饰的成员方法(包括继承的方法)
for (Method method : methods) {
System.out.println(method);
}
}
private static void getM2(Class cls) throws NoSuchMethodException {
Method eat = cls.getMethod("eat"); //获取指定名称的public修饰的成员方法
System.out.println(eat);
Method sleep = cls.getMethod("sleep",int.class); //注意要加上所获取成员方法的 参数类型.class
System.out.println(sleep);
}
private static void getM3(Class cls) {
Method[] declaredMethods = cls.getDeclaredMethods(); //获取所有成员方法 不考虑修饰符(不包括继承的方法)
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
}
private static void getM4(Class cls) throws NoSuchMethodException {
Method eat = cls.getDeclaredMethod("eat"); //获取指定名称的成员方法 不考虑修饰符
System.out.println(eat);
Method sleep = cls.getDeclaredMethod("sleep",int.class);//注意要加上所获取成员方法的 参数类型.class
System.out.println(sleep);
}
}
咦,有…有异常?NoSuchMethodException
?
我们来看看异常出在哪里,可见华丽的分隔符2之前的输出都是正常的,但是getM2
方法中只显示了eat
方法,没有sleep
方法,异常所报的也是这个地方,有同学可能会有疑问,我Person类中明明有sleep
方法呀,为什么说没有呢?
注意了,getMethod
只能获取public
修饰的成员方法,而在Person类中sleep
方法可是private
的,所以他找不到这个sleep
方法,下面我们try...catch
处理一下
public class DemoMethod {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("practice.java03.coding.day02Reflection.Person");
System.out.println(cls);
System.out.println("1============");
getM1(cls);
System.out.println("2============");
try {
getM2(cls);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("3============");
getM3(cls);
System.out.println("4============");
getM4(cls);
}
private static void getM1(Class cls) {
Method[] methods = cls.getMethods(); //获取所有public修饰的成员方法(包括继承的方法)
for (Method method : methods) {
System.out.println(method);
}
}
private static void getM2(Class cls) throws NoSuchMethodException {
Method eat = cls.getMethod("eat"); //获取指定名称的public修饰的成员方法
System.out.println(eat);
Method sleep = cls.getMethod("sleep",int.class); //注意要加上所获取成员方法的 参数类型.class
System.out.println(sleep);
}
private static void getM3(Class cls) {
Method[] declaredMethods = cls.getDeclaredMethods(); //获取所有成员方法 不考虑修饰符(不包括继承的方法)
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
}
private static void getM4(Class cls) throws NoSuchMethodException {
Method eat = cls.getDeclaredMethod("eat"); //获取指定名称的成员方法 不考虑修饰符
System.out.println(eat);
Method sleep = cls.getDeclaredMethod("sleep",int.class);//注意要加上所获取成员方法的 参数类型.class
System.out.println(sleep);
}
}
再看看结果,认真观察有些方法是可以获取到父类的方法,有些是只能获取到本类,四种方法都有不同的特性,需要好好甄别
现在我已经能获取到一个类的任意成员方法了,那么怎样可以使用这个成员方法呢?我们又要再来学习一个方法,在Method类中有一个invoke
方法(没错Method也是一个类):
Object invoke(Object obj, Object... args):对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
那下面就来示范一下使用这个成员方法的过程:
来看看运行结果:
又有细心的同学发现,程序中倒数第二行代码,是一个暴力反射,什么是暴力反射呢?为什么需要这个暴力反射呢?我们前面介绍了,getDeclaredMethod
是可以获取到私有方法的,但是不代表获取了就能直接使用,因为在使用时候它还会有一个权限的安全检查,因此我们需要利用暴力反射绕开安全检查,那么私有方法才可以正常使用
有同学会立马反应过来这个暴力反射的是Method
类的方法吗?打开API文档猛的一查,嗯?没有?确实,setAccessiable
并不是Method
的特有方法,该方法是属于java.lang.reflect.AccessibleObject
,而本章我们所介绍的Constructor
类、Method
类以及下一节所介绍的Field
类都是AccessibleObject
的子类,因此都可以使用setAccessiable
方法设置权限以使得无论什么权限的方法和变量都可以被使用,这也是反射的魅力所在
成员方法介绍完啦,接下来是成员变量,在Class类中,关于成员变量的获取也有四种方法:
Field getField(String name):返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。
Field[] getFields():返回一个包含某些 Field 对象的数组,这些对象反映此 Class 对象所表示的类或接口的所有可访问公共字段。
Field getDeclaredField(String name):返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段。
Field[] getDeclaredFields():返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段。
国际惯例,简单介绍完之后下面直接来给出获取例子:
public class DemoField {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class cls = Class.forName("practice.java03.coding.day02Reflection.Person");
System.out.println(cls);
System.out.println("1============");
getF1(cls);
System.out.println("2============");
try{
getF2(cls);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("3============");
getF3(cls);
System.out.println("4============");
getF4(cls);
}
private static void getF1(Class cls) {
Field[] fields = cls.getFields(); //获取所有public修饰的成员变量
for (Field field : fields) {
System.out.println(field);
}
}
private static void getF2(Class cls) throws NoSuchFieldException {
System.out.println(cls.getField("name")); //获取指定名称的public修饰的成员变量
System.out.println(cls.getField("age"));
}
private static void getF3(Class cls) {
Field[] declaredFields = cls.getDeclaredFields(); //获取所有成员变量 不考虑修饰符
for (Field declaredField : declaredFields) {
System.out.println(declaredField);
}
}
private static void getF4(Class cls) throws NoSuchFieldException {
System.out.println(cls.getDeclaredField("name")); //获取指定名称的成员变量 不考虑修饰符
System.out.println(cls.getDeclaredField("age"));
}
}
同样的,与上一节一样,在getF2
方法中,也会报出异常NoSuchMethodException
,原因同样是该方法只能访问public成员变量,在日后使用过程中一定要注意方法的适用范围,好好甄别,在这里我已经进行了try...catch
处理,下面看看运行的结果:
现在我已经能获取到一个类的任意成员变量了,那么怎样可以使用这个成员变量呢?我们又要再来学习两个方法,在Field类中有一个set
方法和一个get
方法(没错Field也是一个类):
void set(Object obj, Object value):将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
Object get(Object obj):返回指定对象上此 Field 表示的字段的值。
那下面就来示范一下使用这个成员变量的过程:
来看看运行结果:
介绍了这么一大堆东西,是不是觉得很麻烦,以前我可以直接new一个Person,直接通过对象就可以设置和使用成员变量,直接通过对象就可以调用方法,为什么我要绕一大个圈子来弄这么一出呢?这不是把简单的事情复杂化了吗?
相信有不少同学会有这样的疑问,包括在我开始学这个反射的时候也有同样的感受,但其实这只是因为我们没有真正触碰到他魅力所在的地方,关于魅力,我们就要从框架设计说起
框架可以理解为一个半成品的软件,利用框架进行开发可以大大简化我们的代码,例如我需要编写一款软件大概需要书写1w行代码,但是如果我利用框架进行开发,我大概只需要写1k行左右就可以了,其他的大概9k行都给你在框架中写好了,你配置去用就行了
怎么样?听起来是不是很神奇?其精粹在于框架的设计过程中大量使用了Java的反射机制,将类的各个组成部分封装为其它对象,使得无论你定义了什么类,什么方法,我都可以去使用,具体使用过程我给你搞好了,你只需要写你要用的方法功能以及变量,修改一下配置文件就可以运行了,因此反射是框架设计的灵魂所在,同时框架也大大提高了我们的开发效率
强大似乎是很强大,但好像也还是很抽象,别担心!下面我们来个例子直观感受一下反射机制的魅力
为了体现反射是框架设计的灵魂,我们来写一个框架,在不改变这个框架的任何代码的前提下,它可以创建任意类对象,使用任意类对象的方法
首先我们来写一个配置文件,该配置文件当中写的是我想要执行的类的全类名,和想要执行的方法名,首先在你的项目模块处新建一个文件
命名为xxx.properties
,我们知道Properties集合是唯一与IO流相结合的集合,因此该处利用Properties集合来配置文件(没有学Properties集合的同学可以到这篇文章中学习,包括流的相关知识https://blog.csdn.net/weixin_45453739/article/details/106975224)
新建完成后我们往里面写键值对,即配置我们想使用的类名、方法名等
写好配置文件之后就开始写我们的框架了,其大致步骤是:
大家可以先自己尝试按着这个思路写一下,下面给出该框架的参考代码:
下面来执行一把看看结果:
假如现在我不想执行Person类的eat
方法了,我想执行Student类的study
方法,那怎么办呢?这时候我们是不需要改框架的代码的,只需要把配置文件中的类名与方法名改成我们想要的即可
下面我们再来运行一下框架:
可见结果已经是我们想要的了
最后,叨叨两句,可能很多同学知道工作过程中很多时候是使用框架,例如Spring,SpringMVC,Mybatis之类的,比较少机会自己设计框架,但掌握反射可以加强你对框架的理解和使用,因此反射技术也是值得去好好品味的!