V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
pursuer
V2EX  ›  Java

踩了一个 Java 编译时和运行时环境不一致导致的一个坑

  •  
  •   pursuer ·
    partic2 · 2020-10-22 20:18:21 +08:00 · 3701 次点击
    这是一个创建于 1491 天前的主题,其中的信息可能已经有所发展或是发生改变。
    这两天测试了一段代码,在 Java11 上正常运行,在 android 上运行报 NoSuchMethodError 异常,异常处就一句

    //buf 是一个 ByteBuffer
    buf.flip()

    报的异常是
    java.lang.NoSuchMethodError: No virtual method flip()Ljava.nio.ByteBuffer in class java.nio.ByteBuffer (我凭记忆还原的,可能不完全一样)

    通常出现这样的问题的时候我第一反应是使用了 android 不支持的高版本 API 导致的。但翻了下 Java 的 API 文档,发现 flip 函数在有 ByteBuffer 的时候就存在了,然后再看方法签名,注意到报异常的函数签名是 flip()Ljava.nio.ByteBuffer,但查阅的文档中这个函数的签名应该是 flip():Ljava.nio.Buffer,猜测是不是编译的时候选择调用了另一个高版本中存在的方法签名,但是不知道怎么处理。
    后来在 StackOverflow 上找到了这个问题的解答。在 Java9 的时候 ByteBuffer 覆写了父类 Buffer 的 flip,mark,reset 等函数,并将返回值改为了 ByteBuffer,导致高版本编译的时候,javac 选择调用了 flip()Ljava.nio.ByteBuffer,在低版本的运行环境中没有这个方法签名,导致出错。解决方法是((Buffer)buf).flip()
    感觉 Java 在这块的设计有些奇特,Java 没有返回值重载,但 JVM 实现上却会将不同返回值认定为不同的方法。
    6 条回复    2020-10-22 23:37:05 +08:00
    pursuer
        1
    pursuer  
    OP
       2020-10-22 21:04:16 +08:00
    更正一下,我再次检查文档的时候发现 Java8 的时候 flip 方法是 final 的,但 Java11 后去掉了,导致编译器在高版本下使用了 invokevirtual 指令调用 flip 函数,导致低版本产生异常。上面提到的“JVM 将不同返回值认定为不同方法“这一说法是不成立的。
    pursuer
        2
    pursuer  
    OP
       2020-10-22 21:25:27 +08:00
    再次更正,我查到 final 关键字对字节码生成没有影响,所以上面说的指令差异也是不对的,那可能还是因为方法签名的问题吧。
    SoloCompany
        3
    SoloCompany  
       2020-10-22 22:10:19 +08:00
    java 兼容性如果都能挑剔的话别的就更不要说了

    你说的的这种问题是常见情形, 如果你一定需要使用高版本的 javac 的话, 正确的做法是给 javac 指定 bootstrapclasspath, 使用 JAVA 8 的 rt.jar, 仅仅靠 -source / -target 参数是无法保证 bytecode 能在低版本的 runtime 下运行
    abcbuzhiming
        4
    abcbuzhiming  
       2020-10-22 22:20:56 +08:00
    java 好像从来没保证说高版本编译的 class 可以在低版本 jvm 上跑啊,我记得 jvm 一直说的是向上兼容,即低版本 jdk 编译的 class 可以跑在高版本 jvm 上(但是从 java9 开始这也不完全保证了,好像只保证 3 个版本之类是兼容的);向下兼容没听说过
    pursuer
        5
    pursuer  
    OP
       2020-10-22 22:46:48 +08:00
    @SoloCompany
    @abcbuzhiming
    无奈 Android 的运行时碎片化太厉害,还是会需要使用高版本 android.jar 编译同时要应用兼容低版本系统。
    Goooogle
        6
    Goooogle  
       2020-10-22 23:37:05 +08:00
    Java 在编译时,会将使用到的方法的签名固化在字节码中的常量池中(类型为 CONSTANT_Methodref_info ),当运行时和编译时的签名不一样时,就会报这个错误。即使是“将参数类型改为其父类型”这种直观看起来可行的方式也不行。
    你例子中,ByteBuffer 是 Buffer 的子类型,单纯从语法上讲,把一个方法的 ByteBuffer 参数的类型替换成 Buffer,所有这个方法的调用方都能继续调用,不会有任何问题,但在编译后的方法执行时先去常量池找到对应的符号引用,但该符号引用在运行时环境中没有,不会判断继承关系,而是直接抛出异常。

    前段时间刚碰到这个问题。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5162 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 09:30 · PVG 17:30 · LAX 01:30 · JFK 04:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.