java反射精讲

本文介绍了Java反射机制的原因、定义、作用,展示了如何通过反射创建和访问类对象,强调了反射在框架中的重要性。同时,讨论了反射的优缺点,包括性能影响和安全问题。文章还详细阐述了访问检查、Class对象的获取方式,以及类加载的五个阶段:加载、验证、准备、解析和初始化。最后,提到了类加载器的层次结构和双亲委派模型,确保类加载的一致性和安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Why?为什么要有反射

首先我们通过一个案例来理解:

我定义了一个property文件dog.properties,文件有两个key-value,一个是名为Dog类的引用路径,一个是这个Dog类里面的方法bark。

Dog类如下:

接下来我们写一个测试方法,

public class test1 {
    public static void main(String[] args) throws Exception {
        //读取dog.properties文件内容
        Properties properties = new Properties();
        properties.load(new FileInputStream("src\\dog.properties"));
        String className = properties.get("className").toString();
        String methodName = properties.get("classMethod").toString();
        Object className1 = properties.get("className");
        //把Object类型的对象强转为Dog类型
        Dog a=(Dog)className1;
        System.out.println(a);
    }
}

然而,以上代码看似合情合理,但是会报类型转换错误这个错误:

Exception in thread "main" java.lang.ClassCastException: 
java.lang.String incompatible with test.Dog

这时就需要使用反射来加载文件中的内容了,这也是我们在一些框架中常见的技术。

如果想通过配置文件加载出Dog类对象,可以修改为一下代码:

    public static void main(String[] args) throws Exception{
        Properties properties = new Properties();
        properties.load(new FileInputStream("src\\dog.properties"));
        String className = properties.get("className").toString();
        String methodName = properties.get("classMethod").toString();
//        Object className1 = properties.get("className");

        //2. 创建对象 , 传统的方法,行不通 =》 反射机制
        //new className();

        //加载类
        Class cls = Class.forName(className);
        //创建实例对象
        Object object = cls.newInstance();

        System.out.println("cls:    "+cls);
        System.out.println("object: "+object);
        System.out.println("object.getClass():  "+object.getClass());

        //得到方法对象
        Method method = cls.getMethod(methodName);
        System.out.println("methodName: "+method);
        //执行方法--传统方法:对象.方法(), 反射机制:方法.invoke(对象)
        method.invoke(object);
    }

由此可见,通过Class.forName(className),可以加载出Class对象,Object object = cls.newInstance();

可以创建Dog的实例对象,打印出cls,System.out.println(cls)的结果为:class test.Dog此时可以成功获取Dog类,再通过cls.newInstance()就可以得到Dog的实例对象了。

通过反射也可以获得成员变量和方法等信息。

比如一个springboot的项目中,会将数据库的地址密码写到一个专门的配置文件中,如果我们之间写在代码中的话,后期维护会比较麻烦,就像下面这样:

Class.forName("com.mysql.jdbc.Driver");

//获取与数据库连接的对象-Connetcion
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/gengzai", "root", "root");

而如果将“jdbc:mysql://localhost:3306/gengzai”这种后期维护容易被更改的信息放到配置文件中统一管理,维护起来就很方便。

知道了反射的作用后,我们看一下反射的定义

二、What?什么是反射

反射的定义如下:java程序在运行状态中,对于任意一个类,都能够在运行时知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

三、反射的作用

1.在运行时判断任意一个对象所属的类

2.在运行时构造任意一个类的对象

3.在运行时得到任意一个类所具有的成员变量和方法

4.在运行时调用任意一个对象的成员变量和方法

5.生成动态代理

四、反射常用Api

以Dog类为例:

  1. 获取反射对象的三种方式

Class cls = Dog.class;

Class cls = new Dog().getClass();

Class cls = Class.forName("类的引用路径");

  1. 通过反射得到Dog全部构造器(包括private)

cls.getDeclaredConstructors()

  1. 只得到无参构造器

cls.getConstructor()

  1. 获取指定的public类型的构造器

cls.getConstructor(String.class,Integer.class)

  1. 通过反射得到Dog全部public构造器

cls.getConstructosr()

  1. 通过反射获取指定的任意类型的构造器(可以为private类型)

cls.getDeclaredConstructor(String.class)

  1. 获取本类以及父类或者父接口中所有的公共方法(public修饰符修饰的)

cls.getMethods()

  1. 获取本类中的所有方法,包括私有的(private、protected、默认以及public)的方法

cls.getDeclaredMethods()

  1. 获取本类以及父类或者父接口中指定的方法(public修饰符修饰的)

cls.getMethod("bark")

  1. 获取本类中指定的方法(可以是private修饰)

cls.getDeclaredMethod("bark")

  1. 获取Dog类的所有属性(包括private修饰的私有属性)

cls.getDeclaredFields()

  1. 获取Dog类的指定属性

cls.getDeclaredField("age")

五、反射优缺点

优点: 可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑。

缺点: 使用反射基本是解释执行,对执行速度有影响

性能问题

1.使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此Java反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用。

2.反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被 执行的代码或对性能要求很高的程序中使用反射。

使用反射会模糊程序内部逻辑

程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。

安全限制

使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了

内部暴露

由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

Java反射可以访问和修改私有成员变量,那封装成private还有意义么?

既然小偷可以访问和搬走私有成员家具,那封装成防盗门还有意义么?这是一样的道理,并且Java从应用层给我们提供了安全管理机制——安全管理器,每个Java应用都可以拥有自己的安全管理器,它会在运行阶段检查需要保护的资源的访问权限及其它规定的操作权限,保护系统免受恶意操作攻击,以达到系统的安全策略。所以其实反射在使用时,内部有安全控制,如果安全设置禁止了这些,那么反射机制就无法访问私有成员。

六、访问检查

什么是访问检查?

访问检查,就是查看成员属性、成员方法的使用是否符合访问权限

举个例子,一个类中,被private修饰的属性只能本类中使用,是不能被外界访问的,如下

有一个Cat类,类中有一个私有属性age。

接下来通过反射获取age的数值:

结果会报错:illegalAccessException

如果想要暴力获取私有属性的值,可以通过age.setAccessible(true)屏蔽Java语言的(运行时)访问检查:

  public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        Class<Cat> catClass = Cat.class;
        Cat cat = catClass.newInstance();
        Field age = catClass.getDeclaredField("age");
        age.setAccessible(true);
        age.set(cat,1);
        Object o = age.get(cat);
        System.out.println(o);
    }

七、Class类介绍

1.Class也是类,因此也继承Object类

  1. Class类对象不是new出来的,而是系统创建的

通过ClassLoader 类:

public Class<?> loadClass(String name) throws ClassNotFoundException {

return loadClass(name, false);

}

2.对于某个类的Class类对象,在内存中只有一份,因为类只加载一次

3.每个类的实例都会记得自己是由哪个 Class 实例所生成

4.通过Class对象可以完整地得到一个类的完整结构

5.Class对象是存放在堆的

6.类的字节码二进制数据,是放在方法区的, 有的地方称为类的元数据(包括 方法代码

变量名,方法名,访问权限等等)

//1 . 获取到 Car 类 对应的 Class 对象
Class<?> cls = Class.forName(classAllPath);
cls.getPackage().getName());
//2. 得到全类名
cls.getName();
//3. 通过 cls 创建对象实例
Car car = (Car) cls.newInstance();
//4. 通过反射获取属性 brand
Field brand = cls.getField("brand");
System.out.println(brand.get(car));
//5. 通过反射给属性赋值
brand.set(car, "奔驰");

八、获得Class对象的多种方式

1、哪些类型有 Class 对象

  1. 不同类型获取Class对象的方式

1. Class.forName

String classAllPath = "com.hspedu.Car"; //通过读取配置文件获取

Class<?> cls1 = Class.forName(classAllPath);

2. 类名.class , 应用场景: 用于参数传递

Class cls2 = Car.class;

3. 对象.getClass(), 应用场景,有对象实例

Car car = new Car();

Class cls3 = car.getClass();

4.通过类加载器来获取到类的 Class 对象

(1)先得到类加载器

ClassLoader classLoader = car.getClass().getClassLoader();

(2)通过类加载器得到 Class 对象

Class cls4 = classLoader.loadClass(classAllPath);

5.基本数据(int, char,boolean,float,double,byte,long,short) 按如下方式得到 Class 类对象

Class<Integer> integerClass = int.class;

Class<Character> characterClass = char.class;

Class<Boolean> booleanClass = boolean.class;

6.基本数据类型对应的包装类,可以通过 .TYPE 得到 Class 类对象

Class<Integer> type1 = Integer.TYPE;

Class<Character> type2 = Character.TYPE;

7.

Class<String> cls1 = String.class;//外部类

Class<Serializable> cls2 = Serializable.class;//接口

Class<Integer[]> cls3 = Integer[].class;//数组

Class<float[][]> cls4 = float[][].class;//二维数组

Class<Deprecated> cls5 = Deprecated.class;//注解

//枚举

Class<Thread.State> cls6 = Thread.State.class;

九、类加载

  1. 什么是类加载

在Java的世界里,每一个类或者接口,在经历编译器后,都会生成一个个.class文件。

类加载机制指的是将这些.class文件中的二进制数据读入到内存中,并对数据进行校验,解析和初始化。最终,每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。

类的生命周期,经历7个阶段,分别是加载、验证、准备、解析、初始化、使用、卸载。

除了使用卸载两个过程,前面的5个阶段 加载、验证、准备、解析、初始化 的执行过程就是类的加载过程。

  1. 什么时候会进行类加载

对于什么时候加载,Java虚拟机规范中并没有约束,各个虚拟机都可以按自身需要来自由实现。但绝大多数情况下,都遵循“什么时候初始化”来进行加载。

什么时候初始化?Java虚拟机规范有明确规定,当符合以下条件时(包括但不限于),虚拟机内存中没有找到对应类型信息,则必须对类进行“初始化”操作:

(1)使用new实例化对象时、读取或者设置一个类的静态字段或方法时

(2)反射调用时,例如 Class.forName("com.xxx.MyTest")

(3)初始化一个类的子类,会首先初始化子类的父类

(4)Java虚拟机启动时标明的启动类

(5)JDK8 之后,接口中存在default方法,这个接口的实现类初始化时,接口会其之前进行初始化

初始化阶段开始之前,自然还是要先经历 加载、验证、准备 、解析的。

类的加载过程分 5 个阶段,其中 验证、准备、解析 可以归纳为 “连接” 阶段。

需要注意的是,这5个阶段,并不是严格意义上的按顺序完成,在类加载的过程中,这些阶段会互相混合,交叉运行,最终完成类的加载和初始化。

例如在加载阶段,需要使用验证的能力去校验字节码正确性。在解析阶段,也要使用验证的能力去校验符号引用的正确性。或者加载阶段生成Class对象的时候,需要解析阶段符号引用转直接引用的能力等等......

接下来分别看看着5个阶段分别发生了什么?

加载

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

通过一个类的全限定名去找到其对应的.class文件

将这个.class文件内的二进制数据读取出来,转化成方法区的运行时数据结构

在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

Java虚拟机并没有规定类的字节流必从.class文件中加载,在加载阶段,程序员可以通过自定义的类加载器,自行定义读取的地方,例如通过网络、数据库等。

验证

Class文件中的内容是字节码,这些内容可以由任何途径产出,验证阶段的目的是保证文件内容里的字节流符合Java虚拟机规范,且这些内容信息运行后不会危害虚拟机自身的安全。

验证阶段会完成以下校验:

文件格式验证:验证字节流是否符合Class文件格式的规范。例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型 ...... 等等

元数据验证:对字节码描述的元数据信息进行语义分析,要符合Java语言规范。例如:是否继承了不允许被继承的类(例如final修饰过的)、类中的字段、方法是否和父类产生矛盾 ...... 等等

字节码验证:对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的。

符号引用验证:发生在解析阶段,符号引用转为直接引用的时候,例如:确保符号引用的全限定名能找到对应的类、符号引用中的类、字段、方法允许被当前类所访问 ...... 等等

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证阶段不是必须的,虽然这个阶段非常重要。Java虚拟机允许程序员主动取消这个阶段,用来缩短类加载的时间,可以根据自身需求,使用 -Xverify:none参数来关闭大部分的类验证措施。

准备

这个阶段,类的静态字段信息(即使用 static 修饰过的变量)会得到内存分配,并且设置为初始值。

对于该阶段有以下几个知识点需要注意:

1、内存分配仅包括 static 修饰过的变量,而不包括实例变量,实例变量得等到对象实例化时分配内存。

2、初始值指的是变量数据类型的默认值,而不是被在Java代码中被显式地赋予的值。但是,当字段信息被 final 修饰成常量(ConstantValue)时,这个初始值就是Java代码中显式地赋予的值。

例如:public static int value = 3
类变量 value 在准备阶段设置的初始值 是 0,不是 3。把value赋值为3的 putstatic 指令是在程序编译后,存放于类构造器 <clinit>() 方法中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。
当使用 final 修饰后:public static final int value = 3
类变量 value 在准备阶段设置的初始值 是 3,不是 0。

3、在JDK8取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在Java堆中的。

解析

这个阶段,虚拟机会把这个Class文件中,常量池内的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。我们可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行“连接“的过程

什么是符号引用?
Java代码在编译期间,是不知道最终引用的类型,具体指向内存中哪个位置的,这时候会用一个符号引用,来表示具体引用的目标是"谁"。Java虚拟机规范中明确定义了符号引用的形式,符合这个规范的前提下,符号引用可以是任意值,只要能通过这个值能定位到目标。

什么是直接引用?
直接引用就是可以直接或间接指向目标内存位置的指针或句柄。

引用的类型,还未加载初始化怎么办?
当出现这种情况,会触发这个引用对应类型的加载和初始化。

初始化

这是类加载的最后一个步骤,初始化的过程,就是执行类构造器 <clinit>()方法的过程。

当初始化完成之后,类中static修饰的变量会赋予程序员实际定义的“值”,同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

<clinit>() 方法的作用是什么?

还记得么?在准备阶段,已经对类中static修饰的变量赋予了初始值。<clinit>() 方法的作用,就是给这些变量赋予程序员实际定义的“值”。同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

<clinit>() 方法是什么?

<clinit>() 方法 和 <init> 方法是不同的,它们一个是“类构造器”,一个是实例构造器。
Java虚拟机会保证子类<clinit>() 方法在执行前,父类的 <clinit>() 已经执行完毕。而 <init> 方法则需要显性的调用父类的构造器。
<clinit>() 方法由编译器自动生成,但不是必须生成的,只有这个类存在static修饰的变量,或者类中存在静态代码块但时候,才会自动生成<clinit>()方法。

加载过程总结

当一个符合Java虚拟机规范的字节流文件,经历 加载、验证、准备、解析、初始化这些阶段相互协作执行完成之后,加载阶段读取到的Class字节流信息,会按虚拟机规定的格式,在方法区保存一份,然后Java 堆中,会创建一个 java.lang.Class 类的对象,这个对象描述了这个类所有信息,也提供了这个类在方法区的访问入口。

方法区中,使用同一加载器的情况下,每个类只会有一份Class字节流信息
Java堆中,使用同一加载器的情况下,每个类只会有一份 java.lang.Class 类的对象

三层类加载器介绍

启动类加载器(Bootstrap Class Loader):负责加载<JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 参数制定的路径,例如 jre/lib/rt.jar 里所有的class文件。由C++实现,不是ClassLoader子类。

拓展类加载器(Extension Class Loader):负责加载Java平台中扩展功能的一些jar包,包括<JAVA_HOME>\lib\ext 目录中 或 java.ext.dirs 指定目录下的jar包。由Java代码实现。

应用程序类加载器(Application Class Loader):我们自己开发的应用程序,就是由它进行加载的,负责加载ClassPath路径下所有jar包。

双亲委派模型

高端的食材往往只需要最简单的烹饪方式,而保证Java程序稳定运行的双亲委派模式,其实也非常简单:

双亲委派模式其实一句话就可以说清楚:任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载。

ClassLoader类示例:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    // 首先要保证线程安全
    synchronized (getClassLoadingLock(name)) {
        // 先判断这个类是否被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 有父类,优先交给父类尝试加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载失败,这里捕获异常,但不需要做任何处理
            }

            if (c == null) {
                // 没有父类,或者父类无法加载,尝试自己加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

为什么要使用双亲委派机制:

不同的类加载器,加载同一个类,结果是虚拟机里会存在两份这个类的信息,所以当判断这两个类是“相等”时,必定是不相等的。

使用双亲委派模式,可以保证,每一个类只会有一个类加载器。例如Java最基础的Object类,它存放在 rt.jar 之中,这是 Bootstrap 的职责范围,当向上委派到 Bootstrap 时就会被加载。

但如果没有使用双亲委派模式,可以任由自定义加载器进行加载的话,Java这些核心类的API就会被随意篡改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值