反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

一直说反射反射,但是为什么要反射却不甚理解。“明明直接 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);

理由很简单,我们不想修改代码,把需要变动的内容写进配置文件,不香吗?但凡有一天,我们的 usernamepasswordurl 甚至是数据库都改了,我们都能够通过修改配置的方式去实现。不需要动丝毫的代码,改下配置就完事了,这就能提供程序的灵活性。修改代码的风险和代价比修改配置大,即使不知道代码的实现,都能通过修改配置来完成要做的事。

像这种通过修改配置文件来进行动态响应的,其内部很可能就是通过反射来做的。

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);
        }
    }
}
为什么要使用反射?

通过上面几个示例,我们可以知道为什么要使用反射了:

  1. 提高程序的灵活性,并符合开闭原则,如 JDBC、通过反射运行配置文件内容;
  2. 屏蔽掉实现的细节,更方便使用,如 Spring MVC 中页面参数与 JavaBean 的映射。

我们写业务代码是用不到反射的,自己去写组件才会用到反射。
当然,如果自定义注解的话,那么是需要用到反射对注解做相应处理的。

参考:

  1. java 的反射到底是有什么用处?怎么用? - Java3y
  2. Java 基础之 — 反射(非常重要)