关于反射的杂谈

一.什么是反射机制

  反射使用的前提条件:需要得到字节码的Class类的实例对象,字节码是代码编译后生成的.class文件,Class类用于表示.class文件(字节码);

  Class类的实例对象表示正在运行的 Java 应用程序中的类和接口;

注意:枚举是一种类,注解是一种接口;

  反射指程序在运行状态可以动态加载,探知和使用编译期间完全未知的类;对于任意一个已经加载的类,都能够知道这个类的所有属性和方法; 对于任意一个对象,都能调用它的任意一个方法和属性;

  

  反射常用对象:

    • Class
      • Class类的实例表示正在运行的Java应用程序中的类和接口;
    • Constructor
      • 关于类的单个构造方法的信息以及对它的访问权限;    
    • Field
      • Field提供有关类或接口的单个字段的信息,以及对它的动态访问权限;    
    • Method      
      • Method提供关于类或接口上单独某个方法的信息;    

  

  每个类被加载进入内存之后(将.class文件加载进内存),系统就会为该类生成一个对应的java.lang.Class类的实例对象,通过该Class类的实例对象就可以访问到JVM中的这个类;

  

  反射的另一种形象的说法:加载完类之后,在堆内存中会产生一个Class类型的对象(一个类只有一个Class对象),这个对象包含了完整的类的结构信息,而且这个Class对象就像一面镜子,透过这个镜子看到类的结构;

 

  反射跟内省两者很像,但它们的含义不一样:

  1. 内省用于在运行时检测某个对象的类型和其包含的属性;

  2. 反射用于在运行时检测和修改某个对象的结构及其行为;

  从它们的定义可以看出,内省是反射的一个子集。有些语言支持内省,但并不支持反射,如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中,反射更接近于内省,因为你无法改变一个对象的结构。虽然一些API可以用来修改方法和属性的可见性,但并不能修改结构;

 

二.为何需要反射

  在刚开始学习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 类名不一样,用具体类名更换麻烦;

 

三.一些扯谈

  使用反射加载类,就好比应用程序不加载动态库模块,应用的所有模块都是在一个工程的源码中,如果某个模块需要修改,那么整个工程都需要重新编译,而应用使用了动态库加载,则会避免这些问题,当然这是扯的有点远了;

 

参考:

http://ju.outofmemory.cn/entry/63866

https://blog.csdn.net/fengyuzhengfan/article/details/38086743

 

原文地址:https://www.cnblogs.com/coder-zyc/p/10702665.html