文章目录
一、对抽象类与接口的疑问
不知道刚刚接触到抽象类与接口的同学是否有以上的疑问,抽象类与接口到底有什么用,他们的应用场景是啥,为什么要写接口和抽象类?我为啥不直接写类,反而要绕一圈通过继承来编写类?等等等等,大概总结一下会有以下几个问题:
- 为什么不直接在类里面写对应的方法, 而要多写1个接口(或抽象类)?
- 既然接口跟抽象类差不多, 什么情况下要用接口而不是抽象类。
下面就让我们一步一步来解答这两个问题。
二、抽象类
Java是面向对象语言,面向对象程序设计有以下优点:
- 可重用性:代码重复使用,减少代码量,提高开发效率。面向对象的三大核心特性(继承、封装和多态)都围绕这个核心。
- 可扩展性:指新的功能可以很容易地加入到系统中来,便于软件的修改。
- 可管理性:能够将功能与数据结合,方便管理。该开发模式之所以使程序设计更加完善和强大,主要是因为面向对象具有继承、封装和多态 3 个核心特性。
而抽象类就是为了实现多态!!!,没有抽象类、接口,继承关系,那么多态将无法实现!
下面举一个小小的代码例子
2.1代码示例
先定义几个类:
- 动物(Animal) 抽象类
- 爬行动物(Reptile) 抽象类 继承动物类
- 哺乳动物(Mammal) 抽象类 继承动物类
- 山羊(Goat) 继承哺乳动物类
- 老虎(Tiger) 继承哺乳动物类
- 兔子(Rabbit) 继承哺乳动物类
- 蛇(Snake) 继承爬行动物类
- 农夫(Farmer) 没有继承任何类 但是农夫可以给Animal喂水(依赖关系)
Animal.java
这是一个动物抽象类,动物都有属于自己的移动方式(跑跳滚游飞等)和喝水动作(低头喝劈叉喝手舀水喝等等),于是在抽象类中定义了moveMethod()
移动方式和drink()
喝水方式这两个方法。
public abstract class Animal {
//移动方法,不同动物的移动方法不一样:老虎山羊是跑,兔子是跳,蛇是爬
//参数move是移动的方法
public abstract void moveMethod(String name,String move);
//喝水方法
public abstract void drink(String name);
public abstract String getName();
public abstract String getMove();
}
Reptile.java
爬行动物抽象类,继承自动物,没添加啥特别的方法,空着吧~
public abstract class Reptile extends Animal{
}
Mammal.java
哺乳动物抽象类,同上~
public abstract class Mammal extends Animal {
}
Goat.java
山羊类,我们的第一个实体类,继承自哺乳动物抽象类,我们需要实现其中的moveMethod()
和drink()
这两个方法。
public class Goat extends Mammal{
private String name;
private String move;
public Goat() {
}
public Goat(String name, String move) {
this.name = name;
this.move = move;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMove() {
return move;
}
public void setMove(String move) {
this.move = move;
}
@Override
public void moveMethod(String name,String move) {
System.out.println(name+move+"喂水池。");
}
@Override
public void drink(String name) {
System.out.println(name+"低头喝水");
}
}
Tiger.java
public class Tiger extends Mammal{
private String name;
private String move;
public Tiger() {
}
public Tiger(String name, String move) {
this.name = name;
this.move = move;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMove() {
return move;
}
public void setMove(String move) {
this.move = move;
}
@Override
public void moveMethod(String name,String move) {
System.out.println(name+move+"喂水池。");
}
@Override
public void drink(String name) {
System.out.println(name+"低头喝水");
}
}
Rabbit.java
class Rabbit extends Mammal{
private String name;
private String move;
public Rabbit() {
}
public Rabbit(String name, String move) {
this.name = name;
this.move = move;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMove() {
return move;
}
public void setMove(String move) {
this.move = move;
}
@Override
public void moveMethod(String name,String move) {
System.out.println(name+move+"喂水池。");
}
@Override
public void drink(String name) {
System.out.println(name+"伸出舌头喝水");
}
}
Snake.java
public class Snake extends Reptile{
private String name;
private String move;
public Snake() {
}
public Snake(String name, String move) {
this.name = name;
this.move = move;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMove() {
return move;
}
public void setMove(String move) {
this.move = move;
}
@Override
public void moveMethod(String name,String move) {
System.out.println(name+move+"喂水池。");
}
@Override
public void drink(String name) {
System.out.println(name+"进入水池喝水");
}
}
Farmer.java
农夫类,没有继承任何类,他就一个目的,每天幸幸苦苦的给上面的动物喂水,所以编写了一个feedWater()
方法。
而feedWater()
分为了三个步骤,农夫将水带到了水池,动物移动到水池,然后用自己的方法去喝水。
Farmer可以给老虎喂水,可以给山羊喂水,还可以给蛇喂水,那么feedWater()
里的参数类型到底是老虎,山羊还是蛇呢?实际上因为老虎,山羊,蛇都继承自Animal这个类,所以feedWater()
里的参数类型设为Animal
就可以了。Farmer类只需要调用feedWater()
方法把水带到水池,至于这个动物是如何走到饲养室和如何喝水的,Farmer类则不用关心。因为执行时, Animal
超类会根据引用指向的对象类型不同而指向不同的被重写的方法。这个就是多态的意义。
public class Farmer {
public void feedWater(Animal a){ // polymorphism
System.out.println("农夫将水带到水池");
a.moveMethod(a.getName(), a.getMove());
a.drink(a.getName());
System.out.println("----------");
}
}
让我们来创建一个测试类让农夫给上面的动物喂水吧。
测试类
在生成一个具体的动物对象时,我们需要给它们取一个名字,和移动的方式,然后创建一个农夫类对象,农夫f使用feedWater()
方法把水带到水池,他可不用知道也懒得知道动物们是怎么过来喝水的!
public class Testdemo0_1 {
public static void main(String[] args) {
Farmer f = new Farmer();
Animal tiger = new Tiger("大老虎", "跑");
f.feedWater(tiger);
Animal goat = new Goat("小山羊", "跑");
f.feedWater(goat);
Animal rabbit = new Rabbit("小白兔", "蹦蹦跳跳");
f.feedWater(rabbit);
Animal snake = new Snake("菜花蛇", "爬");
f.feedWater(snake);
}
}
运行结果:
我相信已经有人会吐槽了,天哪怎么这么多代码,看起来这继承多态抽象类这杂七杂八的东西也并没有简洁方便啊!但是,对就是但是,如果不使用继承、抽象类呢?我们直接创建老虎、山羊、兔子、蛇的实体类,工作量也就减少了三个代码量很低的抽象类(动物、哺乳动物、爬行动物),但是在实现农夫类的时候,feedWater()
方法的参数类型就不能使用Animal
,而是得重载四个feedWater()
方法,且参数类型分别是Tiger
、Goat
,Rabbit
,Snake
这里是还只有四种,如果数量更多呢?
你在调用的的时候也得在大量的feedWater()
中找到你需要的那一个,相信我,只用几十个就能找得你眼花!!
相当于本来你只需要准备一桶水给所有的动物喂水喝,但现在你要给每种动物准备一桶水,在喂水的时候还不能将他们搞混,如果有几十上百种动物…我相信你如果不用多态的话,农夫会冲出来和你打一架让你去喂水!
2.2抽象类存在的问题
抽象类虽然很好的实现了多态性,那么什么情况使用接口会更好呢?
对于上面的例子,我们加一点需求:
农夫多了一项任务,那就是给另一个动物喂兔子 ,依旧是农夫将兔子带到水池,然后捕食者移动过来,再将兔子捕食掉。那么问题来了,这个动物就必须有捕猎这个技能,所以我们需要给被喂食的动物加上一个方法hunt(Animal a)
。
但是只有一部分动物拥有捕猎这个技能,比如上面的蛇或者老虎,所以我们不应该把hunt(Animal a)
添加到Goat类和Rabbit类里。
下面有3个方案:
- 分别在Tiger类里和Snake类里加上Hunt() 方法, 其它类(例如Goat) 不加。
- 在基类Animal里加上Hunt()抽象方法,在Tiger里和Snake里重写这个Hunt() 方法。
- 添加肉食性动物这个抽象类。
先来说说第一种方案:
这种情况下,Tiger里的Hunt(Animal a)方法与Snake里的Hunt(Animal a)方法毫无关联。也就是说不能利用多态性,导致Farm类里的feedAnimal()方法需要分别为Tiger 与 Snake类重载,否决。
第二种方案:
如果在抽象类Animal里加上Hunt()方法,则所有它的非抽象派生类都要重写实现这个方法,包括Goat类和Rabbit类。这是不合理的,因为Goat类根本没必要用到Hunt()方法,山羊难道能去捕猎吗?造成了资源(内存)浪费。
第三种方案:
假如我们在哺乳类动物下做个分叉, 加上肉食性哺乳类动物, 非肉食性哺乳动物这两个抽象类?首先,这种方案会另类族图越来越复杂,假如以后再需要辨别能否飞的动物呢,增加飞翔 fly()这个方法呢?是不是还要分叉?本Demo因为实体类很少,所以看起来这么修改并不难,但是实际项目中,你需要将之前的实体类的继承关系全部改一遍…你可以估计一下这个工作量。
其次,很现实的问题,在项目中,你很可能没机会修改上层的类代码,因为它们是用Jar包发布的,或者你没有修改权限。
这种情况我们就需要接口登场了。
三、接口
3.1 接口与多态以及多继承性
上面的问题,抽象类解决不了,根本问题是Java的类不能多继承。因为Tiger类继承了动物Animal类的特性(例如 move() 和 drink()) ,但是严格上来将捕猎(hunt())并不算是动物的特性之一,有些植物,单细胞生物也会捕猎的。所以Tiger要从别的地方来继承Hunt()这个方法. 接口就发挥作用了。
3.2 Huntable接口
让我们创建一个Huntable接口,接口里有1个方法hunt(Animal a),就是捕捉动物,至于怎样捕捉则由实现接口的类自己决定。
public interface Huntable {
public void hunt(Animal a);
}
3.3 Tiger类实现Huntable接口
public class Tiger extends Mammal implements Huntable{
private String name;
private String move;
public Tiger() {
}
public Tiger(String name, String move) {
this.name = name;
this.move = move;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMove() {
return move;
}
public void setMove(String move) {
this.move = move;
}
@Override
public void moveMethod(String name,String move) {
System.out.println(name+move+"喂水池。");
}
@Override
public void drink(String name) {
System.out.println(name+"低头喝水");
}
@Override
public void hunt(Animal a) {
System.out.println(name+"扑食了"+a.getName());
}
}
3.4 Farmer类添加feedWater(Animal a)方法
public void feedAnimal(Animal ht,Animal a){
System.out.println("农夫将"+a.getName()+"带到水池");
ht.moveMethod(ht.getName(),ht.getMove());
Huntable hab = (Huntable) ht;
hab.hunt(a);
}
测试
在测试类中添加f.feedAnimal(tiger,rabbit);
,然后运行。
但是如果我们直接在Snake类中添加hunt(Animal a)
方法,然后在测试类中添加f.feedAnimal(snake,rabbit);
运行会发生什么呢?会报错!
下一个问题,为什么会报错?因为Snake类不是Huntable接口的实现类!再来读一遍:父类的引用类型变量指向了子类的对象或者是接口类型的引用类型变量指向了接口实现类的对象。这是多态的实现机制!
所以接口类型的引用类型变量无法指向Snake类的对象,也就无法实现多态。这也是上面的第一种方案被pass的原因。
四、总结
4.1 需要实现多态
很明显,接口其中一个存在意义就是为了实现多态,而抽象类(继承) 也可以实现多态。
4.2 要实现的方法(功能)不是当前类族的必要(属性)
上面的例子就表明,捕猎这个方法不是动物这个类必须的,在动物的派生类中,有些类需要,有些不需要。如果把捕猎方法写在动物超类里面是不合理的浪费资源。所以把捕猎这个方法封装成1个接口,让派生类自己去选择实现。
4.3 要为不同类族的多个类实现同样的方法(功能)
上面说过了, 其实不是只有Animal类的派生类才可以实现Huntable接口。如果Farmer实现了这个接口,那么农夫自己就可以去捕猎动物了…我们拿另个常用的接口Comparable来做例子。
这个接口是应用了泛型,首先,比较(CompareTo) 这种行为很难界定适用的类族,实际上,几乎所有的类都可以比较。比如数字类可以比较大小,人类可以比较财富,动物可以比较体重等。
所以各种类都可以实现这个比较接口,一旦实现了这个比较接口,就可以开启另1个隐藏技能:就是可以利用Arrays.sort()来进行排序了。就如实现了捕猎接口的动物,可以被农夫Farmer拿兔子喂一样。
这是一个Java小白学习过程中的笔记,如果有错误希望能在评论区指出,谢谢~
都看到这里了,不留个赞再走吗?