第6章 面向对象编程
(视频讲解:1小时25分钟)
最早的程序开发使用的是结构化程序设计语言,但是随着时间的流逝,软件的规模逐渐庞大,使用结构化语言出现各种弊端,导致无休止的拖延开发周期,产品的质量也不尽如人意。这一切都体现了结构化语言已经不再适合当前的软件开发。现在程序设计者们将另一种开发思想引入程序中,那就是面向对象的开发思想。
面向对象最关键的两个词汇是类与对象,实质上可以将类看作是对象的抽象,它定义了对象所具有的属性与方法,学习Java语言必须要掌握类与对象,这样可以从深层次去理解Java这种面向对象语言的开发理念,因此掌握类与对象是学习Java语言的基础,并且可以使程序员更好、更快捷地掌握Java编程思想与编程方式。本堂课详细地介绍了类的定义和对象使用。
学习摘要:
如何定义类
Java包的使用
成员变量与成员方法
对象的创建与使用
类的静态成员变量和静态方法
类的构造方法
6.1 类的定义
类是用来定义一组对象共同具有的状态和行为的模板。而对象是现实世界中个体或事物的抽象表示,并封装了它们的属性和行为,例如一个员工可以表示为一个对象,它有姓名、性别、年龄等属性,同时也具有吃、喝、走、跑、说话等行为。这些属性和行为都是“人类”所具有的,也就是说类声明了这些共同的特性,对象(即类的实例)在使用之前,必须定义该对象的类,知道了对象的类型,才能够访问它的属性和行为。
在Java语言中,对象的行为被定义成类的方法,对象的属性定义为类成员变量。所以一个类包括对象的属性和行为。它由class关键字声明,其语法格式如下。
其中className是要定义的类的名称,在类体中可以定义多个成员变量和成员方法,在前面的实例中,使用的main()方法就是一种特殊的成员方法,因为它作为程序执行的入口方法,用于执行程序。
6.2 包
视频讲解
一个Java项目会定义多个类,类多了,就会涉及重名的问题,即命名冲突。那么JDK API中提供成千上万具有各种功能的类是如何管理的?Java中提供了一种管理类文件的机制,即是包(package)。
6.2.1 类包简介
Java中每个接口或类都来自不同的包,不论是Java API中的类与接口还是自定义的类与接口都需要隶属某一个包,这个包相当于一个文件夹,包含了一些类和接口的文件。如果没有包的存在,管理程序中的类将是一件非常麻烦的事情,当程序只有一个类组成(就像之前章节的所有实例),则并不会给程序带来什么影响,但是随着程序代码的增多,免不了会出现类同名的问题,而且多个类放在一个文件夹中也不好分类管理。
例如在程序中定义一个用户登录的Login类,而因业务需要,还要定义一个管理员登录的Login类,这两个类所实现的业务完全不同,于是问题就产生了,编译器不会允许存在同名的类文件。解决这类问题的办法是将这两个类放置不同的包中。
6.2.2 定义类的包
在Eclipse中创建包的步骤如下。
(1)在项目的src节点上单击鼠标右键,选择“新建”→“包”命令。
(2)在“新建Java包”对话框中的“名称”文本框中输入新建的包名,例如com.mrsoft,如图6.1所示,单击“完成”按钮。
图6.1 新建Java包对话框
另外,在Eclipse中创建类时,可以同时输入该类分配的包名,向导会自动创建相应的包。
在Java中包被设计与文件系统结构相对应,例如一个包名为com.mrsoft,则该包中的类位于com文件夹中的mrsoft子文件夹中。没有定义包的类会被归纳为缺省包中。在实际开发中,应该为所有类分配包名,这样会养成良好的编程习惯与代码风格。在类中定义包名的语法格式如下。
语法:package包名
参数:包名是要创建的Java包的名称。
在类中指定包名时必须将package语句放置在程序的第一行,它必须是文件中的第一行非注释代码,当使用package关键字为类分配包名之后,包名将会成为类名中的一部分,这时,类的全名即是包名加上类名。例如在使用位于com.animal包中的Dog.java类时,在其他包使用该类的情况下,需要使用例如com.animal.Dog这样的全名。
注意
Java的命名规则要求,包全部使用小写字母。
类名的冲突问题,也许有的读者会产生疑问,如此多的包许也会产生包名冲突现象,这个可能性比较大。为了避免这样的问题,在Java中定义包名时通常使用创建者的Internet域名的反序,由于Internet域名是独一无二的,所以包名自然不会发生冲突问题,而且在团队工作中,往往实现定义好包的结构,才开始程序的开发。
【例6.1】 创建包名为com.mrsoft的Math.java类,在主方法中输出字符串信息。该类并非java.lang包的Math类,实例代码如下。(实例位置:资源包\源码\06\6.01)
运行结果如图6.2所示。
图6.2 在程序中指定包名
说明
包名是类名的一部分,它们组合在一起即是这个类的完整名称,如果在一个类中同时引用了两个同名的类名,那么就要使用该类的完整名称,即指定其包名,就像本实例中那样。
在本实例中,程序的第一行指定包名为com.mrsoft,在com.mrsoft包中定义Math类,而Java也在java.lang包中定义了Math类,这两个类的名称相同,但是它们却没有冲突,因为它们处在不同的包中。
6.2.3 类包的导入
如果其他类中需要使用上面定义的Math类,那么如何告知编译器当前应该使用的是哪一个包中的Math类,是java.lang.Math类还是com.mrsoft.Math类。
java.lang包是java默认导入的包,即不需要任何操作java.lang包中的所有类,都可以直接使用,例如Math.abs()将调用java.lang包的Math类的abs()方法。但是如果使用这种方式是无法调用com.mrsoft.Math类的方法,除非当前类也在com.lang包中。如果在其他包中就必须使用类的全名com.mrsoft.Math,另外,还可以使用Java的import语句导入指定的类包的单个或所有类。其语法格式如下。
语法:import包名.类名;
参数:包名是要导入到当前类的包的名称。类名是要导入的包名中包含的类名。例如导入com.mrsoft包中的Math类的语法如下。
import com.mrsoft.Math;
执行这个语句之后,再次使用Math就不是java.lang包的Math类了,而是导入的com.mrsoft.Math类。
在使用import语句的时候,包名可以使用通配符“*”代表所有类。例如:
import com.mrsoft.*;
这样就在当前类导入了com.mrsoft包中的所有的类。请读者回忆一下,在讲解数组时,使用过Arrays类,它是java.util包的类,相关的实例代码都使用“import java.util.Arrays”语句导入了Arrays类。
注意
当import导入了一个包中所有的类,并不会导入相应子包中的类,如果用到这个包中的子类,需要再次对子包作单独导入。
6.2.4 静态导入
import关键字除了导入包之外,还可以导入静态成员,这是在JDK5.0以上版本提供的新功能“静态导入”,导入静态成员可以使程序员更为方便。import导入静态成员语法格式如下。
语法:import static静态成员
参数:静态成员是某个类的静态成员变量。例如,我们经常使用System类的out静态成员的println()方法,如果能够直接使用out静态成员会更加方便。下面举例演示其实现方法。
【例6.2】 在类中静态导入System类的out静态变量,然后以更简便的方式执行println()方法,实例代码如下。(实例位置:资源包\源码\06\6.02)
运行结果如图6.3所示。
图6.3 使用import关键字导入静态成员
在本实例中使用import static导入了java.lang.System类中的out静态成员变量,这时就可以在程序中直接引用这个静态成员变量,就好像它是在本类中定义的一样。
注意
import static静态导入语句只能导入类的静态成员。
6.3 成员变量与成员方法
视频讲解
定义类之后要在类体中定义各种成员变量和成员方法来表示状态与动作行为,例如名字、年龄以及走、跑、打架、打印、唱歌等。本节将向读者介绍成员变量和成员方法。
6.3.1 成员变量
成员变量是在类体中定义的变量,即全局变量,成员变量用于定义对象的状态。例如Student类有name、age、sex等成员变量分别表示姓名、年龄、性别等状态。
成员变量必须直接定义在类体中,如果定义在其他方法或代码块中,就会成为局部变量,而不是成员变量。
成员变量是给对象使用的,每个对象被创建以后,都会有属于自己的属性,即成员变量。通过修改这些属性,从而改变对象的某个状态。如图6.4所示,不同的实例对象设置了不同的属性。
图6.4 学生类的属性
6.3.2 成员方法
成员方法是对象行为的描述。每个方法都是由复合语句描述相应的行为。定义成员方法的语法格式如下。
修饰符:可以是public、private、protected以及static、final等。修饰符是可选的语法部分。有关权限修饰符将在第9章讲解。
返回值类型:如果方法需要返回值,必须在这里声明方法的返回值类型,可以使基本数据类型(int、short、double、boolean等),也可以是对象类型(例如数组、字符串等)。
方法名:这是必需的方法定义部分,程序将通过该名称调用这个方法。
形参表:这是可选部分,说明方法被调用时,应该向它传递的数据。
形参表由1到多个形式参数构成,当方法有多个形式参数时,参数之间用逗号“,”分隔;如果方法没有参数,可以省略“形参表”部分。例如:
这段代码定义了saveBookInfo()方法实现图书信息的保存,最简单的图书信息也要有书名、出版社、作者等信息,所以该方法的形参表定义了bookName、publishing和writer参数。
说明
成员变量和成员方法统称为类成员。
6.3.3 方法的返回值
一个成员方法的参数可以是对象也可以是基本数据类型的变量,同时成员方法存在有返回值和无返回任何值的情况,例如计算应收款的compute()方法,至少要返回应收款的金额,如图6.5所示。
图6.5 compute()方法返回float值
而print()打印方法可以没有返回值,直接执行打印指令,控制打印机输出,如图6.6所示。当然也可以返回boolean值,反映是否打印完毕的状态。
图6.6 print()方法无返回值
声明方法时必须指定返回值类型,如果一个方法没有返回值,必须使用void关键字作为返回值类型。例如:
如果声明方法时,指定了方法的返回值类型,就必须在方法体中使用return语句返回相应类型的值,使用这个关键字后,方法的执行将被终止。
语法:return返回值;
参数:返回值的类型必须和声明方法时的返回值类型兼容。
例如:
计算金额的方法在声明时,是float类型的返回值,那么return语句就必须返回float兼容类型的数值。学习基本数据类型的自动类型转换后,我们知道低于float级别的基本数据类型会自动转换为float类型。所以如果return语句返回int、short等类型的返回值,会自动转换为float类型,即这些基本数据类型和float类型是兼容的。例如:
这段代码在声明返回值类型为float的compute()方法中,使用return语句返回了char类型的值。根据自动类型转换的原理,float是兼容char类型的,所以这是可行的。但是高于float级别的数据类型必须强制类型转换为float类型,才能作为方法的返回值。例如:
声明的double类型变量sum存储了最后经过计算的金额,如果把它作为方法的返回值,由于高于float类型,所以必须强制类型转换,否则编译时无法通过。
另外,在没有返回值,即返回值类型被声明为void的方法中,也可以使用不带返回值的return语句,它可以结束该方法的执行。例如:
上述方法定义了pageNum参数作为打印的页数,方法体中判断这个打印页数是否大于10页,如果大于,就调用空的return语句返回到方法的调用位置,即结束该方法。否则执行打印操作。
注意
如果方法声明了返回值类型,那么return语句就必须指定返回值,而不能使用空的return语句,只有方法的返回值类型为void类型,即没有返回值的方法,才能使用空的return语句作为方法结束语句。
6.4 对象的创建与使用
视频讲解
在学习了类的定义、成员变量与成员方法之后,必须创建该类的实例对象,才能够访问成员变量与成员方法。本节将介绍创建对象、访问成员变量和方法以及销毁对象。
6.4.1 创建类的对象
一个汽车的设计图纸永远不能用来驾驶出行,你必须用它生产出真正的汽车才能使用。这个汽车的设计图纸可以比作我们之前设计的类,而真正的汽车(例如红色轿车)可以比作是对象,只有对象才能被程序使用,而且同一类型的对象还有不同的属性,例如颜色、高度、速度等,另外,对象还可以执行类设计的方法,即对象的行为,例如开车、换挡、加油、转向和刹车等。
创建类的实例对象需要使用new语句,声明并创建对象的语法格式如下。
语法:类名 对象名=new类构造方法();
参数:类名是要创建实例对象的类的名称,对象名可以看作是一个变量,该变量名就是创建的实例对象的名称,构造方法是类创建对象时必须执行的方法,用于构造一个新的对象并初始化对象属性。
例如:
String name = new String("一个新的字符串对象");
这句代码声明了字符串变量name,并且使用new语句创建了字符串对象,String是一个特殊的类,它可以使用英文格式的双引号创建对象,也可以使用new语句创建,但是我们自己写的类,只能使用new语句创建。
6.4.2 访问对象的属性
创建了对象以后,可以访问对象的属性,即成员变量。访问对象属性需要使用“.”操作符。
语法:对象名.属性
在对象名后面使用“.”操作符连接对象的属性,即类的成员变量,就可以引用相应的属性值,或者为某个属性赋值。例如:
String carColor = whiteCar.color; whiteCar.color = "白色";
第一条语句把whiteCar对象的颜色赋值给了carColor变量,第二条语句把whiteCar对象的color颜色属性赋值为“白色”。
6.4.3 执行对象的行为
对象的行为即类的成员方法,我们通常说调用或执行对象的某个方法。它的语法格式和访问对象的属性类似。
语法:对象名.成员方法名();
调用成员方法时,如果该方法定义了参数,就必须在括号内提供相同类型的参数值,而且顺序要和定义该方法参数时的顺序相同。例如:
上面代码分别调用了gear()方法和drive()方法,并且gear()方法还传递了int类型的参数值5。该方法会根据这个参数值做相应的业务处理。
【例6.3】 定义一个Car类作为汽车类,并为它声明颜色、速度、挡位等成员变量,还有开车、换挡等成员方法。然后创建该类的实例对象,并修改颜色属性、执行换挡与开车方法。(实例位置:资源包\源码\06\6.03)
(1)首先创建Car类,注意这个类不是主类,所以使用Eclipse开发工具创建时,不要选择创建main()方法,该类的关键代码如下。
(2)编写该实例的主类Program。在使用Eclipse创建该类的向导中,选择创建main()方法,因为我们要通过它执行程序,调用Car类做实例演示,该类的关键代码如下。
运行结果如图6.7所示。
图6.7 使用import关键字导入静态成员
6.4.4 对象的销毁
大多数面向对象的编程语言,要求显示地销毁某个对象,这点就没有Java做得好,Java语言提供了垃圾回收机制,对于不再使用的对象会自动销毁,也可以在程序中显示的为某个对象赋值null值,使对象不再被使用。例如:
whiteCar = null;
垃圾回收器会找到并销毁它,释放该对象所占用的资源。
6.5 类成员与类方法
视频讲解
成员变量与成员方法是每个对象的属性与行为的抽象,如果把它们定义成静态的,就会在内存中为它们分配固定的内存空间,同时它们也会成为类的成员,而非对象的成员,本节将向读者介绍类变量与类方法。
6.5.1 类变量
类变量也称为静态成员变量,它的语法在成员变量的基础上添加了static关键字,使成员变量成为静态的。静态的成员变量不是分配给每个实例变量的,而是属于类的变量,它在内存中是唯一的,可以直接使用“类名.成员变量”的格式去访问;它在内存中的位置是固定的,是该类的所有实例对象所共享的存储单元。
例如:中国人是一个类,其中country属性代表国家,把这个属性声明为静态的成员变量,并赋值为“中国”,它就是静态成员变量,即类的变量。
所有该类的实例都会共享该变量,并且只占用一份内存空间,如图6.8所示。
图6.8 类的变量
如果修改了类变量country的值,例如:
Chinese.country = "中华人民共和国";
那么该类所有实例对象的country属性都会改变。如果使用成员变量,那么每个中国人的实例对象都会分配一个country属性,有多少个中国人的实例对象,就会创建多少份country属性的内存空间。
注意
声明为静态的变量必须是成员变量,而不能是局部变量,在方法体内或任何代码块中声明的变量不能使用static关键字。
6.5.2 类方法
成员变量与成员方法必须创建对象,通过对象才能访问。但是,有时候也希望能够不必创建对象,直接调用类的某个方法,即类方法。
类方法由static关键字定义,它也是静态成员方法。例如:
静态成员方法与静态成员变量一样,都可以使用类名直接访问,同时也可以使用类的实例对象进行访问。
【例6.4】 定义一个Car类作为汽车类,并为它声明产地、使用年限等成员变量,还有成员方法printYieldInfo(),它用于输出本款轿车的出厂信息。然后创建该类的3个实例对象,并在修改产地静态成员变量前后,分别输出每个实例对象的产地信息。(实例位置:资源包\源码\06\6.04)
(1)首先创建Car类,注意这个类不是主类,所以使用Eclipse开发工具创建时,不要选择创建main()方法,该类的关键代码如下。
(2)编写该实例的主类Program。在使用Eclipse创建该类的向导中,选择创建main()方法,因为我们要通过它执行程序,调用Car类做实例演示,该类的关键代码如下。
运行结果如图6.9所示。
图6.9 使用import关键字导入静态成员
注意
静态成员属于类的成员,在使用方式上应该使用类名做前缀访问静态成员,而不是使用对象名。上面的实例为了演示静态成员才使用了对象名,在Eclipse的Java编辑器中会提示警告信息。另外,静态成员变量和静态成员方法不能够被非静态方法访问,反之,非静态方法可以访问静态成员。
说明
现在读者应该明白main()主方法为什么要声明为静态了,它必须能被Java虚拟机直接访问,而不用创建对象。
6.6 实战
视频讲解
6.6.1 用静态代码块初始化
定义StaticCode类,在类中定义静态成员变量count,该变量是对象计数器用于记录该类创建了多少个对象。另外,在该类中定义静态代码块,初始化count变量的同时输出提示信息。再定义一个StaticDemo类,该类的静态代码块输出提示信息,然后在主方法中创建3个StaticCode类的实例对象,在控制台查看StaticCode类的静态代码块被执行几次,运行结果如图6.10所示。(实例位置:资源包\源码\06\实战\01)
图6.10 实例运行结果
6.6.2 方法参数传递
在定义方法的时候,可以为方法指定参数列表,方法根据参数列表与方法的调用者进行数据交互,参数列表中的参数类型可以是int、short、double等基本数据类型也可以是对象类型。但是这两种参数类型的值传递方式并不一样:如果传递的参数是基本数据类型,该方法会直接获得指定类型的数值;如果传递的参数是对象类型,该方法获得的只是一个对象地址的引用,通过这个引用可以访问和操作对象属性与行为,但是对象的实体并没有传递给该方法,它还在内存的某个区域,这种情况下,当方法改变对象的属性,会直接改变对象实体的数据,而基本类型的数据是值传递的,改变它不会影响方法调用者传递的变量。下面以实例演示方法传递对象参数和传递基本数据类型参数,运行结果如图6.11所示。(实例位置:资源包\源码\06\实战\02)
图6.11 实例运行结果
6.6.3 整数进制转换器
由于计算机的特殊结构,其内部使用二进制数据。为了方便记忆与阅读,又定义了八进制和十六进制格式来表示数据。一个八进制数位可以表示3位二进制数,一个十六进制数位可以表示4位二进制数。而对于普通人而言,使用十进制更加容易阅读。本实例将通过包装类的Integer类实现一个简单的进制转换器,完成十进制到其他不同进制的转换,运行结果如图6.12所示。(实例位置:资源包\源码\06\实战\03)
6.6.4 获取数字的取值范围
Java是一种强类型语言,每当定义变量时,都需要先指明其类型。对于数字基本类型而言,其取值范围有限制。例如byte类型的范围是−128~127,如果将128赋值给一个byte类型的数据就会报错。本实例将通过各种数据的包装类获取指定类型数据的最大值和最小值,并输出到控制台中,运行结果如图6.13所示。(实例位置:资源包\源码\06\实战\04)
图6.12 实例运行效果
图6.13 各种数据类型的取值范围
6.6.5 构造方法初始化员工对象
Java程序的各种功能是通过对象调用相关方法完成的,因此必须先获得对象,并完成对象的初始化。使用构造方法来获得对象是一种很常用的方式,构造方法也支持重载,本实例将演示使用不同的构造方法来获得对象,运行结果如图6.14所示。(实例位置:资源包\源码\06\实战\05)
图6.14 实例运行效果