资讯专栏INFORMATION COLUMN

Java反射学习小记

frank_fun / 3181人阅读

摘要:反射使用类对象提供的基本元数据,能从类对象中找出方法或字段的名称,然后获取表示方法或字段的对象。常见的反射手段有反射和反射。以之前的反射为例其中指定了方法的返回类型,其实不止如此。

Java反射机制主要提供了以下功能:

在运行时判断任意一个对象所属的类

在运行时构造任意一个类的对象

在运行时判断任意一个类所具有的成员变量和方法

在运行时调用任意一个对象的方法

生成动态代理

很多框架都用到了反射机制,包括大名鼎鼎的Spring。因此,了解反射也可以说是为之后学习框架源码而打下坚实的基础。

即便编译时不知道类型和方法名称,也能使用反射。反射使用类对象提供的基本元数据,能从类对象中找出方法或字段的名称,然后获取表示方法或字段的对象。

在Java中,静态成员和普通数据类型不是对象,其他皆是。

那么问题来了,类是谁的对象?

java.lang.Class的实例对象。

Class.forName(ClassName)//可以动态加载类——也就是运行时加载

(使用 Class::newInstance() 或另一个构造方法)创建实例时也能让实例具有反射功能。如果有一个能反射的对象和一个 Method 对象,我们就能在之前类型未知的对象上调用任何方法。

反射出来的对象信息是几乎未知的,所以反射也并不是那么的好用。

什么时候用反射

很多,也许是多数 Java 框架都会适度使用反射。如果编写的架构足够灵活,在运行时之前都不知道要处理什么代码,那么通常都需要使用反射。例如,插入式架构、调试器、代码浏览器和 REPL 类环境往往都会在反射的基础上实现。

反射在测试中也有广泛应用,例如,JUnit 和 TestNG 库都用到了反射,而且创建模拟对象也要使用反射。如果你用过任何一个 Java 框架,即便没有意识到,也几乎可以确定,你使用的是具有反射功能的代码。

常见的反射手段有JDK反射和cglib反射。

在自己的代码中使用反射 API 时一定要知道,获取到的对象几乎所有信息都未知,因此处理起来可能很麻烦。

只要知道动态加载的类的一些静态信息(例如,加载的类实现一个已知的接口),与这个类交互的过程就能大大简化,减轻反射操作的负担。

使用反射时有个常见的误区:试图创建能适用于所有场合的反射框架。正确的做法是,只处理当前领域立即就能解决的问题。

如何使用反射

使用反射的第一步就是获取Class对象,Class对象里存储了很多关键信息——毕竟这是用来描述类的class。

我们可以这样来获取Class信息:

Class clz = Class.forName(obj.getClazz());
//通过class生成相应的实例
Object newObj = clz.newInstance
从Java1.5开始,Class类就支持泛型化了。比如:String.class就是Class类型。
Method对象

在反射最常用的API就是Method了。

类对象中包含该类中每个方法的 Method 对象。这些 Method 对象在类加载之后惰性创建,所以在 IDE 的调试器中不会立即出现。

Method对象中保存的方法和元数据:

private Class                   clazz;
private int                        slot;
// This is guaranteed to be interned by the VM in the 1.4
// reflection implementation
private String                     name;
private Class                   returnType;
private Class[]                 parameterTypes;
private Class[]                 exceptionTypes
private int                        modifiers;
// Generics and annotations support
private transient String           signature;
// Generic info repository; lazily initialized
private transient MethodRepository genericInfo;
private byte[]                     annotations;
private byte[]                     parameterAnnotations;
private byte[]                     annotationDefault;
private volatile MethodAccessor    methodAccessor;

我们可以通过getMethod获得对象的方法:

Object rcvr = "str";
try {
    Class[] argTypes = new Class[] { };
    //其实这个参数没有也没关系,因为hashCode方法不需要参数
    Object[] args = null;

    Method hasMeth = rcvr.getClass().getMethod("hashCode", argTypes);
    Object ret = hasMeth.invoke(rcvr,args);
    System.out.println(ret);

} catch (IllegalArgumentException | NoSuchMethodException |
        SecurityException e) {
    e.printStackTrace();
} catch (IllegalAccessException | InvocationTargetException x) {
    x.printStackTrace();
}

如果要调用非公开方法,必须使用 getDeclaredMethod() 方法才能获取非公开方法的引用,而且还要使用 setAccessible() 方法覆盖 Java 的访问控制子系统,然后才能执行:

public class MyCache {
    private void flush() {
        // 清除缓存……
    }
}

Class clz = MyCache.class;
try {
    Object rcvr = clz.newInstance();
    Class[] argTypes = new Class[]{};
    Object[] args = null;
    Method meth = clz.getDeclaredMethod("flush", argTypes);
    meth.setAccessible(true);
    meth.invoke(rcvr, args);
} catch (IllegalArgumentException | NoSuchMethodException |
        InstantiationException | SecurityException e) {
    e.printStackTrace();
} catch (IllegalAccessException | InvocationTargetException x) {
    x.printStackTrace();
}
反射的问题

Java 的反射 API 往往是处理动态加载代码的唯一方式,不过 API 中有些让人头疼的地方,处理起来稍微有点困难:

大量使用 Object[] 表示调用参数和其他实例;

大量使用 Class[] 表示类型;

同名方法可以重载,所以需要维护一个类型组成的数组,区分不同的方法;

不能很好地表示基本类型——需要手动打包和拆包。

void 就是个明显的问题——虽然有 void.class,但没坚持用下去。Java 甚至不知道 void 是不是一种类型,而且反射 API 中的某些方法使用 null 代替 void。

这很难处理,而且容易出错,尤其是稍微有点冗长的数组句法,更容易出错。

动态代理

Java反射的API中还提供了动态代理。动态代理是实现了一些接口的类(扩展 java.lang.reflect.Proxy 类)。这些类在运行时动态创建,而且会把所有调用都转交给 InvocationHandler 对象处理:

InvocationHandler h = new InvocationHandler() {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args)
                                        throws javaThrowable {
    String name = method.getName();
    System.out.println("Called as: "+ name);
    switch (name) {
      case "isOpen":
        return false;
      case "close":
        return null;
    }

    return null;
  }
};

Channel c =
  (Channel) Proxy.newProxyInstance(Channel.class.getClassLoader(),
                            new Class[] { Channel.class }, h);

c.isOpen();
c.close();

代理可以用作测试的替身对象(尤其是测试使用模拟方式实现的对象)。

代理的另一个作用是提供接口的部分实现,或者修饰或控制委托对象的某些方面:

public class RememberingList implements InvocationHandler {
  private final List proxied = new ArrayList<>();

  @Override
  public Object invoke(Object proxy, Method method, Object[] args)
                         throws Throwable {
    String name = method.getName();
    switch (name) {
      case "clear":
        return null;
      case "remove":
      case "removeAll":
        return false;
    }

    return method.invoke(proxied, args);
  }
}

RememberingList hList = new RememberingList();

List l =
  (List) Proxy.newProxyInstance(List.class.getClassLoader(),
                                        new Class[] { List.class },
                                        hList);
l.add("cat");
l.add("bunny");
l.clear();
System.out.println(l);
Java7中的方法句柄

Java7中提供了方法句柄,比起“传统”的反射机制。更为好用,而且性能更好。

以之前的反射hashCode为例

Object rcvr = "str";
       try {
           MethodType mt = MethodType.methodType(int.class);
           MethodHandles.Lookup l = MethodHandles.lookup();
           MethodHandle hashMeth = l.findVirtual(rcvr.getClass(), "hashCode", mt);

           int result;
           try {
               result = (int) hashMeth.invoke(rcvr);
               System.out.println(result);
           } catch (Throwable t) {
               t.printStackTrace();
           }
       } catch (IllegalArgumentException |
               NoSuchMethodException | SecurityException e) {
           e.printStackTrace();
       } catch (IllegalAccessException x) {
           x.printStackTrace();
       }

其中MethodType.methodType(int.class);指定了方法的返回类型,其实不止如此。methodType还可以填入其函数参数

通过MethodHandles.Lookup可以获得当前执行方法的上下文对象,在这个对象上可以调用几个方法(方法名都以 find 开头),查找需要的方法,包括findVirtual()findConstructor()` 和 findStatic()

反射 API 和方法句柄 API 之间一个重大的区别是处理访问控制的方式。Lookup 对象只会返回在创建这个对象的上下文中可以访问的方法——没有任何方式能破坏这个规则(不像反射 API 可以使用 setAccessible() 方法调整访问控制)

通过 Lookup 对象可以为任何能访问的方法生成方法句柄,还能访问方法无法访问的字段。在 Lookup 对象上调用 findGetter()findSetter() 方法,分别可以生成读取字段和更新字段的方法句柄。

之后通过MethodHandles.Lookup.findVirtual()获得了方法句柄(MethodHandle)

方法句柄表示调用方法的能力。方法句柄对象是强类型的,会尽量保证类型安全。方法句柄都是 java.lang.invoke.MethodHandle 类的子类实例,JVM 会使用特殊的方式处理这个类。

一般来说,invoke() 方法会调用 asType() 方法转换参数。转换的规则如下:

如果需要,打包基本类型的参数。

如果需要,拆包打包好的基本类型参数。

如果需要,放大转换基本类型的参数。

会把 void 返回类型修改为 0 或 null,具体是哪个取决于期待的返回值是基本类型还是引用类型。

不管静态类型是什么,都能传入 null。

小结

方法句柄提供的动态编程功能和反射一样,但处理方式更清晰明了。而且,方法句柄能在 JVM 的低层执行模型中很好地运转,因此,性能比反射好得多。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66841.html

相关文章

  • FEDay 参会小记

    摘要:介绍微信风格的,与客户端体验一致,这个自己去微信上看吧,略。微信调试一件套,网页授权模拟集成代理远程调试。这些在微信开发者中心有介绍,略。年微信开发经验的人,终于又成为了零年开发经验的人,重新走上了踩坑之路。 showImg(https://segmentfault.com/img/bVtEd1);活动地址:http://fequan.com/2016/ 注意:英文不好,小记也带有自己...

    xcc3641 评论0 收藏0
  • Sublime text3学习小记(macOS系统下的安装使用)

    摘要:等待其安装完成后关闭程序,重新启动,点开菜单可见项,说明插件管理包已安装成功。在出现的悬浮对话框中输入然后点选下面的插件,就会自动开始安装,请耐心等待。【注:以下内容参考https://blog.csdn.net/stilling2006/article/details/54376743】 一、认识Sublime text 1、一款跨平台代码编辑器,在Linux、OSX和Windows下均可...

    Paul_King 评论0 收藏0
  • Java抽象类和接口小记

    摘要:抽象类和接口小记抽象类和接口实现了的多态多态是面向对象程序语言的核心在项目开发过程中其实很少使用抽象类接口用得比较多今天小记一下抽象类和接口的区别抽象类抽象类不能被实例化抽象类可以继承可以定义变量可以定义构造方法抽象方法的要显式的写出来其子 Java抽象类和接口小记 Java抽象类和接口实现了java的多态.多态是面向对象程序语言的核心,在项目开发过程中,其实很少使用抽象类,接口用得比...

    Gemini 评论0 收藏0
  • Java内部类(Inner Class)小记

    摘要:要注意的是,成员内部类不能含有的变量和方法。匿名内部类是唯一一种没有构造器的类。静态嵌套类又叫静态局部类嵌套内部类,就是修饰为的内部类。以上是对内部类的一些学习和总结,纰漏之处希望各位小伙伴友情指点,共同进步。 内部类(inner class)是定义在另一个类中的类,类名不需要和文件夹相同。但为什么要使用内部类呢?其主要原因有以下三点:  1.内部类方法可以访问该类定义所在的作用域中的...

    jackzou 评论0 收藏0
  • JavaScript使用小记

    摘要:简单地说程序就是数据和方法计算机能做的就是计算这个数据可以是字符串各种类型的数值整数小数等类内的属性根本上是还是的基本数据类型布尔类型的东东为了更加快速地写出代码现在的语言都是高层次的抽象即所谓的高级编程语言了高级编程语言中的一些特性如访问 简单地说, 程序就是数据和方法, 计算机能做的就是计算, 这个数据可以是: 1.字符串, 2.各种类型的数值(整数, 小数等), 3.Java类内...

    stefan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<