为了实现接口,类可以定义显式接口成员执行体(Explicit interface member
implementations)。显式接口成员执行体可以是一个方法、一个属性、一个事件或者是一个索引指示器的定义,定义与该成员对应的全权名应保持一致。
using System ; interface ICloneable { object Clone( ) ; } interface IComparable { int CompareTo(object other) ; } class ListEntry: ICloneable, IComparable { object ICloneable.Clone( ) {…} int IComparable.CompareTo(object other) {…} } |
上面的代码中ICloneable.Clone 和IComparable.CompareTo 就是显式接口成员执行体。
说明:
· 不能在方法调用、属性访问以及索引指示器访问中通过全权名访问显式接口成员执行体。事实上,显式接口成员执行体只能通过接口的实例,仅仅引用接口的成员名称来访问。
· 显式接口成员执行体不能使用任何访问限制符,也不能加上abstract, virtual, override或static 修饰符。
· 显式接口成员执行体和其他成员有着不同的访问方式。因为不能在方法调用、属性访问以及索引指示器访问中通过全权名访问,显式接口成员执行体在某种意义上是私有的。但它们又可以通过接口的实例访问,也具有一定的公有性质。
· 只有类在定义时,把接口名写在了基类列表中,而且类中定义的全权名、类型和返回类型都与显式接口成员执行体完全一致时,显式接口成员执行体才是有效的,例如:
class Shape: ICloneable { object ICloneable.Clone( ) {…} int IComparable.CompareTo(object other) {…} } |
使用显式接口成员执行体通常有两个目的:
· 因为显式接口成员执行体不能通过类的实例进行访问,这就可以从公有接口中把接口的实现部分单独分离开。如果一个类只在内部使用该接口,而类的使用者不会直接使用到该接口,这种显式接口成员执行体就可以起到作用。
· 显式接口成员执行体避免了接口成员之间因为同名而发生混淆。如果一个类希望对名称和返回类型相同的接口成员采用不同的实现方式,这就必须要使用到显式接口成员执行体。如果没有显式接口成员执行体,那么对于名称和返回类型不同的接口成员,类也无法进行实现。
下面的定义是无效的,因为Shape 定义时基类列表中没有出现接口IComparable。
class Shape: ICloneable { object ICloneable.Clone( ) {…} } class Ellipse: Shape { object ICloneable.Clone( ) {…} } |
在Ellipse中定义ICloneable.Clone是错误的,因为Ellipse即使隐式地实现了接口ICloneable,ICloneable仍然没有显式地出现在Ellipse定义的基类列表中。
接口成员的全权名必须对应在接口中定义的成员。如下面的例子中,Paint的显式接口成员执行体必须写成IControl.Paint。
using System ; interface IControl { void Paint( ) ; } interface ITextBox: IControl { void SetText(string text) ; } class TextBox: ITextBox { void IControl.Paint( ) {…} void ITextBox.SetText(string text) {…} } |
实现接口的类可以显式实现该接口的成员。当显式实现某成员时,不能通过类实例访问该成员,而只能通过该接口的实例访问该成员。显式接口实现还允许程序员继承共享相同成员名的两个接口,并为每个接口成员提供一个单独的实现。
下面例子中同时以公制单位和英制单位显示框的尺寸。Box类继承IEnglishDimensions和IMetricDimensions两个接口,它们表示不同的度量衡系统。两个接口有相同的成员名 Length 和 Width。
程序清单1 DemonInterface.cs interface IEnglishDimensions { float Length ( ) ; float Width ( ) ; } interface IMetricDimensions { float Length ( ) ; float Width ( ) ; } class Box : IEnglishDimensions, IMetricDimensions { float lengthInches ; float widthInches ; public Box(float length, float width) { lengthInches = length ; widthInches = width ; } float IEnglishDimensions.Length( ) { return lengthInches ; } float IEnglishDimensions.Width( ) { return widthInches ; } float IMetricDimensions.Length( ) { return lengthInches * 2.54f ; } float IMetricDimensions.Width( ) { return widthInches * 2.54f ; } public static void Main( ) { //定义一个实类对象 "myBox":: Box myBox = new Box(30.0f, 20.0f); // 定义一个接口" eDimensions":: IEnglishDimensions eDimensions = (IEnglishDimensions) myBox; IMetricDimensions mDimensions = (IMetricDimensions) myBox; // 输出: System.Console.WriteLine(" Length(in): {0}", eDimensions.Length( )); System.Console.WriteLine(" Width (in): {0}", eDimensions.Width( )); System.Console.WriteLine(" Length(cm): {0}", mDimensions.Length( )); System.Console.WriteLine(" Width (cm): {0}", mDimensions.Width( )); } } |
输出:Length(in): 30,Width (in): 20,Length(cm): 76.2,Width (cm): 50.8
代码讨论:如果希望默认度量采用英制单位,请正常实现 Length 和 Width 这两个方法,并从 IMetricDimensions 接口显式实现 Length 和 Width 方法:
public float Length( ) { return lengthInches ; } public float Width( ){ return widthInches; } float IMetricDimensions.Length( ) { return lengthInches * 2.54f ; } float IMetricDimensions.Width( ) { return widthInches * 2.54f ; } |
这种情况下,可以从类实例访问英制单位,而从接口实例访问公制单位:
System.Console.WriteLine("Length(in): {0}", myBox.Length( )) ; System.Console.WriteLine("Width (in): {0}", myBox.Width( )) ; System.Console.WriteLine("Length(cm): {0}", mDimensions.Length( )) ; System.Console.WriteLine("Width (cm): {0}", mDimensions.Width( )) ; |
接口具有不变性,但这并不意味着接口不再发展。类似于类的继承性,接口也可以继承和发展。
注意:接口继承和类继承不同,首先,类继承不仅是说明继承,而且也是实现继承;而接口继承只是说明继承。也就是说,派生类可以继承基类的方法实现,而派生的接口只继承了父接口的成员方法说明,而没有继承父接口的实现,其次,C#中类继承只允许单继承,但是接口继承允许多继承,一个子接口可以有多个父接口。
接口可以从零或多个接口中继承。从多个接口中继承时,用":"后跟被继承的接口名字,多个接口名之间用","分割。被继承的接口应该是可以访问得到的,比如从private类型或internal类型的接口中继承就是不允许的。接口不允许直接或间接地从自身继承。和类的继承相似,接口的继承也形成接口之间的层次结构。
请看下面的例子:
using System ; interface IControl { void Paint( ) ; } interface ITextBox: IControl { void SetText(string text) ; } interface IListBox: IControl { void SetItems(string[] items) ; } interface IComboBox: ITextBox, IListBox { } |
对一个接口的继承也就继承了接口的所有成员,上面的例子中接口ITextBox和IListBox都从接口IControl中继承,也就继承了接口IControl的Paint方法。接口IComboBox从接口ITextBox和IListBox中继承,因此它应该继承了接口ITextBox的SetText方法和IListBox的SetItems方法,还有IControl的Paint方法。
一个类继承了所有被它的基本类提供的接口实现程序。
不通过显式的实现一个接口,一个派生类不能用任何方法改变它从它的基本类继承的接口映射。例如,在声明中:
interface IControl { void Paint( ); } class Control: IControl { public void Paint( ) {...} } class TextBox: Control { new public void Paint( ) {...} } |
TextBox中的方法Paint隐藏了Control中的方法Paint,但是没有改变从Control.Paint到IControl.Paint 的映射,而通过类实例和接口实例调用Paint将会有下面的影响。
Control c = new Control( ) ; TextBox t = new TextBox( ) ; IControl ic = c ; IControl it = t ; c.Paint( ) ; // 影响Control.Paint( ) ; t.Paint( ) ; // 影响TextBox.Paint( ) ; ic.Paint( ) ; // 影响Control.Paint( ) ; it.Paint( ) ; // 影响Control.Paint( ) ; |
但是,当一个接口方法被映射到一个类中的虚拟方法,派生类就不可能覆盖这个虚拟方法并且改变接口的实现函数。例如,把上面的声明重新写为:
interface IControl { void Paint( ) ; } class Control: IControl { public virtual void Paint( ) {...} } class TextBox: Control { public override void Paint( ) {...} } |
就会看到下面的结果:
Control c = new Control( ) ; TextBox t = new TextBox( ) ; IControl ic = c ; IControl it = t ; c.Paint( ) ; // 影响Control.Paint( ); t.Paint( ) ; // 影响TextBox.Paint( ); ic.Paint( ) ; // 影响Control.Paint( ); it.Paint( ) ; // 影响TextBox.Paint( ); |
由于显式接口成员实现程序不能被声明为虚拟的,就不可能覆盖一个显式接口成员实现程序。一个显式接口成员实现程序调用另外一个方法是有效的,而另外的那个方法可以被声明为虚拟的以便让派生类可以覆盖它。例如:
interface IControl { void Paint( ) ; } class Control: IControl { void IControl.Paint( ) { PaintControl( ); } protected virtual void PaintControl( ) {...} } class TextBox: Control { protected override void PaintControl( ) {...} } |
这里,从Control继承的类可以通过覆盖方法PaintControl来对IControl.Paint的实现程序进行特殊化。
使用getResourceAsStream()时,必须给定一个URL,由于我们要存取JAR内部的资源,如果使用「/」作为开头,代表绝对路径,「/」代表JAR文件之中的根目录。如果没有使用「/」,则视为相对路径,要看调用getResourceAsStream()类别的所在路径而定,这样容易造成混淆。所以请尽量使用「/」作为开头的绝对路径。
如果曾经撰写过Java Applet或Java Servlet,就知道制作Java Applet,必须继承自java.applet.Applet这个类别;制作Java Servlet,则程序必须继承自javax.servlet. http.HttpServlet这个类别。同理,要撰写能够在手机上执行的Java MIDlet必须要继承自javax.microedition.midlet. MIDlet类别,如图8所示。
javax.microedition.midlet.MIDlet类别中定义了三个抽象方法,它们分别是:startApp() ==> 至运作状态;pauseApp() ==> 至停止状态;destoryApp() ==> 至消灭状态。应用程序管理员就是透过这三个抽象方法来控制MIDlet的生命周期。因此,所有的MIDlet都必须实现这三个方法,才保证能正常运作。因此,一个MIDlet的程序外壳至少要如下:
import javax.microedition.midlet.*; public class HelloMIDlet extends MIDlet { public HelloMIDlet() { //建构式 } public void startApp() { } public void pauseApp() { } public void destroyApp(boolean unconditional) { } } |
根据MIDP规格,MIDlet中不应该有“public static void main(Straing[] args)”这个方法。如果有则应用程序管理员会忽略不管。
一旦MIDlet被JAM载入之后,首先会先呼叫MIDlet中没有参数的建构式以进行初始化的工作。如果熟悉Java语法一定知道Java语言的一些特性,就是如果没有在程序中加入任何建构式,编译器会自动帮助加入一个预设建构式。但是如果已经撰写了自己的建构式(有任何参数),那么编译器将不会自动帮助加上预设建构式。因此,如果有必要撰写有参数的建构式,别忘了再手动加上一个没有参数的建构式,否则MIDlet将无法正确地初始化。
当MIDlet被应用程序管理员产生之后,MIDlet就开始运作,程序设计师应该做的事情如图9所示。
我们会使用Display.getDisplay(this)来取得代表该MIDlet显示屏的Display对象。从应用程序管理员呼叫startApp()到MIDlet结束运作这段时间之内,不管何时呼叫Display.getDisplay(this),取得的都是同一份Display对象的参考。
要设定显示在屏幕上的画面,会使用Display对象的参考,并调用其setCurrent()方法,即display.setCurrent( Displayable类别的子类别实体)。因此一个可以运作的MIDlet程序,至少如HelloMIDlet.java,代码为:
import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class HelloMIDlet extends MIDlet { private Display display; public HelloMIDlet() { display = Display.getDisplay(this); } public void startApp() { Form t = new Form("画面"); display.setCurrent(t); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } } |
注意:根据规格MIDlet只能由应用程序管理员产生,我们不能自己在程序里生成其它的MIDlet,并呼叫其生命周期函数,这样做将引发SecurityException异常。
前面简单地叙述了应该如何撰写一个MIDlet的轮廓。但事实上,MIDlet的运作稍微复杂一点。所以接下来要仔细探讨MIDlet的运作细节,也就是MIDlet的生命周期。
当MIDlet被应用程序管理员成功地初始化之后,就开始展开了它的生命周期。图10描述了一个MIDlet的生命周期。MIDlet的生命周期完全由应用程序管理员控制,也就是说,当MIDlet要从一个状态变成另外一个状态时,应用程序管理员会呼叫对应的回呼函数(Call Back,也就是MIDlet类别定义的那三个抽象方法)。MIDlet基本上有三种状态,分别是停止状态(Paused)、启动状态(Active)及毁灭状态(Destroyed)。MIDlet开始时一定是先进入停止状态,然后应用程序管理员再将它转换成启动状态,然后调用startApp(),见图10。只有当应用程序管理员认为MIDlet的状态必须改变时,才会呼叫图中的相关函数。