Java高级——反射

178 阅读8分钟

首先思考一下Java有几种方式创建一个对象

Java创建对象的方式大概有一下三种:

  1. 使用new关键字:这是我们最常见的也是最简单的创建对象的方式
  2. 使用Clone的方法:无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去
  3. 使用序列化:当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象

一、概述

Java的反射机制,是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的以及动态调用对象的方法的功能称为Java的反射机制。

自己思考一下什么是反射

简单地说,Java的反射就是:
我们需要一个对象,通过Java的反射机制我们可以获取到类的信息,将一个类的属性和方法利用反射技术映射成一个个Java对象,从而拿这些Java对象来做一些事情。也就是说,反射是反射Java类的各个组成部分。

二、反射的作用

一般来说,反射是用来做一些基础框架的,或者说写抽象都比较高的底层代码。你搞懂了反射,就可以帮助你理解框架的一些原理,有一句很经典的话:反射是框架设计的灵魂

三、通过反射获取到想要的对象

开发人员平常写的代码是.java文件,通过Java编译器编译成.class文件。Java是面向对象的语言,一切皆对象,所以java认为 这些编译后的 class文件,也可以抽象成一个类,这个类就是Class类

所以,使用Java反射机制的起步,就是获取这个Class类的对象。
在Class类中有个静态方法
这个方法是我们使用反射的起步方法

Class clazz = Class.forName("com.example.Person");

获取Class类对象的三种方式

  • Class.forName("com.example.Person");
  • 对象.getClass();
  • 类名.class;

Class类的常用方法
Class类提供了如下几个常用方法

Constructor<T> getConstructor(Class<?>... parameterTypes) 
返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。 
Method getMethod(String name, Class<?>... parameterTypes) 
返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。 
Field getField(String name) 
返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 
返回一个 Constructor 对象,该对象反映此 Class 对象所表示的类或接口的指定构造方法。 
Method getDeclaredMethod(String name, Class<?>... parameterTypes) 
返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。 
Field getDeclaredField(String name) 
返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段。 

四、反射的使用

先创建一个Person类如下:

package com.example;

public class Person {
    private String name;

    private int age;

    public Person() {
        System.out.println("这是无参构造方法");
    }

    public Person(String name) {
        System.out.println("姓名:" + name);
    }

    private Person(int age) {
        System.out.println("年龄:" + age);
    }

    public Person(String name, int age) {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }
}
  • 反射构造方法
    • 反射无参构造方法
    • 反射一个参数的构造方法
    • 反射多个参数的构造方法
    • 反射私有的构造方法
      • 注意:在反射私有的构造函数时,用普通的clazz.getConstructor()会报错,因为它是私有的,所以提供了专门反射私有构造函数的方法getDeclaredConstructor(Class<?>... parameterTypes);//读取私有的构造函数,用这个方法读取完还需要设置一下暴力反射才可以constructor.setAccessible(true);
    • 反射获得所有构造方法
  • 反射普通方法
package com.example;

import java.util.Date;

public class Person {
    private String name;

    private int age;

    public Person() {
        System.out.println("这是无参构造方法");
    }

    public Person(String name) {
        System.out.println("姓名:" + name);
    }

    private Person(int age) {
        System.out.println("年龄:" + age);
    }

    public Person(String name, int age) {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }

    public void m1() {
        System.out.println("m1");
    }

    public void m2(String name) {
        System.out.println(name);
    }

    public String m3(String name,int age) {
        System.out.println(name+":"+age);
        return "aaa";
    }

    private void m4(Date d) {
        System.out.println(d);
    }

    public static void m5() {
        System.out.println("m5");
    }

    public static void m6(String[] strs) {
        System.out.println(strs.length);
    }

    public static void main(String[] args) {
        System.out.println("main");
    }
}
@org.junit.jupiter.api.Test//public void m1()
public void test1() throws Exception{
    Class clazz = Class.forName("com.cj.test.Person");
    Person p = (Person)clazz.newInstance();
    Method m = clazz.getMethod("m1", null);
    m.invoke(p, null);
}
@org.junit.jupiter.api.Test//public void m2(String name)
public void test2() throws Exception{
    Class clazz = Person.class;
    Person p = (Person) clazz.newInstance();
    Method m = clazz.getMethod("m2", String.class);
    m.invoke(p, "张三");
}
@org.junit.jupiter.api.Test//public String m3(String name,int age)
public void test3() throws Exception{
    Class clazz = Person.class;
    Person p = (Person) clazz.newInstance();
    Method m = clazz.getMethod("m3", String.class,int.class);
    String returnValue = (String)m.invoke(p, "张三",23);
    System.out.println(returnValue);
}
@org.junit.jupiter.api.Test//private void m4(Date d)
public void test4() throws Exception{
    Class clazz = Person.class;
    Person p = (Person) clazz.newInstance();
    Method m = clazz.getDeclaredMethod("m4", Date.class);
    m.setAccessible(true);
    m.invoke(p,new Date());
}
@org.junit.jupiter.api.Test//public static void m5()
public void test5() throws Exception{
    Class clazz = Person.class;
    Method m = clazz.getMethod("m5", null);
    m.invoke(null,null);
}
@org.junit.jupiter.api.Test//private static void m6(String[] strs)
public void test6() throws Exception{
    Class clazz = Person.class;
    Method m = clazz.getDeclaredMethod("m6",String[].class);
    m.setAccessible(true);
    m.invoke(null,(Object)new String[]{"a","b"});
}
@org.junit.jupiter.api.Test
public void test7() throws Exception{
    Class clazz = Person.class;
    Method m = clazz.getMethod("main",String[].class);
    m.invoke(null,new Object[]{new String[]{"a","b"}});
}

注意:看下上边代码里test6和test7的invoke方法里传的参数和其他的有点不一样
这是因为 jdk1.4和jdk1.5处理invoke方法有区别

  • 1.5:public Object invoke(Object obj,Object…args)
  • 1.4:public Object invoke(Object obj,Object[] args)

由于JDK1.4和1.5对invoke方法的处理有区别, 所以在反射类似于main(String[] args) 这种参数是数组的方法时需要特殊处理

启动Java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个main方法时,如何为invoke方法传递参数呢?按jdk1.5的语法,整个数组是一个参数,而按jdk1.4的语法,数组中的每个元素对应一个参数,当把一个字符串数组作为参数传递给invoke方法时,javac会到底按照哪种语法进行处理呢?jdk1.5肯定要兼容jdk1.4的语法,会按jdk1.4的语法进行处理,即把数组打散成为若干个单独的参数。所以,在给main方法传递参数时,不能使用代码mainMethod.invoke(null,new String[]{“xxx”}),javac只把它当作jdk1.4的语法进行理解,而不把它当作jdk1.5的语法解释,因此会出现参数个数不对的问题。

上述问题的解决方法

  • mainMethod.invoke(null,new Object[]{new String[]{"xxx"}}); - 这种方式,由于你传的是一个数组的参数,所以为了向下兼容1.4的语法,javac遇到数组会给你拆开成多个参数,但是由于咱们这个Object[ ] 数组里只有一个元素值,所以就算它拆也没关系
  • mainMethod.invoke(null,(Object)new String[]{"xxx"}); - 这种方式相当于你传的参数是一个对象,而不是数组,所以就算是按照1.4的语法它也不会拆,所以问题搞定 - 编译器会作特殊处理,编译时不把参数当作数组看待,也就不会数组打散成若干个参数了

对上边的描述进行一下总结:在反射方法时,如果方法的参数是一个数组,考虑到向下兼容问题,会按照JDK1.4的语法来对待(JVM会把传递的数组参数拆开,拆开就会报参数的个数不匹配的错误) 解决办法:防止JVM拆开你的数组

  • 方式一:把数组看做是一个Object对象
  • 方式二:重新构建一个Object数组,那个参数数组作为唯一的元素存在。
  • 反射属性字段
package com.example;

import java.util.Date;

public class Person {
    public String name = "李四";
    
    private int age = 18;
    
    public static Date time;

    public int getAge() {
        return age;
    }

    public Person() {
        System.out.println("默认的无参构造方法执行了");
    }

    public Person(String name) {
        System.out.println("姓名:" + name);
    }

    public Person(String name, int age) {
        System.out.println(name + "=" + age);
    }

    private Person(int age) {
        System.out.println("年龄:" + age);
    }

    public void m1() {
        System.out.println("m1");
    }

    public void m2(String name) {
        System.out.println(name);
    }

    public String m3(String name, int age) {
        System.out.println(name + ":" + age);
        return "aaa";
    }

    private void m4(Date d) {
        System.out.println(d);
    }

    public static void m5() {
        System.out.println("m5");
    }

    public static void m6(String[] strs) {
        System.out.println(strs.length);
    }

    public static void main(String[] args) {
        System.out.println("main");
    }
}
//public String name="李四";
@org.junit.jupiter.api.Test
public void test8() throws Exception{
    Class clazz = Person.class;
    Person p = (Person)clazz.newInstance();
    Field f = clazz.getField("name");
    String s = (String)f.get(p);
    System.out.println(s);
    //更改name的值
    f.set(p, "王六");
    System.out.println(p.name);
}
@org.junit.jupiter.api.Test//private int age = 18;
public void test9() throws Exception{
    Class clazz = Person.class;
    Person p = (Person)clazz.newInstance();
    Field f = clazz.getDeclaredField("age");
    f.setAccessible(true);
    int age = (Integer)f.get(p);
    System.out.println(age);
    f.set(p, 28);
    age = (Integer)f.get(p);
    System.out.println(age);
}
@org.junit.jupiter.api.Test//public static Date time;
public void test10() throws Exception{
    Class clazz = Person.class;
    Field f = clazz.getField("time");
    f.set(null, new Date());
    System.out.println(Person.time);
}

看完上边有关反射的东西, 对常用框架里的配置文件是不是有点思路了

上边是Spring配置文件里的常见的bean配置,这看起来是不是可以用反射很轻易的就可以实现:解析xml然后把xml里的内容作为参数,利用反射创建对象。

除了这个,常用的框架里还有很多地方都用到了反射,反射是框架的灵魂,具备反射知识和思想,是看懂框架的基础

平常用到的框架,除了配置文件的形式,现在很多都使用了注解的形式,其实注解也和反射息息相关,使用反射也能轻而易举的拿到类、字段、方法上的注解,然后编写注解解析器对这些注解进行解析,做一些相关的处理,所以说不管是配置文件还是注解的形式,它们都和反射有关