Java中的反射|SquirrelNote

96
跳动的松鼠
2017.11.28 10:27* 字数 2793

前言

本篇简介:

  1. 反射概述
  2. 反射具体功能实现
  3. Android中的反射应用

一、反射(Reflection)概述

1.定义
是指在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;并且对于任何一个对象,都能够调用它的任何一个方法和属性;这种动态获取信息以及动态调用对象方法的功能就叫做反射。

例如在下面的入口函数中,就可以看到 HashMapClass 里所有的方法。

import java.lang.reflect.Method;
import java.util.HashMap;
public class HashMapClass extends HashMap {
     /**
      * @param args
      */
     public static void main(String[] args) {
          Method[] methods = HashMapClass.class.getMethods();
          for(Method method:methods){
              System.out.println("method name is:"+method.getName());
          }
     }
}
image.png

静态加载和动态加载
Java初始化一个类的时候可以用new 操作符来初始化,也可通过Class.forName的方式来得到一个Class类型的实例,然后通过这个Class类型的实例的newInstance来初始化.我们把前者叫做JAVA的静态加载,把后者叫做动态加载。有时候我们说某个语言具有很强的动态性,有时候我们会区分动态和静态的不同技术与作法。
静态加载的时候如果在运行环境中找不到要初始化的类,抛出的是NoClassDefFoundError,它在JAVA的异常体系中是一个Error.
动态态加载的时候如果在运行环境中找不到要初始化的类,抛出的是ClassNotFoundException,它在JAVA的异常体系中是一个checked异常,在写代码的时候就需要catch.

2.优点和缺点
优点:运行期类型的判断,动态类加载,动态代理使用反射。
缺点:性能是一个问题,反射相当于一系列解释操作,通知jvm要做的事情,性能比直接的java代码要慢很多。
总结:Java的反射机制在平时的业务开发过程中很少使用到,但是在一些基础框架的搭建上应用非常广泛

3.应用场景
在Java程序中许多对象在运行时都会出现两种类型:编译时类型和运行时类型。
编译时的类型由声明对象时实用的类型来决定,运行时的类型由实际赋值给对象的类型决定
如:
Person p=new Student();
其中编译时类型为Person,运行时类型为Student。
除此之外,程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为Object,但是程序有需要调用该对象的运行时类型的方法。为了解决这些问题,程序需要在运行时发现对象和类的真实信息。然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,此时就必须使用到反射了。

  • 逆向代码 ,例如反编译
  • 与注解相结合的框架 例如Retrofit
  • 单纯的反射机制应用框架 例如EventBus 2.x
  • 动态生成类框架 例如Gson

之前项目里面,jni里面,(C语言要操作java的某个实例用到反射 )要拿到java某个类的实例,用反射把它实例化,c语言去调用java,通过反射去实例化java代码中某个类,然后去调用它的方法

同样,我们在解析xml文件的时候,一个类之所以可以让它显示到界面,在xml里面得到全类名,通过反射把它实例化,因为任何一个类通过实例化才能把它显示。

以合适的方式使用反射,会让我们写代码的方式更加灵活。反射使用不当,反而会适得其反,会对性能造成影响。但是EventBus,Retrofit 的如此火爆,让我们有理由相信,对性能的影响也许没那么大,或者说面对现如今硬件配置堪比电脑的手机,这点影响也许可以忽略不计。

4.Class对象
要想使用反射,首先需要获得待操作的类所对应的Class对象。Java中,无论生成某个类的多少个对象,这些对象都会对应于同一个Class对象。这个Class对象是由JVM生成的,通过它能够获悉整个类的结构。

之前的方法:
Person p=new Person();
在内存中新建一个Person的实例,对象p对这块内存地址进行引用
获取Class对象的三种方式(使用反射机制实现):
(1)使用Class类的静态方法:比如Class.forName("类的全路径");(最常用)

Class clazz=Class.forName("com.it.Person");//forName(包名.类名)
Person p=(Person)clazz.newInstance();
  • 通过JVM查找并加载指定的类(上面的代码指定加载了com.it包中的Person类)
  • 调用newInstance()方法让加载完的类在内存中创建对应的实例,并把实例赋值给p

(2)使用类的.class语法:比如String.class;

Class clazz=Person.Class();
Person p=(Person)clazz.newInstance();
  • 获取指定类型的Class对象,这里是Person
  • 调用newInstance()方法在让Class对象在内存中创建对应的实例,并且让p引用实例的内存地址

(3)使用对象的getClass()方法。

Person p=new Person();
Class clazz=p.getClass();
Person p2=(Person)clazz.newInstance();
  • 在内存中新建一个Person的实例,对象p对这个内存地址进行引用
  • 对象p调用getClass()返回对象p所对应的Class对象
  • 调用newInstance()方法让Class对象在内存中创建对应的实例,并且让p2引用实例的内存地址

注意

  • cls.newInstance()方法返回的是一个泛型T,我们要强转成Person类
  • cls.newInstance()默认返回的是Person类的无参数构造对象
  • 被反射机制加载的类必须有无参数构造方法,否者运行会抛出异常

5.通过类的不带参数的构造方法来生成对象
两种方式:
(1)先获得Class对象,然后通过该Class对象的newInstance()方法直接生成即可:

Class<?> classType = InvokeTester.class;
//用newInstance()方法,生成新的对象
Object invokeTester = classType.newInstance();

(2)先获得Class对象,然后通过该对象获得对应的Constructor对象,再通过该Constructor对象的newsInstance方法生成(其中InvokeTester 是一个自定义的类,有一个无参的构造方法,也有带参数的构造方法):

Class<?> classType = InvokeTester.class;
//获得Constructor对象,此处获取第一个无参数的构造方法的
Constructor cons=classType.getConstructor(new Class[]{});
//通过构造方法来生成一个对象
Object invokeTester = cons.newInstance(new Object[]{});

6.通过类的带参数的构造方法生成对象
带参数的构造方法,传入字符串和整型

Class<?> classType = InvokeTester.class;
//获得Constructor对象,此处获取第一个无参数的构造方法的
Constructor cons=classType.getConstructor(new Class[]{String.class,int.class});
//通过构造方法来生成一个对象
Object invokeTester = cons.newInstance(new Object[]{"zhangsan",22});

可以看出调用构造方法生成对象的方法和调用一般方法的类似,不同的是从Class对象获取Constructor对象时不需要指定名字,而获取Method对象时需要指定名字。

二、反射具体功能实现

clazz内部有哪些方法供我们使用:


image.png

示例一:

import java.lang.reflect.Method;
public class DumpMethods {
     /**
      * 演示Reflection API的基本作用,它读取命令行参数指定的类名,然后打印这个类所具有的方法信息。
      * @param args
      * @throws ClassNotFoundException
      */
     public static void main(String[] args) throws ClassNotFoundException {
          // 获得字符串所标识的class对象,在此处传入字符串指定类名,所以参数获取可以是一个运行期行为,可以用
          //args[0]
          Class<?> forName = Class.forName("java.lang.String");
          //返回class对象所对应的类或接口中所有方法的数组(包括私有方法)
          Method[] methods = forName.getDeclaredMethods();
          //遍历所有方法的声明
          for(Method method:methods){
              System.out.println(method);
          }
     }
}

输出结果:


image.png

示例二:

public class InvokeTester {
     public int add(int params1, int params2) {
          return params1 + params2;
     }
     public String say(String content) {
          return content;
     }
     /**
      * 通过反射调用方法
      * @param args
      * @throws Exception
      */
     public static void main(String[] args) throws Exception {
          // 常规的执行手段
          InvokeTester test = new InvokeTester();
          System.out.println("add方法:" + test.add(1, 2));
          System.out.println("say方法:" + test.say("我是xiaoming!"));
          System.out.println(".........");
          // 通过反射的方式(通过反射调用自身类的方法)
          // 1.获取class对象,前面是通过Class.forName()方法获取,这里使用类名.class(第二种方式)
          Class<?> classType = InvokeTester.class;
          //用newInstance()方法,生成新的对象
          Object invokeTester = classType.newInstance();
          System.out.println(invokeTester instanceof InvokeTester);//true
          //通过反射调用方法,首先要获得与该方法对应的Method对象
          //第一个参数是方法名,第二个参数是这个方法所需要的参数的class对象的数组
          Method addMethod = classType.getMethod("add", new Class[]{int.class,int.class});
          //调用目标方法
          Object result1 = addMethod.invoke(invokeTester, new Object[]{2,3});
          System.out.println(result1);//result1是Integer类型
          
          //调用第二个方法
          Method sayMethod = classType.getMethod("say", new Class[]{String.class});
          Object result2=sayMethod.invoke(invokeTester, new Object[]{"我是Tom!"});
          System.out.println(result2);
     }
}

输出结果:


image.png

示例三:

public class A {
     public void speak(String content){
          System.out.println("Hello,"+content);
     }
     public int add(int params1,int params2){
          return params1+params2;
     }
}
import java.lang.reflect.Method;
public class TestClassLoader {
     /**
      * 反射调用A上的方法
      * @param args
      * @throws Exception
      */
     public static void main(String[] args) throws Exception {
          A a=new A();
          //获取class对象
//        Class<?> clazz = Class.forName("com.it.demo1.A");
          Class<? extends A> clazz = a.getClass();
          //生成新的对象实例
          Object obj = clazz.newInstance();
          Method speakMethod = clazz.getMethod("speak", String.class);
          Method addMethod = clazz.getMethod("add", new Class[]{int.class,int.class});
          speakMethod.invoke(obj, new Object[]{"我是Tom!"});
          speakMethod.invoke(obj, "我是Jim");
          Object result = addMethod.invoke(obj, new Object[]{3,5});
          System.out.println(result);
     }
}

输出结果:


image.png

注意到TestClassLoad类上不会有对类A的符号依赖——也就是说在加载并初始化TestClassLoad类时不需要关心类A的存在与否,而是等到main()方法执行到调用Class.forName()时才试图对类A做动态加载;这里用的是一个参数版的forName(),也就是使用当前方法所在类的ClassLoader来加载,并且初始化新加载的类。

反射的好处
可能有人会有疑问,明明直接new对象就好了,为什么非要用反射呢?代码量不是反而增加了?
其实反射的初衷不是方便你去创建一个对象,而是让你在写代码的时候可以更加灵活,降低耦合,提高代码的自适应能力.

怎么样降低耦合度,提高代码的自适应能力?
通过接口实现,但是接口如果需要用到new关键字,这时候耦合问题又会出现

示例四:

public class HeroFactory {
     public static void main(String[] args) {
          HeroFactory facrty = new HeroFactory();
          Hero iroman = facrty.CreateHero("IronMan");
          iroman.attach();
     }
     public hero CreateHero(String name) {
          if ((name).equals("IronMan")) {
              return new IronMan();
          }
          if ((name).equals("Hulk")) {
              return new Hulk();
          }
          return null;
     }
     interface Hero {
          public void attach();
     }
     class IronMan implements Hero {
          @Override
          public void attach() {
              System.out.println("Laser Light");
          }
     }
     class Hulk implements Hero {
          @Override
          public void attach() {
              System.out.println("fist");
          }
     }
}

假设有1000个不同Hero需要创建,那你打算写1000个 if语句来返回不同的Hero对象?
那么,如果使用反射机制,代码如下:

package com.it.demo1;
public class HeroFactory {
     public static void main(String[] args) {
        HeroFactory facrty = new HeroFactory();
        Hero hero=facrty.CreateHero("com.it.demo1.IroMan");
        hero.attack();
    }
    public Hero CreateHero(String name) {
        try {
            Class cls = Class.forName(name);
            Hero hero = (Hero) cls.newInstance();
            return hero;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
class IroMan implements Hero {
    @Override
    public void attack() {
        System.out.println("Laser Light");
    }
}
class Hulk implements Hero {
    @Override
    public void attack() {
        System.out.println("Fist");
    }
}
interface Hero {
    public void attack();
}

利用反射机制进行解耦的原理就是利用反射机制"动态"的创建对象:向CreateHero()方法传入Hero类的包名.类名 通过加载指定的类,然后再实例化对象.

示例五:

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class Test {
     /**
      * @param args
      * @throws Exception
      */
     public static void main(String[] args) throws Exception {
          // 获取类
          Class c = Class.forName("java.lang.String");
          // 获取所有的属性
          Field[] fields = c.getDeclaredFields();
          StringBuffer sb = new StringBuffer();
          sb.append(Modifier.toString(c.getModifiers()) + " class "
                   + c.getSimpleName() + "{\n");
          // 遍历每一个属性
          for (Field field : fields) {
              sb.append("\t");// 空格
               sb.append(Modifier.toString(field.getModifiers()) + " ");// 获得属性的修饰符,例如public,static等等
              sb.append(field.getType().getSimpleName() + " ");// 属性的类型的名字
              sb.append(field.getName() + ";\n");// 属性的名字+回车
          }
          sb.append("}\n");
          System.out.println(sb);
     }
}

输出结果:


image.png

三、Android中的反射应用

总结一下,在Android中使用反射非常之慢。为了向用户提供最流畅的用户体验,我们强烈建议:
尽可能避免所有对反射的使用(以及使用了反射的第三方库),尤其是使用类型反射来对Java对象进行序列化操作。

Android FrameWork中的反射:
一个类中的每个成员都可以用相应的反射API的一个实例对象来表示——反射机制。
了解这些,那我们就知道了,我们可以利用反射机制在Java程序中,动态的去调用一些protected甚至是private的方法或类,这样可以很大程度上满足我们的一些比较特殊需求。例如Activity的启动过程中Activity的对象的创建。

ClassLoader和DexClassLoader
上面说到JAVA的动态加载的机制就是通过 ClassLoader 来实现的,ClassLoader 也是实现反射的基石。ClassLoader 是JAVA提供的一个类,顾名思义,它就是用来加载Class文件到JVM,以供程序使用的。

但是问题来了,ClassLoader加载文件到JVM,但是Android是基于DVM的,用ClassLoader 加载文件进DVM肯定是不行的。于是Android提供了另外一套加载机制,分别为 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader,区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk(因为安装过的 apk 在 cache 中存在缓存的 dex 文件)。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。

ClassLoader在Java中的应用(利用反射来调用另一个类中的方法)

DexClassLoader使用场景
上面是是使用的已经安装过的Apk,如果采用未安装过的jar包或者Apk,则实例化 DexClassLoader 的时候把相应路径改为需要加载的jar包或者Apk路径亦可拿到结果。这就使得 DexClassLoader 可以应用在HotFix(热修复),动态加载框架等等 一些基于插件化的架构中。

以上是根据我的一些理解,做的总结分享,旨在抛砖引玉,希望有更多的志同道合的朋友一起讨论学习,共同进步!

Java
Web note ad 1