JAVA编程讲义.面向对象编程
面向对象是一种现在最为流行的程序设计思想,它把现实中的事物抽象为程序设计中的对象,又进一步把同类对象抽象成类,力图使计算机语言对事物的表述与现实世界中该事物的本来面目保持一致,极大地提高了代码的复用性,让复杂的项目开发变得更加简单。Java是纯粹的面向对象的语言,只有深刻理解面向对象的开发理念,才能更好、更快地掌握Java编程技能。
5.1 面向对象概述
面向对象技术最早是在编程语言Simula中提出的。1967年5月20日,在挪威奥斯陆郊外的小镇莉沙布举行的IFIP TC-2 工作会议上,挪威科学家Ole-Johan Dahl和Kristen Nygaard正式发布了Simula 67语言。Simula 67被认为是最早的面向对象程序设计语言,是面向对象的开山祖师,它引入了所有后来面向对象程序设计语言所遵循的基础概念:对象、类、继承,但它的实现并不是很完整。
Smalltalk是第一个完整实现了面向对象技术的语言,由艾伦·凯于20世纪70年代初提出。Smalltalk引领了面向对象的设计思想的思潮,对其它众多的程序设计语言的产生起到了极大的推动作用。C++,C#,Objective-C,Actor,Java和Ruby等,无一不受到Smalltalk的影响,这些程序语言中也随处可见Smalltalk的影子。
Java是目前使用最广的面向对象编程语言,拥有全球最多的开发者,常年稳居开发语言排行榜第一名。是简单、面向对象、分布式、解释性、健壮、安全、跨平台、可移植、高性能、多线程、动态的高级程序设计语言。
面向对象编程更加模块化,更加易于构建大型项目,而且有利于更新和维护, 极大程度地简化了企业级编程的协同问题。面向对象的优点是项目可以做的更大、模块之间解耦、调用更简单、易于修改和维护、适合大型项目开发。然而,面向对象开发方法需要一定的软件作为支持,在大型项目的开发过程中,如果管理不好,极容易造成系统结构不合理、各部分关系失调等问题。
面向对象的编程思想是一种符合人类思维习惯的编程思想,它把要解决的问题按照一定的规则分为多个对象,然后通过调用对象的方法来解决问题,其主要特点可以概括为封装、继承、多态,下面针对这3个特点进行介绍。
1.封装
封装是面向对象编程的核心思想,具体是指把属于同一类对象的属性和行为封装起来,对外界隐藏其实现细节。例如,用户玩游戏时,只需要点击游戏人物就可以实现一些操作,无须知道游戏内部是如何工作的;你想知道朋友那边的天气如何,假设他或她在异国他乡,你只需要打开手机查看他或她所在地区的天气数据就可以,不用关注天气数据是如何被推送到你的手机上的。上述两个实例中,游戏人物的的属性和行为的实现细节被封装了来,天气数据推送服务的实现细节也被封装了起来,这就是封装思想的具体应用。
2.继承
继承主要是指子类与父类之间在属性、行为等方面的某些传承关系,通过继承,每一个子类都可以从它的父类那儿继承所有的通用属性和方法,从而只需定义其独一无二的属性和行为即可,无需编写相同的代码,便能开发出新类,很好地实现了代码的重用,极大地降低了重复的代码量,能够大大缩短开发周期,降低开发费用。例如,牧羊犬类属于狗类,狗类又属于哺乳动物类,哺乳动物类又属于动物类。如果不使用层级的概念,就不得不分别定义牧羊犬类、狗类、哺乳类、动物类的所有属性和行为,编写重复性的代码。在使用了继承后,可以先定义动物类,其中包含动物共有的属性和行为;接着定义哺乳动物类,使其继承动物类的基础上新增哺乳动物特有的属性和行为;再定义狗类,使其继承哺乳动物类的基础上新增狗特有的属性和行为;最后定义牧羊犬类,使其在继承狗类的基础上新增牧羊犬特有的属性和行为,这样就可以极大程度地减少代码编写量。
3.多态
多态是面向对象编程的又一特性,面向对象编程有两层意义上的多态。
第一层意义上的多态又称方法名多态,它具体是指向对象的名称相同的方法传递不同参数时,对象会根据不同参数而做出不同的行为反应。例如一条狗,当它闻到猫的气味时,会吠叫并且追着猫跑;当它闻到食物的气味时,会分泌唾液并向着食物跑去。这两种状况下,是同一种嗅觉器官在工作,但闻的气味不同,狗做出的反应也不同。如果要使用Java程序来模拟狗的上述反应,就可以采用多态的思想,在狗类中编写模拟狗的嗅觉器官的方法,该方法能够针对不同气味参数,实现狗的不同反应。
第二层意义上的多态则是与继承相关的,它具体是指相同的方法被不同对象引用时可能产生不同的行为。例如,羊和狼都都具有哺乳动物类相同的行为“叫”,但羊的叫声是“咩……”,而狼的叫声是“嗷……”。对于同类中的这种随具体对象的不同而有所不同的行为,Java程序中推荐在父类中定义统一风格的方法处理,然后在实例化对象或子类对象时通过传递实际参数来进一步完善。这样一来,整个行为的处理都只依赖于父类的方法,以后只要维护和调整父类的方法即可,从而降低了维护的难度,节省了时间。
5.2 类与对象
5.2.1 类与对象的关系
在现实世界中,“万事万物,皆为对象”。人类解决问题总是将复杂的事物简单化,于是就会思考这些对象都是由哪些部分组成的。通常都会将对象划分为两个部分,即静态部分与动态部分。静态部分就是不能动的部分,这个部分被称为“属性”,任何对象都会具备其自身属性,如一个人,其属性包括高矮、胖瘦、年龄、性别等;动态部分则是可以运动的部分,也被称为“行为”,如一个人可以转身、微笑、说话、奔跑等,这些都属于这个人具备的行为。人类通过探讨对象的属性和观察对象的行为了解对象。
应用面向对象程序设计思想解决编程问题时,首先将待解决问题的实体抽象为对象,然后考虑这个对象具备的属性和行为,属性和行为都确定后,这个对象就被定义完成了,接下来便可以根据具体问题制定解决方案。
更进一步地,同类对象一般具有许多相同的属性和行为,可以将这些共同的属性和行为封装起来,以描述这类对象,这就形成了类。由此可见,类就是封装对象属性和行为的载体,如“狗类”,而对象则是类抽象出来的一个实例,如“旺财”这只狗。如图5.1所示,是Dog类和具体对象的关系。
5.2.2 类的定义
类是将数据和方法封装在一起的一种数据结构,数据表示为类的属性,方法表示为类的行为。Java中使用class关键字来定义类,语法格式如下:
[类修饰符]class 类名{
[修饰符] 数据类型 属性名;
…
[修饰符] 返回值类型 方法名([参数列表]) {
… // 方法体
return 返回值;
}
}
其中,“[]”中的修饰符是可选项,类修饰符可以分为4种:public、protected、private、default,各修饰符的含义如表5.1所示。
表5.1 类修饰符的说明
修饰符 |
说 明 |
public |
将一个类声明为公共类,它可以被任何对象访问 |
default(默认) |
在同一个包中可以访问,default不用写 |
abstract |
表示该类为一个抽象类,不能实例化该类 |
final |
表示该类不能被子类继承,该类即为最终类,不可再被继承 |
注意:类名的定义一般为名词,需要符合Java规范的标识符、首字母大写等。
5.2.3 成员变量
在Java程序中,一般将类的属性名表示为成员变量,成员变量描述了类的内部信息。成员变量可以是基本数据类型,也可以是数组、对象等引用数据类型。语法格式如下:
[修饰符]数据类型 成员变量名 = [初值];
其中,“[]”中的修饰符是可选项,成员变量修饰符有public、protected、private、default、static、final,各修饰符的含义如表5.2所示。
表5.2 成员变量修饰符的说明
修饰符 |
说 明 |
public |
指定该变量可以被任何对象的方法访问 |
protected |
指定该变量可以被该类及其子类或同一包中的其他类访问,在子类中可以重写此变量 |
private |
指定该只允许该类的方法访问,其他任何类(包括子类)中的方法都不能访问 |
default(默认) |
指定该变量可以被同一包中所有类访问,其他包中的类不能访问该变量,default不用写 |
static |
指定该变量可以被所有对象共享 |
final |
指定该变量不能被修改 |
5.2.4 成员方法
在编程中,一般将类的行为表示为成员方法,成员方法用来表示类的操作,实现类与外部的交互。声明方法的语法格式如下:
[修饰符]返回值数据类型 成员方法名([参数列表]){
… //方法体
return 返回值;
}
其中,“[]”中的修饰符是可选项,成员方法修饰符有public、protected、private、default、static、final,各修饰符的含义如表5.3所示。
注意:类的名字必须由大写字母开头而单词中的其他字母均为小写;如果类名称由多个单词组成,则每个单词的首字母均应为大写例如TestPage;如果类名称中包含单词缩写,则这个所写词的每个字母均应大写,如:XMLDemo还有一点命名技巧就是由于类是设计用来代表对象的,所以在命名类时应尽量选择名词。
表5.3 成员方法修饰符的说明
修饰符 |
说 明 |
public |
指定该方法可以被任何对象的方法访问 |
protected |
指定该方法可以被该类及其子类或同一包中的其他类访问,在子类中可以重写此方法 |
private |
指定该只允许该类的方法访问,其他任何类(包括子类)中的方法都不能访问 |
default(默认) |
指定该方法可以被同一包中所有类访问,其他包中的类不能访问,default不用写 |
static |
指定该方法可以被所有对象共享 |
final |
指定该方法不能被修改 |
注意:形参出现在函数定义中,在整个函数体内都可以使用, 离开该函数则不能使用。实参出现在主函数中,进入被调函数后,实参变量也不能使用。
参数列表中的参数用逗号分开,列表中包含了传递给调用函数的变量的声明。如果方法中没有参数,参数列表可以省略,小括号内不用填写任何内容。
若方法没有返回值,则返回值的数据类型应为void,且return语句可以省略。
5.2.5 类的UML图
为了更加方便地用于描述类的属性、方法,以及类和类之间的关系,可以采用UML(Unified Modeling Language)类图进行分析设计。UML即统一建模语言,它是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的软件密集系统的制品。在UML类图中,一个矩形表示一个类,矩形中有类名、成员变量和成员方法。如图5.2所示,是一个Dog类的UML类图,包含了name、age、weight、colour等成员变量,还有bark()、show()等方法。成员变量或成员方法前的 -、+、#、~表示权限,分别代表private、public、protected、default。
接下来,我们根据上面的语法格式定义一个Dog类,如例5-1所示。
例5-1 Dog.java
package com.aaa.p050205;
class Dog {
String name; // 声明名字属性
int age; // 声明年龄属性
double weight; // 声明体重属性
protected String colour; // 声明颜色属性
protected String bark(){ // 定义吠叫的方法
String a = "汪汪叫";
return a;
}
public void show() { // 定义显示信息的方法
System.out.println("名字是" + name + ",年龄:" + age);
}
}
例5-1中定义了一个类,Dog是类名,其中name、age、weight、colour是该类的成员变量,也称为对象属性,bark()、show ()是该类的成员方法,在show()方法体中可以直接对name、age成员变量进行访问,在后续的例子中只使用name、age成员变量和show()成员方法。
编程技巧: 定义类时先定义成员变量,再定义成员方法,由方法实现对成员变量的操作。
5.2.6 对象的创建与使用
对象之间靠互相传递消息而相互作用,消息传递的结果是启动方法,完成一些行为或修改对象的属性。创建对象之前,必须先声明对象,语法格式如下:
类名 对象名;
该对象名是一个引用变量,默认值为null,存放于栈内存中,表示不指向任何堆内存空间。接下来,需要对该变量进行初始化,Java使用new关键字来创建对象,也称实例化对象,语法格式如下:
对象名 = new 类名();
上述语法使用new关键字在堆内存中创建类的对象,对象名引用此对象。声明和实例化对象的过程可以简化,其语法格式如下:
类名 对象名 = new 类名();
接下来。演示创建Dog类的实例对象,具体示例如下:
Dog d = new Dog();
上述示例中,“Dog d”在栈内存中声明了一个Dog类型的引用变量,“new Dog()”为对象在堆中分配内存空间,最终返回对象的引用并赋值给变量d,如图5.3所示。
知识点拨:Java将内存分为栈和堆两种。
在函数中定义的基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中声明了一个变量时,Java就会在栈内存中为这个变量分配内存空间,当超过变量的作用域之后,Java也会自动释放为该变量分配的空间,这个回收的空间可以即刻用作他用。
堆内存用于存放由new创建的对象和数组。在堆内存中分配的内存空间,由Java虚拟机自动垃圾回收器来管理。
对象实例化后,就可以访问对象的成员变量和成员方法,其语法格式如下:
对象名.成员变量;
对象名.成员方法();
下面演示如何访问对象的成员变量和调用对象的成员方法,如例5-2所示。
例5-2 Demo0502.java
package com.aaa.p050206;
class Dog {
// 声明名字属性
public String name;
public int age; // 声明年龄属性
public void show() { // 定义显示信息的方法
System.out.println("名字是" + name + ",年龄:" + age);
}
}
public class Demo0502{
public static void main(String[] args) {
Dog d1 = new Dog(); // 实例化第一个Dog对象
Dog d2 = new Dog(); // 实例化第二个Dog对象
d1.name = "旺财"; // 为name属性赋值
d1.age = 5; // 为age属性赋值
d1.show(); // 调用对象的方法
d2.show();
}
}
程序运行结果如下:
名字是旺财,年龄:5
名字是null,年龄:0
例5-2中,实例化了两个Dog对象,通过“对象.属性”的方式为成员变量赋值,通过“对象.方法”的方式调用成员方法。从运行结果可发现,变量d1、d2引用的对象同时调用了show()方法,但输出结果却不相同。
因为用new创建对象时,会为每个对象开辟独立的堆内存空间,用于保存对象成员变量的值。因此,对变量d1引用的对象属性赋值并不会影响变量d2引用对象属性的值。为了更好地理解,变量d1、d2引用对象的内存状态如图5.4(a)和图5.4(b)所示。
需要注意的是,一个对象能被多个变量所引用,当对象不被任何变量所引用时,该对象就会成为垃圾,不能再被使用。
接下来演示内存垃圾对象是如何产生的,如例5-3所示。
例5-3 Demo0503.java
package com.aaa.p050206;
class Dog {
public String name; // 声明名字属性
public int age; // 声明年龄属性
public void show() { // 定义显示信息的方法
System.out.println("名字是" + name + ",年龄:" + age);
}
}
public class Demo0503{
public static void main(String[] args) {
Dog d1= new Dog(); // d1为第一个Dog对象
Dog d2 = new Dog(); // d2为第二个Dog对象
d1.name = "旺财"; // 为d1对象name属性赋值
d1.age = 5; // 为d1对象age属性赋值
d2.name = "花花"; // 为d2对象name属性赋值
d2.age = 6; // 为d2对象age属性赋值
d2 = d1; // 将d1对象传递给d2对象
d1.show(); // 调用对象的方法
d2.show();
}
}
程序的运行结果如下:
名字是旺财,年龄:5
名字是旺财,年龄:5
例5-3中,d2被赋值为d1后,会断开原有引用的对象,并和d1引用同一对象,因此打印出上述结果。此时,d2原有的引用对象不再被任何变量所引用,就成了垃圾对象,不能再被使用,只等待垃圾回收机制进行回收。垃圾产生的过程,如图5.4(c)所示。
(a)实例化两个对象且分配内存 (b)为对象d1的属性赋值 (c)d2断开原有内存地址并指向d1的地址
图5.4 对象的内存关系及垃圾对象的产生
图5.4(c)中,首先实例化两个对象d1和d2,其次分别为d1和d2的属性赋值,最后将d2重新赋值为d1,d2将断开原有引用,此时被断开引用的对象也不被其他引用变量所引用,就成为垃圾对象,等待被回收。
5.2.7 成员变量与局部变量的区别
在例5-1定义的Dog类的代码中,定义在bark()方法外的变量被称为成员变量,它的作用域为整个Dog类;定义在bark ()方法中的变量或方法的参数被称为局部变量,它的作用域在这个方法体内。为了更好的理解成员变量和局部变量,把成员变量和局部变量的区别列举如下:
定义的位置不同:成员变量定义在类内,在方法外部;局部变量定义在方法内部。
声明周期不同:成员变量随着对象的出现而出现,随着对象的消失而消失;局部变量是随着方法的运行而出现,随着方法的弹栈而消失。
默认值不同:成员变量如果没有赋值,会有默认值,规则和数组一样;局部变量没有默认值,如果要使用,必须手动赋值。
在内存中的位置不同:员变量存在于堆内存中,和类一起创建,局部变量存在于栈内存中,当方法执行完成,让出内存空间,让其他方法来使用内存。
5.3 类的封装
封装是面向对象程序设计的3大特征之一。封装也称为信息隐藏,将数据和方法的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在方法的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。封装具有优点:
实现专业的分工。将能实现某一特定功能的代码封装成一个独立的实体,开发人员在需要的时候调用,从而实现了专业的分工。
隐藏信息,实现细节。通过控制访问权限可以将不想让客户看到的信息隐藏起来,提高代码的安全性。例如,某客户的银行的密码需要保密,只能对该客户开放权限。
下面通过两个实例来理解如何实现类的封装。
首先,来看没有封装的Dog类在调用时会出现哪些问题,如例5-4所示。
例5-4 Demo0504.java
package com.aaa.p0503;
class Dog {
public String name; // 声明名字属性
public int age; // 声明年龄属性
public void show() { // 定义显示信息的方法
System.out.println("名字是" + name + ",年龄:" + age);
}
}
public class Demo0504 {
public static void main(String[] args) {
Dog d1= new Dog(); // 实例化一个Dog对象
d1.name = "旺财"; // 为name属性赋值
d1.age = -6; // 为age属性赋值
d1.show(); // 调用对象的方法
}
}
程序的运行结果如下:
名字是旺财,年龄:-6
运行结果输出的年龄为-6,程序运行没有任何问题,但现实生活中明显不合理。为了避免这种不合理的情况,这就需要用到封装,即不让使用者访问类的内部成员。这时候,就可以使用类的封装机制。封装是指定义一个类时,使用private关键字修饰属性,同时又提供public关键字修饰的公有方法以保证外部使用者访问类中的私有属性,即提供设置属性的setXxx()方法和获取属性的getXxx()方法,对不合理年龄进行过滤限制。
接下来,演示封装的实现过程,如例5-5所示。
例5-5 Demo0505.java
package com.aaa.p0503;
class Dog {
private String name; // 声明名字私有属性
private int age; // 声明年龄私有属性
public void setName(String str) { // 设置属性方法
name = str;
}
public String getName() { // 获取属性方法
return name;
}
public void setAge(int n) {
if (age<0) // 验证年龄,过滤掉不合理的
{
System.out.println(“年龄不合法...”);
}else
age = n;
}
public int getAge() {
return age;
}
public void show() { // 定义显示信息的方法
System.out.println("名字是" + name + ",年龄:" + age);
}
}
public class Demo0505 {
public static void main(String[] args) {
Dog d1= new Dog(); // 实例化一个Dog对象
d1.setName("旺财"); // 为name属性赋值
d1.setAge(-8); // 为age属性赋值
d1.show(); // 调用对象的方法
}
}
程序的运行结果如下:
年龄不合法...
姓名是旺财,年龄:0
例5-5中,使用private关键字将name和age属性声明为私有的,并对外提供public关键字修饰的属性访问器。其中,setName()设置name属性的值,getName()获取name属性的值;同理,getAge()和setAge()方法用于获取和设置age属性。在main()方法中创建Dog对象,并调用setAge()方法传入-8,在setAge()方法中对参数n的值进行了检查,由于当前传入的值小于0,age属性没有被赋值,仍为默认初始值0,这样针对无效数据进行了有效性限制,保证了程序的健壮性。
5.4 构造方法
前面的讲解中,成员变量都是在对象建立之后,由相应的方法来对其赋值。如果一个对象在被创建时就完成所有的初始化工作,将会非常简洁。在Java中提供了一个特殊的成员方法——构造方法。
构造方法是一种特殊的方法,用来在对象被创建时初始化成员变量。构造方法有如下3个特征:
构造方法名与类名相同。
构造方法没有返回值类型。
构造方法中不能使用return返回一个值。
下面演示如何定义类的无参构造方法,如例5-6所示。
例5-6 Demo0506.java
package com.aaa.p0504;
class Dog {
public Dog() {
System.out.println("狗类的构造方法自动被调用");
}
}
public class Demo0506 {
public static void main(String[] args) {
System.out.println("声明对象:Dog d = null");
Dog d = null; // 声明对象时不调用构造方法
System.out.println("实例化对象:d = new Dog()");
d = new Dog(); // 实例化对象时调用构造方法
}
}
程序的运行结果如下:
声明对象:Dog d = null
实例化对象:d = new Dog()
狗类的构造方法自动被调用
从程序运行结果可发现,当调用关键字new实例化对象时才会调用构造方法。细心的读者会发现,在之前的示例中并没有定义构造方法,但是也能被调用。这是因为类未定义任何构造方法,系统会自动提供一个默认构造方法。但是,如果已存在带参数的构造方法,则系统将不会提供默认构造方法。
接下来,演示如何定义类的有参构造方法,如例5-7所示。
例5-7 Demo0507.java
package com.aaa.p0504;
class Dog {
private String name; // 声明名字私有属性
private int age; // 声明年龄私有属性
public Dog(String str, int n) { // 构造方法初始化成员属性
name = str;
age = n;
}
public void show() { // 定义显示信息的方法
System.out.println("名字:"+name+",年龄:"+age);
}
}
public class Demo0507 {
public static void main(String[] args) {
Dog d = new Dog();
d.show();
}
}
程序编译报错,提示“Expected 2 arguments but found 0”,中文含义为“期待2个参数,但是发现0个”,出现错误的原因在于,类中已经提供有参数的构造方法,系统将不会提供缺省构造方法,编译器因找不到无参构造方法而报错。修改第16行代码如下:
Dog d = new Dog("旺财", 5);
程序的运行结果下:
名字:旺财,年龄:5
从程序运行结果可发现,实例化对象时调用了有参构造方法为属性赋值。
误区警告:只有当类没有任何构造方法的时候,系统才会提供无参构造方法。编写程序时,为避免出现编译错误,每次定义类的构造方法时,预先定义一个无参的构造方法,有参的构造方法可以根据需求再定义。
5.5 方法重载
方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有的参数类型、参数个数或不同类型的参数顺序不同。方法重载不要求用户在调用一个方法之前转换数据类型,它会自动地寻找匹配的方法。方法重载对于编写结构清晰而简洁的类有很大的作用。
5.5.1 构造方法的重载
在实际开发中定义类时,只提供一个构造方法往往不能满足需求,所以需要提供多个构造方法,以提高类的适用性,这就符合方法重载的条件。以现实生活中的汽车为例,汽车厂家生产卡车与轿车时,出厂的配置是不一样的,这时就需要设置不同参数配置。Java程序中的类也一样,使用构造方法创建对象并初始化成员变量的时候,也需要根据不同的实际需求配置不同的成员变量,而构造方法的重载就可以满足这一需求。
下面演示构造方法的重载,如例5-8所示。
例5-8 Demo0508.java
package com.aaa.p050501;
class Dog {
private String name; // 声明名字私有属性
private int age; // 声明年龄私有属性
public Dog(String str) {
name = str;
}
public Dog(String str, int n) { // 构造方法初始化成员属性
name = str;
age = n;
}
public void show() { // 定义显示信息的方法
System.out.println("名字:"+name+",年龄:"+age);
}
}
public class Demo0508 {
public static void main(String[] args) {
Dog d1= new Dog("旺财"); // 创建对象并调用一个参数构造方法
Dog d2 = new Dog("花花", 5); // 创建对象并调用两个参数构造方法
d1.show();
d2.show();
}
}
程序的运行结果如下:
名字是旺财,年龄:0
名字是花花,年龄:5
例5-8在Dog类中定义了两个构造方法,两个方法的参数列表不同,符合重载条件。在创建对象时,根据参数的不同,分别调用不同的构造方法。其中,一个参数的构造方法只对name属性进行初始化,此时age属性为默认值0;两个参数的构造方法根据实参分别对nane和age属性进行初始化。
想一想:构造方法访问权限可以是private的吗?如果使用private修饰构造方法,该构造方法只能在当前类中使用,不允许在外边访问,一般用于单例设计模式的设计场景。
5.5.2 成员方法的重载
成员方法的重载即在同一个类中定义多个方法名相同,但参数列表不同的成员方法。调用重载的成员方法时,Java编译器会根据实参列表寻找最匹配的方法进行调用。
下面通过在一个类中分别定义求两个整数、浮点数和的重载方法来演示成员方法的重载,如例5-9所示。
例5-9 Demo0509.java
package com.aaa.p050502;
public class Demo0509{
public static void main(String[] args) {
Demo0509 demo0509 = new Demo0509();
// 调用add(int, int)方法
System.out.println("5和9的和:" + demo0509.add(5, 9));
// 调用add(double, double)方法
System.out.println("5.0和9.0的和:" + demo0509.add(5.0, 9.0));
}
public int add(int n1, int n2) { //返回两个整数的和
return n1 + n2;
}
public double add(double n1, double n2) { // 返回两个浮点数的和
return n1 + n2;
}
}
程序的运行结果如下:
5和9的和:14
5.0和9.0的和:14.0
在调用重载方法时,出现两个或多个可能的匹配时,编译器无法判断哪个是最精确的匹配,则会产生编译错误,称为歧义调用。
接下来,我们来演示重载的成员方法歧义调用的情况,如例5-10所示。
例5-10 Demo05010.java
package com.aaa.p050502;
public class Demo05010{
public double add(int n1,double n2) { // 返回整数和浮点数的和
return n1 + n2;
}
public double add(double n1,int n2) { // 返回浮点数和整数的和
return n1 + n2;
}
public static void main(String[] args) {
Demo0510 demo0510 =new Demo0510();
System.out.println(demo0510.add(5,9));
}
}
程序编译错误,并提示“Ambiguous method call. Both”,中文含义为“期待2个参数,但是发现0个”,出错的原因在于min(int, double)和min(double, int)与min(5,9)都匹配,从而产生二义性,导致编译错误。
知识点拨:Java的方法调用的就近原则归结为:向上就近匹配原则。如果方法的参数表中的数据类型和调用时给出的参数类型不尽相同时会根据向上匹配的就近原则。即类型就近向上转化匹配。此处int和double处于同一级别,参数同时符合两种参数类型,从而导致了二义性错误。
5.6 this关键字
类在定义成员方法时,局部变量和成员变量可以重名,但此时不能访问成员变量。为了避免这种情形,Java提供了this关键字,表示当前对象,指向调用的对象本身。
5.6.1 this关键字的三种用法
this关键字在程序中主要有3种用法,下面来分别讲解。
1. this关键字调用成员变量
通过this关键字调用成员变量,可以解决与局部变量名称冲突的问题。请看下述示例代码:
class Dog{
int age; // 成员变量age
public Dog(int age) { // 局部变量age
this.age = age; // 将局部变量age的值赋给成员变量age
}
}
在上面的代码中,构造方法的参数是age是一个局部变量,Dog类还定义了一个成员变量,名称也是age。在构造方法中如果使用“age”,则是访问局部变量,但如果使用“this.age”则是访问成员变量。
2. this关键字调用成员方法
可以通过this关键字调用成员方法,具体示例代码如下:
class Dog{
public void openMouth() {
... //方法的代码块
}
public void eat() {
this.openMouth();
}
}
在上面的代码中,eat()方法使用this关键字调用了openMouth()方法。需要注意的是,此处的this关键字可以省略不写。
3. this关键字调用构造方法
构造方法是在实例化对象时被Java虚拟机自动调用的,在程序中不能像调用其他方法一样去调用构造方法,但可以在一个构造方法中使用“this([参数1,参数2…])”的形式来调用其他的构造方法。
下面,我们综合演示this关键字调用成员变量、成员方法、构造方法,如例5-11所示。
例5-11 Demo05011.java
package com.aaa.p050601;
class Dog {
private String name; // 声明名字私有属性
private int age; // 声明年龄私有属性
public Dog() {
System.out.println("调用无参构造方法");
}
public Dog(String name){
this.name = name;
System.out.println("调用带一个参数的构造方法");
}
public Dog(String name, int age) {
this(name);
System.out.println("调用两个参数的构造方法");
this.age = age; // 明确表示为类中的age属性赋值
}
public void testShow(){
System.out.println("使用this调用方法");
this.show();
}
public void show() { // 定义显示信息的方法
System.out.println("名字是" + this.name + ",年龄:" + this.age);
}
}
public class Demo0511{
public static void main(String[] args) {
Dog dog = new Dog("旺财", 6);
dog.testShow();
}
}
程序的运行结果如下:
调用带一个参数的构造方法
调用两个参数的构造方法
使用this调用方法
名字是旺财,年龄:6
实例化对象时,调用了两个参数的构造方法,在该构造方法中通过this(name)调用了带一个参数的构造方法。因此,运行结果中显示两个构造方法都被调用了。
5.6.2 this关键字调用构造方法的常见错误
在使用this调用构造方法时,还需注意以下常见错误。
1. this关键字调用构造方法的位置不当
在构造方法中,使用 this调用构造方法的语句必须位于首行,且只能出现一次,如果将this语句放到代码最后面,程序将报错。例如:
public Dog(String name, int age) {
System.out.println("调用有参构造函数");
this.name = name; // 明确表示为类中的name属性赋值
this.age = age; // 明确表示为类中的age属性赋值
this(); // 调用无参构造函数
}
编译报错,并提示“Call to 'this()' must be first statement in constructor body”,中文含义为“对this的调用必须是构造器中的第一个语句”。因此,使用this调用构造方法的语句必须位于构造方法的第一行。
2.两个构造方法中使用this关键字互相调用
不能在一个类的两个构造方法中使用this互相调用,例如:
class Dog{
public Dog() {
this(6); // 调用有参的构造方法
System.out.println("无参的构造方法被调用了...");
}
public Dog(int age) {
this(); // 调用无参的构造方法
System.out.println("有参的构造方法被调用了...")
}
}
编译报错,并提示“Recursive constructor invocation”,中文含义“递归调用函数”的错误信息,在构造方法中不能互相调用。
5.7 static关键字
Java语言中,static关键字用于修饰类的成员变量、成员方法以及代码块,被static修饰的成员称为静态成员。为什么要用static关键字呢?就像现实世界中有公共设施(公园、卫生间、汽车等)资源共享一样,Java程序中类的一些成员变量、成员方法、代码块也可以被程序所共享。
在现实场景中,汽车站乘客买票需要向销售员进行购票,下面使用程序模拟两个售票员同时售卖一辆大巴车票的情形,如例5-12演示。
例5-12 Demo0512.java
package com.aaa.p0507; //后期改包名
class Saler{ // 定义售票员类
int ticket = 5; // 初始化客车总票数
public void sale(){
ticket--;
}
}
public class Demo0512 { // 模拟售票
public static void main(String[] args) {
Saler s1 = new Saler(); // 创建售票员1;
s1.sale(); //销售员1卖票1张
System.out.println("销售员1剩余票:"+s1.ticket +"张");
Saler s2 = new Saler(); //创建售票员2
s2.sale(); //销售员2卖票1张
System.out.println("销售员2剩余票:"+s2.ticket +"张");
}
}
程序的运行结果如下:
销售员1剩余票:4张
销售员2剩余票:4张
很明显,出现这样的问题是不应该的,因为ticket是Saler类的共有属性,而不是被其某个类独有。要解决这一问题,就需要使用static关键字。
5.7.1 静态变量
使用static修饰的成员变量,称为静态变量或类变量,它被类的所有对象共享,属于整个类所有,因此可以通过类名直接来访问。而未使用static修饰的成员变量称为实例变量,它属于具体对象独有,只能通过引用变量访问。修改例5-12的代码如下:
class Saler{
static int ticket = 5;
public void sale(){
ticket--;
}
}
… //其他代码和之前一样
程序的运行结果如下:
销售员1剩余票:4张
销售员2剩余票:3张
很明显,使用static关键字修饰Saler类的成员变量ticket以后,程序运行结果符合实际情况。
注意:static关键字在修饰变量的时候只能修饰成员变量,不能修饰方法中的局部变量,具体示例如下:
public class Test {
public void show() {
static int count = 0; // 非法,编译会报错
}
}
5.7.2 静态方法
在实际开发中,开发人员往往需要在不创建实例的情况下直接直接调用类的某些方法。使用static修饰的成员方法,称为静态方法,它无须创建类的实例就可以直接通过类名来调用,当然也可以通过对象名来调用。
接下来,我们演示静态方法的使用,如例5-13所示。
例5-13 Demo0513.java
package com.aaa.p050702;
class Dog {
private static int count; // 保存对象创建的个数
public Dog() {
count++;
}
public static void show() {
System.out.println("类实例化次数:" + count);
}
}
public class Demo0513{
public static void main(String[] args) {
Dog.show(); // 调用静态方法
Dog d1 = new Dog(); // 创建Dog对象
Dog d2 = new Dog();
Dog d3 = new Dog();
Dog d4 = new Dog();
Dog d5 = new Dog();
Dog.show(); // 再次调用静态方法
}
}
程序的运行结果如下:
类实例化次数:0
类实例化次数:5
例5-13中,Dog类定义了静态方法show(),并通过Dog.show()的形式调用了该静态方法,由此可见,不需要创建对象就可以调用静态方法。
静态方法只能访问类的静态成员(静态变量、静态方法),不能访问类中的实例成员(实例变量和实例方法)。这是因为未被static修饰的成员都是属于对象的,所以需要先创建对象才能访问,而静态方法在被调用时可以不用创建任何对象。
5.7.3 静态代码块
代码块是指用大括号“{}”括起来的一段代码,根据位置及声明关键字的不同,代码块可分为普通代码块、构造代码块、静态代码块和同步代码块,其中同步代码块在多线程部分进行讲解。
静态代码块就是static修饰的代码块,执行优先级高于非静态的初始化块,它会在类初始化的时候执行一次,执行完成便销毁,它仅能初始化类变量,即static修饰的数据成员。
接下来,我们演示静态代码块的使用,如例5-14所示。
例5-14 Demo0514.java
package com.aaa.p050703;
class Dog {
public Dog() { // 定义构造方法
System.out.println("Dog类的构造方法");
}
static { // 定义静态代码块
System.out.println("Dog类内的静态代码块");
}
}
public class Demo0514{
public static void main(String[] args) {
new Dog(); // 实例化对象
new Dog();
new Dog();
}
static { // 定义静态代码块
System.out.println("main方法所在类的静态代码块");
}
}
程序的运行结果如下:
main方法所在类的静态代码块
Dog类内的静态代码块
Dog类的构造方法
Dog类的构造方法
Dog类的构造方法
从程序运行结果中可以发现,静态代码块先于主方法和构造代码块执行,而且无论类的对象被创建多少次,由于Java虚拟机只加载一次类,所以静态代码块只会执行一次。
5.8 包
当一个大型程序交由数个不同的程序开发人员进行开发时,用到相同的类名是很有可能的,当声明的类很多时,类名冲突的几率会大增,为了解决这个问题,Java引入了包机制来管理类。类似于使用操作系统通过文件夹管理各类的文件,针对不用的文件分门别类的存放。
5.8.1 包的概念和作用
包是Java提供的一种解决类、接口、注释、枚举等命名冲突的机制,可以说包提供了一种命名机制和可见性限制机制。包在物理上就是一个文件夹,逻辑上代表一个分类概念。包的作用如下:
区分相同名称的类、接口、枚举、注释等。
把功能相似或相关的类、接口组织在同一个包中,方便类的查找和使用。
增加对类、接口等访问权限。
5.8.2 创建包
Java使用package关键字来创建包,package 语句应该放在源文件的第一行,在每个源文件中只能有一个包定义语句。定义包的语法格式如下:
package 包名1[.包名2[包名3…]];
包的命名规范如下:
包名全部由小写字母(多个单词也全部小写)组成。
如果包名包含多个层次,层次之间用“.”进行分割。
包名一般由倒置的域名开头,比如 com.aaa,不要有 www。
自定义包不能以java 开头。
注意:如果在源文件中没有定义包,那么类、接口、枚举和注释类型文件将会被放进一个无名的包中,也称为默认包。在实际企业开发中,通常不会把类定义在默认包下。
在IDEA上创建包,可在项目下的src目录上点击右键,选择“New-Package”,如图5.5所示。然后输入相应的包名即可,本处输入是“com.aaa”,如图5.6所示。确定回车,会在IDEA的项目生成com.aaa包。
5.8.3 导入包
如果使用不同包中的其他类,需要使用该类的全名(包名.类名),代码如下:
com.aaa.Dog wangcai = new com.aaa.Dog();
其中,com.aaa 是包名,Dog是包中的类名, wangcai是类的对象。
为了简化编程,Java 引入了 import 关键字,使用它可以向某个 Java 文件中导入指定包层次下的某个类或全部类。import 语句位于 package 语句之后,类定义之前。一个 Java 源文件只能包含一个 package 语句,但可以包含多个 import 语句。使用import导入包中某个类的语法格式如下:
import 包名1[[.包名2[.包名3…]]].类型名|*;
其中,“类型名”格式表示导入的具体类型名,“*”表示导入的这个包下所有的类型。Java编译器自动隐式导入java.lang包,无需使用import语句,前面在 Java 程序中使用 String类、System 类时都可以使用其中的类型,但是要使用其他包中的类,就必须使用import语句导入。
注意:使用星号(*)可能会增加编译时间,特别是引入多个大包时,所以明确的导入具体的类型是一个好方法,可以提高程序的可读性。
通过使用 import 语句可以简化编程,但 import 语句并不是必需的,如果在类里使用其它类的全名,可以不使用 import 语句。在一些特殊的情况下,import语句也会失效,此时只能在源文件中使用类全名。例如,需要在程序中使用 java.sql 包下的类,也需要使用 java.util 包下的类,则可以使用如下两行 import 语句:
import java.util.*;
import java.sql.*;
如果,接下来在XXX.java程序中需要使用 Date 类,则会引起如下编译错误:
XXX.java:25:对Date的引用不明确,
java.sql中的类java.sql.Date和java.util中的类java.util.Date都匹配
上面错误提示:在 XXX.java文件的第 25 行使用了 Date 类,而 import 语句导入的 java.sql 和 java.util 包下都包含了 Date 类,系统不知道使用哪个包下的 Date 类。在这种情况下,如果需要指定包下的 Date 类,则只能使用该类的全名,代码如下:
// 为了让引用更加明确,即使使用了 import 语句,也还是需要使用类的全名
java.sql.Date d = new java.sql.Date()
5.8.3 常用的包
Java提供了一些系统包,其中包含了Java 开发中常用的基础类,常用的系统包如表5.4所示。
表5.4 系统常用包
包 |
说 明 |
java.lang |
Java 的核心类库,包含运行 Java 程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、异常处理和线程类等,系统默认加载这个包 |
java.io |
Java 语言的标准输入/输出类库,如基本输入/输出流、文件输入/输出、过滤输入/输出流等 |
java.util |
包含如处理时间的 Date 类,处理动态数组的 Vector 类,以及 Stack 和 HashTable 类 |
java.awt |
构建图形用户界面(GUI)的类库,低级绘图操作 Graphics 类、图形界面组件和布局管理(如 Checkbox 类、Container 类、LayoutManger 接口等),以及用户界面交互控制和事件响应(如 Event 类) |
java.net |
实现网络功能的类库有 Socket 类、ServerSocket 类 |
java.lang.reflect |
提供用于反射对象的工具 |
java.util.zip |
实现文件压缩功能 |
java.sql |
实现 JDBC 的类库 |
java.rmi |
提供远程连接与载入的支持 |
java. security |
提供安全性方面的有关支持 |
Java.swing |
提供了Java图形用户界面开发所需要的各种类和接口。javax.swing提供了一些高级组件 |
5.9 Java修饰符总结
前文已经多处提到修饰符,为了能够系统地掌握Java修饰符,这里对其进行全面总结。在Java语言中,修饰符分为两类:访问控制符和非访问控制符。
5.9.1 访问控制符
访问控制符是一组限定类、属性或方法是否可以被程序里的其他部分访问和调用的修饰符。合理地使用访问控制符,可以降低类和类之间的耦合性(关联性),进而降低整个项目的复杂度,也便于整个项目的开发和维护。类的访问控制符只能是空或者 public,方法和属性的访问控制符有 4 个,分别是 public、 private、protected 和 default,其中 default是一种没有定义专门的访问控制符的默认情况。如图5-7所示,给出了4类访问控制符的控制级别。下面,我们分别对这4类访问控制符进行总结:
private。用 private 修饰的类成员变量和成员方法,只能被该类自身的方法访问和修改,而不能被任何其他类(包括该类的子类)访问和引用。因此,private 修饰符具有最高的保护级别。
default(默认)。如果一个类、类成员变量和成员方法没有访问控制符,说明它具有默认的访问控制特性。这种默认的访问控制权限是,该类或成员只能被同一个包中的类访问和引用,而不能被其他包中的类使用,即使其他包中有该类的子类。这种访问特性又称为包访问性。
protected。用保护访问控制符 protected 修饰的类成员变量、成员方法可以被三种类所访问:该类自身、与它在同一个包中的其他类、在其他包中的该类的子类。使用 protected 修饰符的主要作用是,允许其他包中它的子类来访问父类的特定属性和方法,否则可以使用默认访问控制符default。
public。这是最宽松的访问级别,当一个类或类成员被声明为 public 时,它就具有了被其他包中的类访问的可能性,只要包中的其他类在程序中使用 import 语句引入 public 类,就可以访问和引用这个类或类成员。
5.9.2 非访问控制符
我们前文用到了Java非访问控制修饰符static和 final,其作用可总结如下:
static。用来创建类方法、类成员变量、类,实现数据的共享和初始化加载。
final。用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。
需要特别指出的是,Java非访问修饰符还有abstract、synchronized、volatile、transient会在后续章节进行讲解。
5.10 本章小结
类是把事物的属性和行为封装在一起的特殊数据结构,用于表达现实世界的一种抽象概念。
Java语言把数据成员称为成员变量,把对数据成员的操作称为成员方法,成员方法简称方法。
封装是把变量和方法包装在一个类内,达到限定成员访问、保护数据的目的。
由类创建的对象称为实例,利用new关键字创建新的对象;访问对象里面的成员变量和成员方法,可以通过“对象名.成员变量名”和“对象名.成员方法”两种形式。
this关键字表示调用对象本身的成员;static称为静态修饰符,它可以修饰类、类的成员变量、类的成员方法、代码块,用于实现数据的共享。
方法的参数可以是任意类型的数据,返回值可以是任意类型;如果方法没有返回值,则必须在方法的前面加void关键字。
用修饰符 private 来修饰的类成员称为类的私有成员,私有成员无法从该类的外部访问到,只能被该类自身访问和修改,而不能被任何其他类(包括该类的子类)来获取或引用;如果在类的成员声明的前面加上修饰符 public,则该成员为公共成员,表示该成员可以被所有其他的类所访问。
构造方法是一种特殊的方法,作用是在创建对象的时候初始化成员变量。如果一个类没有定义构造方法,则系统会自动生成默认的构造方法。
重载是在同一个类内定义相同名称的多个方法,这些同名的方法的参数列表不同,于是他们便可具有不同的功能。
包是在使用多个类、接口、注释、枚举时,避免名称重复而采取的一种机制。导入包中的某个类,格式为“import 包名.类名”。
5.11 理论测试与实践练习
1.填空题
1.1 面向对象的3大特征是 、 、 。
1.2 在类体中,变量定义部分所定义的变量称为类的 。
1.3 在关键字中,能代表当前类或对象本身的是 。
2.选择题
2.1 类的定义必须包含在以下哪种符号之间( )
A.{} B.“”
C.() D.[]
2.2 在以下哪种情况下,构造方法被调用( )
A.类定义时 B.创建对象时
C.使用对象的属性时 D.使用对象的方法时
2.3 有一个Test类,下面为其构造方法的声明,正确的是( )
A.test(int x) {} B.void Test(int x) {}
C.void test(int x) {} D.Test(int x) {}
2.4 下面哪一种是正确的类的声明( )
A.public class Qf{} B.public void QF{}
C.public class void min{} D.public class min(){}
2.5定义类时不能用到的关键字是( )
A.final B.public
C.protected D.abstract
3.思考题
3.1 请简述什么是类和对象。
3.2 请简述构造方法与成员方法的区别。
3.3 请简述this关键字的使用。
3.4 请简述什么是包及其注意事项?
4.编程题
4.1 在现实生活中,假设一个司机可以驾驶轿车、客车和货车。现在要求使用方法重载来实现这个生活案例。实现思路参考如下:
创建轿车类、客车类和货车类,属性自定。
创建司机类,属性自定。
在司机类中针对轿车、客车和货车分别创建3个驾驶方法。
创建测试类并进行测试。
4.2 改写本章正文中的Dog类,使之包含属性:name、color、kind、age,同时包含如下方法:
display():输出狗的所有属性。
eat(参数):没有返回值,根据传入的参数输出“XXX小狗正在吃XXX”。
guard():有返回值,根据传入的进门的人的不同返回不同的结果。如果是生人,返回“旺旺”;如果是主人则返回“摇尾巴”;
编写测试类,创建具体狗的对象,测试各个方法。
- 点赞
- 收藏
- 关注作者
评论(0)