第16章_反射机制

第3阶段:Java高级应用-第17章


1. 反射(Reflection)的概念

1.1 反射的出现背景

Java程序中,所有的对象都有两种类型:编译时类型运行时类型,而很多时候对象的编译时类型和运行时类型不一致。 Object obj = new String(“hello”); obj.getClass()

例如:某些变量或形参的声明类型是Object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是Object中的方法,那么如何解决呢?

解决这个问题,有两种方案:

方案1:在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用instanceof运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。

方案2:编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

1.2 反射概述

反射允许程序在运行时获取类的信息,并可以创建类的实例,调用类的方法,访问和修改类的字段等。
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。

从内存加载上看反射:

1.3 Java反射机制研究及应用

Java反射机制提供的功能:

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

1.4 反射相关的主要API

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

1.5 反射的优缺点

优点:

  • 提高了Java程序的灵活性和扩展性,降低了耦合性,提高自适应能力

  • 允许程序创建和控制任何类的对象,无需提前硬编码目标类

缺点:

  • 反射的性能较低

    • 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上
  • 反射会模糊程序内部逻辑,可读性较差

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

要想解剖一个类,必须先要获取到该类的Class对象。而剖析一个类或用反射解决具体的问题就是使用相关API:

  • java.lang.Class
  • java.lang.reflect.*

所以,Class对象是反射的根源。

2.1 理解Class

2.1.1 理论上

Class类同样也是Object的子类。Class类的作用是表示JVM运行时类或接口的信息。它提供了很多方法用于获取类的各种信息。

在Object类中定义了以下的方法,此方法将被所有子类继承:

public final Class getClass()

以上的方法返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。

对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。

  • Class本身也是一个类
  • Class 对象只能由系统建立对象
  • 一个加载的类在 JVM 中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件
  • 每个类的实例都会记得自己是由哪个 Class 实例所生成
  • 通过Class可以完整地得到一个类中的所有被加载的结构
  • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象

2.1.2 内存结构上

说明:上图中字符串常量池在JDK6中存储在方法区;JDK7及以后,存储在堆空间。

2.2 获取Class类的实例(四种方法)

方式1:要求编译期间已知类型

前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高

实例:

Class clazz = String.class;

方式2:获取对象的运行时类型

前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象

实例:

Class clazz = "www.atguigu.com".getClass();

方式3:可以获取编译期间未知的类型

前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException

实例:

Class clazz = Class.forName("java.lang.String");

方式4:其他方式(不做要求)

前提:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型

实例:

ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass("类的全类名");

再举例:

public class GetClassObject {
    @Test
    public void test01() throws ClassNotFoundException{
        Class c1 = GetClassObject.class;
        GetClassObject obj = new GetClassObject();
        Class c2 = obj.getClass();
        Class c3 = Class.forName("com.atguigu.classtype.GetClassObject");
        Class c4 = ClassLoader.getSystemClassLoader().loadClass("com.atguigu.classtype.GetClassObject");

        System.out.println("c1 = " + c1);
        System.out.println("c2 = " + c2);
        System.out.println("c3 = " + c3);
        System.out.println("c4 = " + c4);

        System.out.println(c1 == c2);
        System.out.println(c1 == c3);
        System.out.println(c1 == c4);
    }
}

2.3 哪些类型可以有Class对象

简言之,所有Java类型!

(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void

举例:

Class c1 = Object.class;
Class c2 = Comparable.class;
Class c3 = String[].class;
Class c4 = int[][].class;
Class c5 = ElementType.class;
Class c6 = Override.class;
Class c7 = int.class;
Class c8 = void.class;
Class c9 = 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);

2.4 Class类的常用方法

方法名 功能说明
static Class forName(String name) 返回指定类名 name 的 Class 对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例
getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型或void)名称
Class getSuperClass() 返回当前Class对象的父类的Class对象
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields() 返回Field对象的一个数组
Method getMethod(String name,Class … paramTypes) 返回一个Method对象,此对象的形参类型为paramType

举例:

String str = "test4.Person";
Class clazz = Class.forName(str);

Object obj = clazz.newInstance();

Field field = clazz.getField("name");
field.set(obj, "Peter");
Object name = field.get(obj);
System.out.println(name);

//注:test4.Person是test4包下的Person类

3. 类的加载与ClassLoader的理解

3.1 类的生命周期

类在内存中完整的生命周期:加载–>使用–>卸载。其中加载过程又分为:装载、链接、初始化三个阶段。

image-20220417173459849

3.2 类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、链接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。

类的加载又分为三个阶段:

(1)装载(Loading)

将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成

(2)链接(Linking)

①验证Verify:确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。

②准备Prepare:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。

③解析Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

(3)初始化(Initialization)

  • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。

  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

3.3 类加载器(classloader)

3.3.1 类加载器的作用

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。

类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

3.3.2 类加载器的分类(JDK8为例)

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader)

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:

(1)启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。获取它的对象时往往返回null
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
  • 并不继承自java.lang.ClassLoader,没有父加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

(2)扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

image-20220417174032702

(3)应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性 java.class.path 指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器。
  • 它是用户自定义类加载器的默认父加载器
  • 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

(4)用户自定义类加载器(了解)

  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
  • 同时,自定义加载器能够实现应用隔离,例如 Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
  • 自定义类加载器通常需要继承于ClassLoader。

3.3.3 查看某个类的类加载器对象

(1)获取默认的系统类加载器

ClassLoader classloader = ClassLoader.getSystemClassLoader();

(2)查看某个类是哪个类加载器加载的

ClassLoader classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader();

//如果是根加载器加载的类,则会得到null
ClassLoader classloader1 = Class.forName("java.lang.Object").getClassLoader();

(3)获取某个类加载器的父加载器

ClassLoader parentClassloader = classloader.getParent();

示例代码:

package com.atguigu.loader;

import org.junit.Test;

public class TestClassLoader {
    @Test
    public void test01(){
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("systemClassLoader = " + systemClassLoader);
    }

    @Test
    public void test02()throws Exception{
        ClassLoader c1 = String.class.getClassLoader();
        System.out.println("加载String类的类加载器:" + c1);

        ClassLoader c2 = Class.forName("sun.util.resources.cldr.zh.TimeZoneNames_zh").getClassLoader();
        System.out.println("加载sun.util.resources.cldr.zh.TimeZoneNames_zh类的类加载器:" + c2);

        ClassLoader c3 = TestClassLoader.class.getClassLoader();
        System.out.println("加载当前类的类加载器:" + c3);
    }

    @Test
    public void test03(){
        ClassLoader c1 = TestClassLoader.class.getClassLoader();
        System.out.println("加载当前类的类加载器c1=" + c1);

        ClassLoader c2 = c1.getParent();
        System.out.println("c1.parent = " + c2);

        ClassLoader c3 = c2.getParent();
        System.out.println("c2.parent = " + c3);

    }
}

3.3.4 使用ClassLoader获取流

关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流

InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream("exer2\\test.properties");
System.out.println(in);

举例:

//需要掌握如下的代码
    @Test
    public void test5() throws IOException {
        Properties pros = new Properties();
        //方式1:此时默认的相对路径是当前的module
//        FileInputStream is = new FileInputStream("info.properties");
//        FileInputStream is = new FileInputStream("src//info1.properties");

        //方式2:使用类的加载器
        //此时默认的相对路径是当前module的src目录
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties");


        pros.load(is);

        //获取配置文件中的信息
        String name = pros.getProperty("name");
        String password = pros.getProperty("password");
        System.out.println("name = " + name + ", password = " + password);
    }

4. 反射的基本应用

有了Class对象,能做什么?

4.1 创建运行时类的对象

这是反射机制应用最多的地方。创建运行时类的对象有两种方式:

方式1:直接调用Class对象的newInstance()方法

要求:

  • 类必须有一个无参数的构造器
  • 类的构造器的访问权限需要足够

步骤:

  • 获取该类型的Class对象
  • 调用Class对象的newInstance()方法创建对象

方式2:通过获取构造器对象来进行实例化

步骤:

  • 通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型的构造器
  • 向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数
  • 通过Constructor实例化对象

如果构造器的权限修饰符修饰的范围不可见,也可以调用setAccessible(true)

示例代码,使用反射构建BeanCopyUtils:

import org.springframework.beans.BeanUtils;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @description: BeanCopy工具类
 */
public class BeanCopyUtils {

    private BeanCopyUtils() {
    }

    public static <V> V copyBean(Object source, Class<V> clazz) {
        //创建目标对象(V 创建的对象即使传过来的对象)
        V result = null;
        try {
           //利用反射创建对应类的实例 
            result = clazz.newInstance();
            //实现属性copy
            BeanUtils.copyProperties(source, result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //返回结果
        return result;
    }

    public static <O, V> List<V> copyBeanList(List<O> list, Class<V> clazz) {
        return list.stream()
                .map(o -> copyBean(o, clazz))
                .collect(Collectors.toList());
    }
}

调用:

//获取到Class类的实例并作为参数传入
copyBean(user,UserVO.class)

4.2 获取运行时类的完整结构

可以获取:包、修饰符、类型名、父类(包括泛型父类)、父接口(包括泛型父接口)、成员(属性、构造器、方法)、注解(类上的、方法上的、属性上的)。

4.2.1 相关API

//1.实现的全部接口
public Class<?>[] getInterfaces()   
//确定此对象所表示的类或接口实现的接口。 

//2.所继承的父类
public Class<? Super T> getSuperclass()
//返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。

//3.全部的构造器
public Constructor<T>[] getConstructors()
//返回此 Class 对象所表示的类的所有public构造方法。
public Constructor<T>[] getDeclaredConstructors()
//返回此 Class 对象表示的类声明的所有构造方法。

//Constructor类中:
//取得修饰符: 
public int getModifiers();
//取得方法名称: 
public String getName();
//取得参数的类型:
public Class<?>[] getParameterTypes();

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

//Method类中:
public Class<?> getReturnType()
//取得全部的返回值
public Class<?>[] getParameterTypes()
//取得全部的参数
public int getModifiers()
//取得修饰符
public Class<?>[] getExceptionTypes()
//取得异常信息

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

//Field方法中:
public int getModifiers()
//以整数形式返回此Field的修饰符
public Class<?> getType()  
//得到Field的属性类型
public String getName()  
//返回Field的名称。

//6. Annotation相关
get Annotation(Class<T> annotationClass) 
getDeclaredAnnotations() 

//7.泛型相关
//获取父类泛型类型:
Type getGenericSuperclass()
//泛型类型:ParameterizedType
//获取实际的泛型类型参数数组:
getActualTypeArguments()

//8.类所在的包
Package getPackage() 

4.2.2案例测试

  1. Car类
public class Car {
    private String name;

    private int age;

    private String color;

    public Car() {
    }

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

    private void init() {
        System.out.println("私有init...");
    }

    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 String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}
  1. 反射具体案例
  • 获取到Class对象
  • 通过反射创建类的实例
  • 获取到类的所有public构造函数
  • 获取到类的所有构造函数
  • 获取到类的有参构造函数
  • 获取属性
  • 获取方法
  • 获取指定的方法
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectDemo {
    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
//        1.获取Class对象
//          1.1通过类名获取
        Class<Car> carClass1 = Car.class;
//          1.2通过对象获取
        Class<? extends Car> carClass2 = new Car().getClass();
//          1.3通过全类名获取
        Class<?> carClass3 = Class.forName("com.xha.pojo.Car");

//        2.创建类的实例
        Car car = carClass1.getDeclaredConstructor().newInstance();
        System.out.println("创建实例------------->");
        System.out.println(car);
        System.out.println("--------------------");

//        3.获取到类的所有public构造函数
        Constructor<?>[] constructors = carClass1.getConstructors();
        System.out.println("获取到类的所有public构造函数------------->");
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }
        System.out.println("--------------------");

//        4.获取到类的所有构造函数
        Constructor<?>[] declaredConstructors = carClass1.getDeclaredConstructors();
        System.out.println("获取到类的所有构造函数------------->");
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println(declaredConstructor);
        }
        System.out.println("--------------------");

//        5.获取到类的有参构造函数
        Constructor<Car> constructor = carClass1.getConstructor(String.class, int.class, String.class);
        Car car1 = constructor.newInstance("宝马", 3, "黑色");
        System.out.println("获取到类的有参构造函数------------->");
        System.out.println(car1);
        System.out.println("--------------------");

//        6.获取属性
//          6.1获取所有public属性
        Field[] fields = carClass1.getFields();
//          6.2获取所有属性
        Field[] declaredFields = carClass1.getDeclaredFields();
        System.out.println("获取所有属性------------->");
        for (Field declaredField : declaredFields) {
            if (declaredField.equals("name")) {
//                6.1设置属性可访问
                declaredField.setAccessible(true);
                declaredField.set(car1, "奔驰");
            }
            System.out.println(declaredField);
        }
        System.out.println("--------------------");

//        7.获取方法
//          7.1获取所有public方法
        Method[] methods = carClass1.getMethods();
        System.out.println("获取所有public方法------------->");
        for (Method method : methods) {
            System.out.println(method);
        }
        System.out.println("--------------------");
//          7.2获取指定的方法
        Method[] declaredMethods = carClass1.getDeclaredMethods();
        System.out.println("2获取指定的方法------------->");
        for (Method declaredMethod : declaredMethods) {
//            7.3设置方法可访问
            declaredMethod.setAccessible(true);
            if (declaredMethod.getName().equals("init")) {
                declaredMethod.invoke(car1);
            }
        }
        System.out.println("--------------------");
    }
}

测试结果:

创建实例------------->
Car{name='null', age=0, color='null'}
--------------------
获取到类的所有public构造函数------------->
public com.xha.pojo.Car()
public com.xha.pojo.Car(java.lang.String,int,java.lang.String)
--------------------
获取到类的所有构造函数------------->
public com.xha.pojo.Car()
public com.xha.pojo.Car(java.lang.String,int,java.lang.String)
--------------------
获取到类的有参构造函数------------->
Car{name='宝马', age=3, color='黑色'}
--------------------
获取所有属性------------->
private java.lang.String com.xha.pojo.Car.name
private int com.xha.pojo.Car.age
private java.lang.String com.xha.pojo.Car.color
--------------------
获取所有public方法------------->
public java.lang.String com.xha.pojo.Car.getName()
public java.lang.String com.xha.pojo.Car.toString()
public void com.xha.pojo.Car.setName(java.lang.String)
public void com.xha.pojo.Car.setColor(java.lang.String)
public int com.xha.pojo.Car.getAge()
public java.lang.String com.xha.pojo.Car.getColor()
public void com.xha.pojo.Car.setAge(int)
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
--------------------
2获取指定的方法------------->
私有init...
--------------------

Process finished with exit code 0

4.3 调用运行时类的指定结构

4.3.1 调用指定的属性

在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()和get()方法就可以完成设置和取得属性内容的操作。

  1. 获取该类型的Class对象

    Class clazz = Class.forName(“包.类名”);

  2. 获取属性对象

    Field field = clazz.getDeclaredField(“属性名”);

  3. 如果属性的权限修饰符不是public,那么需要设置属性可访问

    field.setAccessible(true);

    关于setAccessible方法的使用:

    • Method和Field、Constructor对象都有setAccessible()方法。
    • setAccessible启动和禁用访问安全检查的开关。
    • 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
      • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。
      • 使得原本无法访问的私有成员也可以访问
    • 参数值为false则指示反射的对象应该实施Java语言访问检查。
  4. 创建实例对象:如果操作的是非静态属性,需要创建实例对象

    Object obj = clazz.newInstance(); //有公共的无参构造

    Object obj = 构造器对象.newInstance(实参…);//通过特定构造器对象创建实例对象

  5. 设置指定对象obj上此Field的属性内容

    field.set(obj,”属性值”);

  6. 如果操作静态变量,那么实例对象可以省略,用null表示

  7. 取得指定对象obj上此Field的属性内容

    Object value = field.get(obj);

示例代码:

package com.atguigu.reflect;

public class Student {
    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
package com.atguigu.reflect;

import java.lang.reflect.Field;

public class TestField {
    public static void main(String[] args)throws Exception {
        //1、获取Student的Class对象
        Class clazz = Class.forName("com.atguigu.reflect.Student");

        //2、获取属性对象,例如:id属性
        Field idField = clazz.getDeclaredField("id");

        //3、如果id是私有的等在当前类中不可访问access的,我们需要做如下操作
        idField.setAccessible(true);

        //4、创建实例对象,即,创建Student对象
        Object stu = clazz.newInstance();

        //5、获取属性值
        /*
         * 以前:int 变量= 学生对象.getId()
         * 现在:Object id属性对象.get(学生对象)
         */
        Object value = idField.get(stu);
        System.out.println("id = "+ value);

        //6、设置属性值
        /*
         * 以前:学生对象.setId(值)
         * 现在:id属性对象.set(学生对象,值)
         */
        idField.set(stu, 2);

        value = idField.get(stu);
        System.out.println("id = "+ value);
    }
}

4.3.2 调用指定的方法

image-20220417181700813
  1. 获取该类型的Class对象

    Class clazz = Class.forName(“包.类名”);

  2. 获取方法对象

    Method method = clazz.getDeclaredMethod(“方法名”,方法的形参类型列表);

  3. 创建实例对象

    Object obj = clazz.newInstance();

  4. 调用方法

    Object result = method.invoke(obj, 方法的实参值列表);

  5. 如果方法的权限修饰符修饰的范围不可见,也可以调用setAccessible(true)

  6. 如果方法是静态方法,实例对象也可以省略,用null代替

示例代码:

package com.atguigu.reflect;

import org.junit.Test;

import java.lang.reflect.Method;

public class TestMethod {
    @Test
    public void test()throws Exception {
        // 1、获取Student的Class对象
        Class<?> clazz = Class.forName("com.atguigu.reflect.Student");

        //2、获取方法对象
        /*
         * 在一个类中,唯一定位到一个方法,需要:(1)方法名(2)形参列表,因为方法可能重载
         *
         * 例如:void setName(String name)
         */
        Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);

        //3、创建实例对象
        Object stu = clazz.newInstance();

        //4、调用方法
        /*
         * 以前:学生对象.setName(值)
         * 现在:方法对象.invoke(学生对象,值)
         */
        Object setNameMethodReturnValue = setNameMethod.invoke(stu, "张三");

        System.out.println("stu = " + stu);
        //setName方法返回值类型void,没有返回值,所以setNameMethodReturnValue为null
        System.out.println("setNameMethodReturnValue = " + setNameMethodReturnValue);

        Method getNameMethod = clazz.getDeclaredMethod("getName");
        Object getNameMethodReturnValue = getNameMethod.invoke(stu);
        //getName方法返回值类型String,有返回值,getNameMethod.invoke的返回值就是getName方法的返回值
        System.out.println("getNameMethodReturnValue = " + getNameMethodReturnValue);//张三
    }

    @Test
    public void test02()throws Exception{
        Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguClass");
        Method printInfoMethod = clazz.getMethod("printInfo", String.class);
        //printInfo方法是静态方法
        printInfoMethod.invoke(null,"尚硅谷");
    }
}

4.4. 处理运行时类的注解

4.4.1注解概述

注解(Annotation)是一种元数据形式,可以理解为注解是一种标记,是给类、属性、方法等加标签;这些标签可以在编译、类加载、运行时被读取,并执行相应的处理。如我们常见的@Override就是注解。而它的源码如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

定义在注解上的@Target@Retention的注解是什么意思呢?这些在自定义注解上的注解,叫做元注解

元注解:

Java 中提供了以下元注解类型:

  • @Retention
  • @Target
  • @Documented
  • @Inherited(JDK8 引入)
  • @Repeatable(JDK8 引入)

常用的就@Target@Retention

4.4.2@Retention

指明了注解的生命周期。如:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD) 
public @interface TestSource {
}

RetentionPolicy 是一个枚举类型,它定义了被 @Retention 修饰的注解所支持的保留级别:

  • RetentionPolicy.SOURCE - 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
  • RetentionPolicy.CLASS - 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
  • RetentionPolicy.RUNTIME - 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

这3个生命周期分别对应于:Java源文件(.java文件) —> .class文件 —> 内存中的字节码。

4.4.3@Target

指定注解可以修饰的元素类型(如方法、类、字段)。如:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TestRunTime {
}

ElementType也是一个枚举类型

  • ElementType.ANNOTATION_TYPE - 标记的注解可以应用于注解类型。
  • ElementType.CONSTRUCTOR - 标记的注解可以应用于构造函数。
  • ElementType.FIELD - 标记的注解可以应用于字段或属性。
  • ElementType.LOCAL_VARIABLE - 标记的注解可以应用于局部变量。
  • ElementType.METHOD - 标记的注解可以应用于方法。
  • ElementType.PACKAGE - 标记的注解可以应用于包声明。
  • ElementType.PARAMETER - 标记的注解可以应用于方法的参数。
  • ElementType.TYPE - 标记的注解可以应用于类的任何元素。

4.4.4@Documented

指定注解会被JavaDoc工具提取成文档。 默认情况下,JavaDoc 是不包括注解的。

4.4.5@Inherited

表示该注解会被子类继承,注意,仅针对类,成员属性、方法并不受此注释的影响。

4.4.6@Repeatable

表示注解可以重复使用,为了解决同一个注解不能重复在同一类/方法/属性上使用的问题。

4.4.7自定义注解

注解的语法格式如下:

public @interface 注解名称 {
[访问级别修饰符] [数据类型] 属性名() default 默认值;
}
  • [访问级别修饰符]只能使用 public 或默认访问级别(即不指定访问级别修饰符)修饰。
  • [数据类型] 有限制要求。支持的数据类型如下:
    • 所有基本数据类型(byte、char、short、int、long、float、double、boolean)
    • String 类型
    • Class 类
    • enum 类型
    • Annotation 类型
    • 以上所有类型的数组
  • isAnnotationPresent(Class<? extends Annotation> annotationClass)方法:作用是判断注解是否位于注解之上。如A.isAnnotationPresent(B.class);意思就是:判断注释B是否定义在元素A上。
  • default 指定该属性的默认值,既然设置了属性,建议还是给出默认值。
  • 如果注解中只有一个属性值,最好将其命名为 value。因为,指定属性名为 value,在使用注解时,指定 value 的值可以不指定属性名称。如:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface TestClass {
    String value() default "haha";
}
@TestClass("aaa")
private final String mTestClass = "";
//或者
@TestClass(value = "aaa")
private final String mTestClass = "";

如果不是命名value,则不可以使用第一种方法,编译器会报错提示。

4.4.8通过反射处理运行时类的注解

我们使用注解一般有两种使用方式:

  • 运行时处理注解运用反射,比如GSON序列化
  • 编译期处理注解:使用注解处理器AbstractProcessor处理注解,然后通过javapoet生成一个新的java类。如:Arouter、Butterknife等

运行时注解

比如我现在有个数据类User,数据上传的时候需要把name转换成”李四”,age转换成”20”。

  1. 定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SerializedName {
    public String name() default "";
    public int age() default 0;
}
  1. User类
import annotation.SerializedName;

public class User {
    @SerializedName(name = "李四")
    private String name;

    @SerializedName(age = 20)
    private int age;

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  1. 利用反射,运行时处理注解

在这个方法中,我们首先获取了User类的Class对象,然后调用getDeclaredFields方法来获取这个类中声明的所有字段。对于每个字段,我们调用isAnnotationPresent方法来检查它是否有SerializedName注解。如果这个注解存在,我们就可以根据字段的类型来获取对应的元素值。例如,如果字段是字符串类型,那么我们就获取name元素的值;如果字段是整数类型,那么我们就获取age元素的值。然后,我们使用反射API来修改字段的值:首先调用field.setAccessible(true)方法来允许访问私有字段,然后调用field.set(user, value)方法来修改字段的值。最后,我们直接返回传入的user参数。

import annotation.SerializedName;

import java.lang.reflect.Field;

public class HandleAnnotation {
    public User handleAnnotation(User user) {
//        1.根据反射获取到属性
        Field[] declaredFields = user.getClass().getDeclaredFields();
        for (Field field : declaredFields) {
//            判断属性上是否有注解
            if (field.getAnnotations() != null) {
//                2.确定属性类型是否是SerializedName注解
                if (field.isAnnotationPresent(SerializedName.class)) {
//                3.允许访问私有属性
                    field.setAccessible(true);
//                4.根据属性类型获取到注解
                    SerializedName annotation = field.getAnnotation(SerializedName.class);
                    if (annotation != null) {
                        if (field.getType() == String.class) {
                            try {
//                5.给属性赋值,赋值为注解中的值
                                field.set(user, annotation.name());
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }
                        } else if (field.getType() == int.class) {
                            try {
                                field.set(user, annotation.age());
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
        return user;
    }
}
  1. 使用
public class Test {
    public static void main(String[] args) {
        HandleAnnotation handleAnnotation = new HandleAnnotation();
        User user = handleAnnotation.handleAnnotation(new User("张三", 18));
        System.out.println(user.toString());
    }

image-20230724234148318

5. 体会反射的动态性

体会1:

public class ReflectionTest {

    //体会反射的动态性:动态的创建给定字符串对应的类的对象
    public <T> T getInstance(String className) throws Exception {

        Class clazz = Class.forName(className);

        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        return (T) constructor.newInstance();

    }

    @Test
    public void test1() throws Exception {
        String className = "com.atguigu.java1.Person";
        Person p1 = getInstance(className);
        System.out.println(p1);
    }
}

体会2:

public class ReflectionTest {
    //体会反射的动态性:动态的创建指定字符串对应类的对象,并调用指定的方法
    public Object  invoke(String className,String methodName) throws Exception {
        Class clazz = Class.forName(className);
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        //动态的创建指定字符串对应类的对象
        Object obj = constructor.newInstance();

        Method method = clazz.getDeclaredMethod(methodName);
        method.setAccessible(true);
        return method.invoke(obj);
    }

    @Test
    public void test2() throws Exception {
        String info = (String) invoke("com.atguigu.java1.Person", "show");

        System.out.println("返回值为:" + info);

    }
}

体会3:

public class ReflectionTest {
	@Test
    public void test1() throws Exception {
        //1.加载配置文件,并获取指定的fruitName值
        Properties pros = new Properties();
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("config.properties");
        pros.load(is);
        String fruitStr = pros.getProperty("fruitName");
        //2.创建指定全类名对应类的实例
        Class clazz = Class.forName(fruitStr);
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Fruit fruit = (Fruit) constructor.newInstance();
        //3. 调用相关方法,进行测试
        Juicer juicer = new Juicer();
        juicer.run(fruit);

    }

}

interface Fruit {
	public void squeeze();
}

class Apple implements Fruit {
	public void squeeze() {
		System.out.println("榨出一杯苹果汁儿");
	}
}

class Orange implements Fruit {
	public void squeeze() {
		System.out.println("榨出一杯桔子汁儿");
	}
}

class Juicer {
	public void run(Fruit f) {
		f.squeeze();
	}
}

其中,配置文件【config.properties】存放在当前Module的src下

com.atguigu.java1.Orange

6.随堂复习

1. 反射的概述(熟悉)

  • Java给我们提供了一套API,使用这套API我们可以在运行时动态的获取指定对象所属的类,创建运行时类的对象,调用指定的结构(属性、方法)等。
  • API:
    • java.lang.Class:代表一个类
    • java.lang.reflect.Method:代表类的方法
    • java.lang.reflect.Field:代表类的成员变量
    • java.lang.reflect.Constructor:代表类的构造器
    • … …
  • 反射的优点和缺点
    • 优点:
      • 提高了Java程序的灵活性和扩展性,降低了耦合性,提高自适应能力

      • 允许程序创建和控制任何类的对象,无需提前硬编码目标类

    • 缺点:
      • 反射的性能较低
        • 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上
      • 反射会模糊程序内部逻辑,可读性较差
  • 反射,平时开发中,我们使用并不多。主要是在框架的底层使用。

2. Class:反射的源头

  • Class的理解 (掌握)

    针对于编写好的.java源文件进行编译(使用javac.exe),会生成一个或多个.class字节码文件。接着,我们使用
    java.exe命令对指定的.class文件进行解释运行。这个解释运行的过程中,我们需要将.class字节码文件加载(使用类的加载器)到内存中(存放在方法区)。加载到内存中的.class文件对应的结构即为Class的一个实例。
  • 获取Class的实例的几种方式(前三种)

    • 类.class
    • 对象.getClass()
    • (使用较多)Class调用静态方法forName(String className)
    • (了解)使用ClassLoader的方法loadClass(String className)
  • Class 可以指向哪些结构。

    简言之,所有Java类型!
    (1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
    (2)interface:接口
    (3)[]:数组
    (4)enum:枚举
    (5)annotation:注解@interface
    (6)primitive type:基本数据类型
    (7)void

3. 类的加载过程、类的加载器(理解)

  • 类的加载过程

    过程1:类的装载(loading)
    将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成
    
    过程2:链接(linking)
    > 验证(Verify):确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。
    > 准备(Prepare):正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
    > 解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
    
    过程3:初始化(initialization)
    执行类构造器<clinit>()方法的过程。
    类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。
  • 类的加载器

    5.1 作用:负责类的加载,并对应于一个Class的实例。
    
    5.2 分类(分为两种):
    > BootstrapClassLoader:引导类加载器、启动类加载器
         > 使用C/C++语言编写的,不能通过Java代码获取其实例
         > 负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)
    
    > 继承于ClassLoader的类加载器
        > ExtensionClassLoader:扩展类加载器
                > 负责加载从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库
        > SystemClassLoader/ApplicationClassLoader:系统类加载器、应用程序类加载器
                > 我们自定义的类,默认使用的类的加载器。
        > 用户自定义类的加载器
                > 实现应用的隔离(同一个类在一个应用程序中可以加载多份);数据的加密。

4. 反射的应用1:创建运行时类的对象(重点)

Class clazz = Person.class;

//创建Person类的实例
Person per = (Person) clazz.newInstance();

System.out.println(per);
要想创建对象成功,需要满足:
条件1:要求运行时类中必须提供一个空参的构造器
条件2:要求提供的空参的构造器的权限要足够。

5. 反射的应用2:获取运行时类所有的结构

(了解)获取运行时类的内部结构1:所有属性、所有方法、所有构造器
(熟悉)获取运行时类的内部结构2:父类、接口们、包、带泛型的父类、父类的泛型等

6. 反射的应用3:调用指定的结构(重点)

3.1 调用指定的属性(步骤)
步骤1.通过Class实例调用getDeclaredField(String fieldName),获取运行时类指定名的属性
步骤2. setAccessible(true):确保此属性是可以访问的
步骤3. 通过Filed类的实例调用get(Object obj) (获取的操作)
                  或 set(Object obj,Object value) (设置的操作)进行操作。

3.2 调用指定的方法(步骤)
步骤1.通过Class的实例调用getDeclaredMethod(String methodName,Class ... args),获取指定的方法
步骤2. setAccessible(true):确保此方法是可访问的
步骤3.通过Method实例调用invoke(Object obj,Object ... objs),即为对Method对应的方法的调用。
     invoke()的返回值即为Method对应的方法的返回值
     特别的:如果Method对应的方法的返回值类型为void,则invoke()返回值为null

3.3 调用指定的构造器(步骤)
步骤1.通过Class的实例调用getDeclaredConstructor(Class ... args),获取指定参数类型的构造器
步骤2.setAccessible(true):确保此构造器是可以访问的
步骤3.通过Constructor实例调用newInstance(Object ... objs),返回一个运行时类的实例。

7. 反射的应用4:注解的使用(了解)

8. 体会:反射的动态性

public class ReflectTest {

    //体会:静态性
    public Person getInstance(){
        return new Person();
    }

    //体会:反射的动态性
    //举例1:
    public <T> T getInstance(String className) throws Exception {

        Class clazz = Class.forName(className);

        Constructor con = clazz.getDeclaredConstructor();
        con.setAccessible(true);

        return (T) con.newInstance();

    }

    @Test
    public void test1() throws Exception {
        Person p1 = getInstance();
        System.out.println(p1);

        String className = "com.atguigu04.other.dynamic.Person";
        Person per1 = getInstance(className);
        System.out.println(per1);

        String className1 = "java.util.Date";
        Date date1 = getInstance(className1);
        System.out.println(date1);
    }

    //体会:反射的动态性
    //举例2:
    public Object invoke(String className,String methodName) throws Exception {
        //1. 创建全类名对应的运行时类的对象
        Class clazz = Class.forName(className);

        Constructor con = clazz.getDeclaredConstructor();
        con.setAccessible(true);

        Object obj = con.newInstance();

        //2. 获取运行时类中指定的方法,并调用
        Method method = clazz.getDeclaredMethod(methodName);
        method.setAccessible(true);
        return method.invoke(obj);
    }

    @Test
    public void test2() throws Exception {
        String className = "com.atguigu04.other.dynamic.Person";
        String methodName = "show";

        Object returnValue = invoke(className,methodName);
        System.out.println(returnValue);
    }

}

8.企业真题

2.1 反射概述

1. 对反射了解吗?反射有什么好处?为什么需要反射?(微*银行)

类似问题:
> Java反射的作用是什么?(三*重工、上海*和网络)
> Java反射机制的作用有什么?(上海明*物联网)
> 反射的具体用途?(阿***芝*信用项目组)

2. 反射的使用场合和作用、及其优缺点(*软国际)

类似问题:
> 反射机制的优缺点(君*科技)
> Java反射你怎么用的?(吉*航空)

3. 实现Java反射的类有什么?(君*科技)

类似问题:
> Java反射 API 有几类?(北京*蓝)

问API。

4. 反射是怎么实现的?(上海立*网络)

从Class说起。

2.2 Class的理解

1. Class类的作用?生成Class对象的方法有哪些?(顺*)

反射的源头。 主要有三种。

2. Class.forName(“全路径”) 会调用哪些方法 ? 会调用构造方法吗?加载的类会放在哪?(上*银行外包)

Class.forName() 会执行执行类构造器()方法。

不会调用构造方法

加载的类放在方法区。

2.3 类的加载

1. 类加载流程(汇**通、同*顺、凡*科技)

2.4 创建对象

1. 说一下创建对象的几种方法?(华油***集团、*科软、凡*科技)

类似问题:
> 除了使用new创建对象之外,还可以用什么方法创建对象?(*云网络)

image-20221214145240412

2. 如何找到对象实际类的?(*度)

对象.getClass();

Object obj = new Date();

obj.getClass();// 获取到的是Date。

3. Java反射创建对象效率高还是通过new创建对象的效率高?(三*重工)

new 的方式。

2.5 调用属性、方法

1. 如何利用反射机制来访问一个类的方法?(神州**软件)

调用指定的方法(步骤)
步骤1.通过Class的实例调用getDeclaredMethod(String methodName,Class ... args),获取指定的方法
步骤2. setAccessible(true):确保此方法是可访问的
步骤3.通过Method实例调用invoke(Object obj,Object ... objs),即为对Method对应的方法的调用。
     invoke()的返回值即为Method对应的方法的返回值
     特别的:如果Method对应的方法的返回值类型为void,则invoke()返回值为null

2. 说一下Java反射获取私有属性,如何改变值?(阿****麻信用项目组)

调用指定的属性(步骤)
步骤1.通过Class实例调用getDeclaredField(String fieldName),获取运行时类指定名的属性
步骤2. setAccessible(true):确保此属性是可以访问的
步骤3. 通过Filed类的实例调用get(Object obj) (获取的操作)
                       或 set(Object obj,Object value) (设置的操作)进行操作。
针对于核心源码的api,内部的私有的结构在jdk17中就不可以通过反射调用了。

第16章_反射机制
https://xhablog.online/2021/03/20/Java基础-第16章_反射机制/
作者
Xu huaiang
发布于
2021年3月20日
许可协议