3.2 类和对象
Java语言是利用面向对象程序设计的思想进行编程的计算机高级语言,面向对象的程序设计方法把程序看成由若干类对象组成的,而每个对象又包含自己的数据字段和操作方法。具有同样属性的对象进行抽象得到Java类,将每个Java类进行实例化得到具体的Java对象。
3.2.1 类
在Java语言程序设计中,类是创建对象的模板,是面向对象程序设计的基本单元,所有对象抽象的数据字段和操作方法都是封装在类中,通过设计类来实现封装、继承和多态。Java语言中的类定义由类声明和类体两部分组成,它的一般书写格式为:
例如:
说明:
(1)[…]表示为可选项。关键字class是类定义的开始,类名应该符合Java标识符命名规则,类名的第一个字母通常要大写。与文件名的命名规则类似,类名通常与所封装的内容有联系。
(2)类修饰符是指类的访问控制符和类型说明符,主要有缺省、public、abstract和final等4种。缺省修饰符(也称friendly)是指没有类修饰符,例如:class MyClass{}。public修饰符是指该类为公共类,例如:public class MyClass{}。Java语言规定包含main()方法的类必须声明为public类。abstract修饰符是指该类为抽象类,不能被实例化为对象,例如:abstract class MyClass{}。final修饰符是指该类为最终类,不能被定义为超类,例如:final class MyClass{}。
(3)extends子句是指该类的直接超类,例如:class MyClass extends MySuperClass{}。如果没有该子句,则Java编译器默认将Object类作为该类的直接超类。
(4)implements子句是指该类实现的一个或多个接口,例如:class Myclass implements MyInterface{}。
(5)类体是类封装的核心部分,用一对英文半角的花括号将所有代码括起来,主要包括类数据成员、类成员方法和构造方法等。类数据成员是类成员方法操作的基础,是类功能实现的存储部分。类成员方法是类的功能实现过程,类的所有语句(包括方法的调用语句)都必须书写在类成员方法体中。构造方法是对象初始化的实现方法,对象初始化时的所有语句都必须书写在构造方法体中。
1.类的数据成员
一旦封装了一个类,就必须要在该类中定义相应的数据成员,Java语言中的数据成员又称字段(Field)。Java语言类体中的数据成员包括成员变量(Member Variable)和常量(Constant)两种。
常量是程序运行时不能改变的量,它在定义时必须同时进行初始化,在整个类体范围内有效。具体见2.1.3节中的自定义常量部分。
成员变量是指在类中定义的变量,包括成员实例变量(Instance Variable)和成员类变量(Static Variable)两种。
1)实例变量
实例变量是指在类中定义,属于每一个被实例化对象的变量,它的一般书写格式为:
例如:
说明:
(1)实例变量可以是Java语言的8个基本数据类型,也可以是Java语言的引用数据类型。
(2)实例变量名必须是Java语言合法的标识符,通常与表示的内容相关,如果是拉丁字母,第一个字母一般要小写。
(3)实例变量通常在赋值后才可以使用。在定义时可以先声明,在后面的语句中赋值。也可以在声明的同时初始化。
(4)变量修饰符是指变量的访问控制符和类型说明符,主要有缺省、public、private、protected、static、final、transient和volatile等。按照面向对象程序设计的要求,建议成员实例变量使用private修饰,以提高程序的内聚程度。
2)类变量
类变量是指在类中定义,属于同类的所有对象共有的变量,它的一般书写格式为:
例如:
说明:
(1)建议修饰符使用public。
(2)其他说明与实例变量相同。
例题3.1 封装了一个只具有数据成员的类,该类除了保存数据之外没有别的用处,要想让其发挥作用,必须扩充相应的操作方法。
2.类的成员方法
类的封装中仅仅有数据成员是不能满足程序设计要求的,大量的操作都是封装在类的功能里。许多程序设计语言用函数(Function)来描述功能,Java语言使用成员方法(Member Method)来表示某一类的操作功能。Java语言的成员方法决定了一个对象能够接收什么样的消息,方法的基本组成部分包括返回类型、方法名、参数和方法体等。Java语言成员方法包括成员实例方法(Instance Method)和成员类方法(Static Method)两种。
1)成员实例方法
成员实例方法是指在类中定义,属于每一个被实例化对象的方法,它的一般书写格式为:
例如:
说明:
(1)修饰符是指该方法的访问控制符和类型说明符,主要有缺省、public、private、protected、static、abstract、final、native和synchronized等。按照面向对象程序设计的要求,建议成员实例方法使用public修饰,以提供对象间消息传递的接口。
(2)返回类型是指该方法执行结束后会返回一个此类型的值,返回类型可以是Java语言的基本数据类型,也可以是Java语言的引用数据类型。返回的值必须由方法体中的return语句显式标出,如果返回类型为void,表示返回值为null,即不返回任何值,此时return语句可以缺省。
(3)方法名必须是Java语言合法的标识符,通常与表示的内容相关,如果是拉丁字母,第一个字母一般要小写。
(4)形式参数列表是指该方法所需要的参数,可以有0个或多个,如果有多个参数,它们之间用英文半角的逗号隔开,形式参数一定要写在方法名后的小括号内,无论有没有参数,方法名后的一对小括号是不可缺少的,它是判断方法是否合法的关键。
(5)throws异常列表是指该方法在被调用时可能会产生的异常类型,需要编写程序时提前指明。
(6)方法体是方法功能实现的核心,它用一对英文半角的花括号将属于该方法的所有语句括起来,在方法体的最后通常是return语句,用来表示方法执行结束后返回程序调用处,继续执行程序后面的代码。
2)成员类方法
成员类方法是指在类中定义,属于同类的所有对象共有的方法,它的一般书写格式为:
例如:
说明:
(1)建议修饰符使用public。
(2)其他说明与实例方法相同。
无论是类的实例方法还是类方法,都需要操作类的成员数据,即类的常量、实例变量和类变量。顾名思义,类的常量、实例变量和类变量的作用范围在整个类内有效,而在方法体或方法参数中定义的变量称为局部变量(Local Variable),局部变量的作用范围只在定义它的方法内有效。例如:
如果局部变量的名字与成员变量的名字相同,在局部变量的方法体内,成员变量将被隐藏,即这个成员变量在这个方法内暂时失效。例如:
如果局部变量的名字与成员变量的名字相同,在局部变量的方法体内,成员变量将被隐藏。此时想在方法体内继续使用该成员变量,就必须用到Java语言的关键字this。例如:
Java语言中的this是指一个对象,它表示引用对象自己。当一个对象创建好后,Java编译器就会给它分配一个引用自身的this标记。在使用对象的成员变量和成员方法时,如果没有指定相应的对象引用,则默认使用的就是this引用。this关键字可以出现在实例方法和构造方法中,但不可以用在类方法中。
说明:
(1)在类的构造方法中,通过this语句可以调用这个类的另一个构造方法,具体见3.2.1节中的构造方法部分。
(2)在类的实例方法中,如果局部变量或形参变量与成员变量同名,此时成员变量被隐藏,此时想在方法体内继续使用该成员变量,就要使用“this.成员变量名”(如this.aint)来显式指明该成员变量。
(3)在一个方法调用中,可以使用this将当前实例对象的引用作为实际参数进行传递,例如:my.callMethod(this)。
例题3.2 封装了一类具有成员变量和成员方法的程序。
3.方法的重载
在面向对象程序设计中,通过合理的方法名可以大体知道该方法的操作过程,所以编程时经常会遇到同名的不同方法共存的情况。这些方法同名的原因是它们的最终功能和目的都相同,但是由于在完成同一功能时可能遇到不同的具体情况,因此需要定义不同的具体内容的方法,来代表多种具体实现形式,这种形式就是面向对象程序设计中的多态。例如:许多平面图形都有求面积的方法,此时方法名应该都取getArea(),但不同的图形,其getArear()方法体的内容是不同的。Java语言主要提供方法的重载和方法的重写两种多态实现方式。方法的重写具体见3.3节。
当在同一类中定义了多个方法名相同而参数不同的方法时,称为方法的重载(Overload)。重载的方法主要通过形式参数列表中参数的个数、参数的数据类型和参数的顺序等方面的不同来区分,Java编译器通过检查每个方法所用的参数个数和类型来调用正确的方法。
例题3.3 封装了一类重载的方法程序。
4.构造方法
Java语言基本数据类型的变量必须初始化后才可以使用,类似地,Java类也必须初始化成对象后才能够体现面向对象技术的特点。Java语言规定,对象的初始化必须由构造方法完成。构造方法(Constructor)是Java语言封装类中的一种特殊方法,它用来定义对象的初始状态,由方法名、参数和方法体等组成。构造方法的方法名必须与其封装的类名相同,它没有返回类型,不能被对象直接调用,只能通过关键字new引用。它的一般书写格式为:
例如:
说明:
(1)构造方法名必须与类名相同,所以成员方法首字母小写的规则对其不适用。
(2)构造方法没有返回类型。
(3)构造方法不能被对象调用,只能对对象进行初始化。
(4)如果程序中没有显式地编写构造方法,Java编译器会自动生成一个空的,没有方法体的构造方法。
(5)如果程序中编写了一个或多个构造方法,Java编译器就不会自动生成一个空的、没有方法体的构造方法,在初始化对象时就必须使用被编写的构造方法。
(6)程序可以重载多个构造方法,初始化对象时按参数决定使用哪个构造方法。
(7)在一个类中,如果一个构造方法中调用另一个构造方法,就必须显式地使用this关键字,并且this语句必须放在构造方法的第一条语句位置。例如:this();。
例题3.4 封装了一类构造方法的程序。
3.2.2 对象
在Java语言中,对象是由类创建的,一个对象就是某个类的一个实例。对类实例化,可以创建多个对象,通过这些对象之间的消息传递进行交互,就可完成复杂的程序功能。对象在计算机内存中的生命周期可分为对象的创建、使用和清除。
1.对象的创建
对象是一组相关变量、常量和相关方法的封装体,是类的一个实例。对象体现了对象的标识、状态和行为。对象的创建包括声明、实例化和初始化三部分,它的一般书写格式为:
例题3.5 封装了一类对象的程序。
说明:
(1)“类型对象名;”是指对象的声明,类型是类的引用类型,对象名必须是Java语言合法的标识符,通常与表示的内容相关,如果是拉丁字母,第一个字母一般要小写。声明对象后,该对象就在内存中还没有任何数据,它还不能被使用。例如:ARectangle myRectangle;。
(2)关键字new是指对象的实例化,它的作用是实例化一个对象,给对象分配内存,它的后面只能跟构造方法,返回对象的引用地址。例如:myRectangle=new ARectangle();,程序执行该语句时,就会为width、height、x、y这四个变量分配内存,然后执行构造方法中的语句,为width和height赋值为0。
(3)构造方法是指对象的初始化,它可以进行重载,按不同类型和个数的参数调用不同的构造方法,从而初始化不同的对象。例如,Rectangle类中重载了三个构造方法:
在使用new实例化对象后,可以得到3个不同的实例化对象:
这些对象将被分配在不同的内存空间,因此改变其中一个对象的状态不会影响其他对象的状态。它们的引用内存如图3-4所示。
如果在程序中出现一条语句:
这条语句表示把yourRectangle的引用赋值给myRectangle,此时yourRectangle和myRectangle本质上是一样的,虽然在源代码中这是两个名字,但对Java系统来说它们的引用是一个:0x1211,系统将释放分配给myRectangle的内存引用。它们的引用内存如图3-5所示。
图3-4 同类的多个对象内存引用
图3-5 同一内存引用多个对象
2.对象的使用
对象的使用原则是先创建后使用。对象的使用包括引用数据成员和调用成员方法,对象作为类的成员和对象作为方法参数(或返回值)等。
Java语言通过引用操作符“.”实现对数据成员和成员方法的引用,它的一般书写格式为:
例如:
数据成员包括常量、实例变量和类变量等,所以在对象引用数据成员时就会有所区别。前面提到,一个类通过使用关键字new可以创建多个不同对象,这些对象被分配不同的内存空间。换句话说,不同对象的实例变量将分别属于相应的内存空间,这些实例变量通过“对象名.实例变量名”被引用。例如:如果number是实例变量,则aDog.number=20;是合法的。
类变量由static修饰符修饰,它由同类的所有对象共有,即同类的所有对象的这个类变量都分配相同的一块内存空间,改变其中一个对象的这个类变量会影响其他对象的这个类变量。因此,类变量可以通过“对象名.类变量名”被引用,也可以通过“类名.类变量名”被引用。例如:如果number是类Dog的类变量,则存在Dog的一个对象aDog,aDog.number=20;是合法的,同时Dog.number=20;也是合法的。
例题3.6 封装了实例变量和类变量被引用的程序。
类的成员方法包括实例方法和类方法,所以在对象引用成员方法时就会有所区别。实例方法通过“对象名.实例方法名”被引用,当某个对象调用实例方法时,该实例方法中出现的数据成员是分配给该对象的数据成员,包括类变量,所以,实例方法既可以操作实例变量,也可以操作类变量,还可以操作其他实例方法和类方法。而类方法由static修饰符修饰,类方法可以通过“对象名.类方法名”被调用,也可以通过“类名.类方法名”被调用。由于类方法中出现的数据成员必须是所有对象共有的类变量,所以类方法中只能操作类变量,不能操作实例变量,而且类方法只能操作类方法,不能操作实例方法。
例题3.7 封装了实例方法和类方法被引用的程序。
无论是实例方法还是类方法,在定义时按照要求可以有形式参数也可以没有形式参数。当被对象或类引用时,如果没有形式参数则不用传递实际参数,但小括号不能省略;如果有形式参数则必须传递实际参数,实际参数与形式参数的个数应相等,类型应匹配,实际参数与形式参数按顺序对应,一对一地传递数据。在Java语言中,方法的所有参数都是按值传递的。对于基本数据类型的形式参数,它所接收到的值是传递的实际参数值的副本,方法改变形式参数的值,不会影响实际参数的值。对于引用数据类型的形式参数,它所接收到的值是传递的实际参数的引用值,而不是实际参数的内容,方法改变形式参数的引用值,就会影响到实际参数的引用值。
Java SE的JDK 5.0版本以后增加了针对方法参数的一项新功能,称为可变参数。可变参数是指在定义方法时不用明确参数的名字和个数,但这些参数的类型必须相同。对于类型相同的参数,如果参数的个数需要灵活的变化,使用可变参数可以使方法的调用更加方便。它的一般书写格式为:
如果形式参数列表中存在可变参数,使用“…”表示不确定的参数个数。如果有多个形式参数,可变参数必须是最后一个形式参数。例如:
例题3.8 封装了可变参数的程序。
将一个对象声明为类的成员时,在使用前必须对该对象实例化和初始化,即分配内存,它的使用方式类似于普通的成员变量。
在方法中使用对象作为参数时,采用引用值被调用。
例题3.9 封装了对象的程序。
3.对象的清除
当一个对象不再为程序使用时,应该将它释放并回收内存空间以供其他新对象使用。为了清除对象,有的面向对象程序设计语言要求单独编写程序以回收其内存空间。Java语言具有自动回收垃圾功能,它会周期性地回收一些长期不用的对象占用的内存,从而不用单独编写回收垃圾内存的程序。
Java系统对闲置内存采用自动垃圾回收机制可以在程序设计时不必跟踪每个对象,不必过多考虑内存管理工作,同时也避免了在管理内存时由于错误的操作而造成系统的崩溃。
3.2.3 包
包(Package)是组织一组相关类和接口的名称空间。从概念上说,包类似于计算机中的目录或文件夹。例如:图片存放在一个文件夹中,应用程序存放在另一个文件夹中,HTML页面文件又存放在另一个文件夹中。因为Java语言编程实现的软件可能由成百上千个独立的类构成,把相关的类和接口存放在包中,非常有利于组织这些内容。
Java语言将每个类生成一个字节码文件,该文件名与类名相同,不同的Java源文件中有可能出现名字相同的类,如果想区分这些类,就需要使用包名。当不同Java源文件中两个类名字相同时,它们可以通过标识隶属于不同的包来相互区分。
1.包的创建
Java语言使用关键字package声明包语句,它的一般书写格式为:
说明:
(1)package语句必须作为Java源文件的第一条有效语句,为该源文件中定义的类指定包名。
(2)包名必须是Java语言合法的标识符,包名的所有字母通常全部小写。如果包与包之间有嵌套关系,父包与子包之间用英文半角的“.”分隔。例如:
创建包名的目的是用来有效地区分名字相同的类,但总会出现包名也重复的情况。为了避免这样的问题,许多公司建议使用全球唯一的域名逆序书写作为包名,从而减少包名和类名同时重名的极端情况。如将域名www.mysite.com.cn的逆序cn.com.mysite作为包名。
(3)如果用package语句指定一个包,则包的层次结构必须与源文件目录的层次结构相同。即Java源文件必须存放在包名指定的目录位置,否则Java虚拟机将无法加载这样的类。
(4)如果源程序中省略了package语句,Java编译器会自动把这个类存放在缺省的无名包中,这个无名包对应的是源程序的当前工作目录。
例题3.10 封装了带有包的类程序,它们对应的目录层次如图3-6所示。
图3-6 包的对应层次目录
有了包和类的创建规则,结合类定义中的访问控制修饰符,就可以确定类中数据成员和成员方法的访问权限了。前面已经提到,Java语言的访问控制修饰符包括public、protected、缺省和private,当修饰类和类中的成员定义时,访问权限如表3-1所示。
表3-1 访问控制修饰符
说明:部分允许是指当子类访问被保护变量时,只有子类同其超类在同一个包中时才可以访问超类对象中的被保护变量。同样,子类对超类中的缺省修饰变量的访问也受到这种限制,它们必须位于同一个包内。
2.包的导入
一个类可能需要另一个类的对象作为自己的成员或方法中的参数,如果这两个类在同一个包中,直接使用是没有问题的。例如:所有的类都在缺省的无名包中,它们存放在相同的目录下,就相当于在同一个包中。对于包名相同的类,它们必然按照包名的结构存放在相应的目录下。如果一个类想要使用的那个类和它不在同一个包中,就必须显式地导入才可以使用,此时要用到关键字import,它的一般书写格式为:
说明:
(1)import语句的功能是导入包或包中的类,但它并不将包实际读入,它只是指引编译器可以到指定的包中去寻找类、方法等,可以直接导入某一个具体的类,例如:
也可以导入某个包中的全部类,例如:
*号方式可能会增加编译时间,尤其是导入一个大包时,不过,*号方式对运行时间的性能或生成的类文件大小是没有影响的。
(2)根据实际需要,import语句可以有多条,它们必须放在程序的package语句之后,所有其他语句之前。
(3)在编写源文件时,除了自己编写类之外,经常需要使用Java语言提供的类,这些类都存放在不同的包中。有效使用Java语言提供的包可以节省程序设计时间,避免一切从头开始,Java语言常用的包有:
java.lang包:包含所有的基本编程语言类。
javax.swing包:包含一组容易使用的图形用户界面GUI类。
java.io包:包含所有的输入输出类。
java.util包:包含常用的使用工具类。
java.sql包:包含有关数据库操作的类。
java.net包:包含网络操作的类。
java.applet包:包含实现Java Applet的类。
这些常用包中的类仅仅是Java语言提供的一部分,其中java.lang包是Java语言的核心类库,Java编译器会自动将这个包中的所有类导入当前程序源文件,不需要使用import语句导入。也就是说,即使不写import java.lang.*;语句,java.lang包中的所有类也会缺省导入,这些类可以直接使用,编译器也不会报错。例如:System.out.println();语句中的System类就是java.lang包中的类,直接使用是完全正确的。
(4)import语句在导入包时,不满足嵌套关系。例如:java.util包还存在很多子包,java.util.regex包只是其中的一个。例如:
如果想导入java.util.regex包中的类,还必须再增加一条语句:
例题3.11 封装了导入Java语言提供的包类程序。
例题3.12 利用包封装类的程序。
3.Java类库
Java SE、Java ME和Java EE平台分别提供了数量庞大的类库,也就是包的集合,用来组织应用程序的开发,这种库称为应用程序编程接口(Application Programming Interface)。Java API包含了和通用编程相关的最常见的封装类和接口,可以选择的类和接口有成百上千个。有了API,使用Java语言编程时,就可以把精力集中于特定的应用程序设计,而不必在基础语言上浪费时间。Java SE平台的API规范(Java SE Platform API Specification)包含了Java SE平台的所有包、类、接口及其数据字段和方法的详细清单,并且随着Java JDK版本的更新相应进行更新,它以Web页的方式分发出来,在浏览器中加载此页面即可使用,已经成为Java SE平台编程的重要参考文档。