日常开发用的不多,但是反射Reflect是后期学习各种框架的基础技能。
反射Reflect非高深难懂,是底层的技术。
一、课程内容
(1)反射及其作用
(2)反射的四个核心类
(3)反射在项目中的应用
二、反射及其作用
2.1 概述
1.对比
反射是运行时动态决定访问类与对象的技术。
是JDK1.2后提供的特性,隶属于java.lang.reflect
创建对象的 | 以前 | 反射 |
---|---|---|
方式 | 要实例化一个对象,要new+指定的类。 这种使用new关键字的方式,是把实例化对象的工作,写死在程序中 | 将一个有效的类名保存为一个字符串,通过用户输入来灵活创建哪个类 |
时机 | 编译阶段 | 运行阶段 |
特点 | 写死了,不灵活 | 更灵活、易管理 |
2.重要性:
大部分Java框架都是基于反射来实现参数配置、动态注入等特性。
程序启动时,会动态的读取配置文件信息,同时利用反射技术,在运行时去创建不同的对象。
2.2 编写第一段反射的程序
在接口MathOperation
.java中:
/**
* 四则运算接口
*/
public interface MathOperation {
public float operate(int a , int b);
}
以下是三个类,分别实现了上述的接口:
/**
* 加法
*/
public class Addition implements MathOperation {
@Override
public float operate(int a , int b) {
System.out.println("执行加法运算");
return a + b;
}
}
/**
* 减法运算
*/
public class Subtraction implements MathOperation {
@Override
public float operate(int a, int b) {
System.out.println("执行减法运算");
return a - b;
}
}
在入口类ReflectSample.java
中case1方法::
/**
* 初识反射的作用
*/
public class ReflectSample {
/**
* 传统的创建对象方式
*/
public static void case1(){
Scanner scanner = new Scanner(System.in);
System.out.print("请输入计算类名:");
String op = scanner.next();
System.out.print("请输入a:");
int a = scanner.nextInt();
System.out.print("请输入b:");
int b = scanner.nextInt();
MathOperation mathOperation = null;
if(op.equals("Addition")){ // 加法
mathOperation = new Addition();
}else if(op.equals("Subtraction")) { // 减法
mathOperation = new Subtraction();
}else{
System.out.println("无效的计算类");
return;
}
float result = mathOperation.operate(a, b);
System.out.println(result);
}
public static void main(String[] args) {
ReflectSample.case1();
}
}
--->
请输入计算类名:Addition
请输入a:5
请输入b:3
执行加法运算
8.0
请输入计算类名:Subtraction
请输入a:5
请输入b:3
执行减法运算
2.0
在入口类ReflectSample.java
中case2方法:
/**
* 初识反射的作用
*/
public class ReflectSample {
/**
* 利用反射创建对象更加灵活
*/
public static void case2(){
Scanner scanner = new Scanner(System.in);
System.out.print("请输入计算类名:");
String op = scanner.next();
System.out.print("请输入a:");
int a = scanner.nextInt();
System.out.print("请输入b:");
int b = scanner.nextInt();
MathOperation mathOperation = null;
try { // Class.forName()用于加载找到指定的类 // 类完整路径:包名+上面用户输入的类名 // 对该类进行实例化(同new)
mathOperation = (MathOperation) Class.forName("com.imooc.reflect." + op).newInstance(); //将Object对象,转换为接口
}catch(Exception e){
System.out.println("无效的计算类");
return; // 中断程序
}
float result = mathOperation.operate(a, b);
System.out.println(result);
}
public static void main(String[] args) {
ReflectSample.case2();
}
}
--->
请输入计算类名:Addition
请输入a:5
请输入b:3
执行加法运算
8.0
请输入计算类名:Subtraction
请输入a:5
请输入b:3
执行减法运算
2.0
以上的结果,跟传统方式一样。这说明,我们的case2的定义和调用是正确的。
目前,暂时还看不出case2的优势在哪里。
2.3 新增需求时,发挥反射的优势
针对当前代码,如果想增加乘法运算,怎么办?
1.传统的方式:
- 既要新增
Multiplication.java
:
/**
* 乘法运算
*/
public class Multiplication implements MathOperation {
@Override
public float operate(int a, int b) {
return a * b;
}
}
- 又要在入口类
ReflectSample.java
的源代码中:增加以下if-else代码:
在真正的企业应用中,一旦代码重新编写,就必须重新测试、重新打包上线,这个过程非常麻烦。
那么,如何在不修改源代码的情况,让程序支持新的功能呢?
2.利用反射技术,在运行时来决定去创建哪些对象
如果项目需求有新增,要有乘法、除法等运算,是不是必须要在上述源代码中增加if-else呢?
不是。应该利用反射技术,在运行时动态创建对应的对象。
本质:因为下面要实例化的类,即op的值,是用户输入的,运行时才能知道。
- 只需要新增
Multiplication.java
:
public class Multiplication implements MathOperation {
@Override
public float operate(int a, int b) {
return a * b;
}
}
- 在入口类
ReflectSample.java
中case2方法:
不需要做任何改变
/**
* 初识反射的作用
*/
public class ReflectSample {
/**
* 利用反射创建对象更加灵活
*/
public static void case2(){
Scanner scanner = new Scanner(System.in);
System.out.print("请输入计算类名:");
String op = scanner.next();
System.out.print("请输入a:");
int a = scanner.nextInt();
System.out.print("请输入b:");
int b = scanner.nextInt();
MathOperation mathOperation = null;
try {
mathOperation = (MathOperation) Class.forName("com.imooc.reflect." + op).newInstance();
}catch(Exception e){
System.out.println("无效的计算类");
return;
}
float result = mathOperation.operate(a, b);
System.out.println(result);
}
public static void main(String[] args) {
ReflectSample.case2();
}
}
--->
请输入计算类名:Multiplication
请输入a:5
请输入b:3
15.0
三、反射的核心类
根据类的完整结构,可以有以下分类:
3.1 分类
(1)Class类
(2)Constructor构造方法类
(3)Method方法类
(4)Field成员变量类
四、Class类及对象实例化
4.1 概述
(1)Class类是JVM中代表“类和接口”的指代
(2)Class类对象是包含了某个特定类的结构信息。
通过Class类对象,可以获得对应类的构造方法/方法/成员变量
4.2 Class类的常用方法
即最常用的api
其中:
- 之前JDBC用过class.forName()方法,用于加载驱动类,就已用过反射的技术
4.3 语法
4.4 代码演示
主要是演示:
- Class.forName( )方法
- classObj.newInstance( )方法
先创建工程:reflect
;
在员工实体类Employee.java
中:
/**
* 员工实体类
*/
public class Employee {
// 通过静态块,来检验是是否已加载和初始化
static {
System.out.println("Employee类已被加载到jvm,并已初始化");
}
private Integer eno;
private String ename;
private Float salary;
private String dname;
// 为让演示过程更加清晰
public Employee() {
System.out.println("Employee默认构造方法已被执行");
}
public Integer getEno() {
return eno;
}
public void setEno(Integer eno) {
this.eno = eno;
}
public String getEname() {
return ename;
}
public void setEname(String ename) {
this.ename = ename;
}
public Float getSalary() {
return salary;
}
public void setSalary(Float salary) {
this.salary = salary;
}
public String getDname() {
return dname;
}
public void setDname(String dname) {
this.dname = dname;
}
}
在入口类ClassSample.java
中:
import com.imooc.reflect.entity.Employee; // 调用上面刚写好的员工实体类
public class ClassSample {
public static void main(String[] args) {
try { // 1.Class.forName()方法将指定的类加载到jvm,并返回对应的Class类
Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee"); //forName是Class类的静态方法,所以可以这样调用
System.out.println("Employee已被加载到jvm"); // 是否真的加载了这个类?
// 2.newInstance()通过默认的构造方法,来创建新的对象
Employee emp = (Employee) employeeClass.newInstance(); // 返回的是Object,是远古父类,因此实际使用时要强制类型转换
System.out.println(emp);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
--->
Employee类已被加载到jvm,并已初始化
Employee已被加载到jvm
Employee默认构造方法已被执行
com.imooc.reflect.entity.Employee@1b6d3586
其中:
- 是否真的加载了这个类?
作为一个类,当被加载完以后,要经过初始化的工作。类的初始化,是通过静态块完成的。
4.5 关于三个异常
1.抛出“类无法找到”的异常:当类名与类路径书写错误时
2.抛出“非法访问异常”:当在作用域外访问对象方法时或成员变量时
3.抛出“实例化异常”:当对象无法被实例化时
上述的案例中,实体类中只有一个默认的构造方法。
但是,如果还有其他的带参构造方法,此时如何创建新的对象?
五、Constructor构造方法类
5.1 概述
(1)Constructor构造方法类是Java类中构造方法的指代、抽象。
(2)Constructor类的对象,是某个具体类的某个构造方法的声明。
可以通过上述对象,调用带参的构造方法,来创建对象
5.2 常用方法
5.3 语法
5.4 代码演示
在员工实体类Employee.java
中:
/**
* 员工实体类
*/
public class Employee {
static {
System.out.println("Employee类已被加载到jvm,并已初始化");
}
private Integer eno;
private String ename;
private Float salary;
private String dname;
public Employee() {
System.out.println("Employee默认构造方法已被执行");
}
// 带参构造方法:为简化属性赋值的过程
public Employee(Integer eno, String ename, Float salary, String dname) {
this.eno = eno;
this.ename = ename;
this.salary = salary;
this.dname = dname;
System.out.println("Employee带参构造方法已被执行");
}
public Integer getEno() {
return eno;
}
public void setEno(Integer eno) {
this.eno = eno;
}
public String getEname() {
return ename;
}
public void setEname(String ename) {
this.ename = ename;
}
public Float getSalary() {
return salary;
}
public void setSalary(Float salary) {
this.salary = salary;
}
public String getDname() {
return dname;
}
public void setDname(String dname) {
this.dname = dname;
}
@Override
public String toString() { // 为了演示的方便,重写toString方法
return "Employee{" +
"eno=" + eno +
", ename='" + ename + '\'' +
", salary=" + salary +
", dname='" + dname + '\'' +
'}';
}
}
在入口类ConstructorSample.java
中:
/**
* 利用带参构造方法,创建对象
*/
public class ConstructorSample {
public static void main(String[] args) {
try { // 1.所有反射在操作之前,都需要获得对应的类
Class employeeClass=Class.forName("com.imooc.reflect.entity.Employee");
// 2.获取指定格式的带参构造方法 // 通过一个数组,来描述构造方法中参数的数量、类型,以作为构造方法的标识
Constructor constructor=employeeClass.getConstructor(new Class[]{
Integer.class,String.class,Float.class,String.class
}); // 3.创建对象
Employee employee = (Employee) constructor.newInstance(new Object[]{
100, "李磊", 3000f, "研发部"
});
System.out.println(employee);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
// 没有找到与之对应格式的方法
e.printStackTrace();
} catch (IllegalAccessException e) { // 无法访问异常:当构造方法是private时
e.printStackTrace();
} catch (InstantiationException e) { // 实例化异常:当实体类是抽象类时
e.printStackTrace();
} catch (InvocationTargetException e) {
// “目标调用异常”:当被调用的方法内部抛出了异常,但是没有被捕获时(较少)
e.printStackTrace();
}
}
}
--->
Employee类已被加载到jvm,并已初始化
Employee带参构造方法已被执行
Employee{eno=100, ename='李磊', salary=3000.0, dname='研发部'}
这说明,通过带参构造方法,成功创建了Emplyee这个对象。
5.5 关于异常
没有找到与之对应格式的方法的异常:
六、Method方法类
6.1 概述
(1)Method方法类指代某个类中的方法
(2)Method方法类的对象,使用classObj.getMethod()
方法获取
(3)用途:通过Method方法类的对象,调用指定对象的对应方法
6.2 常用方法
invoke:
use,put into use
6.3 语法
其中:
- 上面的是形参列表
- 下面的是实参列表
6.4 代码演示
在员工实体类Employee.java
中:
/**
* 员工实体类
*/
public class Employee {
static {
System.out.println("Employee类已被加载到jvm,并已初始化");
}
private Integer eno;
private String ename;
private Float salary;
private String dname;
public Employee() {
System.out.println("Employee默认构造方法已被执行");
}
public Employee(Integer eno, String ename, Float salary, String dname) {
this.eno = eno;
this.ename = ename;
this.salary = salary;
this.dname = dname;
System.out.println("Employee带参构造方法已被执行");
}
public Integer getEno() {
return eno;
}
public void setEno(Integer eno) {
this.eno = eno;
}
public String getEname() {
return ename;
}
public void setEname(String ename) {
this.ename = ename;
}
public Float getSalary() {
return salary;
}
public void setSalary(Float salary) {
this.salary = salary;
}
public String getDname() {
return dname;
}
public void setDname(String dname) {
this.dname = dname;
}
@Override
public String toString() {
return "Employee{" +
"eno=" + eno +
", ename='" + ename + '\'' +
", salary=" + salary +
", dname='" + dname + '\'' +
'}';
}
// 为员工调薪的方法
public Employee updateSalary(Float val) {
this.salary = this.salary + val;
System.out.println(this.ename+"调薪至"+this.salary+"元");
return this; // 返回的是当前的对象。如果没有,就会报错
}
}
在入口类MethodSample.java
中:
/**
* 利用Method方法类进行调用
*/
public class MethodSample {
public static void main(String[] args) {
try { // 1.加载指定的类
Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
// 2.获取指定格式的构造方法
Constructor constructor = employeeClass.getConstructor(new Class[]{
Integer.class, String.class, Float.class, String.class
}); // 3.创建对象
Employee employee = (Employee) constructor.newInstance(new Object[]{
100, "李磊", 3000f, "研发部"
});
// 4. 获取方法
Method updateSalaryMethod = employeeClass.getMethod("updateSalary", new Class[]{
Float.class
});
// 5.执行上述方法
Employee employee1 = (Employee) updateSalaryMethod.invoke(employee, new Object[]{ // 参数是一个对象数组
1000f
});
System.out.println(employee1); // 会自动调用toString方法,进行输出
} 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();
}
}
}
--->
Employee类已被加载到jvm,并已初始化
Employee带参构造方法已被执行
李磊调薪至4000.0元
Employee{eno=100, ename='李磊', salary=4000.0, dname='研发部'}
# 七、Field成员变量类
7.1 概述
(1)Field成员变量类,指代某个类中的成员变量
(2)Field成员变量类的对象,通过classObj.getField()
方法获取
(3)用途:通过上述对象,可以为成员变量赋值、取值
7.2 常用方法
7.3 语法
7.4 代码演示:获取成员变量值
在计算机术语中,属性也叫成员变量、字段,三者都是一个东西。
在员工实体类Employee.java
中:
在入口类FieldSample.java
中:
public class FieldSample {
public static void main(String[] args) {
try {
// 1.加载指定的类
Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
// 2.找到指定的构造方法
Constructor constructor = employeeClass.getConstructor(new Class[]{
Integer.class, String.class, Float.class, String.class
});
// 3. 实例化一个对象
Employee employee = (Employee)constructor.newInstance(new Object[]{
100, "李磊", 3000f, "研发部"
});
// 4.通过反射,得到员工的姓名(属性的值)
Field enameField = employeeClass.getField("ename"); // 先得到属性名
String ename = (String) enameField.get(employee); // 再得到属性值:直接get()方法
System.out.println("ename:" + ename);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
// 没有找到对应成员变量时抛出的异常
e.printStackTrace();
}
}
}
--->
Employee类已被加载到jvm,并已初始化
Employee带参构造方法已被执行
ename:李磊 // 成功通过反射,得到了员工的姓名
7.5 关于异常
没有找到对应成员变量时抛出的异常:
7.6 代码演示:更改成员变量值
结果为:
Employee类已被加载到jvm,并已初始化
Employee带参构造方法已被执行
ename:李雷 // 属性值已经被更改
7.7 异常
上面的get开头的获取的方法,比如,getMethod、getField()都是针对的是public修饰的公开变量。
如果是私有属性,就会报错:
但是,在实际工作中,难免会有获取私有属性的场景。那么,该怎么做呢?
见下节。
八、getDeclared系列方法说明
8.1 概述
(1)getConstructor(s)|Method(s)|Field(s)
只能获取public对象
(2)getDeclaredConstructor(s)|Method(s)|Field(s)
可以获取任意对象
(3)访问非作用域内的构造方法、方法、成员变量,会抛出异常
8.2 案例应用
要求:希望运行时,将对象所有的成员变量都打印输出?
分析:如果是public属性,直接获取;如果是private等非public的属性,那就需要调用getter方法来提取。
public class getDeclaredSample {
public static void main(String[] args) {
try {
// 1.加载指定的类
Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
// 2.找到指定的构造方法
Constructor constructor = employeeClass.getConstructor(new Class[]{
Integer.class, String.class, Float.class, String.class
});
// 3. 实例化一个对象
Employee employee = (Employee)constructor.newInstance(new Object[]{
100, "李磊", 3000f, "研发部"
});
// 4.获取当前类所有的成员变量,并放到一个数组中
Field[] fields = employeeClass.getDeclaredFields(); // getDeclaredFields()方法
for (Field field : fields) {
// 5.如何区分public和private
if (field.getModifiers() == 1) { // public修饰
Object val = field.get(employee); // 此处的val其实是String类型,调用的是String类改写的toString方法
System.out.println(field.getName() + ":" + val);
}else if(field.getModifiers()==2){ // private修饰
// 先组织得到方法名
String methodName="get"+field.getName().substring(0, 1).toUpperCase()+
field.getName().substring(1);
// 得到方法的对象
Method getmethod = employeeClass.getMethod(methodName);
Object ret=getmethod.invoke(employee); // 此处不能类型转换,因为属性类型有很多
System.out.println(field.getName()+":"+ret);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
--->
开发场景中,你的同事传过来一个对象,但是你对该对象的结构不了解。此时,你就可以使用上述代码对所有的数据进行打印。
九、反射的项目应用案例
9.1 编码
通过反射,加载配置文件,以输出不同版本的名人名言
在接口I18N.java
中:
public interface I18N {
public String say();
}
在Zhcn.java
中:
// 简体中文
public class Zhcn implements I18N { // 实现上面的接口
@Override
public String say() {
return "生命不息,奋斗不止";
}
}
在配置文件config.properties
中:
language=com.imooc.i18n.Zhcn // 将上面的中文类放进来
在入口类Application.java
中:
public class Application {
// 通过反射,加载配置文件,以输出不同版本的名人名言
public static void say(){
Properties properties = new Properties();
String configPath = Application.class.getResource("/config.properties").getPath();
try {
configPath = new URLDecoder().decode(configPath, "UTF-8");
properties.load(new FileInputStream(configPath));
String language = properties.getProperty("language");
// 这里的language,等同于"com.imooc.i18n.Zhcn".开始使用反射技术
I18N i18N = (I18N)Class.forName(language).newInstance();
System.out.println(i18N.say());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Application.say();
}
}
--->
生命不息,奋斗不止
上面演示的是通过反射技术,实现的网站的中文版本。
如果想增加英文版本,就很简单:不需要修改源代码,只需要增加新的国家语言,并将其放置在配置文件中即可。
即:
在En.java
中:
public class En implements I18N {
@Override
public String say() {
return "Cease to the struggle and cease to the life";
}
}
在配置文件config.properties
中:
language=com.imooc.i18n.En
结果为:
--->
Cease to the struggle and cease to the life
最终实现了运行时,来决定去实例化哪个类
Comments | NOTHING