推了好长时间,总结一下这个反射机制关于底层代码方面的知识点,所以参考了几篇文章总结一波
参考:https://www.jianshu.com/p/3ea4a6b57f87
参考:https://www.cnblogs.com/whgk/p/6122036.html
一、什么是反射
-
在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。
- 想要使用反射机制,就必须要*先获取到该类的字节码文件对象(.class),通过字节码文件对象,就能够通过该类中的方法获取到我们想要的所有信息(方法,属性,类名,父类名,实现的所有接口等等),每一个类对应着一个字节码文件也就对应着一个Class类型的对象,也就是字节码文件对象。
附:Class对象
要正确使用Java反射机制就得使用java.lang.Class这个类。它是Java反射机制的起源。当一个类被加载以后,Java虚拟机就会自动产生一个Class对象。通过这个Class对象我们就能获得加载到虚拟机当中这个Class对象对应的方法、成员以及构造方法的声明和定义等信息。
即当一个类被加载以后,Java虚拟机就会自动产生一个Class对象,同这个Class对象实现反射进行相关的业务实现。
附:获取字节码文件对象(类对象)的三种方式(区别!!重点)
以下三种获取Class对象的方式
-
new Object().getClass
-
Object.class
-
Class.forName(“java.util.String”)
实例场景一
public class Person {
static {
System.out.println("Person:静态代码块"); //静态代码块
}
{
System.out.println("Person:动态代码块"); //动态代码块
}
public Person(){
System.out.println("Person:构造方法"); //构造方法
}
}
测试场景
public class GetClassTest {
@Test
public void test1(){
Class<?> clz = Person.class;
}
@Test
public void test2() throws ClassNotFoundException {
Class<?> clz = Class.forName("com.choupangxia.reflect.Person");
}
@Test
public void test3() {
Class<?> clz = new Person().getClass();
}
}
分别执行三个单元测试发现,第一个单元测试没打印任何内容;第二个单元测试打印了“静态方法”中的内容;第三个单元测试打印出了全部内容:
Person:静态代码块
Person:动态代码块
Person:构造方法
也就是说通过Person.class的方法获取对象的Class对象,根本不会调用对象中任何的代码块或代码。而Class.forName()会调用静态代码块的内容。
而第三种方式打印所有内容的原因很显然,就因为要先实例化对象。
实例场景二
@Test
public void test4() throws ClassNotFoundException {
Class<?> clz = Person.class;
System.out.println("---------------");
clz = Class.forName("com.choupangxia.reflect.Person");
System.out.println("---------------");
clz = new Person().getClass();
}
打印:
---------------
Person:静态代码块
---------------
Person:动态代码块
Person:构造方法
说明Class.forName()方法执行过静态代码块之后,new Person().getClass()就不再会执行同样的静态代码块了。这也证明静态代码块只会被初始化一次即在编译的时候。
二、关于类加载机制方面的解析
我们知道类加载过程分:加载阶段、连接阶段和初始化阶段。
类的加载阶段是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转化为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.class对象,作为方法区数据结构的入口。
类加载阶段的最终产物是堆内存中的class对象,对于同一个Classloader对象,不管某个类被加载多少次,对应堆内存中的class对象始终只有一个。
也就是说无论通过哪种形式来获取Class对象,获得的都是堆内存中对应的Class对象。
总结
(1)类名.class:JVM将使用类装载器,将类装入内存(前提是:类还没有装入内存),不做类的初始化工作,返回Class的对象。当类被加载成.class文件时,此时Person类变成了.class,在获取该字节码文件对象,也就是获取自己, 该类处于字节码阶段。
(2)Class.forName(“类名字符串”):装入类,并做类的静态初始化,返回Class的对象。直接获取到一个类的字节码文件对象,此时该类还是源文件阶段,并没有变为字节码文件。
(3)实例对象.getClass():对类进行静态初始化、非静态初始化;返回引用运行时真正所指的对象(子对象的引用会赋给父对象的引用变量中)所属的类的Class的对象。通过类的实例获取该类的字节码文件对象,该类处于创建对象阶段
附:同一个字节码文件(*.class)在一次程序运行中,只会被加载一次,不论通过哪种方式获取的class对象,都是同一个,地址值都一样。
三、Java反射机制的作用
看到前面脑海里大概会有一点关于其作用的概念了,但是还是进行一个官方性总结吧
在编译时根本无法知道该对象或类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息。
通过反射可以使程序代码访问装载到JVM 中的类的内部信息
- 获取已装载类的成员变量信息
- 获取已装载类的方法
- 获取已装载类的构造方法信息
常用方法:
getName()获得类的完整名字
getPackage()获取此类所属的包
getSuperclass()获得此类的父类对应的Class对象
getField(String name)获得类的指定属性
getMethods()获得类的public类型的方法
getMethod (String name,Class [] args)获得类的指定方法
每个Method对象对应一个方法,获得Method对象后,可以调用其invoke() 来调用对应方法
Object invoke(Object obj,Object [] args):obj代表当前方法所属的对象的名字,args代表当前方法的参数列表,返回值Object是当前方法的返回值,即执行当前方法的结果。
四、为什么要用反射机制
附:首先要搞清楚为什么要用反射机制?直接创建对象不就可以了吗,这就涉及到了动态与静态的概念
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
然后套用个不用反射的例子吧
interface fruit{
public abstract void eat();
}
class Apple implements fruit{
public void eat(){
System.out.println("Apple");
}
}
class Orange implements fruit{
public void eat(){
System.out.println("Orange");
}
}
// 构造工厂类
// 也就是说以后如果我们在添加其他的实例的时候只需要修改工厂类就行了
class Factory{
public static fruit getInstance(String fruitName){
fruit f=null;
if("Apple".equals(fruitName)){
f=new Apple();
}
if("Orange".equals(fruitName)){
f=new Orange();
}
return f;
}
}
class hello{
public static void main(String[] a){
fruit f=Factory.getInstance("Orange");
f.eat();
}
}
而反射无疑是一种聪明的办法,看代码。
interface fruit{
public abstract void eat();
}
class Apple implements fruit{
public void eat(){
System.out.println("Apple");
}
}
class Orange implements fruit{
public void eat(){
System.out.println("Orange");
}
}
class Factory{
public static fruit getInstance(String ClassName){
fruit f=null;
try{
f=(fruit)Class.forName(ClassName).newInstance();
}catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
class hello{
public static void main(String[] a){
fruit f=Factory.getInstance("Reflect.Apple");
if(f!=null){
f.eat();
}
}
}
五、反射机制的优点和缺点
优点:
附:与new不同,Java反射可以通过setAccessible()方法来访问其中的私有属性。
- 首先,反射机制极大的提高了程序的灵活性和扩展性,降低模块的耦合性,提高自身的适应能力。
- 其次,通过反射机制可以让程序创建和控制任何类的对象,无需提前硬编码目标类。
- 再次,使用反射机制能够在运行时构造一个类的对象、判断一个类所具有的成员变量和方法、调用一个对象的方法。
- 最后,反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中。
java的反射机制就是增加程序的灵活性,避免将程序写死到代码里。
例如: 实例化一个 person()对象, 不使用反射, new person(); 如果想变成 实例化 其他类, 那么必须修改源代码,并重新编译。
使用反射: class.forName("com.xxx.Person").newInstance(); 而且这个类描述可以写到配置文件中,如 **.xml, 这样如果想实例化其他类,只要修改配置文件的"类描述"就可以了,不需要重新修改代码并编译。
缺点:(疑)
- 性能问题
Java反射机制中包含了一些动态类型,所以Java虚拟机不能够对这些动态代码进行优化。因此,反射操作的效率要比正常操作效率低很多。我们应该避免在对性能要求很高的程序或经常被执行的代码中使用反射。而且,如何使用反射决定了性能的高低。如果它作为程序中较少运行的部分,性能将不会成为一个问题。
- 安全限制。
使用反射通常需要程序的运行没有安全方面的限制。如果一个程序对安全性提出要求,则最好不要使用反射。
- 序健壮性。
反射允许代码执行一些通常不被允许的操作,所以使用反射有可能会导致意想不到的后果。反射代码破坏了Java程序结构的抽象性,所以当程序运行的平台发生变化的时候,由于抽象的逻辑结构不能被识别,代码产生的效果与之前会产生差异。
六、Java反射和new效率对比
通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低。
1)首先第一点,一般我们的Java代码是需要编译后在虚拟机里面运行的。
首先我们一般都是通过一个前端编辑器,比如javac,把java文件转为class文件。
接下来,程序运行期间,可能会通过一个JIT,即时编译器将字节码文件转换为计算机认识的机器码文件。
另外一种可能是通过一个AOT编译器,直接把java文件编译为本地机器码文件。其中JIT在程序运行期会对程序进行优化,但是反射是通过动态解析的方式,因此可能无法执行某些java虚拟机的优化。
总结起来有下面几个原因:
1.Method的invoke 方法会对参数做封装和解封操作
需要检查方法可见
需要校验参数
反射方法难以内联
JIT 无法优化