性能文章>03 回顾反射参数问题>

03 回顾反射参数问题原创

1年前
277156

前言 

在上周的时候, 看了一下 我的 "05 问题" 这一系列博客, 看了一下 第一篇文章, "01 反射参数问题", 发现了一些当时 遗留的一些问题, 呵呵 当时可能由于 水平啊, 理解啊 各方面的原因, 这个就当做了一个问题 记录在了这里, 但是没有 给出具体的问题的来龙去脉, 以及解决思路, 所以 最近翻了一下 相关的资料, 准备回来 整理一下这个问题  

如果没有看过这篇文章, 可以先看一下, 了解一下 这个问题一些具体的信息, 放传送门 

01 反射参数问题 : https://blog.csdn.net/u011039332/article/details/49387217

另外还有一点就是, 我们这里仅仅解释 上文中的 "测试2" 和 "参数更新1" 的调用结果不一致的相关原因, 因为 这个原因你有所了解的话, 其他的几个现象, 应该能够 很快想明白 

还有一点, "参数更新3" 应该是有问题, 要么是更新方式不正确, 要么是结论描述的不正确 

这里我们会有一些层次, 首先是 最表面的代码层面上看, 然后是 字节码, 然后 jls 规范, 然后是编译时, 然后 是运行时  

一下部分代码, 截图 基于 : jdk7u40, idea2019 的 bytecode viewer, jls7, jdk7 的 javac, 运行时vm基于openjdk9[编译于jdk8] 

 

代码层面 - 抛出问题

首先是 "测试2" 的代码如下 

public class Test04ReflectionMethodInvoke {

    // Test04ReflectionMethodInvoke
    public static void main(String []args) throws Exception {

        String[] args02 = {"1", "2", "3"};
        Object[] objs = new Object[1];
        objs[0] = args02;
        for(int i=0; i<objs.length; i++) {
            System.out.println(objs[i]);
        }

        // 使用反射遇到下面的情况是时 如果objs只有一个元素 传入printArgs的参数居然是objs的第一个元素
        Test04ReflectionMethodInvoke tt = new Test04ReflectionMethodInvoke();
        String methodName = "printArgs";
        Method printArgs = Test04ReflectionMethodInvoke.class.getMethod(methodName, String[].class);
        printArgs.invoke(tt, objs);
        printArgs.invoke(tt, objs[0]);

    }

    // 测试方法
    public void printArgs(String []args) {
        for(String arg : args) {
            System.out.print(arg + " ");
        }
        System.out.println();
    }

}

 

然后是 "参数更新1" 的代码如下 

public class Test05ReflectionMethodInvoke {

    // Test05ReflectionMethodInvoke
    public static void main(String []args) throws Exception {

        String[] objs = new String[]{"1", "2", "3" };
        for(int i=0; i<objs.length; i++) {
            System.out.println(objs[i]);
        }

        // 使用反射遇到下面的情况是时 如果objs只有一个元素 传入printArgs的参数居然是objs的第一个元素
        Test05ReflectionMethodInvoke tt = new Test05ReflectionMethodInvoke();
        String methodName = "printArgs";
        Method printArgs = Test05ReflectionMethodInvoke.class.getMethod(methodName, String[].class);
        printArgs.invoke(tt, objs);
        printArgs.invoke(tt, objs[0]);

    }

    // 测试方法
    public void printArgs(String []args) {
        for(String arg : args) {
            System.out.print(arg + " ");
        }
        System.out.println();
    }
    
}

 

"测试2" 执行结果如下 

"参数更新1" 执行结果如下

从结果上来看, 我们粗略一看 大致应该可以看出两个问题 

1. "测试2" 为什么我传递 objs, objs[0] 的效果都是一样的呢 ? 

2. "参数更新1" 为什么, 我传入了 printArgs 的需要的参数 String[] 为什么反而还报错了呢?, 而且还是一个 莫名其妙的 wrong number of arguments ? 

 

字节码层面

为了一探问题的久竟, 查看一下 字节码是相对比较容易, 并且 也稍微最有可能看出问题的方式 

 "测试2" 的字节码如下 

 "参数更新1" 的字节码如下 

从上图, 可以看到一些问题, 比如我们 上面的 "1. "测试2" 为什么我传递 objs, objs[0] 的效果都是一样的呢 ?  ", 原来是 前端编译器给我们做了事情, 发现 objs 类型为 Object[], 直接使用了 objs, 发现 objs[0] 的类型为 Object, 帮我们将其封装为了 Object[]  

第一个问题 在这里 似乎是就能这样愉快的解决了 

但是 第二个问题呢?, 从字节码上面来看, "测试2" 和 "参数更新1" 除了局部变量的 slot 不同之外, 根本没有区别啊 

 

jls 规范 

那么为什么, 前端编译器 要帮我们做 问题1 这些事情呢 ?

Method.invoke 方法声明如下 

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException

这里的 "Object... args" 是 jdk5 新增的 feature : variable parameter, 那么这个 feature 会有哪些性质呢 ?  

 

首先看一下 jls 里面 8.4 讲解 方法声明, 然后 进一步追踪到 方法形参 的相关片段 

refer : https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.4.1

8.4.1. Formal Parameters

Invocations of a variable arity method may contain more actual argument expressions than formal parameters. All the actual argument expressions that do not correspond to the formal parameters preceding the variable arity parameter will be evaluated and the results stored into an array that will be passed to the method invocation (§15.12.4.2).

如果调用 可变参数的时候 实参多于形参的数量的时候, 不属于可变参数之前的所有实参将会被存放在一个数组里面 

 

根据上面的这个片段, 连接到更加相信的 15.12.4.2 

refer : https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.4.2

15.12.4.2. Evaluate Arguments

The process of evaluating the argument list differs, depending on whether the method being invoked is a fixed arity method or a variable arity method (§8.4.1).

If the method being invoked is a variable arity method m, it necessarily has n > 0 formal parameters. The final formal parameter of m necessarily has type T[] for some T, and m is necessarily being invoked with k ≥ 0 actual argument expressions.

If m is being invoked with k ≠ n actual argument expressions, or, if m is being invoked with k = n actual argument expressions and the type of the k'th argument expression is not assignment compatible with T[], then the argument list (e1, ..., en-1, en, ..., ek) is evaluated as if it were written as (e1, ..., en-1, new |T[]| { en, ..., ek }), where |T[]| denotes the erasure (§4.6) of T[].

如果 实参的数量不等于形参的数量 或者 (实参的数量等于形参的数量 但是最后一个实参 和 variableArityType[]不兼容), 则将参数列表 重写为  (e1, ..., en-1new |T[]{  en, ..., ek }

 

 "测试2" 的情况如下 

1. "printArgs.invoke(tt, objs);" : objs 类型为 Object[], 等于 Object[], 所以方法列表不用重写 

2. "printArgs.invoke(tt, objs[0]);"  objs[0] 类型为 Object, 不兼容于 Object[], 所以封装一下, 成为 "printArgs.invoke(tt, new Object[]{objs[0] });"

 "参数更新1" 的情况如下 

1. "printArgs.invoke(tt, objs);" : objs 类型为 String[], 兼容于 Object[], 所以方法列表不用重写 

2. "printArgs.invoke(tt, objs[0]);"  objs[0] 类型为 Object, 不兼容于 Object[], 所以封装一下, 成为 "printArgs.invoke(tt, new Object[]{objs[0] });"

到这里, 我们就知道为什么 前端编译器 需要帮我们做这些事情了, 约定如此 

 

编译时

我们这里只讨论  "测试2" 的相关处理, 因为 这里 "测试2" 和 "参数更新1" 是类似的 

"printArgs.invoke(tt, objs);"  

在 javac 的 attribute 阶段, 解析当前表达式的时候, 判断当前方法调用 是否满足可变参数的使用场景, 结果发现 tt, objs 完全满足 Method.invoke 的形参类型表, 就不使用 可变参数  

解析  表达式 printArgs.invoke(tt, objs);

使用 基础形参实参类型校验, 自动装箱, 可变参数特性 来获取当前方法调用应该选择 哪一个方法 

在 基础形参实参类型校验 的时候 发现形参, 实参的类型完全匹配 

然后再 lower 阶段, 解析 当前表达式的时候, 发现当前表达式没有使用 可变参数特性, 因此包装参数的这个阶段没有做额外的处理 

包装参数之前 

包装参数之后, 所以最后生成的字节码就是我们肉眼可见的代码, load 了 方法, 两个参数, 然后 invokevirtual 

最终生成的字节码如图所示 

"printArgs.invoke(tt, objs[0]);"

接下来我们, 再来看一下 这一个方法调用的处理 

首先是在 attribute 阶段, 解析当前方法调用表达式的时候, 发现 objs[0] 的类型 和 实参类型 Object[] 不匹配, 因此 基础实参形参类型校验没有通过 

然后使用了 装箱特性, 可变参数特性来兼容 参数列表 

发现 objs[0] 的类型 是兼容于 Object[] 的元素类型的, 所以这里需要使用 可变参数特性 

使用基础形参实参类型校验的时候, 这里之所以形参实参只有一个, 是因为 第一个参数已经走过了, 参见 argtypes= argtypes.tail; 

使用可变参数进行校验的时候, 发现 实参兼容于 可变参数类型的子类型 

然后在 lower 阶段, 对于 objs[0] 进行了封装处理一下 

包装参数之前 

包装参数之后, 将 objs[0] 包装成了 new Object[]{objs[0] }, 也就是我们最终看到的额字节码的效果 

最终生成的字节码如图所示 

 

运行时 

那么我们开篇的这个 "wrong number of arguments" 的这个异常是怎么发生的呢?, 也就是 我们一直还未关注的 问题2 

基于 "参数更新1", 从 Method.invoke 往下跟, 这里 传入的额参数为 objs, 是一个 String[]{"1", "2", "3"} 

这里传入的是 objs, 但是实际上按照我们(用户) 期望的参数传递, 这里应该是传入 Object[]{objs }

NativeMethodAccessorImpl.invoke0 的实现 

这上面的两个红框处为 调试输出的 ptypes, 和 args 的内容的信息 

异常实际抛出的地方, 校验了 形参长度 和 实参长度的时候, 实际上 我们(用户)期望传递的是 一个参数数组, 长度为1, 并且数组的第一个对象是一个 String[], 但是这里实际传入的是 一个 String[], 长度为 3

所以这里 抛出了, 我们看到的 "Exception in thread "main" java.lang.IllegalArgumentException: wrong number of arguments" 

好了, 到这里, 我们的 问题2 就已经解释完了 

但是另外还有一个有意思的事情, 还需要一些描述 

了解反射方法调用的人一般都知道, 我们前面 十来次方法调用 都是使用的 NativeMethodAccessorImpl, 但是之后 使用的是 java 层面运行时生成的 MethodAccessor, 那么 这个时候, 对于这总情况下的方法调用, 又会是 什么现象呢 ? 

我们首先在 "printArgs.invoke(tt, objs);" 之前增加如下代码, 然后运行 

        for(int i=0; i<20; i++) {
            printArgs.invoke(tt, new Object[]{new String[]{"xx"}});
        }

这下 抛出的却是一个 "Exception in thread "main" java.lang.IllegalArgumentException"

拿到运行时生成的 MethodAccessor 反编译如下  

public class sun.reflect.GeneratedMethodAccessor1 extends sun.reflect.MethodAccessorImpl {
  public java.lang.Object invoke(java.lang.Object, java.lang.Object[]) throws java.lang.reflect.InvocationTargetException;
    Code:
       0: aload_1
       1: ifnonnull     12
       4: new           #20                 // class java/lang/NullPointerException
       7: dup
       8: invokespecial #28                 // Method java/lang/NullPointerException."<init>":()V
      11: athrow
      12: aload_1
      13: checkcast     #6                  // class com/hx/test11/Test05ReflectionMethodInvoke
      16: aload_2
      17: arraylength
      18: sipush        1
      21: if_icmpeq     32
      24: new           #22                 // class java/lang/IllegalArgumentException
      27: dup
      28: invokespecial #29                 // Method java/lang/IllegalArgumentException."<init>":()V
      31: athrow
      32: aload_2
      33: sipush        0
      36: aaload
      37: checkcast     #14                 // class "[Ljava/lang/String;"
      40: invokevirtual #10                 // Method com/hx/test11/Test05ReflectionMethodInvoke.printArgs:([Ljava/lang/String;)V
      43: aconst_null
      44: areturn
      45: invokespecial #42                 // Method java/lang/Object.toString:()Ljava/lang/String;
      48: new           #22                 // class java/lang/IllegalArgumentException
      51: dup_x1
      52: swap
      53: invokespecial #32                 // Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V
      56: athrow
      57: new           #24                 // class java/lang/reflect/InvocationTargetException
      60: dup_x1
      61: swap
      62: invokespecial #35                 // Method java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V
      65: athrow
    Exception table:
       from    to  target type
          12    40    45   Class java/lang/ClassCastException
          12    40    45   Class java/lang/NullPointerException
          40    43    57   Class java/lang/Throwable

  public sun.reflect.GeneratedMethodAccessor1();
    Code:
       0: aload_0
       1: invokespecial #36                 // Method sun/reflect/MethodAccessorImpl."<init>":()V
       4: return
}

可以看到 生成的 MethodAccessor, 校验了 参数, 所以 得到了 我们看到的结果 

另外, 最后还有一点就是, 假设我们 将 "String[] objs = new String[]{"1", "2", "3"};", 调整成 "String[] objs = new String[]{"1"};" 呢?, 调整之后会通过这个参数数量的校验, 又会发生怎么样的事情呢?, 然后 原因又是什么呢 ? 

这个就留给大家自己去看了 

 

引用

jls 的两个章节

https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.4.1

https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.4.2

关于反射调用方法的一个log

https://www.iteye.com/blog/rednaxelafx-548536

01 反射参数问题

https://blog.csdn.net/u011039332/article/details/49387217

点赞收藏
分类:标签:
黄金键盘
请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

随机一门技术分享之Netty

随机一门技术分享之Netty

从 Linux 内核角度探秘 JDK MappedByteBuffer

从 Linux 内核角度探秘 JDK MappedByteBuffer

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

6
5