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中一模一样