Java高级编程—反射机制

本文详细介绍了Java反射机制,包括其概述、提供的功能及相关API。阐述了Class类的理解与实例获取,类的加载和ClassLoader。还介绍了反射创建对象、获取运行时类结构、调用指定结构的方法,最后讲解了反射在动态代理中的应用,体现了Java反射的动态性和强大功能。
摘要由CSDN通过智能技术生成

1.Java反射机制概述

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
程序在加载完类之后(执行java.exe的命令),在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。(这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射)
对反射机制的理解:
在之前我们创建一个对象的过程,是先引入需要使用的类所在的"包名",然后就可以使用该包中的类了,通过new去实例化具体的类,就可以得到实例化的对象;然而在反射的方式下,我们可以从一个实例化的对象出发,通过反射得知这个对象是由哪个类创建出来的,进而知道这个类又是属于哪个包下的,即得到完整的"包类"名称。
image.png

1.1动态语言&静态语言

动态语言是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
主要动态语言: Object-C、C#、JavaScript、PHP、Python、Erlang。
静态语言是在运行时结构不可变的语言。如Java、C、C++ 。

Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动 态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。 Java的动态性让编程的时候更加灵活!

1.2Java反射机制提供的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

1.3反射相关的主要API

  • java.lang.Class: 代表一个类
  • java.lang.reflect.Method: 代表类的方法
  • java.lang.reflect.Field: 代表类的成员变量
  • java.lang.reflect.Constructor: 代表类的构造器

对Class类的理解(难点):
平时我们创建一个类的多个对象,比如创建了Person的对象p1,p2,p3,这些对象当中都有相同的属性比如name,age 和 方法,所以Person类就把这些对象共有的部分抽象了出来。而对于不同的类,比如现在有A,B,C三个类,他们都有属性Field,方法Method和构造器Constructor,所以Class类就把他们的共性抽离了出来,抽象为了Class类,Class类的作用就是通用的描述其他的类的信息的,也就是A,B,C这三个类是作为Class类的实例而存在的。(所以就不难理解,Class类是描述类的类。)

2. 理解Class类并获取Class的实例

我们先声明一个Person类,并声明一个私有属性name和公共属性age,一个公共方法show()和私有方法showNation()。设计如下代码段:

public class Person {

    private String name;
    public int age;

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Person() {
//        System.out.println("Person");

    }

    private Person(String name) {
        this.name = name;
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void show(){
        System.out.println("你好,我是一个人");
    }

    private String showNation(String nation){
        System.out.println("我的国籍是:" + nation);
        return nation;
    }
}

那么回顾一下,在反射之前,我们可以对Person类做哪些操作呢?

//反射之前,对于Person的操作
@Test
public void test1(){
    //1.创建Person类的对象 
    Person p1 = new Person("Tom", 12);

    //2.通过对象,调用其内部的属性、方法
    p1.age = 10;   //因为age属性是public的
    System.out.println(p1.toString());

    p1.show();

    //在Person类外部,不可以通过Person类的对象调用其内部私有结构。
    //比如:name、showNation()以及私有的构造器
}

测试结果如下:
image.png
而使用反射,一样可以实现上述的操作,那么如何使用反射实现上述代码段中对Person类的操作呢?下面就来体会一下反射的使用:

//反射之后,对于Person的操作
@Test
public void test2() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    //创建Class类的对象,对应Person运行时类
    Class clazz = Person.class;
    //1.通过反射,创建Peroson类的对象
    //调用指定参数结构的构造器,生成Constructor的实例
    Constructor cons = clazz.getConstructor(String.class, int.class);
    //.通过Constructor的实例创建对应类的对象,并初始化类属性
    Object obj = cons.newInstance("Tom", 12);   //调用newInstance()方法获取Person类的对象
    Person p = (Person) obj;
    System.out.println(p.toString());

    //2.通过反射,调用对象指定的属性、方法
    //调用属性
    Field age = clazz.getDeclaredField("age");
    age.set(p, 10);
    System.out.println(p.toString());

    //调用方法
    Method show = clazz.getDeclaredMethod("show");
    show.invoke(p);  //show方法被调用
}

测试结果如下:
image.png
可见,之前面向对象中可以做的事,使用反射都可以做到。
此外,反射的强大之处就在于,可以调用Person类的私有结构。比如:私有的构造器、方法、属性。下面来做一下测试:

@Test
public void test3() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, NoSuchFieldException {
    //创建Class类的对象,对应Person运行时类
    Class clazz = Person.class;

    //通过反射,可以调用Person类的私有结构。比如:私有的构造器、方法、属性
    //调用私有构造器
    Constructor cons1 = clazz.getDeclaredConstructor(String.class);
    cons1.setAccessible(true);
    Person p1 = (Person) cons1.newInstance("Jerry");
    System.out.println(p1);

    //调用私有的属性
    Field name = clazz.getDeclaredField("name");
    name.setAccessible(true);
    name.set(p1, "Hanmeimei");
    System.out.println(p1);

    //调用私有的方法 (showNation方法是有返回值的)
    Method showNation = clazz.getDeclaredMethod("showNation", String.class);
    showNation.setAccessible(true);
    String nation = (String) showNation.invoke(p1, "中国"); //相当于p1.showNation("中国")
    System.out.println(nation);
    
}

在Person类中,Person(String name)构造器,name属性和showNation()方法是私有的,所以在上述代码中我们使用私有构造器(只有形参name)创建了一个名为**“Jerry”**对象p1;调用私有属性name更改了p1的name为"Hanmeimei";调用了私有的方法showNation()。
测试结果如下:
image.png
了解了反射的用法后,下面来思考一下这两个问题。
1.通过直接new的方式或反射的方式都可以调用公共的结构,那么开发中到底用哪个,什么时候该用反射,什么时候使用直接new的方式?
——建议使用直接new的方式。(可以从逻辑上辅助记忆一下,这两种方式都可以调用公共的结构,而反射不但可以调用公用的结构,还可以调用私有的结构,所以是功能更全面的一种方式。如果是调用私有结构就只能使用反射了,那么在调用公共的结构时如果还是建议使用反射的方式的话,那么"直接new的方式"不就没有存在的意义了吗)所以在绝大多数的情况下还是应该使用直接new的方式的。
那么什么时候会使用反射的方式呢?这就要提到反射特征了,对于在编译的时候确定不下来要new哪个类的对象的情况下,要使用反射的方式,这就是反射的动态性特征。(可以从配置servlet的映射路径的做法中得到验证,加深印象)
2.反射机制与面向对象中的封装性是不是矛盾的?如何看待这两个技术?
——不矛盾。封装性表述的是:建议你去调什么,不建议调用什么;而反射说明的是能不能调的问题,对于不建议调用的成分,非要用反射调用也是可以做到的。

2.1 Class类

对 java.lang.Class 类的理解:
1.类的加载过程:程序经过 javac.exe 命令以后,会生成一个或多个字节码文件(.class结尾);
接着我们使用 java.exe 命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。 (加载表示的只是这一部分,与 javac.exe 命令无关)
2.换句话说,Class的实例就对应着一个运行时类。
3.加载到内存中的运行时类,会缓存一段时间。在此时间之内,我们可以通过不同的方式来获取此运行时类。

2.2 获取Class的实例

下面介绍一下获取Class的实例的几种方式:

public class ReflectionTest{


//获取Class的实例的方式(前三种方式需要掌握) 用的最多的是方式三
@Test
public void test4() throws ClassNotFoundException {

   //方式一:调用运行时类的属性:.class
    Class clazz1 = Person.class;
    System.out.println(clazz1);

    //方式二:通过运行时类的对象,调用getClass()
    Person p1 = new Person();
    Class clazz2 = p1.getClass();
    System.out.println(clazz2);

    //Class类有一个静态方法forName
    //方式三:调用Class的静态方法:forName(String classPath)
    Class clazz3 = Class.forName("com.atguigu.java.Person");
    System.out.println(clazz3);

    //方式四:使用类的加载器:ClassLoader
    //就是使用类加载器ClassLoader的对象去加载某个类而获取该类的Class对象。 加载方式:调用ClassLoader对象的loadClass方法。
    ClassLoader classLoader = ReflectionTest.class.getClassLoader();
    Class clazz4 = classLoader.loadClass("com.atguigu.java.Person");
    System.out.println(clazz4);
    System.out.println(clazz1 == clazz4);
   }

}

方式三和方式四需要给出类的具体路径,这是由于在同一个module下的不同的包之下可能都有Person类,所以要指明是哪个包下的Person类。
测试结果如下:
image.png
可以看到,这几个Class的实例地址是相同,即是同一个对象。因为加载到内存中的运行时类,会缓存一段时间。在此时间之内,我们可以通过不同的方式来获取此运行时类。
其中,方式三可以体现出反射的动态性,用的最多。(因为有些时候我们事先是不知道要加载哪个类的,而方式二则是先有了运行时类的对象,再根据对象获取它所属的运行时类;方式一的写法则是限定好了是要加载哪个类,是在编译的时候就已经定死了的)
**哪些类型可以有Class对象? **

class外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
interface接口
[]数组
enum枚举
annotation注解@interface
primitive type基本数据类型
voidvoid类型
ClassClass.class还是一个Class实例

下面给出测试程序:

//Class实例可以是哪些结构?
@Test
public void test5(){

    Class c1 = Object.class;         //类
    Class c2 = Comparable.class;     //接口
    Class c3 = String[].class;       //一维数组
    Class c4 = int[][].class;        //二维数组
    Class c5 = ElementType.class;   //ElementType是一个枚举类
    Class c6 = Override.class;      //注解
    Class c7 = int.class;           //基本数据类型
    Class c8 = void.class;          //void类型
    Class c9 = Class.class;         //Class类
    int[] a = new int[10];
    int[] b = new int[100];
    Class c10 = a.getClass();
    Class c11 = b.getClass();

    //只要数组的元素类型与维度一样,就是同一个Class
    System.out.println(c10 == c11);   //true,因为数组a和数组b都是int类型的一维数组
} 

测试结果如下:
image.png

2.3 类的加载 与 ClassLoader的理解

2.3.1 类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化。
image.png
所以类的加载过程包括上面的三个过程,也就是说整体上叫做类的加载,只不过第一步的过程正好也叫做类的加载。
下面详细说明一下这三个过程:

  1. 加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class对象,作为方法区中类数据的访问 入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的 过程需要类加载器参与。
  2. 链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。
  • 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题。
  • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存 都将在方法区中进行分配。
  • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  1. 初始化:
  • 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信 息的,不是构造该类对象的构造器)。
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类 的初始化。
  • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。

下面给出一个例子验证:

public class ClassLoadingTest {
	public static void main(String[] args) {
	System.out.println(A.m);  //结果为100 (静态代码块和显示赋值的优先级相同,谁写在后面就是谁生效)
   }
}
class A {
	static {
		m = 300;
	}
	static int m = 100;
}

第一步:将类A加载到内存当中。
第二步:链接结束后m=0
第三步:初始化后,m的值由()方法执行决定, 这个A的类构造器()方法由类变量的赋值和静态代码块中的语句按照顺序合并产生,类似于 :

<clinit>(){
	m = 300;
	m = 100;
}

2.3.2 类加载器的作用

类加载的作用: 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class对象,作为方法区中类数据的访问入口。
类缓存 : 标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器 中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

2.3.3 对ClassLoader的理解

类加载器的作用是用来把类(class)装载进内存。JVM 规范定义了如下类型的类的加载器。
image.png
( Bootstrap ClassLoader : 引导类加载器(图片中的拼写错了))
下面我们自定义一个类ClassLoaderTest来做一下测试:

/*
* 类的加载器  */
public class ClassLoaderTest {

   @Test
    public void test1(){
       //对于自定义类,使用系统类加载器进行加载
       ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
       System.out.println(classLoader);

       //调用系统类加载器的getParent(): 获取扩展类加载器
       ClassLoader classLoader1 = classLoader.getParent();
       System.out.println(classLoader1);

       //调用扩展类加载器的getParent():无法获取引导类加载器
       //引导类加载器主要负责加载java的核心类库,无法加载自定义类
       ClassLoader classLoader2 = classLoader1.getParent();
       System.out.println(classLoader2);

       //String类为java的核心类库,获取String类的加载器
       ClassLoader classLoader3 = String.class.getClassLoader();
       System.out.println(classLoader3);
   }
    
}

测试结果如下:
image.png
classLoader2 和 classLoader3 这两个加载器显示为 null ,这是因为引导类加载器BootStrap ClassLoader 无法直接获取。
使用类加载器读取配置文件:

public class ClassLoaderTest {
    
    @Test
    public void test2() throws IOException {
    
      Properties pros = new Properties();
      //此时的文件默认在当前的module下。
      //读取配置文件的方式一:
    //     FileInputStream fis = new FileInputStream("jdbc.properties");
      //使用properties类的对象读取该jdbc.properties文件(配置文件)
    //   pros.load(fis);   //传入的参数为输入流
    
      //读取配置文件的方式二: 使用ClassLoader
      //配置文件默认识别为: 当前module的 src下
      ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
      InputStream is = classLoader.getResourceAsStream("jdbc1.properties");
      BufferedReader bf = new BufferedReader(new InputStreamReader(is, "UTF-8"));
      pros.load(bf);
    
    
      /*在Java中利用Properties类读取配置文件的信息,一般要经历如下三步:
    
      1.创建 Properties 实例;
      2.调用 load()方法 读取配置文件;
      3.调用 getProperty()方法 获取具体的配置信息。*/
      String user = pros.getProperty("user");
      String password = pros.getProperty("password");
      System.out.println("user = " + user + ", password = " + password);
    
    }
}

jdbc1.properties文件:

user = 郭凯旋
password = abc1234

测试结果如下:

image.png

3.创建运行时类的对象
通过反射创建对应的运行时类的对象,给出如下的例子:

//通过反射创建对应的运行时类的对象
public class NewInstanceTest {
    
    @Test
    public void test1() throws IllegalAccessException, InstantiationException {
        Class<Person> clazz = Person.class;

     /*   newInstance():调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。
        要想此方法正常的创建运行时类的对象,要求:
        1.运行时类必须提供空参的构造器
        2.空参的构造器的访问权限得够。通常,设置为public。*/
/*
        在javabean中要求提供一个public的空参构造器。原因:
        1.便于通过反射,创建运行时类的对象
        2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器*/

        Person obj = clazz.newInstance();
        System.out.println(obj);
        
    }
}

测试结果如下:
image.png
可见,newInstance()会调用相应类(Person)的空参构造器。所以从本质上来讲:只要是造对象,都得使用构造器来造。
如果Person类没有空参构造器时会报错,下面我们去掉Person类中的空参构造器再做测试:
image.png
构造器的权限的问题:
空参的构造器的访问权限得够,即:在java四大权限修饰符的规则之下能够访问到空参构造器即可,四大权限修饰符的作用范围如下图所示。 但是我们通常是设置为public。
image.png
如果修改Person类的空参构造器为private的话,测试时就会报"非法访问异常":
image.png
另外,从前面对反射的使用来看,在反射当中还可以使用别的方式造运行时类的对象,然而用的最多的造对象的方式还是Class对象调用newInstance()方法。

4.体会反射的动态性

在本篇的开头我们就曾提到:反射 是被视为动态语言的关键,在运行时代码可以根据某些条件改变自身结构。下面给出一个例子来体会一下反射的动态性:

//体会反射的动态性
@Test
public void test2(){
    for(int i = 0; i < 100; i++){
        int num = new Random().nextInt(3);   //0,1,2
        String classPath = "";
        switch (num){
            case 0:
                classPath = "java.util.Date";
                break;
            case 1:
                classPath = "java.lang.Object";
                break;
            case 2:
                classPath = "com.atguigu.java.Person";
                break;
        }
        Object obj = null;
        try {
            obj = getInstance(classPath);
            System.out.println(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

//创建一个指定类的对象
//classPath: 指定类的全类名
public Object getInstance(String classPath) throws Exception {
    Class clazz = Class.forName(classPath);
    return clazz.newInstance();
}

在上述代码段中,会根据classPath的值创建对应运行时类的对象(分别是util包下的Date类,lang包下的Object类,以及我们自定义的Person类)。即在编译完之后,我们是确定不了要创建的是哪个类的对象的,只有在运行的时候才能知道,到底造的是哪个类的对象。
测试结果如下:
image.png

5.获取运行时类的完整结构

通过反射可以获取运行时类内部丰富的结构,比如可以取得运行时类所实现的全部接口、所继承的父类、 全部的构造器、全部的方法、全部的属性、注解相关、泛型相关、所在的包等。
为了验证反射的这一功能,我们首先丰富一下运行时类中的结构:以前面例子中的Person类为例,让Person类继承Creature类,实现两个接口:Comparable、MyInterface,其中MyInterface接口为我们自定义的接口,里面定义一个返回类型为void的抽象方法info()。
Creature类的设计如下:

package com.atguigu.java2;

import java.io.Serializable;

public class Creature<T> implements Serializable{

    private char gender;
    public double weight;

    private void breathe(){
        System.out.println("生物呼吸");

    }
    public void eat(){
        System.out.println("生物吃东西");
    }
}

MyInterface接口的设计如下:

package com.atguigu.java2;

public interface MyInterface {
    void info();
}

此外,为Person类提供不同权限的属性,分别为:私有的name、缺省的age和公有的id。如下所示:

 private String name;
 int age;
 public int id;

提供三个不同权限的构造器:

//公有构造器
public Person() {
}

//私有构造器
private Person(String name) {
    this.name = name;
}

//缺省构造器
Person(String name, int age){
    this.name = name;
    this.age = age;
}

提供两个不同权限的方法:show()和display(),设计如下:

private String show(String nation){
    System.out.println("我的国籍是: " + nation);
    return nation;
}

public String display(String interests){
    return  interests;
}

再声明一个注解MyAnnotation并把它添加到Person类及其构造器和方法上,MyAnnotation的设计如下:

package com.atguigu.java2;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
     String value() default "hello";
}

丰富结构后的Person类的设计如下:

package com.atguigu.java2;

@MyAnnotation(value="hi")
public class Person extends Creature<String> implements Comparable<String>, MyInterface{

     private String name;
     int age;
     public int id;

     //公有构造器
    public Person() {
    }

    @MyAnnotation(value = "abc")
    private Person(String name) {
        this.name = name;
    }
    Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    @MyAnnotation
    private String show(String nation){
        System.out.println("我的国籍是: " + nation);
        return nation;
    }

    public String display(String interests){
        return  interests;
    }
    
    @Override
    public void info(){
        System.out.println("我是一个人");
    }

    @Override
    public int compareTo(String o) {
        return 0;
    }
    
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }
}

5.1 获取当前运行时类的属性结构

Class类提供的获取运行时类的属性的方法:

public Field[] getFields()返回此Class对象所表示的类或接口的public的Field。
public Field[] getDeclaredFields()返回此Class对象所表示的类或接口的全部Field。

下面给出对于Person类的测试:

@Test
public void test1(){
    Class clazz = Person.class;

    //获取属性结构
    //getFields(): 获取当前运行时类及其父类中声明为public访问权限的属性
    Field[] fields = clazz.getFields();
    for(Field f : fields){
        System.out.println(f);
    }
    
    System.out.println();

    //getDeclaredFields(): 获取当前运行时类中声明的所有属性。(不包含父类中声明的属性)
    Field[] declaredFields = clazz.getDeclaredFields();
    for(Field f : declaredFields){
        System.out.println(f);
    }
}

测试结果如下:
image.png
可见,getField()获取的当前运行时类及其父类中权限修饰为public的属性;而getDeclaredFields()获取的是当前运行时类中的全部属性。
除了可以获取到属性外,还可以获取到属性的具体结构。比如:属性的权限修饰符、属性的类型以及属性名称。Field类提供了相应的获取属性结构的方法如下:

public int getModifiers()以整数形式返回此Field的修饰符
public Class getType()得到Field的属性类型
public String getName()返回Field的名称

5.1.1 获取属性的权限修饰符

首先使用Class类提供的getDeclaredFields()得到由Person类的属性所构成的Field类型的数组,再对该数组进行遍历从而获取到每一个属性的权限修饰。

@Test
public void test2(){
    Class clazz = Person.class;
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field f : declaredFields){
        //1.权限修饰符
        int modifier = f.getModifiers();
        System.out.println(modifier);
    }
}

测试结果如下:
image.png
可以看到,这样打印出来的"权限修饰符"是以整形数字表示的,这是因为在java.lang.reflect.Modifier类中把权限修饰符专门定义为了静态常量,如下所示:PUBLIC被定义为1,PRIVATE被定义为2,缺省被定义为了0。
image.png
那么要想打印出权限修饰符的名字,只需调用Modifier类的toString方法即可:

@Test
public void test2(){
    Class clazz = Person.class;
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field f : declaredFields){
        //1.权限修饰符
        int modifier = f.getModifiers();
        System.out.println(Modifier.toString(modifier));
    }
}

测试结果如下:
image.png
age属性的权限为缺省,所以这里是空白。
注:虽然可以使用反射获取到运行时类属性的具体结构,但是在实际的开发中,是不会用到这些的。

5.1.2 获取属性的类型

@Test
public void test2(){
    Class clazz = Person.class;
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field f : declaredFields){
        //2.数据类型
        Class type = f.getType();
        System.out.println(type.getName());
    }
}

测试结果如下:
image.png

5.1.2 获取属性的变量名

@Test
public void test2(){
    Class clazz = Person.class;
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field f : declaredFields){
       //3.变量名
       String fName = f.getName();
       System.out.print(fName);
    }
}

测试结果如下:
image.png

5.2 获取当前运行时类的方法结构

Class类提供的获取运行时类的属性的方法如下:

public Method[] getMethods()返回此Class对象所表示的类或接口的public的方法
public Method[] getDeclaredMethods()返回此Class对象所表示的类或接口的全部方法

不难看出,获取运行时类的方法的api和获取属性的api是类似的。即getMethods()可以获取到当前类及其父类中的权限为public的方法;而getDeclaredMethods()可以获取到当前类中声明的所有方法(不用管权限修饰)。
下面给出测试:

@Test
public void test1(){
    //getMethods()::获取当前运行时类及其所有父类中声明为public权限的方法
    Class clazz = Person.class;
    Method[] methods = clazz.getMethods();
    for (Method m : methods){
        System.out.println(m);
    }

    System.out.println();

    //getDeclaredMethods():获取当前运行时类中声明的所有方法。(不包含父类中声明的方法)
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for(Method m : declaredMethods){
        System.out.println(m);
    }
}

测试结果如下:
image.png
和获取运行时类的属性的具体结构类似,通过反射不但可以获取到运行时类的方法,还可以获取到一个具体的方法的详细结构:权限修饰符、返回值类型、方法名、参数列表以及方法所抛出的异常信息。所用的 api 如下:

public int getModifiers()取得所有方法的权限修饰符
public Class getReturnType()取得所有方法的返回值类型
public Class[] getParameterTypes()取得所有方法的参数列表
public Class[] getExceptionTypes()取得所有方法的异常信息

下面还以Person类中的方法做一下测试,并且以如下的呈现方式把Person类中方法的具体结构信息打印出来。

@Xxxxx(注解)
权限修饰符   返回值类型    方法名(参数类型1  形参名1, ...) throws XxxException{}

设计如下:

@Test
public void test2(){
    Class clazz = Person.class;
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for(Method m : declaredMethods){
        //1.获取方法声明的注解
        Annotation[] annos = m.getAnnotations();
        for(Annotation a : annos){
            System.out.println(a);
        }

        //2.权限修饰符 (getModifiers() )
        System.out.print(Modifier.toString(m.getModifiers()) + "\t");

        //3.返回值类型 (getReturnType() )
        System.out.print(m.getReturnType().getName() + "\t");

        //4.方法名
        System.out.print(m.getName());
        System.out.print("(");

        //5.形参列表
        Class[] parameterTypes = m.getParameterTypes();

        if(!(parameterTypes == null || parameterTypes.length == 0)){
            for (int i = 0; i < parameterTypes.length; i++){
                if(i == parameterTypes.length - 1){
                    System.out.print(parameterTypes[i].getName() + " args_" + i);
                    break;
                }
                System.out.print(parameterTypes[i].getName() + " args_" + i + ",");
            }
        }
        System.out.print(")");

        //6.抛出的异常
        Class[] exceptionTypes = m.getExceptionTypes();
        if(!(exceptionTypes == null || exceptionTypes.length == 0)){
            System.out.print(" throws ");
            for (int i = 0; i < exceptionTypes.length; i++){
                if(i == exceptionTypes.length - 1){
                    System.out.print(exceptionTypes[i].getName());
                    break;
                }

                System.out.print(exceptionTypes[i].getName() + ",");
            }
        }

        System.out.println();
    }
    
}

为了验证得到正确的形参列表呈现形式 和 获取到方法的异常信息,我们为display()方法添加一个形参并抛出两个异常。

public String display(String interests, int age) throws NullPointerException, ClassCastException{
    return  interests + age;
}

测试结果如下:
image.png
需要注意的是:和获取属性的具体结构一样,虽然我们可以实现获取方法内部的具体结构,但是在实际开发当中,这个操作几乎是用不到的,只需要知道可以完成这样的操作即可。

5.3 获取当前运行时类的构造器

Class类提供的获取运行时类的构造器的方法如下:

public Constructor[] getConstructors()返回此 Class 对象所表示的类的所有public构造方法。
public Constructor[] getDeclaredConstructors()返回此 Class 对象表示的类声明的所有构造方法。 (不管权限)

可以看到,getConstructors方法与getFields 和 getMethods的区别是:getConstructors方法只能获取到本类中声明为public的构造器,而不能获取到它的父类的构造器。这个其实也很好理解,一个类的对象可以继承父类已有的属性,调用父类中的方法,但是获取父类的构造器是没有什么用的呀。
下面给出测试程序:

@Test
public void test1(){
   Class clazz = Person.class;
   //getConstructors():获取当前运行时类中声明为public的构造器
   Constructor[] constructor = clazz.getConstructors();
   for(Constructor c : constructor){
       System.out.println(c);
   }

   System.out.println();

   Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
   for (Constructor c : declaredConstructors){
       System.out.println(c);
   }
}

测试结果如下:
image.png

5.4 获取当前运行时类的父类

Class类所提供的 api 如下:

public Class getSuperclass()返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。

可以看到,该方法的返回结果为Class的实例。
下面给出测试程序:

/*
* 获取运行时类的父类
* */
@Test
public void test2(){
   Class clazz = Person.class;

   Class superclass = clazz.getSuperclass();
   System.out.println(superclass);
}

测试结果如下:
image.png
此外,由于我们当时为Person继承的父类是带有泛型的Creature,所以还可以获取运行时类的带泛型的父类。
Class类提供的api如下:

Type getGenericSuperclass()获取带有泛型的父类

下面给出测试程序:

/*
获取运行时类的带泛型的父类

 */
@Test
public void test3(){
    Class clazz = Person.class;

    Type genericSuperclass = clazz.getGenericSuperclass();
    System.out.println(genericSuperclass);
}

测试结果如下:
image.png
既然父类是带泛型的,我们也可以获取它的泛型,也就是获取带泛型的父类的泛型:
给出如下的测试程序:

/*
获取运行时类的带泛型的父类的泛型
 */
 @Test
 public void test4(){
     Class clazz = Person.class;

     Type genericSuperclass = clazz.getGenericSuperclass();
     ParameterizedType paramType = (ParameterizedType) genericSuperclass;
     //获取泛型类型
     Type[] actualTypeArguments = paramType.getActualTypeArguments();
     System.out.println(actualTypeArguments[0].getTypeName());

 }

测试结果如下:

image.png

5.5 获取当前运行时类实现的接口
Class类所提供的 api 如下:

public Class[] getInterfaces()确定此对象所表示的类或接口实现的接口。

给出如下的测试程序:

/*
获取运行时类实现的接口
*/
@Test
public void test5(){
  Class clazz = Person.class;

  Class[] interfaces = clazz.getInterfaces();
  for(Class c : interfaces){
      System.out.println(c);
  }

  System.out.println();
  //获取运行时类的父类实现的接口
  Class[] interfaces1 = clazz.getSuperclass().getInterfaces();
  for(Class c : interfaces1){
      System.out.println(c);
  }
}

测试结果如下:
image.png
可以看到,使用getInterfaces方法只能得到当前类所实现的接口(对应于我们的Person类中就是:Comparable, MyInterface),而不能得到Person的父类Creature所实现的接口Serializable。那么要想得到父类所实现的接口,那就如上述代码段中描述的那样:先得到父类,再获取父类所实现的接口。

5.6 获取当前运行时类所在的包

Class类提供的api: Package getPackage()
给出如下的测试程序:

/*
 获取运行时类所在的包

*/
@Test
public void test6(){
    Class clazz = Person.class;

    Package pack = clazz.getPackage();
    System.out.println(pack);
}

测试结果如下:
image.png
验证一下Person类所在的位置:
image.png

5.7 获取当前运行时类所声明的注解

Class类所提供的api: get Annotation(Class annotationClass)
给出如下的测试程序:

/*
   获取运行时类声明的注解

*/
@Test
public void test7(){
    Class clazz = Person.class;

    Annotation[] annotations = clazz.getAnnotations();
    for(Annotation annos : annotations){
        System.out.println(annos);
    }
}

测试结果如下:
image.png
验证一下Person类中声明的注解:

@MyAnnotation(value="hi")
public class Person extends Creature<String> implements Comparable<String>, MyInterface{
}

6. 调用运行时类的指定结构

6.1 调用运行时类中指定的属性

既然是操作属性,那么我们首先应该获取属性,在第5部分中提到了获取运行时类属性的两个方法:getFields() 和 getDeclaredFields()。它们是不需要传入参数的,因为这两个方法返回的是一个元素为满足条件的属性的数组。那么现在如果想要获取的是某一个指定的属性,那么实现该功能的方法应该是要有个接收属性名的形参的。api如下:

public Field getField(String name)获取该运行时类及其父类中属性名为name的公有属性
public Field getDeclaredField(String name)获取该运行时类中属性名为name的属性(不管权限)

获取到具体的某个属性后,操作属性使用set方法:

| public void set(Object obj, Object value) | obj:指明设置哪个对象的属性
value:将属性的值设置为多少 |
| — | — |

下面给出测试程序:

@Test
public void testField() throws Exception {

    Class clazz = Person.class;

    //创建运行时类的对象
    Person p = (Person) clazz.newInstance();


    //获取指定的属性:要求运行时类中属性声明为public
    //通常不采用此方法
    Field id = clazz.getField("id");
    //设置当前属性的值
    // set():参数1:指明设置哪个对象的属性   参数2:将此属性值设置为多少
    id.set(p, 1001);

    /*
    获取当前属性的值
    get():参数1:获取哪个对象的当前属性值
     */
    int o = (int) id.get(p);
    System.out.println(o);
}

测试结果如下:
image.png
但是通常不采用这种方式,因为它要求属性必须是public的,但是在实际的开发当中,类的属性通常都是被封装为私有的。
下面给出另一种方式的测试程序:

/*
如何操作运行时类中的指定的属性 -- 需要掌握
 */
 @Test
 public void testField1() throws Exception {
     Class clazz = Person.class;

     //创建运行时类的对象
     Person p = (Person) clazz.newInstance();

     //1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
     Field name = clazz.getDeclaredField("name");

     //2.保证当前属性是可访问的 (设置为可访问)
     name.setAccessible(true);

     //3.获取、设置指定对象的此属性值
     name.set(p, "Tom");
    /* String m = (String) name.get(p);
     System.out.println(m);*/

     System.out.println(name.get(p));
 }

测试结果如下:
image.png
其中,第二步是至关重要的,因为我们从第一步当中获取到的属性只是获取到了,但是由于它的权限问题(在Person类中的name属性是设置为私有的),依然不能对其操作,只有先设置该属性是可访问的,而后才能对其进行设置与获取的操作。

6.1 调用运行时类中的指定方法

和操作属性的方式类似,操作运行时类中的指定方法也有两种方式,但是方式一一般不常用,所以我们这里主要演示一个方式二的做法。
首先需要获取想要调用的方法,这里使用getDeclaredMethod()方法:

| public Method getDeclaredMethod(String name, Class<?>... parameterTypes) | name:指明获取的方法的名称 Class<?>… parameterTypes:指明获取的方法的形参列表 |
| — | — |

为了保证获取到的方法是可以访问的,就要像调用属性一样,设置要操作的方法是可访问的。最后调用方法的invoke()方法实现运行时类中的指定方法的调用。

| public Object invoke(Object obj, Object… args) | obj:方法的调用者、
Object… args:给方法形参赋值的实参 |
| — | — |

给出如下的测试程序:

/*
如何操作运行时类中的指定的方法 -- 需要掌握
 */
@Test
public void testMethod() throws Exception {
    
    Class clazz = Person.class;
    //创建运行时类的对象
    Person p = (Person) clazz.newInstance();

    /*
    1.获取指定的某个方法
    getDeclaredMethod():参数1 :指明获取的方法的名称  参数2:指明获取的方法的形参列表
     */
    Method show = clazz.getDeclaredMethod("show", String.class);
    //2.保证当前方法是可访问的
    show.setAccessible(true);
     /*
    3. 调用方法的invoke():参数1:方法的调用者  参数2:给方法形参赋值的实参
    invoke()的返回值即为对应类中调用的方法的返回值。
     */
    Object returnValue = show.invoke(p, "CHN");
    System.out.println(returnValue);
}

测试结果如下:
image.png
可以看到,方法show()被调用了,并且得到了该方法的返回值。这里 invoke()的返回值即为对应类中调用的方法的返回值。
对于静态方法,应该如何调用?

@Test
public void testMethod() throws Exception {

    Class clazz = Person.class;
    //创建运行时类的对象
    Person p = (Person) clazz.newInstance();

    System.out.println("********************调用静态方法*********************");

    Method showDesc = clazz.getDeclaredMethod("showDesc");
    showDesc.setAccessible(true);
    //如果调用的运行时类中的方法没有返回值,则此invoke()返回null
    //   Object returnVal = showDesc.invoke(null);
    Object returnVal = showDesc.invoke(Person.class);
    System.out.println(returnVal);       //null

}

showDesc()方法设计如下:

private static void showDesc(){
    System.out.println("我是一个可爱的人");

}

image.png
可以看到,showDesc方法被调用了,由于该方法的返回值类型为void,所以得到的返回值为null。
由于showDesc是一个静态方法,所以在向invoke()中填入参数时应该填入的是该运行时类。

6.3 调用运行时类中的指定构造器(了解)

实现过程和调用方法的过程是类似的,首先需要使用getDeclaredConstructor方法获取想要调用的构造器,getDeclaredConstructor方法的参数只有一个,因为构造器名和类名时一样的,只需要给出构造器的参数列表就行了。

public Constructor getDeclaredConstructor(Class<?>… parameterTypes)Class<?>… parameterTypes:指明构造器的参数列表

获取到指定形参列表的构造器后,使用newInstance方法调用该构造器创建运行时类的对象,当然在调用newInstance方法时需要传入构造器相应的实参。
下面我们通过下面的这个私有构造器创建对象:

private Person(String name) {
    this.name = name;
}

给出如下的测试程序:

/*
如何调用运行时类中的指定的构造器
 */
@Test
public void testConstructor() throws Exception{
    Class clazz = Person.class;

    //private Person(String name)
     /*
    1.获取指定的构造器
    getDeclaredConstructor():参数:指明构造器的参数列表
     */
    Constructor constructor = clazz.getDeclaredConstructor(String.class);
    //2.保证此构造器是可访问的
    constructor.setAccessible(true);

    //3.调用此构造器创建运行时类的对象
    Person per = (Person) constructor.newInstance("Tom");
    System.out.println(per);

}

测试结果如下:
image.png

7.反射的应用:动态代理

7.1 静态代理与动态代理特征

**代理设计模式的原理: **
使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原 始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
静态代理的特征:
静态代理的特征是代理类和目标对象的类都是在编译期间确定下来的,不利于程序的扩展。同时,每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。 **所以最好可以通过一个代理类完成全部的代理功能。 **
动态代理:
动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。
动态代理的使用场合:调试 & 远程方法调用
动态代理相比于静态代理的优点:
抽象角色(接口)中声明的所有方法都被转移到调用到处理器中一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。
动态代理的特点就是:根据加载进内存的被代理类,在运行期间创建一个对应的代理类。

7.2 静态代理实例

先来回顾一下静态代理。设计一个静态代理的例子:声明一个制造衣服的接口ClothFactory,里面定义一个生产衣服的抽象方法produceCloth()。设计如下:

interface ClothFactory{
    void produceCloth();
}

接着声明一个代理类ProxyClothFactory,实现ClothFactory接口,在该代理类中,声明一个ClothFactory引用,用于初始化被代理类的对象;并重写ClothFactory接口中的抽象方法。设计如下:

//代理类
class ProxyClothFactory implements ClothFactory{

    private ClothFactory factory;   //用被代理类对象进行初始化

    public ProxyClothFactory(ClothFactory factory){
        this.factory = factory;
    }

    @Override
    public void produceCloth() {
        System.out.println("代理工厂做一些准备工作");

        factory.produceCloth();

        System.out.println("代理工厂做一些后续工作");
    }
}

接下来声明一个具体的衣服工厂类NikeClothFactory(即被代理类)并实现ClothFactory接口,在该类中重写ClothFactory接口中的抽象方法produceCloth()。设计如下:

//被代理类
class NikeClothFactory implements ClothFactory{

    @Override
    public void produceCloth() {
        System.out.println("Nike工厂生产一批运动服");
    }

}

最后我们给出静态代理的测试:

public class StaticProxyTest {
    public static void main(String[] args) {

        //创建被代理类的对象
        NikeClothFactory nike = new NikeClothFactory();
        //创建代理类的对象
        ProxyClothFactory proxyClothFactory = new ProxyClothFactory(nike);

        proxyClothFactory.produceCloth();

    }
}

测试结果如下:
image.png
为什么叫静态代理呢?
因为代理类ProxyClothFactory和被代理类NikeClothFactory在编码时就已经写死了,即在编译期间代理类和被代理类就已经确定下来了。这就是静态代理的特点。

7.3 动态代理的实现

动态代理的相关API:
Proxy :专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。 它提供了 创建动态代理类和动态代理对象的静态方法 ,如下:
image.png
动态代理的实例设计如下:

package com.atguigu.java;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/*
* 动态代理的举例
*
*
* */

interface Human{
    String getBelief();
    void eat(String food);
}

class SuperMan implements Human{

    @Override
    public String getBelief() {
        return "I believe I can fly!";
    }

    @Override
    public void eat(String food) {
        System.out.println("我喜欢吃" + food);
    }
}

/*
*要想实现动态代理,需要解决什么问题?
*问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象?
*问题二:当通过代理类的对象调用方法时,如何动态的去调用被代理类中的同名方法?
*
* */

class ProxyFactory{
    //调用此方法,返回一个代理类的对象。解决问题一
    public static Object getProxyInstance(Object obj){ //obj:被代理类的对象(就是要根据传入的被代理类的对象动态的创建出代理类的对象)

        MyInvocationHandler handler = new MyInvocationHandler();

        handler.bind(obj);

        //调用Proxy的静态方法newProxyInstance创建代理类的对象
        //该方法的三个参数:
        //1.ClassLoader loader : 表示要创建的代理类对象是由哪个类加载的
        //2. Class<?>[] interfaces : 查看被代理类实现了哪些接口(因为代理类要和被代理类实现同样的接口)
        //3.InvocationHandler h : 该参数解决问题二
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),handler);
                //最后一个参数时invocationHandler接口的引用
    }

}

//定义InvocationHandler接口的实现类
class MyInvocationHandler implements InvocationHandler{

    private Object obj;  //需要使用被代理类的对象进行赋值

    public void bind(Object obj){
        this.obj = obj;
    }

    //当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
    //将被代理类要执行的方法a的功能声明在invoke()中
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        //method:代理类对象调用的方法,此方法也就作为了被代理类对象调用的方法。
        //obj:被代理类的对象
        Object returnValue = method.invoke(obj, args);
        //上述方法的返回值就作为当前类中的invoke()的返回值
        return returnValue;
    }
}

public class DynamicProxyTest {
    public static void main(String[] args) {
        //
        SuperMan superMan = new SuperMan();
        //proxyInstance:代理类的对象
        Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);

        //当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
        String belief = proxyInstance.getBelief();
        System.out.println(belief);
        proxyInstance.eat("清蒸南湾鱼");

    }
}

测试结果如下:
image.png

·

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值