14.接口

<pre>

实现防盗门的功能

第一种方案

  • 门有“开”和“关”的功能,锁有“上锁”和“开锁”的功能

  • 将门和锁分别定义为抽象类

防盗门可以继承门的同时又继承锁吗?

第二种方案

  • 将门定义为抽象类,锁定义为接口

  • 防盗门继承门,实现锁的接口

接口

public interface MyInterface {
    public void foo();
    //其他方法
}

必须知道的接口特性

  • 接口不可以被实例化

  • 实现类必须实现接口的所有方法

  • 实现类可以实现多个接口

  • 接口中的变量都是静态常量

用程序描述USB接口

image

USB接口本身没有实现任何功能

USB接口规定了数据传输的要求

USB接口可以被多种USB设备实现

  • 编写USB接口

    根据需求设计方法

  • 实现USB接口

    实现所有方法

  • 使用USB接口

    用多态的方式使用

1定义usb接口

public interface UsbInterface {
    /**
     * USB接口提供服务。
     */
    void service();
}

2 实现接口

public class UDisk implements UsbInterface {
    public void service() {
        System.out.println("连接USB口,开始传输数据。");
    }
}

使用接口

UsbInterface uDisk = new UDisk();
uDisk.service();

接口表示一种能力(体现在接口的方法上 )

做这项工作需要一个程序员

编程是一种“能力”,不关心具体是谁(具体的实现类是谁)

面向接口编程

设计程序时

关心实现类有何能力,而不关心实现细节

面向接口的约定而不考虑接口的具体实现

实现防盗门功能

image

防盗门是一个门

防盗门有一个锁

  • 上锁

  • 开锁

image
public abstract class Door {
    public abstract void open();  //开
    public abstract void close();  //关
}

public interface Lock {
    void lockUp();  //锁
    void openLock();  //开锁
}

public class TheftproofDoor extends Door implements Lock {

    @Override
    public void lockUp() {
        System.out.println("插进钥匙,向左旋转钥匙三圈,锁上了,拔出钥匙。");
    }

    @Override
    public void openLock() {
        System.out.println("插进钥匙,向右旋转钥匙三圈,锁打开了,拔出钥匙。");

    }

    @Override
    public void open() {
        System.out.println("用力推,门打开了。");
    }

    @Override
    public void close() {
        System.out.println("轻轻拉门,门关上了。");
    }
}

public class DoorTest {
    public static void main(String[] args) {
      //创建具体防盗对象
        TheftproofDoor tfd=new TheftproofDoor();
        tfd.close();  //关门
        tfd.lockUp();  //锁门
        tfd.openLock(); //开锁
        tfd.open();  //开门
    }
}

接口 vs 抽象类

接口有比抽象类更好的特性:

1.可以被多继承

2.设计和实现完全分离

3.更自然的使用多态

4.更容易搭建程序框架

5.更容易更换实现

对接口的2个疑问

1.为什么不直接在类里面写对应的方法, 而要多写1个接口(或抽象类)?

2.既然接口跟抽象类差不多, 什么情况下要用接口而不是抽象类.

接口引用可以指向实现该接口的对象

我们清楚接口是不可以被实例化, 但是接口引用可以指向1个实现该接口的对象.

也就是说.

假如类A impletments 了接口B

那么下面是合法的:

B b = new A();

也可以把A的对象强制转换为 接口B的对象

A a = new A90;

B b = (B)a;

这个特性是下面内容的前提.

抽象类为了多态的实现

第1个答案十分简单, 就是为了实现多态.

下面用详细代码举1个例子.

先定义几个类,

动物(Animal) 抽象类

爬行动物(Reptile) 抽象类 继承动物类

哺乳动物(Mammal) 抽象类 继承动物类

山羊(Goat) 继承哺乳动物类

老虎(Tiger) 继承哺乳动物类

兔子(Rabbit) 继承哺乳动物类

蛇(Snake) 继承爬行动物类

农夫(Farmer) 没有继承任何类 但是农夫可以给Animal喂水(依赖关系)

Animal类

这个是抽象类, 显示也没有"动物" 这种实体

类里面包含3个抽象方法.

  1. 静态方法getName()

  2. 移动方法move(), 因为动物都能移动. 但是各种动物有不同的移动方法, 例如老虎和山羊会跑着移动, 兔子跳着移动, 蛇会爬着移动.

作为抽象基类, 我们不关心继承的实体类是如何移动的, 所以移动方法move()是1个抽象方法. 这个就是多态的思想.

  1. 喝水方法drink(), 同样, 各种动物有各种饮水方法. 这个也是抽象方法.

代码:

abstract class Animal{  
    public abstract String getName();  
    public abstract void move(String destination);  
    public abstract void drink();  
}  

Mammal类

这个是继承动物类的哺乳动物类, 后面的老虎山羊等都继承自这个类.

Mammal类自然继承了Animal类的3个抽象方法, 实体类不再用写其他代码.

abstract class Mammal extends Animal{  

} 

Reptile类

这个是代表爬行动物的抽象类, 同上, 都是继承自Animal类.

abstract class Reptile extends Animal{  

}  

Tiger类

老虎类就是1个实体类, 所以它必须重写所有继承自超类的抽象方法, 至于那些方法如何重写, 则取决于老虎类本身.

老虎的移动方法很普通, 低头喝水.

class Tiger extends Mammal{
    private static String name = "Tiger";
    public String getName(){
        return this.name;
    }

    public void move(String destination){
        System.out.println("Goat moved to " + destination + ".");
    }

    public void drink(){
        System.out.println("Goat lower it's head and drink.");
    }
}

Goat类 和 Rabbit类

这个两个类与Tiger类似, 它们都继承自Mammal这个类.

class Goat extends Mammal{  
    private static String name = "Goat";  
    public String getName(){  
        return this.name;  
    }  

    public void move(String destination){  
        System.out.println("Goat moved to " + destination + ".");  
    }  

    public void drink(){  
        System.out.println("Goat lower it's head and drink.");  
    }  
}  

class Rabbit extends Mammal{  
    private static String name = "Rabbit";  
    public String getName(){  
        return this.name;  
    }  

    public void move(String destination){  
        System.out.println("Rabbit moved to " + destination + ".");  
    }  

    public void drink(){  
        System.out.println("Rabbit put out it's tongue and drink.");  
    }  
}  

Snake类

蛇类继承自Reptile(爬行动物)

class Snake extends Reptile{  
    private static String name = "Snake";  
    public String getName(){  
        return this.name;  
    }  

    public void move(String destination){  
        System.out.println("Snake crawled to " + destination + ".");  
    }   

    public void drink(){  
        System.out.println("Snake dived into water and drink.");  
    }  
}  

Farmer 类

Farmer类不属于 Animal类族, 但是Farmer农夫可以给各种动物, 喂水.

Farmer类有2个关键方法, 分别是

bringWater(String destination) -> 把水带到某个地点

另1个就是feedWater了,

feedWater这个方法分为三步:

首先是农夫带水到饲养室,(bringWater())

接着被喂水动物走到饲养室,(move())

接着动物喝水(drink())

Farmer可以给老虎喂水, 可以给山羊喂水, 还可以给蛇喂水, 那么feedWater()里的参数类型到底是老虎,山羊还是蛇呢.

实际上因为老虎,山羊, 蛇都继承自Animal这个类, 所以feedWater里的参数类型设为Animal就可以了.

Farmer类首先叼用bringWater("饲养室"),

至于这个动物是如何走到饲养室和如何喝水的, Farmer类则不用关心.

因为执行时, Animal超类会根据引用指向的对象类型不同 而 指向不同的被重写的方法. 这个就是多态的意义.

class Farmer{  
    public void bringWater(String destination){  
        System.out.println("Farmer bring water to " + destination + ".");  
    }  

    public void feedWater(Animal a){ // polymorphism  
        this.bringWater("Feeding Room");  
        a.move("Feeding Room");  
        a.drink();  
    }  

}  

执行农夫喂水的代码

下面的代码是1个农夫依次喂水给一只老虎, 一只羊, 以及一条蛇

public static void f(){  
       Farmer fm = new Farmer();  
       Snake sn = new Snake();  
       Goat gt = new Goat();  
       Tiger tg = new Tiger();  

       fm.feedWater(sn);  
       fm.feedWater(gt);  
       fm.feedWater(tg);  
   }  

农夫只负责带水过去制定地点, 而不必关心老虎, 蛇, 山羊它们是如何过来的. 它们如何喝水. 这些农夫都不必关心.

只需要调用同1个方法feedWater.

执行结果:

[java] Farmer bring water to Feeding Room.  
  [java] Snake crawled to Feeding Room.  
  [java] Snake dived into water and drink.  
  [java] Farmer bring water to Feeding Room.  
  [java] Goat moved to Feeding Room.  
  [java] Goat lower it's head and drink.  
  [java] Farmer bring water to Feeding Room.  
  [java] Goat moved to Feeding Room.  
  [java] Goat lower it's head and drink.  

不使用多态的后果?:

而如果老虎, 蛇, 山羊的drink() 方法不是重写自同1个抽象方法的话, 多态就不能实现.

农夫类就可能要根据参数类型的不同而重载很多个 feedWater()方法了.

而且每增加1个类(例如 狮子Lion)

就需要在农夫类里增加1个feedWater的重载方法 feedWater(Lion l)...

而接口跟抽象类类似,

这个就回答了第一个问题.

1.为什么不直接在类里面写对应的方法, 而要多写1个接口(或抽象类)?

抽象类解决不了的问题

既然抽象类很好地实现了多态性, 那么什么情况下用接口会更加好呢?

对于上面的例子, 我们加一点需求.

Farmer 农夫多了1个技能, 就是给另1个动物喂兔子(囧).

BringAnimal(Animal a, String destination) 把兔子带到某个地点...

feedAnimal(Animal ht, Animal a) 把动物a丢给动物ht

注意农夫并没有把兔子宰了, 而是把小动物(a)丢给另1个被喂食的动物(ht).

那么问题来了, 那个动物必须有捕猎这个技能. 也就是我们要给被喂食的动物加上1个方法(捕猎) hunt(Animal a).

但是现实上不是所有动物都有捕猎这个技能的, 所以我们不应该把hunt(Animal a)方法加在Goat类和Rabbit类里, 只加在Tiger类和Snake类里.

而且老虎跟蛇的捕猎方法也不一样, 则表明hunt()的方法体在Tiger类里和Snake类里是不一样的.

下面有3个方案.

  1. 分别在Tiger类里和Snake类里加上Hunt() 方法. 其它类(例如Goat) 不加.

  2. 在基类Animal里加上Hunt()抽象方法. 在Tiger里和Snake里重写这个Hunt() 方法.

  3. 添加肉食性动物这个抽象类.

先来说第1种方案.

这种情况下, Tiger里的Hunt(Animal a)方法与 Snake里的Hunt(Animal a)方法毫无关联. 也就是说不能利用多态性.

导致Farm类里的feedAnimal()方法需要分别为Tiger 与 Snake类重载. 否决.

第2种方案:

如果在抽象类Animal里加上Hunt()方法, 则所有它的非抽象派生类都要重写实现这个方法, 包括 Goat类和 Rabbit类.

这是不合理的, 因为Goat类根本没必要用到Hunt()方法, 造成了资源(内存)浪费.

第3种方案:

加入我们在哺乳类动物下做个分叉, 加上肉食性哺乳类动物, 非肉食性哺乳动物这两个抽象类?

首先,

肉食性这种分叉并不准确, 例如很多腐蚀性动物不会捕猎, 但是它们是肉食性.

其次

这种方案会另类族图越来越复杂, 假如以后再需要辨别能否飞的动物呢, 增加飞翔 fly()这个方法呢? 是不是还要分叉?

再次,

很现实的问题, 在项目中, 你很可能没机会修改上层的类代码, 因为它们是用Jar包发布的, 或者你没有修改权限.

这种情况下就需要用到接口了.

接口与多态 以及 多继承性.

上面的问题, 抽象类解决不了, 根本问题是Java的类不能多继承.

因为Tiger类继承了动物Animal类的特性(例如 move() 和 drink()) , 但是严格上来将 捕猎(hunt())并不算是动物的特性之一. 有些植物, 单细胞生物也会捕猎的.

所以Tiger要从别的地方来继承Hunt()这个方法. 接口就发挥作用了.

Huntable接口

我们增加了1个Huntable接口.

接口里有1个方法hunt(Animal a), 就是捕捉动物, 至于怎样捕捉则由实现接口的类自己决定.

interface Huntable{  
    public void hunt(Animal a);  
}  

Tiger 类

既然定义了1个Huntable(可以捕猎的)接口.

Tiger类就要实现这个接口并重写接口里hunt()方法.

class Tiger extends Mammal implements Huntable{  
    private static String name = "Tiger";  
    public String getName(){  
        return this.name;  
    }  

    public void move(String destination){  
        System.out.println("Goat moved to " + destination + ".");  
    }  

    public void drink(){  
        System.out.println("Goat lower it's head and drink.");  
    }  

    public void hunt(Animal a){  
        System.out.println("Tiger catched " + a.getName() + " and eated it");  
    }  

}  

Snake类

class Snake extends Reptile implements Huntable{  
    private static String name = "Snake";  
    public String getName(){  
        return this.name;  
    }  

    public void move(String destination){  
        System.out.println("Snake crawled to " + destination + ".");  
    }   

    public void drink(){  
        System.out.println("Snake dived into water and drink.");  
    }  

    public void hunt(Animal a){  
        System.out.println("Snake coiled " + a.getName() + " and eated it");  
    }  
}  

Farmer类

这样的话. Farmer类里的feedAnimal(Animal ht, Animal a)就可以实现多态了.

class Farmer{  
    public void bringWater(String destination){  
        System.out.println("Farmer bring water to " + destination + ".");  
    }  

    public void bringAnimal(Animal a,String destination){  
        System.out.println("Farmer bring " + a.getName() + " to " + destination + ".");  
    }  

    public void feedWater(Animal a){  
        this.bringWater("Feeding Room");  
        a.move("Feeding Room");  
        a.drink();  
    }  

    public void feedAnimal(Animal ht , Animal a){  
        this.bringAnimal(a,"Feeding Room");  
        ht.move("Feeding Room");  
        Huntable hab = (Huntable)ht;  
        hab.hunt(a);  
    }  

}  

关键是这一句

Huntable hab = (Huntable)ht;

本文一开始讲过了, 接口的引用可以指向实现该接口的对象.

当然, 如果把Goat对象传入Farmer的feedAnimal()里就会有异常, 因为Goat类没有实现该接口. 上面那个代码执行失败.

如果要避免上面的问题.

可以修改feedAnimal方法:

public void feedAnimal(Huntable hab, Animal a){  
    this.bringAnimal(a,"Feeding Room");  
    Animal ht = (Animal)hab;  
    ht.move("Feeding Room");  
    hab.hunt(a);  
}  

这样的话, 传入的对象就必须是实现了Huntable的对象, 如果把Goat放入就回编译报错.

但是里面一样有一句强制转换

Animal ht = (Animal)hab  

反而更加不安全, 因为实现的Huntable的接口的类不一定都是Animal的派生类.

相反, 接口的出现就是鼓励多种不同的类实现同样的功能(方法)

例如,假如一个机械类也可以实现这个接口, 那么那个机械就可以帮忙打猎了(囧)

1个植物类(例如捕蝇草),实现这个接口, 也可以捕猎苍蝇了.

也就是说, 接口不会限制实现接口的类的类型.

执行输出:

[java] Farmer bring Rabbit to Feeding Room.  
[java] Snake crawled to Feeding Room.  
[java] Snake coiled Rabbit and eated it  
[java] Farmer bring Rabbit to Feeding Room.  
[java] Goat moved to Feeding Room.  
[java] Tiger catched Rabbit and eated it  

这样, Tiger类与Snake类不但继承了Animal的方法, 还继承(实现)了接口Huntable的方法, 一定程度上弥补java的class不支持多继承的特点.

总结 什么情况下应该使用接口而不用抽象类

好了, 回到本文最重要的一个问题.

做个总结

  1. 需要实现多态

  2. 要实现的方法(功能)不是当前类族的必要(属性).

  3. 要为不同类族的多个类实现同样的方法(功能).

下面是分析:

1 需要实现多态

很明显, 接口其中一个存在意义就是为了实现多态. 这里不多说了.

而抽象类(继承) 也可以实现多态

2 要实现的方法(功能)不是当前类族的必要(属性).

上面的例子就表明, 捕猎这个方法不是动物这个类必须的,

在动物的派生类中, 有些类需要, 有些不需要.

如果把捕猎方法卸载动物超类里面是不合理的浪费资源.

所以把捕猎这个方法封装成1个接口, 让派生类自己去选择实现!

3 要为不同类族的多个类实现同样的方法(功能).

上面说过了, 其实不是只有Animal类的派生类才可以实现Huntable接口.

如果Farmer实现了这个接口, 那么农夫自己就可以去捕猎动物了...

我们拿另个常用的接口Comparable来做例子.

这个接口是应用了泛型,

首先, 比较(CompareTo) 这种行为很难界定适用的类族, 实际上, 几乎所有的类都可以比较.

比如 数字类可以比较大小, 人类可以比较财富, 动物可以比较体重等.

所以各种类都可以实现这个比较接口.

一旦实现了这个比较接口. 就可以开启另1个隐藏技能:

就是可以利用Arrays.sort()来进行排序了.

就如实现了捕猎的动物,

可以被农夫Farmer喂兔子一样...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 配套视频教程 本文B站配套视频教程[https://www.bilibili.com/video/BV1fp4y1...
    __豆约翰__阅读 527评论 0 2
  • 想像成电脑接口。 接口是用来连接连个物品的,几个物品之所以用得着接口是因为他们之间有共同话题它是他们之间的桥梁用来...
    培根好吃阅读 182评论 0 0
  • 银行卡系统的要求如下: 1) 银联接口,用于描述银联统一制定的规则,该接口提供检测密码方法、取钱方法以及查询余额方...
    smallnumber阅读 443评论 0 1
  • 1、使用前缀避免命名空间冲突 选择与你、你公司、应用程序或与之皆关联之名作为类名的前缀,并在所有代码(类名, 方法...
    写代码写到人生巅峰阅读 156评论 0 1
  • 你很清楚的知道什么时候用抽象类,什么时候用接口么?p.s. 多文字预警! 1 抽象类和接口简介 1.1 抽象类 ...
    Sharember阅读 2,349评论 9 55