面向对象概述
面向对象是一种符合人类思维习惯的编程思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系。在程序中使用对象来映射现实中的事物,使用对象的关系来描述事物之间的联系,这种思想就是面向对象。
对于学习过其他编程语言的人员来说,可能会想到面向过程。面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候依次调用就可以了。面向对象则是把构成问题的事务按照一定规则划分为多个独立的对象,然后通过调用对象的方法来解决问题。当然,一个应用程序会包含多个对象,通过多个对象的相互配合即可实现应用程序所需的功能,这样当应用程序功能发生变动时,只需要修改个别的对象就可以了,这使得代码更容易得到维护。
面向对象的特点主要可以概括为封装、继承和多态,接下来针对这三种特性进行简单介绍。
1.封装
封装是面向对象的核心思想,将对象的属性和行为封装起来,不需要让外界知道具体实现细节,这就是封装思想。例如,用户使用电脑,只需要使用手指敲键盘就可以了,无需知道电脑内部是如何工作的,即使用户可能碰巧知道电脑的工作原理,但在使用时,并不完全依赖电脑工作原理这些细节。
2.继承
继承主要描述的就是类与类之间的关系,通过继承,可以在无需重新编写原有类的情况下,对原有类的功能进行扩展。例如,有一个汽车的类,该类中描述了汽车的普通属性和功能,而轿车的类中不仅应该包含汽车的属性和功能,还应该增加轿车特有的属性和功能,这时,可以让轿车类继承汽车类,在轿车类中单独添加轿车特有的属性和功能就可以了。继承不仅增强了代码的复用性、提高开发效率,还为程序的维护补充提供了便利。
3.多态
多态指的是在一个类中定义的属性和功能被其他类继承后,当把子类对象直接赋值给父类引用变量时,相同引用类型的变量调用同一个方法所呈现出的多种不同行为特性。例如,当听到“Cut” 这个单词时,理发师的行为表现是剪发,演员的行为表现是停止表演等。不同的对象,所表现的行为是不一样的。
面向对象的思想只凭上面的介绍是无法让初学者真正理解的,初学者只有通过大量的实践练习和思考,才能真正领悟面向对象思想。
java中的类与对象
类与对象的关系
面向对象的编程思想,力图让程序中对事物的描述与该事物在现实中的形态保持一致。为了做到这一点,面向对象的思想中提出了两个概念——类和对象。其中,类是对某一类事物的抽象描述,而对象用于表示现实中该类事物的个体。接下来通过一个图例来描述类与对象的关系,如图1所示。
图1 类与对象
在图1中,可以将人看作是一个类,将每个具体的人(如小韩、小石等)看作对象,从人与具体个人之间的关系便可以看出类与对象之间的关系。类用于描述多个对象的共同特征,它是对象的模板,而对象用于描述现实中的个体,它是类的实例。从图3-1可以看出,对象是类的具体化,并且一个类可以对应多个对象。
类的定义
在面向对象的思想中,最核心的就是对象。为了在程序中创建对象,首先需要定义一个类。类是对象的抽象,它用于描述一组对象的共同特征和行为,例如人都有姓名、年龄、性别等特征,还有学习、工作、购物等行为。以面向对象的编程思想,就可以将某一类中共同的特征和行为封装起来,把共同特征作为类的属性(也叫成员变量),把共同行为作为类的方法(也叫成员方法)。本节将对Java中类的定义格式、类的成员变量和成员方法进行详细讲解。
1.类的定义格式
Java中的类是通过class关键字来定义的,其语法格式如下:
[修饰符] class 类名 [extends 父类名] [implements 接口名]{
// 类体,包括类的成员变量和成员方法
}
在上述语法格式中,class前面的修饰符可以是public,也可以不写(默认);class之后是定义的类名,类名首字母要大写,并且其命名要符合标识符的命名规则;extends和implements是可选项,均为Java中的关键字,其中extends用于说明所定义的类继承于哪个父类,implements关键字用于说明当前类实现了哪些接口(这两个关键字将在下一章详细讲解,这里作为了解即可)。后面大括号{}中的内容是类体,即需要在类中编写的内容,它主要包括类的成员变量和成员方法。
2.声明(定义)成员变量
类的成员变量也被称作类的属性,它主要用于描述对象的特征。例如,一个人的基本属性特征有姓名、年龄、职业、住址等信息,在类中要使用姓名、年龄等信息时,就需要先将它们声明(定义)为成员变量。
声明(定义)成员变量的语法格式如下:
[修饰符] 数据类型 变量名 [ = 值];
在上述语法格式中,修饰符为可选项,用于指定变量的访问权限,其值可以是public、private等;数据类型可以为Java中的任意类型;变量名是变量的名称,必须符合标识符的命名规则,它可以赋予初始值,也可以不赋值。通常情况下,将未赋值(没有被初始化)的变量称之为声明变量,而赋值(初始化)的变量称之为定义变量。
例如,姓名和年龄属性在类中的声明和定义方式如下:
private String name; // 声明一个String类型的name;
private int age = 20; // 定义一个int类型的age,并赋值为20;
3.声明(定义)成员方法
成员方法也被称为方法,类似于C语言中的函数,它主要用于描述对象的行为。一个人的基本行为特征有吃饭、睡觉、运动等,这些行为在Java类中,就可以定义成方法。
定义一个方法的语法格式如下:
[修饰符] [返回值类型] 方法名([参数类型 参数名1,参数类型 参数名2,...]){
//方法体
...
return 返回值; //当方法的返回值类型为void时,return及其返回值可以省略
}
上面语法格式中,[]中的内容表示可选,各部分的具体说明如下:
● 修饰符:方法的修饰符比较多,有对访问权限进行限定的(如public、protected、private),有静态修饰符static,还有最终修饰符final等,这些修饰符在后面的学习过程中会逐步讲解。
● 返回值类型:用于限定方法返回值的数据类型,如果不需要返回值,可以使用void关键字。
● 参数类型:用于限定调用方法时传入参数的数据类型。
● 参数名:是一个变量,用于接收调用方法时传入的数据。
● return关键字:用于结束方法以及返回方法指定类型的值,当方法的返回值类型为void时,return及其返回值可以省略。
● 返回值:被return语句返回的值,该值会返回给调用者。
在上述语法中,{}之前的内容被称之为方法签名(或方法头),而{}中的执行语句被称为方法体。需要注意的是,方法签名中的“[参数类型 参数名1,参数类型 参数名2,...]”被称作参数列表,它用于描述方法在被调用时需要接收的参数,如果方法不需要接收任何参数,则参数列表为空,即()内不写任何内容。关于上述语法结构中的修饰符内容,将在后面进行逐一的讲解,这里读者只需了解如何定义类、成员变量和成员方法即可。
了解了类及其成员的定义方式后,接下来通过一个具体的案例来演示一下类的定义,如文件1所示。
文件1 Person.java
1 public class Person {
2 int age; // 声明int类型的变量age
3 // 定义 speak() 方法
4 void speak() {
5 System.out.println("我今年" + age + "岁了!");
6 }
7 }
文件1中定义了一个Person类,并在类中定义了类的成员变量和成员方法。其中,Person是类名,age是类的成员变量,speak()是类的成员方法。在成员方法speak()中可以直接访问成员变量age。
脚下留心
在Java中,定义在类中的变量被称为成员变量,定义在方法中的变量被称为局部变量。如果在某一个方法中定义的局部变量与成员变量同名,这种情况是允许的,此时方法中通过变量名访问到的是局部变量,而并非成员变量,请阅读下面的示例代码:
public class Person {
int age = 10; // 类中定义的变量被称作成员变量
void speak() {
int age = 30; // 方法内部定义的变量被称作局部变量
System.out.println("我今年" + age + "岁了!");
}
}
上面的代码中,speak()方法中的打印语句所访问的变量age,就是局部变量,也就是说,当有另外一个程序来调用speak()方法时,输出的值为30,而不是10。
对象的创建与使用
应用程序想要完成具体的功能,仅有类是远远不够的,还需要根据类创建实例对象。在Java程序中,可以使用new关键字来创建对象,具体语法格式如下:
类名 对象名称 = new 类名();
例如,创建Person类的实例对象代码如下:
Person p = new Person();
上面的代码中,“new Person()”用于创建Person类的一个实例对象,“Person p”则是声明了一个Person类型的变量p,中间的等号用于将Person对象在内存中的地址赋值给变量p,这样变量p便持有了对象的引用。为了便于描述,本书接下来的章节,通常会将变量p引用的对象简称为p对象。在内存中变量p和对象之间的引用关系如图1所示。
图1 内存分析
从图1可以看出,在创建Person对象时,程序会占用两块内存区域,分别是栈内存和堆内存。其中Person类型的变量p被存放在栈内存中,它是一个引用,会指向真正的对象;通过new Person()创建的对象则放在堆内存中,这才是真正的对象。
小提示:
Java将内存分为两种,即栈内存和堆内存。其中栈内存用于存放基本类型的变量和对象的引用变量(如Person p),堆内存用于存放由new创建的对象和数组。
在创建Person对象后,可以通过对象的引用来访问对象所有的成员,具体格式如下:
对象引用.对象成员
接下来通过一个案例来学习如何访问对象的成员,如文件1所示。
文件1 Example02.java
1 public class Example02 {
2 public static void main(String[] args) {
3 Person p1 = new Person(); // 创建第一个Person类对象
4 Person p2 = new Person(); // 创建第二个Person类对象
5 p1.age = 18; // 为age属性赋值
6 p1.speak(); // 调用对象的方法
7 p2.speak();
8 }
9 }
运行结果如图2所示。
图2 运行结果
文件1中,p1、p2分别引用了Person类的两个实例对象。从图2可以看出,p1和p2对象在调用speak()方法时,打印的age值不同。这是因为p1对象和p2对象是两个完全独立的个体,它们分别拥有各自的age属性,对p1对象的age属性进行赋值并不会影响到p2对象age属性的值。程序运行期间p1、p2引用的对象在内存中的状态如图3所示。
图3 P1、P2对象在内存中的状态
小提示:
在实际情况下,除了可以使用文件3-2中介绍的对象引用来访问对象成员外,还可以直接使用创建的对象本身来引用对象成员,具体格式如下:
new 类名().对象成员
这种方式是在通过new关键字创建实例对象的同时就访问了对象的某个成员,并且在创建后只能访问其中某一个成员,而不能像对象引用那样可以访问多个对象成员。同时,由于没有对象引用的存在,在完成某一个对象成员的访问后,该对象就会变成垃圾对象。所以,在实际开发中,创建实例对象时多数会使用对象引用。
在文件1中,通过“p1.age=18”将p1对象的age属性赋值为18,但并没有对p2对象的age属性进行赋值,按理说p2对象的age属性应该是没有值的。但从图2可以看出,p2对象的age属性也是有值的,其值为0。这是因为在实例化对象时,Java虚拟机会自动为成员变量进行初始化,针对不同类型的成员变量赋予不同的初始值,如表1所示。
表1 成员变量的初始化值
成员变量类型 | 初始值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0 |
float | 0.0 |
double | 0.0 |
char | 空字符,'\u0000' |
boolean | false |
引用数据类型 | null |
当对象被实例化后,在程序中可以通过对象的引用变量来访问该对象的成员。需要注意的是,当没有任何变量引用这个对象时,它将成为垃圾对象,不能再被使用。接下来通过两段程序代码来分析对象是如何成为垃圾的。
第一段程序代码:
{
Person p1 = new Person();
......
}
上面的代码中,使用变量p1引用了一个Person类型的对象。当这段代码运行完毕时,变量p1就会超出其作用域而被销毁,这时Person类型的对象将因为没有被任何变量所引用而变成垃圾。
第二段程序代码:
{
Person p2 = new Person();
......
p2 = null;
......
}
上面的代码中,使用变量p2引用了一个Person类型的对象,接着将变量p2的值置为null,则表示该变量不指向任何一个对象,被p2所引用的Person对象就会失去引用,成为垃圾对象,过程如图4所示。
图4 垃圾对象
访问控制符
在Java中,针对类、成员方法和属性提供了四种访问级别,分别是private、default、protected和public。接下来通过一个图将这四种控制级别由小到大依次列出,如图1所示。
图1 访问级别
图1中展示了Java中的四种访问控制级别,具体介绍如下:
● private(当前类访问级别):如果类的成员被private访问控制符来修饰,则这个成员只能被该类的其他成员访问,其他类无法直接访问。类的良好封装就是通过private关键字来实现的。
● default(包访问级别):如果一个类或者类的成员不使用任何访问控制符修饰,则称它为默认访问控制级别,这个类或者类的成员只能被本包中的其他类访问。
● protected(子类访问级别):如果一个类的成员被protected访问控制符修饰,那么这个成员既能被同一包下的其他类访问,也能被不同包下该类的子类访问。
● public(公共访问级别):这是一个最宽松的访问控制级别,如果一个类或者类的成员被public访问控制符修饰,那么这个类或者类的成员能被所有的类访问,不管访问类与被访问类是否在同一个包中。
接下来通过一个表将这四种访问级别更加直观的表示出来,如表1所示。
表1 访问控制级别
访问范围 | private | default | protected | public |
---|---|---|---|---|
同一类中 | √ | √ | √ | √ |
同一包中 | √ | √ | √ | |
子类中 | √ | √ | ||
全局范围 | √ |
小提示:
如果一个Java源文件中定义的所有类都没有使用public修饰,那么这个Java源文件的文件名可以是一切合法的文件名;如果一个源文件中定义了一个public修饰的类,那么这个源文件的文件名必须与public修饰的类的类名相同。
类的封装
为什么需要封装
在正式讲解如何实现类的封装之前,先通过一个程序来了解一下为什么需要对类进行封装,如文件1所示。
文件1 Example03.java
1 class Person{
2 String name;
3 int age;
4 public void speak(){
5 System.out.println("我叫"+name+",今年"+age+"岁了");
6 }
7 }
8 public class Example03 {
9 public static void main(String[] args) {
10 Person p = new Person();
11 p.name = "张三";
12 p.age = -18;
13 p.speak();
14 }
15 }
运行结果如图1所示。
图1 运行结果
在文件1的第12行代码中,将年龄赋值为一个负数-18,这在语法上不会有任何问题,因此程序可以正常运行,但在现实生活中明显是不合理的。为了避免出现这种不合理的问题,在设计一个Java类时,应该对成员变量的访问作出一些限定,不允许外界随意访问,这就需要实现类的封装。
如何实现封装
类的封装,是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息,而是通过该类所提供的方法来实现对内部信息的操作访问。
具体的实现过程是,在定义一个类时,将类中的属性私有化,即使用private关键字来修饰,私有属性只能在它所在类中被访问,如果外界想要访问私有属性,需要提供一些使用public修饰的公有方法,其中包括用于获取属性值的getXxx()方法和设置属性值的setXxx()方法。接下来通过对上一节的案例进行修改,来演示如何实现类的封装,如文件1所示。
文件1 Example04 .java
1 class Person{
2 private String name;
3 private int age;
4 public String getName() {
5 return name;
6 }
7 public void setName(String name) {
8 this.name = name;
9 }
10 public int getAge() {
11 return age;
12 }
13 public void setAge(int age) {
14 if(age <= 0){
15 System.out.println("您输入的年龄不正确!");
16 } else {
17 this.age = age;
18 }
19 }
20 public void speak(){
21 System.out.println("我叫"+name+",今年"+age+"岁了");
22 }
23 }
24 public class Example04 {
25 public static void main(String[] args) {
26 Person p = new Person();
27 p.setName("张三");
28 p.setAge(-18);
29 p.speak();
30 }
31 }
运行结果如图1所示。
图1 运行结果
文件1中,使用private关键字将属性name和age声明为私有变量,并对外界提供公有的访问方法,其中getName()方法用于获取name属性的值,setName()方法用于设置name属性的值,依次类推。
在main()方法中创建了Person类对象,并调用了setAge()方法传入一个负数-18,在setAge()方法中会对参数age的值进行检查,由于当前传入的值小于0,因此会打印“您输入的年龄不正确!”的信息。由于此时的age属性没有被赋值,所以仍为初始值0。
方法的重载和递归
方法的重载
假设要在程序中实现一个对数字求和的方法,由于参与求和数字的个数和类型都不确定,因此要针对不同的情况去设计不同的方法。接下来通过一个案例来实现对两个整数相加、对三个整数相加以及对两个小数相加的功能,具体实现如文件1所示。
文件1 Example05.java
1 public class Example05 {
2 // 1、实现两个整数相加
3 public static int add01(int x, int y) {
4 return x + y;
5 }
6 // 2、实现三个整数相加
7 public static int add02(int x, int y, int z) {
8 return x + y + z;
9 }
10 // 3、实现两个小数相加
11 public static double add03(double x, double y) {
12 return x + y;
13 }
14 public static void main(String[] args) {
15 // 针对求和方法的调用
16 int sum1 = add01(1, 2);
17 int sum2 = add02(3, 4, 7);
18 double sum3 = add03(0.2, 5.3);
19 //打印求和的结果
20 System.out.println("sum1=" + sum1);
21 System.out.println("sum2=" + sum2);
22 System.out.println("sum3=" + sum3);
23 }
24 }
运行结果如图1所示。
图1 运行结果
从文件1中的代码不难看出,程序需要针对每一种求和的情况都定义一个方法,如果每个方法的名称都不相同,在调用时就很难分清哪种情况该调用哪个方法。为了解决这个问题,Java允许在一个程序中定义多个名称相同,但是参数的类型或个数不同的方法,这就是方法的重载。
接下来以方法重载的方式对文件3-5进行修改,如文件2所示。
文件2 Example06.java
1 public class Example06 {
2 // 1、实现两个整数相加
3 public static int add(int x, int y) {
4 return x + y;
5 }
6 // 2、实现三个整数相加
7 public static int add(int x, int y, int z) {
8 return x + y + z;
9 }
10 // 3、实现两个小数相加
11 public static double add(double x, double y) {
12 return x + y;
13 }
14 public static void main(String[] args) {
15 // 针对求和方法的调用
16 int sum1 = add(1, 2);
17 int sum2 = add(3, 4, 7);
18 double sum3 = add(0.2, 5.3);
19 // 打印求和的结果
20 System.out.println("sum1=" + sum1);
21 System.out.println("sum2=" + sum2);
22 System.out.println("sum3=" + sum3);
23 }
24 }
文件2的运行结果和文件1一样,如图1所示。文件2中定义了三个同名的add()方法,但它们的参数个数或参数类型不同,从而实现了方法的重载。在main()方法中调用add()方法时,通过传入不同的参数便可以确定调用哪个重载的方法,如add(1,2)调用的是两个整数求和的方法add(int x, int y)。
需要注意的是,方法的重载与返回值类型无关,它只需要满足两个条件:一是方法名相同,二是参数个数或参数类型不同。
方法的递归
方法的递归是指在一个方法的内部调用自身的过程。递归必须要有结束条件,不然就会陷入无限递归的状态,永远无法结束调用。接下来通过一个案例来学习如何使用递归算法计算自然数之和,如文件1所示。
文件1 Example07.java
1 public class Example07 {
2 // 使用递归实现 求1~n的和
3 public static int getSum(int n) {
4 if (n == 1) {
5 // 满足条件,递归结束
6 return 1;
7 }
8 int temp = getSum(n - 1);
9 return temp + n;
10 }
11 public static void main(String[] args) {
12 int sum = getSum(4); // 调用递归方法,获得1~4的和
13 System.out.println("sum = " + sum); // 打印结果
14 }
15 }
运行结果如图1所示。
图1 运行结果
文件1中,定义了一个getSum()方法用于计算1~n之间的自然数之和。第8行代码相当于在getSum()方法的内部调用了自身,这就是方法的递归,整个递归过程在n==1时结束。
由于方法的递归调用过程很复杂,接下来通过一个图例来分析整个调用过程,如图2所示。
图2 递归调用过程
图2描述了文件1中整个程序的递归调用过程,其中getSum()方法被调用了4次,每次调用时,n的值都会递减。当n的值为1时,所有递归调用的方法都会以相反的顺序相继结束,所有的返回值会进行累加,最终得到的结果10。
构造方法
构造方法的定义
构造方法的语法格式与定义成员方法的语法格式相似,其语法格式如下:
[修饰符] 方法名 ([参数列表]){
// 方法体
}
上述语法格式所定义的构造方法需同时满足以下三个条件:
① 方法名与类名相同。
② 在方法名的前面没有返回值类型的声明。
③ 在方法中不能使用return语句返回一个值,但是可以单独写return语句来作为方法的结束。
了解了构造方法的定义语法后,接下来通过一个案例来演示如何在类中定义构造方法,如文件1所示。
文件1 Example08.java
1 class Person {
2 // 类的构造方法
3 public Person() {
4 System.out.println("调用了无参的构造方法");
5 }
6 }
7 public class Example08 {
8 public static void main(String[] args) {
9 Person p = new Person(); // 实例化Person 对象
10 }
11 }
运行结果如图1所示。
图1 运行结果
文件1中,Person类中定义了一个无参的构造方法Person()。从图1可以看出,Person类中无参的构造方法被调用了。这是因为第9行代码在通过“new Person()”实例化Person对象时会自动调用该类的构造方法。
在一个类中除了可以定义无参的构造方法外,还可以定义有参的构造方法,通过有参的构造方法就可以实现对属性的赋值。接下来对文件1进行改写,改写后的代码如文件2所示。
文件1 Example09.java
1 class Person {
2 // 声明int类型的变量age
3 int age;
4 // 定义有参构造方法
5 public Person(int a) {
6 age = a; //为age属性赋值
7 }
8 // 定义 speak() 方法
9 public void speak() {
10 System.out.println("我今年" + age + "岁了!");
11 }
12 }
13 public class Example09 {
14 public static void main(String[] args) {
15 Person p = new Person(18); // 实例化Person对象
16 p.speak();
17 }
18 }
运行结果如图2所示。
图2 运行结果
文件2中,Person类中定义了一个有参的构造方法Person(int a),第15行代码中的“new Person(18);”会调用有参的构造方法来实例化一个Person对象,并传入参数18,对age属性进行赋值。从图2可以看出,Person对象在调用speak()方法时,其age属性已经被赋值为18。
构造方法的重载
与普通方法一样,构造方法也可以重载,在一个类中可以定义多个构造方法,只要每个构造方法的参数类型或参数个数不同即可。在创建对象时,可以通过调用不同的构造方法来为不同的属性进行赋值。接下来通过一个案例来学习构造方法的重载,如文件1所示。
文件1 Example10.java
1 class Person {
2 // 声明String类型的变量name
3 String name;
4 // 声明int类型的变量age
5 int age;
6 // 定义有参构造方法
7 public Person(int a) {
8 age = a; // 为age属性赋值
9 }
10 public Person(String n,int a){
11 name = n; // 为name属性赋值
12 age = a; // 为age属性赋值
13 }
14 // 定义 speak() 方法
15 public void speak() {
16 System.out.println("我今年" + age + "岁了!");
17 }
18 // 定义say()方法
19 public void say(){
20 System.out.println("我叫"+name+",今年" + age + "岁了!");
21 }
22 }
23 public class Example10 {
24 public static void main(String[] args) {
25 Person p1 = new Person(18);
26 Person p2 = new Person("张三",32);
27 p1.speak();
28 p2.say();
29 }
30 }
运行结果如图1所示。
图1 运行结果
文件1中,Person类中定义了两个构造方法,它们实现了方法的重载。在创建p1对象和p2对象时,根据传入参数个数的不同,分别调用了不同的构造方法。从图1可以看出,两个构造方法对属性赋值的情况是不一样的,其中p1对象只对age属性进行赋值,在调用speak()方法后,输出年龄信息;而p2对象对name和age属性进行赋值,在调用say()方法后,会输出姓名和年龄信息。
脚下留心
①在Java中的每个类都至少有一个构造方法,如果在一个类中没有显示地定义构造方法,系统会自动为这个类创建一个默认的构造方法,这个默认的构造方法没有参数,在其方法体中没有任何代码,即什么也不做。
下面程序中Person类的两种写法效果是完全一样的。
第一种写法:
class Person {
}
第二种写法:
class Person {
public Person() {
}
}
对于第一种写法,类中虽然没有显示地声明构造方法,但仍然可以用new Person()语句来创建Person类的实例对象。由于系统提供的无参构造方法往往不能满足需求,因此,可以自己在类中定义构造方法,一旦为该类定义了构造方法,系统将不再提供默认的无参构造方法。
如果将文件1中实例化p1对象时传入的参数18删除,会发现Eclipse中出现了构造器没有定义的错误提示,如图2所示。
图2 错误信息
从图2可以看出,程序提示“The constructor Person() is undefined”,即构造器Person()未定义。其原因是调用new Person()创建Person类的实例对象时,需要调用无参的构造方法,而Person类中已经定义了两个有参的构造方法,此时系统将不再提供无参的构造方法,所以Person类中没有无参的构造方法。为了避免出现上面的错误,在一个类中如果定义了有参的构造方法,最好再定义一个无参的构造方法。
②思考一下,声明构造方法时,可以使用private访问修饰符吗?下面就来测试一下,看看会出现什么结果。
将文件1中定义的第一个有参构造的修饰符public修改为private后,会发现Eclipse中又出现了一个错误,如图3所示。
图3 运行结果
从图3可以看出,Eclipse中的错误提示为“The constructor Person(int) is not visible”,即构造方法Person(int)不可见。出现此错误的原因是被private访问控制符修饰的构造方法Person(int)只能在当前Person类中被访问无法在类的外部被访问,也就无法通过该私有构造方法来创建对象。因此,为了方便实例化对象,构造方法通常会使用public来修饰。
this关键字
在上一节案例中使用变量表示年龄时,构造方法中使用的参数是a,成员变量使用的是age,虽然在语法上没有任何问题,但这样的程序可读性很差。这时可以将Person类中表示年龄的变量进行统一命名,例如都声明为age,但是这样做又会导致成员变量和局部变量的名称冲突,在方法中将无法访问成员变量age。
为了解决这个问题,Java中提供了一个关键字this来指代当前对象,用于在方法中访问对象的其他成员。接下来将为读者详细地讲解this关键字在程序中的三种常见用法,具体如下:
1.通过this关键字调用成员变量,解决与局部变量名称冲突问题。具体示例代码如下:
class Person {
int age; // 成员变量age
public Person(int age) { // 局部变量age
this.age = age; // 将局部变量age的值赋给成员变量age
}
}
在上面的代码中,构造方法的参数被定义为age,它是一个局部变量,在类中还定义了一个成员变量,名称也是age。在构造方法中如果使用“age”,则是访问局部变量,但如果使用“this.age”则是访问成员变量。
2.通过this关键字调用成员方法,具体示例代码如下:
class Person {
public void openMouth() {
...
}
public void speak() {
this.openMouth();
}
}
在上面的speak()方法中,使用this关键字调用了openMouth()方法。需要注意的是,此处的this关键字可以省略不写,也就是说上面的代码中,写成“this.openMouth()”和“openMouth()”效果是完全一样的。
3.通过this关键字调用构造方法。构造方法是在实例化对象时被Java虚拟机自动调用的,在程序中不能像调用其他方法一样去调用构造方法,但可以在一个构造方法中使用“this([参数1,参数2…])”的形式来调用其他的构造方法。
接下来通过一个案例来演示这种形式构造方法的调用,如文件1所示。
文件1 Example11.java
1 class Person {
2 public Person() {
3 System.out.println("无参的构造方法被调用了...");
4 }
5 public Person(int age) {
6 this(); // 调用无参的构造方法
7 System.out.println("有参的构造方法被调用了...");
8 }
9 }
10 public class Example11 {
11 public static void main(String[] args) {
12 Person p = new Person(18); // 实例化 Person 对象
13 }
14 }
运行结果如图1所示。
图1 运行结果
在文件1中,第12行代码在实例化Person对象时,调用了有参的构造方法,在有参构造方法中又通过this()调用了无参的构造方法,因此运行结果中显示两个构造方法都被调用了。
在使用this调用类的构造方法时,应注意以下几点:
①只能在构造方法中使用this调用其他的构造方法,不能在成员方法中使用。
②在构造方法中,使用this调用构造方法的语句必须是该方法的第一条执行语句,且只能出现一次。下面的写法是错误的:
public Person(int age) {
System.out.println("有参的构造方法被调用了...");
this(); // 调用无参的构造方法。
}
在上述代码中,由于调用语句this()不是构造方法的第一条执行语句,所以Eclipse在编译时会报出“Constructor call must be the first statement in a constructor(调用构造函数必须是构造函数中的第一条语句)”的错误提示信息。
③不能在一个类的两个构造方法中使用this互相调用。下面的写法是错误的:
class Person {
public Person() {
this(18); // 调用有参的构造方法
System.out.println("无参的构造方法被调用了...");
}
public Person(int age) {
this(); // 调用无参的构造方法
System.out.println("有参的构造方法被调用了...")
}
}
在上述代码中,无参构造方法和有参构造方法分别使用了this关键字对方法进行了相互调用,此时在编译时,将会报出“Recursive constructor invocation Person()(递归调用构造函数Person())”的错误提示信息。
static关键字
静态变量
在定义一个类时,只是在描述某类事物的特征和行为,并没有产生具体的数据。只有通过new关键字创建该类的实例对象后,系统才会为每个对象分配内存空间,存储各自的数据。有时候,开发人员会希望某些特定的数据在内存中只有一份,而且能够被一个类的所有实例对象所共享。例如某个学校所有学生共享同一个学校名称,此时完全不必在每个学生对象所占用的内存空间中都声明一个变量来表示学校名称,而可以在对象以外的空间声明一个表示学校名称的变量,让所有对象来共享。具体内存中的分配情况如图1所示。
图1 静态变量内存分配图
从图1可以看出,所有学生对象共享一个名称为schoolName的变量。在一个Java类中,要实现这种功能可以使用static关键字来修饰成员变量,该变量被称作静态变量,它可以被所有实例所共享。
静态变量可以使用如下语法来访问:
类名.变量名
了解了静态变量的声明和访问方式后,接下来通过一个案例来实现图3-18所描述的情况,如文件1所示。
文件1 Example12.java
1 class Student {
2 static String schoolName; // 声明静态变量schoolName
3 }
4 public class Example12 {
5 public static void main(String[] args) {
6 Student stu1 = new Student(); // 创建第1个学生对象
7 Student stu2 = new Student(); // 创建第2个学生对象
8 Student.schoolName = "清华大学"; // 为静态变量赋值
9 // 分别输出两个学生对象的信息
10 System.out.println("我是" + stu1.schoolName+"的学生");
11 System.out.println("我是" + stu2.schoolName+"的学生");
12 }
13 }
运行结果如图2所示。
图2 运行结果
文件2中,Student类中定义了一个静态变量schoolName,用于表示学生所在的学校,它被所有的实例对象所共享。由于schoolName是静态变量,因此可以直接使用Student.schoolName的方式进行调用,也可以通过Student的实例对象进行调用,如stu2.schoolName。在第8行代码将变量schoolName赋值为“清华大学”后,学生对象stu1和stu2的schoolName属性值均为“清华大学”。
脚下留心:static关键字注意事项
static关键字只能用于修饰成员变量,不能用于修饰局部变量,否则编译会报错,例如下面的代码是非法的。
public class Student {
public void study() {
static int num = 10; // 这行代码是非法的,编译会报错
}
}
静态方法
通过前面的学习已经知道,如果想要使用类中的成员方法,就需要先将这个类实例化,而在实际开发时,开发人员有时会希望在不创建对象的情况下就可以调用某个方法,这种情况就可以使用静态方法。
静态方法的定义十分简单,只需要在类中定义的方法前加上static关键字即可。在使用时,静态方法可以通过如下两种方式来访问:
类名.方法
或
实例对象名.方法
了解了静态方法的定义和访问方式后,接下来通过一个案例来学习静态方法的使用,如文件1所示。
文件1 Example13.java
1 class Person {
2 public static void say() { // 定义静态方法
3 System.out.println("Hello!");
4 }
5 }
6 public class Example13 {
7 public static void main(String[] args) {
8 // “类名.方法”的方式调用静态方法
9 Person.say();
10 // 实例化对象
11 Person person = new Person();
12 // “实例对象名.方法”的方式来调用静态方法
13 person.say();
14 }
15 }
运行结果如图1所示。
图1 运行结果
在文件1中,首先在Person类中定义了静态方法say(),然后在main()方法中分别使用了两种方式来调用静态方法。在第9行代码处通过“Person.say()”的形式调用了静态方法,由此可见静态方法不需要创建对象就可以直接通过类名调用。在第13行代码处通过实例化对象的方式来调用静态方法,这说明通过实例化的对象的方式,同样可以调用静态方法。
注意:
在一个静态方法中只能访问用static修饰的成员,原因在于没有被static修饰的成员需要先创建对象才能访问,而静态方法在被调用时可以不创建任何对象。
静态代码块
在Java类中,使用一对大括号包围起来的若干行代码被称为一个代码块,用static关键字修饰的代码块称为静态代码块。静态代码块的语法如下:
static {
...
}
当类被加载时,静态代码块会执行,由于类只加载一次,因此静态代码块也只执行一次。在程序中,通常会使用静态代码块来对类的成员变量进行初始化。接下来通过一个案例来了解静态代码块的使用,如文件1所示。
文件1 Example14.java
1 class Person{
1 static {
2 System.out.println("执行了Person类中的静态代码块");
3 }
4 }
5 public class Example14{
6 static{
7 System.out.println("执行了测试类中的静态代码块");
8 }
9 public static void main(String[] args){
10 // 实例化2个Person对象
11 Person p1 = new Person();
12 Person p2 = new Person();
13 }
14 }
运行结果如图1所示。
图1 运行结果
从图1可以看出,程序中的两段静态代码块都执行了。使用Eclipse运行文件1中的程序后,Java虚拟机首先会加载类Example14,在加载类的同时就会执行该类的静态代码块,紧接着会调用main()方法。在main()方法中创建了两个Person对象,但在两次实例化对象的过程中,静态代码块中的内容只输出了一次,这就说明静态代码块在类第一次使用时才会被加载,并且只会加载一次。
未完,持续更新中。。。
自学java教程视频+资料+笔记,企鹅49.89.138.68