前不久在工作中,遇到了几次编译class引起的NoSuchMethodError,经过分析与测试验证,也算是搞清楚了中间的来龙去脉,现在把一些结论性的东西(附带一些过程性的分析)分享出来。

在使用javac -source 1.6 -target 1.6来编译低版本的(这里为1.6)class时,记得要使用-bootclasspath参数来指定1.6版本的类库(一般是rt.jar),不指定的话,会产生一个警告:

警告: [options] 未与 -source 1.6 一起设置引导类路径

或者英文版的

warning: [options] bootstrap class path not set in conjunction with -source 1.6

如果忽视这个警告(当时我在网上搜索上述中文警告时,没有任何资料说需要引起注意,以及该如何解决),编译出来的class可能无法在低版本的jre中运行,假如源码中调用了一些特殊方法,则会在执行时抛出NoSuchMethodError。比如ConcurrentHashMap的keySet方法,在jdk1.6中,该方法返回的是Set,在jdk1.8中,该方法返回的是KeySetView,它是jdk1.8中新增的一个类,为Set的一个实现。当把这样编译出来的class放到jre1.6中去运行时,会因为找不到返回类型为KeySetView的keySet方法而抛出NoSuchMethodError,虽然编译后的class的版本是1.6。

基于上面的认知,来讨论一下如下场景
    现在有apiA_1.0.jar与apiB_1.0.jar,apiB_1.0.jar依赖apiA_1.0.jar,前者是基于后者编译的,也就是这两个版本之间不存在兼容问题。
    然后假如apiA进行了修改,升级为apiA_1.1.jar,其中某个类的某个方法的返回值由Object改为了String(从源码上来讲,这样改是兼容的,因为String是一个Object,这应该就是里氏替换吧),此时apiB_1.0.jar就不兼容apiA_1.1.jar了,如果单方面把apiA升级到1.1,apiB在调用apiA中的那个返回值为Object的方法时,会因为找不到方法而抛出NoSuchMethodError(如果对此有异议,请看后文),因为现在在apiA中,只有那个返回值为String的方法了,并且,你也不可能保留返回值为Object的那个方法,它们是互相冲突的。
    当然,此时也可以重新发布一个apiB_1.1.jar,基于apiA_1.1.jar编译出来的版本。但这样,也就意味着,apiB依赖了apiA特定的版本,这样非常不利于依赖维护,使用过程中很容易出问题,而且这种问题只有在运行时,调用了有问题的方法时才会发现,应用程序的编译过程中是不会报错的(apiA和apiB是已经编译的jar了)。

也许此时你已经注意到了,难道jdk也不向前兼容了?为什么我用jdk1.6编译出来的程序能在jre1.8中正常的调用ConcurrentHashMap.keySet?它不是也存在上面所说的问题吗?它为什么不会因为找不到返回值为Set的keySet方法而抛异常?
    这里就需要介绍一下class中的桥接方法(bridge method)了,它不报错,是因为1.8中确实也存在一个返回值为Set的keySet方法,只不过不是存在于源文件中,而是存在于class文件中,通过javap -v java.util.concurrent.ConcurrentHashMap反编译1.8的ConcurrentHashMap,可以看到一个返回值为java.util.Set的keySet方法:

public java.util.Set keySet();
descriptor: ()Ljava/util/Set;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #838 // Method keySet:()Ljava/util/concurrent/ConcurrentHashMap$KeySetView;
4: areturn
LineNumberTable:
line 267: 0

ps:flag参数中的ACC_BRIDGE表明了这是一个桥接方法

虽然java语法层面不允许存在仅返回值不同的两个方法,但在class文件中,并没有此限制,在此桥接方法中,调用了返回值为KeySetView的keySet方法。另外java.lang.reflect.Method.isBridge()就是指的这个。

那为什么ConcurrentHashMap.keySet会有桥接方法呢?其实也不是jdk给自己搞的特殊化,是因为keySet是一个重写方法(接口方法也有此效果),重写了父类AbstractMap的public Set<K> keySet()方法,这个大致可以理解为,父类或接口已经对外宣称了该方法(也就是返回Set),那如果子类或实现者自己返回了其它子类型,那么编译器就得来做这个兼容性工作,即创建桥接方法。如果直接改了顶层方法,编译器自然不可能去做这个事情,它怎么知道要跟谁兼容?同理,静态方法也会有问题。

最后总结一下:

  • 如果你要保持跟以前的版本兼容,除了接口方法或重写父类方法,其它时候就不要改变返回值类型,否则就不兼容了。(我认为在参与开源项目时尤其需要注意这一点)
  • 使用高版本javac配置source、target参数来编译低版本class或打jar包时,必需用bootclasspath指定对应低版本的类库,否则也可能产生不兼容。这也意味着:不要仅仅装一个jdk8就期待编译出一定能在jre1.6上正常运行的程序,你还需要一个1.6版本的java类库来完成编译。
  • 如果有替换个别class文件来打补丁的习惯,那么也需要特别小心兼容问题,原理是一样的。

上面所说的不兼容问题,会延后到真正调用问题方法时候才会暴露,所以值得加以重视。

相关链接:

javac官方文档:http://docs.oracle.com/javase/8/docs/technotes/tools/windows/javac.html

java class的兼容问题的更多相关文章

  1. Java与.NET兼容的RSA密钥持久化方法

    默认情况下,.NET生成的RSA密钥对可以用XML或字节流来保存,而JAVA中生成的RSA密钥对只能用字节流来保存.而它们的字节流格式不同,就导致Java中生成的RSA密钥对不能在.NET中使用,而. ...

  2. android开发 java与c# 兼容AES加密

    由于android客户端采用的是AES加密,服务器用的是asp.net(c#),所以就造成了不一致的加密与解密问题,下面就贴出代码,已经试验过. using System; using System. ...

  3. Java读取excel(兼容03和07格式)

    读取excel,首先需要下载POI的jar,可以去官网下,也可以在这里下载 一.简单说明 excel2003和excel2007区别比较大,最直观的感受就是扩展名不一样,哈哈 不过,使用POI的API ...

  4. 记录java版本不兼容的坑,(kafka运行报错)

    启动kafka报错 错误原因是: 由较高版本的jdk编译的java class文件 试图在较低版本的jvm上运行的报错 解决办法是: 查看java版本 C:\Users\Administrator&g ...

  5. Java Controller下兼容xls和xlsx且可识别合并单元格的excel导入功能

    1.工具类,读取单元格数据的时候,如果当前单元格是合并单元格,会自动读取合并单元格的值 package com.shjh.core.util; import java.io.IOException; ...

  6. eclipse环境问题-java版本不兼容

    有时候虽然我们给项目配置的jdk版本.项目编译版本都一直,但是还是会报如下的错误: Description Resource Path Location Type Java compiler leve ...

  7. 使用 Java 开发兼容 IPv6 的网络应用程序

    根据现有 IPv4 地址的部署速度,剩余的地址将在 10 到 20 年被使用殆尽.因此网络逐渐从 IPv4 向 IPv6 转换是不可避免的,相应的各种网络应用程序都将支持 IPv6.对于 Java,从 ...

  8. C++、Java、Objective-C、Swift 二进制兼容测试

    鉴于目前动态库在iOS App中使用越来越广泛,二进制的兼容问题可能会成为一个令人头疼的问题.本文主要对比一下C++.Java.Objecive-C和Swift的二进制兼容问题. iOS端动态库使用情 ...

  9. Java基础知识笔记(三:文件与数据流)

    一.输入流与输出流 输入流将数据从文件.标准输入或其他外部输入设备中加载到内存.输出流的作用则刚好相反,即将在内存中的数据保存到文件中,或传输给输出设备.输入流在Java语言中对应于抽象类java.i ...

随机推荐

  1. Flask-RESTful 快速入门

    Flask-RESTful 快速入门 hello world from flask import Flask from flask_restful import Resource, Api app = ...

  2. js中array的filter用法

    function bouncer(arr) { // Don't show a false ID to this bouncer. arr = arr.filter(function(val) { i ...

  3. 基本变换(读书笔记5 --- Real-Time rendering)

    刚体变换 即变换不改变了被变换顶点之间的距离,以及偏手性(不会让左右手坐标系颠倒). 下面的平移变换.旋转变换即属于刚体变换 平移 从一个位置变到另一个位置可以用平移矩阵T来表示,这个矩阵将一个实体变 ...

  4. MATLAB处理信号得到频谱、相谱、功率谱

    (此帖引至网络资源,仅供参考学习)第一:频谱 一.调用方法 X=FFT(x):X=FFT(x,N):x=IFFT(X);x=IFFT(X,N) 用MATLAB进行谱分析时注意: (1)函数FFT返回值 ...

  5. PE新手教程

    先确定你的手机是 IPhone 还是 Android  Android 的在你的应用商店搜索  我的世界 下载 0.14.2 版本 , 也可以在群文件中下载 安装 然后点击 下载  等待下载..  下 ...

  6. Processing与Java混编初探

    Processing其实是由Java开发出的轻量级JAVA开发语言,主要用于做原型,有setup.draw两个主接口和几个消息相应接口 Processing和Java混编很简单...在Java中引入外 ...

  7. http://blog.csdn.net/shawnkong/article/details/52045894

    http://blog.csdn.net/shawnkong/article/details/52045894

  8. TortoiseGit 图标不显示

    1. 确认注册表:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdent ...

  9. NSQ的消息订阅发布测试

    在测试NSQ的Quick Start发现这样一个问题,就是同时只能有一个订阅实例 $ nsq_to_file --topic=test --output- 当存在两个实例时则消息会被发送给其中的一个实 ...

  10. SQL Server 数据库的安全管理(登录、角色、权限)

    ---数据库的安全管理 --登录:SQL Server数据库服务器登录的身份验证模式:1)Windows身份验证.2)Windows和SQL Server混合验证 --角色:分类:1)服务器角色.服务 ...