【JAVA进化论】LV2-4:类的特性、关系

一、继承和多态

1.1:一个例子

参考之前的Cat类,如果我现在让你设计一个Dog类,它仍然具备眼睛、性别等属性,也会跑、吃等动作,这个时候你一定会发现,单独再为Dog搞一套跟Cat差不多的类定义吗?就没有更聪明的办法吗?

此时java里允许的类继承就可以起到作用了,想想我们的目标,我们的目标是让这些重复的属性不再重复定义一遍,那么像眼睛、毛色这种属性,或者跑、吃饭这类的操作就可以再拆出一个类:动物类

现在我们来定义下动物类:

代码块1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Animal {

private String name = "xx"; //动物学名,动物们都有学名,这里默认为xx,因为动物类是用来描述动物的,动物有学名,但你并不知道是那种动物,name只能未知

private int age; //动物们的年龄

public void cry() { //控制叫声的方法
System.out.println("叫声为:未知");
}

public void eat() { //负责吃饭的方法
System.out.println(name + "正在吃东西");
}

//用来给name赋值的方法
public void setName(String name) {
this.name = name;
}
//用来给age赋值的方法
public void setAge(int age) {
this.age = age;
}

}

然后再定义一下Cat和Dog类:

代码块2
1
2
3
public class Cat extends Animal { //让Cat类继承Animal类

}
代码块3
1
2
3
public class Dog extends Animal { //让Dog类继承Animal类

}

Cat和Dog都是一个”空“类,但是分别利用extends关键词继承了Animal类,现在让我们进行如下测试:

代码块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
public static void main(String[] args) {
//造一只9岁的猫
Cat cat = new Cat();
cat.setAge(9);
cat.setName("猫");
//让这只猫叫一声,再吃一口东西
cat.cry();
cat.eat();

//造一只10岁的狗
Dog dog = new Dog();
dog.setAge(10);
dog.setName("狗");
//让这只狗叫一声,再吃一口东西
dog.cry();
dog.eat();


//造一只10岁的狗,但不设置学名
Dog dog2 = new Dog();
dog2.setAge(10);
//让这只狗叫一声,再吃一口东西
dog2.cry();
dog2.eat();
}

上述程序输出结果如下:

代码块5
1
2
3
4
5
6
叫声为:未知
猫正在吃东西
叫声为:未知
狗正在吃东西
叫声为:未知
xx正在吃东西

可以看到,Cat和Dog虽然啥都没定义,但仍然可以使用Animal类里的方法,我们此时管Cat和Dog叫做Animal的子类,相反的Animal叫做Cat和Dog的父类。

可以通过这个例子发现,子类通过继承,可以得到父类的一些功能,需要确认的一个点是:在初始化完成一个子类初始化的同时其父类信息也会自动加载进子类,然后子类可以共享父类里的方法和属性。

这里说一下this关键词,它表示的是本类所产生的具体对象对其内部内容做访问时用的一个标识,你甚至可以省略不写。

1.2:父类方法覆盖

通过上面的输出,我们发现,猫狗的cry方法输出的是Animal默认的内容,这时我们需要猫和狗能有自己的叫声,那么这时就可以选择重写父类的方法:

Cat类重写父类的cry方法:

代码块6
1
2
3
4
5
6
7
public class Cat extends Animal {

@Override //加上@Override标识为方法重写
public void cry() {
System.out.println("喵喵喵~"); //这里重新定义方法的实现
}
}

Dog类重写父类cry方法:

代码块7
1
2
3
4
5
6
7
8
public class Dog extends Animal {

@Override
public void cry() {
System.out.println("汪汪汪~");
}

}

现在再运行下测试代码输出如下:

代码块8
1
2
3
4
喵喵喵~
猫正在吃东西
汪汪汪~
狗正在吃东西

通过这个例子,我们可以知道,如果子类不想要父类里的实现,那么可以通过重写的方式重新设计对应的方法,例子中通过重写cry方法,让Cat和Dog都拥有了自己的cry方法。

1.3:子类方法扩展

经过上面一步,我们知道猫和狗都是动物,所以它们离不开动物类里定义的属性和方法,但是猫和狗仍存在一些不太一样的地方,比如猫具有捕鼠的能力,但不具备看家的能力,而狗具有看家能力,却没有捕鼠的能力,这种相对比较独立的能力就可以用来扩展:

Cat类新增捕鼠方法

代码块9
1
2
3
4
5
6
7
8
9
10
11
12
public class Cat extends Animal {

@Override //加上@Override标识为方法重写
public void cry() {
System.out.println("喵喵喵~"); //这里重新定义方法的实现
}

public void catchMice() { //扩展方法:捕鼠
System.out.println("我会捉老鼠");
}

}

Dog类新增看家方法:

代码块10
1
2
3
4
5
6
7
8
9
10
11
public class Dog extends Animal {

@Override
public void cry() {
System.out.println("汪汪汪~");
}

public void houseKeeping() { //扩展方法:看家
System.out.println("我会看家");
}
}

通过本例我们知道,子类是可以自由扩展的,通过子类的引用变量依然可以完成调用:

代码块11
1
2
3
4
5
6
7
8
public static void main(String[] args) {

Cat cat = new Cat();
cat.catchMice();

Dog dog = new Dog();
dog.houseKeeping();
}

输出:

代码块12
1
2
我会捉老鼠
我会看家

1.4:多态

现在来介绍下多态,多态是一种类和类之间的一种引用关系,因为有了继承,才有了多态这种特性,我们现在来改造下代码块4

代码块13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {

Animal cat = new Cat(); //声明变量时,改成父类类型
cat.setAge(9);
cat.setName("猫");

cat.cry();
cat.eat();

Animal dog = new Dog(); //声明变量时,改成父类类型
dog.setAge(10);
dog.setName("狗");

dog.cry();
dog.eat();

}

运行如下:

代码块14
1
2
3
4
喵喵喵~
猫正在吃东西
汪汪汪~
狗正在吃东西

仍然可以正常运行,并且被重写的方法cry仍然执行的是子类里的那个。

这就是多态,如果不太好理解,我们可以通过下图来加深一下记忆:

图1

这种使用父类修饰的引用变量,可以指向其任意子类对象的行为,我们称为类的多态,可以发现父类接收的子类对象无法访问扩展内容,但仍然可以访问父类所具备的内容,哪怕被重写的方法cry,但这时由于父类指向的还是子类对象,因此所触发的内容均属于子类,这里先不探讨多态的好处,你只需要加深对代码块13图1的理解类的这种特性即可。

❓ 疑问点1:为什么通过Animal的引用变量指向的Cat对象,无法调用其catchMice方法?为什么cry方法可以调用触发的却是Cat里的cry方法?

1.5:如何直接访问父类的内容?

1.5.1:直接new

父类也是一个普通的类嘛,要想不受子类的任何影响,直接new不就完事儿了。但一般父类都不太可能直接new,在多子类的情况下,又该怎样直接访问自己的资源呢?来接着往下看1.5.2吧。

1.5.2:super关键词

通过上面的介绍,我们了解了类的继承和多态,子类在继承了父类的功能和属性之后,可以自定义方法实现对父类的扩展,针对实现不合子类要求的方法,子类也可以通过方法重写来覆盖原父类方法,然后就是多态,多态简单来说就是通过一个父类引用变量直接指向一个子类对象,还是结合图1理解。

那么我们现在再来做一个实验,现在我想让Cat和Dog在吃饭的时候自带叫声,现在我们来继续改造下这些类:

首先把Animal里的eat方法改造下,让它的逻辑执行之前,先调用下cry方法:

代码块15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Animal {

private String name = "xx";
private int age;

public void cry() {
System.out.println("叫声为:未知");
}

public void eat() {
this.cry(); //吃饭之前先叫两嗓子
System.out.println(name + "正在吃东西");
}

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

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

}

别的代码不动,单独测试下eat方法:

代码块16
1
2
3
4
5
6
7
public static void main(String[] args) {
Animal cat = new Cat();
cat.eat();

Animal dog = new Dog();
dog.eat();
}

最终输出:

代码块17
1
2
3
4
喵喵喵~
xx正在吃东西
汪汪汪~
xx正在吃东西

这里说一下,即便代码块16直接用Cat和Dog接收对应的类对象,结果都是一致的。

🦩 结论:new出来具体的子类之后,父类里即便使用this调用它内部的被子类重写的方法,实际触发的仍然是子类的方法。

❓ 疑问点2:为什么呢?我明明是在Animal对象里调用的this.cry,为什么执行的却是子类的?

ok,那我们怎么直接访问父类里的方法呢?借助super关键词即可,同this关键词一样用来描述类对象内部引用的,与this不同的是,子类利用super可以调用父类资源,注意,这里说了,想用super,就得在某子类内,我们现在如果想调用Animal本身的cry方法,那么就需要在Cat或Dog里,通过super触发:

首先把Animal里面的eat方法还原:

代码块18
1
2
3
4
5
6
7
8
9
10
11
public class Animal {

...省略...

public void eat() { //负责吃饭的方法
System.out.println(name + "正在吃东西");
}


...省略...
}

然后改造下Cat类,让它重写eat方法,我们目标是让这个eat方法先调用一遍父类原始的cry方法,再调用一次父类原始的eat方法:

代码块19
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Cat extends Animal {

...省略...

@Override
public void eat() { //这里重写eat,同时使用super直接调用父类的cry和eat方法
super.cry();
super.eat();
}

...省略...

}

现在再次调用eat方法,输出结果如下:

代码块20
1
2
叫声为:未知
xx正在吃东西

你会发现,现在的调用就是父类里面原生的方法。

现在,让我们把代码块19里的cry的super关键词去掉(之前说过,去掉相当于使用this关键词调用):

代码块21
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Cat extends Animal {

...省略...

@Override
public void eat() {
cry(); //这里把super去掉
super.eat(); //等看完这一块的内容之后,结合Cat类的代码,分析一下这里eat的super关键词去掉会发生什么?
}

...省略...

}

然后调用eat的结果如下:

代码块22
1
2
喵喵喵~
xx正在吃东西

可见,Cat里使用this调用cry方法,当然是调用的它自己的咯。

❓ 疑问点3:为什么父类的被子类重写过的方法可怜到只能通过子类的super关键词才能触发?为什么父类里也有的方法要优先执行子类里重写后的?不管在父类还是子类,用this关键词调用的资源都是优先以子类为准吗?找不到的资源才去考虑找父类吗?

1.6:继承链

首先请记住现在的Animal类:

代码块23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Animal {

private String name = "xx"; //动物学名,动物们都有学名,这里默认为xx,因为动物类是用来描述动物的,动物有学名,但你并不知道是那种动物,name只能未知

private int age; //动物们的年龄

public void cry() { //控制叫声的方法
System.out.println("叫声为:未知");
}

public void eat() { //负责吃饭的方法
System.out.println(name + "正在吃东西");
}

//用来给name赋值的方法
public void setName(String name) {
this.name = name;
}

//用来给age赋值的方法
public void setAge(int age) {
this.age = age;
}
}

Cat类清理一下之前的测试代码,记住现在的Cat类:

代码块24
1
2
3
4
5
6
7
8
9
10
11
public class Cat extends Animal {

@Override //加上@Override标识为方法重写
public void cry() {
System.out.println("喵喵喵~"); //这里重新定义方法的实现
}

public void catchMice() { //扩展方法:捕鼠
System.out.println("我会捉老鼠");
}
}

java语法里,只允许一个类继承一个类,也就是说,extends关键词只能在一个类定义里出现一次,根据这个规则,父类也是一个类,类允许继承一个别的类(套娃警告),描述起来是越来越乱,不如再改造下前面的例子来说明问题;现在我们发现Cat也只是定义了猫的基本概念,包括被它重写后的cry,确实,猫都是喵喵喵的叫,猫都会捕鼠,但是不同品种的猫,也存在差异,例如相比其它猫,橘猫的食量惊人,它还可以扮猪,如果这时定义一个橘猫类,首先它也是猫,那就让它继承Cat类,那么eat这个方法就可以单独拎出来再被重写一次,以显示橘猫特有的食量,其次应该单独加一个橘猫特有技能:扮猪,看代码:

代码块25
1
2
3
4
5
6
7
8
9
10
11
12
public class OrangeCat extends Cat {

@Override
public void eat() { //重写eat方法
System.out.println("我寻思你不能因为我吃的多就专门为我量身定做一个eat方法吧?。。。。。。艾玛真香~");
}

//假装自己是头猪
public void playAPig() {
System.out.println("我说我是🐷,你能怎么办?(🤪)");
}
}

ok,至少通过橘猫类,我们知道了,java虽然不允许一个类继承多个类,但可以间接继承多个类,现在的继承链为:

图2

那么一样的,我们前面了解了java的继承和多态,这种继承链也是符合上述所有的继承相关的特性,比如我们来利用OrangeCat做个试验:

代码块26
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
//继承链仍符合多态规则,看下方的代码,我用Animal和Cat都可以接收OrangeCat对象,因为它们俩都是OrangeCat的父类
Animal orangeCat1 = new OrangeCat();
Cat orangeCat2 = new OrangeCat();
OrangeCat orangeCat3 = new OrangeCat();

orangeCat1.cry();
orangeCat1.eat();

orangeCat2.cry();
orangeCat2.eat();

orangeCat3.cry();
orangeCat3.eat();
}

结果如下:

代码块27
1
2
3
4
5
6
喵喵喵~
我寻思你不能因为我吃的多就专门为我量身定做一个eat方法吧?。。。。。。艾玛真香~
喵喵喵~
我寻思你不能因为我吃的多就专门为我量身定做一个eat方法吧?。。。。。。艾玛真香~
喵喵喵~
我寻思你不能因为我吃的多就专门为我量身定做一个eat方法吧?。。。。。。艾玛真香~

可以看到,不管用谁去接收OrangeCat对象,最终输出结果都是正确的,首先OrangeCat没有重写cry方法,因此触发的是父类Cat里的cry方法,又因为自己重写了eat对象,所以eat调用时就直接触发了自己的eat方法。

同样的,playAPig方法属于OrangeCat自定义的扩展方法,它只能由OrangeCat类型的引用变量触发,因此代码块26里,只有orangeCat3可以调用这个方法,由于OrangeCat继承了Cat,所以它也会捕鼠(catchMice)了。

我们再来看看OrangeCat具备了哪些能力:

代码块28
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
OrangeCat orangeCat3 = new OrangeCat();
orangeCat3.setAge(9); //其直系父类Cat继承了Animal,因此它也可以调用Animal的setAge方法
orangeCat3.setName("橘猫"); //同上
orangeCat3.cry(); //其直系父类Cat有cry方法(虽然是重写Animal的cry来的),因此这里触发的是Cat里那个cry方法
orangeCat3.eat(); //因为自己有eat方法,因此这里触发的是自己的eat方法
orangeCat3.catchMice(); //其直系父类Cat有catchMice方法,因此这里触发的是Cat里那个catchMice方法
orangeCat3.playAPig(); //这是OrangeCat特有的方法,自然可以调用
}

看到了吗,经过一层层的继承,OrangeCat拥有的技能要比俩父类加起来还多,这是因为继承的特性,就是父类所有的东西都是可以给子类的(当然能不能访问就是另一回事了,下面会说访问权限相关的内容),而子类又可以重写父类的方法,如果不重写,则按照就近原则触发对应的方法,比如例子里虽然OrangeCat没有重写cry方法,但它调用cry的输出结果却是Cat类里的那个,因此,在重写方法调用上,一般遵循就近原则,此外子类也可以扩展自己的内容,比如Cat相比Animal,可以捕鼠,再比如OrangeCat相比Cat和Animal,可以扮猪。

1.7:巩固继承&多态

前面讲了那么多,现在我们通过画图的方式,系统的解释一遍继承多态

1.7.1:继承关系下,重复方法的就近调用规则

先看下我们在前面留下的3个问题:

❓ 疑问点1:为什么通过Animal的引用变量指向的Cat对象,无法调用其catchMice方法?为什么cry方法可以调用触发的却是Cat里的cry方法?

❓ 疑问点2:为什么呢?我明明是在Animal对象里调用的this.cry,为什么执行的却是子类的?

❓ 疑问点3:为什么父类的被子类重写过的方法可怜到只能通过子类的super关键词才能触发?为什么父类里也有的方法要优先执行子类里重写后的?不管在父类还是子类,用this关键词调用的资源都是优先以子类为准吗?找不到的资源才去考虑找父类吗?

先不用急着看问题,我们先来分析下一个类方法或属性在调用时,都经历了些什么。

LV2-1的时候,我们就知道了,对象实际上就是一个整体,被保存在了内存里,然后通过一个引用变量来指向它,然后就可以利用这个引用变量来操纵它了,现在我们来看下,正常没有继承任何类的类,是如何访问方法的:

🦜 这里说明一下,下面的流程图只是为了便于让大家理解并记住java继承关系下的访问规则,真实的java对象在内存里并不长这样(虽然确实包含属性值),方法的调用在java底层也很复杂,这些都要对jvm有深入了解才行。

图3

如果一个类没有任何父类,那么它的方法调用就很纯粹,没有任何悬念,现在让我们的看看Cat类:

图4

让我们再来看看更复杂的橘猫:

图5

我们仍然可以找到一些规则,来说明在继承里方法的调用和最终触发哪个方法的问题。

好的,截止目前,集合图4图5,我们解决了疑问点2疑问点3,通过图4,我们知道了this关键词取决于当前的对象究竟属于哪个类,仍然符合就近调用规则,例如Animal里面使用this.cry调用cry,如果当前对象是Animal自己,那么毫无疑问,最终触发的会是它自己的cry方法,但如果当前对象是Cat,那么this调用cry时就遵循就近规则,优先触发自己所属类(也就是Cat类)里的cry方法。

图4告诉我们,super关键字在子类里可用来直接调用其父类里的方法:

图6

这个可以解答疑问点3.

现在让我们解答下疑问点1,为什么父类引用变量可以接收子类对象?

还记得讲基本类型时,存在大类型和小类型吗?大类型可以自动接收小类型,而小类型想要变成大类型就得强转,类同样拥有类似的规则,比如你可以把父类理解成比较大的类,子类是比较小的类,那么父类当然可以自动接收子类咯,只不过类有不一样的地方,一个子类被父类接收以后,访问域就受父类限制了:

图7

既然规则跟基本类型差不多,当然也有类型强转啦:

代码块29
1
2
Animal cat = new Cat();
Cat cat2 = (Cat)cat;

看吧,连写法都跟基本类型的强转一样,括号里加上要转的目标类型即可。

但是需要注意的是,别瞎转,比如你知道这个Animal的确是一个Cat,然后像代码块29里那样转成Cat类型即可,但如果对象并不是Cat,那样强转语法里是允许的,但是运行时会报类型转换错误,比如下面这样:

代码块30
1
2
Animal dog = new Dog();
Cat cat = (Cat)dog;

上面的代码在语法里不会有问题,但实际上运行时便会报错,即类型转换错误。

到目前为止,有关问题1、2、3都说完了,继承和多态这一块确实很复杂,最好能结合本节流程图记下来这个规则,以后做分析时就不会乱了。

1.7.2:属性在继承关系下的访问规则

上面只是说了方法的调用规则,却没有说属性,我们来做个试验:

现在让Cat类里也有一个叫age的属性:

代码块31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Cat extends Animal {

public int age = 12; //新增一个跟父类一样的属性,初始值为12

@Override
public void cry() {
System.out.println(this.age); //通过this关键词访问age
System.out.println(super.age); //通过super关键词访问父类里的age
System.out.println("喵喵喵~");
}

public void catchMice() { //扩展方法:捕鼠
System.out.println("我会捉老鼠");
}

}

然后让父类里的age访问权限变成public(详细看下方1.8,这里改成public的目的是为了让Cat可以直接访问这个属性),然后让其初始值为15,并且在其eat方法里使用this关键词访问age:

代码块32
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Animal {

...省略...

public int age = 15; //动物们的年龄,初始值为15

public void eat() {
System.out.println(this.age); //输出age
System.out.println(this.name + "正在吃东西");
}


...省略...
}

现在让我们来测试下:

代码块33
1
2
3
4
5
public static void main(String[] args){
Animal cat = new Cat();
cat.cry();
cat.eat();
}

输出如下:

代码块34
1
2
3
4
5
12
15
喵喵喵~
15
xx正在吃东西

🎑 结论:由运行结果我们可以知道,与方法的重写不同,类的属性本身该属于哪个类还是属于哪个类,记住这个规则。

好了,上面的这个试验有画蛇添足的嫌疑,因为age父类里就有,为什么Cat里还要加上一个同名的age呢?是的,真实设计类的时候没人会这样设计的,我们需要明确一个点,方法是行为,方法在继承关系里允许各种重写,但是属性作为一种没有任何行为方式的东西,你重复定义它的意义大吗?显然是不大的,而且极易跟父类混淆,所以我们说子类可以重写父类的方法,继承关系里,方法调用符合以对象所属类为参照就近调用的规则,子类还可以自己拓展父类里没有的方法,子类的对象还可以被父类引用变量接收,只是这样会影响子类对象的访问域(图7),子类还可以拓展父类里没有的属性,但不建议子类覆盖父类的属性,这种覆盖是没有意义的,通过上面的例子我们也发现了,同名属性在类继承链里是相互隔离的。

1.8:访问权限

通过1.7,你应该知道了某种类与父类访问规则,但是我们在LV2-2的访问权限符的介绍里,介绍了访问权限修饰符以及被它们修饰了的资源的访问权限,也就是说理想状态下1.7的流程图都是成立的,但是你有没有发现,这些方法的权限修饰符都是public的?是的,public表示资源公开,比如Animal类里的age和name两个属性都是private类型的,子类可以通过super直接访问这俩属性吗?显然是不行的,必须得通过public的setName和setAge俩方法完成对name和age的赋值,这就是访问权限符的意义了,至于怎么区分能不能访问呢?我们再把那张表里有关修饰符访问等级的表贴来:

表1

只需要记住,如果俩类为继承关系,则protected关键词无视包目录,均可访问,其余规则全部跟普通类一样,由于之前讲过权限修饰符,因此这里不再做详细介绍。

1.9:继承和多态的好处

通过前面对继承和多态的了解,我们大体上对这俩特性有了一个认识,首先继承是为了解决什么问题的?

回归到我们的例子上,Animal里定义了符合动物特性一些行为和属性,Cat是一种动物,符合Animal定义的行为和属性,它们俩建立起父子关系后,Cat就可以直接使用父类里定义的属性和方法了,这样像eat这种方法,Cat和Dog都不需要自己定义了,这样会让我们的代码看起来很简洁,而且同样的代码逻辑块,不需要重复定义多次,有需要可以重写,子类还可以自定义自己的行为方法。

多态呢?它有什么好处?

多态就是指使用一个父类引用变量,可以接收子类对象并允许触发它内部的方法,参见图1,那么这样的一种特性的好处究竟是什么呢?我们本节先不会涉及,之后讲JDBC时可以展开讲,现在你要做的是,牢记java的这种机制。

1.10:抽象类

了解完了继承多态,我们再来了解一种特殊的类:抽象类

什么是抽象类?让我们来改造下Animal方法,不知道你有没有发现,由Animal派生出来的子类Cat和Dog都重写了cry方法,说明大部分时候,Animal里的cry方法都不会主动生效,因为意义不大,这时我们认为cry方法在绝大多数情况下都是需要被重写的,像这种方法,我们就可以把它定义成一个抽象方法,包含抽象方法的类被称作抽象类,普通的类是不能有抽象方法的,因此想要cry变成抽象方法,首先要把Animal变成抽象类:

代码块35
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//利用abstract修饰的class,被称为抽象类
public abstract class Animal {

private String name = "xx"; //动物学名,动物们都有学名,这里默认为xx,因为动物类是用来描述动物的,动物有学名,但你并不知道是那种动物,name只能未知

public int age = 15; //动物们的年龄

//被abstract修饰的方法被称为抽象方法
public abstract void cry(); //动物会叫,但每个动物的叫声都完全不一样,有的甚至不会叫,那"叫"就应该是一个标准,作为动物的标准,动物本身只要定义好这个标准就好了,其余交给具体的动物(子类)去实现

public void eat() { //负责进食的方法
System.out.println(this.name + "正在进食");
}

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

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

}

我们可以看到,抽象方法没有任何实现,所以称它为一个标准,所有直系子类都必须实现这个标准,这时Cat类继承了Animal类,如果不实现cry方法,语法上就会报错:

图8

现在我们只能让Cat类实现这个抽象方法:

图9

这就相当于强制让子类去重写cry方法,事实上确实需要每个动物都实现自己的cry方法,但是像eat这种方法就没有必要这样做,因为大部分动物都需要进食且都是用嘴进食的,大同小异,如果真的存在不一样的吃东西的方法,那么就跟之前一样重写就好了,但大部分动物都不需要重写eat方法,父类Animal里的已经够用了,这是在告诉你什么时候需要搞抽象方法,什么时候不需要。

现在你知道了,有的方法需要被抽象,那就定义抽象方法,因为存在抽象方法,所以类必须被声明为抽象类(abstract关键词)。

抽象类的特点:

  1. 抽象类不能被实例化(也就是说不可以利用new关键词实例化一个抽象类)
  2. 由于1的特性,我们认为,抽象类是为继承而生的,如果你定义了一个抽象类,而它没有任何子类,那么这个抽象类就没有任何意义
  3. 抽象类不一定有抽象方法,但有抽象方法的类一定是抽象类
  4. 抽象类可以继承抽象类,当继承了抽象类时,子类抽象类也可以不实现其父类的抽象方法,全部交给非抽象的子类去做
  5. 抽象类也可以继承普通类,也可以选择重写普通类的方法,更神奇的是,它还可以让通过重写,让普通类的方法变成一个抽象方法
  6. 抽象类虽然不可以被实例化,但是它仍然存在构造器(这是句废话,抽象类是天生的父类,父类构造器在子类被实例化的时候会自动调用一次,所以作为天生的父类,它必有构造器)

🌵 思考:

现在Animal定义如下:

Animal定义

Cat类是它的子类,定义如下:

Cat定义

结合前面对继承和多态以及本部分对抽象类的理解,请问如下测试代码输出结果是什么?

这段代码的结果是什么呢?

1.11:万类の父-Object

不知道你有没有发现,任何对象都自动有这些方法:

图10

这是因为类被定义出来,就隐藏继承了一个父类,名叫Object,这是java自带的类,不需要显式的extends出来。

二、封装

封装充满我们代码,还是以前面的例子为准,我们认为对一类操作使用一个方法定义来圈起来,就是封装,比如eat、cry等方法,例子里面很简单,只打印了一句话,但实际开发中,一个方法可能会完成很复杂的操作,这里拿之前一个例子来说明问题:

代码块36
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
public class Cat {

public int gender;

protected int age;

int color;

private String name;

public Cat(int gender, int age, int color, String name) {
this.gender = gender;
this.age = age;
this.color = color;
this.name = name;
}

public void cry(Cat cat) {
if (this.tooOld()) { //通过tooOld来判断当前这只猫是否年龄太大了
System.out.println("叫不动了...");
} else {
System.out.println("喵喵喵~");
}
}

public void eat() {
System.out.println("要恰饭的嘛");
}

protected void emm() {
if (this.tooOld()) { //通过tooOld来判断当前这只猫是否年龄太大了
System.out.println("骚不动了...");
} else {
System.out.println("猪肉卷和千层面不存在二选一,我全都要!");
}
}

void run() {
if (tooOld()) { //通过tooOld来判断当前这只猫是否年龄太大了
System.out.println("跑不动了...");
} else if (age > 10) {
System.out.println("奔跑速度:10km/h");
} else {
System.out.println("奔跑速度:15km/h");
}
}

...省略...

//被拆出来的逻辑块,因为这段逻辑会出现在多个方法里,因此单拆出来封装成一个方法,这种方法建议设置成private权限的,因为仅限本类内调用
private boolean tooOld() {
return this.age > 15; //经过本层封装,只要需要这段逻辑的地方,直接调用下这个方法即可,本例太简单,因为只做了age大小判断,实际开发里可以把更复杂的且重复度过高的代码块像这样拆出来,封装成一个方法提供服务
}
}

通过本例,我们知道了封装的目的是减少代码的重复度,提高复用率,这点是不是跟继承也很像?继承的意义之一也是为了解决代码重复度的。

封装仅仅只是为了解决重复度吗?并不是,通过之前的Animal的例子,我们发现像age、name这种属性的赋值操作也被封装成了一个个方法(setName和setAge),而Animal里的name和age全是private的,这是为什么呢?为什么不让age和name变成protected或public这种权限呢?让别的类直接赋值不是更好?为啥还要多此一举搞俩专门赋值的方法?

这就体现了封装的另外一层意义:隔离

现在我们试着将Animal里的age变成public的,来看看下面:

代码块37
1
2
Animal animal = new Animal();
animal.age = 10000; //由于Animal的age变成public公开了,因此在外界可以随意对其赋值

但是Animal这时不干了,你见过能活10000岁的生物吗??因此Animal把自己的age封了起来,首先把访问权限变成private,然后提供一个setAge方法用来给age赋值,setAge就是Animal对外封装的一个专门给age赋值的方法,这个时候Animal就由之前的被动状态变成主动状态了,如果你不想让别人设置的age太过分,就可以在setAge里做限制,反正是自己的方法嘛,还不是想怎么设计怎么设计,让我们来改一下Animal里setAge的逻辑:

代码块38
1
2
3
4
5
6
7
public void setAge(int age) {
if (age > 10000) { //当传入的age超过1w时,输出这句话反问对方
System.out.println("这种生物你给我找来个康康?");
} else { //否则的话就给自己的age赋值
this.age = age;
}
}

看看,这样age的赋值就完全被隔离进本类内了,外界进行赋值时就不敢那么放肆了~

封装的意义:

  • 封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
  • 保护成员属性,不让类以外的程序直接访问和修改;
  • 隐藏方法细节

这也是为啥我们在定义一个类的时候,属性经常被设置成private访问级别的,然后通过定义赋值方法来让外界给自己的属性赋值。

关于对象封装的原则:

  • 内聚:内聚是指一个模块内部各个部分之间的关联程度
  • 耦合:耦合指各个模块之前的关联程度
  • 封装原则:隐藏对象的属性和实现细节,仅对外公开访问方法,并且控制访问级别
  • 在面向对象方法中,用类来实现上面的要求。用类实现封装,用封装来实现高内聚低耦合

↑上面的话先读一下,做多了东西自然就懂了,我们做开发时所作的封装多态等,都是为了这一个目标:高内聚、低耦合来进行的。