820 likes | 1.02k Views
第 5 章 面向对象程序设计. 5.1 类( class). 类是组成 Java 程序的基本元素,它封装了一系列的变量(即数据成员,也称为“域( field)”) 和方法(即成员方法 method), 是一类对象的原型。创建一个新的类,就是创建一个新的数据类型。实例化一个类,就得到一个对象。因此,对象就是一组变量和相关方法的集合,其中变量表明对象的状态、属性,方法表明对象所具有的行为。. 5.1.1 类定义 对一个用户自定义的类,要为类取一个名字,并指明类中包含哪些变量和方法以及相应的类型、实现等,这称为类的定义。类定义语句的一般形式为:
E N D
5.1 类(class) • 类是组成Java程序的基本元素,它封装了一系列的变量(即数据成员,也称为“域(field)”)和方法(即成员方法 method),是一类对象的原型。创建一个新的类,就是创建一个新的数据类型。实例化一个类,就得到一个对象。因此,对象就是一组变量和相关方法的集合,其中变量表明对象的状态、属性,方法表明对象所具有的行为。
5.1.1 类定义 • 对一个用户自定义的类,要为类取一个名字,并指明类中包含哪些变量和方法以及相应的类型、实现等,这称为类的定义。类定义语句的一般形式为: [修饰符]class 类名 [extends 父类名] [implements 接口名表] { 成员变量声明 // 类体 方法成员声明 } • 其中class、extends和implements都是Java的关键字。修饰符、extends(表示继承)和implements(表示接口)等内容,以后逐渐介绍。类定义中必须要写的内容是类名,应该使用Java语言合法的标识符来对自定义的类命名。
在大括号中定义类体,指明该类中包含的数据成员和成员方法。在Java语言中也允许定义没有任何成员的空类。在大括号中定义类体,指明该类中包含的数据成员和成员方法。在Java语言中也允许定义没有任何成员的空类。 • 【例5.1】定义一个名为Rect表示长方形的类,它仅包含double类型的长length和宽width两个数据成员。 class Rect { double length; double width; } • 在类的定义中,可加入对数据成员进行操作的方法成员。 • 【例5.2】定义一个名为Rectangle表示长方形的类,它包含double类型的长length和宽width两个数据成员和设置长方形长宽值的方法setDim()、计算长方形面积的方法area()。
class Rectangle { double length; double width; double area() { return length * width; } void setDim(double w, double l) { width = w; length = l; } }
在一个程序中,有时需要定义多个类。多个类的定义形式有两种:并列和嵌套。在一个程序中,有时需要定义多个类。多个类的定义形式有两种:并列和嵌套。 • 常见的多个类定义形式为并列定义,即一个类接着一个类进行定义,它们之间是并列的关系。 • 另一种形式为嵌套定义,即在一个类中定义另外的类,它们之间是包含和被包含的关系,可分别称为包含类和内部类(或嵌套类)。采用何种形式定义多个类,由类之间的访问关系确定。
类定义了一个类型(type)。与Java语言提供的几种基本类型一样,类型用来声明、定义该类型的变量。例如,下面的语句:类定义了一个类型(type)。与Java语言提供的几种基本类型一样,类型用来声明、定义该类型的变量。例如,下面的语句: Rect rect1; 声明变量rect1的类型为类Rect。 • 类型为类的变量与基本类型变量有所不同,类是一种引用(reference)类型。实际上,rect1是一个对类型为类Rect的对象的引用,rect1不是对象本身,可理解为一个指针,上述声明仅生成一个空(null)引用。
5.1.2 类对象 • 一旦定义了所需的类,就可以创建该类的变量,创建类的变量称为类的实例化,类的变量也称为类对象、类的实例等。 • 类的对象是在程序运行中创建生成的,其所占的空间在程序运行中动态分配。当一个类的对象完成了它的使命,为节省资源,Java的垃圾收集程序就会自动收回这个对象所占的空间。即类对象有自己的生命周期。 • 1.创建对象 • 创建类的对象需用new运算符,一般形式为: 对象名 = new 类名()
new运算符用指定的类在内存中分配空间,并将存储空间的引用存入语句中的对象变量。new运算符用指定的类在内存中分配空间,并将存储空间的引用存入语句中的对象变量。 例如:rect1 = new Rect(); • new运算符也可以与类声明一起使用来创建类的对象。 例如:Rect rect1 = new Rect(); • 2.引用对象 • 在创建了类的对象后,就可以对对象的各个成员进行访问,进行各种处理。访问对象成员的一般形式为: 对象名.数据成员名 对象名.成员方法名() // 方法名带圆括号
运算符“.”在这里称为成员运算符,在对象名和成员名)之间起到连接的作用,指明是哪个对象的哪个成员。运算符“.”在这里称为成员运算符,在对象名和成员名)之间起到连接的作用,指明是哪个对象的哪个成员。 • 例如:设已经定义了例5.2中的Rectangle类,可以用如下的方法来引用对象的成员: Rectangle r = new Rectangle(); // 定义类的对象 r.length,r.width // 引用对象的数据成员 r.area() // 引用对象的成员方法 • 【例5.3】利用例5.1定义的类Rect,计算长和宽分别为20和10的长方形面积。
class RectDemo { public static void main(String args[]) { Rect rect1 = new Rect(); // 为对象rect1分配存储空间 double area; rect1.width = 20; // 向数据成员赋值 rect1.length = 10; area = rect1.width * rect1.length; //对数据成员运算 System.out.println("长方形面积是:" + area); } } • 本程序由两个类构成,设已经编译类Rect文件Rect.class和RectDemo.class,程序的运行结果如下: 长方形面积是:200.0
若程序需要更多的类对象,可用new运算符多次为不同的对象分配空间,但各个对象有各自自己的存储空间,它们是互不相干的。若程序需要更多的类对象,可用new运算符多次为不同的对象分配空间,但各个对象有各自自己的存储空间,它们是互不相干的。 • 【例5.4】利用例5.1中定义的Rect类,分别计算长、宽分别为20、10和6、3的两个长方形面积。 class RectDemo1{ public static void main(String args[]){ Rect rect1 = new Rect(); Rect rect2 = new Rect(); double area;
rect1.width = 10; rect1.length = 20; area = rect1.width * rect1.length; System.out.println("长=20,宽=10的长方形面积是:" + area); rect2.width = 3; rect2.length = 6; area = rect2.width * rect2.length; System.out.println("长= 6,宽= 3的长方形面积是:" + area); } } 程序运行结果如下: 长=20,宽=10的长方形面积是:200.0 长= 6,宽= 3的长方形面积是:18.0
【例5.5】用例5.2中定义的类Rectangle分别计算长、宽分别为20 、10和6 、3的两个长方形面积。 class RectDemo2 { public static void main(String args[]) { Rectangle rect1 = new Rectangle(); Rectangle rect2 = new Rectangle(); double ar; rect1.setDim(20, 10); // 初始化每个长方形 rect2.setDim(6, 3); ar = rect1.area();
System.out.println("第一个长方形的面积是: " + ar); ar = rect2.area(); System.out.println("第二个长方形的面积是: " + ar); } } • 程序运行结果如下: • 第一个长方形的面积是: 200.0 • 第二个长方形的面积是: 18.0
5.1.3 构造方法 • 在Java中,任何变量在被使用前都必须先设置初值。Java提供了为类的成员变量赋初值的专门功能:构造方法(constructor)。构造方法是一种特殊的成员方法,它的特殊性反映在如下几个方面: (1)构造方法名与类名相同。 (2)构造方法不返回任何值,也没有返回类型。 (3)每一个类可以有零个或多个构造方法。 (4)构造方法在创建对象时自动执行,一般不能显式地直接调用。
【例5.6】分别计算长、宽分别为20、10和6、 3的两个长方形面积。本程序用构造方法来初始化长方形的大小。 class RectConstructor{ double length; double width; double area() { return length * width; } RectConstructor(double l, double w) { length = l; width = w; } }
class RectDemo3 { public static void main(String args[]) { RectConstructor rect1= new RectConstructor (20,10); RectConstructor rect2=new RectConstructor (6,3); double ar; ar = rect1.area(); System.out.println("第一个长方形的面积是: " + ar); ar = rect2.area(); System.out.println("第二个长方形的面积是: " + ar); } } • 程序运行结果如下: • 第一个长方形的面积是: 200.0 • 第二个长方形的面积是: 18.0
需注意的是,当方法虚拟参数名与成员变量名相同时,使用时会产生混淆,在Java语言中,可用this关键字表示本对象。需注意的是,当方法虚拟参数名与成员变量名相同时,使用时会产生混淆,在Java语言中,可用this关键字表示本对象。 • 在Java语言中,也允许构造方法重载,即定义多个构造方法。在有多个构造方法时,一个构造方法可以调用另一个构造方法,调用的方法是: this(实际参数表) • 这个语句调用与this中参数匹配的构造方法。 • 没有参数的构造方法叫做无参构造方法。一个类若没有任何用户定义的构造方法,Java会自动提供一个无参的空构造方法,在创建对象时,使用这个无参的构造方法为类对象的成员变量赋数据类型的缺省值。一旦用户定义了自己的构造方法,Java就不再提供无参构造方法。
【例5.7】构造方法重载的例。 class RectOverload{ double length; double width; double area(){ return length * width; } RectOverload(double l, double w) { length = l; width = w; } RectOverload(double s) { length = s; width = s; } }
class RectDemo4 { public static void main(String args[]) { RectOverload rect1 = new RectOverload(10,20); RectOverload rect2 = new RectOverload(6); double ar; ar = rect1.area(); // 调用area方法得到一个长方形的面积 System.out.println("长方形的面积是: " + ar); ar = rect2.area(); // 调用area方法得到一个正方形的面积 System.out.println("正方形的面积是: " + ar); } } • 程序运行结果如下: 长方形的面积是: 200.0 正方形的面积是: 36.0
5.1.4 类和成员的修饰符 • 在类和类的成员定义时可以使用一些修饰符(modifier)来对类和成员的使用作某些限定。一般将修饰符分为两类:访问控制符和非访问控制符。访问控制符有public、protected、private等,它们的作用是给予对象一定的访问权限,实现类和类中成员的信息隐藏。非访问控制符作用各不相同,有static、final、native、volatile、abstract等。某些修饰符只能应用于类的成员,某些修饰符既可应用于类也可应用于类的成员。
1.static修饰符 • 使用static修饰符来修饰类的成员变量和方法成员,使它们成为静态成员,也称为类成员。静态成员存储于类的存储区,属于整个类,而不属于一个具体的类对象。因为静态成员属于整个类,所以它被所有该类对象共享。在不同的类对象中访问静态成员,访问的是同一个。 • 对静态成员的使用要注意两点: (1)静态方法不能访问属于某个对象的成员变量,而只能处理属于整个类的成员变量。即静态方法只能处理静态变量。 (2)可以用两种方式调用静态成员,它们的作用相同。 变量:类名.变量、类对象.变量。 方法:类名.方法名()、类对象.方法名()。
程序运行结果如下: • a = 42 • b = 99 • 【例5.8】静态成员的使用。 class StaticDemo { static int a = 42; static int b = 99; static void callme() { System.out.println("a = " + a); } } class StaticByName { public static void main(String args[]) { StaticDemo.callme(); System.out.println("b = " + StaticDemo.b); } }
2.final修饰符 • final修饰符可应用于类、方法和变量。final的意义为终极或最终。final类不能被继承,即final类无子类。final方法不能被覆盖,即子类的方法名不能与父类的final方法名相同。final变量实际上是Java语言的符号常量,可在定义时赋初值或在定义后的其他地方赋初值,但不能再次赋值,习惯上使用大写的标识符表示final变量。例如: final double PI = 3.1416; final double G = 9.18; • 因为final变量不能改变,没有必要在每个对象中进行存储,可以将final变量声明为静态的,以节省存储空间。 例如:static final double PI = 3.1416;
3.abstract修饰符 • abstract修饰符可应用于类和方法,称为抽象类和抽象方法。抽象类需要继承、抽象方法需要在子类中实现才有意义。进一步的讨论见5.2.2抽象类和抽象方法。 • 4.native修饰符 • native修饰符一般用来声明用其他语言如C、C++、FORTRAN、汇编等书写方法体并具体实现方法功能的特殊方法。由于native方法的方法体使用其他语言编写。所以所有的native方法都没有方法体。native方法可应用于实时性强、执行效率高,运行速度要求较高的场合。
5.synchronized修饰符 • synchronized修饰符可应用于方法或块,在多线程 • 程序中,对用于共享的方法和块加互斥锁,使得任一时刻,synchronized方法或块只能由一个线程执行或操作。 • 5.1.5 类的使用 • 1.私有成员的访问 • 为了降低类间的耦合性,可以为类成员指定private修饰符,表示该成员只能在该类内部访问。若需要在其他类中访问私有成员,只能通过取数和送数的方法来访问。这样的方法常命名为getXxx()和setXxx()等。
【例5.9】私有成员的访问。 class RectangleRC { private double length; private double width; double getLength() { return length; } double getWidth() { return width; } RectangleRC(double l, double w) { length = l; width = w; } }
class RectDemo5 { public static void main(String args[]) { RectangleRC rect1 = new RectangleRC(8,7); double ar = rect1.getLength()*rect1.getWidth(); System.out.println(“长方形的面积是: ” + ar); } } • 程序运行结果如下: 长方形的面积是: 56.0
2.方法参数是类的对象 • 在Java语言中,方法的参数类型除了可以是基本类型外,还可以是引用类型-类。因为在类的对象中实际存储为对象的引用,因此在调用类参数时方法间传送的是引用。尽管Java采用值传送,引用从调用方法单向传送到被调方法,但由于调用方法与被调方法对应类参数的引用相同,它们引用同一对象,所以,若在被调方法中修改了引用类型形式参数的取值,则调用方法对应的实际参数也将发生相应的变化。即调用方法与被调方法之间是“引用单向传送,数据双向传送”。 • 应用引用类型的方法参数,可在方法间传送数据。
【例5.10】引用类型的方法参数是方法间传送数据的桥梁。【例5.10】引用类型的方法参数是方法间传送数据的桥梁。 class RefParameter{ double width,length,area; RefParameter(double w,double l){ width = w; length = l; } void calArea(RefParameter r){ r.area = r.width * r.length; } } class PassObject{ public static void main(String args[]){ RefParameter rr = new RefParameter(10,20); rr.calArea(rr); System.out.println("长方形面积为 :" + rr.area); } } • 程序运行结果如下: • 长方形面积为 :200.0
3.方法返回值为类的对象 • 在Java语言中,方法的返回值类型也可以为引用类型,例如:类。 • 【例5.11】方法的返回值类型为引用类型。 class RetClass{ double width,length,area; RetClass(double w,double l){ width = w; length = l; } RetClass calArea(RetClass r){ r.area = r.width * r.length;
return r; } } class ReturnObject{ public static void main(String args[]){ RetClass rr = new RetClass(10,20); rr = rr.calArea(rr); System.out.println("长方形面积为 :" + rr.area); } } • 程序运行结果如下: 长方形面积为 :200.0
4.类对象作为类的成员 • 类的数据成员也可以是引用类型的数据,如数组、字符串和类等。若一个类的对象是一个类的成员时,要用new运算符为这个对象分配存储空间。在包含类数据成员的类及类的实例中可以访问类数据成员的成员。 • 【例5.12】类对象作为类的成员。 class RectC{ double width,length; RectC(double w,double l){ width = w; length = l; } }
class RectangleC{ // 具有两个成员的类 RectC r = new RectC(10,20); // 类成员要分配空间 double area; // 基本类型成员 } class ClassMember{ public static void main(String args[]){ RectangleC rr = new RectangleC(); rr.area = rr.r.width * rr.r.length; System.out.println("长方形面积为 :" + rr.area); } } • 程序运行结果如下: 长方形面积为 :200.0
5.内部类(Inner class) • 内部类,也称为嵌套类,被附加到JDK1.1及更高版本中。内部类允许一个类定义被放到另一个类定义里、一个语句块里或一个表达式内部。内部类是一个有用的特征,因为它们允许将逻辑上同属性的类组合到一起,并在另一个类中控制一个类的可视性。 • 【例5.13】在一个类中定义类(内部类)。 class RectDemo6 { public static void main(String args[]) { double ar; class RectangleR { double length; double width; double area(){ return length * width; // 返回面积 }
void setDim(double w, double l){ width = w; length = l; } } RectangleR rect1 = new RectangleR(); RectangleR rect2 = new RectangleR(); rect1.setDim(10, 20); // 初始化每个长方形 rect2.setDim(3, 6); ar = rect1.area(); System.out.println("第一个长方形面积是: " + ar); ar = rect2.area(); System.out.println("第二个长方形面积是: " + ar); } }
程序运行结果如下: 第一个长方形面积是: 200.0 第二个长方形面积是: 18.0 • 因为内部类定义在一个类中,因此内部类的名称不能与所嵌套的类相同,而且只能在定义的范围中使用。内部类具有下面一些特性: (1)内部类可以被定义在方法中。它可以访问嵌套类的方法的final变量。 (2)内部类可以使用所嵌套类的类变量和实例变量以及所嵌套的块中的本地变量。 (3)内部类可以被定义为abstract抽象类。
(4)内部类可以被声明为private或protected,以便防护它们不受来自外部类的访问。访问保护不阻止内部类使用其他类的任何成员。 (5)一个内部类可以作为一个接口,由另一个内部类实现。 (6)声明为static的内部类成为顶层类。这些内部类失去了在本地范围和其他内部类中使用数据或变量的能力。 (7)内部类不能声明任何static成员,只有顶层类可以声明static成员。因此,一个需求static成员的内部类必须使用来自顶层类的成员。
5.2 类的继承(inheritance) • 继承是面向对象程序设计的另一个重要特色,类继承也称为类派生,是指一个类可以继承其他类的非私有成员,实现代码复用。被继承的类称为父类或超类,父类包括所有直接或间接被继承的类;继承父类或超类后产生的类称为派生类或子类。 • Java语言以Object类作为所有类的父类,所有的类都是直接或间接地继承Object类得到的。Java还提供不同层次的标准类,使用户可根据需要派生自己的类。
在Java语言中,只允许单继承。所谓单继承是指每个类只有一个父类,不允许有多个父类。但一个类允许同时拥有多个子类,这时这个父类实际上是所有子类的公共成员变量和公共方法成员的集合,而每一个子类则是父类的特殊化,是对公共成员变量和方法成员的功能、内涵方面的扩展和延伸。Java语言的多继承可通过接口来实现。在Java语言中,只允许单继承。所谓单继承是指每个类只有一个父类,不允许有多个父类。但一个类允许同时拥有多个子类,这时这个父类实际上是所有子类的公共成员变量和公共方法成员的集合,而每一个子类则是父类的特殊化,是对公共成员变量和方法成员的功能、内涵方面的扩展和延伸。Java语言的多继承可通过接口来实现。 • 类继承不改变成员的访问权限,父类中的成员为公有的或被保护的,则其子类的成员访问权限也继承为公有的或被保护的。
5.2.1 类继承的实现 • Java中的继承是通过extends关键字来实现的,在定义新类时使用extends关键字指明新类的父类,就在两个类之间建立了继承关系。 • 1.定义子类 • 通过继承系统类定义子类的例子已经见过的有例1.2的小程序和例3.32的自定义异常处理程序,其类头分别为: public class HelloApplet extends Applet{......} class MyException extends Exception {......}
一般地,在类声明中,加入extends子句来创建一个类的子类。extends后即为父类名,若父类名又是某个类的子类,则定义的类也是该类的(间接)子类。若无extends子句,则该类为java.lang.Object的子类。一般地,在类声明中,加入extends子句来创建一个类的子类。extends后即为父类名,若父类名又是某个类的子类,则定义的类也是该类的(间接)子类。若无extends子句,则该类为java.lang.Object的子类。 • 【例5.14】继承例5.6中的类RectConstructor时对父类的成员变量赋初值。 class Cube extends RectConstructor{ double height; Cube(double l,double w,double h){ super(l,w); height = h; }
void vol(){ System.out.println("长方体体积 = "+ area() * height); } } public class CubeDemo { public static void main(String args[]){ Cube c = new Cube(7,6,5); c.vol(); } } • 在例中,子类Cube继承了父类的变量length、width和父类的area方法,代码复用使得程序简洁高效。程序运行结果如下:长方体体积 = 210.0
2.子类的初始化 • 在创建子类的对象时,使用子类的构造方法对其初始化,不但要对自身新增的成员变量初始化,还要对继承的变量初始化。由于成员变量初始化通常在构造方法中完成,因此在Java语言中,允许子类调用父类的构造方法对继承的变量初始化。父类构造方法的调用遵循如下的原则: (1)若父类是无参数的构造方法,则子类无条件的调用该构造方法。
(2)若子类无自己的构造方法,则它将调用父类的无参构造方法作为自己的构造方法;若子类有自己的构造方法,则在创建子类对象时,它将先调用父类的无参构造方法,然后再执行自己的构造方法。(2)若子类无自己的构造方法,则它将调用父类的无参构造方法作为自己的构造方法;若子类有自己的构造方法,则在创建子类对象时,它将先调用父类的无参构造方法,然后再执行自己的构造方法。 (3)若父类是有参数的构造方法,子类可以通过在自己的构造方法中使用super关键字来调用它,但这个调用语句必须是子类构造方法的第一个可执行语句。 在本例5.14子类的构造方法中,由于子类没有继承父类的任何构造方法,因此需要用super调用父类的构造方法对父类的成员变量初始化。
3.类继承的传递性 • 类继承具有传递性,即子类继承父类的所有非私有成员,也继承父类的父类直至祖先所有的所有非私有成员。 • 4.成员变量的隐藏和方法成员的覆盖 • 在类的继承中,子类(派生类)可以定义新成员,也可以重定义继承的成员,这里主要讨论成员变量和方法成员。
在子类中重定义继承的成员的目的是修改父类的属性和行为,此时,在子类中,通过成员名称仅能直接访问本身新增的成员,若有必要访问继承的成员,可用关键字super。在子类中重定义继承的成员的目的是修改父类的属性和行为,此时,在子类中,通过成员名称仅能直接访问本身新增的成员,若有必要访问继承的成员,可用关键字super。 (1)成员变量的隐藏 • 若子类重定义了继承的变量,则继承的变量被隐藏起来,直接使用的是子类重定义的变量,但继承的变量仍占据空间,可通过super或父类名来访问。
【例5.15】在子类中定义了与父类同名的变量,从而隐藏了父类成员变量,这种隐藏变量可加super前缀来访问。【例5.15】在子类中定义了与父类同名的变量,从而隐藏了父类成员变量,这种隐藏变量可加super前缀来访问。 class A{ int x = 100; } class B extends A{ int x = 200; // 在子类中定义与父类同名变量x void print(){ System.out.println("Subclass : " + x); // 直接输出为子类变量
System.out.println("Superclass : " + super.x); // 父类变量用super访问 } public static void main(String args[]){ (new B()).print(); } } 程序运行结果如下: Subclass : 200 Superclass : 100 需注意的是:super不能用于静态方法中。因为静态方法只能访问静态变量,在静态方法中,父类的静态变量可通过类名前缀来引用。