当前位置:天才代写 > tutorial > JAVA 教程 > Java二进制兼容性道理

Java二进制兼容性道理

2017-11-11 08:00 星期六 所属: JAVA 教程 浏览:628

副标题#e#

一、概述

此刻的软件越来越依赖于差异厂商、作者开拓的共享组件,组件打点也变得越来越重要。在这方面,一个极其重要的问题是类的差异版本的二进制兼容性,即一个类改变时,新版的类是否可以直接替换本来的类,却不至于损坏其他由差异厂商/作者开拓的依赖于该类的组件?

Java二进制兼容性观念的主要方针是敦促Internet上软件的遍及重用,同时它还制止了大大都C++情况面对的基本类懦弱性问题——譬喻,在C++中,对域(数据成员或实例变量)的会见被编译成相对付工具起始位置的偏移量,在编译时就确定,假如类插手了新的域并从头编译,偏移量随之改变,原先编译的利用老版本类的代码就不能正常执行;虚拟要领挪用也存在同样的问题。

C++情况凡是回收从头编译所有引用了被修改类的代码来办理问题。在Java中,少量开拓情况也回收了同样的计策,但这种计策存在诸多限制。譬喻,假设有人开拓了一个措施P,P引用了一个外部的库L1,但P的作者没有L1的源代码;L1要用到另一个库L2。此刻L2改变了,但L1无法从头编译,所以P的开拓和变动也受到了限制。

为此,Java引入了二进制兼容的观念——假如对L2的变动是二进制兼容的,那么变动后的L2、本来的L1和此刻的P可以或许顺利毗连,不会呈现任何错误。

首先来看一个简朴的例子。Authorization和Hello种别离来自两个差异的作者,Authorization提供身份验证和授权处事,Hello类要挪用Authorization类。

package com.author1;
public class Authorization {
 public boolean authorized(String userName) {
  return true;
 }
}
package com.author2;
import com.author1.*;
class Hello {
 public static void main(String arg[]) {
  Authorization auth = new Authorization();
  if(auth.authorized("MyName"))
   System.out.println("您已经通过验证");
  else
   System.out.println("您未能通过身份验证");
 }
}

此刻author1宣布了Authorization类的2.0版,Hello类的作者author2但愿在不变动原有Hello类的环境下利用新版的Authorization类。2.0版的Authorization要比本来的巨大不少:

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

作者author1理睬2.0版的Authorization类与1.0版的类二进制兼容,可能说,2.0版的Authorization类仍旧满意1.0版的Authorization类与Hello类的约定。显然,author2编译Hello类时,无论利用Authorization类的哪一个版本都不会堕落——实际上,假如仅仅是因为Authorization类进级,Hello类基础无需从头编译,同一个Hello.class可以挪用任意一个Authorization.class。

这一特性并非Java独占。UNIX系统很早就有了共享工具库(.so文件)的观念,Windows系统也有动态链接库(.dll文件)的观念,只要替换一下文件就可以将一个库换取为另一个库。就象Java的二进制兼容特性一样,名称的毗连是在运行时完成,而不是在代码的编译、毗连阶段完成,而因它也同样拥有Java二进制兼容性所具有的利益,譬喻修改代码时只需从头编译一个库,便于对措施的某一部门举办修改。可是,Java的二进制兼容性尚有其奇特的优势:

⑴ Java将二进制兼容性的粒度从整个库(大概包括数十、数百个类)细化到了单个的类。

⑵ 在C/C++之类的语言中,建设共享库凡是是一种有意识的行为,一个应用软件一般不会提供许多共享库,哪些代码可以共享、哪些代码不行共享都是预先筹划的功效。但在Java中,二进制兼容酿成了一种与生俱来的天然特性。

⑶ 共享工具只针对函数名称,但Java二进制兼容性思量到了重载、函数签名、返回值范例。

⑷ Java提供了更完善的错误节制机制,版本不兼容会触发异常,但可以利便地捕捉和处理惩罚。对比之下,在C/C++中,共享库版本不兼容往往引起严重问题。

二、类和工具的兼容性

二进制兼容的观念在某些方面与工具串行化的观念相似,两者的方针也有必然的重叠。串行化一个Java工具时,类的名称、域的名称被写入到一个二进制输出流,串行化到磁盘的工具可以用类的差异版原来读取,前提是该类要求的名称、域都存在,且范例一致。下表较量了二进制兼容和串行化这两个观念。

  工具串行化 二进制兼容
合用于 工具
兼容要求 类,域 类,域,要领
删除操纵导致不兼容 老是 不必然
修改会见属性(public,private等)后是否兼容

#p#分页标题#e#

二进制兼容和串行化都思量到了类的版本不绝更新的问题,答允为类插手要领和域,并且纯粹的插手不会影响措施的语义;雷同地,纯真的布局修改,譬喻从头分列域或要领,也不会引起任何问题。


#p#副标题#e#

三、延迟绑定

领略二进制兼容的要害是要领略延迟绑定(Late Binding)。延迟绑定是指Java直到运行时才查抄类、域、要领的名称,而不象C/C++的编译器那样在编译期间就排除了类、域、要领的名称,代之以偏移量数值——这是Java二进制兼容得以发挥浸染的要害。

由于回收了延迟绑定技能,要领、域、类的名称直到运行时才理会,意味着只要域、要领等的名称(以及范例)一样,类的主体可以任意替换——虽然,这是一种简化的说法,尚有其他一些法则制约Java类的二进制兼容性,譬喻会见属性(private、public等)以及是否为abstract(假如一个要领是抽象的,那么它必定是不行直接挪用的)等,但延迟绑定机制无疑是二进制兼容的焦点地址。

只有把握了二进制兼容的法则,才气在改写类的时候担保其他类不受到影响。下面再来看一个例子,FrodoMail和SamMail是两个Email措施:

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

假如我们从头实现Message,不再让它实现Classifiable接口,SamMail仍能正常运行,但FrodoMail会抛出异常:java.lang.IncompatibleClassChangeError at FrodoMail.main。这是因为SamMail不要求EmailMessage是一个Classifiable,但FrodoMail却要求EmailMessage是一个Classifiable,编译FrodoMail获得的二进制.class文件引用了Classifiable这个接口名称。切合Classifiable接口界说的要领仍旧存在,但该类却基础没有提到Classifiable这个接口。

四、兼容法则:要领

从二进制兼容的角度来看,一个要领由四部门组成,别离是:要领的名称,返回值范例,参数,要领是否为static。改变这四个项目中的任意一个,对JVM而言它已经酿成了另一个要领。

以“boolean isValid()”要领为例,假如让isValid吸收一个Date参数,酿成“boolean isValid(Date when)”,修改后的类不能直接替换原有的类,试图会见新类的isValid()要领只能获得雷同下面的错误信息:java.lang.NoSuchMethodError: Ticket.isValid()Z。JVM用“()Z”这个标记暗示要领不接管参数且返回一个boolean。关于这一问题,下文将有更具体的说明。

JVM操作一种称为虚拟要领调治(Virtual Method Dispatch)的技能判定要挪用的要领体,它按照被挪用要领地址的实际实例来抉择要利用的要领体,可以看作一种扩展的延迟绑定计策。

假如该类没有提供一个名称、参数、返回值范例完全匹配的要领,它就利用从超类担任的要领。由于Java的二进制兼容性法则,这种担任实际上在运行期间确定,而不是在编译期间确定。假设有下面几个类:

class Poem {
 void perform() {
  System.out.println("白昼依山尽");
 } }
class ShakespearePoem extends Poem {
 void perform() {
  System.out.println("To be or not to be.");
 } }
class Hamlet extends ShakespearePoem { }

那么,

Poem poem = new Hamlet();
poem.perform();

将输出“To be or not to be.”。这是因为perform的要领体是运行时才确定的。固然Hamlet没有提供perform的要领体,但它从ShakespearePoem担任了一个。至于为何不消Poem界说的perform要领,那是因为ShakespearePoem界说的perform已经包围了它。我们可以随时修改Hamlet,却无需从头编译ShakespearePoem,如下例所示:

class Hamlet extends ShakespearePoem {
 System.out.println("连一支耗子都没闹");
}

此刻,前面的例子将输出“连一支耗子都没闹”。可是,

Poem poem = new ShakespearePoem();
poem.perform();

这段代码的输出功效是“To be or not to be.”假如我们删除ShakespearePoem的内容,同样的代码将输出“白昼依山尽”。

#p#副标题#e#

五、兼容法则:域

#p#分页标题#e#

域和要领差异。删除了类的一个要领后,它有大概通过担任得到一个具有同样名称、参数的差异要领,但域不能包围,这使得域在二进制兼容方面的表示也有所差异。

譬喻,假设有下面三个类:

class Language {
 String greeting = "你好";
}
class German extends Language {
 String greeting = "Guten tag";
}
class French extends Language {
 String greeting = "Bon jour";
}

则“void test1() { System.out.println(new French().greeting); }”的输出功效是“Bon jour”,可是,“void test2() { System.out.println(((Language) new French()).greeting); }”的输出功效是“你好”。这是因为,实际会见的域依赖于实例的范例。在第一个输出例子中,test1会见的是一个French工具,所以输出功效是French的问候语;但在第二个例子中,固然实际上会见的是一个French工具,但由于French工具已经被定型成Language工具,所以输出功效是Language的问候语。

假如把上例的Language改成下面的形式:

class Language { }

再次运行test2(不从头编译),获得的功效是一个错误信息:java.lang.NoSuchFieldError: greeting。假如从头编译test2,则呈现编译错误:cannot resolve symbol,symbol : variable greeting ,location: class Language System.out.println(((Language) new French()).greeting);。test1仍能正常运行,无需从头编译,因为它不需要Language包括的greeting变量。

六、深入领略延迟绑定

下面几个类用于确定本日晚餐要喝的酒以及酒的温度。

class Sommelier {
 Wine recommend(String meal) { ... }
}
abstract class Wine {
 // 推荐酒的温度
 abstract float temperature();
}
class RedWine extends Wine {
 // 红酒的温度凡是略高于白酒
 float temperature() { return 63; }
}
class WhiteWine extends Wine {
 float temperature() { return 47; }
}
class Bordeaux extends RedWine {
 float temperature() { return 64; }
}
class Riesling extends WhiteWine {
 // 担任WhiteWine类的温度
}

#p#副标题#e#

下面的例子操作上面的类推荐一种酒:

void example1() {
 Wine wine = sommelier.recommend("duck");
 float temp = wine.temperature();
}

example1的第二个挪用中,对付wine工具我们独一可以必定的是它是一个Wine,但可以是Bordeaux,也可以是Riesling或其他。别的,我们可以必定wine工具不行能是Wine类自己的实例,因为Wine类是一个抽象类。编译源代码,源代码中的wine.temperature()挪用将酿成“invokevirtual Wine/temperature ()F”(class文件实际包括的是该文本暗示形式的二进制代码,这种文本化的指令描写要领称为Oolong要领),它暗示的是一个要领挪用——一个普通的(虚拟)要领挪用,而不是一个静态挪用。它挪用的要领是Wine工具的temperature,右边的“()F”参数称为签名(signature),“()F”这个签名中的空括号暗示要领不需要输入参数,F暗示返回值是一个浮点数。

JVM执行到该语句时,它挪用的不必然是Wine界说的temperature要领。实际上,在本例中,JVM不行能挪用Wine界说的temperature要领,因为该temperature要领是一个虚拟要领。JVM首先查抄该工具所属的类,寻找一个切合invokevirtual语句指定的名称、签名特征的要领,假如找不到,则查抄该类的超类,然后是超类的超类,直至找到一个符合的要领实现为止。

在本例中,假如实际建设的工具是一个Bordeaux,则JVM挪用Bordeaux类界说的temperature()F,该temperature()F要领将返回64。假如工具是一个Riesling,JVM在Riesling类中找不到适当的要领,所以继承查找WhiteWine类,在WhiteWine类中找到了一个符合的temperature()F要领,该要领的返回值是47。

#p#分页标题#e#

因此,查找可用要领的进程就是沿着类的担任树通过字符串匹配寻找符合要领的进程。相识这一道理有助于领略哪些修改不至于影响二进制兼容性。

首先,从头分列类内里的要领显然不会影响到二进制兼容性——这在C++措施中一般是不答允的,因为C++措施操作数值性偏移量而不是名称来确定要挪用的要领。延迟绑定的要害优势正是在此,假如Java也利用要领在类内里的偏移量来确定要挪用的要领,一定极大地限制二进制兼容机制的发挥,纵然极小的窜改也大概导致大量的代码需要从头编译。

● 说明:也许有人会认为C++的处理惩罚方法要比Java的快,来由是按照数值性偏移量寻找要领必定要比字符串匹配快。这种说法有必然原理,但只说明白类方才装入时的环境,从此Java的JIT编译器处理惩罚的也是数值性偏移量,而不再靠字符串匹配的步伐寻找要领,因为类装入内存之后不行能再改变,所以这时的JIT编译器基础无须记挂到二进制兼容问题。因此,至少在要领挪用这一点上,Java没有来由必然比C++慢。

其次,尚有很重要的一点是:不只仅编译时需要查抄类的担任干系,并且运行时JVM还要查抄类的担任干系。

七、重载与包围

通过前面的例子该当把握的最重要的一点是:要领匹配的依据是要领的名字和签名的文本描写。下面我们为Sommelier类插手一些有关羽觞的要领:

Glass fetchGlass(Wine wine) { ... }
Glass fetchGlass(RedWine wine) { ... }
Glass fetchGlass(WhiteWine wine) { ... }

再来编译下面的代码:

void example2() {
 Glass glass;
 Wine wine = sommelier.recommend("duck");
 if(wine instanceof Bordeaux)
  glass = sommelier.fetchGlass((Bordeaux) wine);
 else
  glass = sommelier.fetchGlass(wine);
}

这里有两个fetchGlass挪用:第一个挪用的参数是一个Bordeaux工具,第二个挪用的参数是一个Wine工具。Java编译器为这两行代码生成的指令别离是:

invokevirtual Sommelier/fetchGlass (LRedWine;)LGlass;
invokeVirtual Sommelier/fetchGlass (LWine;)LGlass;

#p#副标题#e#

留意这两者的区别是编译时确定的,而不是运行时确定的。JVM用“L<类名称>”这个标记暗示一个类(就象前面例子中F的浸染一样),这两个要领挪用的输入参数是一个Wine或RedWine,返回值是一个Glass。

Sommelier类没有提供输入参数是Bordeaux的要领,但有一个要领的输入参数是RedWine,所以第一个挪用的要领签名就用了输入参数是RedWine的要领。至于第二个挪用,编译时只知道参数是一个Wine工具,所以编译后的指令利用了输入参数是Wine工具的要领。对付第二个挪用,纵然sommelier推荐的是一个Riesling工具,实际挪用的也不会是fetchGlass(whiteWine),而是fetchGlass(wine),原因也一样,被挪用的要领老是一个签名完全匹配的要领。

在这个例子中,fetchGlass要领的差异界说是重载(Overload)干系,而不是包围(Override)干系,因为这些fetchGlass要领的签名互不沟通。假如一个要领要包围另一个要领,那么两者必需有沟通的参数和返回值范例。虚拟要领挪用是在运行时查找特定的范例,只针对包围的要领(拥有沟通的签名),而不是针对重载的要领(拥有差异的签名)。重载要领的理会在编译时完成,包围要领的理会则在运行时举办。

假如删除fetchGlass(RedWine),不从头编译,再运行example2,JVM将提示错误信息:java.lang.NoSuchMethodError: Sommelier.fetchGlass (LRedWine;)LGlass;。

可是,删除该要领之后,编译example2仍旧可以顺利通过,不外这时两个sommelier.fetchGlass挪用将生成同样的invokevirtual指令,即:invokevirtual Sommelier/fetchGlass (LWine;)LGlass;。

假如再次放回fetchGlass(RedWine)要领,除非从头编译example2,不然fetchGlass(RedWine)不会被挪用,JVM将利用fetchGlass(wine)。当传入的工具是一个Riesling时,由于同样的原因,它也不会利用fetchGlass(WhiteWine):因为编译时基础不能确定详细的工具。,所以选用了一个更一般化的要领。

在“invokevirtual Wine/temperature ()F”这个指令中,JVM没有严格僵持利用Wine工具,而是自动寻找实际实现了temperature要领的工具;但在“invokevirtual Sommelier/fetchGlass (LRedWine;)LGlass;”指令中,JVM却很在乎RedWine。这是为什么呢?因为第一个指令中,Wine不属于要领签名,只是用于挪用之前的范例查抄;而在第二个指令中,RedWine属于要领签名的一部门,JVM必需按照要领签名和要领名称来寻找要挪用的要领。

假设我们为Sommelier类插手了一个fetchGlass要领:

class RedWineGlass extends Glass { ... }
RedWineGlass fetchGlass(RedWine wine) { ... }

#p#分页标题#e#

再来看本来编译的example2,它用“invokevirtual Sommelier/fetchGlass (LRedWine;)LGlass;”指令挪用fetchGlass要领。新插手的要领不会自动起浸染,因为RedWineGlass和Glass是两种差异的范例。可是,假如我们从头编译example2,挪用Bordeaux的例子将酿成“invokevirtual Sommelier/fetchGlass (LRedWine;)LRedWineGlass;”。

综上所述,我们可以总结出如下Java二进制兼容性的重要原则:

⑴ 编译时,Java编译器选择最匹配的要领签名。

⑵ 运行时,JVM查找准确匹配的要领名称和签名。相似的名称和签名将被忽略。

⑶ 假如找不到适当的要领,JVM抛出异常,且不装入指定的类。

⑷ 重载的要领在编译时处理惩罚,包围的要领在运行时处理惩罚。

 

    关键字:

天才代写-代写联系方式