谈谈Java反射:从入门到实践,再到原理

简介: 谈谈Java反射:从入门到实践,再到原理


前言

反射是Java底层框架的灵魂技术,学习反射非常有必要,本文将从入门概念,到实践,再到原理讲解反射,希望对大家有帮助。

反射理解

官方解析

Oracle 官方对反射的解释是:

Reflection is commonly used by programs which require the ability to examine or
modify the runtime behavior of applications running in the Java virtual machine.
This is a relatively advanced feature and should be used only by developers who
have a strong grasp of the fundamentals of the language. With that caveat in
mind, reflection is a powerful technique and can enable applications to perform
operations which would otherwise be impossible.
复制代码

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

白话理解

正射

万物有阴必有阳,有正必有反。既然有反射,就必有“正射”。

那么正射是什么呢?

我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。

Student student = new Student();
student.doHomework("数学");
复制代码

反射

反射就是,一开始并不知道我们要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。

Class clazz = Class.forName("reflection.Student");
 Method method = clazz.getMethod("doHomework", String.class);
 Constructor constructor = clazz.getConstructor();
 Object object = constructor.newInstance();
 method.invoke(object, "语文");
复制代码

正射与反射对比

以上两段代码,执行效果是一样的,如图

但是,其实现的过程还是有很大的差别的:

  • 第一段代码在未运行前就已经知道了要运行的类是Student
  • 第二段代码则是到整个程序运行的时候,从字符串reflection.Student,才知道要操作的类是Student

结论

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

Class 对象理解

要理解Class对象,我们先来了解一下RTTI吧。 RTTI(Run-Time Type Identification)运行时类型识别,其作用是在运行时识别一个对象的类型和类的信息。

Java是如何让我们在运行时识别对象和类的信息的?主要有两种方式: 一种是传统的RRTI,它假定我们在编译期已知道了所有类型。 另一种是反射机制,它允许我们在运行时发现和使用类的信息。

每个类都有一个Class对象,每当编译一个新类就产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。比如创建一个Student类,那么,JVM就会创建一个Student对应Class类的Class对象,该Class对象保存了Student类相关的类型信息。

Class类的对象作用是运行时提供或获得某个对象的类型信息

反射的基本使用

获取 Class 类对象

获取反射中的Class对象有三种方法。

第一种,使用 Class.forName 静态方法。

Class class1 = Class.forName("reflection.TestReflection");
复制代码

第二种,使用类的.class 方法

Class class2 = TestReflection.class;
复制代码

第三种,使用实例对象的 getClass() 方法。

TestReflection testReflection = new TestReflection();
Class class3 = testReflection.getClass();
复制代码

反射创造对象,获取方法,成员变量,构造器

本小节学习反射的基本API用法,如获取方法,成员变量等。

反射创造对象

通过反射创建类对象主要有两种方式:

实例代码:

//方式一
Class class1 = Class.forName("reflection.Student");
Student student = (Student) class1.newInstance();
System.out.println(student);
//方式二
Constructor constructor = class1.getConstructor();
Student student1 = (Student) constructor.newInstance();
System.out.println(student1);
复制代码

运行结果:

反射获取类的构造器

看一个例子吧:

Class class1 = Class.forName("reflection.Student");
Constructor[] constructors = class1.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
    System.out.println(constructors[i]);
 }
复制代码

反射获取类的成员变量

看demo:

// student 一个私有属性age,一个公有属性email
public class Student {
    private Integer age;
    public String email;
}
public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class class1 = Class.forName("reflection.Student");
        Field email = class1.getField("email");
        System.out.println(email);
        Field age = class1.getField("age");
        System.out.println(age);
    }
}
复制代码

运行结果:

getField(String name) 根据参数变量名,返回一个具体的具有public属性的成员变量,如果该变量不是public属性,则报异常。

反射获取类的方法

demo

public class Student {
    private void testPrivateMethod() {
        
    }
    public void testPublicMethod() {
        
    }
}
public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class class1 = Class.forName("reflection.Student");
        Method[] methods = class1.getMethods();
        for (int i = 0; i < methods.length; i++) {
            System.out.println(methods[i]);
        }
    }
}
复制代码

运行结果:

反射的实现原理

通过上一小节学习,我们已经知道反射的基本API用法了。接下来,跟着一个例子,学习反射方法的执行链路。

public class TestReflection {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("reflection.TestReflection");
        Method method = clazz.getMethod("target", String.class);
        method.invoke(null, "666");
    }
    public static void target(String str) {
        //打印堆栈信息
        new Exception("#" +str).printStackTrace();
        System.out.println("invoke target method");
    }
}
复制代码

堆栈信息反映出反射调用链路:

java.lang.Exception: #666
invoke target method
  at reflection.TestReflection.target(TestReflection.java:17)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:498)
  at reflection.TestReflection.main(TestReflection.java:11)
复制代码

invoke方法执行时序图

我们跟着反射链路去看一下源码,先看Method的invoke方法:

public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
       InvocationTargetException
{
    //校验权限
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor(); //获取MethodAccessor
    }
    //返回MethodAccessor.invoke
    return ma.invoke(obj, args);
}
复制代码

由上可知道,Method 的 invoke 方法,其实是返回接口MethodAccessor的invoke方法。MethodAccessor接口有三个实现类,到底调用的是哪个类的 invoke 方法呢?

进入acquireMethodAccessor方法,可以看到MethodAccessor由ReflectionFactory 的 newMethodAccessor方法决定。

再进ReflectionFactory的newMethodAccessor方法,我们可以看到返回的是DelegatingMethodAccessorImpl对象,也就是说调用的是它的invoke方法。

再看DelegatingMethodAccessorImpl的invoke方法

DelegatingMethodAccessorImpl的invoke方法返回的是MethodAccessorImpl的invoke方法,而MethodAccessorImpl的invoke方法,由它的子类NativeMethodAccessorImpl重写,这时候返回的是本地方法invoke0,如下

因此,Method的invoke方法,是由本地方法invoke0决定的,再底层就是c++相关了,有兴趣的朋友可以继续往下研究。

反射的一些应用以及问题

反射应用

反射是Java框架的灵魂技术,很多框架都使用了反射技术,如spring,Mybatis,Hibernate等。

JDBC 的数据库的连接

在JDBC连接数据库中,一般包括加载驱动,获得数据库连接等步骤。而加载驱动,就是引入相关Jar包后,通过Class.forName() 即反射技术,加载数据库的驱动程序。

Spring 框架的使用

Spring 通过 XML 配置模式装载 Bean,也是反射的一个典型例子。

装载过程:

  • 将程序内XML 配置文件加载入内存中
  • Java类解析xml里面的内容,得到相关字节码信息
  • 使用反射机制,得到Class实例
  • 动态配置实例的属性,使用

这样做当然是有好处的:

不用每次都去new实例了,并且可以修改配置文件,比较灵活。

反射存在的问题

性能问题

java反射的性能并不好,原因主要是编译器没法对反射相关的代码做优化。 有兴趣的朋友,可以看一下这个文章java-reflection-why-is-it-so-slow

安全问题

我们知道单例模式的设计过程中,会强调将构造器设计为私有,因为这样可以防止从外部构造对象。但是反射可以获取类中的域、方法、构造器,修改访问权限。所以这样并不一定是安全的。

看个例子吧,通过反射使用私有构造器实例化。

public class Student {
    private String name;
    private Student(String name) {
        System.out.println("我是私有构造器,我被实例化了");
        this.name = name;
    }
    public void doHomework(String subject) {
        System.out.println("我的名字是" + name);
        System.out.println("我在做"+subject+"作业");
    }
}
public class TestReflection {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("reflection.Student");
        // 获取私有构造方法对象
        Constructor constructor = clazz.getDeclaredConstructor(String.class);
        // true指示反射的对象在使用时应该取消Java语言访问检查。
        constructor.setAccessible(true);
        Student student = (Student) constructor.newInstance("jay@huaxiao");
        student.doHomework("数学");
    }
}
复制代码

运行结果:

显然,反射不管你是不是私有,一样可以调用。 所以,使用反射通常需要程序的运行没有安全限制。如果一个程序对安全性有强制要求,最好不要使用反射啦。

参考与感谢




目录
相关文章
|
5天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
27天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
27天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
29天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
7天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
26天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
23天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
27天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
36 3
|
26天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
29天前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。