关于Java反射
一.什么是反射机制
反射使用的前提条件:需要得到字节码的Class类的实例对象,字节码是代码编译后生成的.class文件,Class类用于表示.class文件(字节码);
Class类的实例对象表示正在运行的 Java 应用程序中的类和接口;
注意:枚举是一种类,注解是一种接口;
反射指程序在运行状态可以动态加载,探知和使用编译期间完全未知的类;对于任意一个已经加载的类,都能够知道这个类的所有属性和方法; 对于任意一个对象,都能调用它的任意一个方法和属性;
反射常用对象:
-
- Class
- Class类的实例表示正在运行的Java应用程序中的类和接口;
- Constructor
- 关于类的单个构造方法的信息以及对它的访问权限;
- Field
- Field提供有关类或接口的单个字段的信息,以及对它的动态访问权限;
- Method
- Method提供关于类或接口上单独某个方法的信息;
- Class
每个类被加载进入内存之后(将.class文件加载进内存),系统就会为该类生成一个对应的java.lang.Class类的实例对象,通过该Class类的实例对象就可以访问到JVM中的这个类;
反射的另一种形象的说法:加载完类之后,在堆内存中会产生一个Class类型的对象(一个类只有一个Class对象),这个对象包含了完整的类的结构信息,而且这个Class对象就像一面镜子,透过这个镜子看到类的结构;
反射跟内省两者很像,但它们的含义不一样:
-
内省用于在运行时检测某个对象的类型和其包含的属性;
-
反射用于在运行时检测和修改某个对象的结构及其行为;
从它们的定义可以看出,内省是反射的一个子集。有些语言支持内省,但并不支持反射,如C++,如下图;
内省示例:instanceof
运算符用于检测某个对象是否属于特定的类
if (obj instanceof Dog) { Dog d = (Dog) obj; d.bark(); }
反射示例:Class.forName()
方法可以通过类或接口的名称(一个字符串或完全限定名)来获取对应的Class
对象,forName
方法会触发类的初始化;
// 使用反射 Class<?> c = Class.forName("classpath.and.classname"); Object dog = c.newInstance(); Method m = c.getDeclaredMethod("bark", new Class<?>[0]); m.invoke(dog);
反射是在运行状态把Java类中的各种成分映射成相应的Java类,可以动态的获取所有的属性以及动态调用任意一个方法,强调的是运行状态;
内省涉及的类
- Introspector
- BeanInfo
- PropertyDescriptor
在Java中,反射更接近于内省,因为你无法改变一个对象的结构;虽然一些API可以用来修改方法和属性的可见性,但并不能修改结构;
内省与反射参考:[https://www.cnblogs.com/linjiqin/archive/2011/02/16/1956266.html]
二.为何需要反射
在刚开始学习JDBC时,写第一行代码是这样的:
Class.forName("com.mysql.jdbc.Driver");
这段代码是用于加载MySQL的JDBC驱动的;或许有人想说可以用另外一种方式加载驱动:
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
通过查看MySQL驱动的源码(5.1版本的)可以看到Driver类的内部实现;
如果使用上面第二种方式加载驱动会导致驱动被注册两次,这会使得程序过度依赖于mysql的api,脱离的mysql的开发包,程序则无法编译;如果使用上面第一种方式加载驱动,驱动只会被加载一次,不需要依赖具体的驱动,灵活性高;
public class Driver extends NonRegisteringDriver implements java.sql.Driver { // // Register ourselves with the DriverManager // static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } /** * Construct a new driver and register it with DriverManager * * @throws SQLException * if a database error occurs. */ public Driver() throws SQLException { // Required for Class.forName().newInstance() } }
而Class.forName(...)是返回与带有给定字符串名的类或接口相关联的 Class 对象,Class.forName(className)实际上是调用Class.forName(className,true, this.getClass().getClassLoader()),第二个参数是指Class被loading后是不是必须被初始化,可以看出,使用Class.forName(className)加载类时则已初始化;
也就是当执行Class.forName(...),JVM会执行该类的静态代码块,静态代码块是和类绑定的,该类装载成功就表示执行了该类的静态代码块;
那么在初始化一个类生成一个实例对象的时候,newInstance()方法和new关键字除了一个是方法,一个是关键字外,最主要有什么区别(参考来源)?
1.它们的区别在于创建对象的方式不一样,前者是使用类加载机制,后者是创建一个新类;
2.那么为什么会有两种创建对象方式?
这主要考虑到软件的可伸缩、可扩展和可重用等软件设计思想;Java中工厂模式经常使用newInstance()方法来创建对象,因此从为什么要使用工厂模式上可以找到具体答案;
class c = Class.forName(“Example”); factory = (ExampleInterface)c.newInstance();
其中ExampleInterface是Example的接口,可以写成如下形式:
String className = "Example"; class c = Class.forName(className); factory = (ExampleInterface)c.newInstance();
进一步可以写成如下形式:
//从配置文件中获得字符串 String className = readfromXMlConfig; class c = Class.forName(className); factory = (ExampleInterface)c.newInstance();
上面代码已经不存在Example的类名称,它的优点是无论Example类怎么变化,上述代码不变,甚至可以更换Example的兄弟类Example2 , Example3 , Example4……,只要他们继承ExampleInterface就可以;但使用newInstance()方法的时候,就必须保证:这个类已经加载;
而完成上面两个步骤的正是Class类的静态方法forName所完成的,forName这个静态方法调用了启动类加载器,即加载 java API的那个加载器; 现在可以看出,newInstance()实际上是把new这个方式分解为两步,即首先调用Class类的加载方法加载某个类,然后实例化;这样分步的好处是显而易见的;在调用Class类的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段;
newInstance与new的区别:
newInstance: 弱类型,低效率,只能调用无参构造;
new: 强类型,相对高效,能调用任何public构造;
现在再看看加载MySQL驱动那行代码,如果使用DriverManager.registerDriver(new com.mysql.jdbc.Driver())来加载,那样的话会向DriverManager加载驱动两次,Class.forName(...)会让JVM查找并加载指定的类,如果在类中有静态初始化器的话,JVM必然会执行该类的静态代码段,所以在这里使用newInstance()跟DriverManager.registerDriver(new com.mysql.jdbc.Driver())
效果是一样的都是加载两次;
换另一方面说,DriverManager.registerDriver(new com.mysql.jdbc.Driver())这种方式的耦合度太高,如果项目中没有导入jar包,就会在编译期间报错;而Class.forName()这种则不会;如果使用第一种方式加载驱动,那么考虑下oracle和mysql 类名不一样,用具体类名更换麻烦;
三.框架中使用反射
框架通过反射使用通过该类的字节码取到该类的信息,也称元数据,框架使用的前提条件:必须先得到代表的字节码的Class,Class类用于表示.class文件(字节码);如果传入的是类,对于框架来说,传入的类是未知的,不能获取到类的信息;
使用反射加载类,就好比应用程序不加载动态库模块,应用的所有模块都是在一个工程的源码中,如果某个模块需要修改,那么整个工程都需要重新编译,而应用使用了动态库加载,则会避免这些问题;
参考:
http://ju.outofmemory.cn/entry/63866