avatar


2.面向对象

这一章,我们讨论面向对象。
什么是面向对象呢?
来,看个视频。

面向对象编程\text{真}\cdot\text{面向对象编程}


当然,计算机中的面向对象,和这里的面向对象不一样。
在计算机中,面向对象,有三大特征:

  1. 封装
  2. 继承
  3. 多态

类和对象

第一个话题,类和对象。

什么是类

类是对一类具有共同属性行为的具体对象(事物)的抽象。

这里提到了属性行为

  • 属性:对象(事物)的特征。
    例如,对于手机而言,品牌、价格、尺寸这些就是属性。
  • 行为:对象(事物)能执行的操作。
    例如,对于手机而言,打电话、发短信这些就是功能。

类和对象的关系:类是对象的抽象,对象是类的实体。

在上一章,我们还讨论过数据类型,我们知道有基本数据类型和引用数据类型。引用数据类型分为三种,数组、类和接口。也就是说,类还是一种数据类型,是对象的数据类型。

类的格式

根据我们上文的讨论,我们知道,类是由属性和行为两部分组成。
那么在定义的过程中:

  • 属性,在类中通过成员变量来体现。
  • 行为,在类中通过成员方法来体现。

所以,定义一个类,其格式如下:

1
2
3
4
5
6
7
8
9
10
11
public class 类名 {
// 成员变量
变量1的数据类型 变量1;
变量2的数据类型 变量2;
...

// 成员方法
方法1;
方法2;
...
}

比如我们刚刚举的例子,手机。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

public class Phone {
// 成员变量
// 品牌 brand
String brand;
// 价格 price
int price;

// 成员方法
// 打电话 call
public void call() {
System.out.println("打电话");
}

// 发短信 sendMessage
public void sendMessage() {
System.out.println("发短信");
}
}

创建对象

类和数组一样,同属于引用数据类型,类是对象的数据类型,创建对象的格式与创建数组的其中一种格式很类似。

创建数组有三种格式,简化格式是:

1
数据类型[] 数组名 = {元素1,元素2,...};

非简化格式是:

1
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,...};

创建对象的格式就与这种非简化格式比较像。

1
类名 对象名 = new 类名();

调用对象中的成员

调用对象中成员的格式:

1
2
对象名.成员变量
对象名.成员方法();

我们来看个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.kakawanyifan;

public class PhoneDemo {
public static void main(String[] args) {
// 创建对象
// 类名 对象名 = new 类名();
Phone p = new Phone();

// 使用成员变量
// 对象名.变量名
System.out.println(p.brand);
System.out.println(p.price);

p.brand = "苹果";
p.price = 5999;

System.out.println(p.brand);
System.out.println(p.price);

// 使用成员方法
// 对象名.方法名()
p.call();
p.sendMessage();
}
}

运行结果:

1
2
3
4
5
6
null
0
苹果
5999
打电话
发短信

根据运行结果,我们发现,在最开始创建对象的时候,其brand属性的值是nullprice属性的值是0
解释一下。
《1.基础语法》,我们在讨论数组的时候,讨论过,数据类型的都有其默认初始化值。在数组中是这样,在对象中也是这样。
具体如下:

  • 整数:默认值 0
  • 浮点数:默认值 0.0
  • 布尔:默认值 false
  • 字符:默认值 空字符
  • 引用数据类型:默认值 null

对象内存结构

接下来,我们讨论对象的内存结构,实际上与数组的内存结构比较相似。
"左边"在栈内存,"右边"在堆内存。

单个对象内存结构

我们以这么一段代码为例。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.kakawanyifan;
/**
* 学生类
*/
public class Student {
//成员变量
String name;
int age;

//成员方法
public void study() {
System.out.println("好好学习,天天向上");
}

public void doHomework() {
System.out.println("键盘敲烂,月薪过万");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentTest01 {
public static void main(String[] args) {
//创建对象
Student s = new Student();
System.out.println(s);
//使用对象
System.out.println(s.name + "," + s.age);
s.name = "张曼玉";
s.age = 28;
System.out.println(s.name + "," + s.age);
s.study();
s.doHomework();
}
}

运行结果:

1
2
3
4
5
com.kakawanyifan.Student@1b6d3586
null,0
张曼玉,28
好好学习,天天向上
键盘敲烂,月薪过万

解释一下。

首先,从main方法开始,main方法进入栈内存。
然后执行第一行Student s = new Student()
所以在栈内存的main方法中会有Student s,在堆内存会有new Student()。引用类型的默认值是null,整数类型的默认值是0。
并且,栈内存中的s会指向堆内存中的new Student()
Student s = new Student()

然后执行System.out.println(s);,这时候输出的是内存地址。
System.out.println(s);

再执行System.out.println(s.name + "," + s.age);,输出"null,0"。
System.out.println(s.name + "," + s.age);

执行s.name = "张曼玉";
s.name = "张曼玉";

之后的s.age = 28;System.out.println(s.name + "," + s.age);都比较简单。
我们不赘述。

接下来执行s.study();
study()进入栈内存,调用对象是s,也就是main方法中内存地址是001的s
s.study()

s.study()执行完毕后,出栈,然后s.doHomework()再进栈,执行完毕也出栈。最后main方法出栈。
main()方法出栈

注意看,这时候我们的程序已经执行完毕了,但是在堆内存还有new Student()这个东西,这是一个没有任何引用的对象,我们也称之为无效对象。但是又占着内存,这个东西怎么处理呢?这个会由垃圾回收器自动回收。

多个对象内存结构

这个与我们之前讨论的"多个数组指向相同内存结构"非常相似。

多个对象指向不同的内存

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentTest02 {
public static void main(String[] args) {
//创建对象
Student s1 = new Student();
Student s2 = new Student();
System.out.println(s1);
System.out.println(s2);
//使用对象
s1.name = "林青霞";
s1.age = 30;
s2.name = "张曼玉";
s2.age = 28;
System.out.println(s1.name + "," + s1.age);
System.out.println(s2.name + "," + s2.age);
}
}

运行结果:

1
2
3
4
com.kakawanyifan.Student@1b6d3586
com.kakawanyifan.Student@4554617c
林青霞,30
张曼玉,28

main方法入栈,然后创建两个对象,指向不同的内存。其内存结构如图:
多个对象指向不同的内存

多个对象指向相同的内存

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentTest03 {
public static void main(String[] args) {
//创建对象
Student s1 = new Student();
//使用对象
s1.name = "林青霞";
s1.age = 30;
System.out.println(s1);
System.out.println(s1.name + "," + s1.age);
Student s2 = s1;
s2.name = "张曼玉";
s2.age = 28;
System.out.println(s1);
System.out.println(s2);
System.out.println(s1.name + "," + s1.age);
System.out.println(s2.name + "," + s2.age);
}
}

运行结果:

1
2
3
4
5
6
com.kakawanyifan.Student@1b6d3586
林青霞,30
com.kakawanyifan.Student@1b6d3586
com.kakawanyifan.Student@1b6d3586
张曼玉,28
张曼玉,28

解释一下。

main方法入栈,创建对象s1,使用对象s1
然后执行Student s2 = s1;s2s1指向同一个内存。
Student s2 = s1;

执行System.out.println(s1.name + "," + s1.age);System.out.println(s2.name + "," + s2.age);
输出"张曼玉,28"和"张曼玉,28"。
System.out.println(s1.name + "," + s1.age);

克隆

那么,如果我们想"复制一个Student呢"?实现Cloneable接口。

浅克隆

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kakawanyifan;
/**
* 学生类
*/
public class Student implements Cloneable {
//成员变量
String name;
int age;

//成员方法
public void study() {
System.out.println("好好学习,天天向上");
}

public void doHomework() {
System.out.println("键盘敲烂,月薪过万");
}

public Student Clone() throws CloneNotSupportedException{
return (Student) super.clone();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentTest03 {
public static void main(String[] args) throws CloneNotSupportedException {
//创建对象
Student s1 = new Student();
//使用对象
s1.name = "林青霞";
s1.age = 30;
System.out.println(s1);
System.out.println(s1.name + "," + s1.age);
Student s2 = s1.Clone();
s2.name = "张曼玉";
s2.age = 28;
System.out.println(s1);
System.out.println(s2);
System.out.println(s1.name + "," + s1.age);
System.out.println(s2.name + "," + s2.age);
}
}

运行结果:

1
2
3
4
5
6
com.kakawanyifan.Student@135fbaa4
林青霞,30
com.kakawanyifan.Student@135fbaa4
com.kakawanyifan.Student@45ee12a7
林青霞,30
张曼玉,28

解释说明:
在这里我们用的是浅克隆(shallow clone),拷贝对象不拷贝对象包含的引用指向的对象。与之对应的有深克隆(deep clone),不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。

我们来看具体现象。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.kakawanyifan;

public class Wife implements Cloneable {
private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Wife(int id,String name) {
this.id = id;
this.name = name;
}

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.kakawanyifan;

import com.kakawanyifan.Wife;

public class Husband implements Cloneable {
private int id;
private Wife wife;

public Wife getWife() {
return wife;
}

public void setWife(Wife wife) {
this.wife = wife;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public Husband(int id) {
this.id = id;
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}


/**
* @param args
* @throws CloneNotSupportedException
*/
public static void main(String[] args) throws CloneNotSupportedException {
Wife wife = new Wife(1,"X");
Husband husband = new Husband(1);
husband.setWife(wife);
Husband husband2 = (Husband) husband.clone();
System.out.println(husband);
System.out.println(husband2);
System.out.println(husband.wife);
System.out.println(husband2.wife);
}
}

运行结果:

1
2
3
4
com.kakawanyifan.Husband@135fbaa4
com.kakawanyifan.Husband@45ee12a7
com.kakawanyifan.Wife@330bedb4
com.kakawanyifan.Wife@330bedb4

两个Husband的确地址不一样,不是同一个,但是wife居然地址一样,是同一个。

深克隆

解决办法为深克隆,我们看具体的例子。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.kakawanyifan;

public class Wife implements Cloneable {
private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Wife(int id,String name) {
this.id = id;
this.name = name;
}

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.kakawanyifan;

public class Husband implements Cloneable {
private int id;
private Wife wife;

public Wife getWife() {
return wife;
}

public void setWife(Wife wife) {
this.wife = wife;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public Husband(int id) {
this.id = id;
}

@Override
protected Object clone() throws CloneNotSupportedException {
Husband husband = (Husband) super.clone();
husband.wife = (Wife) husband.getWife().clone();
return husband;
}


/**
* @param args
* @throws CloneNotSupportedException
*/
public static void main(String[] args) throws CloneNotSupportedException {
Wife wife = new Wife(1,"jin");
Husband husband = new Husband(1);
husband.setWife(wife);
Husband husband2 = (Husband) husband.clone();
System.out.println(husband);
System.out.println(husband2);
System.out.println(husband.wife);
System.out.println(husband2.wife);
}
}

运行结果:

1
2
3
4
com.kakawanyifan.Husband@135fbaa4
com.kakawanyifan.Husband@45ee12a7
com.kakawanyifan.Wife@330bedb4
com.kakawanyifan.Wife@2503dbd3

注意huaband的clone()方法。

成员变量和局部变量

在上文讨论类的定义的时候,我们说类的属性是通过成员变量来体现。
在一个类中,除了有成员变量,还有局部变量。
成员变量是指在类中,且在方法的变量。
局部变量是指在类中,且在方法的变量。

我们举个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Student {
//成员变量
String name;
int age;

//成员方法
public void study() {
// 局部变量
int i = 1;
System.out.println("好好学习,天天向上");
}

public void doHomework() {
// 局部变量
int j = 1;
System.out.println("键盘敲烂,月薪过万");
}
}
  • nameage是成员变量。
  • ij是局部变量。

成员变量和局部变量的区别:

  1. 类中位置不同:
    成员变量在类中,但在类的方法外。
    局部变量在类的方法内部或方法声明上。(方法声明指的是方法的形参上)
  2. 生命周期不同:
    成员变量随着对象的存在而存在,随着对象的消失而消失。
    局部变量随着方法的调用而存在,随着方法的调用完毕而消失。
  3. 初始化值不同:
    成员变量有默认初始化值。
    局部变量没有默认初始化值,必须先定义再赋值才能使用。

有一种方法是说,内存中位置不同,成员变量在堆内存,局部变量在栈内存。我认为这个有待商榷,如果是引用类型的局部变量,我认为也会在堆内存中,然后栈内存有东西会指向堆内存的地址。

封装

在讨论了类和对象之后,我们来讨论面向对象的第一个特点,封装。

private关键字

从我们最常见的一个关键字"private"开始。
"private"是一个权限修饰符,作用是修饰成员变量和成员方法,也就和这个关键字的含义一样,私有的,保护成员不被别的类使用。被"private"修饰的成员,只有在本类中才可以访问。

现在,我们为上文的Student类的成员变量age添加修饰符private
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;
/**
* 学生类
*/
public class Student {
//成员变量
String name;
// private
private int age;

//成员方法
public void show() {
System.out.println(name + "," + age);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentDemo {
public static void main(String[] args) {
//创建对象
Student s = new Student();
System.out.println(s);
//使用对象
s.name = "张曼玉";
s.age = 28;
s.show();
}
}

运行结果:

1
java: age 在 com.kakawanyifan.Student 中是 private 访问控制

报错就对了。
这印证了我们说的那句话,被"private"修饰的成员,只有在本类中才可以访问。
ageStudent类中是私有的,在StudentDemo类中访问就报错了。

那么,如果需要被其他类使用怎么办?
额外提供相应的操作。

  • 提供get变量名()方法,用于获取成员变量的值,方法用public修饰。
  • 提供set变量名(参数)方法,用于设置成员变量的值,方法用public修饰。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.kakawanyifan;
/**
* 学生类
*/
public class Student {
//成员变量
private String name;
// private
private int age;

public String getName() {
return name;
}

public void setName(String n) {
name = n;
}

public int getAge() {
return age;
}

public void setAge(int a) {
age = a;
}

//成员方法
public void show() {
System.out.println(name + "," + age);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentDemo {
public static void main(String[] args) {
//创建对象
Student s = new Student();
//使用对象
s.setName("张曼玉");
s.setAge(28);
s.show();
}
}

运行结果:

1
张曼玉,28

this关键字

关于封装,我们讨论的第二个关键字是this

this的用法

我们直接来看个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.kakawanyifan;
/**
* 学生类
*/
public class Student {
//成员变量
private String name;
// private
private int age;

public void setName(String name) {
name = name;
}

public String getName() {
return name;
}

public void setAge(int age) {
this.age = age;
}

public int getAge() {
return age;
}

//成员方法
public void show() {
System.out.println(name + "," + age);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentDemo {
public static void main(String[] args) {
//创建对象
Student s = new Student();
//使用对象
s.setName("张曼玉");
s.setAge(28);
s.show();
}
}

运行结果:

1
null,28

解释一下。
setName()方法中,我们写的是name = name。这时候不带this的变量name,是指形参name,是局部变量name,而不是成员变量name。而在setAge()方法中,我们写的是this.age = age,这里的this.age指的是成员变量age

方法的形参如果与成员变量同名,不带this修饰的变量指的是形参,而不是成员变量。

this代表当前调用方法的引用,哪个对象调用的方法,this就代表哪一个对象。

this的内存结构

我们以这段代码为例。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.kakawanyifan;
/**
* 学生类
*/
public class Student {
//成员变量
private String name;
// private
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentDemo {
public static void main(String[] args) {
Student s1 = new Student();
s1.setName("林青霞");
System.out.println(s1.getName() + "," + s1.getAge());

Student s2 = new Student();
s2.setName("张曼玉");
System.out.println(s2.getName() + "," + s2.getAge());
}
}

运行结果:

1
2
林青霞,0
张曼玉,0

解释一下。

main方法入栈。
执行Student s1 = new Student()
s1.setName("林青霞");
setName()方法入栈,调用者是s1,执行的操作是this.name = name;
this代表当前调用方法的引用,哪个对象调用的方法,this就代表哪一个对象。
这里的this就是调用者s1
s1.setName("林青霞");
setName()方法在执行完毕后出栈。

main方法继续。
Student s2 = new Student();
s2.setName("张曼玉");
setName()方法入栈,调用者是s2,执行的操作是this.name = name;,这里的this就是调用者s2
s2.setName("张曼玉");
setName()方法在执行完毕后出栈。
最后,main方法出栈。

构造方法

在上述的操作中,我们是先创建一个对象,然后调用setXxx()方法赋予我们希望的值。
我们有没有方法在创建对象的同时,把对象数据初始化为我们希望的值呢?
这就是构造方法的功能。

构造方法的使用

方法名就是类名,格式如下:

1
2
3
4
5
public class 类名{
修饰符 类名(参数) {

}
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.kakawanyifan;
/**
* 学生类
*/
public class Student {
private String name;
private int age;

public Student(String name,int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentDemo {
public static void main(String[] args) {
Student s1 = new Student("林青霞",28);
Student s2 = new Student("张曼玉",30);
System.out.println(s1.getName());
System.out.println(s2.getName());
}
}

运行结果:

1
2
林青霞
张曼玉

构造方法的注意

构造方法的创建:

  • 如果没有定义构造方法,系统将给出一个默认的无参数构造方法。
  • 如果定义了构造方法,系统将不再提供默认的构造方法。

构造方法的重载:

  • 如果自定义了带参构造方法,还要使用无参数构造方法,那怎么办呢?构造方法的重载,再写一个无参数构造方法。

建议的使用方式:无论是否使用,都手工书写无参数构造方法。
(为什么这么建议?就在本章,我们讨论"继承"的时候,就会解释。在《15.Spring Framework [1/2]》讨论Bean的创建方式-构造方法的部分,也会看到。)

封装小结

在上文,不论是"private"也好,"this"也好。其实我们都是在做一件事情:将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。
我们把成员变量设置为private,提供对应的getXxx()/setXxx()方法,或者提供构造方法。这么做的好处是:通过方法来控制成员变量的操作,提高了代码的安全性。
这也是面向对象编程语言对客观世界的模拟,客观世界里成员变量都是隐藏在对象内部的,外界是无法直接操作的。

内部类

在讨论了"类和对象"和"封装"之后,我们来讨论内部类。
这这里,我们将会看到封装思想的实践。

内部类概述

内部类概念:在一个类中定义另一个类。
例如,在一个类A的内部定义一个类B,类B就被称为内部类。

内部类定义格式:

1
2
3
4
5
class 外部类名{
修饰符 class 内部类名{

}
}

内部类的访问特点:

  1. 内部类可以直接访问外部类的成员,包括私有。
  2. 外部类要访问内部类的成员,必须创建对象。

举个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

public class Outer {
private int num = 10;
public class Inner {
public void show() {
System.out.println(num);
}
}
public void method() {
Inner i = new Inner();
i.show();
}
}

在上文,我们讨论过,变量在类中的位置不同,可以分为成员变量和局部变量。
根据内部类在类中的位置不同,内部类可以分为:

  1. 成员内部类,在类的成员位置。
  2. 局部内部类,在类的局部位置。

我们分别讨论。

成员内部类

成员内部类的定义位置:和成员变量是一样的位置。

外界实例化成员内部类格式:

1
外部类名.内部类名 对象名 = 外部类对象.内部类对象;

等等!我们刚刚讨论过封装!
我们将一个类,设计为内部类的目的是什么?
是为了封装!
是不想让外界去访问!
所以内部类的定义应该私有化,私有化之后,再提供一个可以让外界调用的方法,方法内部创建内部类对象并调用。

所以,上述那个格式,我们不建议!
正确用法如下。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

public class Outer {
private int num = 10;
private class Inner {
public void show() {
System.out.println(num);
}
}
public void method() {
Inner i = new Inner();
i.show();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

public class InnerDemo {
public static void main(String[] args) {
// 不建议
// Outer.Inner oi = new Outer().new Inner();
// oi.show();
// 正确方法
Outer o = new Outer();
o.method();
}
}

运行结果:

1
10

局部内部类

局部内部类定义位置:局部内部类是在方法中定义的类。

局部内部类的特点:

  • 外界无法直接使用,需要在方法内部创建对象并使用。
  • 该类可以直接访问外部类的成员,也可以访问方法内的局部变量。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan;

public class Outer {
private int num = 10;
public void method() {
int num2 = 20;
class Inner {
public void show() {
System.out.println(num);
System.out.println(num2);
}
}
Inner i = new Inner();
i.show();
}
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class OuterDemo {
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
}

运行结果:

1
2
10
20

继承

我们继续讨论面向对象。
第二个特征:继承。
使得子类具有父类的属性和方法。此外,还可以在子类中重新定义,和追加属性和方法。

  • 也有一些资料把"父类"称之为"基类"或者"超类",把"子类"称之为"派生类"。

继承的实现

在Java中,继承通过extends实现,格式如下:

1
class 子类 extends 父类 { }

看个例子。
示例代码:

1
2
3
4
5
6
7
package com.kakawanyifan;

public class Fu {
public void show() {
System.out.println("show方法被调用");
}
}
1
2
3
4
5
6
7
package com.kakawanyifan;

public class Zi extends Fu{
public void method() {
System.out.println("method方法被调用");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Fu f = new Fu();
f.show();

Zi z = new Zi();
z.show();
z.method();
}
}

运行结果:

1
2
3
show方法被调用
show方法被调用
method方法被调用

继承的优缺点

就像每一枚硬币都有正反两面,继承这个特性,“一箭双雕”,既有优点,也有缺点。
优点是:

  1. 提高了代码的复用性
    因为:多个类相同的成员可以放到同一个类中。
  2. 提高了代码的维护性
    因为:如果方法的代码需要修改,修改一处即可。

缺点是:让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时子类实现也不得不跟着变化,削弱了子类的独立性。

正因为如此,所以,使用继承,需要考虑类与类之间是否真的存在继承关系,不能盲目使用继承。

我是你爸爸

继承中的成员访问

接下来,我们讨论继承中的成员访问。
就近原则
顺序如下:

  1. 子类局部范围找
  2. 子类成员范围找
  3. 父类成员范围找
  4. 如果都没有就报错(不考虑父亲的父亲…)

当然,上述是说变量,对于方法,没有所谓的"子类局部范围"。

super关键字

如果我们不想遵守上述就近原则,比如我们一定要访问子类的成员变量,怎么办?
在上一章《2.面向对象之封装》,我们讨论过"this"关键字。this代表当前调用方法的引用,哪个对象调用的方法,this就代表哪一个对象。所以用"this"关键字,可以实现。

那么,如果我们一定要访问父类的成员变量呢?
super

看个例子。
示例代码:

1
2
3
4
5
package com.kakawanyifan;

public class Fu {
int num = 10;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;

public class Zi extends Fu{
int num = 20;

public void show(){
int num = 30;
System.out.println(num);
// 访问本类的成员变量
System.out.println(this.num);
// 访问父类的成员变量
System.out.println(super.num);

}
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Zi z = new Zi();
z.show();
}
}

运行结果:

1
2
3
30
20
10

解释:

  • this:代表本类对象的引用
  • super:可以理解为父类对象引用

this和super的使用区别:

  • 对于成员变量:
    this.成员变量:访问本类成员变量
    super.成员变量:访问父类成员变量
  • 对于成员方法:
    this.成员方法:访问本类成员方法
    super.成员方法:访问父类成员方法
  • 对于构造方法:
    this(…):访问本类构造方法
    super(…):访问父类构造方法

继承中构造方法的访问

刚刚我们讨论了this和super对于构造方法的区别,在这里就会看到实践。

直接看例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
package com.kakawanyifan;

public class Fu {
public Fu() {
System.out.println("Fu的无参构造方法被调用");
}

public Fu(int num) {
System.out.println("Fu的含参构造方法被调用 " + num);
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.kakawanyifan;

public class Zi extends Fu{
public Zi() {
System.out.println("Zi的无参构造方法被调用");
}

public Zi(int num) {
System.out.println("Zi的含参构造方法被调用 " + num);
}
}
1
2
3
4
5
6
7
8
9
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Zi z1 = new Zi();
System.out.println("------");
Zi z2 = new Zi(1);
}
}

运行结果:

1
2
3
4
5
Fu的无参构造方法被调用
Zi的无参构造方法被调用
------
Fu的无参构造方法被调用
Zi的含参构造方法被调用 1
  • 注意:Fu的无参构造方法被调用居然输出了,还是两次。

解释一下。
子类中所有的构造方法默认都会访问父类中无参的构造方法。

原因是:
子类会继承父类中的数据,而且可能还会使用父类的数据。所以,子类初始化之前,一定会先完成父类数据的初始化。所以呢,会访问父类的构造方法。

那么,为什么是无参构造方法呢?
因为每一个子类构造方法的第一条语句默认都是:super(),访问父类的无参构造方法。
特别的,如果父类中没有无参构造方法呢?直接报错。
直接报错

那么,对于这种父类中没有无参构造方法,怎么办呢?

  1. 把父类的无参构造方法加回来【推荐】。
  2. 通过super()指定父类的含参构造方法。

这也是为什么在上文讨论封装的时候,我们建议无论是否使用,都手工书写无参数构造方法。每次都手工写无参构造方法,就省的下次把父类的无参构造方法加回来。

关于"把父类的无参构造方法加回来",这个很简单。
我们来看一下"通过super()指定父类的含参构造方法",方法的重载。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

public class Fu {

// public Fu() {
// System.out.println("Fu的无参构造方法被调用");
// }

public Fu(int num) {
System.out.println("Fu的含参构造方法被调用 " + num);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

public class Zi extends Fu{
public Zi() {
super(1);
System.out.println("Zi的无参构造方法被调用");
}

public Zi(int num) {
super(1);
System.out.println("Zi的含参构造方法被调用 " + num);
}
}
1
2
3
4
5
6
7
8
9
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Zi z1 = new Zi();
System.out.println("------");
Zi z2 = new Zi(1);
}
}

运行结果:

1
2
3
4
5
Fu的含参构造方法被调用 1
Zi的无参构造方法被调用
------
Fu的含参构造方法被调用 1
Zi的含参构造方法被调用 1

继承的内存结构

为了加深我们对继承的理解,我们来看看继承到底做了什么。
继承的内存结构。
我们以这么一段代码为例。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

public class Fu {
int age = 40;
public Fu() {
System.out.println("Fu的无参构造方法被调用");
}

public void method(){
System.out.println("Fu的method被调用");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;

public class Zi extends Fu{
int age = 20;
public Zi() {
System.out.println("Zi的无参构造方法被调用");
}

public void show() {
int age = 30;
System.out.println(age);
System.out.println(this.age);
System.out.println(super.age);
}
}
1
2
3
4
5
6
7
8
9
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Zi z = new Zi();
z.show();
z.method();
}
}

运行结果:

1
2
3
4
5
6
Fu的无参构造方法被调用
Zi的无参构造方法被调用
30
20
40
Fu的method被调用

解释一下。

首先main方法进栈,然后执行Zi z = new Zi(),调用Zi类的构造方法,Zi类的构造方法进栈。
Zi z = new Zi()

这时候需要注意了,首先执行的是super()
所以,Fu类先进行初始化,堆内存中会多一块super区域。然后,进行Fu的无参构造方法。
构造方法Fu()进栈。
输出"Fu的无参构造方法被调用"。
Fu()方法出栈。
继续执行,输出"Zi的无参构造方法被调用",Zi类的构造方法Zi()也出栈。
super()

执行z.show()show()方法进栈,依次执行
int age = 30;
System.out.println(age)

System.out.println(this.age)
输出"20"
System.out.println(this.age)

System.out.println(super.age);
输出"40"
System.out.println(super.age)

show()方法出栈。

执行z.method();,Zi类中没有,去父类中找。
z.method()

最后method方法出栈,main方法出栈。

方法重写

在上文我们讨论了。继承的概念:使得子类具有父类的属性和方法。此外,还可以在子类中重新定义,和追加属性和方法。
关于追加属性和方法,我们已经见过了。重新定义属性,这个也见过了。在上文,Fu类有成员变量num,然后子类也有成员变量num。
那么,可不可以重新定义方法呢?
这就是方法重写。

方法重写的实现

方法重写概念:子类出现了和父类中一模一样的方法声明(方法名一样,参数列表也一样)。
方法重写的应用场景:当子类需要父类的功能,而功能主体子类有自己特有内容时,可以重写父类中的方法,这样,即沿袭了父类的功能,又定义了子类特有的内容。
方法重写需要@Override注解:用来检测当前的方法,是否是重写的方法,起到校验的作用。

示例代码:

1
2
3
4
5
6
7
package com.kakawanyifan;

public class Fu {
public void show() {
System.out.println("Fu中show()方法被调用");
}
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class Zi extends Fu {
@Override
public void show() {
System.out.println("Zi中show()方法被调用");
}
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Zi z = new Zi();
z.show();
}
}

运行结果:

1
Zi中show()方法被调用

方法重写的注意

两点注意:

  1. 私有方法不能被重写(父类私有成员子类是不能继承的)
  2. 子类方法访问权限不能更低(public > 默认 > 私有)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
package com.kakawanyifan;

public class Fu {
private void show() {
System.out.println("Fu中show()方法被调用");
}

void method() {
System.out.println("Fu中method()方法被调用");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.kakawanyifan;

public class Zi extends Fu{

// 编译【出错】,子类不能重写父类私有的方法
/*
@Override
private void show() {
System.out.println("Zi中show()方法被调用");
}
*/

// 编译【出错】,子类重写父类方法的时候,访问权限需要大于等于父类
/*
@Override
private void method() {
System.out.println("Zi中method()方法被调用");
}
*/

/* 编译【通过】,子类重写父类方法的时候,访问权限需要大于等于父类 */
@Override
public void method() {
System.out.println("Zi中method()方法被调用");
}
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Zi z = new Zi();
z.method();
}
}

运行结果:

1
Zi中method()方法被调用

继承的注意

最后,我们讨论一下继承的注意。

  1. 一次只能继承一个,不能同时继承多个。错误范例:class A extends B, C { }
  2. 可以多层继承

注意"同时继承多个"和"多层继承"。

举一个多层继承的例子。
示例代码:

1
2
3
4
5
6
7
package com.kakawanyifan;

public class Ye {
public void smoke() {
System.out.println("抽烟");
}
}
1
2
3
4
5
6
7
package com.kakawanyifan;

public class Fu extends Ye {
public void drink() {
System.out.println("喝酒");
}
}
1
2
3
4
5
6
7
package com.kakawanyifan;

public class Zi extends Fu{
public void perm() {
System.out.println("烫头");
}
}
1
2
3
4
5
6
7
8
9
10
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Zi z = new Zi();
z.smoke();
z.drink();
z.perm();
}
}

运行结果:

1
2
3
抽烟
喝酒
烫头

题外话:《Python、JavaScript、Git和ffmpeg:6.Python [2/2]》,我们讨论过,在Python中,只有一个类可以同时继承多个类,即多继承。

修饰符

在讨论了继承之后,我们来讨论一下修饰符。

在之前的内容中,我们其实已经用到了修饰符。有public,有private,上文我们讨论封装的时候,就是从private开始讨论的。
现在,我们更系统的讨论一下修饰符。

Java中的修饰符分为三类:

  1. 权限修饰符
  2. 状态修饰符
  3. 抽象修饰符

这里我们主要讨论前两类。
(关于抽象修饰符,我们会在本章讨论抽象类的时候讨论。)

但在讨论修饰符之前,我们要先讨论一下包,因为很快我们就会看到权限修饰符和包的关系。

包的概念:包就是文件夹,用来管理类文件的。
包的格式:package 包名;,多级包用.分开,例如:java.util.Scanner;
导包的格式:格式:import 包名;。例如:import java.util.Scanner;
导包的意义:使用不同包下的类时,使用的时候要写类的全路径,写起来太麻烦了。为了简化操作,Java提供了导包的功能。

我们来比较一下"没有导包"和"有导包"。

没有导包:
示例代码:

1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
int i = new java.util.Random().nextInt();
System.out.println(i);
}
}

有导包:
示例代码:

1
2
3
4
5
6
7
8
9
10
package com.kakawanyifan;

import java.util.Random;

public class Demo {
public static void main(String[] args) {
int i = new Random().nextInt();
System.out.println(i);
}
}

权限修饰符

在讨论了什么是包之后,我们来讨论第一种修饰符,权限修饰符。
权限修饰符有四种。

  1. private
  2. 不写修饰符,默认。
  3. protected
  4. public

我们举几个例子。

例子一。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.kakawanyifan;

public class Fu {
private void show_private(){
System.out.println("show_private");
}

void show_default(){
System.out.println("show_default");
}

protected void show_protected(){
System.out.println("show_protected");
}

public void show_public(){
System.out.println("show_public");
}

public static void main(String[] args) {
Fu f = new Fu();
f.show_private();
f.show_default();
f.show_protected();
f.show_public();
}
}

运行结果:

1
2
3
4
show_private
show_default
show_protected
show_public

结论:在同一个类中,四种修饰符修饰的都可以被访问。

例子二。
示例代码:

1
2
3
4
5
6
7
8
9
10
package com.kakawanyifan;

public class Zi extends Fu {
public static void main(String[] args) {
Zi z = new Zi();
z.show_default();
z.show_protected();
z.show_public();
}
}

运行结果:

1
2
3
show_default
show_protected
show_public

结论:在同一个包的子类中,不能访问private修饰的。
(因为private不能被继承)

例子三。
示例代码:

1
2
3
4
5
6
7
8
9
10
package com.kakawanyifan;

public class Demo {
public static void main(String[] args) {
Fu f = new Fu();
f.show_default();
f.show_protected();
f.show_public();
}
}

运行结果:

1
2
3
show_default
show_protected
show_public

结论:在同一个包的无关类中,不能访问private修饰的。

例子四。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
package com.kakawanyifan.m;

import com.kakawanyifan.Fu;

public class Zi extends Fu {
public static void main(String[] args) {
Zi z = new Zi();
z.show_protected();
z.show_public();
}
}

运行结果:

1
2
show_protected
show_public

结论:在不同包的子类中,只能访问protected和public修饰的。

例子五。
示例代码:

1
2
3
4
5
6
7
8
9
10
package com.kakawanyifan.m;

import com.kakawanyifan.Fu;

public class Demo {
public static void main(String[] args) {
Fu f = new Fu();
f.show_public();
}
}

运行结果:

1
show_public

结论:在不同包的无关类中,只能访问public修饰的。

继承的权限

上文讨论的,其实就是继承的权限,我们画成一个表格,如下。

private 默认 proteced public
同一个类
同一个包的子类
同一个包的无关类
不同包的子类
不同包的无关类

大家都是一个类的话,就无所谓什么private,不private了。但是如果不是一个类了,但还在同一个包下,那private就不行了。如果还出包了,那就看有没有"亲戚"关系,如果有的话,还可以访问protected的,如果"亲戚"关系都没有了,那就只有public了。

状态修饰符

接下来,我们讨论状态修饰符。

状态修饰符有两个。

  1. final
  2. static

final

fianl修饰符的作用:final代表最终的,“不可修改的”。

final可以修饰类、方法和变量。

  • fianl修饰类:该类不能被继承(不能有子类,但是可以有父类)。
  • final修饰方法:该方法不能被重写。
  • final修饰变量:表明该变量是一个常量,不能再次赋值。

在上文,我们对"不可修改的",打了引号。因为不是严格意义上的不可修改。

虽然只要是final修饰的变量,就是不能再次进行赋值了,但是对于修饰的是基本数据类型的变量还是引用数据类型的变量,却还是有区别。

  • fianl修饰基本数据类型变量:final修饰指的是基本类型的数据值不能发生改变。
  • final修饰引用数据类型变量:final修饰指的是引用类型的地址值不能发生改变,但是地址里面的内容是可以发生改变的。

这就是我们对"不可修改的"打引号的原因。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.kakawanyifan;

public class Student {
private String name;
private int age;

public Student(String name,int age) {
this.name = name;
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setAge(int age) {
this.age = age;
}

public int getAge() {
return age;
}

public void show() {
System.out.println(name + "," + age);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

public class StudentDemo {
public static void main(String[] args) {
final Student s = new Student("林青霞",23);
s.show();
// 错误
// s = new Student("张曼玉",24);
// 正确
s.setAge(32);
s.show();
}
}

运行结果:

1
2
林青霞,23
林青霞,32

static

static的概念:static关键字是静态的意思,可以修饰"成员方法"和"成员变量"。
static的特点:被类的所有对象共享,这也是我们判断是否使用静态关键字的条件。
被static修饰的,被访问的特点:可以直接通过类名访问,因为static修饰的含义是说这是类的变量。(当然,也可以通过对象名访问)。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;
/**
* 学生类
*/
public class Student {
public String name;
public int age;
//学校 共享数据 静态
public static String school;

public void show() {
System.out.println(name + "," + age + "," + school);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.kakawanyifan;
/**
* 学生测试类
*/
public class StudentDemo {
public static void main(String[] args) {
Student.school= "某学校";

Student s1 = new Student();
s1.name = "林青霞";
s1.age = 30;
s1.show();

Student s2 = new Student();
s2.name = "张曼玉";
s2.age = 28;
s2.show();
}
}

运行结果:

1
2
林青霞,30,某学校
张曼玉,28,某学校

被static修饰的,访问其他的特点:只能访问其他的被static修饰的。

即:
非静态的成员方法:

  1. 能访问静态的成员变量
  2. 能访问非静态的成员变量
  3. 能访问静态的成员方法
  4. 能访问非静态的成员方法

静态的成员方法:

  1. 能访问静态的成员变量
  2. 能访问静态的成员方法

总结成一句话就是:静态成员方法只能访问静态成员方法和变量。

多态

我们继续讨论面向对象。
最后一个特征:多态。

什么是多态

同一个对象,在不同时刻表现出来的不同形态。即:多态研究的是对象的不同形态。

多态的前提:

  1. 要有继承或实现关系
    (关于什么是实现,我们会在下文讨论接口的时候讨论。)
  2. 要有方法的重写
  3. 要有父类引用指向子类对象

我们直接看例子。
示例代码:

1
2
3
4
5
6
7
8
9
package com.kakawanyifan;

public class Animal {
public int age = 40;

public void eat() {
System.out.println("动物吃东西");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;

public class Cat extends Animal {
public int age = 20;
public int weight = 10;

@Override
public void eat() {
System.out.println("猫吃鱼");
}

public void playGame() {
System.out.println("猫捉迷藏");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan;

public class AnimalDemo {
public static void main(String[] args) {
// 有父类引用指向子类对象
Animal a = new Cat();
System.out.println(a.age);
// 编译不过
// System.out.println(a.weight);

a.eat();

// 编译不过
// a.playGame();
}
}

运行结果:

1
2
40
猫吃鱼
  • Cat类继承自Animal类,这个是有继承关系。
  • Cat类重写了eat()方法,这个是有重写。
  • 我们创建的对象的确是Cat,但是,我们却有父类Animal的引用指向了创建的Cat对象。这个是有父类引用指向子类对象。

多态中的成员访问

我们再看看我们刚刚的代码。

对于成员变量"a.weight",编译的时候就直接报错,这是因为左边Animal父类就没有weight这个成员变量。而对于"age",编译的确可以过,左边Animal父类有age这个成员变量。不过,需要注意的是,打印的并不是右边Cat的20,而是左边Animal的40。
这是因为对于成员变量,运行也看左边父类。

而对于成员方法"a.playGame()“,直接编译不过。这是因为左边Animal父类就没有playGame()这个方法。而对于"a.eat()”,编译的确可以过,左边Animal父类有playGame()这个成员方法。不过与刚刚不同的是,运行的时候,运行的是右边Cat的方法。

总结成员访问特点:
成员变量:编译看左边,运行看左边。
成员方法:编译看左边,运行看右边。

多态的优缺点

优点:提高程序的扩展性。定义方法时候,使用父类型作为参数,在使用的时候,使用具体的子类型参与操作。
缺点:不能使用子类的特有成员。

多态中的转型

正如上文所属,多态的缺点是不能使用子类的特有成员。但是,如果我们一定要使用子类的特有成员呢?这就是多态中的转型。
向上转型:从子到父,父类引用指向子类对象。
向下转型:从父到子,父类引用转为子类对象。

向下转型的格式:

1
子类型 对象名 = (子类型)父类引用;

我们来看一个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan;

public class AnimalDemo {
public static void main(String[] args) {
// 多态
// 向上转型:从子到父,父类引用指向子类对象。
Animal a = new Cat();
a.eat();
// a.playGame();

// 向下转型:从父到子,父类引用转为子类对象。
Cat c = (Cat)a;
c.eat();
c.playGame();
}
}

运行结果:

1
2
3
猫吃鱼
猫吃鱼
猫捉迷藏

多态转型的内存结构

我们以这么一段代码为例。
示例代码:

1
2
3
4
5
6
7
package com.kakawanyifan;

public class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

public class Cat extends Animal{
@Override
public void eat() {
System.out.println("猫吃鱼");
}

public void playGame() {
System.out.println("猫捉迷藏");
}
}
1
2
3
4
5
6
7
package com.kakawanyifan;

public class Dog extends Animal{
public void eat() {
System.out.println("狗吃骨头");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

public class AnimalDemo {
public static void main(String[] args) {
// 向上转型:从子到父,父类引用指向子类对象。
Animal a = new Cat();
a.eat();
// 向下转型:从父到子,父类引用转为子类对象。
Cat c = (Cat)a;
c.eat();
c.playGame();
// 向上转型:从子到父,父类引用指向子类对象。
a = new Dog();
a.eat();
// 向下转型:从父到子,父类引用转为子类对象。
Cat cc = (Cat)a;
cc.eat();
cc.playGame();
}
}

运行结果:

1
2
3
4
5
6
猫吃鱼
猫吃鱼
猫捉迷藏
狗吃骨头
Exception in thread "main" java.lang.ClassCastException: com.kakawanyifan.Dog cannot be cast to com.kakawanyifan.Cat
at com.kakawanyifan.AnimalDemo.main(AnimalDemo.java:16)

解释一下。

首先,main方法入栈。
执行Animal a = new Cat();
向上转型,父类引用指向子类对象。
Animal a = new Cat()

a.eat()
对于成员方法,编译看左边,运行看右边。
所以,eat方法所属的类别是Cat类。
Animal a = new Cat()

Cat c = (Cat)a
向下转型,父类引用转为子类对象。
c指向堆内存地址001。
Cat c = (Cat)a

c.eat()
c.playGame()
输出"猫吃鱼",“猫捉迷藏”。
c.eat();c.playGame()

a = new Dog()
向上转型,父类引用指向子类对象。
a指向堆内存地址002。
a = new Dog()

a.eat()
对于成员方法,编译看左边,运行看右边。
属于Dog类的eat方法进栈,输出"狗吃骨头"。
a.eat()

Cat cc = (Cat)a
向下转型,父类引用转为子类对象。
cc指向的是Dog对象在堆内存的地址。但cc是属于Cat类的,转换失败。
a.eat()

向下转型的注意事项

通过刚刚的例子,我们已经已经知道了。
向下转型只能转型为本类对象(猫是不能变成狗的)。

现在,再来看一个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

public class AnimalDemo {
public static void main(String[] args) {
Animal a = new Animal();

// 向下转型:从父到子,父类引用转为子类对象。
Cat c = (Cat)a;
c.eat();
c.playGame();
}
}

运行结果:

1
2
Exception in thread "main" java.lang.ClassCastException: com.kakawanyifan.Animal cannot be cast to com.kakawanyifan.Cat
at com.kakawanyifan.AnimalDemo.main(AnimalDemo.java:8)

包括了?
刚刚不是这样的啊!
刚刚挺好的啊。

因为刚刚,a本身就是Cat对象,所以它理所当然的可以向下转型为Cat。但是呢,现在a就是Animal对象,它也不能被向下转型为任何子类对象。
比如你去考古,发现了一个新生物,知道它是一种动物,但是你不能直接说,啊,它是猫,或者说它是狗。

小结一下,向下转型的注意事项:

  • 向下转型只能转型为本类对象(猫是不能变成狗的)。
  • 向下转型的前提是父类对象指向的是子类对象(也就是说,在向下转型之前,它得先向上转型)。

抽象类

提到了多态,我们就不得不讨论抽象类。
因为这种类有一个特点:
无法直接实例化,必须通过子类对象实例化,这个被称为抽象类多态

在上文,我们讨论了修饰符。
Java中的修饰符分为三类:

  1. 权限修饰符
  2. 状态修饰符
  3. 抽象修饰符

当时,我们讨论前两类。关于抽象修饰符,我们说在讨论抽象类的时候就会讨论。
就在这里。

当我们在做子类共性功能抽取时,有些方法在父类中并没有具体的体现,这个时候就需要抽象类了。
在Java中,一个没有方法体的方法应该定义为抽象方法,而且如果类中如果有抽象方法,该类必须定义为抽象类。

抽象类和抽象方法必须使用abstract关键字修饰。

1
2
3
4
5
//抽象类的定义
public abstract class 类名 {}

//抽象方法的定义
public abstract void eat();

抽象类中不一定有抽象方法,有抽象方法的类不一定是抽象类(还可能是接口)。

抽象类的子类:要么重写抽象类中的所有抽象方法,要么本身还是抽象类。
抽象类的成员变量:抽象类中可以有成员变量。
抽象类的成员方法:抽象类中可以有抽象的成员方法,也可以有非抽象的普通的成员方法。
抽象类的构造方法:抽象类中可以有参数的构造方法,也可以有无参的构造方法。

我们举个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.kakawanyifan;

public abstract class Animal {

private int age = 20;
private final String city = "北京";

public Animal() {
}

public Animal(int age) {
this.age = age;
}

public void show() {
age = 40;
System.out.println(age);
// city = "上海";
System.out.println(city);
}

public abstract void eat();

}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
1
2
3
4
5
6
7
8
9
package com.kakawanyifan;

public class AnimalDemo {
public static void main(String[] args) {
Animal a = new Cat();
a.eat();
a.show();
}
}

运行结果:

1
2
3
猫吃鱼
40
北京

接口

在上文,我们讨论多态的时候,我们说多态的前提:

  1. 要有继承或实现关系
  2. 要有方法的重写
  3. 要有父类引用指向子类对象

当时,我们说关于什么是实现,我们会在下文讨论接口的时候讨论。就在这里。

接口的概述

Java中的接口体现在对行为的抽象。

接口用关键字interface修饰。

1
public interface 接口名 {}

类实现接口用implements表示。

1
public class 类名 implements 接口名 {}

接口不能实例化,但可以通过实现类对象实例化,这叫接口多态。
接口的实现类,要么重写接口中的所有抽象方法,要么子类也是抽象类。

小结一下,截止现在,我们一共讨论了三种多态形式:

  1. 具体类多态
  2. 抽象类多态
  3. 接口多态

接口的成员

成员变量:即使没有加final修饰符,实际上也会是final的。而且是static的。
构造方法:没有,因为接口主要是扩展功能的,而没有具体存在。
成员方法:只能是抽象方法,即使没有加abstract修饰符,也是抽象的。

举个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kakawanyifan;

public interface InterFace {
public int num = 10;
public final int num2 = 20;

// 报错 只能是抽象方法
// public Inter() {};

// 报错 只能是抽象方法
// public void show() {};

public abstract void method();

// 即使没有加"abstract"修饰符,也是抽象的。
void show();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

public class InterImpl implements InterFace{

@Override
public void method() {
System.out.println("method");
}

@Override
public void show() {
System.out.println("show");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

public class InterDemo {
public static void main(String[] args) {
InterFace inter = new InterImpl();

// 虽然看起来没有被final修饰,但是接口中的成员变量实际是final的
// inter.num = 10;

System.out.println(inter.num);

// 肯定报错,因为num2是被final修饰的
// inter.num2 = 20;

System.out.println(inter.num2);

// 默认是static的,所以可以直接通过类名访问
System.out.println(InterFace.num);
}
}

运行结果:

1
2
3
10
20
10

类、抽象类和接口

类和接口之间的相互关系

类与类的关系:继承关系,只能单继承,但是可以多层继承。
类与接口的关系:实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口。
接口与接口的关系:继承关系,可以单继承,也可以多继承

抽象类和接口的区别

成员区别

  • 抽象类:变量、常量,构造方法,抽象方法,非抽象方法。
  • 接口:只有常量(被final修饰了)和抽象方法

关系区别

  • 类与抽象类:继承,单继承。
  • 类与接口:实现,可以单实现,也可以多实现。

设计理念区别

  • 抽象类:对类抽象,包括属性、行为。
  • 接口:对行为抽象,主要是行为

参数传递

类作为形参和返回值

  • 方法的形参是类,其实需要的是该类的对象。
  • 方法的返回值是类,其实返回的是该类的对象。

抽象类作为形参和返回值

  • 方法的形参是抽象类,其实需要的是该抽象类的子类对象。
  • 方法的返回值是抽象类,其实返回的是该抽象类的子类对象。

接口作为形参和返回值

  • 方法的形参是接口,其实需要的是该接口的实现类对象。
  • 方法的返回值是接口,其实返回的是该接口的实现类对象。

匿名内部类

既然抽象类和接口都讨论了,那么就来讨论一下匿名内部类。

匿名内部类的概述

匿名内部类其实是上文讨论的内部类中的局部内部类的一种特殊形式。
匿名内部类的前提:存在一个类或者接口,这里的类可以是具体类也可以是抽象类。
匿名内部类的格式:

1
new 类名 ( ) { 重写方法 }

1
new  接口名 ( ) { 重写方法 }

举例:

1
2
3
4
new Inter(){
@Override
public void method(){}
}

匿名内部类的本质:是一个继承了该类或者实现了该接口的子类匿名对象。本质是对象。

示例代码:

1
2
3
4
5
package com.kakawanyifan;

public interface Inter {
void show();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

public class Outer {
public void method() {
new Inter() {
@Override
public void show() {
System.out.println("匿名内部类");
}
}.show();

Inter i = new Inter() {
@Override
public void show() {
System.out.println("匿名内部类");
}
};
i.show();
}
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class OuterDemo {
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
}

运行结果:

1
2
匿名内部类
匿名内部类

匿名内部类的应用

当发现某个方法需要接口或抽象类的子类对象,我们就可以传递一个匿名内部类过去,来简化传统的代码。

示例代码:

1
2
3
4
5
package com.kakawanyifan;

public interface Jumping {
void jump();
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class Cat implements Jumping {
@Override
public void jump() {
System.out.println("猫可以跳高了");
}
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class Dog implements Jumping {
@Override
public void jump() {
System.out.println("狗可以跳高了");
}
}
1
2
3
4
5
6
7
package com.kakawanyifan;

public class JumpingOperator {
public void method(Jumping j) {
j.jump();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.kakawanyifan;

public class JumpingDemo {
public static void main(String[] args) {
//需求:创建接口操作类的对象,调用method方法
JumpingOperator jo = new JumpingOperator();

Jumping j = new Cat();
jo.method(j);

Jumping j2 = new Dog();
jo.method(j2);

System.out.println("--------");

// 匿名内部类的简化
jo.method(new Jumping() {
@Override
public void jump() {
System.out.println("猫可以跳高了");
}
});
// 匿名内部类的简化
jo.method(new Jumping() {
@Override
public void jump() {
System.out.println("狗可以跳高了");
}
});
}
}

运行结果:

1
2
3
4
5
猫可以跳高了
狗可以跳高了
--------
猫可以跳高了
狗可以跳高了

小结

最后,我们用一张图,做一个重点要点的小结。
重点要点小结

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10802
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Kaka Wan Yifan

留言板