二进制兼容原理 - 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

目录
相关文章
|
13天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
8天前
|
算法 Java C语言
C++和Java中的随机函数你玩明白了吗?内附LeetCode470.rand7()爆改rand10()巨详细题解,带你打败LeetCode%99选手
C++和Java中的随机函数你玩明白了吗?内附LeetCode470.rand7()爆改rand10()巨详细题解,带你打败LeetCode%99选手
|
21天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
60 0
|
23天前
|
Java
软件工程设计原理里氏替换原则 ,具体实现及JAVA代码举例
里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的基本原则之一,由Barbara Liskov提出。这个原则指出,如果类 S 是类 T 的子类型,则程序中使用 T 的对象的地方都可以不经修改地使用 S 的对象。换句话说,子类的对象应该能够替换掉它们的父类对象,而不影响程序的正确性。这个原则强调了继承关系中的行为兼容性,保证了基类和派生类之间的正确抽象和继承关系。
23 3
|
8天前
|
运维 NoSQL 算法
Java开发-深入理解Redis Cluster的工作原理
综上所述,Redis Cluster通过数据分片、节点发现、主从复制、数据迁移、故障检测和客户端路由等机制,实现了一个分布式的、高可用的Redis解决方案。它允许数据分布在多个节点上,提供了自动故障转移和读写分离的功能,适用于需要大规模、高性能、高可用性的应用场景。
15 0
|
16天前
|
Java 开发者
软件工程设计原理接口隔离原则 ,具体实现及JAVA代码举例
【4月更文挑战第7天】接口隔离原则(Interface Segregation Principle, ISP)是面向对象设计原则之一,旨在减少不必要的依赖关系,通过拆分庞大且臃肿的接口为更小、更具体的接口来实现。这个原则强调“客户端不应该被迫依赖于它不使用的接口”,意味着一个类不应该被迫实现它不使用的方法。
16 1
|
16天前
|
Java
软件工程设计原理依赖倒置原则 ,具体实现及JAVA代码举例
【4月更文挑战第5天】在软件工程中,依赖倒置原则(Dependency Inversion Principle, DIP)是一项重要的设计原则,它是SOLID原则中的一个组成部分。这个原则主张高层模块不应该依赖于低层模块,而是应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。这种设计方法有助于降低代码间的耦合度,增强系统的灵活性和可维护性
20 0
|
17天前
|
Java 关系型数据库
软件工程设计原理开放封闭原则 ,具体实现及JAVA代码举例
【4月更文挑战第4天】开放封闭原则(Open/Closed Principle, OCP)是面向对象设计的核心原则之一,它指出软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着在不修改已有代码的前提下,可以通过扩展来增加新的功能,从而提高软件系统的灵活性和可维护性
17 1
|
24天前
|
算法 IDE Java
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
39 0
|
25天前
|
Java API 开发工具
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用(三)
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用
30 0

热门文章

最新文章