踩了一个 Java 编译时和运行时环境不一致导致的一个坑 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
pursuer
V2EX    Java

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

  •  
  •   pursuer
    partic2 2020-10-22 20:18:21 +08:00 4180 次点击
    这是一个创建于 1818 天前的主题,其中的信息可能已经有所发展或是发生改变。
    这两天测试了一段代码,在 Java11 上正常运行,在 androi 上运行报 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     Solana     984 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 23ms UTC 22:40 PVG 06:40 LAX 15:40 JFK 18:40
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86