Java高级:反射

1.1 Java反射机制概述

1.1.1 概念

  • 反射(Reflection)是被视为动态语言的关键,反射机制允许程序在执行期间借助Reflection API获取任何类的内部信息,并能够直接操作任意对象的内部属性和方法。
  • 类加载完成后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类结构信息。我们可以通过这个对象看到类的结构,这个对象就像是一面镜子,透过这个镜子可以看到类的结构,故称之为反射

1.1.2 反射的应用

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

1.1.3 反射相关的主要API

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

1.1.4 反射与封装的思考

通过反射是可以访问类的私有方法和私有属性的,那么是不是封装性就没有意义了呢?

固然反射可以打破封装性,其实封装性更多的像是一种约定,约定类的私有方法和私有属性在外部就正常情况下最好不要调用,可以通过公共的方法简介调用私有属性和方法。就像是一台手机,类似于cpu调度等等是私有的,已经封装好了,在使用的时候通过APP可以实现对cpu的调度,反射就像是root,可以访问内部的cpu调度,一般情况下没有必要这么做,但是如果某些特殊情况则可以这么做。

1.2 Class类的理解

1.2.1 获取Class实例对象的方式

    @Test
    public void test2() throws ClassNotFoundException {
        // 方式一:次奥用运行时类的属性:.class
        Class clazz1 = User.class;
        System.out.println(clazz1);
        // 方式二:通过运行时类对象,调用getClass()方法
        User user = new User();
        Class clazz2 = user.getClass();
        System.out.println(clazz2);
        // 方式三:调用Class的静态方法:forName(String classPath)
        Class clazz3 = Class.forName("pojo.User");
        System.out.println(clazz3);
        // 方式四:使用类加载器ClassLoader
        ClassLoader classLoader = System.class.getClassLoader();
        Class clazz4 = classLoader.loadClass("pojo.User");
        System.out.println(clazz4);

        // 加载到内存的运行时类对象,只会存有一份,且会缓存一段时间,在此时间内,我们可以通过不同的方式获取此运行时类。
        System.out.println(clazz1 == clazz2);       //ture
        System.out.println(clazz1 == clazz3);       //ture
        System.out.println(clazz1 == clazz4);       //ture
    }

1.2.2 Class对象的分类

  • class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
  • interface:接口
  • []:数组
  • enum:枚举
  • annotation:注解@interface
  • primitive type:基本数据类型,例如:int.class
  • void
@Test
public void test3(){
    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];
    int[][] c = new int[10][10];
    Class c10 = a.getClass();
    Class c11 = b.getClass();
    Class c12 = c.getClass();
    // 对于相同类型相同维度的数组,运行时类是一样的
    // 不同类型或者不同维度的数组,运行时类不一样
    System.out.println(c10 == c11);     // true
    System.out.println(c10 == c12);     // false
}

1.3 类的加载

1.3.1 类加载的时机

关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行 强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》 则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之 前开始):

  1. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  2. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始 化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
    • 使用new关键字实例化对象的时候。
    • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外) 的时候。
    • 调用一个类型的静态方法的时候。
  3. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。
  4. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  6. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

1.3.2 类加载的过程

  1. 编译:经过javac.exe编译后Person.java 会生成一个Person.class文件。

    img

  2. 加载:Person.class经过java.exe加载后,会把类的相关信息加载到JVM内存中,这些信息会转化成数据结构保存到方法区的元空间中,同时在堆中生成一个代表这个类的java.lang.Class的运行时类对象,作为元空间中这个类的数据的访问入口。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xIMv4ytZ-1658343277072)(https://s2.loli.net/2022/07/19/RhByxL9onu5ZDaA.jpg)]

  3. 连接:连接阶段会对静态变量的值进行默认赋值,此时Person类的静态变量赋值为0;

  4. 初始化:首先会对Person类的静态变量进行真正的赋值的操作。

  5. 对象创建:当我们new一个对象的时候JVM首先会去找到对应的类元信息,如果找不到意味着类信息还没有被加载,所以在对象创建的时候也可能会触发类的加载操作。当类元信息被加载之后,我们就可以通过类元信息来确定对象信息和需要申请的内存大小。

1.3.3 对象的创建过程

当我们new一个对象的时候,对象就开始创建了,执行流程大致分为三步:

  1. 构建对象:首先JVM首先会去找到对应的类元信息,确定对象的大小。然后向JVM堆中申请一块内存区域并构建对象,同时为对象成员变量信息赋默认值。
  2. 初始化对象:搜集到的{}代码块逻辑生成init方法,然后执行对象内部生成的init方法,初始化成员变量值,最后执行对象构造方法。
  3. 引用对象:对象实例化完毕后,再把栈中的对象引用地址指向Person对象在堆内存中的地址。

1.4 类加载器ClassLoader

1.4.1 类加载器的作用

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

1.4.2 类缓存

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

1.4.3 类加载器分类

  1. Bootstrap ClassLoader:引导类加载器,使用C++语言编写的,是JVM自带的类加载器,负责加载Java平台核心类库。该加载器无法直接获取。
  2. Extension ClassLoader:扩展类加载器,负责jre/lib/ext目录下的jar包或者-D java.ext.dirs指定目录下的jar包的加载。
  3. System ClassLoader:负责java -classpath或-D java.class.path所之的目录下的类与jar包的加载,是最常用的加载器。

1.4.4 双亲委派机制

image-20220719193320758

双亲委派机制:如果一个类加载器收到了类加载的请求,这个类加载器不会先尝试加载这个类,而是会先委派给自己的父类加载器去完成,在每个层次的类加载器都是依此类推的。因此所有的类加载器请求其最后都应该被委派到顶层的启动类加载器中(Bootstrap),只有当父类的加载器在自己管辖范围内没有找到所需要的类的时候,子类加载器才会尝试自己去加载的,如果自己管理范围内也找不到需要加载的就会抛出:ClassNotFoundException 这个异常了。这就是为什么如果我们自己写java.lang.string类的时候,是不行的。

1.5 创建运行时类对象

1.5.1 代码

public void test4() throws Exception {
    Class clazz = Class.forName("pojo.User");
    User user = (User) clazz.getDeclaredConstructor().newInstance();
}

Java 9之前建议调用clazz.newInstance()来创建对象,Java 9之后,推荐使用clazz.getDeclaredConstructor().newInstance()来创建对象。

1.5.2 为什么要提供空参构造器

因为clazz.getDeclaredConstructor().newInstance()底层是通过调用类的空参构造器进行对象的创建。为了代码的健壮性,方便以后可能通过反射获取对象,推荐在所写的类中提供空参构造器。

当然也可以使用带参构造器进行对象的创建。例如clazz.getDeclaredConstructor().newInstance(String str, Integer num),但带参构造器的参数千差万别,为了代码的通用性,建议使用空参构造器进行对象的创建。

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

1.6.1 获取全部的接口:

  • public Class<?>[] getInterfaces() :返回此对象所表示的类或接口实现的接口。

1.6.2 获取所有继承的父类

  • public Class<? Super T> getSuperclass() :返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class

1.6.3 获取全部的构造器

  • public Constructor<T>[] getConstructors():返回此 Class 对象所表示的类的所有 public 构造方法。
  • public Constructor<T>[] getDeclaredConstructors():返回此Class对象表示的类声明的所有构造方法。

在Constructor类中:

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

1.6.4 获取全部的方法

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

Method 类中:

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

1.6.5 获取全部的Field

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

Field 方法中

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

1.6.6 获取全部的Annotation信息

  • get Annotation(Class<T> annotationClass):获取某个注解信息
  • getDeclaredAnnotations():获取全部的注解信息

1.6.7 获取泛型信息

  • Type getGenericSuperclass():获取带泛型的父类
  • getActualTypeArguments():获取实际的泛型类型参数数组

实际代码

@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());
    System.out.println(((Class)actualTypeArguments[0]).getName());
}

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

1.7.1 调用指定属性

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

  • public Field getField(String name) :返回此 Class 对象表示的类或接口的指定的 publicField

  • public Field getDeclaredField(String name): 返回此 Class 对象表示的类或接口的指定的 Field

    Field 中:

  • public Object get(object obj): 取得指定对象 obj 上此 Field 的属性内容

  • public void set(Object obj,Object value): 设置指定对象 obj 上此 Field 的属性内容

1.7.2 调用指定方法

通过反射,调用类中的方法,通过 Method 类完成。步骤:

  1. 通过 Class 类的 getMethod(String name,Class… parameterTypes) 方法取得一个 Method 对象,并设置此方法操作时所需要的参数类型。
  2. 之后使用 Object invoke(Object obj, Object[] args) 进行调用,并向方法中传递要设置的 obj 对象的参数信息。

Object invoke(object obj,Object… args)方法:

  1. Object 对应原方法的返回值,若原方法无返回值,此时返回 null
  2. 若原方法若为静态方法,此时形参 Object obj 可为 null
  3. 若原方法形参列表为空,则 Object[] argsnull
  4. 若原方法声明为 private,则需要在调用此 invoke() 方法前,显式调用方法对象的 setAccessible(true) 方法,将可访问 private 的方法。

关于 setAccessible 方法的使用:

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

1.7.3 调用指定构造器

@Test
public void testConstructor() throws Exception {
    Class clazz = Person.class;

    Constructor constructor = clazz.getDeclaredConstructor(String.class);

    //2.保证此构造器是可访问的
    constructor.setAccessible(true);

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

1.8 反射的应用:动态代理

1.8.1 代理模式

概念:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

1.8.2 静态代理

静态代理的缺点

  • 代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。
  • 每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。

1.8.3 动态代理

需要解决的两个主要问题

  1. 如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象:通过 Proxy.newProxyInstance() 实现

  2. 当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法:通过 InvocationHandler 接口的实现类及其方法 invoke()

动态代理相关的API

Proxy:专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。提供用于创建动态代理类和动态代理对象的静态方法。

  • static Class<?> getProxyClass(ClassLoader loader, Class<?>...interface) 创建一个动态代理类所对应的 Class 对象
  • static Object newProxyInstance(ClassLoader loader, Class<?>...interface, InvocationHandler h) 直接创建一个动态代理对象

动态代理实现步骤

  1. 创建一个实现接口 InvocationHandler 的类,它必须实现invoke方法,以完成代理的具体操作。
  2. 创建被代理类对象
  3. 通过Proxy的静态方法 newProxyInstance(ClassLoader loader, Class<?>...interface, InvocationHandler h) 创建一个代理对象
  4. 通过代理类的实例调用被代理类的方法

动态代理代码

public interface Human {
    public void getBelief();
    public void eat(String str);
}

public class SuperMan implements Human{
    @Override
    public void getBelief() {
        System.out.println("我是超人我会飞");
    }

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

public class TestProxy {
    @Test
    public void testProxy(){
        SuperMan superMan = new SuperMan();
        ProxyFactory proxyFactory = new ProxyFactory(superMan);
        Human human = (Human) proxyFactory.getInstance();
        human.getBelief();
        human.eat("四川麻辣烫");
    }
}
class ProxyFactory{
    Object object;

    public ProxyFactory(Object object) {
        this.object = object;
    }

    public Object getInstance(){
        return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return method.invoke(object, args);
            }
        });
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值