9.1 引言
- 面向对象编程使得大型软件及图形用户界面的开发变得更加高效
- 面向对象编程实质上是一种开发可重用软件的技术。学习过前几章的内容之后,你已经能够使用选择、循环、方法和数组解决很多程序设计问题。但是,这些Java的特性还不足以开发图形用户界面和大型软件系统。假设希望开发一个GUI(图形用户界面,发音为goo-ee),如图9-1所示,该如何用程序实现它呢?
9.2 为对象定义类
- 类为对象定义属性和行为
- 面向对象程序设计(OOP)就是使用对象进行程序设计。对象(object)代表现实世界中可以明确标识的一个实体。例如:一个学生、一张桌子、一个圆、一个按钮甚至一笔贷款都可以看作一个对象。每个对象都有自己独特的标识、状态和行为
- 一个对象的状态(state,也称为特征(property)或属性(attribute))是由数据域以及当前值来表示的。例如,圆对象具有一个数据域radius,它是描述圆的特征的属性。矩形对象具有数据域width和height,它们都是描述矩形特征的属性。
- 一个对象的行为(behavior,也称为动作(action))是由方法定义的。调用对象的一个方法就是要求对象完成一个动作。例如,可以为圆对象定义名为getArea()和getPerimeter()的方法。圆对象可以调用getArea()返回其面积,调用getPerimeter()返回其周长。还可以定义setRadius(radius)方法,圆对象调用这个方法来修改半径。
- 使用一个通用类来定义同一类型的对象。类是一个模板、蓝本或者合约,用来定义对象的数据域以及方法。对象是类的实例。可以从一个类中创建多个实例。创建实例的过程称为实例化(instantiation)。对象(object)和实例(instance)经常是可以互换的。类和对象之间的关系类似于苹果派配方和苹果派之间的关系。可以用一种配方做出任意多的苹果派来。图9-2显示名为Circle的类和它的三个对象。
- Java类使用变量定义数据域,使用方法定义动作。除此以外,类还提供了一种称为构造方法(constructor)的特殊类型的方法,调用它可以创建一个新对象。构造方法本身是可以完成任何动作的,但是设计构造方法是为了完成初始化动作,例如初始化对象的数据域。图9-3显示了定义圆对象的类的例子
- Circle 类与目前所见过的所有其他类都不同,它没有main方法,因此是不能运行的;它只是对圆对象的定义。为了方便,本书将包含main方法的类称为主类(main class)。
- 图9-2中类的模板和对象的图示可以使用统一建模语言(Unified Modeling Language,UML)的图形符号进行标准化,如图9-4所示的表示方法称为UML类图(UML class diagram),或简称为类图(class diagram)。在类图中,数据域表示为:
dataFieldName: dataFieldType
- 构造方法可以表示为:
ClassName(parameterName:parameterType)
- 方法可以表示为:
methodName(parameterName:parameterType):returnType
9.3 示例:定义类和创建对象
程序清单 9-1 TestCircle.java
public class TestCircle {/*** Main method*/public static void main(String[] args) {//Create a circle with radius 1Circle circle1 = new Circle();System.out.println("The area of the circle of radius "+ circle1.radius + " is " + circle1.getArea());//Create a circle with radius 25Circle circle2 = new Circle(25);System.out.println("The area of the circle of radius "+ circle2.radius + " is " + circle2.getArea());//Create a circle with radius 125Circle circle3 = new Circle(125);System.out.println("The area of the circle of radius "+ circle3.radius + " is " + circle3.getArea());//Modify circle radius//or circle2.setRadius(100)circle2.radius = 100;System.out.println("The area of the circle of radius "+ circle2.radius + " is " + circle2.getArea());}}//Define the circle class with two constructors//在公有类外定义class Circle {double radius;/*** Construct a circle with radius 1*/Circle() {radius = 1;}/*** Construct a circle with a specified radius*/Circle(double newRadius) {radius = newRadius;}/*** Return the area of this circle*/double getArea() {return radius * radius * Math.PI;}/*** Return the perimeter of this circle*/double getPerimeter() {return 2 * radius * Math.PI;}/*** Set a new radius for this circle*/void setRadius(double newRadius) {radius = newRadius;}}
- 程序包括两个类。其中第一个类TestCircle是主类。它只是用来测试第二个类Circle。
- 这种使用类的程序通常称为该类的客户(client)
- 可以把两个类放在同一个文件中,但是 文件中只能由一个类是公共(public)类。此外,公共类必须与文件同名。因此,文件名就应该是TestCircle.java,因为TestCircle是公共的。源代码中的每个类编译成 .class 文件。当编译TestCircle.java 时,产生两个类文件TestCircle.class和Circle.class,如图9-5所示。
程序清单 9-2 Circle2.java
//由于书本上的文件名与9-1的程序中Circle类重名,所以这里更名为Circle2//这里是将程序9-1,写成一个文件,在主方法外定义public class Circle2 {/*** Main method*/public static void main(String[] args) {//Create a circle with radius 1Circle2 circle1 = new Circle2();System.out.println("The area of the circle of radius "+ circle1.radius + " is " + circle1.getArea());//Create a circle with radius 25Circle2 circle2 = new Circle2(25);System.out.println("The area of the circle of radius "+ circle2.radius + " is " + circle2.getArea());//Create a circle with radius 1Circle2 circle3 = new Circle2(125);System.out.println("The area of the circle of radius "+ circle3.radius + " is " + circle3.getArea());//Modify circle radiuscircle2.radius = 100;System.out.println("The area of the circle of radius "+ circle2.radius + " is " + circle2.getArea());}double radius;/*** Construct a circle with radius 1*/Circle2() {radius = 1;}/*** Construct a circle with a specified radius*/Circle2(double newRadius) {radius = newRadius;}/*** Return the area of this circle*/double getArea() {return radius * radius * Math.PI;}/*** Return the perimeter of this circle*/double getPerimeter() {return 2 * radius * Math.PI;}/*** Set a new radius for this circle*/void setRadius(double newRadius) {radius = newRadius;}}
- 由于组合后的类中有一个main方法,所以它可以由Java解释器来执行。main方法和程序清单9-1中的是一样的。它演示如何通过在一个类中加入main方法来测试这个类。
- 另一个例子是关于电视机的。每台电视机都是一个对象,每个对象都有状态(当前频道、当前音量、电源开或关)以及动作(转换频道、调节音量、开启/关闭)。可以使用一个类对电视机进行建模。这个类的UML图如图9 - 6 所示。
- 程序清单9-3给出了定义TV类的程序。
程序清单 9-3 TV.java
public class TV {//Default channel is 1int channel = 1;//Default volum level is 1int volumeLevel = 1;//TV is offboolean on = false;public TV() {}public void turnOn() {on = true;}public void turnOff() {on = false;}public void setChannel(int newChannel) {if (on && newChannel >= 1 && newChannel <= 120) {channel = newChannel;}}public void setVolume(int newVolumeLevel) {if (on && newVolumeLevel >= 1 && newVolumeLevel <= 7) {volumeLevel = newVolumeLevel;}}public void channelUp() {if (on && channel < 120) {channel++;}}public void channelDown() {if (on && channel > 1) {channel++;}}public void volumeUp() {if (on && volumeLevel < 7) {volumeLevel++;}}public void volumeDown() {if (on && channel > 1) {volumeLevel++;}}}
- TV类中的构造方法和其他方法定义为公共的,因此可以从其他类中访问。注意,如果没有打开电视,那么频道和音量都没有改变。在改变它们中的任何一个之前,要检查它的当前值以确保在正确的范围内
- 程序清单9-4给出了使用TV类创建两个对象的程序。
程序清单 9-4 TestTV.java
public class TestTV {public static void main(String[] args) {TV tv1 = new TV();tv1.turnOn();tv1.setChannel(30);tv1.setVolume(3);TV tv2 = new TV();tv2.turnOn();tv2.channelUp();tv2.channelUp();tv2.volumeUp();System.out.println("tv1's channel is " + tv1.channel+ " and volume level is " + tv1.volumeLevel);System.out.println("tv2's channel is " + tv2.channel+ " and volume level is " + tv2.volumeLevel);}}
9.4 使用构造方法构造对象
- 使用new操作符调用构造方法创建对象
- 构造方法是一种特殊的方法,有以下三个特殊之处
- 构造方法必须和所在类名字相同
- 构造方法没有返回值类型,甚至连void也没有
- 构造方法是在创建一个对象时由new操作符调用的。构造方法的作用是初始化对象。
- 构造方法和定义它的类的名字完全相同。和所有其他方法一样,构造方法也可以重载(也就是说,可以有多个同名但签名不同的构造方法),这样更易于用不同的初始数据值来构造对象。
- 一个常见的错误就是将关键字void放在构造方法的前面。例如:
public void Circle(){}
- 在这种情况下,Circle( ) 是一个方法,而不是构造方法。
- 构造方法是用来构造对象的。为了能够从一个类构造对象,使用new操作符调用这个类的构造方法,如下所示:
new ClassName(arguments);
- 例如:new Circle( ) 使用Circle 类中定义的第一个构造方法创建一个Circle对象。new Circle(25)调用Circle 类中定义的第二个构造方法创建一个Circle对象。
- 通常,类会提供一个没有参数的构造方法。这样的构造方法被称为无参构造方法(no-arg 或 no - argument constructor)。
- 在一个类中,用户可能没有定义构造方法。在这种情况下,类中会隐式定义一个方法体为空的无参构造方法。这个构造方法称为默认构造方法(default constructor),当且仅当类中没有明确定义任何构造方法时才会自动提供。
9.5 通过引用变量访问对象
- 对象的数据和方法可以运用点操作符(.)通过对象的引用变量进行访问
- 新创建的对象在内存中被分配空间。它们可以通过引用变量来访问。
9.5.1 引用变量和引用类型
- 对象是通过对象引用变量(reference variable)来访问的,该变量包含了对对象的引用,使用如下语法声明这样的变量:
ClassName objectRefVar;
- 本质上来说,类是程序员定义的类型。类是一种引用类型(reference type),这意味着该类类型的变量都可以引用该类的一个实例。下面的语句声明变量myCircle的类型是Circle类型:
Circle myCircle;
- 变量myCircle能够引用一个Circle对象。下面的语句创建一个对象,并且将它的引用赋给变量myCircle:
myCircle = new Circle();
- 采用如下所示的语法,可以写一条结合了声明对象引用变量、创建对象以及将对象的引用赋值给这个变量的语句。
ClassName objectRefVar = new ClassName();
- 下面是一个例子
Circle myCircle = new Circle();
- 从表面看,对象引用变量中似乎存放了一个对象,但事实上,它只是存放了对该对象的引用。严格地讲,对象引用变量和对象是不同的,但是大多数情况下,这种差异是可以忽略的。因此,可以简单地说myCircle是一个Circle对象,而不用冗长地描述说myCircle是一个存放了对Circle对象引用的变量
- 在Java中,数组被看作对象。数组是用new操作符创建的。一个数组变量实际上是一个包含数组引用的变量。
9.5.2 访问对象的数据和方法
- 在面向对象编程中,对象成员指该对象的数据域和方法。在创建一个对象之后,它的数据访问和方法调用可以使用点操作符 ( . ) 来进行,该操作符也称为对象成员访问操作符(object member access operator):
- objectRefVar.dataField 引用对象的数据域
- objectRefVar.method(arguments)调用对象的方法
- 例如:myCircle.radius 引用myCircle 的半径,而myCircle.getArea()调用myCircle的getArea方法。方法作为对象上的操作被调用
- 数据域radius称作实例变量(instance variable),因为它依赖于某个具体的实例。基于同样的原因,getArea方法称为实例方法(instance method),因为只能在具体的实例上调用它。实例方法被调用的对象称为调用对象(calling object)。
- 回想一下,我们曾经使用过Math.methodName(参数)(例如:Math.pow(3,2.5))来调用Math类中的方法。那么能否用Circle.getArea()来嗲用getArea方法呢?答案是不能。Math类中的所有方法都是用关键字static定义的静态方法。但是,getArea( )是实例方法,因此它是非静态的。它必须使用objectRefVar.methodName(参数)的方式(例如:myCircle.getArea( ))从对象调用。
- 通常,我们创建一个对象,然后将它赋值给一个变量,之后就可以使用这个变量来引用对象。有时候,对象在创建之后并不需要引用。在这种情况下,可以创建一个对象,而并不将它明确地赋值给一个变量,如下所示:
new Circle();
- 或者
System.out.println("Area is " + new Circle(5).getArea());
- 前面的语句创建了一个Circle对象。后面的语句创建了一个Circle对象,然后调用它的getArea方法返回其面积。这种方式创建的对象称为匿名对象(anonymous object)
9.5.3 引用数据域和null值
- 数据域也可能是引用型的。例如:下面的Student 类包含一个String 类型的name数据域,String 是一个预定义的Java类。
class Student{String name;//name has the default value nullint age; //age has the default value 0boolean isScienceMajor; //isScienceMajor has default value false}
- 如果一个引用类型的数据域没有引用任何对象,那么这个数据域就有一个特殊的Java值null。null同true和false一样都是字面值。true和false是boolean类型字面值,而null是引用类型字面值
- 引用类型数据域的默认值是null,数值类型数据域的默认值是0,boolean类型的数据域默认值是false,而char类型数据域的默认值是
'\u0000'。但是Java没有给方法中的局部变量赋默认值。下面的代码显示Student 对象中数据域name、age、isScienceMajor和gender的默认值:class TestStudent{public static void main(String[] args){Student student = new Student();System.out.println("name? " + student.name);System.out.println("age " + student.age);System.out.println("isScienceMajor? " + student.isScienceMajor);System.out.println("gender? " + student.gender);}}
- 下面代码中的局部变量x和y都没有被初始化,所以会出现编译错误:
class TestLocalVariables{public static void main(String[] args){int x; //x has no default valueString y; //y has no default valueSystem.out.println("x is " + x);System.out.println("y is " + y);}}
- NullPointerException是一种常见的运行时错误,当调用值为null的引用变量上的方法时会方式此类异常。在通过引用变量调用一个方法之前,确保先将对象引用赋值给这个变量
9.5.4 基本类型变量和引用类型变量的区别
- 每个变量都代表一个保存了存储值的内存位置。声明一个变量时,就是在告诉编译器这个变量可以存放什么类型的值。对基本类型变量来说,对应内存所存储的值是基本类型值。对引用类型变量来说,对应内存所存储的值是一个引用,是对象的存储地址。例如:如图9-7所示,int类型变量i的值就是int值1,而Circle对象c的值保存的是一个引用,它指明这个Circle 对象的内容存储在内存中的什么位置。
- 将一个变量赋值给另一个变量时,另一个变量就被赋予同样的值。对基本类型变量而言,就是将一个变量的实际值赋给另一个变量。对引用数据类型变量而言,就是将一个变量的引用赋给另一个变量。如图9-8所示,赋值语句i=j将基本类型变量j的内容复制给基本类型变量i。如图9-9所示,对引用变量来讲,赋值语句c1 = c2 是将c2的引用赋给c1。赋值之后,变量c1和c2指向同一个对象。
- 如图9-9所示,执行完赋值语句c1 = c2之后,c1指向c2所指的同一个对象。c1以前引用的对象就不在有用,因此,现在它就成为垃圾(garbage)。垃圾会占用内存空间。Java运行系统会检测垃圾并自动回收它所占据的空间,这个过程称为垃圾回收(garbage collection)
- 如果你不再需要某个对象,可以显式地给该对象的引用变量赋null值。如果该对象没有被任何引用变量所引用,Java虚拟机将自动回收它所占据的空间。
9.6 使用Java库中的类
- Java API 包含了丰富的类的集合,用于开发Java程序
9.6.1 Date类
9.6.2 Random类
- 产生相同随机值序列的功能在软件测试以及其他许多应用中是很有用的。在软件测试中,经常需要从一组固定顺序的随机数中来重复生成测试案例
- 可以使用java.security.SecureRandom类而不是Random类来产生随机数字。从Random类产生的随机数字是确定的,可能被黑客预测到。而从SecureRandom类产生的随机数字是不确定的,因而是安全的。
9.6.3 Point2D类
程序清单 9-4 TestPoint2D.java
import javafx.geometry.Point2D;import java.util.Scanner;public class TestPoint2D {public static void main(String[] args) {Scanner input = new Scanner(System.in);System.out.print("Enter point1's x -, y-coordinates: ");double x1 = input.nextDouble();double y1 = input.nextDouble();System.out.print("Enter point1's x -, y-coordinates: ");double x2 = input.nextDouble();double y2 = input.nextDouble();Point2D p1 = new Point2D(x1, y1);Point2D p2 = new Point2D(x2, y2);System.out.println("p1 is " + p1.toString());System.out.println("p2 is " + p2.toString());System.out.println("The distance between p1 and p2 is " +p1.distance(p2));System.out.println("The midpoint between p1 and p2 is " +p1.midpoint(p2).toString());}}
9.7 静态变量、常量和方法
- 静态变量被类中的所有对象所共享。静态方法不能访问类中的实例成员(即实例数据域和方法)
- Circle类的数据域radius称为一个实例变量。实例变量是属于类的某个特定实例的,不能被同一个类的不同对象所共享。例如,假设创建了如下的两个对象:
Circle circle1 = new Circle();Circle circle2 = new Circle(5);
- circle1中的radius和circle2中的radius是不相关的,它们存储在不同的内存位置。circle1中radius的变化不会影响circle2中的radius,反之亦然。
- 如果想让一个类的所有实例共享数据,就要使用静态变量(static variable),也称为类变量(class variable)。静态变量将变量值存储在一个公共的内存地址。因为是公共的地址,所以如果某一个对象修改了静态变量的值,那么同一个类的所有对象都会受到影响。Java支持静态方法和静态变量,无需创建类的实例就可以调用静态方法(static method)。
- 修改Circle类,添加静态变量numberOfObjects用于统计创建的Circle对象的个数。当该类的第一个对象创建后,numberOfObjects的值是1。当第二个对象创建后,numberOfObjects的值是2。新Circle类的UML图如图9-13所示。Circle类定义了实例变量radius和静态变量numberOfObjects,还定义了实例方法getRadius、setRadius和getArea以及静态方法getNumberOfObjects。(注意,在UML类图,静态变量和静态方法都是以下划线标注的。)
- 要声明一个静态变量或定义一个静态方法,就要在这个变量或方法的声明中加上修饰符static。静态变量numberOfObjects和静态方法getNumberOfObjects()可以声明如下:
static int numberOfObjects;static int getnumberOfObjects(){return numebrOfObjects;}
- 类中的常量是被该类的所有对象所共享的。因此,常量应该声明为final static,例如,Math类中的常量PI是如下定义的:
final static double PI = 3.14159265358979323846;
程序清单 9-6 Circle3.java
public class Circle3 {/*** The radius of the circle*/double radius;/*** The numebr of objects created*/static int numberOfObjects = 0;/*** Construct a circle with radius 1*/Circle3() {radius = 1;numberOfObjects++;}/*** Construct a circle with a specified radius*/Circle3(double newRadius) {radius = newRadius;numberOfObjects++;}/*** Return numberOfObjects*/static int getNumberOfObjects() {return numberOfObjects;}/*** Return the area of this circle*/double getArea() {return radius * radius * Math.PI;}}
- Circle类中的getNumberOfObjects( ) 方法是一个静态方法。Math类中所有的方法都是静态的。main方法也是静态方法。
- 实例方法和实例数据都是从属于实例的,所以在实例创建之后才能使用。它们是通过引用变量来访问的,静态方法和静态数据可以通过引用变量或它们的类名来调用,
- 程序清单9-7中的程序演示如何使用实例变量、静态变量、实力方法和静态方法,以及使用它们的效果。
程序清单 9-7 Circle3.java
public class TestCircleWithStaticMembers {/*** Main method*/public static void main(String[] args) {System.out.println("Before creating objects: ");System.out.println("The number of Circle objects is " +Circle3.numberOfObjects);//Create c1//Use the Circle class in Listing 9.6Circle3 c1 = new Circle3();//Display c1 BEFORE c2 is createdSystem.out.println("\nAfter creating c1");System.out.println("c1: radius (" + c1.radius +") and number of Circle objects (" +c1.numberOfObjects + ")");//Create c2Circle3 c2 = new Circle3(5);//Modify c1c1.radius = 9;//Display c1 and c2 AFTER c2 was createdSystem.out.println("\nAfter creating c2 and modifying c1");System.out.println("c1: radius (" + c1.radius +") and number of Circle objects (" + c1.numberOfObjects + ")");System.out.println("c2: radius (" + c2.radius +") and number of Circle objects (" + c2.numberOfObjects + ")");}}
- 编译TestCircleWithStaticMembers.java时,如果Circle.java在最后一次修改之后还没有编译过的话,Java编译器就会自动编译它。
- 静态变量和方法可以在不创建对象的情况下访问。
- main方法创建两个圆c1和c2。c1中的实例变量radius修改为9。这个变化不会影响c2中的实例变量radius,因为这两个实例变量是独立的。c1创建之后变量numberOfObjects变成1,而c2创建之后numberOfObjects变成2。
- 注意,PI 是一个定义在Math中的常量,可以使用Math.PI 来访问这个常量。最好使用Circle.numberOfObjects 来代替c1.numberOfObjects和 c2.numberOfObjects。这样可以提高可读性,因为其他程序员可以很容易地辨别出类中的静态变量。也可以用Circle.getNumberOfObjects( ) 替换掉Circle.numberOfObjects。
- 使用
类名.方法名(参数)的方式调用静态方法,使用类名.静态变量的方式访问静态变量。这会提高可读性,因为可以很容易地识别出类中的静态方法和数据。 - 实例方法可以调用实例方法和静态方法,以及访问实例数据域或者静态数据域。静态方法可以调用静态方法以及访问静态数据域。然而,静态方法不能调用实例方法或者访问实例数据域,因为静态方法和静态数据域不属于某个特定的对象。静态成员和实例成员的关系总结在下图中。
- 设计指南
- 如何判断一个变量或方法应该是实例的还是静态的?如果一个变量或方法依赖于类的某个具体实例,那就应该将它定义为实例变量或实例方法。如果一个变量或方法不依赖于类的某个具体实例,就应该将它定义为静态变量或静态方法。例如:每个圆都有自己的半径,半径都依赖于某个具体的圆。因此,半径radius就是Circle类的一个实例变量。由于getArea方法依赖于某个具体的圆,所以,它也是一个实例方法。在Math类中没有一个方法是依赖于特定实例的,例如random、pow、sin和cos。因此,这些方法都是静态方法。main方法是静态的,可以从类中直接调用。
- 一个常见的错误设计是将本应该声明为静态的方法声明为实例方法。例如:方法factorial(int n )应该定义为静态的,如下所示,因为它不依赖于任何具体的实例。
9.8 可见性修饰符
- 可见性修饰符可以用于确定一个类以及它的成员的可见性
- 可以在类、方法和数据域前使用public 可见性修饰符,表示它们可以被任何其他的类访问。如果没有使用可见性修饰符,那么默认类、方法和数据域是可以被同一个包中的任何一个类访问的。这称作包私有(package - private)或包访问(package - access)。
- 包可以用来组织类。为了完成这个目标,需要在程序中出现下面这行语句,作为程序中第一条非注释和非空白行的语句:
package packageName;
- 如果定义类时没有声明包,就表示把它放在默认包中。
- Java建议最好将类放入包中,而不要使用默认包。但是,本书为了简化问题使用的是默认包。但是,本书为了简化问题使用的是默认包。关于包的更多的信息,参见补充材料Ⅲ.E。
- 除了public和默认可见性修饰符,Java还为类成员体哦那个private 和 protected 修饰符。本节介绍private修饰符。protected修饰符将在11.14节介绍
- private 修饰符限定方法和数据域只能在它自己的类中被访问。图9-14 演示类C1中的公共的、默认的和私有的数据域或方法能否被同一个包内的类C2访问,以及能否被不在同一个包内的类C3访问。
- 如果一个类没有被定义为公共类,那么它只能在同一个包内被访问。如图9-15所示,C2可以访问C1,而C3不能访问C1.
- 可见性修饰符指名类中的数据域和方法是否能在该类之外被访问。在该类之内,对数据域和方法的访问是没有任何限制的。如图9-16b所示,C类的对象c不能引用它的私有成员,因为c在Test类中。如图9-16a所示,C类的对象c可以访问它的私有成员,因为c在自己的类内定义。
- 修饰符private 只能应用在类的成员上。修饰符public 可以应用在类或类的成员上。在局部变量上使用修饰符public 和private 都会导致编译错误。
- 大多数情况下,构造方法应该是公共的。但是,如果想防止用户创建类的实例,就该使用私有构造方法。例如:因为Math类的所有数据域和方法都是静态的,所以没必要创建Math类的实例。为了防止用户创建Math类的实例,在java.lang.Math中的构造方法定义如下:
private Math(){}
9.9 数据域封装
- 将数据域设为私有可以保护数据,并且使得类易于维护。
- 在程序清单9-6中,可以直接修改Circle类的数据域radius和numberOfObjects(例如:c1.radius = 5 或 Circle.numberOfObjects = 10)。这不是一个好做法,原因有两点:
- 首先,数据可能被篡改。例如:numberOfObjects是用来统计被创建的对象的个数的,但是它可能会被错误的设置为一个任意值(Circle.numberOfObjects = 10)
- 其次,它使得类难以维护,同时容易出现错误。假设你想修改Circle类以确保半径是非负数,然而已经有其他程序使用Circle类。那么,不仅要修改Circle类。那么,不仅要修改Circle类,而且还要修改使用Circle类的程序。因为这些客户程序可能已经直接修改了radius
- 为了避免对数据域的直接修改,应该使用private 修饰符将数据域声明为私有的,这称为数据域封装(data field encapsulation)
- 私有数据域不能被对象从定义该四由于的类外访问。但是也经常会有客户端需要存取、修改数据域。为了访问私有数据域,可以提供一个获取(getter)方法返回数据域的值。为了更新数据域,可以提供一个设置(setter)方法给数据域设置新值。获取方法也称为访问器(accessor),而设置方法称为修改器(mutator)。获取方法有如下签名:
public returnType getPropertyName()
- 如果returnType是boolean 型,习惯上如下定义获取方法:
public boolean isPropertyName()
- 设置方法有如下签名:
public void setPropertyName(dataType propertyValue)
- 现在创建一个新的圆类,半径设置为私有数据域,并有相关的访问器和修改器。类图如图9-17所示。程序清单9-8中定义一个新的圆类。
程序清单 9-8 Circle4.java
package Based_On_Article.The_Textbook_Source_Code.Chapter09;public class Circle4 {/*** The radius of the circle*/private double radius = 1;/*** The number of objects created*/private static int numberOfObjects = 0;/*** Construct a circle with radius 1*/public Circle4() {numberOfObjects++;}/*** Construct a circle with a specified radius*/public Circle4(double newRadius) {radius = newRadius;numberOfObjects++;}/*** Return radius*/public double getRadius() {return radius;}/*** Set a new radius*/public void setRadius(double newRadius) {radius = (newRadius >= 0) ? newRadius : 0;}/*** Return numberOfObjects*/public static int getNumberOfObjects() {return numberOfObjects;}/*** Return the area of this circle*/public double getArea() {return radius * radius * Math.PI;}}
程序清单 9-9 TestCircleWithPrivateDataFields.java
public class TestCircleWithPrivateDataFields {/*** Main method*/public static void main(String[] args) {//Create a circle with radius 5.0Circle4 myCircle = new Circle4(5.0);System.out.println("The area of the circle of radius "+ myCircle.getRadius() + " is " + myCircle.getArea());//Increase myCircle's radius by 10%myCircle.setRadius(myCircle.getRadius() * 1.1);System.out.println("The area of the circle of radius "+ myCircle.getRadius() + " is " + myCircle.getArea());System.out.println("The number of objects created is "+ Circle4.getNumberOfObjects());}}
- 为防止数据被篡改以及使类更易于维护,将数据域声明为私有的。
- 从现在开始,除非特别的原因而另外指定,否则所有的数据域都应该被声明为私有的,并且所有的构造方法和方法应该被声明为公共的
9.10 向方法传递对象参数
给方法传递一个对象,是将对象的引用传递给方法
可以将对象传递给方法。同传递数组一样,传递对象实际上是传递对象的引用。下面的代码将myCircle对象作为参数传递给printCircle方法:
public class Test {public static void main(String[] args) {//Circle is defined in Listing 9.8Circle myCircle = new Circle(5.0);printCircle(myCircle);}public static void printCircle(Circle c) {System.out.println("The area of the circle of radius "+ c.getRadius() + " is " + c.getArea());}}
- Java只有一种参数传递方式:值传递(pass - by - value)。在上面的代码中,myCircle的值被传递给printCircle方法。这个值就是一个对Circle对象的引用值。
- 程序清单9-10 中的程序展示了传递基本类型值和传递引用值的差异
程序清单 9-10 TestPassObject.java
package Based_On_Article.The_Textbook_Source_Code.Chapter09;public class TestPassObject {/*** Main method*/public static void main(String[] args) {//Create a Circle object with radius 1//Use the Circle class in Listing 9.8Circle4 myCircle = new Circle4(1);//Print areas for radius 1,2,3,4, and 5int n = 5;printAreas(myCircle,n);//See myCircle.radius and timesSystem.out.println("\n" + "Radius is " + myCircle.getRadius());System.out.println("n is " + n);}/*** Print a table of areas for radius*/public static void printAreas(Circle4 c, int times) {System.out.println("Radius \t\t Area");while (times >= 1){System.out.println(c.getRadius() + "\t\t" + c.getArea());c.setRadius(c.getRadius() + 1);times--;}}}
- Circle类是在程序清单9-8中定义的。这个程序使用Circle类的一个对象myCircle和整数值n调用printAreas(myCircle, n)方法,从而打印半径为1、2、3、4和5的圆面积所构成的表格,如样本输出所示。
- 图9-18 展示执行程序中的方法时的调用堆栈。注意,对象存储在堆中
- 当传递基本数据类型的参数时,传递的是实参的值。在这种情况下,n(5)的值就被传递给times。在printAreas方法内,times的内容改变,这并不会影响n的内容。
- 传递引用类型的参数时,传递的是对象的引用。在这种情况下,c包含对一个对象的引用,该对象也被myCircle所引用。因此,通过在printAreas方法内部的c与在方法外的白能量myCircle来改变对象的属性,效果的一样的。引用上的按值传参在语义上最好描述为按共享传参(pass - by - sharing),也就是说,在方法中引用的对象和传递的对象是一样的。
9.11 对象数组
- 数组既可以存储基本类型值,也可以存储对象
- 当使用new操作符创建对象数组后,这个数组中的每个元素都是默认值为null的引用变量
- 程序清单9-11给出了一个例子,演示如何使用对象数组。这个程序求圆数组的总面积。程序创建了包含5个Circle对象的数组circleArray,接着使用随机值初始化这些圆的半径,然后显示数组中圆的总面积。
程序清单 9-11 TotalArea.java
public class TotalArea {/*** Main method*/public static void main(String[] args) {//Declare circleArrayCircle4[] circleArray;//Create circleArraycircleArray = createCircleArray();//Print circleArray and total areas of the circlesprintCircleArray(circleArray);}/*** Create an array of Circle objects*/public static Circle4[] createCircleArray() {Circle4[] circleArray = new Circle4[5];for (int i = 0; i < circleArray.length; i++) {circleArray[i] = new Circle4(Math.random() * 100);}//Return Circle arrayreturn circleArray;}/*** Print an array of circles and their total area*/public static void printCircleArray(Circle4[] circleArray) {System.out.printf("%-30s%-15s\n", "Radius", "Area");for (int i = 0; i < circleArray.length; i++) {System.out.printf("%-30f%-15f\n", circleArray[i].getRadius(),circleArray[i].getArea());}System.out.println("---------------------------");//Compute and display the resultSystem.out.printf("%-30s%-15f\n", "The total area of circle is ", sum(circleArray));}/*** Add circle areas*/public static double sum(Circle4[] circleArray) {//Initialize sumdouble sum = 0;//Add areas to sumfor (int i = 0; i < circleArray.length; i++) {sum += circleArray[i].getArea();}return sum;}}
- 程序调用createCircleArray()方法创建一个由5个圆对象组成的数组。本章介绍了几个圆类。本例中使用的是9.9节中介绍的Circle类
- 圆的半径是使用Math.random()方法随机生成的。createCircleArray方法返回一个Circle对象的数组。这个数组作为参数传给printCircleArray方法,该方法显示每个圆的半径和面积以及它们的总面积。
- 圆的面积之和是用sum方法计算出来的,该方法以Circle对象的数组为参数,返回的是double类型的总面积值。
9.12 不可变对象和类
- 可以定义不可变类来产生不可变对象。不可变对象的内容不能被改变。
- 通常,创建一个对象后,它的内容是之后允许改变的。有时候也需要创建一个一旦创建其内容就不能再改变的对象。我们称这种现象为不可变对象(immutable object),而它的类就称为不可变类(immutable class)。例如:String 类就是不可变的。如果把程序清单9-8 中Circle类的设置方法删掉,该类就变成不可变类,因为半径是私有的,所以如果没有设置方法,它的值就不能再改变。
如果一个类是不可变的,那么它的所有数据域必须都是私有的,而且没有对任何一个数据域提供公共的设置方法。一个类的所有数据都是私有的且没有修改器并不意味着它一定是不可变类。例如下面的Student类,它的所有数据域都是私有的,而且也没有设置方法,但它不是不可变类。
public class Student{private int id ;private String name;private java.util.Date dateCreated;public Student(int ssn, String newName){id = ssn;name = newName;dateCreated = new java.util.Date();}public int getId(){return id;}public String getName(){return name;}public java.util.Date getDateCreated(){return dateCreated;}}
- 如下面的代码所示,使用getDateCreated()方法返回数据域dateCreated。它是对Date对象的引用,通过这个引用可以改变dateCreated的值。
public class Test{public static void main(String[] args){Student student = new Student(111222333, "John");java.util.Date dateCreated = student.getDateCreated();dateCreated.setTime(200000); //Now dateCreated field is changed!}}
9.13 变量的作用域
- 实例变量和静态变量的作用域是整个类,无论变量是在哪里声明的。
- 一个类中的实例变量和静态变量称为类变量(class variable)或数据域(data field)。在方法内部定义的变量称为局部变量。无论在何处声明,类变量的作用域都是整个类。类的变量和方法可以在类中以任意顺序出现,如图9-20a所示。但是当一个数据域是基于对另一个数据域的引用来进行初始化时则不是这样的。在这种情况下,必须首先声明另一个数据域,如图9-20b所示。为保持一致性,本书在类的开始处就声明数据域。
- 类变量只能声明一次,但是在一个方法内不同的非嵌套块中,可以多次声明相同的变量名。
如果一个局部变量和一个类变量具有相同的名字,那么局部变量优先,而同名的类变量将被隐藏(hidden)。例如:在下面的程序中,x被定义为一个实例变量,也在方法中被定义为局部变量。
public class F{private int x = 0; // Instance variableprivate int y = 0;public F(){}public void p(){int x = 1;//Local variableSystem.out.println("x = " + x);System.out.println("y = " + y);}}
- 假设f是F的一个实例,那么f.p( )的打印输出是什么呢?f.p( )的打印输出是:x为1,y为0。其原因如下:
- x被声明为类中初始值为0的数据域,但是它在方法p()中又被声明了一次,初值为1。System.out.println语句中引用的x是后者。
- y在方法p()的外部声明,但在方法内部也是可访问的。
- 为避免混淆和错误,除了方法中的参数,不要将实例变量或静态变量的名字作为局部变量名。我们将在下一节中讨论被方法参数所隐藏的数据域。
9.14 this引用
- 关键字this引用对象自身。它也可以在构造方法内部用于调用同一个类的其他构造方法
- 关键字this是一个对象可以用来引用自身的引用名。可以用this关键字引用对象的实例成员。例如,下面a的代码使用this来显式地引用对象的radius以及调用它的getArea()方法。this引用通常可省略,如b所示。然而,在引用被方法或者构造方法的参数所隐藏的数据域以及调用一个重载的构造方法时,this引用是必需的。
9.14.1 使用this引用数据域
- 使用数据域作为设置方法或者构造方法的参数是一个好方法,这样可以使得代码易于阅读,并且可以避免创建不必要的名字。在这种情形下,在设置方法中需要使用this关键字来引用数据域。例如,setRadius方法可以如a中实现,b中的实现是错误的。
- 数据域radius被设置方法中的参数radius所隐藏。需要采用this.radius这样的语法来引用方法中的数据域名字。隐藏的静态变量可以简单地通过ClassName.staticVariable引用。隐藏的实例变量可以使用关键字this来访问,如图9-21a所示
- 关键字this给出一种引用调用实例方法的对象的方法。调用f1.setI(10)时,执行了this.i = i,将参数i的值赋给调用对象f1的数据域i。关键字this是指调用实例方法setI的对象,如图9-21b所示。F.k = k 这一行的意思是将参数k 的值赋给这个类的静态数据域k,k是被类的所有对象所共享的。
9.14.2 使用this调用构造方法
关键字this可以用于调用同一个类的另一个构造方法。例如,可以如下改写Circle类:
public class Circle{private double radius;public Circle(double radius){this.radius = radius;}public Circle(){this(1.0);}}
- Java要求,在构造方法中语句this(arg - list)应在任何其他可执行语句之前出现。
- 如果一个类有多个构造方法,最好尽可能使用this(参数列表)实现它们。通常,无参数或参数少的构造方法可以用this(arg - list) 调用参数较多的构造方法。这样做通常可以简化代码,使类易于阅读和维护。
