文章目录
什么是反射
反射就是在程序运行期间,动态的获取一个类的Class对象,然后通过Class对象来解析类的结构,比如说得到这个类有什么方法,有什么属性。得到这些方法或属性后,可以调用指定对象上的这个方法,也可以给指定对象上的这个属性赋值。
一个类都有什么东西:成员变量、方法、构造方法、注解等信息,利用反射技术把这些信息映射成一个对象,这正体现了在java中“万物皆对象”的理念,连java类都被作为了一个对象。
在Java中用来表示运行时类型信息的对应类就是Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中
public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
private static final int ANNOTATION= 0x00002000;
private static final int ENUM = 0x00004000;
private static final int SYNTHETIC = 0x00001000;
private static native void registerNatives();
static {
registerNatives();
}
/*
* Private constructor. Only the Java Virtual Machine creates Class objects.(私有构造,只能由JVM创建该类)
* This constructor is not used and prevents the default constructor being
* generated.
*/
private Class(ClassLoader loader) {
// Initialize final field for classLoader. The initialization value of non-null
// prevents future JIT optimizations from assuming this final field is null.
classLoader = loader;
}
反射能干什么
一直以来反射技术都是Java中的闪亮点,这也是目前大部分框架(如Spring/Mybatis等)得以实现的支柱。在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。
一般来说反射是用来做框架的,或者说可以做一些抽象度比较高的底层代码,反射在日常的开发中用到的不多,但是咱们还必须搞懂它,因为搞懂了反射以后,可以帮助理解框架的一些底层原理。当你看完下面的示例之后,你就体会到反射的作用了,会让人感觉很舒服,你会体会到在java中你基本上可以为所欲为了。另外一句话说:反射是框架设计的灵魂。
使用场景,比如:
- 需要根据运行时候的配置来动态生成相应的对象,例如通过一个类的全限定类名字符串来生成类的对象
- 根据注解来动态确定程序执行逻辑
- 生成动态代理
- 突破一些sdk的API接口限制(访问一个类的私有字段,私有方法等)
等其他使用场景。
实现反射的几种方式
实现反射有3中方式
class.forName
Class clazz=Class.forName(“com.zejian.Gum”),Class.forName()方法的调用将会返回一个对应类的Class对象,forName方法是Class类的一个static成员方法,记住所有的Class对象都源于这个Class类,因此Class类中定义的方法将适应所有Class对象
实例对象的getClass方法
通过一个实例对象获取一个类的Class对象,其中的getClass()是从顶级类Object继承而来的,它将返回表示该对象的实际类型的Class对象引用
Gum gum = new Gum();
Class clazz2=gum.getClass();
System.out.println("new=clazz2:"+clazz2.getName());
字面常量,类名.class
Class clazz = Gum.class;这种方式相对前面两种方法更加简单,更安全。因为它在编译器就会受到编译器的检查同时由于无需调用forName方法效率也会更高,因为通过字面量的方法获取Class对象的引用不会自动初始化该类,不仅可以应用于普通的类,也可以应用用接口,数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助。
反射的实现
import java.lang.reflect.Constructor;
import java.lang.reflect.*;
/*Class:代表一个字节码文件的对象,每当有类被加载进内存,JVM就会在堆上给
* 该类创建一个代表该类的对象。每个类的Class对象是的。
*Class类没有构造方法,获得类对应的Class方法有3种
*1.:getClass()、2.类、接口.class 、3.Class.forName("类全名");
*比较推荐使用第3种方式,使用前两种方式程序扩展性不好。
*
*Class类中定义了许多关于获取类中信息的方法:
*1.获得该类的构造方法,属性,方法、实例的方法。包含特定情况的获得
*2.获得该类的父类,实现的接口,该类的类加载器,类名、包名等。
*3.判断该类的具体是接口、类、内部类等
*4.方法中加Declared表示可以获得本类定义的任何方法和属性
*
*注意:关于获得到的方法、属性、构造器的具体操作被封装在import java.lang.reflect包里面
*Method:里面最常用的方法invoke(对象,可变参数列表)--调用指定的方法
*Field:get/set;获取和修改属性值
*Constrcutor:使用newInstance(可变参数列表)--调用指定构造方法创建类的实例
*注意:私有的要调用前先去掉访问权限限制setAccssible()
* */
public class ReflectionWithClass {
public static void main(String[] args) throws Exception {
//第一种方式获得Class对象,比较麻烦,要先创建对象,再使用对象调用方法
HelloKitty ht = new HelloKitty();
Class clazz = ht.getClass();
//第二种方式获得Class对象。使用静态的属性创建
Class clazz1 = HelloKitty.class;
//第三种使用Class对象的静态方法获得Class对象
Class clazz2 = Class.forName("HelloKitty");
//获得该类的类加载器
ClassLoader c = clazz2.getClassLoader();
System.out.println(c.toString());
Class clazz3 = String.class;
System.out.println(clazz3.getClassLoader());
//获得该类的实例
Object obj = clazz2.newInstance();
//获得该类的构造器---公开的,getDeclaredConstructors()--可以获得私有的
Constructor[] con = clazz2.getDeclaredConstructors();
for(Constructor cc:con){
System.out.print(cc + " ");
}
//获得类的方法
Method[] mm = clazz2.getDeclaredMethods();
for(Method mmm:mm){
System.out.print(mmm + " ");
}
System.out.println();
//获取特定的方法
Method m = clazz2.getMethod("walk",null);
System.out.println(m.toString());
Field[] f = clazz2.getDeclaredFields();
for(Field ff:f){
System.out.print(ff+ " ");
}
//调用指定的方法---先获取,在调用;注意私有方法先设置访问权限
Method m1 = clazz2.getMethod("walk", null);
System.out.println("hahahhha");
m1.invoke(obj,null);
//调用指定的构造方法创建类实例;先获取在调用
Constructor cc = clazz2.getConstructor(int.class,String.class);
Object o1 = cc.newInstance(12,"blue");
//获取和修改对象的属性值
Field ffs = clazz2.getDeclaredField("age");
ffs.setAccessible(true);
ffs.set(obj, 29);
Object oo = ffs.get(obj);
System.out.println(oo);
}
}
class HelloKitty {
private int age;
public String color = "pink";
public HelloKitty() {}
public HelloKitty(int age) {
this.age = age;
}
public HelloKitty(int age,String color) {
this.age = age;
this.color = color;
System.out.println("okokok");
}
public void walk(){
System.out.println("hhhhhhhhhhhhh");
}
public void talk(int i){
System.out.println(i + "----------" + age);
}
}
参考链接:
https://blog.csdn.net/ju_362204801/article/details/90578678
反射的缺点
优点:
优点就是反射的特性,可以在运行时获得一个编译时还不存在的类的信息,灵活性特别强
缺点:
但反射的使用原则是:能不用则尽量不用,为啥:
- 编译器将无法在编译期间为我们做类型检查的工作,就是说类型错误不能在编译时候被发现了
- 使用反射时候的代码,乱、难写、代码可读性不高,而且还容易出错
- 慢。为什么慢,大家提及最多的是:
3.1 不使用反射时编译器会优化对象实例化的过程,而使用反射则完全不优化,那么实例化对象这块就会有比较大的差距。
3.2 查找检查等操作上的差距
需要考虑反射的慢吗
https://blog.csdn.net/f641385712/article/details/81225239
最终得到的结论就是,不需要考虑反射的慢。
反射大概比直接调用慢50~100倍(JDK7以后对反射有优化,大概在5倍到20倍不等了),但是需要你在执行100万遍的时候才会有所感觉,如果你只是偶尔调用一下反射,请忘记反射带来的性能影响,如果你需要大量调用反射,请考虑缓存
有什么方法可以替代反射吗
由于反射的优点是其动态性,而缺点之一就是有性能损失,所以大家就想怎么保留其动态性而减少性能损失。目前比较流行的一种做法是使用编译时代码生成的方式取代反射!很多库现在都是使用这种方式,例如依赖注入库 dagger2,MyBatis等
反射跟代码生成完全是两种不同的原理,反射是从现有的文件类中获取信息,而代码生成,则是动态的从无到有生成想要的代码。
反射是动态的获取,在运行时根据逻辑获取想要的信息,代码生成是根据逻辑生成想要的代码,两者有异曲同工的意思。
javassit生成代码
Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。
参考链接:https://www.cnblogs.com/rickiyang/p/11336268.html
或者看api文档http://www.javassist.org/html/
BCEL
使用 Apache Byte Code Engineering Library (BCEL)。与 Javassist 所支持的源代码接口不同,BCEL 在实际的 JVM 指令层次上进行操作。在希望对程序执行的每一步进行控制时,底层方法使 BCEL 很有用,但是当两者都可以胜任时,它也使 BCEL 的使用比 Javassist 要复杂得多。
关于BCEL的博客、文档非常少,博主在找资料的时候,以下几个博客还行,但是找现有被使用到的应用场景,却没有,故博主也失去了对他的兴趣。
https://blog.csdn.net/yczz/article/details/14497897
https://blog.csdn.net/iteye_12751/article/details/82550534
官网:https://sourceforge.net/projects/bcel/
ASM
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
具体的ASM实现,请参看以下博主搜到的连接
https://zhuanlan.zhihu.com/p/94498015?utm_source=wechat_timeline
https://blog.csdn.net/zhuoxiuwu/article/details/78619645
https://www.jb51.net/article/103701.htm
三个连接从不同的方面讲述了使用、底层等信息,请自行参考。
其他框架
也有其他框架,博主就不列举了,请有兴趣的同学一起共勉。
应该选哪个呢
那么这几个都是字节码操作框架,应该选哪个呢?—答案是ASM,ASM框架也是最多被使用。
ASM字节码处理框架是用Java开发的而且使用基于访问者模式生成字节码及驱动类到字节码的转换。这允许开发人员避免直接处理方法字节码中的类常量池及偏移,因此为开发人员隐藏了字节码的复杂性并且相对于其他类似工具如BCEL, SERP, or Javassist提供了更好的性能。
比对链接:https://www.jianshu.com/p/db1edeb013ff
总之,记住有其他框架,了解最优的框架即可。
扩展
字面常量不会触发初始化
使用字面常量的方式获取Class对象的引用不会触发类的初始化,先了解一下类加载过程
加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象
链接:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,注意此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用。
初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量。
由此可知,我们获取字面常量的Class引用时,触发的应该是加载阶段,因为在这个阶段Class对象已创建完成,获取其引用并不困难,而无需触发类的最后阶段初始化。下面通过小例子来验证这个过程:
class Initable {
//编译期静态常量
static final int staticFinal = 47;
//非编期静态常量
static final int staticFinal2 =
ClassInitialization.rand.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
//静态成员变量
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
//静态成员变量
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random rand = new Random(47);
public static void main(String[] args) throws Exception {
//字面常量获取方式获取Class对象
Class initable = Initable.class;
System.out.println("After creating Initable ref");
//不触发类初始化
System.out.println(Initable.staticFinal);
//会触发类初始化
System.out.println(Initable.staticFinal2);
//会触发类初始化
System.out.println(Initable2.staticNonFinal);
//forName方法获取Class对象
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
输出结果:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74