博客内容

每一篇都是认真思考与总结的结晶


带你掌握框架的灵魂——反射技术

2020年03月26日   0 个评论   28 次访问   0 人点赞   夜光

Java反射机制指的是在Java程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法

引言

反射概述

Java反射机制指的是在Java程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法;对于给定的一个对象,都能够调用它的任意一个属性和方法。这种动态获取类的内容以及动态调用对象的方法称为反射机制。
Java的反射机制允许编程人员在对类未知的情况下,获取类相关信息的方式变得更加多样灵活,调用类中相应方法,是Java增加其灵活性与动态性的一种机制。
反射能动态编译和创建对象,极大的激发了编程语言的灵活性,强化了多态的特性,进一步提升了面向对象编程的抽象能力,在很多框架中被大量使用,所以可以说框架的灵魂即是:反射技术。

这些都是很官方的一些解释,通过概述能够知道反射技术的强大,所以接下来,我们细细品味一下反射的用法。

类的加载过程

当程序要使用某个类的时候,如果该类还没有被加载到内存,则系统会通过加载、连接、初始化三个步骤来实现对这个类的初始化。

  • 加载:指将class文件读入内存,并为之创建一个Class对象,任何类被使用时系统都会为其创建Class对象
  • 连接:连接又分为三个步骤(验证、准备、解析)
               验证:验证是否有正确的内部结构,并和其它类协调一致
               准备:负责为类的静态成员分配内存,并设置默认初始化值
               解析:将类的二进制数据中的符号引用替换为直接引用
  • 初始化:初始化会为所有的静态变量赋予正确的值。注意初始化操作和准备操作的区别,准备操作为静态成员分配内存,设置默认初始化值,而初始化操作是设置正确的值。举个例子:static int num = 1024,这样的一个成员变量在准备阶段会为其赋值为0,只有到初始化阶段才会赋值为1024。

类加载器

了解完类的加载过程之后,我们来看看到底是谁完成了类的加载操作,它就是:类加载器。
类加载器负责将.class文件加载到内存中,并为之生成对应的Class对象,类加载器由以下三大加载器组成:

  1. 根类加载器(Bootstrap ClassLoader):根类加载器也被称为引导类加载器,负责Java核心类的加载,例如System、String类等
  2. 扩展类加载器(Extension ClassLoader):扩展类加载器负责JRE的扩展目录中jar包的加载
  3. 系统类加载器(System ClassLoader):系统类加载器负责在Java虚拟机启动时加载来自Java命令的class文件以及classpath变量所指定的jar包和类路径

获取Class对象

有了理论的知识之后,我们就可以开始实践了,先来看看如何获取类的Class对象(有三种方式)。
首先创建一个基本类用于测试:

package com.wwj.reflect;

public class Programmer { private String name; public int age; private String address;

public Programmer() {
}

private Programmer(String name, int age) { this.name = name; this.age = age; }

public Programmer(String name, int age, String address) { this.name = name; this.age = age; this.address = address; }

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; }

public String getAddress() { return address; }

public void setAddress(String address) { this.address = address; }

public void test() { System.out.println("test---无参无返回值方法"); }

public void test2(String str) { System.out.println("test2---带参无返回值方法"); }

public String test3(String str, int num) { System.out.println("test3--带参带返回值方法"); return str + "--" + num; }

private void test4() { System.out.println("test4---私有方法"); }

@Override public String toString() { return "Programmer{" + "name='" + name + ''' + ", age=" + age + ", address='" + address + ''' + '}'; }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

那么第一种获取Class对象的方式就是通过Object类的getClass()方法:

Programmer programmer = new Programmer();
Class pClass = programmer.getClass();
  • 1
  • 2

第二种方式就是通过静态属性class:

Class pClass = Programmer.class;
  • 1

第三种方式通过Class类中的静态方法forName():

Class pClass = Class.forName("com.wwj.reflect.Programmer");
  • 1

获取构造方法

拿到了Class对象后,我们就可以通过该对象获取类的成员并使用,先来看看如何获取类的构造方法。

	public static void main(String[] args) throws ClassNotFoundException {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor[] constructors = pClass.getConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行结果:

public com.wwj.reflect.Programmer(java.lang.String,int,java.lang.String)
public com.wwj.reflect.Programmer()
  • 1
  • 2

控制台只打印了两个构造方法,但很显然,Programmer类中有三个构造方法,其中的私有构造方法获取不到。所以Class类中的getConstructors()只能获取到公共的构造方法,要想获取到所有的构造方法,可以使用getDeclaredConstructors()方法:

	public static void main(String[] args) throws ClassNotFoundException {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor[] constructors = pClass.getDeclaredConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行结果:

public com.wwj.reflect.Programmer(java.lang.String,int,java.lang.String)
private com.wwj.reflect.Programmer(java.lang.String,int)
public com.wwj.reflect.Programmer()
  • 1
  • 2
  • 3

通常情况下,我们并不需要这么多的构造方法,往往我们只需要一个构造方法就行了。

1.获取无参构造方法

先说说如何获取类的无参构造方法。

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        System.out.println(object);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们可以通过Class类的getConstructor()方法获得单个的构造方法,不传参则代表获取无参构造方法,然后通过返回的构造方法对象调用newInstance()方法即可创建Programmer对象,所以运行结果应为Programmer类的信息。

Programmer{name='null', age=0, address='null'}
  • 1
2.获取带参构造方法

获取带参构造方法的方式同样是通过getConstructor()方法,只不过需要传入参数,返回的是对应参数的构造方法对象,直接看例子:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor(String.class,int.class,String.class);
        Object object = constructor.newInstance("张三",18,"杭州");
        System.out.println(object);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

运行结果:

Programmer{name='张三', age=18, address='杭州'}
  • 1
3.获取私有构造方法

前面已经说到,getConstructor()方法无法获取到私有构造方法,所以我们改用getDeclaredConstructors()方法即可,用法和getConstructor()相同。

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor declaredConstructor = pClass.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);//取消访问检查
        Object object = declaredConstructor.newInstance("李四", 20);
        System.out.println(object);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行结果:

Programmer{name='李四', age=20, address='null'}
  • 1

需要注意的是,虽然getDeclaredConstructor()方法能够获取到私有构造方法,但由于Java语言的访问检查机制,在创建对象的时候会抛出非法访问异常,所以我们需通过setAccessible()方法取消访问检查,参数为true则为取消,取消了访问检查后才能正常创建对象。

获取成员变量

我们再来看看如何通过Class对象获得类的成员变量。

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Field[] fields = pClass.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行结果:

public int com.wwj.reflect.Programmer.age
  • 1

输出结果不难理解,和获取构造方法一样,getFields()方法无法获取类的私有成员变量,可以通过getDeclaredFields()方法获取:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Field[] fields = pClass.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行结果:

private java.lang.String com.wwj.reflect.Programmer.name
public int com.wwj.reflect.Programmer.age
private java.lang.String com.wwj.reflect.Programmer.address
  • 1
  • 2
  • 3
1.获取公共成员变量
	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Field ageField = pClass.getField("age");
        ageField.set(object, 20);
        System.out.println(object);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行结果:

Programmer{name='null', age=20, address='null'}
  • 1

通过Class对象的getField()方法能够获取指定属性名的成员变量,但若想对属性进行赋值,则首先需要创建出Programmer对象,然后调用成员变量对象的set()方法,传入要赋值的对象和属性值。这个逻辑其实和正常创建对象赋值是刚好相反的,反射是通过成员变量对象调用方法并将类对象和参数值传入。

2.获取私有成员变量

获取私有成员变量的方式和获取私有构造方法相同,通过getDeclaredField()方法获得成员变量对象,并且在赋值之前需要先取消访问检查,直接看示例:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Field nameField = pClass.getDeclaredField("name");
        nameField.setAccessible(true);//取消访问检查
        nameField.set(object,"李四");
        System.out.println(object);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行结果:

Programmer{name='李四', age=0, address='null'}
  • 1

获取成员方法

获取成员方法的方式和前面相同,通过getMethods()方法可以获取到公共的成员方法,通过getDeclaredMethods()方法可以获取到包括私有的所有成员方法,在此不做重复讲解,接下来说一说如何获取单个成员方法。

1.获取无参无返回值成员方法
	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Method method = pClass.getMethod("test");
        method.invoke(object);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行结果:

test---无参无返回值方法
  • 1

同样地,通过getMethod()方法可以获取到对应参数名的成员方法,该方法需要传入两个参数:第一个参数为方法名;第二个参数为方法的参数类型。
这里因为是无参方法,所以无需传入第二个参数,获取到成员方法的对象之后,同样是调用该对象的invoke()方法并将需要执行方法的对象传入才能成功执行方法。

2.获取带参无返回值成员方法

获取带参成员方法就很简单了,在getMethod()方法中传入参数类型即可,直接看示例:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Method method = pClass.getMethod("test2", String.class);
        method.invoke(object, "王五");
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行结果:

test2---带参无返回值方法
  • 1
3.获取带参带返回值成员方法

获取带参带返回值成员方法同样十分简单,只不过多了一个返回值处理罢了:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Method method = pClass.getMethod("test3", String.class, int.class);
        Object obj = method.invoke(object, "赵六", 20);
        System.out.println(obj);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行结果:

test3--带参带返回值方法
赵六--20
  • 1
  • 2
4.获取私有成员方法

获取私有成员方法,即通过getDeclaredMethod()方法获取成员方法对象,并取消访问检查,然后执行方法即可:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Method method = pClass.getDeclaredMethod("test4");
        method.setAccessible(true);
        method.invoke(object);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行结果:

test4---私有方法
  • 1

利用反射无视泛型检查

到这里关于反射的基本知识就介绍完了,接下来我们用泛型来解决一个问题:无视掉Java的泛型检查。
我们看这样的一段代码:

	public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList<>();
        list.add("hello");
        list.add("world");
        list.add(1024);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

因为list集合的泛型被指定为String类型,所以该集合将只能存储字符串,所以我们在放入1024的时候编译器会报错,那有没有可能通过一些手段将其它类型也能够放入该集合呢?办法是有的,那就是通过反射。
因为Java泛型机制其实只在编译阶段有效,在真正运行的时候是不带泛型的,这种现象叫泛型擦除。这是因为这一特点,我们就能通过反射越过编译期的泛型检查,实现将其它类型的数据存放到指定类型的集合中。

	public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList<>();
        list.add("hello");
        list.add("world");
        Class listClass = list.getClass();
        Method addMethod = listClass.getMethod("add", Object.class);
        addMethod.invoke(list, 1024);
        System.out.println(list);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行结果:

[hello, world, 1024]
  • 1

这样,int类型数据就成功被存放到了集合中。

动态代理

动态代理是反射技术的高级应用,其目的就是为其它对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
在Java中,JDK为我们提供了Proxy类和InvocationHandler接口,通过这个类和接口就可以生成动态代理对象,但需要注意,JDK提供的代理只能针对接口做代理,如果需要对普通类做代理,我们可以使用cglib。
由于篇幅有限,这里不对动态代理做详细介绍,就通过一个案例让大家先了解一下动态代理。
新建一个接口ICat:

interface ICat{
	public void run();
}
  • 1
  • 2
  • 3

新建一个类继承该接口:

public class Cat implements ICat{
@Override
public void run(){
	System.out.println("喵喵~一只猫在奔跑");
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这是一只会奔跑的猫,通常我们会使用动态代理来增强某个类的方法,例如该类中,我们可以增强run()方法,使其还会抓老鼠:

	public static void main(String[] args) throws Exception {
        final ICat cat = new Cat();//原对象
        ICat catProxy = (ICat) Proxy.newProxyInstance(cat.getClass().getClassLoader(), cat.getClass().getInterfaces(), new InvocationHandler() {
        public Object invoke(Object proxy, Method method, Object[] objs)
                throws Throwable {
            //增强run方法
            if (method.getName().equals("run")) {
                method.invoke(cat, objs);//调用原对象的方法,保留原方法的功能
                //新增功能
                System.out.println("抓住一只老鼠");
            }
            return null;
        }
    });
    catProxy.run();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

运行结果:

喵喵~一只猫在奔跑
抓住一只老鼠
  • 1
  • 2

主要说一说invoke()方法,该方法有三个参数:

  • proxy:在其调用方法的代理实例
  • method:对应于在代理实例上调用的接口方法的Method实例。Method对象的声明类将是在其中声明方法的接口,该接口可以是代理类赖以继承方法的代理接口的超接口
  • objs:包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为null。基本类型的参数被包装在适当基本包装器类的实例中

返回值即是代理方法的返回值,因为这里run()方法没有返回值,所以返回null即可,然后调用method对象的invoke()方法,并将需要执行方法的对象和参数值objs传入即可执行原方法的逻辑,这在如何获取成员方法中已经说过,然后我们就可以在下面写上需要添加的功能,这样该方法就比原先的方法功能更加丰富了。

最后

本篇文章总体是偏简单的,适合刚入门的学习者,虽然简单,但也写了挺久,从8点多一直写到11点,目的也是希望大家能够快速掌握反射技术,反射技术在后期的框架学习中是至关重要的,理解反射,对于框架的底层实现你就能够更加了解。

评论区



发表评论

昵称和评论内容是必填项,邮箱选填,用来交流信息

请输入评论

请输入昵称

显示评论区

  • 。。  2020年03月30日 13:43:45

    666

  •  2020年03月30日 13:43:37

    666

  • 。。  2020年03月30日 13:42:02

    12345666

  • yubao99599  2020年03月27日 09:13:19

    非常棒

  • yubao99599  2020年03月27日 09:13:18

    非常棒



67

夜光网站

网站均来自自己课余时间所做,感谢来访

© 2019 夜光网. ALL RIGHTS RESERVED.
本站已运行 184天19小时59分19秒
THEME KRATOS MADE BY XIAOYOU MODIFIED BY XIAOYOU SITEMAP
赣ICP备19003009号