第三章
来源:互联网 发布:淘宝宝贝设置二级规格 编辑:程序博客网 时间:2024/04/29 22:54
第3章 类与继承
3.1类和对象
万物皆是对象,人、汽车、日期时间、银行帐号、音乐、图形图像、颜色、字体、文件、记录、网络地址等等,在面向对象的程序设计中都视为对象。把这些现实世界的对象和概念用计算机语言抽象表示出来就是类。
在Java中,类是的一种重要的复合数据类型,是组成Java程序的基本要素,也是面向对象的程序设计的基本单位。类定义了某类对象的共有变量和方法。变量是现实对象的属性或状态的数字化表示,方法是对现实对象进行的某种操作或其对外表现的某种行为。对象是由一组相关的变量和方法共同组成的一个具体的软件体。
类实例化就成为对象。对象和类之间的关系就如同房子和其设计图纸的关系。类的作用就象一个模板,所有对象实例依照它来创建。
3.1.1类声明
类由传统的复合数据类型(如C语言中的结构)发展而来,几乎所有的面向对象的程序设计语言都采用class来声明类,在Java中类声明的格式如下:
[public][abstract | final] class className
[extends superClassName] [implements interfaceNameList ...]
{...class body...}
l public为类的访问控制符。Java类具有两种访问控制符public和package。public允许一个类具有完全开放的可见性,所有其他类都可以访问它;省略public则为package可见性,即只有位于同一个包中的类可以访问该类。
l abstract指明该类为一个抽象类,暗含该类需要被继承,才能实例化。final阻止一个类被继承。
l className指定该类类名。
l extends类继承,superClassName为父类。如果在类定义时没有指定类继承关系,Java将自动从Object类派生该类。
l implements实现接口,interfaceNameList为被实现的一个或多个接口名,接口实现是可选的。虽然Java类只可继承一个父类,却可同时实现多个接口。
l {...} 由一对花括号括起来的部分构成类的主体。这是一个类的具体实现部分,在其中我们可以定义类的变量和方法。
3.1.2类成员
类体是一个类的功能部分,由变量和方法两部分组成,两者通称为类成员。
类体的格式如下:
class className{
//成员变量
[public | protected | private ] [static]
[final] [transient] [volatile] type variableName;
//成员方法
[public | protected | private ] [static]
[final | abstract] [native] [synchronized]
returnType methodName ([paramList]) [throws exceptionList]
{statements}
}
l Java定义了4种访问级别:public、protected、package和private。用来控制其他类对当前类的成员的访问,这种控制对于类变量和方法来说是一致的。当省略了访问控制符,则为package。
l 具有static的声明的成员属于类本身的成员,而不是类被实例化后的具体对象实例的成员。
l final变量用来定义不变的常量,fianl方法在类继承时不允许子类覆盖。
l transient表明类成员变量不应该被序列化,序列化是指把对象按字节流的形式进行存储。
l volatile变量阻止编译器对其进行优化处理。
l native方法是指Java本地方法,Java本地方法是指利用C或C++等语言实现的方法。
√Java语言不支持C++友元方法或友类。
3.1.2.1变量
成员变量表示类的静态属性和状态,可以是任何的类型,既可以是基本数据类型也可以是复合数据类型。这就是说一个类的变量可以是其他类的对象。
在Java中,所有的类成员变量都具有初始值。我们可以在定义类的成员变量时,对其进行赋值,如果一个类成员变量在定义时没有指定初始值,则系统会给它赋于缺省值(见表3.1.1)。
表3.1.1 变量的缺省初始化表
变量的类型
初始值
布尔型(boolean)
false
字符型(char)
'/u0000'
整型(byte、short、int、long)
0
浮点型(float,double)
+0.0f或+0.0d
对象引用
null
类成员变量定义在所有的方法(包括成员方法和构建器)之外,而定义在方法中的变量称为局部变量,另外方法参数也是一种局部变量。所有的局部变量,须经程序赋值才能使用,否则在编译Java程序时将发生变量未初始化错误。例如:
public class MyClass {
//x 为类成员变量
int x;
public void print() {
//y和z 为局部变量
int y = 0;
int z;
//x 值为0
System.out.println("x = " + x);
//y 值为0
System.out.println("y = " + y);
//非法使用尚未初始化的局部变量z,编译时无法通过
System.out.println("z = " + z);
}
}
3.1.2.2方法
Java中所有用户定义的操作均用方法(method)来实现,方法由一组完成某种特定功能可执行的代码组成。在Java类中可以定义方法,在Java的接口中可以声明方法,当一个类要实现某个接口时,就需要实现该接口中声明的方法。
方法包括两种:构建器(constructor)和普通方法。和C++一样,构建器在新建对象时被调用,它没有返回类型。普通方法都有返回类型,如果一个普通方法不需要返回任何值,那么它必须标上返回类型void。
方法的参数表由成对的类型及参数名组成,相互间用逗号分隔。如果方法无参数,则参数表为空。
方法内部定义的局部变量不能和方法的参数同名,否则会产生编译错误,例如:
public class TestSketch {
void test(int i,int j){
for (int i=0; i<=100; i++){
…
}
}
}
编译报错如下:
TestSketch.java:3: i is already defined in test(int,int)
for (int i=0; i<=100; i++){
^
1 error
√方法体中声明的局部变量的作用域在该方法内部。若局部变量与类的成员变量同名,则类的成员变量被隐藏。
例3.1.1 VariableDemo.java
//本例说明局部变量和同名
//类成员变量的作用域不同
class Variable{
//类成员变量
int a=0;
int b=0;
int c=0;
void test(int a,int b){
this.a=a;
this.b=b;
int c=5; //局部变量
System.out.println("--print data in method test--");
System.out.println("a="+a+" b="+b+" c="+c);
}
}
public class VariableDemo{
public static void main(String args[]){
Variable v=new Variable();
System.out.println("--print original data--");
System.out.println("a="+v.a+" b="+ v.b+" c="+v.c);
v.test(3,4);
System.out.println("--print data after method test--");
System.out.println("a="+v.a+ " b="+ v.b+" c="+v.c);
}
}
程序运行结果:
--print original data--
a=0 b=0 c=0
--print data in method test--
a=3 b=4 c=5
--print data after method test--
a=3 b=4 c=0
在C程序中main()作为一个程序的入口方法,在Java中也同样利用这个方法来启动一个Java程序。main()使用一个字符串数组作为参数,它表示启动Java这个程序时的命令行参数,在下面的例子中我们展现了如何使用main的这个参数。
例3.1.2 TestMain.java
public class TestMain {
public static void main(String[] args) {
for(int i=0; i < args.length; i++) {
System.out.println("参数[" + i + "]:" + args[i]);
}
}
}
程序运行结果:
C:/>java TestMain Hello World
参数[0]:Hello
参数[1]:World
命令行参数并不是必须的,但大多数应用都热衷于这种方式向程序输入一组参数。需要指出的是,在上例中Hello对应的args索引为0,World对应的args索引为1,如此类推,熟悉C语言的会发现其中的不同。
main方法应该声明为static,否则虽然程序编译可以通过,但运行时Java虚拟机将抱怨main方法不存在。错误如下:
Exception in thread "main" java.lang.NoSuchMethodError: main
方法执行结束,可以向调用者返回一个值,返回值的类型必须匹配方法声明中的返回类型,返回值类型可以是基础数据类型,也可以是一个对象类型。返回值跟随在方法体内的return语句之后,在一个void类型的方法中也可以包含return语句,不过此时return语句后不能跟随变量,它只是表示方法已执行完毕。在一个方法体内可能同时包含多个return语句,在程序运行中不管遇到那个return语句,则表示方法执行结束,并返回方法的调用者。
public class MessageQueue{
private Vector queue;
//构建器不能有返回值
public MessageQueue(){
...
}
//无返回值
public void add(Message m){
queue.add(m);
}
//返回boolean类型
//与C语言不同,对于返回boolean类型的方法,不能返回一个整数
public boolean isEmpty(){
if(queue.size()==0){
return true;
}
return false;
}
//返回一个对象
//对于对象类型的返回值,可以返回一个null
public Message get(){
if(isEmpty()){
return null;
}
Message first =(Message)queue.elementAt(0);
queue.remove(0);
return first;
}
}
3.1.2.3构建器
当一个类实例化时,调用的第一个方法就是构建器。构建器是方法的一种,但它和一般方法有着不同的使命。构建器(constructor)是提供对象初始化的专用方法。它和类的名字相同,但没有任何返回类型,甚至不能为void类型。
构建器在对象创建时被自动地调用,它不能被显式调用。
Java 中的每个类都有构建器,用来初始化该类的对象。如果在定义Java类时没有创建任何构建器,Java编译器自动添加一个默认的构建器。例如:
class Point{
double x,y;
}
等同于
class Point{
double x,y;
Point(){ //默认构建器。
}
}
在类中,可以通过方法的重载来提供多个构建器:
例3.1.3 Point.java
class Point{
double x,y;
//构建器的重载
Point(){
x=0;
y=0;
}
Point(double x){
this.x=x;
y=0;
}
Point(double x, double y){
this.x=x;
this.y=y;
}
Point(double x, double y, double z){
this.x=x;
this.y=y+z;
}
}
我们还可以在构建器中利用this关键字调用类中其他构建器,需要注意的是利用this来调用类中其他构建器时,其必须放在代码第一行。例如上例中的不带任何参数的构建器Point()可以改写为:
Point(){
this(0, 0);
}
在调用类的构建器创建一个对象时,当前类的构建器会自动地调用父类的默认构建器,也可以在构建器中利用super关键字指定调用父类的特定构建器。一般方法可以被子类继承,而构建器却不可被继承。与this关键字一样,super调用也必须放在代码第一行。例如:
class StylePoint extends Point{
int style;
StylePoint(double x, double y , int style) {
super(x, y);
this.style = style;
}
}
√构建器只能由new运算符调用。
new运算符,除了分配存储之外,还初始化实例变量,调用实例的构建器。
下面的例子是分配并初始化Point的一个实例:
Point a; //声明
a = new Point( ); //初始化
以下构建器是带有参数的例子:
Point b; //声明
b = new Point(3.4, 2.8); //初始化
对象的声明并不为对象分配内存空间,而只是分配一个引用空间;对象的引用类似于指针,是32位的地址空间,它的值指向一个中间的数据结构,它存储有关数据类型的信息以及当前对象所在堆的地址,而对于对象所在的实际的内存地址是不可操作的,这就保证了安全性。
√类是用来定义对象状态和行为的模板,对象是类的实例。类的所有实例都分配在可作无用单元回收的堆中。声明一个对象引用并不会为该对象分配存储,程序员必须显式地为对象分配存储,但不必显式删除存储,因为无用单元回收器会自动回收无用的内存。
3.1.3关键字this
前面多次用到了关键字this,在一个方法内部如果局部变量与类变量的名字相同,则局部变量隐藏类变量,在这种情况下如果要访问类变量,我们必须使用this。
在类的构建器和非静态方法内,this代表当前对象的引用。利用关键字this,可以在构建器和非静态方法内,引用当前对象的任何成员。
√this用在方法中,表示引用当前对象。
其实在Java程序设计中,一个方法引用它自己的实例变量及其他实例方法时,在每个引用的前面都隐含着“this”。例如:
class Test{
int a,b,c;
…
void myPrint ( ) {
print (a+ "/n"); // 等价于 print (this.a+ "/n");
}
…
}
一个对象要把自己作为参数传给另一个对象时,就需要用到this。例如:
class MyClass {
void method (OtherClass obj) {
…
obj.method (this)
…
}
}
构建器用来创建和初始化一个对象,通过构建器可以传入多个参数用来初始化类的成员变量,如果对应的构建器参数和类的成员变量具有相同的名字,则不失为一种良好的编程风格,使得程序一目了然,此时就需要使用this来引用类成员变量。例:
class Moose {
String hairDresser;
Moose(String hairDresser) {
this.hairDresser = hairDresser;
}
}
在此例中,类Moose的构建器的参数hairDresser和类成员变量具有同样的名字。具有this 前缀的变量hairDresser指向类成员,而不具有this 前缀的变量hairDresser指向构建器的参数。
另外在构建器中,我们还可以利用0个或多个参数的this()方法,调用该类的其他构建器,这种方法称为显式构建器调用。
除了this之外,super关键字可用于访问超类中被隐藏的变量和被改写的方法。关于super的细节将在3.2.2.2节讨论。
3.1.4方法重载
方法重载(overload)是指多个方法具有相同的名字,但是这些方法的参数必须不同(或者是参数的个数不同,或者是参数类型不同)。
方法在同一个类的内部重载,类中方法声明的顺序并不重要。
√返回类型不能用来区分重载的方法。方法重载时,参数类型的区分度一定要足够,例如不能是同一类型的参数。重载的认定是指要决定调用的是哪一个方法,在认定重载的方法时,不考虑返回类型。
例3.1.4 OverloadDemo.java
public class OverloadDemo{
//方法1
public void print(int a){
System.out.println("一个参数:a="+a);
}
//方法2
public void print(int a, int b) {
System.out.println("两个参数:a="+a+" b="+b);
}
public static void main(String args[]) {
OverloadDemo oe=new OverloadDemo();
oe.print(100);
oe.print(100,200);
}
}
程序运行结果:
一个参数:a=100
两个参数:a=100 b=200
√方法重载时,编译器会根据参数的个数和类型来决定当前所使用的方法。
通过参数个数来区分方法重载,还是比较容易分辨的;而通过参数类型来区分方法重载,就略显复杂了,有时要格外小心,避免出现“二义性”。
public class OverloadDemo2 {
//方法1
public void print(int a, long b, long c) {
...
}
//方法2
public void print(long a, long b, long c) {
...
}
}
当我们调用print(1, 2L, 3L),方法1将被调用;当我们调用print(1L, 2L, 3L),方法2将被调用。当我们调用print(1, 2, 3)时,哪一个方法将被调用?答案是方法1。因为方法1的第一个参数为int型,与print(1, 2, 3)的第1个参数正好匹配,虽然第2和第3参数类型并不相同,但相对于方法2来说,方法1与print(1, 2, 3)调用更为接近,所以方法1将被调用。
现在我们对OverloadDemo2的方法2略作修改,如下所示:
public class OverloadDemo3 {
//方法1
public void print(int a, long b, long c) {
...
}
//方法2
public void print(long a, int b, long c) {
...
}
}
当我们再次调用print(1, 2, 3)时,将会怎样?方法1和方法2和调用print(1, 2, 3)不存在哪个更近,这就产生了一种模棱两可的情形。那么参数顺序是否会产生影响呢?不会!如果利用参数顺序来解决这个问题,必将给程序编写带来极大的潜在危险。
对于这种二义性的情形,程序编译将不能通过,Java编译器将会指出具有二义性的方法调用。
3.1.5类继承
在构造一个新的类时,首先找到一个已有的类,新类在这个已有类的基础上构造,这种特性我们称之为继承,也可以称作派生(derived)。继承使用关键字extends声明。继承出的类称为原来类的子类,而原来类被称为父类或者超类。
√类的继承具有传递性:如果B是A的子类,C是B的子类,则C是A的子类。
下面是一个点类继承的例子。
//在平面直角坐标系中的点类
public class Point{
float x,y;
...
}
//可打印的点类
class PintablePoint extends Point implements Printable{
...
public void Print ( ) {
}
}
关键字extends只能引出一个超类superClassName,即Java语言仅支持单继承(single inheritance)。
Java程序运行时建立的每个对象都具有类Object定义的数据和功能。例如,每个对象可调用类Object定义的实例方法equals和toString。方法equals用于比较两个对象是否相等,方法toString用于将对象转换成字符串的描述形式。
所有的类均从一个根类 Object中派生出来。除Object之外的任何类都有一个直接超类。如果一个类在声明时未指明其直接超类,那么缺省即为Object。
例如:
class Point {
float x,y;
}
与下面写法等价:
class Point extends Object {
float x,y;
}
继承关系使得Java中所有的类构成一棵树,Object类就是这棵树的根。
√Java语言之所以没有采用C++的多继承机制,是为了避免多继承带来的诸多不便,比如:二义性的产生、编译器更加复杂、程序难以优化等问题。Java语言虽然仅支持单继承,但是可以通过接口机制来支持某些在其他语言中用多继承实现的机制(详见第4章)。
3.1.6类的初始化过程
当创建一个对象时,对象的各个变量根据其类型被设置为相应的缺省初始值(详细设置见3.1.2.1节),然后调用构建器。每个构建器有三个执行阶段:
l 调用超类的构建器。
l 由初始化语句对各变量进行初始化。
l 执行构建器的体。
下面是一个例子:
class Mask{
int rightMask = 0x00ff;
int fullMask;
public Mask(){
fullMask = rightMask;
}
public int init(int orig){
return (orig&fullMask);
}
}
class EnhancedMask extends Mask{
protected int leftMask = 0xff00;
public EnhancedMask(){
fullMask |= leftMask;
}
}
若创建一个类型为EnhancedMask的对象,逐步完成构造,下面是每一步结束后各个变量的值:
表3.1.2 EnhancedMask对象的初始化过程表
步骤
动作
rightMask
leftMask
fullMask
1
各个变量设置为初始值
0
0
0
2
调用EnhancedMask构建器
0
0
0
3
调用Mask构建器
0
0
0
4
Mask变量初始化
0x00ff
0
0
5
Mask构建器执行
0x00ff
0
0x00ff
6
EnhancedMask变量初始化
0x00ff
0xff00
0x00ff
7
EnhancedMask构建器执行
0x00ff
0xff00
0xffff
在构建器中调用其他方法时,应密切注意程序代码的执行次序和当前成员变量的赋值变化,因为此时对象尚未创建完成,有些变量还没有初始化。上面第5步中,如果在Mask构建器最后调用了方法init(),fullMask的值此时为0x00ff,而不是0xffff;只有当EnhancedMask对象构造完成之后,fullMask的值才为0xffff。
另外,我们还必须注意有关多态性的问题,参见3.2.3小节。假若有一天,我们在类EnhancedMask中重写了init()方法,如果我们在Mask构建器调用用了init()方法,那么当我们创建EnhancedMask的对象时,它实际调用的是EnhancedMask的init()方法,而不是Mask的init()方法
√若在构造对象阶段需要调用方法,那么在这些方法设计时,必须考虑以上因素。而且,对于构建器所调用的每个非终结(non-final)方法,因为能被改写,所以对它们应该仔细编制文档,以告示他人;他们可能想要改写具有潜在限制的构建器。
3.1.7源文件
源文件是我们开发程序的基本单位,Java源文件是扩展名为java的纯文本文件。Java编译器处理Java源文件,输出Java字节码文件,扩展名为class的文件。
在一个Java源文件中只允许定义零个或一个public类或接口,但可以同时有不受限制多个default类和接口。如果源文件包含了public类或接口,则文件名必须和public类或接口一样;如果源文件中不包含public类或接口,文件名可以是任意合法的文件名。
一个Java源文件的内容通常由三个功能部分构成:
l package包声明:命名当前包。
l import包引入:引入其他程序包。
l 类和接口定义:定义新的类和接口。
下面的例子是一个典型的Java源文件的组成格式。
//包声明
package com.mycompany.myproject;
//引入其他程序包或类
import java.io.*;
import java.util.Vector;
//类定义
public class MyClass{
...
}
interface MyInterface{
...
}
不同的程序员,在组织源程序时有自己的爱好:有的喜欢在一个源文件中只存放一个类或是接口;而有的喜欢在同一个源文件中存放多个类或是接口。在例3.1.1中 ,源文件VariableDemo.java中同时存放了两个类Variable和VariableDemo。也可以将这两个类分别存放到不同的文件中。例如,将类Variable存放在文件Variable.java,将类VariableDemo存放在文件VariableDemo.java中。使用这种方式时,如何进行编译呢?可以有两种方法。一是使用通配符:
javac Variable*.java
这样,所有以Variable开头的并以.java结尾的源程序都得到编译。
另一种方法是直接键入:
javac VariableDemo.java
虽然我们并没有显式地指定对Variable.java进行编译,但是编译器在对VariableDemo.java进行编译时,发现其中使用了类Variable,因此会主动寻找Variable.class,如果没有能够找到,则继续寻找Variable.java并编译之。此外,即使Variable.class已经存在,如果Variable.java的时间戳比Variable.class的时间戳新,仍旧会对Variable.java进行编译。
3.2 面向对象特性
基于面向对象特性,我们来剖析Java语言机制。
3.2.1封装性
访问控制符是Java语言控制对方法和变量访问的修饰符。对象是对一组变量和相关方法的封装,其中变量表明了对象的状态,方法表明了对象具有的行为。通过对象的封装,实现了模块化和信息隐藏;通过对类的成员施以一定的访问权限,实现了类中成员的信息隐藏。
我们可以通过一个类的接口来使用该类。这里所指的接口不同于下一章中的接口(后者是Java语言的成分,是一种类型)。这里我们认为类的接口表示一个类的设计者和使用者之间的约定。一般地,一个类同时具有两种接口:保护(protected) 接口和公共(public) 接口。
l 一个类的保护接口是指该类中被说明为protected的所有变量和方法。通过保护接口扩展类,使子类可共享保护接口所提供的变量和方法,且只能被子类所继承和使用;子类还可以改写接口中提供的方法,可以隐藏接口中提供的变量。子类也可以直接使用其超类的公共接口。
l 一个类的公共接口是指该类中被说明为public的所有变量和方法。通过公共接口,该类可作为其他类的域成分(聚集),或者在其他类的方法中创建该类对象并使用(关联);若要如此使用一个类,仅能通过其公共接口来进行。
l 一个类中除了保护接口和公共接口,还有私有(private)成分。私有成分仅被该类所持有,并仅能被该类中方法访问。类的私有成分被严格保护,不容其他类直接访问,即使子类也不例外。
一个类把其私有成分封装起来,其保护接口和公共接口提供给其他类使用,这样的结构体现了类的“封装性”。封装性是面向对象程序设计的一个重要的特征。我们在设计或使用一个类时应谨慎考虑上述这些约定关系,依照类中各成分的语意做合适的安排。
3.2.1.1 private
类中用private修饰的成员,只能被这个类本身访问。
如果一个类的成员声明为private,则其他类(不管是定义在同一个包中的类、还是该类的子类或是别的类)都无法直接使用这个private修饰的成员。例如:
例3.2.1 PrivateDemo.java
class Display{
private int data =8;
private void displayData(){
System.out.println("data value=" + data);
}
}
class EnhancedDisplay extends Display{
void enhancedDisplayData(){
System.out.println("********");
displayData();
System.out.println("********");
}
void changeData(int x){
data = x;
}
}
public class PrivateDemo{
public static void main(String args[]){
EnhancedDisplay ed = new EnhancedDisplay();
ed.enhancedDisplayData();
ed.changeData(100);
ed.enhancedDisplayData();
}
}
编译PrivateDemo.java,出现如下错误:
PrivateDemo.java:10: displayData() has private access in Display
displayData();
^
PrivateDemo.java:14: cannot resolve symbol
symbol : variable i
location: class EnhancedDisplay
i = x;
^
2 errors
3.2.1.2 default
类中不加任何访问权限限定的成员属于缺省的(default)访问状态,这些成员可以被这个类本身和同一个包中的类所访问。例如,我们去掉例3.2.1中的两个private,然后再编译运行之,可以得到:
****************
data value=8
****************
****************
data value=100
****************
3.2.1.3 protected
类中限定为protected的成员,可以被这个类本身、它的子类(包括同一个包中以及不同包中的子类)和同一个包中的所有其他的类访问。
3.2.1.4 public
public成员可以被所有的类访问,包括包内和包外的类。
表3.2.1列出了上述这些访问控制符的作用范围。
表3.2.1 Java中类的访问控制符的作用范围比较
同一个类
同一个包
不同包的子类
不同包非子类
private
*
default
*
*
protected
*
*
*
public
*
*
*
*
√public可用于类、方法和变量。标以public的类、方法和变量,可为任何地方的任何其他类、方法访问。覆盖不能改变public访问权限。没有被指定public或private的类、方法和变量,只能在声明它们的包(package)中访问。
3.2.2继承性
面向对象的中最为强大的功能是类的继承,继承允许你在一个已经存在的类之上编写新的程序。比如想建立一个FillRect类,该类可以使用Rectangle类中所有已定义的数据和成员方法,如:width、height等数据和getArea等方法,就可以通过继承来实现。为了继承Rectangle类,你必须引用旧的Rectangle类(使用extends关键字),并且在新类的说明中引用它。如:
class FillRect extends Rectangle{
…
}
Java中所有的类都是通过直接或间接地继承Java.lang.Object类得到的。继承而得到的类称为子类,被继承的类称为父类。子类不能继承父类中访问权限为private的变量和方法。子类可以重写父类的方法,即命名与父类中同名的方法。
Java不支持多重继承,即不具有一个类从多个超类派生的能力,这种单一继承使类之间具有树型的层次结构。所有类共享该层次结构的根结点Object,也就是说,每个类都隐式地继承于Object。继承是面向对象程序设计的主要特点和优势之一。利用类继承,可利用已有的类方便地建立新的类,最大程度地实现代码重用。
Java由继承引出了“多态”的概念:方法的多态和类型的多态。
(1) 关于方法的多态,3.1.2.4节详细介绍了在一个类中方法的重载(overload),这是一种方法多态的形式。下一节还将引入了另一种方法多态的形式:扩展类继承其超类的方法,它们有相同的基调,但对方法的实现加以改写。这种方法多态形式在有些书中也称为方法的覆盖(override)。
(2) 类型的多态。假设由超类F扩展出类Z,即类Z继承了超类F。由类Z实例化创建的对象d不仅属于类Z,而且属于其超类F,也就是说,对象d的域包含了超类F的域,因此对象d也是超类F的对象。所以创建一个类对象,也隐含着创建了其超类的一个对象,因此,类构建器往往需要调用其超类构建器。另一个结论是,一个类的对象不仅可以以创建类的类型所引用,也可以以其超类的类型所引用。所以Object类型的引用可以引用任何对象。
例3.2.2 VehicleDemo.java
//类的继承
class Vehicle{//车辆类
int VehicleID; //性质:车辆的ID号
void setID(int ID){
VehicleID=ID;
}
void displayID( ) { //方法:显示ID号
System.out.println("车辆的号码是:"+ VehicleID);
}
}
class Car extends Vehicle{ //轿车类
int mph; //时速
void setMph(int mph){
this.mph=mph;
}
void displayMph( ) { //显示轿车的时速
System.out.println("轿车的时速是:"+ mph);
}
}
public class VehicleDemo{
public static void main(String[] args){
//产生一个车辆对象
Car benz = new Car();
benz.setID(9527);
benz.setMph(10);
benz.displayID();
benz.displayMph();
}
}
程序运行结果:
车辆的号码是:9527
轿车的时速是:10
在例3.2.2中,定义了两个类Car和Vehicle。显然,Car是Vehicle的一种,Car除了Vehicle的所有特性都有之外,它还延伸出自己一些特性。
继承关系是由原来的类延伸而来,所以子类所产生的对象也是父类的一种。就以上例为例,类Vehicle是类Car的父类,因此,由Car产生的对象实体,也是一种Vehicle。这样的关系在程序中,可以表示成,即使类型是父类的一个引用,也可以拿来引用子类所产生的对象。例如:
class Example {
Vehicle Benz;
Benz= new Car();// 父类型引用子类对象。
}
3.2.2.1成员变量的隐藏和方法的重写
子类通过隐藏父类的成员变量和重写父类的方法,可以把父类的状态和行为改变为自身的状态和行为,例如:
class A{
int x;
void setX(){
x=0;
}
}
class B extends A{
int x; //隐藏了父类的变量x
void setX(){ //重写了父类的方法 setX()
x=5;
}
}
√子类中重写的方法和父类中被重写的方法要具有相同的名字,相同的参数表和相同的返回类型,只是方法体不同。
3.2.2.2关键字super
Java中通过super来实现对父类成员的访问,super用来引用当前对象的父类。在扩展类的所有非静态方法中均可使用super关键字。在访问字段和调用方法时,super将当前对象作为其超类的一个实例加以引用。
使用super时应特别注意,super引用类型明确决定了所使用方法的实现。具体地说,super.method总是调用其超类的method实现,而不是在类层次中较下层该方法的任何改写后的实现。使用super关键字调用特定方法与其他引用不同,后者根据对象的实际类型选择方法,而不是根据引用的类型。当你通过super调用一个方法时,得到的是基于超类类型的方法的实现。
√super 的使用有三种情况:
访问父类被隐藏的成员变量,如:super.variable;
调用父类中被重写的方法,如:super.method([paramlist]);
调用父类的构建器,如:super([paramlist]);
下面例子说明了super的作用。
例3.2.3:SuperDemo.java
class Father{
int x;
Father(){
x=3;
System.out.println("Calling Father : x=" +x);
}
void doSomething(){
System.out.println("Calling Father.doSomething()");
}
}
class Son extends Father{
int x;
Son(){
//调用父类的构造方法
//super() 必须放在方法中的第一句
super();
x=5;
System.out.println("Calling Son : x="+x);
}
void doSomething() {
super.doSomething( ); //调用父类的方法
System.out.println("Calling Son.doSomething()");
System.out.println("Father.x="+super.x+" Son.x="+x);
}
}
public class SuperDemo{
public static void main(String args[]) {
Son son=new Son();
son.doSomething();
}
}
程序运行结果:
Calling Father : x=3
Calling Son : x=5
Calling Father.doSomething()
Calling Son.doSomething()
Father.x=3 Son.x=5
√在Java中,this通常指当前对象,super则指父类。当你想要引用当前对象的某种东西,比如当前对象的某个方法,或当前对象的某个成员,你便可以利用this来实现这个目的,当然,this的另一个用途是调用当前对象的另一个构建器。如果你想引用父类的某种东西,则非super莫属。
3.2.2.3类Object
类java.lang.Object处于Java开发环境的类层次的根部,其他所有的类都是直接或间接地继承了此类。Object类定义了一些最基本的状态和行为。
一些常用的方法有:
equals() :比较两个对象(引用)是否相同。
getClass():返回对象运行时所对应的类的表示,从而可得到相应的信息。
toString():用来返回对象的字符串描述。
finalize():用于在垃圾收集前清除对象。
notify(),notifyAll(),wait():用于多线程处理中的同步。
3.2.3多态性
在Java语言中,多态性体现在两个方面:由方法重载实现的静态多态性(编译时多态)和方法覆盖实现的动态多态性(运行时多态)。
(1) 编译时多态。在编译阶段,编译器会根据参数的不同来静态确定调用相应的方法,即具体调用哪个被重载的方法。
(2) 运行时多态。由于子类继承了父类所有的属性(私有的除外),所以子类对象可以作为父类对象使用。程序中凡是使用父类对象的地方,都可以用子类对象来代替。一个对象可以通过引用子类的实例来调用子类的方法。
√重载方法的调用原则:Java运行时系统根据调用该方法的实例,来决定调用哪个方法。对子类的一个实例,如果子类重写了父类的方法,则运行时系统调用子类的方法;如果子类继承了父类的方法(未重写),则运行时系统调用父类的方法。
例3.2.4 Dispatch.java
class C{
void abc() {
System.out.println("Calling C's method abc");
}
}
class D extends C{
void abc() {
System.out.println("Calling D's method abc");
}
}
public class Dispatch{
public static void main(String args[]) {
C c=new D();
c.abc( );
}
}
程序运行结果:
Calling D's method abc
√在上例中,父类对象c引用的是子类的实例,所以,Java运行时调用子类D的abc方法。
下面我们再举一例来说明在对象创建过程中,发生在构建器中的多态性方法调用的例子。
例3.2.5 SuberClass.java
class BaseClass{
public BaseClass(){
System.out.println("Now in BaseClass()");
init();
}
public void init(){
System.out.println("Now in BaseClass.init()");
}
}
public class SuberClass extends BaseClass{
public SuberClass() {
System.out.println("Now in SuberClass()");
}
public void init() {
System.out.println("Now in SuberClass.init()");
}
public static void main(String[] args) {
System.out.println("创建BaseClass对象:");
new BaseClass();
System.out.println("创建SuberClass对象:");
new SuberClass();
}
}
程序运行结果:
创建BaseClass对象:
Now in BaseClass()
Now in BaseClass.init()
创建SuberClass对象:
Now in BaseClass()
Now in SuberClass.init()
Now in SuberClass()
3.3关键字static
有时候,可能需要同类的各对象之间共享某些变量或者方法,这些共享的变量或是方法被称为类变量或者类方法。类变量或者类方法统称为静态对象。
用static 关键字来声明类变量和类方法,格式如下:
类变量:
static variableType variableName;
类方法:
static returnType classMethod([paramlist]) {…}
√每个实例对象对类变量的改变都会影响到其他的实例对象。类变量可通过类名直接访问,无需先生成一个实例对象,也可以通过实例对象访问类变量。
√类方法不能访问实例变量,只能访问类变量。类方法可以由类名直接调用,也可由实例对象进行调用。类方法中不能使用this或super关键字。
√静态变量可以有初值,就象实例变量一样。静态变量和方法都是通过类名字来访问的。为方便起见,也可以用具体对象来访问。
例3.3.1演示了显式调用静态方法。
例3.3.1 StaticDemo.java
//静态方法的调用
class MyMath{
static final double PI=3.14159;
static int max(int a,int b){
return a>b?a:b;
}
}
public class StaticDemo{
public static void main(String[] args){
//无需创建实例,即可直接调用
System.out.println("PI="+MyMath.PI);
System.out.println("max(100,200)="+MyMath.max(100,200));
}
}
调用静态方法的格式——“类名.方法名”。
√如果在声明时不用static 关键字修饰变量或者方法,则表示是实例变量或实例方法。
√每个对象的实例变量都独立分配内存,通过对象来访问这些实例变量。实例方法可以对当前对象的实例变量进行操作,也可以对类变量进行操作,实例方法由实例对象调用。
例3.3.2 StaticDemo2.java
//静态成员的使用
class StaticDemo2{
//实例变量
int i;
//静态变量
static int j;
static int arr[ ] = new int[12];
//静态初始成员,初始化数组
static{
for(int i=0;i<arr.length;i++)
arr[i] =i;
}
//实例方法
void setI(int i){
this.i =i;
}
//静态方法
static void setJ(int j) {
StaticDemo2.j=j;
}
public static void main(String []args ){
StaticDemo2 sd2 = new StaticDemo2();
StaticDemo2.j=2; //正确: 通过类访问静态变量
sd2.j =3; //正确: 通过实例访问静态变量
StaticDemo2.setJ(2); //正确: 通过类访问静态方法
sd2.setJ(3); //正确: 通过实例访问静态方法
sd2.i=4; //正确: 通过实例访问实例变量
sd2.setI(7); //正确: 通过实例访问实例方法
StaticDemo2.i=5; //错误: 通过类访问实例变量
StaticDemo2.setI(5); //错误: 通过类访问实例方法
}
}
√static也可以来修饰一个类,称为静态类。通常只有内部类才允许声明为静态的。
3.4关键字final
final 关键字可以修饰类、变量和方法。
3.4.1 final变量
在第2章中,我们已经知道,使用final关键字修饰的变量,只能被初始化一次,也即变量一旦被初始化便不可改变。这里的不可改变具有两层含义:对基本类型来说是其值不可改变;对于对象变量来说其引用不可改变。
√用final修饰的变量,自动成为常量。
格式:
final variableType variableName;
3.4.2 final方法
方法被声明为final,格式为:
final returnType methodName(paramList) {…}
表示这个方法不需要进行扩展(继承),也不允许任何子类覆盖这个方法,但是可以继承这个方法。
例3.4.1 FinalMethodDemo.java
class FinalMethod{
final void aMethod(){
System.out.println("a final method");
}
}
public class FinalMethodDemo extends FinalMethod{
//错误:不能覆盖父类的final方法
void aMethod(){
System.out.println("override a final method");
}
}
编译该程序,报错如下:
FinalMethodDemo.java:8: aMethod() in FinalMethodDemo cannot override aMethod() in FinalMethod; overridden method is final
void aMethod(){
^
1 error
3.4.3 final类
final修饰类,格式为:
final class finalClassName{…}
表示这个类不能被任何其他类继承。final类中的方法,自然也就成了final型的。你可以显式定义其为final,也可以不显式定义为final,效果都一样。
例3.4.2 FinalClassDemo.java
final class FinalClass{
void method (){
}
}
//错误:不能继承final类
public class FinalClassDemo extends FinalClass{
}
编译该程序,报错如下:
FinalClassDemo.java:6: cannot inherit from final FinalClass
public class FinalClassDemo extends FinalClass{
^
1 error
下面的程序演示了final方法和final类的用法。
例3.4.3 FinalDemo.java
final class AFinalClass{
final String strA="This is a final String";
public String strB="This is not a final String";
final public void print(){
System.out.println("a final method named print()");
}
public void showString(){
System.out.println(strA+"/n"+strB);
}
}
public class FinalDemo{
public static void main(String[] args){
AFinalClass f=new AFinalClass();
f.print();
f.showString();
}
}
√final类与普通类的使用几乎没有差别,只是它失去了被继承的特性。
3.5对象复制
假设我们需要对一个对象进行复制,怎么办呢?很多读者会脱口而出——赋值,例如:obj2=obj1。但是这个方法实际上并没有复制对象,而仅仅是建立一个新的对象引用,在执行这个操作后仍然只有一个对象,新建对象引用obj2也指向了对象引用obj1所指的对象。
本节介绍一个极其有用的方法Object.clone(),实现对象的复制。
既然clone是类Object中的一个方法,那么它能否向toString()这些方法一样,直接调用呢?我们来看下面一个例子。
例3.5.1 CloneDemo.java
class AnObject{
private int x;
public AnObject(int x){
this.x =x;
}
public int getX(){
return x;
}
}
public class CloneDemo{
public static void main(String args[]){
AnObject obj1 = new AnObject(100);
AnObject obj2 = (AnObject)obj1.clone();
System.out.println("obj1 locate at "+obj1+" x="+obj1.getX());
System.out.println("obj2 locate at "+obj2+" x="+obj2.getX());
}
}
上面的代码会引发编译错误。查阅Java API文档,我们会发现:Object.clone()是一个protected方法,因此不能直接调用clone()方法。我们将类AnObject修改如下:
class AnObject{
private int x;
public AnObject(int x) {
this.x =x;
}
public int getX(){
return x;
}
public Object clone(){
try{
return super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
return null;
}
}
}
修改后的类AnObject定义了自己的clone()方法,它扩展Object.clone()方法。虽然CloneDemo可以编译,但是当你运行它时会抛出一个CloneNotSupportedException异常。通过阅读Java API文档我们发现,还必须让那些包含clone()方法的类实现Cloneable接口:
class AnObject implements Cloneable{
private int x;
public AnObject(int x){
this.x =x;
}
public int getX(){
return x;
}
public Object clone(){
try{
return super.clone();
}catch (CloneNotSupportedException e){
e.printStackTrace();
return null;
}
}
}
再次编译并运行CloneDemo,得到:
obj1 locate at AnObject@182f0db x=100
obj2 locate at AnObject@192d342 x=100
观察运行结果,显然obj2复制了obj1,因为这两个对象中存储的x值均为100,并位于内存中不同的位置。
由上面的程序可知,要使得一个类的对象具有复制能力,必须显式地定义clone()方法,并且该类必须实现Cloneable接口。Cloneable接口中没有定义任何内容,只是起“标记”的作用,说明类的设计者已经为该类设计了复制的功能(这一点与第4章将要讲到的接口有所区别)。如果类没有实现Cloneable接口,则在运行时会抛出一个CloneNotSupportedException异常。
3.6内部类
在一个类的内部我们还可以定义类,这就是内部类,也称为嵌套类。内部类的定义范围要比包小,它定义在另一个类里面,也可以定义在一个方法里面,甚至可以定义在一个表达式中。与内部类相对而言,包含内部类的类成为外部类或顶级类。
内部类本身是一个类,但它同时又是外部类一个成员。作为外部类的成员,它可以毫无限制地访问外部类的变量和方法,包括private成员。这和private的含义并不矛盾,因为private修饰符只是限制从一个类的外部访问该类成员的权限,而内部类在外部类内部,所以它可以访问外部类的所有资源。
内部类又具有多种形式,可细分为:静态内部类、成员内部类、本地内部类和匿名内部类。在一个顶级类中声明一个类,并用static修饰符修饰的该类,就是静态内部类。例如:
package mypackage;
public class OuterClass {
//...
public static class StaticInnerClass {
//...
}
}
该例中静态内部类的完全限定名称为mypackage.OuterClass.StaticInnerClass,编译时Java产生两个class文件OuterClass.class和OuterClass$StaticInnerClass.class。静态内部类作为外部类的静态成员,和其他静态变量、静态方法一样与对象无关,静态内部类只可以访问外部类的静态变量和静态方法,而不能直接引用定义在外部类中的实例变量或者方法,但可以通过对象的引用来使用它们。
匿名内部类是没有名字的内部类。由于匿名内部类没有名称,在程序中没办法引用它们。在Java中创建匿名内部类对象的语法如下:
new 类或接口() {类的主体}
这种形式的new语句声明一个匿名内部类,它对一个给定的类进行扩展,或者实现一个给定的接口。它还创建匿名内部类的一个对象实例,并把这个对象实例作为new语句的返回值。在Java程序中,匿名内部类使用十分广泛,它常被用来实现某个接口。例如实现Enumeration接口(它定义在java.util包中),Enumeration接口提供了方法用来遍历集合数据结构的每个成员而不暴露集合对象本身。
下面通过一个例子来说明匿名内部类的用法,该例子实现了一个简单的动态数组。
例3.6.1 JDynamicArray.java
import java.util.Enumeration;
public class JDynamicArray{
private Object[] array = null;
private int count = 0;
public JDynamicArray(int size) {
if (size <= 0) {
throw new IllegalArgumentException("size must > 0");
}
array = new Object[size];
}
public int add(Object obj) {
if (count == array.length) {
Object[] newArray = new Object[array.length * 2 + 1];
for (int i = 0; i < array.length; i++) {
newArray[i] = array[i];
}
array = newArray;
}
array[count++] = obj;
return count;
}
public Enumeration getEnumeration() {
//匿名内部类实现Enumeration接口
return new Enumeration() {
private int index = 0;
public boolean hasMoreElements() {
return index < count;
}
public Object nextElement() {
return array[index++];
}
};
}
public static void main(String[] args) {
JDynamicArray da = new JDynamicArray(10);
for(int i=0; i<10; i++) {
da.add(new Integer(i));
}
for (Enumeration e = da.getEnumeration(); e.hasMoreElements(); ) {
System.out.println(e.nextElement());
}
}
}
程序运行结果:
0
1
...
9
3.7小结
类是Java面向对象编程的基本元素,它定义了一个对象的结构和功能。
一个类可用关键词final限定。用final限定的类是最终类(final class),不能作为其他类的超类。类定义也可用关键词public限定。用public限定的类是公开类。没用public限定的类的使用范围是包(package)。公开类和公开接口可被其他包中的类和接口的定义使用,未使用public修饰的类和接口只能被同一个包中的类和接口使用。
类的构建器定义与方法定义类似,但没有返回类型。构建器初始化新建立的对象,其名与类名相同。构建器不被子类继承。如果一个类没有定义任何构建器,Java编译器会自动生成空的缺省构建器(default constuctor)。
每个Java变量或直接保存数据、或保存指向对象的引用。成员变量有两种,用static 关键字修饰的变量为类变量,无static 修饰的变量为实例变量。相应地,成员方法也有两种,用static 修饰的为类方法,无static修饰的为实例方法。实例方法不仅可以对当前对象的实例变量进行操作,也可以对类变量进行操作;但类方法只能访问类变量。实例变量和实例方法必须由实例对象来调用,而类变量和类方法不仅可由实例对象来调用,还可由类名直接调用。Java通过在类定义的花括号里声明变量来把数据封装在一个类里,这里的变量称为成员变量。为了解决类名可能相同的问题,Java 中提供包来管理类名空间。
Java语言允许多态方法命名,即用一个名字声明方法,这个名字已在这个类或其超类中使用过,从而实现方法的覆盖(override)及重载(overload)。所谓覆盖是对继承来的方法提供另一种不同的实现。而重载是指声明一个方法,它与另外一个方法有相同的名字,但参数表不同。
习 题
3.1 选择题
(1) 程序TestSketch.java的代码如下,4个选项中正确的描述是( )。
class A{
}
class B extends A{
}
public class TestSketch{
public static void main(String args[]){
A a=new A();
B B=(B)a;
}
}
A. 通过编译并正常运行
B. 编译时出现异常
C. 编译通过,运行时出现异常
D. 编译器报告找不到TestSketch.java
(2) 下面有关类Demo的描述正确的有( )。
public class Demo extends Base{
private int count;
public Demo(){
System.out.println("A Demo object has been created");
}
protected void addOne(){
count++;
}
}
A. 当创建一个Demo类的实例对象时,count的值为0
B. 当创建一个Demo类的实例对象时,count的值是不确定的
C. Base类型的对象中可以包含改变count 值的方法
D. Demo的子类对象可以访问count
(3) 阅读下面的程序,正确的选项为( )。
class Person{
String name;
String nickName;
public Person(String s,String t){
name=s;
nickName=t;
}
public String toString(){
return name+" nickname="+nickName;
}
}
public class Teacher extends Person{
String rank;
public Teacher(String s,String t,String r){
super(s,t);
rank=r;
}
public String toString(){
return name+" nickname="+nickName+" rank="+rank;
}
public static void main(String args[]){
Person a= new Person("Tom","Tiger");
Person b= new Teacher("Jack","Horse","Professor");
Teacher c= new Teacher("Bobby","Elephant","Lecturer");
System.out.println("a is "+a.toString());
System.out.println("b is "+b.toString());
System.out.println("c is "+c.toString());
}
}
A. 编译时会出现错误
B. 运行时会出现错误
C. 运行结果为:
a is Tom nickname=Tiger
b is Jack nickname=Horse rank=Professor
c is Bobby nickname=Elephant rank=Lecturer
D. 运行结果为:
a is Tom nickname=Tiger
b is Jack nickname=Horse
c is Bobby nickname=Elephant rank=Lecturer
3.2 阅读程序ShapeTest.java,写出其运行结果。
class Shape{
public void draw(){
System.out.println("draw Shape");
}
}
class Circle extends Shape{
public void draw(){
System.out.println("draw Circle");
}
}
class Rectangle extends Shape{
public void draw(){
System.out.println("draw Rectangle");
}
}
public class ShapeTest{
public static void main(String[] args){
Shape []shapes=new Shape[3];
shapes[0]=new Shape();
shapes[1]=new Rectangle();
shapes[2]=new Circle();
for(int i=0;i<shapes.length;i++)
shapes[i].draw();
}
}
3.3 假如我们在开发一个系统时需要对员工进行建模,员工包含3个属性:姓名、工号以及工资。经理也是员工,除了含有员工的属性外,另外还有一个奖金属性。请使用继承的思想设计出员工类和经理类。要求类中提供必要的方法进行属性访问。
3.4 定义一个自己的数学类MyMath。类中提供静态方法max,该方法接收3个同类型的参数(例如整形),返回其中的最大值。
3.5 下面的程序运行输出是什么?并请读者阅读附录1中关于参数传递的内容。
public class Util{
public static void swap(int a,int b){
int temp=a;
a=b;
b=temp;
}
public static void swap(String a,String b){
String temp=a;
a=b;
b=temp;
}
public static void swapArray(Object []a,int i,int j){
if(i!=j){
Object temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
public static void main(String []args){
int a=100;
int b=200;
System.out.println("---before swap---");
System.out.println("a="+a+" b="+b);
Util.swap(a,b);
System.out.println("---after swap---");
System.out.println("a="+a+" b="+b);
String aStr="Hello";
String bStr="World";
System.out.println("---before swap---");
System.out.println("aStr="+aStr+" bStr="+bStr);
Util.swap(aStr,bStr);
System.out.println("---after swap---");
System.out.println("aStr="+aStr+" bStr="+bStr);
String []strArray={"Hello","World"};
System.out.println("---before swap---");
System.out.println("strArray[0]="+strArray[0]+
" strArray[1]="+strArray[1]);
Util.swapArray(strArray,0,1);
System.out.println("---after swap---");
System.out.println("strArray[0]="+strArray[0]+
" strArray[1]="+strArray[1]);
}
}
- 第三章第三题
- 第三章第三题
- 第三章第三题
- 第三章
- 第三章
- 第三章
- 第三章
- 第三章
- 第三章
- 第三章
- 《第三章》
- 第三章
- 第三章
- 第三章
- 第三章
- 第三章
- 第三章
- 第三章
- iis日志搜索引擎相关访问次数统计vbs版
- C语言专题——格式化I/O详解
- 配置数据库链接池 tomcat
- 水杉
- [转载]JSP与JavaMail (一)
- 第三章
- [转载]JSP与JavaMail (二)
- 070616清远漂流
- 解决Ubuntu 7.04无法在外接投影机上显示图像的方法
- 古树幻灯片图片
- Ubuntu 版本数字及开发代号之谜
- sa 权限 创建系统用户
- 帕虫”(瑞星) “AV终结者”(金山)“U盘寄生虫”(江民)』查杀综述
- 第四章