在这里插入图片描述

学完继承、学完多态,但面对汹涌而来🌊的接口,相信很多同学都不知所措,因此我耗费几天几夜的时间,搜寻大量书籍资料,苦心闭关钻研,写出了一篇关于Java的接口从入门小白到精通大佬的学习之路,相信这篇文章一定对您有所帮助📖

🌳接口的基本概念引入

Java接口是一系列方法的声明,是一些方法特征的集合

  • 对于接口,它是Java中一个新增的知识点,而C++中没有,因为Java有一个缺陷就是不可以实现多继承,只可以单继承,这就限制了有些功能的使用,于是为了让Java也能有这种能力,因为提出了接口的概念
  • 对于接口的基本概念,大家应该要回想一下我们上一文所讲的abstract抽象类的概念,因为接口它与抽象类非常类似,在抽象类中我们可以了解到其实除了不可以实现一些抽象方法外,其余的和正常的类没有什么本质的区别,一些常量、变量,私有、静态方法都可以定义,但是在接口中,就只能有抽象方法以及常量,而且接口中所有抽象方法的访问权限都是public公开的,因为它也算是static静态方法,所以可以省略public和static这两个关键字
  • 但是从JDK8开始,就有了一些改变,接口中也可以定义default和private关键字修饰的方法,对于default关键字修饰的方法,不可以将此关键字省略,因为在接口体中不允许定义通常的带方法体的public实例方法;而对于private修饰的方法,则是配合default默认方法进行使用,即将某些算法封装在private方法中,供接口中的实例方法调用

🌳如何去定义和实现一个接口

了解了接口的基本概念之后,是不是很想知道一个接口时怎么去定义和实现呢,让我们马上来看一看吧

🔑【interface关键字】

  • 首先最基本的定义一个接口,对于接口,如果你觉得有一些方法它们有一个功能的类名,那你就可以把它定义为一个接口,在这个接口中去定义一些抽象方法,比方是大家都会运动,运动的方式有很多,比方说跑步、游泳等等,但是你做这些事情的方式的方式和节奏和人家专业运动员又不一样,所以可以由不同的类去继承这个接口,然后去实现具体的对应的功能
public interface Sport {
    void run();     //跑步
    
    void swim();    //游泳
}
  • 这和类的定义很相似,细心的小伙伴可以看出来这是将class关键字换成inferface关键字,但是变得可不止这一种哦,接口可是有它的专属图表的🎇

IDEA👉
在这里插入图片描述
eclipse👉
在这里插入图片描述

  • 这两个Java编译器应该是大家用的最用的了,对于接口,细心的小伙伴应该可以发现,存在一个【大写的I】,这个标志就是【interface】的首字母大写了,相信这点很多人都没有发现吧

🔑【implements关键字】

  • 好,说完如何入定义一个接口,接下去就来讲讲怎么去实现一个接口吧
  • 那就是用【implements】这个关键字,通过一个具体的类去实现
public class People implements Sport{
}
  • 但是这样的话就会出现报错,这个我们在继承抽象类的时候就有说过,继承一个抽象类,就要去重写其所有的抽象方法
public class People implements Sport{
    @Override
    public void run() {
        System.out.println("我会慢跑🏃‍");
    }

    @Override
    public void swim() {
        System.out.println("我会自由泳🏊‍");
    }
}

【注意事项】

  • 对于抽象类的话,如果你用一个抽象类去继承,那么你就不用重写这个抽象方法,当然对于接口也是一样
  • 如果一个非abstract类实现了某个接口,那么这个类就必须重写该接口的所有抽象方法
  • 如果一个abstract类实现了某个接口,那么这个类可以选择重写接口中的抽象方法或者该接口的抽象方法

🌳接口特点及作用

了解了接口的定义和实现之后,接下来我们来说一说接口有哪些特点以及其具体的作用

🍃接口的特点

  1. 接口虽与抽象类相似,但是比抽象类更加抽象,却不需要写abstract关键字,因为接口中所有方法都是抽象的,因此可以省略这个关键字

  2. 接口中只可以有常量,而且都是public、static、final关键字修饰的【默认都有,可以不加】,但是不可以有变量
    在这里插入图片描述

  3. 接口没有构造方法,因此不可以用new关键字去创建接口的对象,而是要用一个具体的类去implements实现这个接口

  4. 一个类可以实现多个接口,一个接口可以继承多个接口【有点抽象,上代码】

public interface Sport {
    void run();
    void competition();
}
public interface Law {
    void rule();
}

public class PingPongMan implements Sport,Law{
    private String name;

    public PingPongMan(String name) {
        this.name = name;
    }

    @Override
    public void rule() {
        System.out.println(name + "必须遵纪守法");
    }

    @Override
    public void run() {
        System.out.println(name + "必须参加训练");
    }

    @Override
    public void competition() {
        System.out.println(name + "必须参加比赛");
    }
}
public class Test {
    public static void main(String[] args) {
        PingPongMan p = new PingPongMan("张继科");
        p.run();
        p.competition();
        p.rule();
    }
}

在这里插入图片描述

  • 上述代码实现的是一个类实现多个接口,这个类不是一个抽象类,那就要重写实现接口的中的所有抽象方法
public interface Law {
    void rule();
}
public interface People {
    void sleep();
    void eat();
}

public interface Sport extends Law,People{
    void run();
    void competition();
}
  • 好,具体测试不给出了,主要是看这个接口可以继承多个接口,可以看出Sport接口使用了extends关键字继承类Law和People这两个接口

以上的这两个特点很重要,也是弥补了Java类不能多继承的缺陷

  1. 一个类,它可以在继承父类的情况下同时实现接口
class Dog extends Animal implements Runnging,Drinking{
}

📕接口的作用和意义

有些刚刚接触Java接口的小伙伴就很疑惑,这个接口到底是用来干嘛的呢,它究竟有什么具体的作用

首先我们来总的概括一下,其实就是一句话:【定义一个公共规范,实现一个统一访问

  • 对于公共规范这个概念,就是大家都认可的,是一种标准,就像是USB(通用串行总线)一样,这个接口是很多家公司联合提出的,因此属于一个规范,在日常生活中,我们使用的很多设备都拥有USB接口,这个USB接口呢,其实就很像Java中所说的接口;
  • 如果有一个物件,比如说笔记本,拥有这个接口,也相当于是实现了这个接口,那就说明笔记本这个类有了USB这个功能,外部设备便可以与它产生一个联系,比如说最常见的U盘,只要是有USB接口的地方,那么这个U盘都可以使用,这么说大家应该有点清楚了吧,下面会更加详细地深入了解接口
  • 对于统一访问,举个例子,对于LOL这款游戏大家应该都玩过,一个英雄,是不是一定会有相同的功能,比如说攻击、点塔、补刀这些,但是LOL中157个英雄,假设它们都对应一个类,难道在每个英雄类中都去写这三个功能吗,那一定不会,这是就可以定义一个基本英雄功能接口,里面封装了所有英雄所具备的基本能力,然后所有英雄类都去访问这个接口就可以
    在这里插入图片描述

🌳接口的UML图(类图)

初步入门了接口后,接下去我们就要了解接口与它的实现类之间所存在的逻辑框架关系,也就是类图,这可以进一步帮助我们去理解接口

🍃UML图的基本概念

统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。可以帮助我们简单、统一、图形化、能表达软件设计中的动态与静态信息

🍃UML图的作用

  • 可以帮助我们清晰勾勒出一个类族的框架接口,继而对此项目整体逻辑接口更加了解
  • UML图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型

🍃接口UML图的基本结构和组成

  • 对于接口的UML图与类的UML图很类似,主要是使用一个长方形去描述一个类或接口,将这个长方形垂直地分为3层
  • 第一层是名字层,接口的字形必须是斜体字形,而且需要用<>修饰名字,格式为【接口修饰符\n接口名称】
  • 第二层是常量层,列出接口中的常量及类型,格式为【常量名字:类型】
  • 第三层是方法层,也称操作层,列出接口中的方法及返回类型,格式为【方法名字(参数列表):类型】

🎈继承关系类图与接口图的区别

  • 子类与父类的继承关系所呈现的UML类图

在这里插入图片描述

  • 接口与实现类所呈现的UML类图

在这里插入图片描述

  • 相信通过这两张UML图的对比分析,你对UML类图也有了一个基本的见解了

🌳接口回调与多态的联系

在前面将多态的时候,讲到上转型对象时我有提到过接口回调这个东西,这在接口中是比较重要的,因此做一个区分

📚权威解释

  • 对于向上转型,就是父类引用去引用子类对象
  • 而对于接口回调,就是把实现某一接口的类创建的对象的引用赋值给该接口【声明的接口变量】,那么该接口就可以调用被类实现的接口方法以及接口提供的default方法
  • 对于它们二者的区别,还要说到使用接口的核心原因:为了能够向上转型为多个基类型。即利用接口的多实现,可向上转型为多个接口基类型,从实现了某接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的
  • 所以对于接口回调,强调使用接口来实现回调对象方法使用权的功能;对于向上转型,则牵涉到多态和运行期绑定的范畴
    以上解释来自《Tinking in Java》这本书

🍃具体案例分析

说了这么多概念,您对接口回调一定还没有一个很清晰的认识,接下去我们通过一个小案例一起来看看

public interface ShowMessage {
    void show(String s);
}
public class TV implements ShowMessage {
    @Override
    public void show(String s) {
        System.out.println("tvtvtvtvtv");
        System.out.println(s);
        System.out.println("tvtvtvtvtv");
    }
}
public class PC implements ShowMessage {
    @Override
    public void show(String s) {
        System.out.println("pcpcpcpcpc");
        System.out.println(s);
        System.out.println("pcpcpcpcpc");
    }
}
public class test {
    public static void main(String[] args) {
        ShowMessage sm;
        sm = new TV();
        sm.show("TCL电视机打开了");
        
        System.out.println("-----------");

        sm = new PC();
        sm.show("Lenovo台式机打开了");
    }
}

在这里插入图片描述

  • 从这个小案例可以看出,对于接口回调,就是将一个实现接口的类所定义的对象的引用给到一个接口所声明的变量,然后上面说了,可以实现回调对象方法使用权的功能,也就是去调用子类重写接口中抽象方法

🌳函数接口与Lambda表达式

讲完了接口回调,我们再来说一下函数接口与Lambda表达式

🍃Lambda表达式

  • 这个Lambda表达式的话也是JDK8新出的,当时出个这个概念的时候备受争议,因为这简直颠覆了大家的想象,都说居然可以这么去优化一个表达式,对此表示非常地惊奇🎃
  • 接着就让我们先来了解一下这个Lambda表达式,了解一下它有什么优缺点

Lambda表达式,也可称为闭包。类似于JavaScript中的闭包,它是推动Java8发布的最重要的新特性

  • 优点

1、代码更加简洁

2、减少匿名内部类的创建,节省资源⭐

3、使用时不用去记忆所使用的接口和抽象函数

  • 缺点

1、若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势)

2、不容易调试。

3、若其他程序员没有学过 lambda 表达式,代码不容易让其他语言的程序员看懂⭐

  • 上面提到了一个叫做匿名内部类,这个我还没讲到,放在下一篇文章,大家可以先去了解一下,匿名内部类,这是内部类的一种

🍃函数式接口

然后我们再来了解一下函数式接口

  • 首先必须是接口、其次接口中有且仅有一个抽象方法的形式⭐
  • 通常我们会在接口上加上一个@FunctionalInterface注解,标记该接口必须是满足函数式接口⭐

定义方法如下

@FunctionalInterface   //一旦加上这个注解必须是函数式接口,里面只能有一个抽象方法
interface Swimming{
    void swim();
    //void run();
}
  • 首先根据上面这个接口,我们去实现一个匿名内部类
public class LambdaDemo1 {
    //Lambda表达式只能简化函数式接口的匿名内部类的写法形式
    //Lambda表达式只能简化接口中只有一个抽象方法的匿名内部类形式
    public static void main(String[] args) {
        //实现了Swimming这个接口
       Swimming s1 = new Swimming() {
            @Override
            public void swim() {
                System.out.println("老师游泳贼溜");
            }
        };
        go(s1);
        go(new Swimming() {
            @Override
            public void swim() {
                System.out.println("学生游泳很开心");
            }
        });
    }

    public static void go(Swimming s){
        System.out.println("开始。。。");
        s.swim();
        System.out.println("结束。。。");
    }
}

对于如何去进行一个简化,我们需要先了解其规则

🍃简化规则定义

  • 参数类型可以省略不写
  • 如果只有一个参数,参数类型可以省略,同时()也可以省略
  • 如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写,同时要省略分号!
  • 如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写。此时,如果这行代码是return语句,必须省略return不写,同时也省略“;”不写

然后我们就对上面的代码进行一个简化

Swimming s1 = new Swimming() {
//    Swimming s1 = () ->{			//简化版
//        System.out.println("老师游泳贼溜");
//    };

	Swimming s1 = () -> System.out.println("老师游泳贼溜");		//最终版
	go(s1);

到这里大家可能还是没有看懂,那我们再来多看几个,就能懂了

public class LambdaDemo2 {
    public static void main(String[] args) {
        Integer[] ages = {66,99,33,78,12};

        //Arrays.sort(ages);        默认升序

        Arrays.sort(ages, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;     //降序
            }
        });

//        Arrays.sort(ages, (Integer o1, Integer o2) ->{
//                return o2 - o1;     //降序
//            }
//        );

//        Arrays.sort(ages, (o1, o2) ->{
//                    return o2 - o1;     //降序
//            }
//        );

        Arrays.sort(ages, (o1, o2) -> o2 - o1);
        System.out.println("排序后的内容为:" + Arrays.toString(ages));
    }
}
  • 好,我们来详细地说明一下,对于Arrays这个API的数组排序,相信大家用的是最多的,默认是升序,这里是进行了一个重写然后令其可以实现降序输出
  • 首先,应该去掉的就是,因为我们只需要这个匿名内部类的形参列表和方法体代码,然后要加上一个【—>】箭头,要注意,这个箭头可不是C/C++里面的指针
    new Comparator() {
    @Override
    public int 这段代码
  • 然后根据第一条规则可以知道, 参数类型可以省略不写,所以只留下(o1, o2)
  • 接着就是省略这个reutrn和语句后面的分号“;”
  • 最终的简化结果就是Arrays.sort(ages, (o1, o2) -> o2 - o1);

再来一个有关按钮监听事件ActionListener的匿名内部类形式简化

//Lambda表达式简化按钮监听器ActionListener的匿名内部类形式
public class LambdaDemo3 {
    public static void main(String[] args) {
        JButton btn = new JButton("登录");
        //给登录按钮绑定点击时间监听器
//        btn.addActionListener(new ActionListener() {
//            @Override
//            public void actionPerformed(ActionEvent e) {
//                System.out.println("登录一下~~~");
//            }
//        });
//
//        btn.addActionListener((ActionEvent e) ->{
//                System.out.println("登录一下~~~");
//            }
//        );
//
//        btn.addActionListener((e) ->{
//            System.out.println("登录一下~~~");
//            }
//        );

        btn.addActionListener( e -> System.out.println("登录一下~~~"));
    }
}
  • 首先是一样,省略这一段代码
    new ActionListener() {
    @Override
    public void actionPerformed
  • 接着是省略形参值ActionEvent
  • 最根据第二条规则,如果只有一个参数,参数类型可以省略,同时()也可以省略,省略e外的小括号
  • 最终形式便是btn.addActionListener( e -> System.out.println(“登录一下~~~”));

好,又看了两个小案例,这些您对Lambda表达式简化匿名内部类有了一个基本的认识了,接下来我们说一些小贴士

💡细心小贴士

  • 如果大家细心的话,对于有些方法,按住ctrl键鼠标点进去,就可以看到这是一个函数式接口,如果看到了【@FunctionalInterface】注解,那就表明这个匿名内部类可以使用Lambda表达式来简化,点进我们刚才那个sort()排序的Comparator接口,就可以看到这个注解

在这里插入图片描述

在这里插入图片描述

🌳深入理解接口【面向接口的思维】

了解接口后,我们要开始第二层次,也就是理解接口,首先就是要进行思考,提出相应的问题

❓提问一:为什么不在一个类中直接实现相应的方法,而是要先进行接口抽象呢?

  • 答:这样会造成代码冗余,众多类中都有相同的一个功能,只是调用的对象不同,继而产生内存浪费。这时就可以使用接口去封装这个功能,通过父类引用去接收子类对象,从而实现多态

❓提问二:接口的真正用处在哪里,用接口可以帮助我们实现什么?

  • 答:对于一个接口,上面在讲概念的时候有提到一些,就类似于一个功能库一般,在这个功能库中呢,你可以定义许多别人可能会用的到的功能,这样当别人有需要时,便无需去继承一个抽象类导致类族群混乱,或者自己重写定义一个方法导致增加内存。完全可以把大家都会用得到的功能,并且可以实现多态的功能放入此方法库,在定义这个方法的时候完全无需去考虑它是如何实现的,只需要定义好其标准以及参数的设定
  • 因此可以看出,在一个项目开发时,拥有一些实用的接口是多么重要,既能有一个统一的规范、有一个严格的标准,而且还可以提高开发的效率,减少类族的复杂性,上面也讲到过,接口其实很好地弥补了Java无法多继承这个缺陷,当你继承了一个父类,但是又不想再添加一个祖先父类的抽象类,将类族混乱。接口就是一个很好的选择,可以帮助我们实现想要实现的功能

❓提问三:接口与抽象类如此地相似,为什么有的时候要使用接口而不用抽象类呢?

  • 答:对于抽象类,它可以让自己的引用去接收子类对象;对于接口,它可以让自己声明的变量,一样去接收子类对象,它们都可以在获取到子类对象后调用子类重写的抽象方法,继而实现多态。
  • 但是对于抽象类,它始终都是一个类,是需要被继承才能让子类去重写自己所拥有的抽象方法,但是当一些子类继承了一些父类拥有但是自己却不需要的功能时,这时候就会出问题,造成内存浪费。如果当我们仅仅是为了实现多态,而且又是很多类都需要这个功能,却不想要去继承一个父类获取这个方法,就可以将此方法写入接口,通过【implements】这个关键字去实现这个接口,继而在类内重写这个抽象方法来实现多态

🌳接口的实战项目演练

在本小节中,我会设置三个实战项目,从浅入深,层层递进,帮助大家来更好地理解接口在实际的应用中到底是如何去使用的

“Hello World”【⭐】

看到这个标题,你不会真的以为只是输出【Helllo World】吧,那是不会的,只是这个案例作为第一个,比较简易一些,帮助大家来回顾上面所学的知识点

上代码

public interface SpeakHello {
    public void speak();
}
public class CPeople implements SpeakHello{
    @Override
    public void speak() {
        System.out.println("你好");
    }
}
public class EPeople implements SpeakHello{
    @Override
    public void speak() {
        System.out.println("Hello");
    }
}
public class Hello {
    public void lookHello(SpeakHello speakHello){
        speakHello.speak();     //实现接口回调
    }
}
public class test {
    public static void main(String[] args) {
        Hello hello = new Hello();

        hello.lookHello(new CPeople());
        hello.lookHello(new EPeople());

        hello.lookHello(new SpeakHello() {
            @Override       //匿名内部类
            public void speak() {
                System.out.println("Hello World");
            }
        });

        //简化版
        hello.lookHello(() -> System.out.println("H W"));	//Lambda表达式
    }
}

在这里插入图片描述

  • 第一个实战项目,作为我们对于上述所讲的接口的基本定义和实现以及接口回调、Lambda表达式做一个总结
  • 首先,说话是一个能力,将其封装成接口的形式,无论是中国人还是英国人都会说话,因此都去实现了这个接口
  • 然后通过一个普通类去搭建接口和实现类之间的关系,将接口所声明的变量作为形参,在主方法中通过调用这个类的方法传入实现类的对象去实现一个接口的回调,继而去展现出多态
  • 然后下面,我又使用了匿名内部类的形式去直接重写了这个接口中的抽象方法,从运行结果可以看出,也能呈现出同样的效果
  • 最后一种,则是对匿名内部类做一个简化操作,这就要使用到我们上面所提到的Lambda表达式了,具体规则不细讲,如果有点遗忘的话可以再翻上去看看

新能源时代【⭐⭐⭐】

很夸张的一个标题,突然脑洞打开想到的,哈哈,和项目也是有一些联系🔒

上代码

public abstract class MotorVehicles {
    abstract void brake();      //刹车功能
}
public interface ControlTemperature {	//控制温度
    void controlAirTemperature();
}
public interface MoneyFare {	//收取费用
    void charge();
}
public class Bus extends MotorVehicles implements MoneyFare{
    @Override
    void brake() {
        System.out.println("公交车正在刹车");
    }

    @Override
    public void charge() {
        System.out.println("公交车正在计费");
    }
}
public class Taxi extends MotorVehicles implements MoneyFare,ControlTemperature{
    @Override
    void brake() {
        System.out.println("出租车正在刹车");
    }

    @Override
    public void charge() {
        System.out.println("出租车正在计费");
    }

    @Override
    public void controlAirTemperature() {
        System.out.println("出租车正在调节空调温度");
    }
}
public class Cinema implements MoneyFare,ControlTemperature{
    @Override
    public void charge() {
        System.out.println("电影院开始收费");
    }

    @Override
    public void controlAirTemperature() {
        System.out.println("电影院正在调节室内温度");
    }
}
public class test {
    public static void main(String[] args) {
        //1.三个类定义对象
        Bus bus = new Bus();
        Taxi taxi = new Taxi();
        Cinema cinema = new Cinema();

        //2.两个接口声明对象
        MoneyFare moneyFare;
        ControlTemperature controlTemperature;

        moneyFare = bus;
        bus.brake();
        moneyFare.charge();

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

        controlTemperature = taxi;
        moneyFare = taxi;
        taxi.brake();
        moneyFare.charge();
        controlTemperature.controlAirTemperature();

        System.out.println("--------");
        
        moneyFare = cinema;
        controlTemperature = cinema;
        moneyFare.charge();
        controlTemperature.controlAirTemperature();

        //上述操作有体现接口回调
    }
}

在这里插入图片描述

  • 这个项目,很明显比上一个小项目稍微复杂了一些,进一步地带大家理解接口,区分接口与抽象类之间的关系,明白接口为什么可以弥补多继承的缺口
  • 首先定义了一个机动车抽象类,里面含有一个刹车方法,对于公交车、出租车都是属于机动车,所以应该让这两个类去继承机动车这个父类,而且他们都可以有【刹车】这一个功能,但是对于电影院类,则不属于机动车,只是一个独立与机动车类族的一个单独类,但是对于【控制温度】和【收取费用】这两个功能电影院也是需要有,可是呢,电影院不会刹车呀,比如放一些爱情片你说怎么刹车,就这么放下去了,管你下面有没有小孩(🐶)
  • 难道为此再去定义一个抽象类例如多功能电影院吗,这如果实在我们这种小项目是没关系,但如果这个场景放在实际企业开发中,新定义一个抽象类这种行为是具有风险性的,因为随着抽象类的增加,就需要重新构建上层类族之间的关系,就会使得整个项目的框架逻辑变得更加复杂,这种想到增加功能就去多写一个抽象类的行为是不可取的,
  • 因此我们就想到了接口这个东西,我们可以将收取费用和控制空调温度封装到一个功能接口中,或者分开定义也可以。这样的话,影院就不需要去继承机动车类,而是只需要实现这两个接口即可
  • 对于主方法中的一些操作,我都清晰地将每个类分割开来,通过接口所声明的变量去接收实现类的对象,继而去呈现一个接口的回调,从运行结果也可以很清晰地看出

疯狂农场【⭐⭐⭐⭐⭐】

首先声明,这个项目不是我的,是从一位大佬那里拿来的,我将其重新实现做一个讲解,这是他的博客链接,大家可以去看看

上代码

public abstract class Animal {
    public abstract String getName();
    public abstract void move(String destination);
    public abstract void drink();
}
public abstract class Mammal extends Animal{
}
public abstract class Reptile extends Animal{
}
public interface Huntable <T>{  //泛型接口
                //不仅是动物可以捕猎,其他生物也是可以捕猎(增加了广泛性)
    void hunt(T o);

    public default int max(int a,int b){
        return a > b ? a : b;
    }
}
public class Tiger extends Mammal implements Huntable<Animal>{
    private String name = "Tiger";

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void move(String destination) {
        System.out.println(name + " move to the " + destination);
    }

    @Override
    public void drink() {
        System.out.println("Tiger lower it's head and drinks");
    }

    @Override
    public void hunt(Animal animal) {
        System.out.println("Tiger catched the " + animal.getName() + " and eat it");
    }
}
public class Goat extends Mammal{
    private String name = "Goat";

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void move(String destination) {
        System.out.println(name + " move to the " + destination);
    }

    @Override
    public void drink() {
        System.out.println("Goat lower it's head and drinks");
    }
}
public class Rabbit extends Mammal{
    private String name = "Rabbit";

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void move(String destination) {
        System.out.println(name + " move to the " + destination);
    }

    @Override
    public void drink() {
        System.out.println("Rabbit put out it's tongue and drinks");
    }
}
public class Snake extends Reptile implements Huntable<Animal>{
    private String name = "Snake";

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void move(String destination) {
        System.out.println(name + " move to the" + destination);
    }

    @Override
    public void drink() {
        System.out.println(name + "dived into water and drinks");
    }

    @Override
    public void hunt(Animal animal) {
        System.out.println("Snake catched the " + animal.getName() + " and eat it");
    }
}
public class Farmer {
    public void BringWater(String desination)
    {
        System.out.println("Farmer bring the water to the " + desination);
    }

    public void FeefWater(Animal animal)
    {
        BringWater("Farm");
        animal.move("Farm");
        animal.drink();
        System.out.println("----------");
    }

    public void work()
    {
        Farmer farmer = new Farmer();
        Tiger tiger = new Tiger();
        Goat goat = new Goat();
        Rabbit rabbit = new Rabbit();

        farmer.FeefWater(tiger);
        farmer.FeefWater(goat);
        farmer.FeefWater(rabbit);
    }

    public void BringAnimal(Animal animal,String desination)
    {
        System.out.println("Farmer bring the " + animal.getName() + " to the " + desination);
    }

//    public void FeedAnimal(Animal hunter,Animal prey)
//    {
//        BringAnimal(prey,"Farm");
//        hunter.move("Farm");
//        Huntable huntable = (Huntable) hunter;
//        huntable.hunt(prey);
//        System.out.println("----------");
//    }

    public void FeedAnimal(Huntable huntable,Animal prey)
    {
        BringAnimal(prey,"Farm");
        Animal animal = (Animal) huntable;
        animal.move("Farm");        //多态
        huntable.hunt(prey);                  //接口回调
        System.out.println("----------");
    }
}
public class test {
    public static void main(String[] args) {
        Farmer farmer = new Farmer();

        farmer.work();

        Tiger tiger = new Tiger();
        Snake snake = new Snake();
        Rabbit rabbit = new Rabbit();
        Goat goat = new Goat();

        farmer.FeedAnimal(tiger,goat);
        farmer.FeedAnimal(snake,rabbit);
    }
}

对于这个项目,我觉得对于接口的深入理解非常好的一个项目,我这里给大家做一个详细的分析

work()方法的运行结果

在这里插入图片描述

  • 首先是Animal动物这个总的抽象类,拥有三个方法,一个是获取名称,第二个是移动到具体的地点,第三则是喝水
  • 然后是它的两个继承抽象类,哺乳类动物Mammal和爬行类动物Reptile,因为他们均为抽象类,上面实现接口的注意事项时,有提到如果一个抽象类去继承另一个抽象类,那么它不需要重写其中的抽象方法
  • 接着就是哺乳类动物的三个继承类老虎类、山羊类以及兔子类,爬行类动物的一个继承类蛇类,然后分别通过重写它们的祖先类中的抽象方法实现自己独有的功能
  • 最后是一个农夫类,因为农夫可以将水带到农场给这些动物喝,这些动物只需要移动到农场即可
  • 通过看Farmer类【伐木累】中的work()方法,通过将各种动物所声明的对象传入FeedWater()方法,从而实现了一个多态的效果,只需要通过父类引用去接收子类对象就可以实现子类具体的功能

FeedAnimal方法的运行结果

在这里插入图片描述

思路分析

  • 首先是思路方面的分析。上面只是引入,就下来就真正涉及到接口了,农夫除了可以将水带到农场之外,还可以将一些猎物带到农场供那些会狩猎的动物进食,那就需要就【狩猎】这个功能,但是对于狩猎,并不是每个动物都具有的,比如说山羊、兔子就不具有,因此不可以将此方法定义在抽象类Animal或Mammal中,否则会造成内存浪费
  • 那该怎么办?因为狩猎这个行为,老虎和蛇是不同的,那难道就在它们各自的类中新增这么一个方法体吗,这就要分析了,因为不仅仅是老虎、蛇,后面这个农场可能还会新增其他的动物,比如说豹子、鳄鱼、狮子这些,它们都会捕猎,难道之后新增动物类也要将这些方法重新写在它们里面吗,这只会造成程序的内存越来越大,就会产生一系列的栈溢出、内存溢出之类的问题
  • 这个时候就要使用到我们的接口了,首先这个功能是很多类都会使用到的,而且这个功能并不是当前这个类族的每个类成员都会使用到的,所以仅仅是为了有这个功能,为了实现一个多态性,将其定义为一个接口

代码分析

  • 其次是代码层面的分析,为了方便观看,将关键代码继续粘入此处👇
public void BringAnimal(Animal animal,String desination)
{
    System.out.println("Farmer bring the " + animal.getName() + " to the " + desination);
}

//    public void FeedAnimal(Animal hunter,Animal prey)
//    {
//        BringAnimal(prey,"Farm");
//        hunter.move("Farm");
//        Huntable huntable = (Huntable) hunter;
//        huntable.hunt(prey);
//        System.out.println("----------");
//    }

public void FeedAnimal(Huntable huntable,Animal prey)
{
    BringAnimal(prey,"Farm");
    Animal animal = (Animal) huntable;
    animal.move("Farm");        //多态
    huntable.hunt(prey);                  //接口回调
    System.out.println("----------");
}
  • 主要是对焦在Farmer农夫这个类,对于BringAnimal()这个方法,需要的参数是农夫需要携带的猎物,以及将猎物带往的目的地
  • 然后是FeedAnimal()这个方法,可以看到这个是有两种方法,第一种被我注释掉了,传入的是狩猎的动物以及猎物,然后为什么要将其强转成接口类型呢,因为捕猎这个方法是在Tiger()和Snake()里的,而原始的抽象类Animal类并没有,因此要将其转为接口类型,然后通过接口回调的方式去实现多态,但是这里有一点漏洞,万一传入的这个捕猎者是其他没有捕猎能力的动物,那就会出问题了
  • 所以第二种方案,我就将此狩猎者的对象直接给到接口,直接用接口去实现一个回调,只是在狩猎者在移动的时候需要转换成Animal类罢了,这样就增加了安全性,外界就无法传入没有实现Huntable接口的实现类到这个FeedAnimal中了
  • 最后相信大家对接口中的这个 <>有所异或吧,这个是Java里的泛型接口,这样hunt(T o)便可以接收多种参数,因为不一定是动物可以捕猎,人也可以捕猎,有些植物像捕蝇草也是可以捕猎的,只是所捕的猎物不同罢了,所以当你将一个接口定义成泛型接口时,要在所实现这个接口的地方加上接口的泛型标志,也就是这里Tiger和Snake类的写法
public class Tiger extends Mammal implements Huntable<Animal>
public class Snake extends Reptile implements Huntable<Animal>

OK,说完这三个实战案例,相信你对Java中的接口如何使用变得更加清楚,知道了接口的真正作用,了解到定义接口的真正含义

🌳总结与提炼

看到这里【接口】,你一定是学会了,让我们来做一个总结。

  • 首先我们先了解了什么是接口以及如何是定义并实现一个接口,然后去初步了解了接口的特点及作用。
  • 有了这些基本知识后,就需要的知识进行一个串联,明白UML类图、继承类图和UML类图之间的细小差别。而且还要和前面的多态进行一个联系,明白上转型对象和回调函数之间的相似和差异性
  • 当知识有了一个串联之后,就要更加进一步去了解接口中的门道,首先我们是知道了用Lambda表达式简化匿名内部类这个操作,提高了开发效率。接着我们便开始深入理解接口,对小伙伴们在日常中对接口可能会碰到的问题做了一个罗列,当前肯定还要其他的,可以在评论区继续提出来,我会解答的
  • 到了最后,学完了所有知识,就要上战场了,我分别是浅到深设置了三个实战项目,更好地帮助大家进一步地理解接口、真正灵活地去使用Java中的接口

以上就是本文所有展现的所有内容,感谢您对本文的观看,如果疑问请于评论区留言或私信指出,谢谢🌷

以下是我开创的社区,感兴趣的小伙伴们可以加入进来一起交流学习,解决编程的难题

我的社区:🔥烈火神盾🔥

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐