二进制兼容原理 - C/C++ &Java

       从某种意义上来讲,现代软件已经不是数据结构与算法的简单聚合,更多的是构件开发以及基于体系结构的构件组装.而这些构件,通常都是由不同厂商、作者开发的共享组件,所以组件管理变得越来越重要。在这方面,一个极其重要的问题是类的不同版本的二进制兼容性,即一个类改变时,新版的类是否可以直接替换原来的类,却不至于损坏其他由不同厂商/作者开发的依赖于该类的组件?

       在C++中,对域(类变量或实例变量)的访问被编译成相对于对象起始位置的偏移量,在编译时就确定,如果类加入了新的域并重新编译,偏移量随之改变,原先编译的使用老版本类的代码就不能正常执行( 也许有人会认为这是C++要比Java的快的一个原因,根据数值性偏移量寻找方法肯定要比字符串匹配快。这种说法有一定道理,但只说明了类刚刚装入时的情况,此后Java的JIT编译器处理的也是数值性偏移量,而不再靠字符串匹配的办法寻找方法,因为类装入内存之后不可能再改变,所以这时的JIT编译器根本无须顾虑到二进制兼容问题。因此,至少在方法调用这一点上,Java没有理由一定比C++慢),不仅如此,虚函数的调用也存在同样的问题。这些我们都称之为二进制不兼容,与之对应的是源码不兼容,如修改成员变量名字等.

       C++环境通常采用重新编译所有引用了被修改类的代码来解决问题。在Java中,少量开发环境也采用了同样的策略,但这种策略存在诸多限制。例如,假设有人开发了一个程序P,P引用了一个外部的库L1,但P的作者没有L1的源代码;L1要用到另一个库L2。现在L2改变了,但L1无法重新编译,所以P的开发和更改也受到了限制。为此,Java引入了二进制兼容的概念—如果对L2的更改是二进制兼容的,那么更改后的L2、原来的L1和现在的P能够顺利连接,不会出现任何错误。

      首先来看一个简单的例子。Authorization和Customer类分别来自两个不同的作者,Authorization提供身份验证和授权服务,Customer类要调用Authorization类。    

package com.author1;
public class Authorization {
 public boolean authorized(String userName) {
  return true;
 }
}

package com.author2;
import com.author1.*;
class Customer{
 public static void main(String arg[]) {
  Authorization auth = new Authorization();
  if(auth.authorized("messi"))
   System.out.println("pass");
  else
   System.out.println("go away");
 }
}

        现在author1发布了Authorization类的2.0版,Customer类的作者author2希望在不更改原有Customer类的情况下使用新版的Authorization类。2.0版的Authorization要比原来的复杂不少:

package com.author1;
public class Authorization {
 public Token authorized(String userName, String pwd) {
  return null;
 }
 public boolean authorized(String userName) {
  return true;
 }
 public class Token { }
}

       作者author1承诺2.0版的Authorization类与1.0版的类二进制兼容,或者说,2.0版的Authorization类仍旧满足1.0版的Authorization类与Customer类的约定。显然,author2编译Customer类时,无论使用Authorization类的哪一个版本都不会出错—实际上,如果仅仅是因为Authorization类升级,Customer类根本无需重新编译,同一个Customer.class可以调用任意一个Authorization.class。
       这一特性并非Java独有。UNIX系统很早就有了共享对象库(.so文件)的概念,Windows系统也有动态链接库(.dll文件)的概念,只要替换一下文件就可以将一个库改换为另一个库。就象Java的二进制兼容特性一样,名称的链接是在运行时完成,而不是在代码的编译、链接阶段完成。但是,Java的二进制兼容性还有其独特的优势:
⑴ Java将二进制兼容性的粒度从整个库(可能包含数十、数百个类)细化到了单个的类。
⑵ 在C/C++之类的语言中,创建共享库通常是一种有意识的行为,一个应用软件一般不会提供很多共享库,哪些代码可以共享、哪些代码不可共享都是预先规划的结果。但在Java中,二进制兼容变成了一种与生俱来的天然特性。
⑶ 共享对象只针对函数名称,但Java二进制兼容性考虑到了重载、函数签名、返回值类型。
⑷ Java提供了更完善的错误控制机制,版本不兼容会触发异常,但可以方便地捕获和处理。相比之下,在C/C++中,共享库版本不兼容往往引起严重问题。

       二进制兼容的概念在某些方面与对象串行化的概念相似,两者的目标也有一定的重叠。串行化一个Java对象时,类的名称、域的名称被写入到一个二进制输出流,串行化到磁盘的对象可以用类的不同版本来读取,前提是该类要求的名称、域都存在,且类型一致。二进制兼容和串行化都考虑到了类的版本不断更新的问题,允许为类加入方法和域,而且纯粹的加入不会影响程序的语义;类似地,单纯的结构修改,例如重新排列域或方法,也不会引起任何问题。

       理解二进制兼容的关键是要理解延迟绑定(Late Binding)。在Java语言里,延迟绑定是指直到运行时才检查类、域、方法的名称,而不象C/C++的编译器那样在编译期间就清除了类、域、方法的名称,代之以偏移量数值—这是Java二进制兼容得以发挥作用的关键。由于采用了延迟绑定技术,方法、域、类的名称直到运行时才解析,意味着只要域、方法等的名称(以及类型)一样,类的主体可以任意替换—当然,这是一种简化的说法,还有其他一些规则制约Java类的二进制兼容性,例如访问属性(private、public等)以及是否为abstract(如果一个方法是抽象的,那么它肯定是不可直接调用的)等,但延迟绑定机制无疑是二进制兼容的核心所在。
只有掌握了二进制兼容的规则,才能在改写类的时候保证其他类不受到影响。下面再来看一个例子,KakaMail和MessiMail是两个Email程序:

abstract class Message implements Classifiable {}
class EmailMessage extends Message {
 public boolean isJunk() { return false; }
}
interface Classifiable {
 boolean isJunk();
}
class KakaMail {
 public static void main(String a[]) {
  Classifiable m = new EmailMessage();
  System.out.println(m.isJunk());
 }
}
class MessiMail {
 public static void main(String a[]) {
  EmailMessage m = new EmailMessage();
  System.out.println(m.isJunk());
 }
}

       如果我们重新实现Message,不再让它实现Classifiable接口,MessiMail仍能正常运行,但KakaMail会抛出异常"java.lang.IncompatibleClassChangeError"。这是因为MessiMail不要求EmailMessage是一个Classifiable,但KakaMail却要求EmailMessage是一个Classifiable,编译KakaMail得到的二进制.class文件引用了Classifiable这个接口名称。

       从二进制兼容的角度来看,一个方法由四部分构成,分别是:方法的名称,返回值类型,参数,方法是否为static。改变其中任何一个,对JVM而言,它已经变成了另一个方法。如果该类没有提供一个名称、参数、返回值类型完全匹配的方法,它就使用从超类继承的方法。由于Java的二进制兼容性规则,这种继承实际上在运行期间确定,而不是在编译期间确定。也正是因为继承,在代码重构过程中,会招致各种错误.比反说删除父类的某个在子类覆盖的域,然后调用了强制类型转换后的子类同名字段,往往会出现"java.lang.NoSuchFieldError".

      最新的jls7一文中,有一章节是专门介绍Java语言的二进制兼容性原理的,感兴趣的同学可以下载翻阅,以便加深理解~

ps: 案例拾遗

运行期异常: Exception in thread "main" java.lang.AbstractMethodError: org.apache.batik.dom.GenericElement.setTextContent(Ljava/lang/String;)V

        Why?AbstractMethodError这个错误挺经典的,一般发生在compile time,那出现在运行期,就可能意味着发生了不兼容类更改,为什么这么说,我们看一个例子,直接上代码:

public class Node {
    public void setTextContent(String text) {
        System.out.println("setting " + text);
    }
}
public class SVGNode extends Node {
    public static void main(String args[]) {
        Node node = new Node();
        node.setTextContent("messi");
    }
}

        这么写当然没有任何问题了~好,那Node类出于升级等目的,改为抽象类,setTextContent改为抽象方法,使用Java 命令行方式执行Java SVGNode,随你怎么编译新版Node,javac也行,后面就昭然若揭了~

        总结一下: 该问题在引用外部包的时候常有发生,尤其当类的继承层次比较复杂时,一般不容肉眼识别,但万变不离其宗~其根本原因可能是父类出现了不兼容修改~另外,要确保编译器和JVM类加载路径完全一致,争取在编译期就发现问题~

参考文献:

1.http://en.wikipedia.org/wiki/Binary_code_compatibility

2.http://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B

3.http://www.javaworld.com/community/node/2915

4.http://www.javapractices.com/topic/TopicAction.do?Id=45

5.http://docs.oracle.com/javase/6/docs/platform/serialization/spec/version.html

6.http://java.sun.com/developer/technicalArticles/Programming/serialization/

7.https://blogs.oracle.com/darcy/entry/kinds_of_compatibility

时间: 2024-09-22 07:19:28

二进制兼容原理 - C/C++ &Java的相关文章

Java二进制兼容性原理

一.概述 现在的软件越来越依赖于不同厂商.作者开发的共享组件,组件管理也变得越来越重要.在这方面,一个极其重要的问题是类的不同版本的二进制兼容性,即一个类改变时,新版的类是否可以直接替换原来的类,却不至于损坏其他由不同厂商/作者开发的依赖于该类的组件? Java二进制兼容性概念的主要目标是推动Internet上软件的广泛重用,同时它还避免了大多数C++环境面临的基础类脆弱性问题--例如,在C++中,对域(数据成员或实例变量)的访问被编译成相对于对象起始位置的偏移量,在编译时就确定,如果类加入了新

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

鉴于目前动态库在iOS App中使用越来越广泛,二进制的兼容问题可能会成为一个令人头疼的问题.本文主要对比一下C++.Java.Objecive-C和Swift的二进制兼容问题. iOS端动态库使用情况 iOS 8开始支持App使用动态库. 苹果对提交的App的__TEXT__段大小是有限制的,很多巨无霸App容易超出这个限制.iOS9之前每个架构的__TEXT__段比较小,iOS9放大到了500MB.详细情况请看:To submit an app for review. 开源库只能通过Podf

java-C#中将double值变成二进制然后写入文件,Java中载入该文件读取此二进制double值时不正确

问题描述 C#中将double值变成二进制然后写入文件,Java中载入该文件读取此二进制double值时不正确 目前已定位到是因为C#中的byte范围是0到255,而java中byte值为-128到127导致的错误. 尝试过使用C#的sbyte来解决: bw1 = new BinaryWriter(new FileStream("C:UsersDELLDesktopSpatialIndexctest1.bin", FileMode.Create)); bw2 = new BinaryW

编译原理scanner的java代码

问题描述 编译原理scanner的java代码 package lexer; public class Token { public final int tag;public Token(int t) { tag = t;}public String toString() { return """" + (char) tag;} } package lexer; public class Tag { public final static int AND = 256

IOS 基于APNS消息推送原理与实现(JAVA后台)

IOS 基于APNS消息推送原理与实现(JAVA后台) Push的原理: Push 的工作机制可以简单的概括为下图   图中,Provider是指某个iPhone软件的Push服务器,这篇文章我将使用.net作为Provider. APNS 是Apple Push Notification Service(Apple Push服务器)的缩写,是苹果的服务器. 上图可以分为三个阶段. 第一阶段:Push服务器应用程序把要发送的消息.目的iPhone的标识打包,发给APNS. 第二阶段:APNS在自

java-ImageJ里的Bonej中的thickness算法,有人能解释原理吗?JAVA我看不懂

问题描述 ImageJ里的Bonej中的thickness算法,有人能解释原理吗?JAVA我看不懂 ImageJ里的Bonej中的thickness算法,有人能解释原理吗? 解决方案 二值图像细化,你可以尝试Zhang-Suen thinning算法,代码在本人博客上有说明

Eero 2011-06-25发布 二进制兼容Objective C 2.0变种

Eero 是一个二进制兼容的 Objective C 2.0 的变种,其目的是为了让代码编写更加简单,提升代码的可读性.Eero 实现了补丁版的 Clang/LLVM 编译器.Eero 使用类 Python 的代码缩进以及操作符重载限制.该语言主要从 Smalltalk 和 http://www.aliyun.com/zixun/aggregation/13430.html">Ruby 中获得灵感. Eero 2011-06-25支持增加了对选择和协议"文本"对象和类转

AccessPlatform v2.2.1发布 兼容各种标准的Java数据持久化框架

DataNucleus Access Platform是一个兼容各种标准的 Java 数据持久化框架,完全兼容 JDO1, JDO2, JDO2.1, JDO2.2, JDO2.3, 和 JPA1 等 Java 标准.提供一个基于 REST 的API.通过它可以访问所有常见的数据库服务器,还包括 LDAP, NeoDatis, JSON, http://www.aliyun.com/zixun/aggregation/16544.html">Excel/ODF spreadsheets,

java 二维码原理以及用java实现的二维码的生成、解码(转)

      http://blog.csdn.net/songylwq/article/details/8643948 http://sjsky.iteye.com/blog/1136934 http://bbs.csdn.net/topics/390398702   http://blog.csdn.net/johnsuna/article/details/8525038