Java学习日志8

First Post:

Last Update:

Word Count:
3.4k

Read Time:
12 min

继承(下)

在实际开发中,满足什么条件的时候,我可以使用继承呢?
凡是采用“is a”能描述的,都可以继承。
例如:
Cat is a Animal:猫是一个动物
Dog is a Animal:狗是一个动物
CreditAccount is a Account:信用卡账户是一个银行账户
….

假设以后的开发中有一个A类,有一个B类,A类和B类确实也有重复的代码,那么他们两个之间就可以继承吗?不一定,还是要看一看它们之间是否能够使用is a来描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Customer{
String name; // 名字
// setter and getter
}

class Product{
String name; // 名字
// setter and getter
}

class Product extends Customer{

}

以上的继承就属于很失败的。因为:Product is a Customer,是有违伦理的。

方法覆盖

2.1、什么时候考虑使用方法覆盖?
父类中的方法无法满足子类的业务需求,子类有必要对继承过来的方法进行覆盖。

2.2、什么条件满足的时候构成方法覆盖?
第一:有继承关系的两个类
第二:具有相同方法名返回值类型形式参数列表
第三:访问权限不能更低
第四:抛出异常不能更多

2.3、关于Object类中toString()方法的覆盖?
toString()方法存在的作用就是:将java对象转换成字符串形式。
大多数的java类toString()方法都是需要覆盖的。因为Object类中提供的toString()
方法输出的是一个java对象的内存地址。
至于toString()方法具体怎么进行覆盖?
格式可以自己定义,或者听需求的。(听项目要求的。)

2.4、方法重载和方法覆盖有什么区别?

  • 方法重载发生在同一个类当中

  • 方法覆盖是发生在具有继承关系的父子类之间

  • 方法重载是一个类中,方法名相同,参数列表不同。

  • 方法覆盖是具有继承关系的父子类,并且重写之后的方法必须和之前的方法一致:方法名一致、参数列表一致、返回值类型一致

代码示例:

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
/**
* @author OliverChen
*/
public class OverrideTest {
public static void main(String[] args) {
Animals.doSomething();
Bird b = new Bird();
b.move();
Cat c = new Cat();
c.move();
}
}

class Animals{
public static void doSomething()
{
O.p("Do something!");
}
public void move()
{
O.p("动物在移动!!!");
}
}

class Bird extends Animals
{
public void move()
{
O.p("鸟儿在飞翔!");
}
}

class Cat extends Animals
{
public void move()
{
O.p("猫在走猫步!");
}
}

多态(非常重要!!!)

向上转型、向下转型与instanceof运算符

两种类型间必须有继承关系!!!

向上转型:子—>父 (upcasting)
又被称为自动类型转换:Animal a = new Cat();

向下转型:父—>子 (downcasting)
又被称为强制类型转换:Cat c = (Cat)a; 需要添加强制类型转换符。

什么时候需要向下转型?
需要调用或者执行子类对象中特有的方法。必须进行向下转型,才可以调用。

向下转型有风险吗?
容易出现ClassCastException(类型转换异常)

怎么避免这个风险?
instanceof运算符,可以在程序运行阶段动态的判断某个引用指向的对象是否为某一种类型。
养成好习惯,向下转型之前一定要使用instanceof运算符进行判断。
不管是向上转型还是向下转型,首先他们之间必须有继承关系,这样编译器就不会报错。

代码示例:(非常重要的示例!!!

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

public class Test01{

public static void main(String[] args){

Animal a1 = new Animal();
a1.move(); //动物在移动!!!

Cat c1 = new Cat();
c1.move(); //cat走猫步!

Bird b1 = new Bird();
b1.move(); //鸟儿在飞翔!!!

// 代码可以这样写吗?
/*
1、Animal和Cat之间有继承关系吗?有的。
2、Animal是父类,Cat是子类。
3、Cat is a Animal,这句话能不能说通?能。
4、经过测试得知java中支持这样的一个语法:
父类型的引用允许指向子类型的对象。
Animal a2 = new Cat();
a2就是父类型的引用。
new Cat()是一个子类型的对象。
允许a2这个父类型引用指向子类型的对象。
*/
Animal a2 = new Cat();
Animal a3 = new Bird();

// 没有继承关系的两个类型之间存在转型吗?
// 错误: 不兼容的类型: Dog无法转换为Animal
// Animal a4 = new Dog();

// 调用a2的move()方法
/*
什么是多态?
多种形态,多种状态。
分析:a2.move();
java程序分为编译阶段和运行阶段。
先来分析编译阶段:
对于编译器来说,编译器只知道a2的类型是Animal,
所以编译器在检查语法的时候,会去Animal.class
字节码文件中找move()方法,找到了,绑定上move()
方法,编译通过,静态绑定成功。(编译阶段属于静态绑定。)
再来分析运行阶段:
运行阶段的时候,实际上在堆内存中创建的java对象是
Cat对象,所以move的时候,真正参与move的对象是一只猫,
所以运行阶段会动态执行Cat对象的move()方法。这个过程
属于运行阶段绑定。(运行阶段绑定属于动态绑定。)

多态表示多种形态:
编译的时候一种形态。
运行的时候另一种形态。
*/
a2.move(); //cat走猫步!

// 调用a3的move()方法
a3.move(); //鸟儿在飞翔!!!

// ======================================================================
Animal a5 = new Cat(); // 底层对象是一只猫。

// 分析这个程序能否编译和运行呢?
// 分析程序一定要分析编译阶段的静态绑定和运行阶段的动态绑定。
// 只有编译通过的代码才能运行。没有编译,根本轮不到运行。
// 错误: 找不到符号
// why??? 因为编译器只知道a5的类型是Animal,去Animal.class文件中找catchMouse()方法
// 结果没有找到,所以静态绑定失败,编译报错。无法运行。(语法不合法。)
//a5.catchMouse();

// 假设代码写到了这里,我非要调用catchMouse()方法怎么办?
// 这个时候就必须使用“向下转型”了。(强制类型转换)
// 以下这行代码为啥没报错????
// 因为a5是Animal类型,转成Cat,Animal和Cat之间存在继承关系。所以没报错。
Cat x = (Cat)a5;
x.catchMouse(); //猫正在抓老鼠!!!!

// 向下转型有风险吗?
Animal a6 = new Bird(); //表面上a6是一个Animal,运行的时候实际上是一只鸟儿。
/*
分析以下程序,编译报错还是运行报错???
编译器检测到a6这个引用是Animal类型,
而Animal和Cat之间存在继承关系,所以可以向下转型。
编译没毛病。

运行阶段,堆内存实际创建的对象是:Bird对象。
在实际运行过程中,拿着Bird对象转换成Cat对象
就不行了。因为Bird和Cat之间没有继承关系。

运行是出现异常,这个异常和空指针异常一样非常重要,也非常经典:
java.lang.ClassCastException:类型转换异常。

java.lang.NullPointerException:空指针异常。这个也非常重要。
*/
//Cat y = (Cat)a6;
//y.catchMouse();

// 怎么避免ClassCastException异常的发生???
/*
新的内容,运算符:
instanceof (运行阶段动态判断)
第一:instanceof可以在运行阶段动态判断引用指向的对象的类型。
第二:instanceof的语法:
(引用 instanceof 类型)
第三:instanceof运算符的运算结果只能是:true/false
第四:c是一个引用,c变量保存了内存地址指向了堆中的对象。
假设(c instanceof Cat)为true表示:
c引用指向的堆内存中的java对象是一个Cat。
假设(c instanceof Cat)为false表示:
c引用指向的堆内存中的java对象不是一个Cat。

程序员要养成一个好习惯:
任何时候,任何地点,对类型进行向下转型时,一定要使用
instanceof 运算符进行判断。(java规范中要求的。)
这样可以很好的避免:ClassCastException
*/
System.out.println(a6 instanceof Cat); //false

if(a6 instanceof Cat){ // 如果a6是一只Cat
Cat y = (Cat)a6; // 再进行强制类型转换
y.catchMouse();
}
}
}

多态的概念

多种形态,多种状态,编译和运行有两个不同的状态。
编译期叫做静态绑定。
运行期叫做动态绑定。

1
2
3
4
5
Animal a = new Cat();
// 编译的时候编译器发现a的类型是Animal,所以编译器会去Animal类中找move()方法
// 找到了,绑定,编译通过。但是运行的时候和底层堆内存当中的实际对象有关
// 真正执行的时候会自动调用“堆内存中真实对象”的相关方法。
a.move();

多态在开发中的应用(非常重要!!!)

多态在开发中的作用是:
降低程序的耦合度,提高程序的扩展力。

代码示例(非常重要!!!!!!!)

Master.java:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 主人类
public class Master{

/*
// 假设主人起初的时候只是喜欢养宠物狗狗
// 喂养宠物狗狗
public void feed(Dog d){
d.eat();
}

// 由于新的需求产生,导致我们“不得不”去修改Master这个类的代码
public void feed(Cat c){
c.eat();
}
*/

// 能不能让Master主人这个类以后不再修改了。
// 即使主人又喜欢养其它宠物了,Master也不需要修改。
// 这个时候就需要使用:多态机制。
// 最好不要写具体的宠物类型,这样会影响程序的扩展性。
public void feed(Pet pet){
// 编译的时候,编译器发现pet是Pet类,会去Pet类中找eat()方法,结果找到了,编译通过
// 运行的时候,底层实际的对象是什么,就自动调用到该实际对象对应的eat()方法上。
// 这就是多态的使用。
pet.eat();
}

}

/*

注意这里的分析:
主人起初的时候只喜欢养宠物狗狗
随着时间的推移,主人又喜欢上养“猫咪”
在实际的开发中这就表示客户产生了新的需求。
作为软件的开发人员来说,必须满足客户的需求。
我们怎么去满足客户的需求呢?
在不使用多态机制的前提下,目前我们只能在Master类中添加一个新的方法。

思考:软件在扩展新需求过程当中,修改Master这个类有什么问题?
一定要记住:软件在扩展过程当中,修改的越少越好。
修改的越多,你的系统当前的稳定性就越差,未知的风险就越多。

其实这里涉及到一个软件的开发原则:
软件开发原则有七大原则(不属于java,这个开发原则属于整个软件业):
其中有一条最基本的原则:OCP(开闭原则)

什么是开闭原则?
对扩展开放(你可以额外添加,没问题),对修改关闭(最好很少的修改现有程序)。
在软件的扩展过程当中,修改的越少越好。


高手开发项目不是仅仅为了实现客户的需求,还需要考虑软件的扩展性。

什么是软件扩展性?
假设电脑中的内存条部件坏了,我们可以买一个新的插上,直接使用。
这个电脑的设计就考虑了“扩展性”。内存条的扩展性很好。

面向父类型编程,面向更加抽象进行编程,不建议面向具体编程。
因为面向具体编程会让软件的扩展力很差。

*/


// 所有宠物的父类
class Pet{

// 吃的行为(这个方法可以不给具体的实现。)
public void eat(){

}


}


// 宠物狗狗类
class Dog extends Pet{
// 吃
public void eat(){
System.out.println("狗狗喜欢啃骨头,吃的很香。");
}
}

class Cat extends Pet{

// 吃
public void eat(){
System.out.println("猫咪喜欢吃鱼,吃的很香!!!");
}
}

Test.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
测试多态在开发中的作用
*/
public class Test{
public static void main(String[] args){
// 创建主人对象
Master zhangsan = new Master();
// 创建宠物对象
Dog zangAo = new Dog();
// 主人喂
zhangsan.feed(zangAo);
// 创建宠物对象
Cat xiaoHua = new Cat();
// 主人喂
zhangsan.feed(xiaoHua);
// 创建宠物对象

}
}

一些补充

面向对象的三大特征:
封装、继承、多态
真的是一环扣一环。

有了封装,有了这种整体的概念之后。
对象和对象之间产生了继承。
有了继承之后,才有了方法的覆盖和多态。

这里提到了一个软件开发原则:
七大原则最基本的原则:OCP(对扩展开放,对修改关闭)
目的是:降低程序耦合度,提高程序扩展力。
面向抽象编程,不建议面向具体编程。

私有方法无法覆盖。

方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。(这是因为方法覆盖通常和多态联合起来)

总结两句话:
私有不能覆盖。
静态不谈覆盖。

在方法覆盖中,关于方法的返回值类型。
什么条件满足之后,会构成方法的覆盖呢?
1、发生具有继承关系的两个类之间。
2、父类中的方法和子类重写之后的方法具有相同的方法名、相同的形式参数列表、相同的返回值类型