1、从JVM构建实例开始
JVM构建实例的步骤
比如我们写了一段代码
Object o=new Object();
那么它就会首先从.java --> .class 文件
然后,由类加载器加载.class字节码文件到JVM内存,在方法区中创建一个类型对象,它对于每个类而言是唯一的。
之后才会执行new,跑到堆去给它创建一个实例对象。
之后再new的话,由于方法区的类型对象已经存在了,就是已经加载过该类了,就直接在堆创建就行。
![img](https://i-blog.csdnimg.cn/blog_migrate/48a21a52543f66f9c5ce73dbe657a341.png)
2、类加载器
这里也是一个大坑,以后详细讲
主要的原理如下所示:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 查看是否已经加载过该类,加载过的类会有缓存,是使用native方法实现的
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//父类不为空则先让父类加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//父类是null就是BootstrapClassLoader,使用启动类类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类类加载器不能加载该类
}
//如果父类未加载该类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//让当前类加载器加载
c = findClass(name);
}
}
return c;
}
}
简单来说分为以下步骤
- 看看类是不是已经加载了。已经加载就别再加载了
- 遵循双亲委派机制,加载.class文件。最顶层就是启动类类加载器
- 如果父类加载不成功,就自己加载
注意,里面出现了findClass类,这种方法在自己加载时出现,可以确定是具体的原理类
它的实现一般如下:
protected Class<?> findClass(String name) throws ClassNotFoundException{
//子类重写
}
因此,更加具体一点的实现步骤就是这样子的了
![img](https://i-blog.csdnimg.cn/blog_migrate/1d643fcf6ea269103c74a45be92152e4.png)
3、Class类
上述折腾之后,.class文件就被加载进了JVM之中。
并且。。。还在方法区中创建一个类对象
类对象保存了本质上就是对从.java -->.class -->类对象 这些文件里面蕴含的信息做了一个保存
因此它里面像类的权限修饰符,接口,注解信息应有尽有
当然,最重要的信息是字段(属性),方法,构造器
所以说,Class类就是操作这个类对象的一个类
它具有的功能当然包括找找看字段,方法,构造器……
然后它的方法就是我们常见的操作了:
![img](https://i-blog.csdnimg.cn/blog_migrate/56b32ee360e0be03e749427fd1e4ea24.png)
比如说 Class.forName(xxx)
就是调用一个类加载器,来把类加载出来
还有一个常用的,newInstance
方法,本质上就是调用自己的构造方法
![img](https://i-blog.csdnimg.cn/blog_migrate/a9ac259a649c5b9c856039a4b0b01b9b.png)
4、应用的场景
由上我们就可以看出JVM执行反射的基本原理。
Object o=new Object();
但是呢,上面的这行代码,程序对象是自己new的,程序相当于写死了给jvm去跑。
假如一个服务器上突然遇到某个请求要用到某个类,但没有加载。是不是要停下来自己写段代码,new一下,启动一下服务器,(脑残)?!
反射是什么呢?当我们的程序在运行时,需要动态的加载一些类这些类可能之前用不到所以不用加载到jvm,而是在运行时根据需要才加载,这样的好处对于服务器来说不言而喻。比如我们在JDBC里面使用Mysql,就是动态找到了那个类。基本的原理也很简单,就是在底层重新执行一遍类加载的过程,这样就OK了。
5、小结
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法。所以先要获取到每一个字节码文件对应的Class类型的对象。
Tip:反射可以加载私有字段/方法/构造器
当类中字段定义为私有的时候我们能调用?不能!当方法是私有的时候我们能获取吗?不能!当构造方法是私有的时候我们能获取吗?不能!
但是反射可以,比如源码中有你需要用到的方法,但是那个方法是私有的,这个时候你就可以通过反射去执行这个私有方法,同理也可以获取私有变量或构造方法。
反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件在运行时加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
举一个例子,在运用 Struts 2 框架的开发中我们一般会在 struts.xml
里去配置 Action
,比如:
<action name="login"
class="org.ScZyhSoft.test.action.SimpleLoginAction"
method="execute">
<result>/shop/shop-index.jsp</result>
<result name="error">login.jsp</result>
</action>
配置文件与 Action
建立了一种映射关系,当 View 层发出请求时,请求会被 StrutsPrepareAndExecuteFilter
拦截,然后 StrutsPrepareAndExecuteFilter
会去动态地创建 Action 实例。比如我们请求 login.action
,那么 StrutsPrepareAndExecuteFilter
就会去解析struts.xml文件,检索action中name为login的Action,并根据class属性创建SimpleLoginAction实例,并用invoke方法来调用execute方法,这个过程离不开反射。
对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。
6、反射的基本应用
- 获得Class
Class.forName(str);
str.getClass();
- 生成对象
通过反射来生成对象主要有两种方式。
- 使用Class对象的newInstance()方法来创建Class对象对应类的实例。
Class<?> c = String.class;
Object str = c.newInstance();
- 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。
//获取String所对应的Class对象
Class<?> c = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
Object obj = constructor.newInstance("23333");
System.out.println(obj);
- 获得变量,方法和构造器
变量 getDeclaredField
方法getDeclaredMethods
构造器 getDeclaredConstructorss
- 执行方法
public class test1 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}
关于invoke方法,可以参考:
优缺点:
反射的优点:
- 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
- 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
- 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
反射的缺点:
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
- 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
- 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
- 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
参考资料: