反射
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
一直说反射反射,但是为什么要反射却不甚理解。“明明直接 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
的映射。
我们写业务代码是用不到反射的,自己去写组件才会用到反射。
当然,如果自定义注解的话,那么是需要用到反射对注解做相应处理的。
参考: