第 6 章 类和对象
程序的主要功能是处理信息(数据),在程序中使用各种类型的变量来存储和表示数据。Java 语言
内置有 8 种基本数据类型:byte、char、short、int、long、float、double、boolean。此外,Java 还提供了
一个 String 类型用来处理字符串。这 8 种基本数据类型加上 String 类型,对应现实业务中的各种类型数
据,那么,用它们来处理业务数据够用吗?
6.1 使用内置原始数据类型的局限
考虑如下的一个需求:编程输出一本图书的基本信息。其实现代码可以如下所示:
public class BookInfoExample {
public static void main(String[] args){
String bookName = "Java 自学实用教程";
String bookAuthor = "….”
int pageSize = 680;
float bookPrice = 68.80f;
System.out.println("图书信息");
System.out.println("书名:" +bookName);
System.out.println("作者:" +bookAuthor);
System.out.println("页数:" +pageSize);
System.out.println("定价:" +bookPrice);
}
}
现在,我们对需求稍加变动:编程输出五本图书的基本信息。这时代码实现可以如下所示:
public class BooksInfoExample {
public static void main(String[] args) {
final int BOOK_NUMBER = 5;
String[] bookNames = {"图书 1", "图书 2", "图书 3", "图书 4", "图书 5"};
String[] bookAuthores = {"张三", "李四", "王老五", "赵小六", "周扒皮"};
int[] pageSizes = {125, 345, 235, 328, 726};
float[] bookPrices = {28.80f, 38.00f, 48.50f, 68.00f, 78.80f};
for (int i = 0; i < BOOK_NUMBER; i++) {
System.out.println("第" + i +"本图书信息");
System.out.print("书名:" + bookNames[i]);
System.out.print(",作者:" +bookAuthores[i]);
System.out.print(",页数:" +pageSizes[i]);
System.out.println(",定价:" +bookPrices[i]);
2
} //end for
} //end main
}
在上面的代码中,我们通过使用数组和循环结构,也可以轻松地实现“编程输出五本图书的基本信
息”这样的需求。但是,如果我们继续变动我们的需求,将需求进一步修改为“编程输出 500 本图书的
基本信息”,或者“编程输出 50000 本图书的基本信息”,代码应该怎么实现?
毫无疑问,仅使用原始数据类型的话,我们可以通过扩展数组的长度来同样实现修改过后的需求。
但是,考虑一下: 8 种基本数据类型表示上述数据,够不够?方便不方便程序处理?有没有更好的办法?
现实中,我们习惯怎样表示数据?--个人简历、图书、汽车、员工信息…
如果在编程中,仅使用原始数据类型的话,我们会发现有如下问题:现实中,我们表示图书信息,
习惯用“图书”这个整体数据载体,但是在实现代码中,必须把整个“图书”信息分解为原始的数据类
型:书名、作者、页码、定价….,完整的图书信息由此变得分散。 虽然这样也可以表示和处理图书信息,
但是很显然,代码显得很笨拙, 可读性变得很差,不便于维护,并且由于数据是分散的,在读写数据时,
很容易出现调用错误且不易发现。
为什么会这样?很明显,仅使用有限的原始数据类型来处理现实生活中几乎是无限的各种各样的数
据,已经不够灵活。特别是在我们要处理的业务越复杂,这种编程和维护的难度就越大。怎样解决呢?
很自然地,我们希望有更丰富的数据类型,方便表示数据和处理数据。
一种编程语言功能是否强大的指标之一,是其能否提供足够丰富的数据类型。数据类型越丰富,能
表示的数据就越丰富,越方便,越容易处理。
Java 语言提供了让我们声明(自定义)新的数据类型的能力,有效地解决了这个问题。也就是说,
在 Java 这种面向对象程序设计语言中, 除了 8 种基本数据类型,我们还可以根据需要随时定义新的数据
类型,以匹配现实生活中的各种各样的数据。这种自定义的新的数据类型基于原始数据类型,由原始数
据类型构造而来,并且要遵循特定的一些规则,所以称为“构造类型”。
理论上,开发者(程序员)可以定义无限多的新数据类型,完全可以满足处理现实生活中各种数据
的需求,只要在定义新的数据类型时,使用 class 关键字指明并遵循一定的约束和要求即可,Java 编译
器和运行时环境就会将这个类型视为 Java 语言可以处理的数据类型。
一旦声明了新的数据类型,在我们的程序中,就可以象 int、 char、 short、 float 等数据类型一样使用:
声明该类型的变量,向内存申请空间,存储数据等。
6.2 自定义新数据类型
如果在 Java 中编程中,要处理类似“图书”这样的信息,我们可以通过定义新的图书数据类型,并
在程序中定义这种数据类型的变量来存储图书信息。这种新的数据类型由原始数据类型构造而成,构造
的过程通常简称为“定义类”。
6.2.1 定义类
定义类,要使用关键字 class。下面是定义一个新的图书类的代码:
class Book{
String bookName; //字段,用来保存图书名称
·3·
int pageSize; //字段,用来保存页码数
float bookPrice; //字段,用来保存图书定价
Book(){} //构造器
}
上述代码中,class 是 Java 语言的关键字,用来告诉 Java 编译器,这是一种自定义的数据类型。关
键字 class 后跟的“Book”,是定义的新的数据类型名字,称为“类名”。在类名后,紧跟一对花括号,
在花括号中的内容,称为“类体”。在这个简单的 Book 类中,类体由三个简单的原始数据类型变量构
成,分别代表 Book 这种类型数据的状态:书名 bookName、页码数 pageSize 和图书定价 bookPrice。这
三个变量称为“成员变量”或“字段”。类体中除了字段之外,还定义了一个构造器,其名称与类名完
全相同,后跟一对小括号,小括号后再跟一对花括号。
可以看出,新的数据类型 Book 类是由原始数据类型构造而成,它把与某个信息相关的数据封装在
一起,便于组织、管理和使用。
将上述代码保存在 Book.java 源文件中。
6.2.2 使用类
在定义了新的数据类型 Book 类以后,我们再编写 Java 程序时,除了 8 个原始数据类型和 String 类
型之外,我们又多了一个新的 Book 类型可以使用。
在代码中使用 Book 类型和使用其他原始数据类型类似。下面的代码中演示了如何使用自定义的
Book 数据类型:
class Main{
public static void main(String[] args){
Book javaBook; //声明 Book 类型变量 javaBook
javaBook = new Book(); //调用构造器创建 Book 类的对象实例,赋给 Book 类型变量
System.out.println(javaBook.bookName);
System.out.println(javaBook.pageSize);
System.out.println(javaBook.bookPrice);
}
}
将上述代码保存到 Main.java 源文件中,与 Book.java 源文件位于同一目录下。编译并运行 Main.java:
javac Main.java
java Main
注意,这时不必分别编译 Book.java 和 Main.java,因为在 Main.java 文件中引用了 Book 这个类型,
所以在编译 Main.java 时,会自动地对 Book.java 源文件进行编译。
程序输出结果如下:
null
0
0.0
在上面的 Main.java 中,执行的第一行代码如下:
Book javaBook; //声明 Book 类型变量 javaBook
与声明原始数据类型变量相似,这里声明了一个 Book 类型的变量 javaBook,由“数据类型 变量名”
的方式声明。
接下来,执行第二行代码:
4
javaBook = new Book(); //调用构造器创建 Book 类的对象实例,赋给 Book 类型变量
在这行代码中,使用关键字 new 并调用 Book 类中的构造器 Book(),创建一个 Book 类型的实例,
并赋给变量 javaBook。由于 Book 类是构造类型,所以必须使用 new 运算符调用构造器,向内存申请与
该构造类型相适应的空间来保存某本具体图书的数据。也就是说,使用 new 运行符会在内存中开辟空间,
存储该类型的各个字段的值,并将该空间的起始内存地址(也称为“引用”)赋给变量 javaBook。其在
内存中的状态如下图所示:
最后,访问 javaBook 实例对象的各个字段的值,并输出:
System.out.println(javaBook.bookName);
System.out.println(javaBook.pageSize);
System.out.println(javaBook.bookPrice);
在上面的代码中,使用“点”运算符,访问对象 javaBook 中的各个字段的值:对象.字段名。由于
在创建 Book 类的实例 javaBook 时,并没有为该实例的各个字段赋值,所以系统会自动为 String 类型(或
其它构造类型)的字段赋默认的初值 null,为 int 类型的字段赋初值 0,为 float 类型的字段赋初值 0.0f。
构造器是一个在创建对象时被编译器自动调用的特殊方法,通常用来初始化成员变量、加载和配置
资源等。当创建对象时,Java 确保用户在操作对象之前自动调用相应的构造器,保证初始化的进行。后
面详细讲解。
6.3 声明类
在上一小节中,定义并使用了 Book 类。这是个非常简单的类,具备了类的最小结构。实际上, Java
语言提供了声明复杂类的能力。
6.3.1 声明类
要声明新的数据类型,需要使用关键字“class”。类的声明如以下代码所示。
class 类的名称{
·5·
//成员变量、构造器和方法声明
}
其中 class 关键字是必须的。类的名称用来标识一个类,它的命名要符合 Java 标识符命名规范。通
常类的名称的第一个字母要大写,例如:
class Book{
//成员变量、构造器和方法声明
}
如果类名是由多个单词组成的,那么每个单词的首字母都要大写,其它字母小写,例如:
class BookManager{
//成员变量、构造器和方法声明
}
在类的声明中,也可以加上访问权限控制符,以控制对该类的访问,其语法如下所示。
[类修饰符] class 类的名称{
//成员变量、构造器和方法声明
}
在这里,用方括号将类修饰符包围起来,它代表的含义是里面的内容是可选的,即声明类时,class
前可加修饰符,也可以不加。
类修饰符可以是 public、 private、 abstract 和 final, 其中与访问权限相关的修饰符是 public 和 private。
public 和 private 也是 Java 中的关键字,它们决定其他类能否访问本类或能访问什么。如果在类前面不含
public 关键字(默认),则定义的类只能在当前的软件包中使用。如果在类的声明前面加上 public 修饰
符,表示定义的类可以被 Java 的所有软件包使用。例如,可以这样定义类 Book:
public class Book{
//成员变量、构造器和方法声明
}
如果在类声明的前面加上 private 修饰符,这样的类只能在其它类内部声明。例如,可以在类 Book
内再定义一个类 A:
public class Book{
private class A{} //类的声明带有 private 修饰符,只能做为其它类的内部类
//成员变量、构造器和方法声明
}
上面使用 private 修饰符声明的类 A,只能在其它类内部声明,称为“内部类”。关于内部类,在后
面的章节详细讲解。
在同一个 Java 源文件中可以声明多个类,但不能包含两个或两个以上的带有 public 修饰词的类。如
果某个类的声明前含有 public 修饰符,那么含有此类的源文件必须与此类的类名相同。例如一个 Java
源文件中含有上述声明 Book 类的代码,那么这个源文件必须命名为 Book.java。
在声明一个类时,还可以提供更多的信息。例如,指明该类的父类(也称为超类)的名称,即该类
是从哪个类派生过来的,这需要在类名后面加上关键字 extends 来实现,例如:
[类修饰符] class 类的名称 [extends 父类名称]{
//成员变量、构造器和方法声明
}
还可以在声明一个类时指明该类是否实现接口,这需要在类名后面加上关键字 implements 来实现,
例如:
[类修饰符] class 类的名称 [extends 父类名称][implements 接口名称]{
6
//成员变量、构造器和方法声明
}
关于继承、派生和接口,在后面的章节讲解。
一般情况下,类的声明可以按顺序包括以下这些内容:
修饰符,如 public、private 以及其它修饰符。
类名,前面加上关键字 class,并且按惯例首字母要大写。
父类(超类)的名称,都要在前面加上关键字 extends。一个类只能继承自一个父类。
被该类实现的接口列表(用逗号进行分隔),在接口前面加上关键字 implements。一个类可以
实现多个接口。
类体,用花括号{}包围。
类体,指的是类名后面大括号中的内容。类体包含所有用于创建自该类的对象的生命周期的代码,
包括构造器(用于初始化新对象)、字段(用于表示类及其对象的状态)、以及方法(实现类及其对象
的行为)。
6.3.2 声明类的成员变量
在程序中有多种类型的变量,如字段、局部变量、参数等。这些变量在程序中的位置不同,所起的
作用也不相同。下面对这些变量加以说明。
在一个类中的成员变量—被称为字段。
在一个方法中或代码块中的变量—被称为局部变量。
在方法声明中的变量—被称为参数。
其中,字段的声明由三部分组成,其语法格式如下。
[修饰符] 数据类型 变量名称;
组成字段的三部分按顺序依次为:
0 个或多个修饰符,如 public 或 private。
字段的类型。
字段的名称。
例如,在上节声明的 Book 类,它有三个字段:书名、页码数和定价。可以使用下面的代码来定义
三个变量,分别代表 Book 类的这三个字段:
public String bookName; //代表书名
public int pageSize; //代表页码
public float bookPrice; //代表图书定价
Book 类的字段被命名为 bookName、 pageSize 和 bookPrice。关键字 public 说明这些字段是公共成员,
可以被任何能够访问该类的对象所访问。下面对构成类的成员变量声明的每一个部分详加说明。
1.访问修饰符
成员字段最左边的修饰符可以让程序员控制其它类对成员字段有什么样的使用权限。目前只考虑
public 和 private(其他访问修饰符将在后面的章节讨论):
public 修饰符:可以从所有的类访问该字段。
private 修饰符:只能从它所在的类内部访问该字段。
无修饰符:允许从同一个包中的其它类访问该字段。
根据封装的原则,通常使用私有字段(即使用 private 修饰符来定义字段),这意味着这些字段只能
从类的内部被直接访问,这样可以增强代码的安全性,防止不可预知的字段访问错误。如下面定义的
·7·
Book 类。
public class Book{
private String bookName; //表示图书的名称
private int pageSize; //表示图书的页码数量
private float bookPrice; //表示图书的定价
}
在这里,声明了三个 private (私有的)字段,没有赋初值。在 Java 中规定,如果声明类的字段没有
初始化,那么 Java 虚拟机会赋给其默认的初始值:String 类型的为 null,float 类型的为 0.0f,int 类型的
为 0,boolean 类型的为 false。
2.数据类型
所有的成员变量(字段)都必须有一个类型。可以是 8 个原始数据类型之一,如 int、float、boolean
等。也可以是构造类型,如字符串、数组或对象。
3.变量名称
所有的变量,不管是字段,还是局部变量,或者参数,都遵循 Java 中关于标识符的命名规范。方法
名和类名也遵循同样的命名规范,但在以下方面与变量命名不同。
类名的首字母应该大写。
方法名的第一个(或者唯一一个)单词应该是一个动词。
6.3.3 声明类的成员方法
所谓方法,类似于其他语言中的函数,是一个相对独立的代码语句块,通常实现一定的功能。在类
中声明成员方法的语法格式如下。
[方法修饰词列表] 返回类型 方法名称(方法的参数列表){
//方法体语句
}
说明:
其中用方括号括起来的部分为可选项,即可以不含该选项。
格式第一行的内容称为成员方法定义的头部,或者称为成员方法的声明。
在上面的格式中,返回类型指定当前这个方法执行以后,需要返回数据的数据类型,可以是原
始数据类型,如 int、float、boolean、char 等,也可以是构造数据类型,如数组、类和对象。如
果该方法执行以后不返回任何数据,则此处应写上关键字 void,否则将出现编译错误。
方法名要符合 Java 标识符命名规范,用来标识当前的成员方法。对该方法的调用需要引用此方
法名称。
在方法名后面紧跟一对圆括号,里面写上需要传递给该方法的参数。参数也是变量,传进来的
参数在方法中可以使用,这是方法接受外部信息的一种方式。参数可以有 0 个、1 个或多个,
所以称为参数列表。如果有 0 个参数,圆括号不能省略,里面什么也不写。如果有多个参数,
参数之间用逗号分隔。
在参数列表后面,紧跟一对花括号,这称为方法体。方法体中是一系列完成特定功能模块的代
码。
下面是一个典型的类的方法声明:
public String getBookDesc(String bookName, int pageSize , floatbookPrice){
//关于图书信息的描述
8
}
对于成员方法的声明来说,必须有返回类型、方法名称、一对圆括号(),以及在一对花括号之间
的方法体,也就是说,在声明方法时,只有返回类型、方法名、小括号和花括号是必须的。总体来说,
方法声明有 6 个部分,按顺序依次是:
修饰符:如 public、private,以及其他一些修饰符(在稍后讲解)。
返回类型:方法所返回的数据值的数据类型。或者如果方法不返回值的话,用 void。
方法名称:遵循 Java 标识符命名规则,但按惯例是以动词开头。
在圆括号中的参数列表:一个逗号分隔的输入参数列表,前面要加上它们的数据类型。如果没
有参数,就使用空的圆括号。
一个异常列表,将在后面章节讨论。
包围在花括号中的方法体:方法的代码,包括局部变量的声明都在这里。
其中,方法的名称和参数类型,组成“方法签名”。例如,可以这样声明一个比较两个整数大小的
方法。
public int getMaxValue(int value1,int value2){ //方法的签名
if(value1>=value2){ //如果 value1 比 value2 的值大
return value1; //方法结束,返回 value1 变量的值
}else{ //否则
return value2; //方法结束,返回 value2 变量的值
}
}
在上面的方法的声明部分(方法头部),访问修饰符为 public,这说明只要能访问该方法所在的类
的对象,都可以调用此方法 getMaxValue()。返回类型为 int,说明这个方法被调用执行以后,需要返回
一个整数(在这里返回的是两个整数当中的较大值)。方法名为 getMaxValue,它要符合 Java 标识符命
名规则,并且第一个单词为动词。在方法名称后面是一对圆括号,在这里声明了两个参数:value1 和
value2。两个参数都是整数,注意这里声明参数的格式,在每一个参数名前都必须指明该参数的数据类
型。参数之间用逗号(,)进行分隔。上述方法声明中的方法签名为:
getMaxValue(int,int)
在下面的示例程序中,我们为 Book 类添加一个输出图书信息的成员方法 bookInfo(),该方法用来输
出图书的基本信息,不需要返回值,因此返回类型指定为 void:
public class Book {
private String bookName;
private int pageSize;
private float bookPrice;
public void bookInfo() {
System.out.println("书名:" + bookName+ ",页数:" + pageSize + ",定价:" +bookPrice);
}
}
6.3.4 成员方法命名
虽然方法的名称可以是任何合法的标识符,但是编码惯例上对方法命名有一定的约束。在惯例上,
方法名称应该是一个小写的动词,或者一个多词组组成的名称,但是要以一个小写的动词起始,后面跟
形容词、名词等。在多词组组成的方法名中,从第二个单词开始,后面的每一个单词的首字母要大写。
·9·
如下面这些方法的名称所示。
run
runFast
getBackGround
getFinalData
compareTo
setX
isEmpty
在下面的代码中,声明了 Book 类中的成员方法:
public class Book {
private String bookName;
private int pageSize;
private float bookPrice;
public void bookInfo() {
System.out.println("书名:" + bookName+ ",页数:" + pageSize + ",定价:" +bookPrice);
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public float getBookPrice() {
return bookPrice;
}
public void setBookPrice(float bookPrice) {
this.bookPrice = bookPrice;
}
}
在上面的代码中, 除了输出图书信息的方法 bookInfo()外,还为 Book 类的每个成员变量(字段)都
声明了两个用来读取该字段的成员方法。
通常,一个方法在一个类中具有唯一的名称。然而,一个方法也可能具有与其他方法相同的名称,
这就是“方法重载”。
10
6.3.5 方法重载
Java 语言支持“重载”方法,并且 Java 能够根据不同的“方法签名”来区分重载的方法。这意味着,
在一个类中,可以有相同名称但具有不同参数列表的方法(当然会有一些限定条件,这在接口和继承当
中讨论)。
假设有一个类,它可以绘制各种类型的数据(字符串、整数等等),并且它包含一个用来画每一种
数据类型的方法。如果为每一个方法都起一个新的名称,是很麻烦的,例如,drawString、drawInteger、
drawFloat 等等。很明显,这些方法所做的操作基本上都是相似的。
在 Java 语言中,允许程序员为所有这些绘制方法使用相同的名称,但是为每一个方法传递一个不同
的参数列表。因此,这个数据绘制类可能会声明四个名为 draw 的方法,每一个都有一个不同的参数列
表,如下所示。
public class DrawDate{
//……
public void draw(String s){ //声明绘制字符串的方法
//……
}
public void draw(int i){ //声明绘制整数的方法
//……
}
public void draw(double f){ //声明绘制双精度小数的方法
//……
}
public void draw(int i , double f){ //声明绘制一个整数和一个小数的方法
//……
}
}
通过传递给方法的参数的数量和类型来区分重载的方法。在上面的示例代码中,draw(String s)和
draw(int i)是不同的方法,并且都是唯一的,因为它们要求不同的参数类型。
在同一个类中,不能声明两个(或两个以上)具有相同签名(即方法名称相同且参数数量和类型也
相同)的方法,即使这两个方法有不同的返回类型也不可以。因为编译器在区分方法时,不考虑返回类
型,因此即使这两个方法有不同的返回类型但方法签名相同,编译器也无法区分。在使用重载的方法时
应谨慎,因为方法重载会降低代码的可读性。
下面是使用方法重载的一个示例:
public class MethodOverride {
public void sayHello() {
System.out.println("hello");
}
public void sayHello(String msg) {
System.out.println(msg);
}
public static void main(String[] args) {
·11·
MethodOverride methodOverride = new MethodOverride();
methodOverride.sayHello(); //使用“点”运算符调用对象的方法
methodOverride.sayHello("hello world!"); //使用“点”运算符调用对象的方法
}
}
编译并运行,输出结果如下:
hello
hello world!
可以看出,Java 会根据重载方法的签名,对调用的方法自动进行区分。
6.3.6 声明类的构造器
类包含有构造器,当创建对象实例时,构造器会被调用以根据类模板创建对象实例。构造器的声明
看上去与方法声明类似,但是构造器有自己的特点:
构造器的名称必须与类名相同。
构造器没有返回类型,包括关键字 void 也不能有。
任何类都含有构造器。一个类可以有多个构造器,多个构造器的参数列表应不同。
如果不明确提供构造器,Java 编译器会默认提供一个无参构造器(即这个默认的构造器不含任
何参数)。
如果在类中明确定义了带参数的构造器,Java 编译器就不再为这个类提供默认的无参构造器。
通常的做法是:明确提供一个无参的构造器
构造器前可以带有访问修饰符,以控制其它类对构造器的调用
例如,我们可以这样来声明一个 Book 类:
public class Book{
String bookName;
int pageSize;
float bookPrice;
}
这个 Book 类中并没有明确声明构造器,所以 Java 编译器会默认提供一个无参的构造器。在创建
Book 类的实例对象时,可以使用 new 运算符调用这个默认的无参构造器,代码如下所示:
class Main{
public static void main(String[] args){
Book javaBook = new Book(); //调用默认的无参构造器
//……
}
}
为了代码的可读性,建议在定义类时,明确地声明无参的构造器:
public class Book{
String bookName;
int pageSize;
float bookPrice;
public Book(){
}
}
12
一个类可以有多个构造器,包括带参数的构造器和不带参数的构造器。例如下面的这个示例:
public class Book{
public String bookName;
public int pageSize;
public float bookPrice;
//无参构造器
public Book(){
bookName = "Java 从初学到精通";
pageSize = 652;
bookPrice = 68.00f;
}
//带参数的构造器
public Book(String _bookName, int _pageSize, float _bookPrice){
bookName = _bookName; //用参数 bookName 的值初始化字段 bookName
pageSize = _pageSize; //用参数 pageSize 的值初始化字段 pageSize
bookPrice = _bookPrice; //用参数 bookPrice 的值初始化字段 bookPrice
}
public static void main(String[] args){
Book javaBook = new Book();
System.out.println(javaBook.bookName);
System.out.println(javaBook.pageSize);
System.out.println(javaBook.bookPrice);
System.out.println("-----------------------");
Book sqlBook = new Book("MySQL 数据库", 265,32.5f);
System.out.println(sqlBook.bookName);
System.out.println(sqlBook.pageSize);
System.out.println(sqlBook.bookPrice);
}
}
通常通过在构造器中传入参数,对字段进行初始化,以达到初始化所创建对象实例的目的,如上述
代码所示。将上述代码保存为 Book.java 源文件,编译并运行,输出结果如下:
Java 从初学到精通
652
68.0
-----------------------MySQL 数据库
265
32.5
两个构造器都可以在 Book 类中声明,因为它们具有不同的参数列表。与重载方法类似,Java 基于
参数列表中的参数数量以及其类型来区分构造器。不能为同一个类写两个具有相同参数数量和参数类型
的构造器,否则将会引起编译时错误。
也可以不提供构造器,但是这时候一定要小心。 Java 编译器自动地为任何没有构造器的类提供一个
无参的默认构造器。这个默认的构造器将调用其超类的无参构造器。在这种情况下,如果超类没有一个
·13·
无参的构造器,编译器将会报错,因此程序员必须对此进行验证。如果一个类没有显式的超类,那么它
隐含地超类是 Object,Object 类有一个无参的构造器。
也可以在一个构造器声明前使用访问控制修饰符, 如 public 或 private,以控制哪一个类可以调用此
构造器。如果另外一个类不能调用这个类的构造器,那么它就无法创建这个类的对象。
6.4 向方法或构造器内传递信息
在方法或构造器的声明中,声明了参数的数量和类型。通过参数,可以向方法或构造器内传递信息。
在创建对象时,通过构造器的参数来初始化对象的初始状态;在对象间传递消息时,是通过调用方法并
将消息封装在参数中实现的。本节就介绍如何向方法或构造器内传递信息。
6.4.1 使用参数
当需要向一个对象传递消息时,可以通过调用对象的方法并传递参数来实现。例如,下面是一个计
算家庭贷款每月还款额的方法,基于贷款额、利率、贷款期限(还款周期数)、以及贷款的最终值:
public double computePayment(double loanAmt, //代表贷款额
double rate, //代表贷款利率
double futureValue, //代表贷款的最终值
int numPeriods) { //代表还款周期
double interest = rate / 100.0;
double partial1 = Math.pow((1 + interest), -numPeriods);
double denominator = (1 - partial1) / interest;
double answer = (-loanAmt / denominator)
- ((futureValue * partial1) / denominator);
return answer;
}
上面方法中计算贷款的方式有些复杂,暂时不去管它。看一看这个方法的参数,总共有 4 个:贷款
额(loanAmt)、利率(rate)、贷款最终值(futureValue)、以及还款周期数。前三个参数都是双精度
浮点数,第四个参数是一个整数。参数被用在方法体中,并且在运行时接收传递进来的实参数值。(这
里的参数指的是形参,是方法声明中的参数变量列表。而实参是在方法被调用时实际传递进来的值。当
调用一个方法时,实参和形参必须在数据类型和顺序上相匹配)。
6.4.2 实参与形参的关系
在调用对象的方法时,实际向方法传递的参数称为“实参”;而在定义方法时,声明的方法参数称
为“形参”。很多初学者在对实参和形参的理解上常常感到迷惑。实际上,要理解实参和形参的不同,
一定要从内存变化的角度上去看,就很容易理解了。
下面通过示例程序来介绍实参与形参的关系,以及它们在内存中的变化。
【例】编写一个类,定义一个成员方法 getMaxValue(),它返回两个参数中较大的值。
public class ParameterExample{
public static void main(String[] args){
int a = 5, b=8; //声明变量 a 和 b,并赋初值
14
int max = 0; //声明变量 max,并赋初值 0
ParameterExample parameterExample = new ParameterExample();
max = parameterExample .getMaxValue(a,b); //调用 getMaxValue()方法,并将返回值赋予 max
System.out.println(“最大值是”+max); //输出 max 的值
}
//定义对两个整数进行比较的成员方法
public int getMaxValue(int value1,int value2){
if(value1>=value2){ //比较形参 value1 和 value2 的大小
return value1; //如果 value1 大,返回其值
}else{ //否则
return value2; //返回 value2 的值
}
}
}
上面程序的输出结果如下。
最大值是 8
在上面的程序中,变量 a 和 b 称为“实参”,而方法 getMaxValue()后面参数列表中的参数 value1
和 value2 称为“形参”。当在 main 方法中声明变量 a、b、max 时,程序在内存中为这三个变量分配空
间,其中变量 a 和 b 分别赋于初值 5 和 8,max 赋予初值 0。(注意,因为 max 是在方法内声明的局部变
量,因此它不会象字段那样被赋予默认 0 值,所以在这里应该为其明确赋予初值 0。)此时变量 a、b 和
max 在内存中的状态如下图所示。
5
8
0
变量a
变量b
变量max
图 实参内存情况
当调用 getMaxValue()方法时,程序转向调用成员方法 getMaxValue()内的代码,并且在内存中为形
参 value1 和 value2 分配内存,同时将实参值按顺序一一赋予形参,这里将 a 的值赋予 value1,将 b 的值
赋予 value2。因此 value1 的值等于 5,value2 的值等于 8,如下图所示。这里实参与形参的关系仅仅是
将实参的值传递给形参,此后再无关系。
5
8
0
变量a
变量b
变量max
5
8
形参value1
形参value2
图 实参传值给形参
·15·
在方法 getMaxValue()中,对 value1 和 value2 的值进行比较,返回其最大值,并将返回值赋给变量
max,如下图所示。此时 max 的值改变为返回的最大值 8。然后主程序接着执行后面的语句,输出 max
的值。
5
8
8
变量a
变量b
变量max
5
8
形参value1
形参value2
图 方法调用返回值赋给变量 max
一定要注意,当实参和形参均为原始数据类型时,实参与形参的关系仅仅是将实参的值传递给形参,
此外实参与形参没有任何关系。所以在成员方法中对形参的改变,不会影响到实参。
【例】形参与实参关系示例。
public class SwapExample{
public static void main(String[] args){
int a = 5, b=8;
SwapExample swapExample = new SwapExample();
System.out.println(“交换之前的值为:”);
System.out.println(“a=” + a);
System.out.println(“b=” + b);
swapExample.exchangeValues(a, b); //调用 exchangeValues()方法
System.out.println(“交换之后的值为:”);
System.out.println(“a=” + a);
System.out.println(“b=” + b);
}
//定义对两个整数进行交换的成员方法,因不需要返回值,在声明中使用 void
public void exchangeValues(int value1,int value2){
int temp = 0; //声明一个中间变量
temp = value1; //交换第一步
value1 = value2; //交换第二步
value2 = temp; //交换第三步
}
}
上面程序的输出结果如下。
交换之前的值为:
a=5
b=8
交换之后的值为:
a=5
b=8
16
可以发现,在方法 exchangeValues()中对两个整数值进行了交换,但是对变量 a 和 b 的值并没有影
响。下面我们根据程序执行过程分析变量在内存中的变化情况。
当程序在 main()方法中执行到下面这行代码时:
int a = 5, b=8;
在程序中声明了两个变量 a 和 b 并赋初值,这时会在内存中开辟相应的空间并保存初值。其内存如
下图所示:
5
8
变量a
变量b
图 实参 a 和 b
接下来当程序在 main()方法中执行到调用成员方法 exchangeValues()的代码时:
exchangeValues(a, b);
实参 a 和 b 将值传递给形参 value1 和 value2,此时会在内存中为参数变量 value1 和 value2 开辟内
存空间,变量 a、b、value1 和 value2 在内存中的状态如下图所示:
5
8
变量a
变量b
5
8
形参value1
形参value2
0
变量temp
图 实参传值给形参
接下来,程序执行流程转向 exchangeValues()方法内部,在该方法内部开始对 value1 和 value2 的值
进行交换。在交换第一步,执行下面的代码:
temp = value1; //交换第一步
这时会将 value1 的值 5 保存在中间变量 temp 中,此时各个变量在内存中的状态如下图所示:
5
8
变量a
变量b
5
8
形参value1
形参value2
5
变量temp
图 交换第一步
然后执行下面的代码,将 value2 的值 8 赋给 value1:
value1 = value2; //交换第二步
此时各个变量在内存中的状态如下图所示:
·17·
5
8
变量a
变量b
8
8
形参value1
形参value2
5
变量temp
图 交换第二步
最后,将中间变量中保存的值 5 赋给 value2:
value2 = temp; //交换第三步
此时各个变量在内存中的状态如下图所示:
5
8
变量a
变量b
8
5
形参value1
形参value2
5
变量temp
图 交换第三步
方法 exchangeValues()调用结束时,只是将形参 value1 和 value2 的值进行了交换,对实参 a 和 b 没
有任何影响,因此输出结果 a 和 b 的值保持不变。
6.4.3 参数类型
一个方法或构造器的参数可以是任何数据类型,包括原始数据类型如 double、float、int 等以及构造
数据类型如对象和数组。下面是一个接收一个原始数据类型数组做为方法参数的方法的示例。
/*
* 原始数据类型数组作为方法参数
*/
public class ParameterIsArraysExample1 {
int[] numbers = new int[3];
//将原始数据类型数组做为方法参数
public void setNumbers(int[] intArrays){
numbers = intArrays;
}
public void printNumbers(){
for(int number: numbers){
System.out.println(number);
}
}
//将原始数据类型数组做为方法参数
public void printNumbers(int[] intArrays){
for(int number: intArrays){
18
System.out.println(number);
}
}
public static void main(String[] args) {
int[] intArrays = {6, 12, 35};
ParameterIsArraysExample1 parameterIsArraysExample = newParameterIsArraysExample1();
parameterIsArraysExample.printNumbers(intArrays);
parameterIsArraysExample.setNumbers(intArrays);
parameterIsArraysExample.printNumbers();
}
}
也可以向构造器传递原始数据类型参数。如下例所示:
/*
* 原始数据类型数组作为构造器参数
*/
public class ParameterIsArraysExample2 {
int[] numbers = new int[3];
public ParameterIsArraysExample2(){}
//将原始数据类型数组作为构造器参数
public ParameterIsArraysExample2(int[] intArrays){
numbers = intArrays;
}
public void printNumbers(){
for(int number: numbers){
System.out.println(number);
}
}
public static void main(String[] args) {
int[] intArrays = {6, 12, 35};
ParameterIsArraysExample2 paramterIsArraysExample = newParameterIsArraysExample2(intArrays);
paramterIsArraysExample.printNumbers();
}
}
构造(引用)类型数组也可以象原始数据类型数组一样作为方法参数传递,如下例所示:
/*
* 构造类型数组作为方法参数
*/
class Book{
String bookName;
int pageSize;
float bookPrice;
public Book(){}
·19·
public Book(String _bookName, int _pageSize, float _bookPrice){
bookName = _bookName;
pageSize = _pageSize;
bookPrice = _bookPrice;
}
}
public class ParameterIsArraysExample3 {
Book[] books = new Book[3];
//将对象数组作为方法参数
public void printBooks(Book[] books){
for(Book book: books){
System.out.print(book.bookName + ",");
System.out.print(book.pageSize + ",");
System.out.println(book.bookPrice);
}
}
public static void main(String[] args) {
Book[] bookArrays = {
new Book("图书 1", 100, 12.00f),
new Book("图书 2", 120, 23.50f),
new Book("图书 3", 560, 64.50f)
};
ParameterIsArraysExample3 paramterIsArraysExample = newParameterIsArraysExample3();
paramterIsArraysExample.printBooks(bookArrays);
}
}
构造(引用)类型数组也可以象原始数据类型数组一样作为构造器参数传递,如下例所示:
/*
* 构造类型数组作为构造器参数
*/
class Book{
String bookName;
int pageSize;
float bookPrice;
public Book(){}
public Book(String _bookName, int _pageSize, float _bookPrice){
bookName = _bookName;
pageSize = _pageSize;
bookPrice = _bookPrice;
}
}
20
public class ParameterIsArraysExample4 {
Book[] books = new Book[3];
public ParameterIsArraysExample4() {}
//将对象数组作为构造器参数
public ParameterIsArraysExample4(Book[] bookArrays) {
books = bookArrays;
}
public void printBooks(){
for(Book book: books){
System.out.print(book.bookName + ",");
System.out.print(book.pageSize + ",");
System.out.println(book.bookPrice);
}
}
public static void main(String[] args) {
Book[] bookArrays = {
new Book("图书 1", 100, 12.00f),
new Book("图书 2", 120, 23.50f),
new Book("图书 3", 560, 64.50f)
};
ParameterIsArraysExample4 paramterIsArraysExample =
new ParameterIsArraysExample4(bookArrays);
paramterIsArraysExample.printBooks();
}
}
需要注意的是,Java 语言不允许将一个方法传递到另一个方法中,但是可以传递一个对象到一个方
法中,然后再调用这个对象的方法。如下例所示:
/*
* 方法参数和构造器参数也可以是构造(引用)类型
*/
class Book{
public void bookInfo(){
System.out.println("这是一本图书");
}
}
public class ParameterIsObjectExample {
Book book ;
public ParameterIsObjectExample(){}
//将对象作为参数传递给构造器
public ParameterIsObjectExample(Book _book){
book = _book;
·21·
}
public void printBookInfo(){
book.bookInfo();
}
//将对象作为参数传递给方法
public void printBookInfo(Book _book){
_book.bookInfo();
}
public static void main(String[] args){
Book book = new Book();
ParameterIsObjectExample parameterIsObjectExample = newParameterIsObjectExample();
parameterIsObjectExample.printBookInfo(book);
ParameterIsObjectExample parameterIsObjectExample2 = newParameterIsObjectExample(book);
parameterIsObjectExample2.printBookInfo();
}
}
6.4.4 传递任意数量的参数
可以使用“可变参数”来传递任意数量的值到一个方法。当程序员不知道有多少个特定类型的参数
要被传递给方法时,使用可变参数。可变参数是手工创建一个数组的简洁方法。
要使用可变参数,在最后一个参数的类型后面跟上省略号“„“,然后一个空格,再接着是参数名。
具有这种可变参数的方法可以接收任意数量的该类型的参数,包括空参数。例如下面代码:
public Polygon polygonFrom(Point... corners) {
//corners 参数被当作数组处理,可以调用数组属性 length 获得长度
int numberOfSides = corners.length;
double squareOfSide1, lengthOfSide1; //声明两个变量
squareOfSide1 = (corners[1].x - corners[0].x)*(corners[1].x -corners[0].x)
+ (corners[1].y - corners[0].y)*(corners[1].y - corners[0].y) ;
lengthOfSide1 = Math.sqrt(squareOfSide1); //调用 Math 类的静态方法求平方根
…
}
可以看出来,在上面这个方法中, corners 被当做数组使用。调用这个方法时,既可以使用一个数组,
也可以使用一系列的参数。不管使用哪种形式,方法中的代码都会将参数作为一个数组对待。
最经常使用可变参数的地方是打印方法。例如 printf()方法:
public PrintStream printf(String format,Object… args)
这个方法允许打印任意数量的对象。例如,它可以这样被调用:
System.out.printf(“%s:%d,%s,%s,%s%n”, name,idnum,address,phone,email);
6.4.5 参数名称
当在一个方法或一个构造器中声明一个形参时,就提供了一个形参名称。形参的名称在方法体内使
22
用,代表传递进来的实参。形参的名称在其作用域内必须是唯一的。不能在同一方法或构造器中有两个
相同的参数名,参数名也不能与方法内或构造器内的局部变量的名称相同。
形参的名称可以与类的字段名称相同。在这种情况下,称之为形参“屏蔽”了字段。屏蔽字段会让
代码难以阅读,按惯例,它只用于构造器中和设置特定字段的方法中这两种情况。例如,下面这个 Book
类和它的构造器方法及 set 方法:
public class Book {
private String bookName;
private int pageSize;
private float bookPrice;
//构造器参数名称与成员变量(字段)的名称相同,这是一种惯例
public Book(String bookName, int pageSize, float bookPrice) {
this.bookName = bookName; //用参数 bookName 的值初始化字段 bookName
this.pageSize = pageSize; //用参数 pageSize 的值初始化字段 pageSize
this.bookPrice = bookPrice; //用参数 bookPrice 的值初始化字段 bookPrice
}
//以下的 set 方法中,参数名称与成员变量(字段)的名称相同,这是一种惯例
public void setBookName(String bookName) {
this.bookName = bookName;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public void setBookPrice(float bookPrice) {
this.bookPrice = bookPrice;
}
}
在上面的代码中,Book 类有三个字段:bookName、pageSize 和 bookPrice。构造器和 set 方法都有
相应的参数,每一个都与字段名相同。每个方法参数屏蔽与其名称相同的字段,因此当在构造器或方法
体中简单地使用名称 bookName、pageSize 或 bookPrice 时,指的是参数 bookName、参数 pageSize 或参
数 bookPrice,而不是字段 bookName、字段 pageSize 或字段 bookPrice。要在构造器或 set 方法中访问相
应的字段,必须使用关键字 this。(这在后面的“使用 this 关键字”讲解)
6.4.6 传递原始数据类型参数
原始数据类型的参数,如 int 或 double,是通过“值传递”的形式传给方法的,这意味着对参数值
的任何改变只影响到方法内部。当方法调用返回时,参数所占用的内存会被释放并且任何对它们所做的
改变会丢失。
【例】编写一个类,定义一个成员方法 passMethod(),向其传递原始数据类型的参数。
public class ValueParExample{
public static void main(String[] args) {
·23·
int x = 3;
ValueParExample valueParExample = new ValueParExample();
valueParExample.passMethod(x); //调用方法 passMethod(),使用 x 作为实参
System.out.println("调用方法 passMethod 以后, x = " +x); //输出 x,看它的值是否被改变了
}
public void passMethod(int p) { // 在方法 passMethod()中改变参数的值
p = 10;
}
}
编译并运行此程序,输出结果如下。
调用方法 passMethod 以后, x = 3
可以看到,虽然在方法 passMethod(int)中改变了参数 p 的值。但是因为方法 passMethod()中的参数 p
只是实参的一个拷贝,所以对 p 的值的改变,并不影响实参。
6.4.7 传递引用数据类型参数
引用数据类型参数,如对象,也是通过传值方式传递给方法的。这意味着,当方法返回时,传进来
的引用仍然引用先前的对象。然而,对象的字段的值有可能在方法中被改变了(如果有合适的访问级别
的话)。例如,考虑下面这个示例中的方法 swap(),其参数是一个整型数组。
public class ReferenceParExample {
public void swap(int[] nums) {
int temp = 0;
temp = nums[0];
nums[0] = nums[1];
nums[1] = temp;
}
public static void main(String[] args) {
int[] numbers = {8, 5, 12, 34, 158};
ReferenceParExample referenceParExample = new ReferenceParExample();
System.out.print("交换之前,数组元素为:");
for (int num : numbers) {
System.out.print(num + " ");
}
System.out.println(); //换行
referenceParExample.swap(numbers); //调用 swap 方法交换数组元素
System.out.print("交换之后,数组元素为:");
for (int num : numbers) {
System.out.print(num + " ");
}
24
}
}
程序从 main()方法入口开始按顺序执行代码。当执行到下面这行代码时:
int[] numbers = {8, 5, 12, 34, 158};
在程序中声明了整型数组并赋初值,这时会在内存中开辟相应的空间并保存每个数组元素的初值。
其内存如下图所示:
图 实参 a 和 b
接下来当程序在 main()方法中执行到调用方法 swap(numbers)的代码时:
swap(numbers);
程序会转向调用方法 swap(int[] nums), 在内存中为参数变量 nums 开辟内存空间, 并将实参 numbers
中的值(地址 0xAE68F9)传递给形参 nums,即 numbers 和 nums 指向同一个数组。这时内存中的状态
如下图所示:
图 实参传值给形参
接下来,程序执行流程转向 swap()方法内部,在该方法内部开始声明一个中间变量 temp 并赋初值
为 0,这时会在内存中开辟一个新的空间给变量 temp。如下图所示:
·25·
图 开辟临时变量 temp 内存空间
然后交换第一个数组元素 nums[0]和第二个数组元素 nums[1]的值。 在交换第一步,执行下面的代码:
temp = nums[0]; //交换第一步
这时会将 nums[0]中的值 8 保存在中间变量 temp 中,此时各个变量在内存中的状态如下图所示:
图 交换第一步
然后执行下面的代码,将第二个数组元素 nums[1]的值 5 赋给第一个数组元素 nums[0]:
nums[0] = nums[1]; //交换第二步
此时各个变量在内存中的状态如下图所示:
图 交换第二步
26
最后,将中间变量 temp 中保存的值 8 赋给数组第二个元素 nums[1]:
nums[1] = temp; //交换第三步
此时各个变量在内存中的状态如下图所示:
图 交换第三步
方法 swap()调用结束时,数组中第一个元素和第二个元素的值进行了交换。由于实参 numbers 和形
参 nums 实际上引用的是同一个数组,所以对 nums 数组的第一个元素和第二个元素的交换,实际上也
是对 numbers 数组的第一个元素和第二个元素进行了交换。即当方法的参数是引用类型时,对形参的改
变会影响到实参。
6.5 对象
当声明了新的数据类型(类)以后,如 Book 类,那么我们在程序中除了可以使用 8 种基本数据类
型和 String 类型之外,还可以使用这个新的数据类型 Book:声明该类型的变量,为该变量赋值,从该
变量获取需要的数据等。
在 Java 中,对于自定义的类的变量,有个专门的术语“对象”来表示,例如,我们将声明自定义的
类 Book 类型的变量称为“声明 Book 类的对象”或“声明 Book 类的一个实例对象”。
6.5.1 对象实例
下面通过一个简单的程序,说明对象的使用。在下面这个程序 CreateObjectDemo中,定义了三个类。
其中两个类 Point 和 Rectangle 分别代表点和矩形。在另外一个主程序 CreateObjectDemo中,创建这两
个类的实例对象并使用其属性和方法完成相应的操作。
【例】创建程序 CreateObjectExample.java。在这个程序中,创建三个对象,一个 Point 对象和两个
Rectangle 对象。实现步骤如下。
(1)打开记事本,输入以下代码:
//创建类 Point,代表一个有着 x 座标和 y 座标的点
class Point {
public int x = 0; //声明 int 类型变量 x,代表点的水平坐标
public int y = 0; //声明 int 类型变量 y,代表点的垂直坐标
·27·
// 构造器
public Point(int a, int b) {
x = a; //在构造器中初始化实例变量 a 和 b
y = b;
}
}
//创建类 Rectangle,代表一个矩形
class Rectangle {
public int width = 0; //声明 int 类型变量 width,代表矩形的宽
public int height = 0; //声明 int 类型变量 height,代表矩形的高
public Point origin; //声明一个 Point 对象,代表一个点
// 四个构造器
public Rectangle() { //无参构造器
origin = new Point(0, 0); //默认创建坐标为(0,0)的点
}
public Rectangle(Point p) { //带有一个 Point 类型的参数的构造器
origin = p; //使用已知的点 p 初始化矩形的左上角的点
}
public Rectangle(int w, int h) { //带有两个整型参数的的构造器
origin = new Point(0, 0); //初始化矩形左上角坐标为(0,0)
width = w; //初始化矩形的宽
height = h; //初始化矩形的高
}
public Rectangle(Point p, int w, int h) { //带有三个参数的构造器
origin = p; //使用已知的 Point 对象初始化矩形左上角坐标
width = w; //使用参数 w 初始化矩形的宽
height = h; //使用参数 h 初始化矩形的高
}
// 移动矩形的方法
public void move(int x, int y) {
origin.x = x; //将矩形左上角点的 x 坐标改变为新的值
origin.y = y; //将矩形左上角点的 y 坐标改变为新的值
}
// 计算矩形面积的方法
public int getArea() {
return width * height;
}
}
//主类,含有 main()方法
public class CreateObjectExample {
public static void main(String[] args) {
//声明并创建一个座标点对象和两个矩形对象
Point originOne = new Point(23, 94); //创建一个 Point 点对象
Rectangle rectOne = new Rectangle(originOne, 100, 200); //使用已知的点和长宽值创建一个矩形对象
Rectangle rectTwo = new Rectangle(50, 100); //使用默认的坐标和长宽值创建一个矩形对象
28
//显示 rectOne 的宽、高和面积
System.out.println("rectOne 的宽是: " +rectOne.width);
System.out.println("rectOne 的高是: " + rectOne.height);
System.out.println("rectOne 的面积是: " +rectOne.getArea());
//设置 rectTwo 的位置
rectTwo.origin = originOne;
//显示 rectTwo 的位置
System.out.println("rectTwo 的 X 座标是: " +rectTwo.origin.x);
System.out.println("rectTwo 的 Y 座标是: " +rectTwo.origin.y);
//移动 rectTwo 并显示它的新位置
rectTwo.move(40, 72);
System.out.println("rectTwo 的 X 座标是:" +rectTwo.origin.x);
System.out.println("rectTwo 的 Y 座标是:" +rectTwo.origin.y);
}
}
(2)将以上程序保存到文件CreateObjectExample.java 中,使用如下命令编译并运行。
javac CreateObjectExample.java
java CreateObjectExample
(3)程序输出结果如下。
rectOne 的宽是: 100
rectOne 的高是: 200
rectOne 的面积是: 20000
rectTwo 的 X 座标是: 23
rectTwo 的 Y 座标是: 94
rectTwo 的 X 座标是: 40
rectTwo 的 Y 座标是: 72
在上面的这个程序中,创建、操纵、显示了各种对象的信息。在后面的三节中,使用上面的这个例
子来描述在一个程序中对象的生命周期。通过这些内容,可以掌握如何在程序中编写创建和使用对象的
代码,以及当一个对象的生命周期结束以后,系统是如何进行清理的。
6.5.2 创建对象
类是对象的模板,可以从一个类创建一个对象实例,赋给该类型的一个对象变量。下面的语句来自
于上面所讲的程序 CreateObjectExample.java,每一个语句都创建一个对象并将其赋予一个变量。
Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);
第一行创建一个 Point 类的对象,第二行和第三行各自创建一个 Rectangle 类的对象。上述三个语句,
每个语句都有三个部分组成。
声明:赋值符号(=)左边的代码都是变量声明,声明了三个对象类型的变量名。
实例化:关键字 new 用来创建一个新的对象。
初始化:new 运算符后面跟一个构造器的调用,由构造器来初始化新的对象。
·29·
1.声明一个变量指向一个对象
在前面的内容中,学习过如何声明一个变量。声明变量的语法格式如下所示。
数据类型 变量名称;
例如,声明一个整型变量,使用如下代码:
int a ;
这个声明通知编译器,程序中使用一个变量名“a”指向一个数据类型为 int 型的数据。对于原始数
据类型的变量,这样的声明还为该变量分配适当大小的内存。也可以相类似地,声明一个构造类型的变
量。例如:
Point originOne;
如果象上面这样声明 originOne,那么它的值是不确定的,除非真正地创建一个对象并赋给它。简单
地这样声明一个引用变量并不会生成一个对象。要真正地生成对象,需要使用 new 运算符。在使用
originOne 之前,必须给它赋一个对象,否则,就会出现编译错误。
对于当前没有引用对象的变量 originOne 来说, 其在内存中状态如下图所示 (一个变量名 originOne,
再添加一个空指针),它没有指向任何实际的对象:
图 没有引用对象的变量
2.实例化一个类
当使用 new 运算符来实例化一个类时(生成这个类的一个实例对象), 实际上是通过为新的对象分
配内存并返回一个对该内存的引用来实现的。new 运算符还会调用对象的构造器。
new 运算符要求后跟一个对构造器的调用。构造器的名称为要进行实例化的类的名称。new 运算符
返回一个对它所创建的对象的引用。这个引用代表其所创建对象的内存地址,通常赋给一个适当类型的
变量,例如:
Point originOne = new Point(23,86);
由 new 运算符所返回的引用不必一定要赋给变量,它也可以直接在一个表达式中使用。例如下面的
代码中,直接使用 new 运算符创建一个 Rectangle 对象,并直接调用该对象的 height 字段。不过这样一
来,创建的对象不可以重复使用,因为没有保存对它的引用:
int height = new Rectangle( ).height;
3.初始化一个对象
在本节的开头,讲到了一个 Point 类,它有两个属性 x 和 y。这两个属性分别代表点的水平坐标和
垂直坐标。Point 类的定义如下。
public class Point {
public int x = 0; //代表点的水平横坐标
public int y = 0; //代表点的垂直纵坐标
//构造器
public Point(int a, int b) { //使用两个参数来初始化 x 和 y 坐标
x = a;
y = b;
}
}
这个类只包括一个单一的构造器。在 Point 类中的构造器需要两个参数。下面的代码提供 23 和 86
作为构造器的实参:
30
Point originOne = new Point(23, 86);
在这行代码中,使用 new 运算符生成一个新的 Point 类的对象,并在内存中为其分配相应的空间,
然后将其引用赋给 Point 类型的变量 originOne 保存,执行结果如图 6.14 所示。
originOne
Point对象
23
94
x
y
图 6.14 对象 originOne 的内存状态
在本节的开头,还讲到了一个 Rectangle 类,代表一个矩形。其定义如下。
public class Rectangle {
public int width = 0; //定义一个成员变量,代表矩形的宽,并赋初值为 0
public int height = 0; //定义一个成员变量,代表矩形的高,并赋初值为 0
public Point origin; //定义一个引用类型的成员变量,代表一个点
//4 个构造器
public Rectangle() {
origin = new Point(0, 0); //默认使用(0,0)来初始化矩形的左上角坐标
}
public Rectangle(Point p) { //带有一个参数的构造器
origin = p; //使用已知的点 p 来初始化矩形的左上角坐标
}
public Rectangle(int w, int h) { //带有两个 int 型参数的构造器
origin = new Point(0, 0); //默认使用(0,0)来初始化矩形的左上角坐标
width = w; //使用参数 w 初始化矩形的宽
height = h; //使用参数 h 初始化矩形的高
}
public Rectangle(Point p, int w, int h) { //带有三个参数的构造器
origin = p; //使用已知的点 p 来初始化矩形的左上角坐标
width = w; //使用参数 w 初始化矩形的宽
height = h; //使用参数 h 初始化矩形的高
}
// 移动矩形的方法
public void move(int x, int y) {
origin.x = x; //将矩形左上角的坐标移动到参数指定的位置
origin.y = y;
}
//计算矩形面积的方法
public int getArea() {
return width * height;
}
}
每个构造器都可以使用原始数据类型和构造类型为矩形的面积和宽度提供初始值。如果一个类有多
·31·
个构造器,这些构造器必须有不同的签名。Java 编译器基于参数的数量和类型来区分不同的构造器。例
如,当 Java 编译器遇到下面的代码,它就知道需要调用 Rectangle 类中带有一个 Point 参数和两个整型
参数的构造器。
Rectangle rectOne = new Rectangle(originOne,100,200);
执行这行代码,将调用相同签名的构造器,将其中的 origin 成员变量初始化为 originOne。此外,构
造器设置 width 字段的值为 100 和 height 字段的值为 200。现在有两个引用指向同一 Point 对象(一个对
象可以有多个引用指向它)。执行结果如下图所示。
originOne
Point对象
23
94
x
y
Rectangle对象
100
200
width
height
origin
图 两个引用指向同一个对象
下面这行代码调用带有两个整型参数的 Rectangle 构造器,这两个参数分别代表要创建的矩形的宽
和高。
Rectangle rectTwo = new Rectangle(50,100);
在上面的这个构造器中,为 width 和 height 字段提供了初始值。如果检查一下构造器中的代码,会
发现它生成了一个新的 Point 对象,该对象的 x 和 y 值都被初始化为 0。
下面这一行代码中使用的 Rectangle 构造器是不带任何参数的构造器,所以它会调用一个无参构造
器。
Rectangle rect = new Rectangle( );
所有的类至少有一个构造器。如果一个类没有显式地声明一个构造器,Java 编译器会自动地提供一
个无参构造器,称为“默认构造器”。这个默认构造器会调用它的父类的无参构造器。如果没有显式的
父类的话,则调用它的隐含的父类 Object 的构造器。如果它的父类没有构造器(Object 类确定有一个),
编译器将会拒绝对该程序进行编译。
6.5.3 使用对象
一旦创建了一个对象以后,就需要在适当的时候使用它。例如需要它的某个字段的值,改变它的某
个字段的值,或者调用它的方法来执行一个动作。
1.引用一个对象的字段
通过对象的字段名称来访问对象的字段。在一个类的内部,可以使用简洁的名称来引用该类的字段。
例如,可以在上一节中讲到的 Rectangle 类中添加一行输出 width 和 height 值的语句。
System.out.println("宽和高分别是: " + width+ ", " + height);
在这里,width 和 height 是简洁的名称。
在对象类的外部的代码,要引用对象中的字段,必须使用一个对象引用或表达式,后面跟一个圆点
(.)运算符,再后面跟一个简洁字段名,例如:
objectReference.fieldName
32
例如,在本节开始的 CreateObjectExample.java 程序中,CreateObjectExample 类的代码位于 Rectangle
类的外部,因此要调用 Rectangle 的对象 rectOne 中的 origin、 width 和 height 字段,在 CreateObjectExample
类中必须分别使用 rectOne.origin、rectOne.width 和 rectOne.height 来引用,如下面代码所示。
System.out.println("矩形 rectOne 的宽度为: " +rectOne.width);
System.out.println("矩形 rectOne 的高度为: " +rectOne.height);
如果在 CreateObjectExample 类中使用简洁的名称来引用 width 和 height,将会导致编译错误。
同一类型的各个对象对相同的实例字段都拥有它们自己的一份拷贝。每一个 Rectangle 对象都有自
己的字段 origin、width 和 height。当通过一个对象的引用访问一个实例字段时,引用的是特定对象的字
段。例如,在 CreateObjectExample.java 程序中,两个对象 rectOne 和 rectTwo 都有各自不同的 origin、
width 和 height 字段。
除了可以通过对象的引用访问对象的字段之外,也可以使用任何能够返回一个对象引用的表达式。
因为 new 运算符返回一个对象的引用,所以可以使用 new 运算符返回的值来访问一个新的对象的字段,
如下面代码所示。
int height = new Rectangle( ).height;
这个语句生成一个新的对象并马上获取它的高度。实际上,这个语句计算一个 Rectangle 对象的默
认高度。注意这条语句执行以后,程序中不再存在一个指向这个生成的对象的引用,因为程序永远没有
在任何地方存储此引用。所以这里生成的对象是不可引用的,它所占用的资源会释放并被 Java 虚拟机所
回收。
2.调用一个对象的方法
还可以使用一个对象的引用来调用该对象的方法。方法调用的语法格式如下。
对象名称.方法名称(参数列表);
或
对象名称.方法名称();
通过一个对象名后面跟一个圆点运算符,再后面跟上方法名称及参数列表(如果没有参数,也要保
留空的圆括号)。
在本节开始的 CreateObjectExample.java 程序中,Rectangle 类有两个方法:getArea( )用来计算矩形
的面积,move( )改变矩形的原点。在CreateObjectExample.java 中调用这两个方法的代码如下:
System.out.println("rectOne 的面积为: " +rectOne.getArea());
…
rectTwo.move(40, 72);
第一行语句调用 rectOne 对象的 getArea( )方法并显示结果。第二行移动 rectTwo 到新的原点。和实
例字段一样,这里的对象名称必须是对一个对象的引用。可以使用变量名,也可以使用任何能够返回一
个对象引用的表达式。因为 new 运算符返回一个对象引用,所以可以使用 new 运算符返回的值来调用
一个新的对象的方法,如下面这行代码所示。
new Rectangle(100, 50).getArea( );
上面语句中的表达式“new Rectangle(100,50)”返回一个指向 Rectangle 对象的引用,并使用圆点运
算符调用新的 Rectangle 对象的的 getArea( )方法来计算它的面积。
有的方法,如 getArea(),调用后会返回一个值。对于有返回值的方法,可以在表达式中直接调用该
方法。可以将返回值赋给一个变量,并使用它来进行判断,或者控制循环。下面这行代码将方法 getArea( )
返回的值赋给变量 areaOfRectangle。
int areaOfRectangle = new Rectangle(100,50).getArea( );
需要记住的是,在一个特定对象上调用其方法,等同于向该对象发送一个消息。在上面这个示例代
码中,被调用 getArea( )方法的对象是由构造器返回的 Rectangle 对象。
·33·
调用方法,就是执行方法体中的代码语句。在程序中,要调用方法,必须指明调用哪个对象的方法,
因为在面向对象的语言中,方法是封装在对象中的。
当调用某个对象的方法时,程序流程就会转向方法定义中的第一条语句,并顺序执行其中的代码,
直到遇到 return 语句或右花括号为止。
【例】类之间消息传递(方法调用)示例。
class Hello{
//定义一个表示问候的成员方法
void sayHello( ){
System.out.println(“Hello,good morning!”);
}
}
public class PassMessageDemo{
public static void main(String[] args){
Hello hello = new Hello( ); //声明一个 Hello 类的实例对象
hello.sayHello( ); //调用对象 hello 的 sayHello 方法
hello.sayHello( ); //调用对象 hello 的 sayHello 方法
hello.sayHello( ); //调用对象 hello 的 sayHello 方法
}
}
程序输出结果如下。
“Hello,good morning!”
“Hello,good morning!”
“Hello,good morning!”
上面的程序中,重复输出相同的问候。可以看出,对于重复性的操作,使用方法将会使代码更加简
洁。
在程序中,定义成员方法 sayHello()时,因其仅仅是简单的输出,不需要返回值,所以返回类型为
void。而且此方法不需要参数,因此参数列表为空,但圆括号不能省略。当调用 sayHello()方法时,只需
要在对象名后面使用圆点运算符 (.)引用其方法名即可。在调用方法时,主程序暂停,转而执行 sayHello()
方法中的代码,至到执行完毕,再转回主程序继续执行后面的语句。
通常在定义类时,为了数据的安全性,一般都是将字段定义为 private 类型的。 在类的定义中,如果
声明了 private(私有的)类型的成员变量(字段),那么就不能从类的外部访问到这些变量的值。但是
有时需要使用这些值,那么当访问这些字段时,就需要通过间接的方法,即添加获取字段值的公共方法
来实现。
【例】具有 get 和 set 方法的 Book 类。
public class Book {
private String bookName;
private int pageSize;
private float bookPrice;
//无参构造器
public Book(){
bookName = "Java 从初学到精通";
pageSize = 652;
34
bookPrice = 68.00f;
}
//有参构造器。构造器的主要作用:初始化成员变量、加载和配置资源
public Book(String _bookName, int _pageSize, float _bookPrice) {
bookName = _bookName; //用参数 bookName 的值初始化字段 bookName
pageSize = _pageSize; //用参数 pageSize 的值初始化字段 pageSize
bookPrice = _bookPrice; //用参数 bookPrice 的值初始化字段 bookPrice
}
//成员方法
public void bookInfo() {
System.out.println("书名:" + bookName+ ",页数:" + pageSize + ",定价:" +bookPrice);
}
//访问器(读写对象的属性值)
public String getBookName() {
return bookName;
}
public int getPageSize() {
return pageSize;
}
public float getBookPrice() {
return bookPrice;
}
public void setBookPrice(float bookPrice) {
this.bookPrice = bookPrice;
}
}
在上面的程序中,因为图书的名称和页数是固定的、不允许用户私自改动的,所以只定义了 get 方
法,即只能获取这两个属性值,但是不能改变它们的值,这样的方法称为“只读方法”。而对于图书的
价格,肯定是允许用户改变的,比如批发用户可以享受一定的折扣等,所以既有 get 方法用来获取当前
价格,也有 set 方法来改变价格(通过传递参数实现),这样的方法称为“读写方法”。
如果要让上述程序运行起来,需要添加一个启动程序类,即含有 main()方法的类,如下面的示例程
序。
【例】使用 Book 类示例。
public class Test{
public static void main(String[] args){
Book book = new Book( ); //创建一个 Book 类的实例对象
book.bookInfo(); //调用该实例对象的 bookInfo()方法
}
}
注意这里将两个类 Book 和 Test 分别保存为两个单独的.java 文件:Book.jar 和 Test.java。然后进行
编译、运行,输出结果如下:
类和对象
最新推荐文章于 2023-05-09 10:55:55 发布