在
Java中,类(class)是用来代表对象的基本单元。对象(object)可以是现实世界中的任何一个实体,它具有若干区别于其它对象的属性和操作。而类则通过为对象定义属性和操作来概括一类实体。它封装了一组变量和方法,是生成实例对象时的模板。如一辆汽车可视为一个对象,它既具有型号、颜色、载重等特点,又有完成启动、行驶、刹车等功能。定义汽车类时需要将这些属性都包括进去,通常用数据变量代表型号、颜色、载重等属性特点,用成员方法来实现启动、行驶、刹车等操作功能。可以说类是对象的抽象,对象是类的实例化。
接口(interface)可看成一个空的抽象的类,只声明了一组类的若干同名变量和方法,而不考虑方法的具体实现。
Java的包(package)中包含一系列相关的类,同一个包中的类可直接互相使用,对包外的类则有一定的使用限制。
Java的包近似于其它语言的函数库,可提供重用的方便。
在下面各部分的详细介绍中,我们将先给出基本概念,然后结合具体实例阐明
Java的类、接口、包以及封装、继承、重载等有关内容。
4.1 Java的类
4.1.1 类的声明
Java是一种很典型的面向对象的程序设计语言。在面向对象的语言中,世界被看成独立的对象集合,相互间通过消息来通信。因而这种语言以数据对象为中心,而不以处理对象的代码为中心。Java中的类将数据和有关的操作封装在一起,描述一组具有相同类型的对象,作为构筑程序的基本单位。
类声明定义的格式为:
[类修饰符] class类名 [extends 父类名][implements 接口名]
其中类修饰符用于指明类的性质,可缺省。接下来的关键字class指示定义的类的名称,类名最好是唯一的。“extends 父类名”通过指出所定义的类的父类名称来表明类间的继承关系,当缺省时意味着所定义类为Object类的子类。“implements 接口名”用来指出定义的类实现的接口名称。一个类可以同时实现多个接口。类体则包括一系列数据变量和成员方法的定义声明。下面是一些略去类体的类定义例子:
public class WelcomeApp
public class Welcome extends java.applet.Applet
public Car extends Automobile implements Runable
其中前两个类是我们在上一章的示例中定义的。第三个类是小汽车类Car,它的父类是交通工具类Automobile,它还实现了接口Runnable。
类修饰符是用以指明类的性质的关键字。基本的类修饰符有三个:
public,abstract和final
■public
如果一个类被声明为public,那么与它不在同一个包中的类也可以通过引用它所在的包来使用这个类;否则这个类就只能被同一个包中的类使用。
■abstract
如果一个类被声明为abstract,那么它是一个抽象的类,不能被实例化生成自己的对象,通常只是定义了它的子类共有的一些变量和方法供继承使用。被声明为abstract的抽象类往往包含有被声明为abstract的抽象方法,这些方法由它的非抽象子类完成实现细节。
■final
如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为abstract的,又被声明为final的。
继承是面向对象程序设计中一个强有力的工具,它允许在已存在的类的基础上创建新的类。新创建的类称为其基础类的子类,基础类称为其子类的父类。子类的对象除了具有新定义的属性和方法外,还自动具有其父类定义的部分或全部属性方法。这样程序员可以在子类中重用父类中已定义好的变量和方法,只需对子类中不同于父类或新添加的部分重新定义,这样就节省了大量的时间、空间和精力。Java在类声明中使用
extends 父类名
的方式定义继承关系。如果不明显地写出继承的父类名,则缺省地认为所声明的类是Java的Object类的一个子类。Object类是Java中所有的类的祖先类。我们可以把这种类继承关系想象为一棵倒置的类家族树,Object类就是这棵树的根。
4.1.2 类的组成
我们已经知道类是代表对象的,而每一个对象总有特定的状态和行为,在类中分别用变量数据和在数据上可进行的操作表示这些状态和行为。因此类的组成成分是变量和方法。变量和方法的声明格式如下:
[变量修饰符] 数据类型 变量名[=初值] ;
[方法修饰符] 返回值类型 方法名(参数表)
其中修饰符用来指明变量和方法的特性。变量可一次定义一个或多个,定义时可以给出初值。例如:
public int a,b=12;
protected String s="Hot Java";
定义方法时一定要给出返回值类型和参数表。当没有返回值时,返回值类型记为void。参数表的形式为:
参数类型 参数值{,参数类型 参数值}
各参数间以逗号分隔。下面是一些简单的例子:
public static void main(String args[])
public void paint(Graphics g)
public int area(int length,int width){return length * width;}
其中前两个是我们在第三章已经见过的方法声明,这里略去了具体语句组成的方法体。第三个则是一个计算长方形面积的简单方法,接受整数类型的长度和宽度参数并返回它们的乘积作为结果。
变量和方法修饰符是用来指明特性的关键字,主要有以下几种:
■public
一个类中被声明为public的变量和方法是“公开”的,意味着只要能使用这个类,就可以直接存取这个变量的数据,或直接使用这个方法。
■protected
一个类中被声明为protected的变量和方法是“受限”的,意味着它们仅能被与该类处于同一个包的类及该类的子类所直接存取和使用。
■private
被声明为private的变量和方法是“私有”的,除了声明它们的类外,不能被任何其它的类直接存取和使用。
当变量或方法前不加以上三种修饰符时,被认为取friendly状态,即它们只能被同一个包中的类直接存取和使用。但不存在friendly关键字。
■static
被声明为static的变量和方法是属于类而不是属于对象的。不管这个类产生了多少个对象,它们都共享这个类变量或类方法。我们可以在不创建类实例对象时直接使用类变量和类方法。一般来说,在Java中,引用一个特定的变量或方法的形式是:
对象名.变量名
对象名.方法名
例如:
int a=rectangle.length;
g.drawString("Welcome to Java World!");
即变量和方法是受限于对象的,但声明为static的变量或方法受限于类,使用形式是
类名.变量名
类名.方法名
例如:
System.out.println("Welcome to Java World!");
String s=String.valueOf(123);
这里我们并没有创建System类或String类的对象,而直接调用System类的类变量out和String类的类方法valueOf。其中valueOf方法将整形参数转换为String类对象。被声明为static的类方法在使用时有两点要特别注意:
(1)类方法的方法体中只能使用类中其它同样是static的变量或方法;
(2)类方法不能被子类修改或重新定义。
■final
将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能重载。
■abstract
这个修饰符仅适用于方法。被声明为abstract的方法不需要实际的方法体,只要提供方法原型接口,即给出方法的名称、返回值类型和参数表,格式如下:
abstract 返回值类型 方法名(参数表);
定义了abstract抽象方法的类必须被声明为abstract的抽象类。
4.1.3 构造方法和finalizer
Java中有两个特殊的方法:用于创建对象的构造方法(constructor)和用于撤销对象的方法finalizer,相当于C++中的构造函数和析构函数。构造方法是生成对象时编译器自动调用的方法,用以给出对象中变量的初值。构造方法必须与类同名,而且绝对不允许有返回值,甚至不允许以void来标记无返回值。一个类的构造方法可以有多个,以不同的参数表区分不同的情形,这是Java多态性的一个体现。下面是一个简单的例子。
例4.1 Rectangle类的构造方法。
class Rectangle{
protected int width;/*类Rectangle的两个整型变量*/
protected int height;/*分代表长方形的长和宽*/
/*下面是类Rectangle的三个构造方法*/
/*第一个构造方法,无参数,缺省地给出长和宽*/
Rectangle()
/*第二个构造方法,给出长、宽参数*/
Rectangle(int w,int h)
/*第三个构造方法,给出另一个Rectangle作参数*/
Rectangle(Rectangle r)
{width=r.width();
height=r.height();
}
/*下面是类Rectangle的另外两个方法,分别为取长和宽的值*/
public int width()
{return width;}
public int height()
{return height;}
}
class Test{
Rectangle r1=new Rectangle();/*调用第一个构造方法*/
Rectangle r2=new Rectangle(12,20);/*调用第二个构造方法*/
Rectangle r3=new Rectangle(r1);/*调用第三个构造方法*/
}
在这个例子中Rectangle有三个构造方法,它们的名字相同,参数不同因而采用的调用形式也不同。第一个构造方法不需要任何参数,调用时系统自动地给出统一的固定的长方形的宽和高(这里我们设定为20和30)。第二个构造方法需要两个整形参数,根据用户给出的长方形的宽和高创建长方形对象。第三个构造方法需要一个长方形参数,创建出与这个长方形具有同样的宽和高的长方形对象。在Rectangle类中,width和height都是protected的,不宜直接存取。为了使用方便,我们定义出width()和height()方法来获得一个特定长方形的宽和高,再将取得的数值传递给新创建的对象。像这样在一类中有两个或两个以上同名方法的现象叫Overloading,是多态的一种表现。这样同名方法应该有且必须有不同的参数表,调用时编译系统就是根据参数的匹配情况,包括个数和类型,来决定实际使用哪一个方法的。如果两同名方法的参数表也相同,会造成混淆,编译时将得到出错信息:
Duplicate method declaration
(重复的方法声明)
为了实际创建出对象,我们要使用new。系统执行遇到new,才根据new后面跟随的构造方法名和参数表,选择合适的构造方式,分配内存,创建对象并初始化。一个类若没有显示地定义构造方法,使用new时将调用它的父类的构造方法,这种上溯可一直到达Object类,而Object类的构造方法是语言预先定义好的。
相对于构造方法,在对象被撤销时调用的方法是finalizer。对所有的类,它的原始定义形式都是一样的:
void finalize();
没有返回值,而且没有任何参数。一般来说,由于Java的内存管理是由系统自动完成,通常不需要我们重写这个方法,而让它自然而然地从父类(最终也就是从Object类)继承。只有当某些资源需要自动归还时,才需要将这一方法重写。
4.1.4 重写(Overriding)和重载(Overloading)
方法的重写Overriding和重载Overloading是Java多态性的不同表现。前者是父类与子类之间多态性的一种表现,后者是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写(Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。这在例4.1中已经可以看到。下面再给出两个简单的例子,分别显示Overriding和Overloading。
例4.2 Overriding的例示
class Father{
void speak(){
System.out.println("I am Father!");//父类定义的speak方法
}
}
class Son extends Father{
void speak(){
System.out.println("I am Son!");//子类重写的speak方法
}
}
public class Check{
public static void main(String args[]){
Son x=new Son();
x.speak();//调用子类的speak方法
}
}
//output of class Check!
I am Son!
从这个例子我们可以看到,类Son中的speak()方法重写了其父类Father中一模一样的方法,而它的对象x调用speak()方法的结果是与Son中定义致的。
例4.3 Overloading例示。
class Father{
void speak(){ //无参的speak方法
System.out.println("I am Father.");
}
void speak (String s){ //有参的speak方法
System.out.println("I like"+s+".");
}
}
public class Check{
public static void main(String args[]){
Father x=new Father();
x.speak();//调用无参的speak方法
x.speak("music");//调用有参的speak方法
}
}
//out put of class Check
I am Father
I like music.
这个例子中类的Father定义了两个speak方法,在类Check中又两次调用,一次无参,一次有参,打印出两行不同的字符串。注意Java在打印字符串时,字符串间的连接用符号“+”来完成。
Overriding是父类与子类之间多态性的一种表现;Overloading是一个类中多态性的一种表现。
4.1.5 几个特殊的变量:null,this和super
Java中有三个特殊的变量:null,this和super,这三个变量是所有的类都可以使用的,用来指示一些特定的对象。
null相当于“空”,可以用来代指任何对象,但没有实例。如
Rectangle r=null;
创建了一个Rectangle的变量r,但并没有一个Rectangle的实例对象由r来代表。r就如同一个可放置Rectangle的盒子,只是这个盒子现在是空的。
this用以指代一个对象自身。它的作用主要是将自己这个对象作为参数,传送给别的对象中的方法。它的使用形式是这样的:
class Painter{
...
void drawing(Father y){
...
}
}
class Father{
...
void draw(Painter x)
{...
x.drawing(this);/*将自身传递给x的drawing方法*/
...
}
}
class Test{
...
Father f=new Father();
Painter p=new Painter();
f.draw(p);
...
}
例中调用Father类的draw方法时,使用语句
f.draw(p);
又Father类中定义draw方法时以this为参数调用了类Painter的drawing方法:
x.drawing(this);
因而实际上调用了Painter类对象p的drawing方法,而将Father类对象f作为参数传递给drawing方法.
super用来取用父类中的方法和变量数据。它的用法如在下面的例子中所示。
例4.4在类中使用super的例示。
/* Check.java */
class Father{
void speak(){
System.out.println("I am Father.");
}
void speak(String s){
System.out.println("I like "+s+".");
}
}
class Son extends Father{
void speak(){
System.out.println("My father says.");
super.speak();//相当于调用Father类的speak()方法
super.speak("hunting");
//相当于调用Father类的speak(String s)方法
}
}
class Check{
public static void main(String args[]){
Son s=new Son();
s.speak();
}
}
//Check.java的执行结果:
My father says:
I am Fater.
I like hunting.
在这个例子中,类Son的speak()方法语句
super.speak();
super.speak("hunting");
实际调用了Son的父类Father中的speak()和speak(String s)方法,以实现执行结果后两行的输出。使用父类的变量形式也很类似。
super.变量名
super和this的另一个重要用途是用在构造方法中。当一个类中不止一个构造方法时,可以用this在一个构造方法中调用中一个构造方法。若想调用父类的构造函数,则直接使用super。例如我们可心如下定义例4.1中类Rectangle的子类ColorRectangle:
public class ColorRectaqngle extends Rectangle{
int color;
ColorRectangle(int w,int h,int c){
super(w,h);
color=c;
}
...
}
与父类Rectangle相比,类ColorRectangle增加了color成员变量代表长方形的颜色。在它的构造方法中,用语句
super(w,h);
调用了类Rectangle的构造方法
Rectangle(int w,int h);
设定长方形的长和宽,然后就只需设定长方形的颜色:
color=c;
这样大大提高了代码的重用性。
4.2 Java的包
在Java中,包的概念和目的都与其它语言的函数库非常类似,所不同的只是其中封装的是一组类。为了开发和重用的方便,我们可以将写好的程序类整理成一个个程序包。Java自身提供了21个预先设定好的包,下面列出其中主要的几个,其余读者参看Java的API:
java.lang 提供基本数据类型及操作
java.util 提供高级数据类型及操作
java.io 提供输入/输出流控制
java.awt 提供图形窗口界面控制
java.awt.event 提供窗口事件处理
java.net 提供支持Internet协议的功能
java.applet 提供实现浏览器环境中Applet的有关类和方法
java.sql 提供与数据库连接的接口
java.rmi 提供远程连接与载入的支持
java.security 提供安全性方面的有关支持
我们可以引用这些包,也可以创建自己的包。
4.2.1 包的声明
为了声明一个包,首先必须建立一个相应的目录结构,子目录名与包名一致。然后在需要放入该包的类文件开头声明包,形式为:
package 包名;
这样这个类文件中定义的所有类都被装入到你所希望的包中。例如
package Family;
class Father{
...//类Father装入包Family
}
class Son{
...//类Son装入包Family
}
class Daughter{
... //类Daughter装入包Family
}
不同的程序文件内的类也可以同属于一个包,只要在这些程序文件前都加上同一个包的说明即可。譬如:
//文件 Cat.java
package Animals;
class Cat{/*将类Cat放入包Animals中*;
...
}
//文件Dog.java
package Animals;
class Dog{ /*将类Dog放入包Animals中*/
...
}
4.2.2 包的使用
在Java中,为了装载使用已编译好的包,通常可使用以下三种方法:
(1) 在要引用的类名前带上包名作为修饰符。如:
Animals.Cat cat=new Animals.Cat();
其中Animals是包名,Cat是包中的类,cat是类的对象。
(2)在文件开头使用import引用包中的类。如:
import Animals.Cat;
class Check{
Cat cat=new Cat();
}
同样Animals是包名,Cat是包中的类,cat是创建的Cat类对象。
(3)在文件前使用import引用整个包。如:
import Animals.*;
class Check{
Cat cat=new Cat();
Dog dog=new Dog();
...
}
Animals整个包被引入,Cat和Dog为包中的类,cat和dog为对应类的对象。
在使用包时,可以用点“.” 表示出包所在的层次结构,如我们经常使用的
import java.io.*;
import java.applet.*;
实际是引入了/java/io/或/java/applet/这样的目录结构下的所有内容。需要指出的是,java.lang这个包无需显式地引用,它总是被编译器自动调入的。使用包时还要特别注意系统classpath路径的设置情况,它需要将包名对应目录的父目录包含在classpath路径中,否则编译时会出错,提示用户编译器找不到指定的类。
4.3 一个邮件类(Mails)的例子
下面我们给出一个较大的例子,让读者在实例中进一步熟悉Java的类和包。
这里所有的类都放在包ch4package中,先定义出一个虚基类Mails,然后派生出它的两个子类Parcel(包裹)和Remittance(汇款)。Show类用于实际执行,允许用户创建自己的邮件,然后显示出所有的邮件信息。为了方便地存取邮件,还定义了类ShowMails。接下来我们逐一介绍这经些类。
例4.5 类Mails程序文件。
1:package ch4package;
2: public abstract class Mails{
3: protected String fromAddress;
4: protected String toAddress;
5: public abstract void showMe();
6: }
类Mails是一个虚类,不能产生自己的实例对象,而只是描述了邮件最基本的特性。类文件的开头首先用
package cha4package;
表明Mails类是放于ch4package这个包里的。然后程序第二行为Mails的类声明。
public abstract class Mails
用修饰符abstract指出这是个虚类。第三至第四行Mails类中定义了两个变量:
protected String fromAddress;
protected String toAddress;
fromAddress和toAddress ,分别代表邮件的寄出地址和送往地址,都是protected类型的,这样cha4package包外的类不能直接引用,保证了信息的隐藏。第五行Mails类定义了方法
showMe(),用于显示一个邮件自身的有在信息:
public abstract voi showMe();
声明时以abstract修饰,意味着这是一个抽象方法,只给出原型,具体实现要由Mails类的非虚子类通过Overriding完成。
接下来是Mails的两个非虚子类。
例4.6 类Parcel和类Remittance程序文件。
//Parcel.java
1: package ch4package;
2: public class Parcel extends Mails{//邮件类的子类Parcel类
3: protected int weight;
4: Parcel(String address1,String address2,int w){//构造方法
5: fromAddress=address1;
6: toAddress=address2;
7: weight=w;
8: }
9: public void showMe(){
10: System.out.print("Parcel:");
11: System.out.println("\tFrom:"+fromAddress+"\tTo:"+toAddress);
12: System.out.println("\tWeigth:"+weight+"g");}
13: }
//Remittance.java
1: package ch4package;
2: public class Remittance extends Mails{//邮件类的子类Remittance
3: protected int money;
4: Remittance(String address1,String address2,int m){//构造方法
5: fromAddress=address1;
6: toAddress=address2;
7: money=m;
8: }
9: public void showMe(){//显示邮件信息
10: System.out.println("Remittance:");
11: System.out.println("\tFrom:"+fromAddress+"\tTo:"+toAddress);
12: System.out.println("\tMoney:"+money+" Yuan");
13: }
14:}
这里是邮件的两个子类:包裹Parcel和汇款Remittance。以类Parcel为例详细说明。首先在程序开头写出:
package ch4package;
一方面将类Parcel装入包ch4package,另一方面方便类Parcel使用包ch4package中的其它类,如已定义的Mails类。接下来类Parcel声明时用
extends Mails
表明自己是Mails的一个子类。在第三行Parcel声明了一个weight变量,用来代表包裹的重量。加上从父类Mails继承下来的变量fromAddress和toAddress,类Parcel一共有三个成员变量:
寄出地址 fromAddress,寄达地址toAddress和重量weight
相对应的,它的构造方法Parcel也必须有三个参数,分别传递给三个成员变量。构造方法的定义如第四行至第八行所示。由于Parcel类不是虚类,所以必须在其中重写完成它的父类Mails中声明的抽象方法showMe。Parcel的showMe()方法仅仅是将自己的邮件类型和三个变量的信息在屏幕上显示出来。
类Remittance与Parcel非常相似,只是它定义的变量为money,用来代表汇款的金额。它也必须具体完成方法showMe。
下面我们看到的是用于存取邮件的类ShowMails。
例4.7 类ShowMails程序文件。
1: package ch4package;
2: import java.lang.*;
3: public class ShowMails{
4: protected Mails showList[];//邮件数组序列
5: protected static final int maxMails=50;//最大邮件个数
6: protected int numMails;//当前邮件个数
7: ShowMails(){
8: showList=new Mails[maxMails];
9: numMails=0;
10: }
11: public void putMails(Mails mail){
12: if(numMails<maxMails){
13: showList[numMails]=mail;//加入邮件
14: numMails++;//修改计数
15: }
16: }
17: public Mails getMails(int index){//获取邮件
18: if((0<=index)&&(index<numMails)) return showList[index];
19: else return null;
20: }
21: public void showAll(){//展示邮件
22: if(numMails>0)
23: for (int i=0;i<numMails;i++){
24: System.out.print("Mail NO"+(i+1)+":");//邮件序号
25: showList[i].showMe();//邮件具体信息
26: }
27: else
28: System.out.println("No mails.");
29: }
30: public int mailnum(){
31: return numMails;
32: }
33:}
程序第四行至第六行类ShowMails定义了三个成员变量:
showList[],maxMails和numMails
变量showList[]是类Mails的一个数组。但由于Mails本身是个虚类,因而showList[]的元素不可能是Mails的对象,它实际上是用来存放Mails的两个子类Parcel和Remittance的对象的。一般说来,一个被声明为类A的的变量,总可以被赋值为任何类A的子类的实例对象。这与父子类之间的类型转换的原则是一致的:父类到子类的转换可以隐式地自动进行,而子类到父类的转换则需要显式地加以说明。
变量maxMails用来指出showList[]中最多可容 纳的邮件数,它对ShowMails的所有对象都应是固定且一致的。因此它被声明为tatatic和final的,为所有对象共享且不可更改。变量numMails则用来作为showList[]中实际邮件个数的计数。
对应ShowMails的三个成员变量,我们在ShowMails()构造方法中只需做两件事:实际创建类mails的数组showList[],然后将邮件计数numMails置零。
第11行开始的方法putMails和第17行开始的方法getMails分别完成对showList[]中邮件的存取。第30行的mailnum方法则返回当时的邮件计数值。putMails方法接受一个邮件类参数,并把它加入到当前邮件序列的末尾。getMails方法接受一个整型参数作为邮件序号,根据该序号找出当前邮件序列中对应邮件返回。当给定的邮件号index不在有效范围时,以据该序号找出当前邮件序列中对应邮件返回。当给定的邮件号index不在有效范围时,以
return null;(19行)
返回一个定值。这一句看上去并没有完成什么实质性的工作,但如果省略则编译时会出错。因为getMails方法的返回值已声明为Mails类,这就要求在任何情况下都返回一个符合这一要求的值。而空变量null可与任何类型匹配,恰好能适合这样的要求。
第21行的方法showAll显示showList[]中所有邮件的信息。每一邮件首先显示自己的邮件号。因为showList[]数组的下标从0开始,为了符合人们的日常习惯,将每一个下标加1后再作为邮件号输出。各个邮件的显示是调用邮件的showMe()方法来实现的。因为showMe()方法已经在虚类Mails中定义了,所以不管showList[]中的实际元素是Parcel还是Remittance,编译器总能顺利地连接调用相应的代码。Java面向对象特性中的动态绑定(Dynamic Binding),保证了无需在编译前确定地知道showList[]每一个数组元素的类型,就能成功地实现这样的链接。
最后给出的类是实际执行的Shos类。
例4.8 类Show程序文件
1: package ch4package;
2: import java.io.*;
3:
4: public class Show{
5: public static ShowMails board=new ShowMails();//邮件库变量
6: public static void main(String args[])throws IOException{
7: boolean finished=false;
8: BufferedReader in =new BufferedReader(new InputStreamReader(System.in));
9: while(!finished){//添加邮件
10: System.out.print("\nDo you want to add mails(Y/N)?");
11: System.out.flush();
12: char ch=in.readLine().charAt(0);
13: if('Y'==Character.toUpperCase(ch)){//输入地址
14: System.out.println("Address information:");
15: System.out.print("\tFrom:");
16: System.out.flush();
17: String address1=in.readLine();
18: System.out.print("\tTo:");
19: System.out.flush();
20: String address2=in.readLine();
//选择邮件各类(包裹或汇款)
21: System.out.print("Choose the mail type:1-Parcel 2-Remittance ");
22: System.out.flush();
23: ch=in.readLine().charAt(0);
24: if('1'==ch){//输入包裹重量
25: System.out.print("Parce\tWeight:");
26: System.out.flush();
27: int w=getInt();
28: Parcel pa=new Parcel(address1,address2,w);
29: board.putMails(pa);
30: }
31: if('2'==ch){//输入汇款金额
32: System.out.print("Remittance\tMoney:");
33: System.out.flush();
34: int m=getInt();
35: Remittance re=
new Remittance(address1,address2,m);
36: board.putMails(re);
37: }
38: }
39: else finished=true;
40: }
41: System.out.println(" ");
42: board.showAll();//输出所有邮件信息
43: }
//键盘输入获取整数
44: public static int getInt() throws IOException{
45: BufferedReader in= new BufferedReader
(new InputStreamReader(System.in));
46: String st=in.readLine();
47: Integer i=new Integer(st);
48: return i.intValue();
49: }
50:}
由于涉及交互,类Show中用到了许多输入输出语句,我们在程序第2行用
import java.io.*;
引入Java的IO包。这个包封装了大量有关输入输出的方法,具体内容将在第七章中详细介绍。这里我们只需要弄清楚所用到的输入/出语句的功能。
在输入/出中,总有可能产生输入输出错误,Java反这引起错误都归入IOException(IO异常)因为我们不打算在程序中加入对这些异常的处理,所以需要在每个方法的参数表后用关键字throws“扔出”这些异常,如第6行
public static void main(String args[])throws IOException
这样异常发生时,将自动中止程序运行并进行标准处理。请参看第五章的内容。
程序的输入来源是一个BufferedReader类的对象in,它的声明在第8行:
BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
因而具有BufferedReader中定义的所有输入功能。
in.readLine()
是读入一行输入,并返回一字符串。而
charAt(i)
是String类的一个方法,取得指定字符串的第i个元素作为字符型返回。这两上方法边用,则可取得想要的输入。而在输入前用
System.out.flush();
将缓冲清空,以保证输入的正确性。
System.out.print
System.out.println
都是输出语句,不同的只是后者在输出结束后自动换行。类System和getInt()中用到的类都是Interger(注意不是int!)都在Java的lang名中定义,我们将在第六章详细介绍。
在了解以上的基本输入输出后,这个程序就变得较等了。为了方便起见,我们不失一般性的将Show类的所有成员都定义为static的,这样,类Show就不同志需要特别定义的构造方法了。在第5行声明的变量board是ShowMails类的对象,用来建立邮件库:
public static ShowMails board=new ShowMails();
第44行开始的getInt方法用来从键盘输入获得一个整数。第6行开始的main方法则是程序的主体。它实现的功能是不断询问是否要加入新邮件,肯定回答时要求选择邮件类型并输入相应信息。据此创建邮件子类对象并加入board中,直至得到不定回答退出。最后显示此时已有的邮件信息。邮件的加入和显示都通过简单的
board.pubMails()
board.showAll()
调用ShowMails的方法来实现的,简洁明了而层次清晰。这就是面向对象进行数据封装和重用的优点所在。要执行类Show,我们需要将例4.5~例4.8的文件依次输入、编译。最后用解释器java执行类Show。下面给出的是Show的运行结果,其中加下划线“_”的是键盘输入。
例4.9 类Show运行结果。
D:\java01>java ch4package.Show Do you want to add mails(Y/N)?n //询问有是否添加邮件
No mails. //显示没有邮件
D:\java01>java ch4package.Show
Do you want to add mails(Y/N)?y//询问有是否添加邮件
Address information: //要求输入地址信息
From:NanJing
To:BeiJing
Choose the mail type:1-Parcel 2-Remittance 1//要求选择邮件类型
Parce Weight:100//要求输入包裹重量 Do you want to add mails(Y/N)?y
Address information:
From:ShangHai
To:TianJing
Choose the mail type:1-Parcel 2-Remittance 2
Remittance Money:400//要求输入汇款金额 Do you want to add mails(Y/N)?n
Mail NO1:Parcel://输出所有邮件信息
From:NanJing To:BeiJing
Weigth:2g
Mail NO2:Remittance:
From:ShangHai To:TianJing
Money:400 Yuan D:\java01
4.4 Java的接口
4.4.1 引进接口的目的
Java的接口也是面向对象的一个重要机制。它的引进是为了实现多继承,同时免除C++中的多继承那样的复杂性。前面讲过,抽象类中包含一个或多个抽象方法,该抽象类的子类必须实现这些抽象方法。接口类似于抽象类,只是接口中的所有方法都是抽象的。这些方法由实现这一接口的不同类具体完成。在使用中,接口类的变量可用来代表任何实现了该接口的类的对象。这就相当于把类根据其实现的功能来分别代表,而不必顾虑它所在的类继承层次。这样可以最大限度地利用动态绑定,隐藏实现细节。接口还可以用来实现不同类之间的常量共享。
为了说明接口的作用,我们不妨假设有一系列的图形类,其中一部分在图形中加入了文字,成为可编辑的,它们应当支持最普遍的编辑功能:
cut,copy,paste和changeFont
将这些方法的原型统一组合在一个EditShape接口中,就可以保证方法名的规范统一和使用的方便。我们画出这个假想的类和接口的继承关系图,可以更直观地了解。
Object
↓
Shape
┌────────────┼─────────────┐
↓ ↓ ↓
Circle Rectangle Triangle
↙ ↘ ↙ ↘ ↙ ↘
PaintCircle TextCircle PaintRectangle TextRectangle PaintTriangle TextTrangle
↑ ↑ ↑
└───────────┼───────────────┘
EditShape
图4.1 Shape 和 EditShape
以图中类Circle的两个子类为例。类PaintCircle未实现EditShape接口,不支持上述编辑功能。而类TextCircle既是Cricle的子类,又实现了EditShape接口,因而不但具有Circle类的图形牲,又支持EditShape定义的编辑功能。而在TextCircle,TextRectangle和TextTriangle中,支持这些编辑功能的方法是同名同参的(与EditShape的定义一致),这又提供了使用上的方便。
4.4.2 接口的声明和使用
Java的接口类似于抽象类,因而它的声明也和抽象类类似,只定义了类中方法的原型,而没有直接定义方法的内容。它的声明格式为:
[接口修饰符] interface 接口名 [extends 父类名]
接口修饰符可以是public或abstract,其中abstract缺省时也有效。public的含义与类修饰符是一致的。要注意的是一个编译单元,即一个.java文件中最多只能有一个public的类或接口,当存在public的类或接口时,编译单必须与这个类或接口同名。
被声明的变量总是被视为static和final的,因而必须在声明时给定初值。被声明的方法总是abstract的,abstarct缺省也有效。与抽象类一样,接口不需要构造方法。接口的继承与为是一样的,当然一个接口的父类也必须是接口。下面是一个接口的例子:
interface EditShape{
void cut();
void copy();
void paste();
void changeFont();
}
在使用时,为了将某个接口实现,必须使用关键字implements。格式是这样的:
[类修饰符] class 类名 [extends 父类名] [implements 接口名表]
其中,接口名表可包括多个接口名称,各接口间用逗号分隔。“实现(implements)“了一个接口的非抽象类必须写出实现接口中定义的方法的具体代码,同时可以读取使用接口中定义的任何变量。
例4.10 接口的实现
class TextCircle extends Circle implements EditShape
{...
void cut()
void copy()
void paste()
void changeFont
...
}
4.4.3 多继承
在Java中,类之间只允许单继承,但我们可以把一个类实现的接口类也看作这个类的父类。类从它实现的接口那里“继承”了变量和方法,尽管这些变量是静态常量,这些方法是未实现的原型。如果一个类实现的接口类不止一个,那么所有这些接口类都被视为它的“父类”。这样,实现了一个或多个接口的类就相当于是从两个(加上该类原有意义上的父类)或两个以上的类派生出来的。Java的多继承正是建立在这种意义之上。通过接口的继承,相当于只选择了一部分需要的特征汇集在接口中由不同的类共享并继承下去,而不必通过父子类间的继承关系将所有的方法和变量全部传递给子类。所以我们又可以把Java的这种多继承称为“有选择的多继承”。这种多继承与一般的多继承相比,更为精简,复杂度也随之大大降低。
在多继承时,一个子类可能会从它的不同父类那里继承到同名的不同变量或方法,这往往会引起两义性问题,即不知道子类中这样的变量或方法究竟是继承了哪一个父类的版本,在Java中,为了防止出现这样的两义性问题,规定不允许一个子类继承的父类和实现的接口类中定义同名的不同变量,否则编译该子类时将出错,无法通过。而对于方法,由于接口类中定义的总是abstract的方法原型,而没有实际代码,所以不会出现类似的两义性问题。相反,常会存在这样的情况:当接口类中要求实现的方法子类没有实现,而子类的父类中定义有同名方法时,编译器将子类从父继承的该方法视为对接口的的实现。这样的继承和实现都被认为是合法的。
4.5 实现了接口的邮件类例子
这一节我们将4.3节邮件类的例子加以改进和扩展,加入有关接口的内容,以说明接口和多继承的概念。
首先定义一个名为MailPost的接口,其中没有定义变量,而是给出两个有关邮寄方法原型。
calPrice()计算邮费并以浮点数形式返回;
post()完成邮寄。
例4.11 接口MailPost。
//MailPost.java
package ch4package;
public interface MailPost{
public float claPrice();
public void post();
}
接下来在包裹Parcel和汇款Remittance的基础上分别派生出可邮寄的包裹和汇款:PostParcel和PostRemit两个子类。
例4.12 子类PostParcel和PostRemit。
---------------------------------
//PostParcel.java
package ch4package;
import java.lang.*;
public class PostParcel extends Parcel implements MailPost{
protected int postage;
protected boolean postable;
protected boolean posted;
PostParcel(Ttring address1,String address2,int w,intp){
//构造方法
super(address1,address2,w);
postage=p;
postable=false;
posted=false;
}
public float calPrice(){//计算邮资
return((float)0.05*weight);
}
public void post(){//邮寄包裹
float price=calPrice();
postable=(price<=postage);
posted=true;
}
public void showMe(){//显示邮件信息
float price=calPrice();
System.out.println("Postable Parcel:");
System.out.println("\tFrom:")+fromAddress+\tTo"
+toAddress);
System.out.println("\tWeigth:)+weigth+"g\tPostage:"
+postage+"Yuan");
if(posted){
if(postable)System.out.println("\tIt has been
posted !");
else{
System.out.println("\tIt needs more postage:");
System.out.println("\tThe current postage
is:"+postage+"Yuan");
System.out.println("\t\tThe price is:"+price+"Yuan");
}
}
}
}
//PostRemit.java
package ch4package;
import java.lang.*;
public class PostRemit exteds Remittance implements MailPost{
protected int postage;
portected boolean postable;
protected boolean posted;
PostRemit(String address1,String address2,int m,int p){
//构造方法
super(address1,address2,m);
postage=p;
postable=false;
posted=false;
}
public float calPrice(){//计算邮资
float price=cealPrice();
postable=(price<=postage);
posted=true;
}
public void showMe(){//显示邮件信息
float price=calPrice();
System.out.println("Postable Remit:");
System.out.println("\tFrom:"+fromAddress+"\tTo:"
+toAddress);
System.out.println("\tMoney:"+money+"Yuan"+"\tPostage:"
+postage+"Yuan");
if(posted){
if(postable)System.out.println("\tIt has been
posted!");
else{
System.out.println("\tIt needs more postage:");
System.out.println("\t\tThe current postage is:"
+postage+"Yuan");
System.out.println("\t\tThe price is:"
+price+"Yuan");
}
}
}
}
---------------------------------
这两个类都实现了接口MailPost。由于两个类非常相似,我们仍然重点讲解其中一个:类PostParce。
PostParcel仍是包ch4package中的一员,它是类Parcel的子类(extends Parcel),又实现了接口MailPost(implements MailPost):
public class PostParcel extends Parcel implements MailPost
在Parcel的基础上,它新增加了三个变量:
postage,posted,postable
其中整型的postage用来记录邮寄人提供的邮资,布尔型的posted和postable分别用来记录是否被尝试邮寄过以及邮寄是束成功。在PostParcel的构造方法中,第9行语句
super(address1,address2,w);
调用了它的父类Parcel的构造方法,设定它从Parcel中继承的变量寄出地址、寄达地址和重量的初值。这就是我们在前面提到过的super变量在构造方法中的用途:调用父类的相应构造方法。这样做的一个好处是可以重用父类的代码,然后PostParcel就只需设定邮资,并将posted和postable初值都置为false。
PostParcel和PostRemit都实现了接口MailPost,国而在它们的定义中,都必须给出方法calPrice()和post()的具体实现。在PostParcel中,为了简单起见,邮费只是根据重量每克收到0.05元,而不考虑寄达的距离,如语句第15行:
return ((float)0.05*weight);
在post()方法中,将计算所得邮资与瑞有邮费加以比较,若邮费已够将postable设为true,包裹可邮寄;否则postable为false,包裹不可邮寄。无论postable取值如何,都已试图邮寄,所以将posted置为true。处理过程见第18行至20行。
最后一个方法是showMe()。在这里,PostParcel重写(Overriding)了它的父类Parcel中的同名方法。当包裹尚未被试图邮寄过,则在基本信息后附加有关的邮寄信息,若未邮寄成功,给出所需最费提示。
PostRemit类的基本构成与PostParcel是一致的,读者可以自己试着读懂它的源文件。
在包ch4package中,类Mails,Parcel,Remittance以及ShowMails都无需改动,只有最后的可执行类Show需要相应的修改。它的源程序如下。
例4.13 可执行类Show程序文件。
-------------------------
//Show.java
1: package ch4package;
import java.lang.*;
2: import java.io.*;
3:
4: public class Show{
5: public static ShowMails board=new ShowMails();
6: public static void main(String args[])throws IOException{
7: boolean finished=false;
8: BufferedReader in =new BufferedReader(new InputStreamReader(System.in));
9: while(!finished){//添加邮件
10: System.out.print("\nDo you want to add mails(Y/N)?");
11: System.out.flush();
12: char ch=in.readLine().charAt(0);
13: if('Y'==Character.toUpperCase(ch)){
14: System.out.println("Address information:");
15: System.out.print("\tFrom:");//输入地址信息
16: System.out.flush();
17: String address1=in.readLine();
18: System.out.print("\tTo:");
19: System.out.flush();
20: String address2=in.readLine();
//选择邮件种类
21: System.out.print("Choose the mail type:1-Parcel
2-Remittance ");
22: System.out.flush();
23: ch=in.readLine().charAt(0);
24: if('1'==ch){//输入包裹重量
25: System.out.print("Parcel\tWeight:");
26: System.out.flush();
27: int w=getInt();
//是否寄出邮件
System.out.print("Do you want to post it(Y/N?");
System.out.flush();
ch=in.readLine().charAt(0);
if('Y'==Character.toUpperCase(ch)){//输入邮资
System.out.println("You want to post in,then
input your postage:");
System.out.flush();
int p=getInt();
//可邮寄包裹
PostParcel pa=new
PostParcel(address1,address2,w,p);
board.putMails(pa);
}
//不可邮寄包裹
else{Parcel pa=new Parcel(address1,address2,w);
board.putMails(pa);}
}
if('2'==ch){
System.out.print("Remittance\tMoney:");
System.out.flush();
int m=getInt();
System.out.print("Do you want to post it(Y/N)?");
System.out.flush():
ch=in.readLine().charAt(0);
if('Y'==Character.toUpperCase(ch)){
System.out.println("You want to post it,then input
postage:");
System.out.flush();
int p=getInt();
//可邮寄汇款
PostRemit re=new PostRemit(address1,address2,m,p);
board.putMails(re);
}
//不可邮寄汇款
else{Remittance re=new Remittance(address1,address2,m);
board.putMails(re);}
}
}
else finished=true;
}
System.out.println("");
board.showAll();//显示邮件信息
post();
}
public static int getInt() throws IEOxception{
BufferedReader in=new BufferedReader
(new InputStreamReader(System.in));
String st=in.readLine();
Integer i=new Integer(st);
return i.intValue();
}
private static void post()throws ClassCastException,IOException{
int n\board.mailnum();
if(n!=0){
System.out.println("You have "+n+" mails");
boolean end=false;
//检查邮寄情况
while(!end){
System.out.print("\nInput the mail NO you want to check the
result(输0退出):");
System.out.flush();
int i=getInt();
if(i!=0){
try{
Mails obj=board.getMails(i-1);
post((MailPost)obj);
obj.showMe();
}catch(ClassCastException ex){
System.out.println("Mail is not postable!");}
}
else end=true;
}
}
}
private static void post(MailPost obj){
obj.calPrice();
obj.post();
}
}
-------------------------
与第三节例4.8中类的Show相比,改动后的Show的main方法增加了询问是否要将邮件设为可邮寄类型的功能以及相应的处理段,并调用Post()方法邮寄邮件并给出邮寄情况说明。类Show定义了两个post方法来实惠邮寄。这两个方法虽同名,但参数不同,完成的功能也大相径庭。
第72行至92行的第一个post方法没有参数。它首先给出现有邮件数量,然后根据输入的邮件号通过ShowMails的getMails方法取得邮件,再调用第二个post方法实际将邮件寄出;当输入的邮件号为零时结束。在调用第二个post方法时,需要将邮件显式转换为接口类MailPost:
83:Mails obj=bord.getMails(i-1);
84:post((MailPost)obj);
因为PostParcel和PostRemit都实现了接口MailPost,都支持这样的转换,就可以通过种形式从功能上将它们统一起来。如果该邮件所属的类没有实现接口MailPost ,如类Parcel或类Remittance,这样的类型转换就不能实现,将引发类型转换异常(ClassCastException),不再转去调用post方法,而由catch结构给出“邮件无法被邮寄”的报错信息:
86:}catch(ClassCastException ex){
87: System.out.println("Mail is not postable!");}
其中的try-catch结构是Java中异常处理的典型结构。
第二个post方法带一个MailPost接口类的参数,它实际调用接口定义的方法calPrice和post将邮件寄出。
下面我们来看一个Show的执行实例,其中带下划线“_”的部分为执行的键盘输入。
例4.14 Show的执行结果。
--------------------
--------------------
当启动Show的运行后,首先依照提示创建三个邮件对象,其中第一个是不可邮寄包裹后两个分别是可邮寄的包裹和汇款。停止添加邮件后顺序显示现有邮件信息,包括邮件号、邮件类别、地址信息、重量/金额以及已付邮资,并提示现有邮件总数。此时我们可依次检查邮件是否可寄出:
输入邮件号“1”,由于此包裹不是可邮寄包裹类,给出报告:邮件不可寄出;
输入邮件号“2”,该邮件是可邮寄包裹,且通过邮资计算已付足,给出报告:邮件可寄出;
输入邮件号“3”,该邮件是可邮寄汇款,但欠缺邮资,给出报告:邮件需补足邮资,然后列出应交邮费与实交邮费比较。
最后输入数字“0”,结束本次执行。
这样我们就完成了对第三节中邮件类的扩充和改进,最终得到的包ch4package中所有类和接口的层次继承关系,如图4.2所示。读者可以对照这个图理清它们的继承和实现关系。
Object
┌─────┼─────┐
↓ ↓ ↓
Mails ShowMails show
┌───┴───┐
↓ ↓
Parcel Remittance
↓ ↓
PostParcel PostRemit
↖ ↗
MailPost
图4.2 包ch4package的类和接口层次