引言
今天是10月24日,行业内的大牛小白都知道, 这是一个中国广大程序员的共同节日,1024是2的十次方,二进制计数的基本单位之一。程序员就像是一个个1024,以最低调、踏实、核心的功能模块搭建起这个科技世界,在此也向IT行业的前辈们致敬。
这也是我第二次过程序员节,但是学习习惯不能丢,今天起了个早,写一篇博客开启这美好的一天吧。
反射概述
Java反射机制指的是在Java程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法;对于给定的一个对象,都能够调用它的任意一个属性和方法。这种动态获取类的内容以及动态调用对象的方法称为反射机制。
Java的反射机制允许编程人员在对类未知的情况下,获取类相关信息的方式变得更加多样灵活,调用类中相应方法,是Java增加其灵活性与动态性的一种机制。
反射能动态编译和创建对象,极大的激发了编程语言的灵活性,强化了多态的特性,进一步提升了面向对象编程的抽象能力,在很多框架中被大量使用,所以可以说框架的灵魂即是:反射技术。
这些都是很官方的一些解释,通过概述能够知道反射技术的强大,所以接下来,我们细细品味一下反射的用法。
类的加载过程
当程序要使用某个类的时候,如果该类还没有被加载到内存,则系统会通过加载、连接、初始化三个步骤来实现对这个类的初始化。
- 加载:指将class文件读入内存,并为之创建一个Class对象,任何类被使用时系统都会为其创建Class对象
- 连接:连接又分为三个步骤(验证、准备、解析)
验证:验证是否有正确的内部结构,并和其它类协调一致
准备:负责为类的静态成员分配内存,并设置默认初始化值
解析:将类的二进制数据中的符号引用替换为直接引用 - 初始化:初始化会为所有的静态变量赋予正确的值。注意初始化操作和准备操作的区别,准备操作为静态成员分配内存,设置默认初始化值,而初始化操作是设置正确的值。举个例子:static int num = 1024,这样的一个成员变量在准备阶段会为其赋值为0,只有到初始化阶段才会赋值为1024。
类加载器
了解完类的加载过程之后,我们来看看到底是谁完成了类的加载操作,它就是:类加载器。
类加载器负责将.class文件加载到内存中,并为之生成对应的Class对象,类加载器由以下三大加载器组成:
- 根类加载器(Bootstrap ClassLoader):根类加载器也被称为引导类加载器,负责Java核心类的加载,例如System、String类等
- 扩展类加载器(Extension ClassLoader):扩展类加载器负责JRE的扩展目录中jar包的加载
- 系统类加载器(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 + '\'' +
'}';
}
}
那么第一种获取Class对象的方式就是通过Object类的getClass()方法:
Programmer programmer = new Programmer();
Class pClass = programmer.getClass();
第二种方式就是通过静态属性class:
Class pClass = Programmer.class;
第三种方式通过Class类中的静态方法forName():
Class pClass = Class.forName("com.wwj.reflect.Programmer");
获取构造方法
拿到了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);
}
}
运行结果:
public com.wwj.reflect.Programmer(java.lang.String,int,java.lang.String)
public com.wwj.reflect.Programmer()
控制台只打印了两个构造方法,但很显然,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);
}
}
运行结果:
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.获取无参构造方法
先说说如何获取类的无参构造方法。
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);
}
我们可以通过Class类的getConstructor()方法获得单个的构造方法,不传参则代表获取无参构造方法,然后通过返回的构造方法对象调用newInstance()方法即可创建Programmer对象,所以运行结果应为Programmer类的信息。
Programmer{name='null', age=0, address='null'}
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);
}
运行结果:
Programmer{name='张三', age=18, address='杭州'}
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);
}
运行结果:
Programmer{name='李四', age=20, address='null'}
需要注意的是,虽然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);
}
}
运行结果:
public int com.wwj.reflect.Programmer.age
输出结果不难理解,和获取构造方法一样,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);
}
}
运行结果:
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.获取公共成员变量
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);
}
运行结果:
Programmer{name='null', age=20, address='null'}
通过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);
}
运行结果:
Programmer{name='李四', age=0, address='null'}
获取成员方法
获取成员方法的方式和前面相同,通过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);
}
运行结果:
test---无参无返回值方法
同样地,通过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, "王五");
}
运行结果:
test2---带参无返回值方法
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);
}
运行结果:
test3--带参带返回值方法
赵六--20
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);
}
运行结果:
test4---私有方法
利用反射无视泛型检查
到这里关于反射的基本知识就介绍完了,接下来我们用泛型来解决一个问题:无视掉Java的泛型检查。
我们看这样的一段代码:
public static void main(String[] args) throws Exception {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add(1024);
}
因为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);
}
运行结果:
[hello, world, 1024]
这样,int类型数据就成功被存放到了集合中。
动态代理
动态代理是反射技术的高级应用,其目的就是为其它对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
在Java中,JDK为我们提供了Proxy类和InvocationHandler接口,通过这个类和接口就可以生成动态代理对象,但需要注意,JDK提供的代理只能针对接口做代理,如果需要对普通类做代理,我们可以使用cglib。
由于篇幅有限,这里不对动态代理做详细介绍,就通过一个案例让大家先了解一下动态代理。
新建一个接口ICat:
interface ICat{
public void run();
}
新建一个类继承该接口:
public class Cat implements ICat{
@Override
public void run(){
System.out.println("喵喵~一只猫在奔跑");
}
}
这是一只会奔跑的猫,通常我们会使用动态代理来增强某个类的方法,例如该类中,我们可以增强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();
}
运行结果:
喵喵~一只猫在奔跑
抓住一只老鼠
主要说一说invoke()方法,该方法有三个参数:
- proxy:在其调用方法的代理实例
- method:对应于在代理实例上调用的接口方法的Method实例。Method对象的声明类将是在其中声明方法的接口,该接口可以是代理类赖以继承方法的代理接口的超接口
- objs:包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为null。基本类型的参数被包装在适当基本包装器类的实例中
返回值即是代理方法的返回值,因为这里run()方法没有返回值,所以返回null即可,然后调用method对象的invoke()方法,并将需要执行方法的对象和参数值objs传入即可执行原方法的逻辑,这在如何获取成员方法中已经说过,然后我们就可以在下面写上需要添加的功能,这样该方法就比原先的方法功能更加丰富了。
最后
本篇文章总体是偏简单的,适合刚入门的学习者,虽然简单,但也写了挺久,从8点多一直写到11点,目的也是希望大家能够快速掌握反射技术,反射技术在后期的框架学习中是至关重要的,理解反射,对于框架的底层实现你就能够更加了解。
最后祝大家节日快乐!