反射
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
一直说反射反射,但是为什么要反射却不甚理解。“明明直接 new 一个对象就可以,为什么还需要使用反射呢?”
反射库
反射就是围绕着 Class 对象和 java.lang.reflect 类库的,就是各种的 API 调用。
涉及的类:
| 类 | 作用 |
|---|---|
| Class | 描述类的信息 |
| Constructor | 构造器类 |
| Method | 方法类 |
| Field | 字段类 |
涉及的方法:
| 方法名 | 作用 |
|---|---|
| getXXX() | 获取公有的构造器、方法、属性 |
| getDeclaredXXX() | 获取所有的构造器、方法、属性 |
掌握以下几种差不多就入门了:
- 知道获取
Class对象的三种途径; - 通过
Class对象创建出对象,获取到构造器、方法、属性; - 通过反射的
API修改属性的值、调用方法。
Class 对象
我们肯定都碰到过类型强转失败(ClassCastException)的情况,那么为什么编译时能通过,运行时却报错呢?JVM 是怎么知道类型不匹配的呢?实际上它是通过 Class 对象来判断的。
.java 文件经过 javac 命令编译成 .class 文件,当我们执行了初始化操作( new、类初始化时父类也一同被初始化、反射等)后,会由类加载器(双亲委派模型)将 .class 文件内容加载到方法区中,并在 Java 堆中创建一个 java.lang.Class 类的对象,这个 Class 对象代表着类相关的信息。
既然说,Class 对象代表着类相关的信息,那说明只要类有什么东西(构造器、方法、属性),在 Class 对象里都能找得到,可以在 IDEA 里面查看 java.lang.Class 类包含哪些方法和属性。
获取 Class 对象的三种途径:
public static void main(String[] args) {
// 第一种方式:对象.getClass();
// 这一 new 操作产生一个 String 对象,一个 Class 对象。
String str1 = new String();
Class strClass = str1.getClass();
System.out.println(strClass.getName());
// 第二种方式:类的静态属性 class
Class strClass2 = String.class;
// 判断第一种方式获取的 Class 对象和第二种方式获取的是否是同一个
System.out.println(strClass == strClass2);
// 第三种方式:Class.forName(String className) 静态方法(常用)
try {
// 注意此字符串必须是类全限定名,就是含包名的类路径,包名.类名
Class strClass3 = Class.forName("java.lang.String");
System.out.println(strClass3 == strClass2);
} catch (ClassNotFoundException e) { // 运行时没有找到这个类
e.printStackTrace();
}
}
通过控制台打印可知:在运行期间,一个类,只有一个 Class 对象产生。
三种方式的区别:
- 第一种,对象都有了还要反射干什么;
- 第二种,需要导入类所在的包,依赖太强,不导包就编译错误;
- 第三种,类全限定名字符串可以作为参数传入,也可从配置文件(常用)中读取。
示例
JDBC
JDBC 的代码(硬编码):
// 将 Driver 类加载到堆中,其静态代码块中会创建一个 Driver 对象并将其注册到 DriverManager 中
Class.forName("com.mysql.jdbc.Driver");
// 获取与数据库连接的对象 Connetcion
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/java3y", "root", "root");
// 获取执行 sql 语句的 statement 对象
statement = connection.createStatement();
// 执行 sql 语句,拿到结果集
resultSet = statement.executeQuery("SELECT * FROM users");
后来为什么要改成下面的形式呢:
// 获取配置文件的读入流
InputStream inputStream = UtilsDemo.class.getClassLoader().getResourceAsStream("db.properties");
Properties properties = new Properties();
properties.load(inputStream);
// 获取配置文件的信息
driver = properties.getProperty("driver");
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");
// 加载驱动类
Class.forName(driver);
理由很简单,我们不想修改代码,把需要变动的内容写进配置文件,不香吗?但凡有一天,我们的 username,password,url 甚至是数据库都改了,我们都能够通过修改配置的方式去实现。不需要动丝毫的代码,改下配置就完事了,这就能提供程序的灵活性。修改代码的风险和代价比修改配置大,即使不知道代码的实现,都能通过修改配置来完成要做的事。
像这种通过修改配置文件来进行动态响应的,其内部很可能就是通过反射来做的。
Spring MVC
Servlet 开发是这样获取页面参数的,一堆的 getParameter() 模板代码:
// 通过 html 的 name 属性,获取到值
String username = request.getParameter("username");
String password = request.getParameter("password");
String gender = request.getParameter("gender");
// 复选框和下拉框有多个值,获取到多个值
String[] hobbies = request.getParameterValues("hobbies");
String[] address = request.getParameterValues("address");
// 获取到文本域的值
String description = request.getParameter("textarea");
// 得到隐藏域的值
String hiddenValue = request.getParameter("aaa");
而 Spring MVC 是这样获取页面参数的:
@RequestMapping(value = "/save")
@ResponseBody
public String doSave(PushConfig pushConfig) {
// 直接使用形参对象获取字段
String userName = pushConfig.getUserName();
}
为什么我们写上 JavaBean,保持字段名与参数名相同,就能 “自动” 得到对应的值呢,其实就是通过反射来做的。
通过反射运行配置文件内容
现有系统:
Student 类:
public class Student {
public void show(){
System.out.println("is show()");
}
}
配置文件 Student.properties:
className = com.fanshe.Student
methodName = show
测试类:
/*
* 我们利用反射和配置文件,可以使得应用程序更新时,无需修改任何源码
* 我们只需要将新类发送给客户端,并修改配置文件即可
*/
public class Test {
public static void main(String[] args) throws Exception {
// 第一步:通过反射获取 Class 对象
// 从配置文件中读取 className 属性的值
Class stuClass = Class.forName(getValue("className"));
// 第二步:获取 show() 方法
Method m = stuClass.getMethod(getValue("methodName"));
// 第三步:调用 show() 方法
m.invoke(stuClass.getConstructor().newInstance());
}
// 此方法接收一个 key,在配置文件中获取相应的 value
public static String getValue(String key) throws IOException{
Properties prop = new Properties(); // 获取配置文件的对象
FileReader in = new FileReader("Student.properties"); // 获取输入流
prop.load(in); // 将流加载到配置文件对象中
in.close();
return pro.getProperty(key); // 返回根据 key 获取的 value 值
}
}
需求:
当我们升级这个系统,需要 main() 方法打印其他内容时,不需要修改 Student 类,新写一个 Student2 的类,并将 Student.properties 文件的内容修改一下就可以了。既存代码就一点不用改动,这也符合对扩展开放、对修改关闭的开闭原则。
要替换的 Student2 类:
public class Student2 {
public void show2(){
System.out.println("is show2()");
}
}
配置文件更改为:
className = com.fanshe.Student2
methodName = show2
通过反射越过泛型检查
泛型用在编译期,编译过后泛型擦除(可以通过 ParameterizedType 获取泛型类型)成 Object 或上限类型(父类类型)。所以是可以通过反射越过泛型检查的,一般不会这样使用,除非自己需要一些特殊的操作。
测试类:
/*
* 通过反射越过泛型检查
*
* 例如:有一个 String 泛型的集合,怎样能向这个集合中添加一个 Integer 类型的值?
*/
public class Test {
public static void main(String[] args) throws Exception{
ArrayList<String> strList = new ArrayList<>();
strList.add("aaa");
strList.add("bbb");
// strList.add(100);
// 获取 ArrayList 的 Class 对象,反向的调用 add() 方法,添加数据
// 得到 strList 对象的字节码对象
Class listClass = strList.getClass();
// 获取 add() 方法
Method m = listClass.getMethod("add", Object.class);
// 调用 add() 方法
m.invoke(strList, 100);
// 遍历集合
for(Object obj : strList){
System.out.println(obj);
}
}
}
为什么要使用反射?
通过上面几个示例,我们可以知道为什么要使用反射了:
- 提高程序的灵活性,并符合开闭原则,如
JDBC、通过反射运行配置文件内容; - 屏蔽掉实现的细节,更方便使用,如
Spring MVC中页面参数与JavaBean的映射。
我们写业务代码是用不到反射的,自己去写组件才会用到反射。
当然,如果自定义注解的话,那么是需要用到反射对注解做相应处理的。
参考:

浙公网安备 33010602011771号