第 8 章纯粹抽象类别与接口 173
174 Android 的对象技术 (OOP) Android 的對象技術教材下載
第 8 章纯粹抽象类别与接口 175 第 8 章 纯粹抽象类别 与接口 (Interface) 8.1 认识 纯粹抽象类别 8.2 从 纯粹抽象类别 到 接口 8.3 Android 的接口之例 8.4 应用于 C/C++
176 Android 的对象技术 (OOP) 8.1 认识 纯粹抽象类别 在前面各章里, 介绍过抽象类别 (Abstract Class) 当我们从具体类别抽掉某些函数的实作 (Body) 部分, 这些函数就称为抽象函数 (Abstract Function), 而含有抽象函数的类别就称为抽象类别 (Abstract Class) 了 一个抽象类别, 常同时含有抽象函数和具象函数 如果抽象类别里的 所有 (All) 函数的实作部份都欠缺, 就称为纯粹抽象类别 (Pure Abstract Class) 换句话说, 所谓纯粹抽象类别, 就是该类别里的每一个函数都是抽象函数 这种函数就是空的函数, 只有定义而无实作指令 由于纯粹抽象函数扮演着对象接口 (Interface) 的角色, 在现代软件语言上, 具有特殊的意义 在 Java 里, 有两个机制可用来表示接口 : 1. 以纯粹抽象类别 (Pure Abstract Class) 表达之 2. 以 Interface 机制表达之 在 1996 年之前, 主流语言 ( 如 C++ 等 ) 都只提供继承 (Inheritance) 机制, 而无 Interface 机制, 所以当时只能用 纯粹抽象类别 (C++ 称之为纯粹虚拟类别 ) 来表达接口 自从 1996 年 Java 问世之后, 主要语言如 Java C# VB.NET 等都提供了 Interface 机制来表达接口 请看看 Coin 多态性对象的范例程序 : 其完整程序码如下 :
第 8 章纯粹抽象类别与接口 177 /* AbstractCoin.java */ package Coin; public abstract class AbstractCoin { public abstract double value(); /* Coin.java */ package Coin; public abstract class Coin extends AbstractCoin { @Override public double value(){ return onvalue(); protected abstract double onvalue(); /* one_dollar.java */ package Coin; public class one_dollar extends Coin { @Override protected double onvalue(){ return 1.0; /* five_dollar.java */ package Coin; public class five_dollar extends Coin { @Override protected double onvalue(){ return 5.0; /* ten_dollar.java */ package Coin; public class ten_dollar extends Coin { @Override protected double onvalue(){ return 10.0;
178 Android 的对象技术 (OOP) /* VendingMachine.java */ package Machine; import java.util.arraylist; import java.util.iterator; import Coin.AbstractCoin; public class VendingMachine { private ArrayList<AbstractCoin> coll = null; public VendingMachine() { coll = new ArrayList<AbstractCoin>(); public void feedcoin(abstractcoin c){ coll.add(c); public void showamount(){ Iterator<AbstractCoin> it; double sum = 0; for(it = coll.iterator(); it.hasnext();) { sum += ((AbstractCoin)it.next()).value(); System.out.println("amt: " + sum); /* JMain.java */ import Coin.*; import Machine.VendingMachine; public class JMain { public static void main(string[] args) { VendingMachine vm = new VendingMachine(); vm.feedcoin(new five_dollar()); vm.feedcoin(new one_dollar()); vm.feedcoin(new ten_dollar()); vm.showamount(); 在这个 Coin 抽样类别里, 含有一个 value() 具体函数和一个 onvalue() 抽象函
第 8 章纯粹抽象类别与接口 179 数 这个 Coin 类别只是一般抽象类别, 而不是一个纯粹抽象类别 当我们也将这个 value() 具象函数里的实作指令部分抽掉, 而独立出上述的 AbstractCoin 抽象类别 这个 AbstractCoin 抽象类别所拥有的函数皆是 ( 虽然只有一个函数 ) 抽象函数, 所以称它为纯粹抽象类别 (Pure Abstract Class) 8.2 从 纯粹抽象类别 到 接口 许多人谈到接口时, 会觉得它是蛮抽象 不易捉摸的 当我们理解到 : 纯粹抽象类别是从抽象类别 (Abstraction) 抽象共同之行为 这有助于解答了 : 接口从哪里来 的问题 就如上一节里的例子, 只要对 Coin 抽象类别进行 行为抽象 ( 即抽出函数定义 ), 就能得出一个 AbstractCoin 纯粹抽象类别, 也就得到接口了 兹看一个稍为复杂一点的范例, 其中含有一个 Graph 纯粹抽象类别, 如下图 : Graph abstract void draw(...); abstract void paint(...); Bird ( 繼承 ) 图 8-1 void draw(...) { // 畫圖指令 void paint(...) { draw(); 纯粹抽象类别及其子类 这个 Graph 就是纯粹抽象类别 在抽象类别里, 如果所有函数皆未含有实作
180 Android 的对象技术 (OOP) 而只有函数原型 (Prototype) 宣告的话, 就称为纯粹抽象类别 现在就来看看 Java 如何表示 Graph 纯粹抽象类别, 如下范例程序 : 其完整程序码如下 : /* Graph.java */ public abstract class Graph { public abstract void draw(); public abstract void paint(); /* Bird.java */ import java.awt.*; public class Bird extends Graph { Graphics m_gr; public Bird(Graphics gr) { super(); m_gr = gr; @Override public void draw(){ // 画图 ( 海鸥 ) 指令 m_gr.setcolor(color.blue); m_gr.drawarc(30,80,90,110,40,100); m_gr.drawarc(88,93,90,100,40,80); m_gr.drawarc(30,55,90,150,35,75); m_gr.drawarc(90,80,90,90,40,80); @Override public void paint(){ this.draw(); /* JMain.java */ import java.awt.*;
第 8 章纯粹抽象类别与接口 181 import javax.swing.*; class JP extends JPanel { public void paintcomponent(graphics gr) { super.paintcomponents(gr); Graph cc = new Bird(gr); cc.paint(); public class JMain extends JFrame { public JMain() { settitle(""); setsize(350, 250); public static void main(string[] args) { JMain frm = new JMain(); JP panel = new JP(); frm.add(panel); frm.setdefaultcloseoperation(jframe.exit_on_close); frm.setvisible(true); 此程序画出两只海鸥 : 上述的 Graph 是纯粹抽象类别, 只有函数的原型 (Prototype) 定义而已, 并没有函数的实作内容 (Implementation), 其就扮演接口的角色了 所以上图 8-1 的意义与下图 8-2 是相同的
182 Android 的对象技术 (OOP) <<Interface>> IGraph void draw(...); void paint(...); Bird ( 實作 ) void draw(...) { // 畫圖指令 void paint(...) { draw(); 图 8-2 接口及其实作类别 这已经不是继承关系了, 而是接口与类别之间的 实作关系 了 那么, 在 Java 程序里, 应该如何表达上图的实作关系呢? 可以使用 Java 的接口 (Interface) 机制来表达之, 而且是很直截了当的 如下述的范例程序 : 其程序码如下 : /* IGraph.java */ interface IGraph { void draw();
第 8 章纯粹抽象类别与接口 183 void paint(); /* Bird.java */ import java.awt.*; public class Bird implements IGraph { Graphics m_gr; public Bird(Graphics gr) { super(); m_gr = gr; public void draw(){ // 画图 ( 海鸥 ) 指令 m_gr.setcolor(color.blue); m_gr.drawarc(30,80,90,110,40,100); m_gr.setcolor(color.black); m_gr.drawarc(30,55,90,150,35,75); public void paint(){ this.draw(); m_gr.drawarc(88,93,90,100,40,80); m_gr.drawarc(90,80,90,90,40,80); /* JMain.java */ import java.awt.*; import javax.swing.*; class JP extends JPanel { public void paintcomponent(graphics gr) { super.paintcomponents(gr); IGraph bird = new Bird(gr); bird.paint(); public class JMain extends JFrame { public JMain(){ settitle(""); setsize(350, 250); public static void main(string[] args) { JMain frm = new JMain(); JP panel = new JP(); frm.add(panel); frm.setdefaultcloseoperation(jframe.exit_on_close); frm.setvisible(true); 此程序画出一样的海鸥
184 Android 的对象技术 (OOP) 此时我们说 :Bird 类别支持 IGraph 接口, 或称 Bird 类别实作 IGraph 接口 兹将上图 8-2 改为更亲切的接口图示, 如下图所示 : IGraph void draw(...) void paint(...) Bird void draw(...) { // 畫圖指令 void paint(...) { this.draw(); 图 8-3 以接口图示表达之 从上图 8-1 到图 8-3, 形成一个联想步骤, 如下图所示 : 純粹抽象類別 function1() function2() Interface function1() function2() function1() function2() 介面 具體類別 function1() { function2() { 實作類別 function1() { function2() { 實作類別 function1() { function2() { 图 8-4 从纯粹抽象类别到接口之步骤
第 8 章纯粹抽象类别与接口 185 例如, 刚才 jx08-01 程序范例里的 AbstractCoin 纯粹抽象类别 : AbstractCoin abstract value() ICoin value() value() ICoin Coin @Override value(){ @Override onvalue(){ Coin value(){ onvalue(){ Coin value(){ onvalue(){ 图 8-5 从纯粹抽象类别到接口之例为什么要特别提出这样的思维步骤呢? 因为这样可以将类别继承体系与接口两种概念紧密连结起来, 我们就不会对接口感到神秘难以捉摸了 由于接口代表一个空间 例如刚才 jx08-01 程序范例里的 VendingMachine 贩卖机和 Coin 硬币各代表一个空间或族群 ( 即一个类别继承体系 ) 贩卖机与硬币两个族群能各自发展, 只要接口一致, 就能一拍即合 例如贩卖机业者可以推出更多种的贩卖机, 如售票机 可乐贩卖机等, 则贩卖机体系就会无限成长了 这两族群在独立成长的过程中, 其接口维持不变, 保持其替换性和整合性 ( 即兼容性 ) 所以新型贩卖机也可接受所有的硬币, 而且新型的硬币也能适用于所有的贩卖机 刚才是从消费者的立场与好处着想 其实对生产者也有好处 生产贩卖机者不必考虑到硬币的新旧, 只要接口一致就行了 另一方面, 政府发展新硬币时, 也不必担心不能适用于各贩卖机, 只要提供一致接口, 就行了 因此, 消费者 电器制造者及插座制造者三方皆获得益处
186 Android 的对象技术 (OOP) 8.3 Android 的接口之例 在 Android 应用程序里, 最常见的画面控制组件就是 按钮 (Button) 了 当我们在画面上按了按钮时,Android 侦测到这项事件, 就会透过 OnClickListener 接口而调用到 Activity 子类的 onclick() 函数 8.3.1 操作情境 : 1. 此程序一开始出现选单如下 : 2. 按下 <OK> 按钮,Android 就会透过 OnClickListener 接口而调用到 Activity 子类的 onclick() 函数, 而结束此程序 8.3.2 撰写步骤 : Step-1: 建立 Android 项目 :Hx01 Step-2: 撰写 Activity 的子类 :ac01, 其程序码如下 : /* ac01.java */ package com.misoo.pkaz; import android.app.activity; import android.os.bundle; import android.view.view; import android.view.view.onclicklistener; import android.widget.linearlayout; public class ac01 extends Activity implements OnClickListener {
第 8 章纯粹抽象类别与接口 187 @Override public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.main); LinearLayout layout = new LinearLayout(this); layout.setorientation(linearlayout.vertical); okbutton ok_btn = new okbutton(this); ok_btn.setonclicklistener(this); LinearLayout.LayoutParams param = new LinearLayout.LayoutParams(ok_btn.get_width(),ok_btn.get_height()); layout.addview(ok_btn, param); setcontentview(layout); public void onclick(view v) { finish(); Step-3: 撰写 Button 的子类 :okbutton, 其程序码如下 : /* okbutton.java */ package com.misoo.pkaz; import android.content.context; import android.widget.button; public class okbutton extends Button{ public okbutton(context ctx){ super(ctx); super.settext("ok"); super.setbackgroundresource(r.drawable.ok); public int get_width(){ return 90; public int get_height(){ return 50; Step-4: 执行之 8.3.3 说明 : 1. 此程序执行时, 先诞生 ac01 类别之对象 由于 ac01 类别实作 (Implement) 了 OnClickListener 接口, 所以这个 ac01 类别之对象就具有了 OnClickListener 接口, 可让外界来调用 如下图 :
188 Android 的对象技术 (OOP) OnClickListener onclick() :ac01 onclick() { 2. 执行到 oncreate() 函数时, 就诞生了 okbutton 之对象, 并且执行到指令 : ok_btn.setonclicklistener(this); 让 okbutton 之对象能参考到 ac01 之对象的 OnClickListener 接口了 如下图 : super:button onclick() OnClickListener listener OnClickListener :ac01 onclick() { ok_btn:okbutton OnClickListener listener 3. 接着, 当我们按下画面上的 <OK> 按钮时,Android 从 okbutton 之对象得知这个按键事件的处理函数 (Event Handler) 写在 ac01 类别里 于是 Android 就透过 OnClickListener 接口而执行了 ac01 类别里的 onclick() 函数了
第 8 章纯粹抽象类别与接口 189 8.4 应用于 C/C++: 接口 8.4.1 关于 C++ C++ 的父类兼具有接口之角色, 它并没有特别提供 Interface 机制 8.4.2 关于面向对象 C LW_OOPC 提供了接口机制, 一个类别可以支持多个接口 ; 此外, 多个类别也可以支持同一个接口 基于这两项特性, 可以展现出对象的多态性 8.4.2.1 LW_OOPC 的接口机制 对象接口之用途是 : 让别的对象知道它们能调用此对象的那些函数 例如, INTERFACE( ICircle ) { void (*draw)(); void (*move)(); 这说明了, 别的对象只能调用 Circle 对象的 draw() 及 move() 函数 换句话说, 接口叙述了对象内的函数中, 有那些是允许别对象经由此接口而调用之 让别的对象知道如何与此对象沟通 例如, 想叙述 长方形 对象时, 可定义如下 : INTERFACE(IA) { double (*cal_area)(void*); double (*cal_side)(void*); ; CLASS(Rectangle) { IMPLEMENTS(IA); void (*init)(void*, double, double); double length; double width; ;
190 Android 的对象技术 (OOP) 类别之定义说明了 长方形 对象内部含有两个数据成员, 以及三个成员函数 接口之定义说明了其它对象能透过 IA 接口而调用 长方形 对象的 cal_area() 及 cal_perimeter() 函数, 但不能调用 paint(), 也不能直接存取 length 及 width 数据值 此时也称 Rectangle 类别实现 (Implement) 了 IA 接口, 其表示于类别定义里 : CLASS(Rectangle) { IMPLEMENTS(IA); /* Rectangle 类别实现 (Implement) 了 IA 接口 */ 8.4.2.2 多个类别实现同一接口 两个以上的类别可以实现同一个接口, 例如 : INTERFACE(IA){ double (*cal_area)(void*); double (*cal_side)(void*); ; CLASS(Circle){ IMPLEMENTS(IA); void (*init)(void*, double); double radius; ; CLASS(Square){ IMPLEMENTS(IA); void (*init)(void*, double); double side; ; 由于 Circle 与 Square 类别皆提供一样的接口 --- IA, 所以能抽换对象 也就是说, 凡是支持同一接口的对象皆可以互换, 例如有两个类别 : 圆形 (Circle) 和正方形 (Square), 而且他们都支持 IA 接口的话, 那么两着的对象就可以互换了 请看其 C 程序码 : 撰写 IA 接口程序码 : /* oopc08-ia.h */
第 8 章纯粹抽象类别与接口 191 #ifndef IA_H #define IA_H INTERFACE(IA){ void (*init)(void*, double); double (*cal_area)(void*); double (*cal_perimeter)(void*); ; #endif 这个接口含有 3 个函数 撰写 Cicle 类别 : /* oopc08-cir.c */ #include "lw_oopc.h" #include "oopc08-ia.h" CLASS(Circle){ IMPLEMENTS(IA); double radius; ; static void init(circle* t, double r) { t->radius = r; static double cal_area(circle* t) { return (3.1416 * t->radius * t->radius); static double cal_perimeter(circle* t) { return (2 * 3.1416 * t->radius); CTOR(Circle) FUNCTION_SETTING(IA.init, init) FUNCTION_SETTING(IA.cal_area, cal_area) FUNCTION_SETTING(IA.cal_perimeter, cal_perimeter) END_CTOR 撰写 Square 类别 : /* oopc08-sq.c */ #include "lw_oopc.h" #include "oopc08-ia.h" CLASS(Square){ IMPLEMENTS(IA); double side; ; static void init(square* t, double s) { t->side = s; static double cal_area(square* t) { return (t->side * t->side); static double cal_perimeter(square* t) { return (4 * t->side); CTOR(Square) FUNCTION_SETTING(IA.init, init)
192 Android 的对象技术 (OOP) FUNCTION_SETTING(IA.cal_area, cal_area) FUNCTION_SETTING(IA.cal_perimeter, cal_perimeter); END_CTOR 以上两个类别都支持 IA 接口 撰写主程序 : /* oopc08-ap1.c */ #include "stdio.h" #include "lw_oopgc.h" #include "oopc08-ia.h" void print_area(ia* pi) { printf("area=%6.2f\n", pi->cal_area(pi)); int main() { IA *pc, *ps; pc = (IA*)CircleNew(); pc->init(pc, 10.0); print_area(pc); ps = (IA*)SquareNew(); ps->init(ps, 10.0); print_area(ps); getchar(); return 0; 这程序重用 (Reuse) 了 print_area() 函数, 也就是说,print_area() 可以跟任何支 持 IA 接口的对象沟通 目前这个例子的 main() 内容较多, 而 print_area() 内容较 少, 可能你看不出 print_area() 的重用价值 一旦 print_area() 内容较复杂或较专业 时, 其重用价值就迅速提高了 下一节的贩卖机例子, 将展现出这种价值 8.4.2.3 以接口实现多态性 (Polymorphism) 具有相同接口的一群对象, 称之为多态对象, 它们是可以互换的 例如 :
第 8 章纯粹抽象类别与接口 193 售票機 <<use>> ICoin 介面 value() 一元硬幣 五元硬幣 十元硬幣 这两个类别实现 ICoin 接口, 都有 value() 函数, 而且各类别实现的方式 ( 即程序码内容 ) 都不相同, 因而产生不同的行为 像 value() 这种函数, 称为 多态函数 (Polymorphic Function) 多态函数通常为多态对象皆能接受之讯息 设 *d1 *d5 *d10 分别为这三类别之对象指针, 则 *d1 *d5 *d10 皆能接受此 value() 讯息, 例如指令 : d1->value(); d5->value(); d10->bonus(); 但各调用不同之 value() 实现程序码 接着, 可设计个函数, 让售票机能接受及分辨硬币体系之对象 例如 : 售票机.feedCoin( 一元硬币指标 ) 售票机.feedCoin( 五元硬币指标 ) 售票机.feedCoin( 十元硬币指标 ) feedcoin() 为 售票机 类别之函数, 能接受硬币对象之指针, 然后根据对 象之类别而自动寻找适当之函数 这不但给予软件设计者方便;更重要的是它亦带给使用者莫大的方便 无论使用者拥有一元 五元或十元之硬币, 皆可投入售票机 请看其 C 程序码 : Step-1: 撰写 ICoin 接口程序码 /*oopc08-con.h */
194 Android 的对象技术 (OOP) #ifndef COIN_H #define COIN_H #include "lw_oopc.h" INTERFACE(ICoin){ void (*init)(); double (*value)(); ; #endif Step-2: 撰写一元 五元或十元之硬币类别之程序码 one_dollar 类别 : /* oopc08-one.c */ #include <stdio.h> #include "oopc08-con.h" CLASS(one_dollar){ IMPLEMENTS(ICoin); int k; ; static void init() { static double value() { return 1.0; CTOR(one_dollar) FUNCTION_SETTING(ICoin.init, init) FUNCTION_SETTING(ICoin.value, value) END_CTOR five_dollar 类别 : /* oopc08-fiv.c */ #include <stdio.h> #include "oopc08-con.h" CLASS(five_dollar){ IMPLEMENTS(ICoin); ; static void init(){ static double value() { return 5.0; CTOR(five_dollar) FUNCTION_SETTING(ICoin.init, init) FUNCTION_SETTING(ICoin.value, value)
第 8 章纯粹抽象类别与接口 195 END_CTOR ten_dollar 类别 : /* oopc08-ten.c */ #include <stdio.h> #include "oopc08-con.h" CLASS(ten_dollar){ IMPLEMENTS(ICoin); ; static void init(){ static double value() { return 10.0; CTOR(ten_dollar) FUNCTION_SETTING(ICoin.init, init) FUNCTION_SETTING(ICoin.value, value) END_CTOR 以上 3 个类别都支持 ICoin 接口 现在准备将这 3 种不同的硬币对象喂入贩卖机里, 此时一般程序员会想到 : 贩卖机类别里应该会用一些 if 指令来判断对象的类别, 才能正确计算出所投入的总金额 其实不然, 这是多态性带来的好处 : 让贩卖机类别非常简洁又清晰 Step-3: 定义贩卖机类别 /* oopc08-vm.h */ #include <stdio.h> #include "oopc08-con.h" CLASS(VendingMachine) { void (*init)(void*); void (*feedcoin)(void*, ICoin*); double (*gettotal)(void*); int index; ICoin* array[10]; ; Step-4: 撰写贩卖机类别 /* oopc08-vm.c */ #include <stdio.h> #include "oopc08-con.h" #include "oopc08-vm.h"
196 Android 的对象技术 (OOP) static void init(void*t){ VendingMachine* cthis = (VendingMachine*)t; cthis->index = 0; static void feedcoin(void*t, ICoin* c){ double v; VendingMachine* cthis = (VendingMachine*)t; cthis->array[cthis->index] = c; cthis->index++; static double gettotal(void*t) { int i, sum; ICoin *pc; VendingMachine* cthis = (VendingMachine*)t; sum = 0; for(i=0; i<cthis->index; i++) { pc = cthis->array[i]; sum += pc->value(pc); return sum; CTOR(VendingMachine) FUNCTION_SETTING(init, init) FUNCTION_SETTING(feedCoin, feedcoin) FUNCTION_SETTING(getTotal, gettotal) END_CTOR Step-5: 撰写主程序 /* oopc08-ap2.c */ #include "lw_oopc.h" #include "oopc08-con.h" #include "oopc08-vm.h" int main(){ ICoin *c1, *c5, *c10; double v; VendingMachine* vm = (VendingMachine*)VendingMachineNew(); vm->init(vm); c1 = (ICoin*)one_dollarNew(); c1->init(); vm->feedcoin(vm, c1); c5 = (ICoin*)five_dollarNew(); c5->init(); vm->feedcoin(vm, c5); c10 = (ICoin*)ten_dollarNew(); c10->init(); vm->feedcoin(vm, c10); c5 = (ICoin*)five_dollarNew(); c5->init(); vm->feedcoin(vm, c5); vm->feedcoin(vm, c1); v = vm->gettotal(vm);
第 8 章纯粹抽象类别与接口 197 printf("total=%6.2f\n", v); getchar(); return 0; 除了上面所提到, 一个类别也可以实现两个以上的接口, 也就说一个对象可以同时具有多个接口 关于这部份, 请继续阅读本书第 13 章的第 13.6 节