前不久在工作中,遇到了几次编译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. stimulsoft Report报表使用笔记

    1.使用设计器设计mrt报表模板,或者从其他文件复制修改 2.删除business object 数据源 3.使用代码添加数据源 ParcelChangeItem change = new Parce ...

  2. 答:SQLServer DBA 三十问之五:有关视图索引

    5. 视图上我们能建索引吗,如果能建的话,会啥好处和坏处:视图上建索引和表上建索引有啥区别: ----------------------------------------------------- ...

  3. 初学python之urllib

    urllib.request urlopen()urllib.urlopen(url, data, proxies) :创建一个表示远程url的类文件对象,然后像本地文件一样操作这个类文件对象来获取远 ...

  4. uri和url的简单理解

    URL:统一资源定位符 也就是网址 例如 http://www.microsoft.com/ URI:通用资源标志符 http://www.acme.com/support/suppliers.htm

  5. WPF绘制矢量图形模糊的问题

    WPF默认提供了抗锯齿功能,通过向外扩展的半透明边缘来实现模糊化.由于WPF采用了设备无关单位,当设备DPI大于系统DPI时,可能会产生像素自动扩展问题,这就导致线条自动向外扩展一个像素,并且与边缘相 ...

  6. linux chmod 755

    chmod是Linux下设置文件权限的命令,后面的数字表示不同用户或用户组的权限. 一般是三个数字: 第一个数字表示文件所有者的权限 第二个数字表示与文件所有者同属一个用户组的其他用户的权限 第三个数 ...

  7. 【java基础学习二】 数组相互转换,转成用逗号隔开的字符串等

    /** * int[],Integer[],List,List<Integer>,HashSet<Integer>相互转换,转成用逗号隔开的字符串 */ public stat ...

  8. Android 之Html的解析(使用jsoup)

    Runnable run=new Runnable() { @Override public void run() { // TODO Auto-generated method stub useri ...

  9. HMC破解控制台密码

    一般我们装完HMC以后默认的登录控制台的账户是:admin 密码:abc123当我们可以自创建登录账户后,忘记密码了怎么办? 这里有两种方法: 1 由于HMC是suse系统,基于linux内核的,所以 ...

  10. Badboy录制脚本参数化

    Jmeter录制脚本多采用Badboy工具 简单参数修改 第一步,先录制场景: a.开始录制,打开浏览器www.sogou.com b.在输入框中输入文字“Badboy" c.回车,关闭录制 ...