《Java8学习笔记》读书笔记(八)

第7章 接口与多态

学习目标

  • 使用接口定义
  • 了解接口的多态操作
  • 利用接口枚举常数
  • 利用enum枚举常数

7.1 何谓接口

接口是在面向对象思想的发展之中逐渐发展出来的。正如前面所说过的继承的隐患,当类继承树太深的时候,会导致项目加载到内存的代码过多,这样其实并不利于程序的执行效率。而接口的提出,可以显著的降低内存的占用,同时还能保持团队合作过程中的效率,降低错误的发生。
接口就是一种协议的签定,就是一种行为的定义,下面我们用一个飞行游戏来说明继承、接口与多态的应用。

7.1.1 接口定义行为

今天我们要开发一个飞行游戏,当中所有的东西都要会飞。一提到飞行,大家首先想到的是鸟,再根据上一章所学到的继承和多态,所以我们肯定会有一个父类Bird,它具有一个fly()方法:

package cn.speakermore.ch07;

/**
 * 飞行乐园基类Bird
 * @author mouyong
 */
public abstract class Bird {
    protected String name;
    public Bird(){
        name="鸟";
    }
    public Bird(String name){
        this.name=name;
    }

    /**
     * @return 字符串,鸟的名称
     */
    public String getName() {
        return name;
    }
    
    /**
     * 抽象方法,飞行,子类必须重写
     */
    public abstract void fly();
    /**
     * Object类方法重写toString
     * @return 字符串,对鸟的属性的简单输出,方便测试
     */
    public String toString(){
        return "我是一只"+name;
    }
}

由于每种鸟的飞行方式都不一样,所以把fly()设置为abstract,同时Bird也就是一个抽象类。下面我们来定义一只鸡,它要继承Bird,并重写fly()方法:

package cn.speakermore.ch07.impl;

import cn.speakermore.ch07.Flyable;
/**
 * 鸟的一个子类,鸡的实现
 * @author mouyong
 */
public class Chicken extends Bird{
    public Chicken(){
        super("小鸡");
    }
    public Chicken(String name){
        super(name);
    }
    @Override
    public void fly() {
        System.out.println(name+"鸡拍了拍翅膀,并没有飞起来");
    }
    @Override
    public String toString(){
        return super.toString()+"(其实是只鸡)";
    }
    
}

或许你还实现了海鸥,老鹰类:它们也都继承了Bird:

package cn.speakermore.ch07.impl;

import cn.speakermore.ch07.Flyable;
/**
 * 海鸥类,继承自鸟类
 * @author mouyong
 */
public class Seagull extends Bird{
    public Seagull(){
        super("海鸥");
    }
    public Seagull(String name){
        super(name);
    }
    @Override
    public void fly() {
        System.out.println("海鸥"+name+"飞得很潇洒,帅气");
    }
    @Override
    public String toString(){
        return super.toString()+"(其实是只海鸥)";
    }
}
package cn.speakermore.ch07.impl;

import cn.speakermore.ch07.Flyable;

/**
 * 老鹰类,继承自鸟类
 * @author mouyong
 */
public class Eagle extends Bird{
    public Eagle(){
        super("鹰");
    }
    public Eagle(String name){
        super(name);
    }
    @Override
    public void fly() {
        System.out.println("老鹰"+name+"翱翔在高空中,俯视众生");
    }
    @Override
    public String toString(){
        return super.toString()+"(其实是只老鹰)";
    }
    
}

到目前为止,一切顺利,没什么毛病,直到老板说话了,蝙蝠也会飞呀,写个蝙蝠吧。于是你就又定义了一个Bat类,继承自Bird。怎么感觉怪怪的?似乎有哪里不对?不过编译器并没有提出什么意见,它是通过了编译的。


图7.1 蝙蝠是只鸟?!
但是你生物学家的同事嚷嚷起来了:这不科学!上一章不是说父子类在表达“是一种”的意思么?蝙蝠是一种鸟?!这不是很荒谬么?
程序上没问题,可以执行,但是逻辑上是不对的。如果老板再说,来个战斗机吧!于是,你又写一个Fighter,继承自Bird?战斗机是一种鸟?!如果我们按这样的思路继续下去,到最后会让你及你的后继者、合作者吐血的,有违常规思维呀。
看来我们一开始的起点是有些问题的,对需求的理解是不到位。让我们一起来回顾下需求。今天我们需要完成一个飞行游戏,游戏里的所有东西都能飞。可是,当我们定义了Bird的时候,Bird并不是“所有东西”,而fly()方法在这里也仅仅只属于Bird,这些会让我们的程序的可扩展性带来问题。因为通过上面的分析我们已经看出来:飞行并不只是鸟类才具备的能力。
对的,当发现这点的时候,我们会意识到,fly()这个方法不应该与Bird绑定,它应该是独立出来的,这样任何东西需要飞,只要实现fly()这个方法就可以了。那如何分离呢?答案是:使用接口。
定义一个接口,我们使用interface关键字,接口里的方法默认都是public abtstract的:

package cn.speakermore.ch07;

/**
 * 飞行接口 Flyable
 * @author mouyong
 */
public interface Flyable {
    public abstract void fly();
}

这里我们创建了我们的第一个接口Flyable。在英文里,able是一个表示能力的后缀词,这里翻译过来就是“飞行的能力”。这也是接口产生的意义,很多的方法并不和特定的类绑定,它们很可能是需要通用的,它们通常都代表了一种能力(飞行的能力,游戏的能力,复制自身的能力等等)。这些能力在Java代码里,常常写成接口,用接口的方式来表达含义。
当我们有了Flyable这个接口之后,我们发现Bird的fly()方法其实多余了,我们可以让Bird继承Flyable,就可以让Bird具备了fly()方法,类继承接口,我们使用implements关键字。

package cn.speakermore.ch07;

/**
 * 飞行乐园基类Bird
 * @author mouyong
 */
public abstract class Bird implements Flyable{
    protected String name;
    public Bird(){
        name="鸟";
    }
    public Bird(String name){
        this.name=name;
    }

    /**
     * @return 字符串,鸟的名称
     */
    public String getName() {
        return name;
    }
    
    /**
     * Object类方法重写toString
     * @return 字符串,对鸟的属性的简单输出,方便测试
     */
    public String toString(){
        return "鸟,名字叫:"+name;
    }
}

使用implements关键字完成继承接口后,子类必须重写fly()方法,但是因为我们的Bird类仍然保持了abstract关键字,所以并不需要去重写fly(),这也是对的,因为Bird并不知道究竟要如何实现fly(),所以它也就不需要再去实现了。(默然说话:不过,如果你的子类是一个非abstract的类,接口中的所有方法都是必须重写的。
通过前面的变化,我们还会发现,虽然设计思路发生了改变,我们扩展了飞行这种能力,它不再由鸟类所独有了,但是,它并没有影响到我们已经实现过的代码,原有代码保持不变,不用作任何修改。并且,我们可以很容易加上蝙蝠类,蝙蝠不是鸟,它不需要继承Bird,但是它具备飞行的能力,它可以通过继承Flyable接口来实现这一点:

package cn.speakermore.ch07.impl;

import cn.speakermore.ch07.Flyable;
/**
 *
 * @author mouyong
 */
public class Bat implements Flyable{
    private String name;
    public Bat(){
        this.name="蝙蝠";
    }
    public Bat(String name){
        this.name=name;
    }
    @Override
    public void fly() {
        System.out.println("蝙蝠"+name+"在夜空中无声的飞行。");
    }
    
}

蝙蝠没有继承Bird,所以它不是一种鸟,但是它具备飞行的能力。


图7.2 总算解决了蝙蝠的逻辑问题
从Java的语义来说,继承在表达“是一种”的关系,而接口则是表达“具备某种能力”的意思。所以,蝙蝠继承了Flyable接口,表示它会飞,但没有继承Bird,表示它不是一种鸟。
提示:在C++中,类是允许进行多继承的,也就是子类可以拥有两个以上的父类。由于Java考虑到多继承很容易导致设计上的考虑不周而引来很多麻烦,所以只允许进行单继承,也就是说,Java的子类只能有一个父类。这样一来,Java更强调父类在表达“是一种”,接口用来表达“具备某种能力”。由于能力是可以有很多的,所以Java类可以继承多个接口。而区分了这一点,对于解决“什么时候继承类,什么时候继承接口”这个问题会很有帮助。
一般来说,Java的继承接口的确是多重继承的一种变向支持,但在JDK8之前,Java的接口只能定义抽象方法,不能有任何的实现,这也是Java为了减少复杂度而做出的限制,但在JDK8中,为了解决随着时代的发展而出现的设计上的一些不便之处,Java又放宽了一些限制,接口中也可以有条件地进行方法实现,这也是为了支持Lambda新特性的引入。这在第12章中会再次讨论。

7.1.2 接口的多态

在6.1.2小节,我们曾经判断过哪些多态语法是可以通过编译的(默然说话:还记得么?子转父,自动转,父转子,不安全,强制转),因为接口也是有继承的特点,所以我们同样也要来判断一下,以下的语句哪些是可以通过编译的:

Flyable fly1=new Bat();
Flyable fly2=new Seagull();
Flyable fly3=new Plan();

这三行代码都能通过编译,判断的依据就是“右边是不是具备左边的能力”。Seagull也具备Flyable的能力?对的,因为Bird继承了Flyable接口,所以Bird具备了飞行的能力,而Seagull继承了Bird,它既是一种Bird,也具备Flyable的能力。(默然说话:子转父,自动转!)
我们再来看二行代码:

Flyable flyer=new Bat();
Bat bat=flyer;

第一行刚刚已经讨论过,是正确的。而第二行呢?一个飞行者是不是一定是Bat?那可不一定,它还可能会是Plan。所以,编译失败。(默然说话:额,编译器其实就是这样,它不会结合上下文来考虑,所以即使这两句代码明显是没有问题的,flyer肯定不会是Plan,而是bat,但是…。唉,算了呗,忍了。
这个编译错误其实同样可以使用强制类型转换搞定。(默然说话:父转子,不安全,强制转

Flyable flyer=new Bat();
Bat bat=(Bat)flyer;

了解了以上的情况之后,我们来实战一下,看看如何发挥多态的力量。我们需要一个调用飞行的通用代码。如果不考虑我们现在所学,你也许会这样来完成:

    public void doFly(Bird bird){
        bird.fly();
    }
    public void doFly(Bat bat){
        bat.fly();
    }
    public void doFly(Plan plan){
        plan.fly();
    }

这样不断重载方法虽然可以解决通用的飞行方法,但是这也会造成重复代码的不断膨胀。其实,考虑到我们刚刚学过的,接口其实也有与类相似的类型转换,那么,我们的通用飞行方法,真的是可以写成一个方法的,象这样。

public void doFly(Flyable flyer){
        flyer.fly();
}

我甚至写了一个测试程序来测试它可不可行。

public class CommonFly {
    
    public static void doFly(Flyable flyer){
        flyer.fly();
    }
    public static void main(String[] args){
        doFly(new Seagull("小红"));
        doFly(new Eagle("卡伊"));
        doFly(new Bat("德古拉"));
        doFly(new Plan("P51"));
    }
}

测试结果:


图7.3 通用飞行方法测试
很明显,只要是实现了Flyable接口的类,都可以作为参数传递给该方法,这样我们的程序的可维护性显然提高了许多,这就是多态的力量。

7.1.3 解决需求变化

经常听见有人说代码要易维护,易扩展(默然说话:比如我就经常这样说。)。这句话是什么意思呢?意思就是说,如果增加新的需求,我们无须修改原有代码(默然说话:原有代码谁知道是哪个鬼老儿写的?与其改,不如重新写呢。),这就叫易修护(默然说话:只写新的,其实是零维护了),易扩展的意思了。
比如客户突然想不但要能飞,还要会打洞。有的东西能飞,有的东西能打洞,有的东西既能飞又能打洞。那我们现有的程序能否适应这个变化呢?
根据前面所说,接口表达一种能力,“有的东西能打洞”,不正是在描述,打洞就是一种能力么?所以很容易,我们添加一个接口Digable。里面会有digHole()方法。

package cn.speakermore.ch07;

/**
 * 打洞的接口,提供打洞的能力
 * @author mouyong
 */
public interface Digable {
    public abstract void digHole();
}

穿山甲会打洞,我们可以写一个类Manis,实现Digable接口,重写digHole()方法。

package cn.speakermore.ch07.impl;

import cn.speakermore.ch07.Digable;

/**
 * 穿山甲,实现Digable接口
 * @author Administrator
 */
public class Manis implements Digable {
    private String name;
    public Manis(){
        name="穿山甲";
    }
    public Manis(String name){
        this.name=name;
    }
    @Override
    public void digHole() {
       System.out.println("穿山甲"+name+"一下就打出去十米");
    }
}

蝼蛄(默然说话:很多地方都叫它土狗。)既会打洞,又会飞,并且它明显不是一只鸟。而接口是可以多继承的,所以我们可以写一个类Mayfly,让它同时实现Digable和Flyable接口,同时具备飞行和打洞的能力。

package cn.speakermore.ch07.impl;

import cn.speakermore.ch07.Digable;
import cn.speakermore.ch07.Flyable;

/**
 *
 * @author Administrator
 */
public class Mayfly implements Digable,Flyable {
    private String name;
    public Mayfly(){
        name="土狗";
    }
    public Mayfly(String name){
        this.name=name;
    }
    @Override
    public void digHole() {
        System.out.println("蝼蛄"+name+"打了一个小小的洞,仅为了藏身");
    }

    @Override
    public void fly() {
       System.out.println("蝼蛄"+name+"一蹦一跳的短距离飞行");
    }
    
}

这就是目前我们的代码结构所提供的易维护性和易扩展性。你可以看出,每次有新的需求,我们都是通过添加的方式来完成的,而不是修改原有结构。而接口就提供了这种思路和可能性。


图7.4 任何一个项目里的类和接口,都会形成一个相互联系的网络
当然需求是会超出我们个人的想象空间的,就是那句话:需求无止境。我们的程序可以满足一些需求,但并不能保证可以很容易的就满足所有的需求。所以,一开始要如何设计才会有弹性,是必须靠经验与分析判断的,如果本身程序就不会有很多的扩展,那就不需要考虑太多的弹性。为了保留弹性而去设计弹性的做法我们也是不赞成的,通常把它称为过度设计。因为过大的弹性表示过度预测了需求,如果你发现在你的程序存在某个接口,它一直都没有被使用,或者在画设计图的时候(默然说话:哦,设计图就是我们前面贴过那些UML图,例如图7.1和7.2就是UML中的类关系图)发现某些接口与整个程序没有发生任何实线或虚线的联接,那么很可能意味着,这是一个过度设计。这时我们需要考虑是否应该把这部分删除,以保持系统的整洁和完整。总之,程序需求弹性不足或者过度设计了弹性都是不可取的,而一个相对良好的弹性设计就是既能保持目前的程序进度推进,又能保证某个有限未来(默然说话:我一般会考虑未来2-3年这样一个有限未来)维护与扩展的需要。而这一切,的确需要长期的实践、观察与思考,积极足够的经验才能做到的。

7.2 接口语法细节

前面介绍了接口的基础概念和语法,如:关键字是interface,使用implement来实现,一个子类可以实现多个接口,接口方法都是抽象的,子类必须全部重写,重写时访问权限只能放大,不能缩小等等。这一节更深入一些向大家介绍接口的一些细节,比如匿名内部类,接口中的常量等内容

7.2.1 接口的默认

在Java中,可以使用interface来声明接口,如接口中的方法可声明为public abstract。例如:

public interface Digable {
    public abstract void digHole();
}

其实你也可以把接口里的方法前的访问修饰词省略,写成这样:

public interface Digable {
    void digHole();//默认就是public abstract
}

有的同学可能很喜欢这样写,并认为自己这样做挺聪明的。但是我想告诉你的是,我极不赞成这样来写代码。因为这样会导致认识上的误差,最终导致代码里出现隐含的bug。我的建议是,abstract可以省略,这不会有问题,但访问修饰符public不要省略。比如,有这样一道题目:

interface ClassA{
    void method();
}
class ClassB implements ClassA{
    void method(){
        System.out.println(“Hello World!”);
    }
}
public class Main{
    public static void main(String[] args){
        ClassA a=new ClassB();
        a.method();
    }
}

然后问你“最后的执行结果是?”。好多同学可能会答“Hello World!”,但是错了。因为在ClassB继承的ClassA之后,它重写method()方法时缩小了访问范围(默然说话:在普通的类中,一个方法如果没有访问修饰,默认权限为包私有,而接口方法的默认却是public),所以其实这个程序是在编译时就会报错的,根本没办法运行。你看,你在interface里不写public有多误事呀?
从JDK8开始,由于引入了新特性Lambda而扩充了接口的特性,使得接口可以有限制的作实现了,但现在还不好讲这部分内容,所以我们放到第12章再做讨论。
在interface中,除了可以做方法声明,我们也可以进行变量声明,但要注意的一个问题是,在接口中声明的变量,统统是有final关键字的。也就是说,你以为是变量?其实你只能赋值一次,之后不能再改变。这样的量,准确的叫法应该是——常量。
我们知道,在写程序的时候,我们常常需要一些固定的值来代表一些状态,最常见的就是我们会使用1来代表男性,0来代表女性(默然说话:当然,你要用1来代表男性,2来代表女性,我也没意见,反正这些都是在程序里使用,重点是定好标准之后,要记住它,并且不要随意更改,不然容易引起混乱),我们也会使用1来代表东,用2来代表西,用3来代表南,用4来代表北(默然说话:当然,你要用11来代表东,12代表南,21代表西,22代表北,我也没意见,重点是定好标准之后,要记住它,并且不要随意更改,不然容易引起混乱),我们还可能使用0来代表未付款,1来代表现金付款,2来代表银联付款,3来代表微信付款,4来代表支付宝付款,5来代表比特币付款,6来代表记账。以上这些都是我们在程序里经常需要使用的“状态值”,使用它们一个是方便计算机的比较判断计算,提高速度,另一个是在比较判断里只有整型数是最精确的,不会出现误差,减少不必要的bug,但这也带来了很大的问题。就是,这会造成非常大的人为记忆量,特别是人对数字又不是非常的敏感。比如,不要看上面,你还记得1代表了哪几个状态么?你看,我不过写了三种情况,你就已经开始犯迷糊了,那实际程序中数字代表的状态有时能到几千个……情何以堪?(默然说话:IT行业套路深,我要打包回农村)。
这时,我们就能看出常量的威力了。比如我可以定义这样一个常量接口:

package cn.speakermore.ch07.util;

/**
 * 性别常量示例
 * @author mouyong
 */
public interface Sex {
    public static int MALE=1;
    public static int FEMALE=0;
}

很明显,Male和Female两个单词会比1和0更容易记住和使用。由于在接口的常量都会声明为static,所以它们都可以使用“接口.常量”的形式来引用,例如:

swicth(sex){
    case Sex.Male:
        //写关于Male的操作代码
        break;
    case Sex.Femal:
        //写关于Femal的操作代码
        break;
}

这就比我们以前直接写值的代码要意思更明确得多,下面就是以前我们会写的代码:

switch(sex){
    case 0:
        //写关于Male的操作代码
        break;
    case 1:
        //写关于Female的操作代码
        break;
}

比较两个代码,很明显上一个要比下一个更容易理解,也不容易出错(默然说话:是的,我不知道你们有没有注意到?其实下一个代码的注释写错了,因为根据前面的声明,0代表的是Female,1代表的才是Male,这是我为了证明直接写数字很容易出错而留下的陷阱!
提示:即使你对英文不是很熟,使用英文字母犯错的可能性也比数字要小很多,这是有科学实验与数据证明的。所以,强烈推荐大家把程序中的状态值都常量化,并把它们分类写到不同的接口中,这也是接口的属性体现。

7.2.2 匿名内部类

在Java中写代码时,比如写到带界面的,或者多线程的程序时,常常需要写一些继承自类或者接口的子类来实现特定的功能。而这些子类又经常是只需要实例化一次就够了的“一次性类”,如果每个都新建一个类文件来完成,既造成一种浪费,效率下降,又造成类数量的迅速膨胀,其实个人觉得最麻烦的是,这些只用一次的类,还要专门绞尽脑汁给它们都想一个名字。所以,Java也提供了一种专门用于创造它们的方式,叫匿名内部类(Anonymous Inner Class),你可以为这些一次性类直接完成从声明到实例化的全过程。真正的一站式服务。
匿名内部类声明的语法一般都是这样写的:

new 父类()|父接口(){
    //子类重写父类|父接口的方法
}

在第5章简单谈过内部类,内部类就是在类的内部又声明另一个类,也可以在方法中声明另一个类,这个也叫内部类。而匿名内部类,就是这个内部类没有名字,你只能看到它继承的是哪个父类:

Object o=new Object(){
    @Override
    public String toString(){
        return "只是一个无聊的教学示例";
    }
}

通过上面的代码我们看出来,我们写了一个没有名字的类,这个类继承了Object,并重写了toString()方法。这个匿名类只有一个对象,就是o,你可以在任何可以使用对象的地方使用它。
接口也可以通过同样形式来声明匿名内部类:

Flyable fly=new Flyable(){
    @Override
    public void fly(){
        System.out.println("这是一个不明类型的飞行物,英文简称UFO");
    }
}

这就是写一个UFO的办法(【鬼脸笑】)。它是一个继承了Flyable接口并重写了fly()方法的类,没有名字,但是有一个叫fly的变量名引用指向它唯一的对象。而你同样可以在任何可以使用对象的地方使用它。


提示:从JDK8开始,如果接口仅声明了一个方法,你可以使用Lambda表达式来简化这个程序的:

Flyable fly=()->{
      System.out.println(“这是一个不明类型的飞行物,英文简称UFO”);
}

举个接口的匿名内部类的例子,在实际飞行乐园中,当玩家登录时,飞行乐园中的许多子程序都很有兴趣要了解玩家的信息,并显示相关提示,当玩家退出的时候,同样也有很多子程序需要知道,以便在运行时避开一些bug。而这些子程序的数量并不是固定的,并且极有可能随我们飞行乐园的规模扩大而增加与减少。说到这里,大家应该能感觉到了,这是一个需要有很好弹性的架构来维护。
首先,既然要有玩家,我们就把玩家类创建出来,为了简化起见,我们只给玩家类两个属性:电话和昵称,并且它们是只读的:

package cn.speakermore.ch07.entity;

/**
 * 飞行乐园的玩家类
 * @author mouyong
 */
public class Player {
    //防止程序意外修改玩家信息,声明为final
    private final String phone;
    private final String nickName;
    //不写无参构造方法,防止意外实例化错误玩家对象
    //玩家信息只能在实例化时传入,且不允许在使用过程中进行更改
    public Player(String phone, String nickName) {
        this.phone = phone;
        this.nickName = nickName;
    }

    public String getPhone() {
        return phone;
    }

    public String getNickName() {
        return nickName;
    }

    @Override
    public String toString() {
        return "玩家【" + nickName + '】';
    }
    
}

接着,我们需要创建一个事件类(Event),它用来对玩家对象进行封装,以便于监听器能够在事件触发时把对应玩家的对象传递给有兴趣知道发生了什么的子程序。

package cn.speakermore.ch07.event;

import cn.speakermore.ch07.entity.Player;

/**
 * 玩家事件类,负责封装玩家,让有兴趣的子程序了解是哪个玩家发生了事件。
 * @author mouyong
 */
public class PlayerEvent {
    private Player player;

    public PlayerEvent(Player player) {
        this.player = player;
    }

    public Player getPlayer() {
        return player;
    }
    
    //为了进一步方便使用,提供一个玩家昵称的获取器
    public String getNickName(){
        return player.getNickName();
    }
}

然后我们需要一个监听器接口(Listener),它的作用是让有兴趣的子程序进行注册,以便当玩家发生某些特定事件的时候通知它(默然说话:这个过程的专业术语叫:回调),并完成特定的事情(默然说话:比如消息显示或记录日志,或者一些其他的事情。我们打算进行一个简单的消息显示)。

package cn.speakermore.ch07.event;

/**
 * 玩家监听器
 * 目前暂时提供登录事件和退出事件
 * @author mouyong
 */
public interface PlayerListener {
    /**
     * 登录事件
     * 在玩家登录时,子程序完成特定功能的回调方法
     * @param pe PlayerEvent对象,封装了登录的Player
     */
    public void onLogin(PlayerEvent pe);
    /**
     * 退出事件
     * 在玩家退出时,子程序完成特定功能的回调方法
     * @param pe PlayerEvent对象,封装了退出的Player
     */
    public void onLoginout(PlayerEvent pe); 
}

好了,相关监听的部件都准备好了,现在需要一个类把它们组合起来。这是一个负责管理所有玩家的类,它声明了两个集合,一个用来保存所有的玩家,另一个用来保存所有申请监听的子程序,以便在玩家发生登录或退出的时候循环通知它们,并调用对应的回调方法。

package cn.speakermore.ch07.event;


import cn.speakermore.ch07.entity.Player;
import java.util.ArrayList;
import java.util.List;

/**
 * 玩家监听的管理,一般英文叫Queue,排队,队列的意思
 * 所以这个类往往会被叫做PlayerQueue,但为了便于大家的理解,我改为PlayerManager
 * 但是感觉还是没有Queue的意思更符合,特此注释
 * @author mouyong
 */
public class PlayerManager {
    private List<Player> playersOnline=new ArrayList<Player>();
    private List<PlayerListener> listeners=new ArrayList<>();
    /**
     * 注册玩家监听器的方法,完成感兴趣的子程序对玩家的监听
     * @param pl PlayerListener 玩家监听器,包含对监听方法的实现
     */
    public void addPlayerListener(PlayerListener pl){
        listeners.add(pl);
    }
    /**
     * 当有登录的情况发生时,回调所有已注册监听器的onlogin方法
     * @param player Player类型,刚登录的玩家对象
     */
    public void login(Player player){
        playersOnline.add(player);
        PlayerEvent pe=new PlayerEvent(player);
        for(PlayerListener pl:listeners){
            //回调
            pl.onLogin(pe);
        }
    }
    /**
     * 当有退出事件发生时,回调所有已注册监听器的onLogOut方法
     * @param player Player类型,刚退出的玩家对象
     */
    public void logOut(Player player){
        //为防止意外,只有已登录的玩家才能进行退出的回调
        if(playersOnline.contains(player)){
            playersOnline.remove(player);
            PlayerEvent pe=new PlayerEvent(player);
            for(PlayerListener pl:listeners){
                //回调
                pl.onLogOut(pe);
            }
        }
    }
}

至此,准备工作完成,可以开始匿名内部类的创建了。


默然说话:嗯嗯嗯,前面的都是准备工作,并非我们要做的正事。作个比喻吧,比如现在我们要砍树(相当于我们的需求:让感兴趣的子程序在用户登录或退出时能做点事情),为了完成砍树的需求,我们自己做了一把斧子,而做斧子的过程,就是上面的这几个类和接口(Event,Listener,PlayerManager)。下面的代码才是真正的开始砍树。


package cn.speakermore.ch07;

import cn.speakermore.ch07.entity.Player;
import cn.speakermore.ch07.event.PlayerEvent;
import cn.speakermore.ch07.event.PlayerListener;
import cn.speakermore.ch07.event.PlayerManager;

/**
 *
 * @author mouyong
 */
public class ListenerTest {
    public static void main(String[] args) {
        Player player1=new Player("15808818048", "默然说话");
        Player player2=new Player("18988243456", "狂狮中中");
        
        PlayerManager pm=new PlayerManager();
        //声明匿名类,并重写两个监听回调方法
        pm.addPlayerListener(new PlayerListener() {
            @Override
            public void onLogin(PlayerEvent pe) {
                System.out.println(pe.getPlayer()+"登录了!");
            }

            @Override
            public void onLogOut(PlayerEvent pe) {
                System.out.println(pe.getPlayer()+"退出了!");
            }
        });
        
        //测试
        pm.login(player1);
        pm.login(player2);
        pm.logOut(player1);
        pm.logOut(player2);
    }
}

下面是执行后的结果图:


图7.5 使用匿名内部类完成玩家事件的监听


默然说话:好吧好吧,我知道大家都晕了。因为平时我们绝对不会自己做一把斧子的,我们都会去商店买一把斧子就好了。这时我们不需要学习怎么做斧子,我们只要知道怎么用斧子就好了。所以其实这里只是多展现了一些细节,如果你对造斧子不感兴趣,那你重点看最后这个ListenTest中的匿名类的写法就可以了,因为其实在实际开发中,监听器的确也是现成的,并不需要我们自己写,你只要会用匿名类的方式实现监听器接口就可以了。


7.2.3 使用enum枚举常数

在前面的小节我们讲过使用int型数字来代替状态值(还记得么?男是1,女是0)。我们还举了一个switch的例子来说明数字的缺点和使用常量之后的好处,但在switch的时候其实会有一个隐藏起来的bug。就是取值范围,其实我们的性别取值只有两个,0和1。可是int型可不止这两个数,所以我们需要额外的代码来防范这个bug。这也是挺麻烦的,象我这样奉行“少干活,多拿钱”主义的人来说,并不是一件好事。
JDK 5开始,Java提供了一个新特性来解决以上描述的麻烦,这就是枚举类型的语法,使用关键字enum可以定义一个枚举类型,从而限制了取值,既避免额外的检查bug的代码,也使代码更加规范有序,看例子:

package cn.speakermore.ch07;

/**
 * 一个关于性别的枚举类型
 * @author mouyong
 */
public enum Sex {
    MALE,FEMALE
}
这是定义枚举类型最简单的例子。实际上编译程序对enum做了很多特殊的处理。如果你反编译Sex.class,你会看到一些细节:
public final class Sex extends Enum{
    private Sex(String s,int i){
        super(s,i);
    }
    public static final Sex MALE;
    public static final Sex FEMALE;

    static{
        MALE=new Sex("MALE",0);
        FEMALE=new Sex("FEMALE",1);
    }
}

这里可以看出,Sex其实是一个类,它既不能被继承(默然说话:class前面的那个final导致),也不能在类的外部被实例化(默然说话:有参构造方法前用了private修饰)。
如何使用呢?看例子:

package cn.speakermore.ch07;

/**
 * 测试枚举类型的类
 * @author mouyong
 */
public class SexTest {
    /**
     * 测试方法,为了方便,声明为static
     * @param sex Sex枚举型,用于测试
     */
    public static void enumTest(Sex sex){
        switch(sex){
            case MALE:
                System.out.println("MALE的序号:"+Sex.MALE.ordinal());
                break;
            case FEMALE:
                System.out.println("FEMALE的序号:"+Sex.FEMALE.ordinal());
        }
    }
    public static void main(String[] args) {
        enumTest(Sex.MALE);
        enumTest(Sex.FEMALE);
    }
}

运行结果如下:


图7.6 enum的输出测试
在这个测试的例子中,由于enumTest()方法的参数被设置为Sex,所以就只能接受MALE和FEMALE两个值,这样就限制了非法参数的传递,我们也不需要专门去编写代码来防止隐藏的bug了。
关于enum更多的信息,会在第18章再做说明。

7.3 重点复习

接口是一种能力,声明一个接口使interface关键字。接口不能new,里面的方法全部默认是public abstract的(默然说话:默认的意思是说,可以不写public abstract,但是前面已经强调过,强烈不推荐这样做)。子类如果要继承一个接口,必须使用implements关键字,并且对接口中的方法进行重写。如果该子类是abstract的,则可以不重写接口中的方法。
类只能单继承,而接口可以多继承,在含意上来说,接口表达的是“具备某种能力”,而能力通常都是可以后天习得的,所以并不违背基本物理世界的常识。
接口也可以继承接口,这时使用extends关键字,并且可以继承多个接口,由于此特性并不常用,所以我不再例举了。
如果有“一次性使用”的需求,我们可以使用匿名内部类的方式解决,书写时要注意语法。
JDK5之后增加了enum关键字,枚举的提供能让我们的程序更加完善与强健。
enum类型其实是一个系统自动生成的,继承自Enum的类,此类无法进行外部的实例化,只能使用内部实例化的对象。

7.4 课后练习

7.4.1 选择题

1.如果有以下的程序片段:

interface Thing{
    protected void doThings();
}
class ThingImpl implements Thing{
public void doThings(){
    System.out.println("测试");
}
}
public class Main{
public static void main(String[] args){
    Thing t=new ThingImpl();
    t.doThings();
}
}

以下描述正确的是()

A. 编译失败
B.显示“测试”
C.发生ClassCastException
D.执行时不显示任何信息

2.如果有以下的程序片段:

interface Thing{
    int x=10;
}
public class Main{
public static void main(String[] args){
    System.out.println(Thing.x);
}
}

以下描述正确的是()

A. 编译失败
B.显示10
C.必须创建Thing的实例才能存取x
D.显示0

3.如果有以下的程序片段:

interface Thing{
    void doThings();
}
class ThingImpl implements Thing{
void doThings(){
    System.out.println("测试");
}
}
public class Main{
public static void main(String[] args){
    Thing t=new ThingImpl();
    t.doThings();
}
}

以下描述正确的是()

A. 编译失败
B.显示“测试”
C.发生ClassCastException
D.执行时不显示任何信息

4.如果有以下的程序片段:

interface Thing{
    void doThings();
}
class ThingImpl implements Thing{
public void doThings(){
    System.out.println("测试");
}
}
public class Main{
public static void main(String[] args){
    Thing t=new ThingImpl();
    t.doThings();
}
}

以下描述正确的是()

A. 编译失败
B.显示“测试”
C.发生ClassCastException
D.执行时不显示任何信息

5.如果有以下的程序片段:

interface Thing{
    void doThings();
}
interface OtherThing{
    void doOther();
}
class ThingOtherImpl implements Thing,OtherThing{
public void doThings(){
    System.out.println("测试doThings");
}
public void doOther(){
    System.out.println("测试doOrhter");
}
}
public class Main{
public static void main(String[] args){
    Thing t=new ThingOtherImpl();
    t.doThings();
    Other o=(Other)t;
    o.doOther();
}
}

以下描述正确的是()

A. 编译失败
B.显示“测试doThings”、“测试doOther”
C.发生ClassCastException
D.执行时不显示任何信息

6.如果有以下的程序片段:

interface Thing{
    void doThings();
}
abstract class ThingImpl implements Thing{
    public abstract void doThings();
public void doService(){
    System.out.println("测试");
}
}
public class Main{
public static void main(String[] args){
    ThingImpl t=new ThingImpl();
    t.doService();
}
}

以下描述正确的是()

A. 编译失败
B.显示“测试”
C.发生ClassCastException
D.执行时不显示任何信息

7.如果有以下的程序片段:

interface Thing{
    void doThings();
}
abstract class ThingImpl implements Thing{
    public abstract void doThings();
public void doService(){
    System.out.println("测试");
}
}
public class Main{
public static void main(String[] args){
    ThingImpl t=new ThingImpl(){
        public void doThings(){
            System.out.println("测试");
        }
        public void doService(){
    
        }
    };
    t.doService();
}
}

以下描述正确的是()

A. 编译失败
B.显示“测试”
C.发生ClassCastException
D.执行时不显示任何信息

8.如果有以下的程序片段:

interface Thing{
    void doThings();
}
public class Main{
public static void main(String[] args){
    Thing t=new Thing(){
    public void doThings(){
        System.out.println("测试doThings");
    }
    public void doService(){
        System.out.println("测试doService");
    }
    };
    t.doService();
}
}

以下描述正确的是()

A. 编译失败
B.显示“测试doService”
C.发生ClassCastException
D.执行时不显示任何信息

9.如果有以下的程序片段:

interface Thing{
    protected static final int x=10
}
public class Main{
public static void main(String[] args){
    System.out.println(Thing.x);
}
}

以下描述正确的是()

A. 编译失败
B.显示10
C.必须创建Thing实例才能存取x
D.显示0

10.如果有以下的程序片段:

interface Thing{
    void doThings();
    void doService(){
        System.out.println("测试doService");
    }
}
class ThingImpl implements Thing{
    public void doThings(){
        System.out.println("测试doThings");
    }
}
public class Main{
    public static void main(String[] args){
        Thing t=new ThingImpl();
        t.doThings();
    t.doService();
    }
}

以下描述正确的是()

A. 编译失败
B.显示“测试doThings”、“测试doService”
C.发生ClassCastException
D.执行时不显示任何信息

7.4.2 操作题

  1. 假设要开发一个动画编辑的程序,每个画面为帧(Frame),多个帧就组合为动画列表(Montion List),动画列表可由其他已完成的动画清单组成,也可以在动画列表与列表间加入个别帧以完成过渡。如何设计?
    提示:搜索关键字“Composite模式”
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容