Day14:抽象类和接口

任务:模拟实现“愤怒的小鸟”游戏

关键步骤如下。
➢定义鸟的抽象类。
➢将各种鸟中统一的行为定义为抽象类中的普通方法。
➢将各种鸟不同的行为定义为抽象类中的抽象方法。
➢实现具体的鸟类,重写抽象方法,实现各种鸟的不同行为。
➢将鸟叫的行为定义为接口。
➢继承接口实现各种具体的叫声。
➢将叫的行为作为抽象鸟类的一个属性。

初识抽象类和抽象方法

1. 区分普通方法和抽象方法

在Java中,当一个类的方法被abstract关键字修饰时,该方法称为抽象方法。抽象方法所在的类必须定义为抽象类。

当一个方法被定义为抽象方法后,意味着该方法不会有具体的实现,而是在抽象类的子类中通过方法重写进行实现。定义抽象方法的语法格式如下。

[访问修饰符] abstract <返回类型> <方法名>([参数列表]);

abstract关键字表示该方法被定义为抽象方法。
普通方法和抽象方法相比,主要有下列两点区别。
(1)抽象方法需要用修饰符abstract 修饰,普通方法不需要。
(2)普通方法有方法体,抽象方法没有方法体。

2.区分普通类和抽象类

在Java中,当一个类被abstract关键字修饰时,该类称为抽象类。定义抽象类的语法格式如下。
抽象类与抽象方法

abstract class<类名>{
}

abstract关键字表示该类被定义为抽象类。
普通类和抽象类相比,主要有下列两点区别。
(1)抽象类需要用修饰符abstract修饰,普通类不需要。
(2)普通类可以实例化,抽象类不能被实例化。

3.定义一个抽象类

当一个类被定义为抽象类时,它可以包含各种类型的成员,包括属性、方法等,其中
方法又可分为普通方法和抽象方法,如下面的抽象类结构示例。

public abstract class类名称{
	修饰符 abstract 返回类型 方法名();
	修饰符 返回类型 方法名(){
		方法体
	}
}

注意
抽象方法只能定义在抽象类中。但是抽象类中可以包含抽象方法,也可以包含普通方法,还可以包含普通类包含的一切成员。

使用抽象类描述抽象的事物

下面通过一个示例简单认识抽象类和抽象方法的用法。
有一个宠物类,宠物具体分为狗狗、企鹅等,实例化一个狗狗类、 企鹅类是有意义的, 而实例化一个宠物类则是不合理的。这里可以把宠物类定义为抽象类,避免宠物类被实例化。
示例1
定义一个抽象的宠物类。
关键代码:

//宠物抽象类, 即狗狗类和企鹅类的父类
public abstract class Pet {
	private String name="无名氏";//昵称
	private int health = 100;//健康值
	private int love = 0;//亲密度
//有参构造方法
	public Pet(String name) {
	this.name = name;
//输出宠物信息
	public void print() {
	System.out.println("宠物的自白:\n我的名字叫"this.name+"健康值是" +this.health + "和主人的亲密本度是"+this.love+"。");
	}
}
class Test {
	public static void main(String[] args) {
	Pet pet= new Pet("贝贝");//错误, 抽象类不能被实例化
	pet.print();
	}
}

示例1的代码中,不可以直接实例化抽象类Pet,但是它的子类是可以实例化的。如果子类中没有重写print()方法,子类将继承Pet类的该方法,但无法正确输出子类信息。在Java中可以将print()方法定义为抽象方法,让子类重写该方法。示例2展示了如何定义一个抽象方法,并在子类中实现该方法。
示例2
在抽象的宠物类中定义抽象方法。
关键代码:

//宠物抽象类,即狗狗类和企鹅类的父类
public abstract class Pet {
    private String name = "无名氏";//昵称
    private int health = 100;//健康值
    private int love = 0;//亲密度//有参构造方法

    public Pet(String name) {
        this.name = name;
    }
//抽象方法,输出宠物信息
	public abstract void print();
}

子类关键代码:

public class Dog extends Pet {
    private String strain;//品种

    public Dog(String name, String strain) {
        super(name);
        this.strain = strain;
    }

    public String getStrain() {
        return strain;
    }

    //重写父类的print()方法
    public void print() {
        System.out.println("我是一只" + this.strain + "。");
    }
}

在示例2中,可以实例化Dog类得到子类对象,并通过子类对象调用子类中的print()方法,从而输出子类信息。

抽象类和抽象方法的优势

下面分析如何设计“愤怒的小鸟”游戏,从而体会使用抽象类和抽象方法的优势。在手机游戏“愤怒的小鸟”中,有小鸟、猪、猴子等角色。以主角小鸟为例,当弹弓拉到极限以后,小鸟就飞出去进行攻击,即小鸟都有飞的行为,并且飞的行为也是一样的:同时发射出来的时候,小鸟都会叫,假设小鸟们的叫声也是一样的,也就是小鸟们都有叫的行为,并且叫声是一样的。但是每个小鸟的攻击方式不一样,分裂鸟会分裂,炸弹鸟会扔炸弹,即每个鸟虽然都有攻击行为,但是攻击方式不同。

在设计这个游戏时,可以设计一个抽象类,即抽象的鸟类,这个抽象类有两个普通方法,一个是飞行,它的实现内容是“弹射飞”;另一个是叫,实现内容是“嗷”:还有一个抽象方法为攻击,由于它是抽象方法,所以它没有方法体。之后,将火箭鸟、分裂鸟设计为一个类,并继承鸟这个抽象类。

鸟抽象类中的飞行方法和叫方法实现后,也就是火箭鸟、 分裂鸟都可以直接使用类的飞行方法和叫方法。但是由于攻击方法是抽象方法,也就是火箭鸟、分裂鸟需要实现自己的攻击方式。
可以看出,通过继承抽象类(鸟),火箭鸟、分裂鸟等鸟类的飞行行为一样,以可以直接使用抽象类中的飞行方法,避免在自己的类中再次实现飞行方法,也就具飞行代码能够在任何一个鸟抽象类的子类中复用。同理,叫的方法也一样。同时,由于攻击方式不同,每一个鸟类都被要求必须实现自身的攻击行为,也体现了每个鸟类的个性。

总之,抽象类中已经实现的方法可以被其子类使用,使代码可以被复用;同时提供了抽象方法,保证了子类具有自身的独特性。

抽象类的局限性

在有些应用场合,仅仅使用抽象类和抽象方法会有一定 的局限性。下面通过“愤怒的小鸟”游戏来进一步分析、认识这种局限性,并学会使用接口来改进设计。

“愤怒的小鸟”游戏中,分裂鸟和火箭鸟飞出来的时候是“嗷嗷”叫,但是红色鸟和炸弹鸟飞出来以后,却是“喳喳”叫,胖子鸟出来时干脆不叫。

此时,使用抽象类就会出现以下问题:
第一,叫的方法不再通用;第二,子类继承鸟抽象类之后,写出来的叫的方法可能会出现代码重复的情况,如红色鸟和炸弹鸟都是“喳喳”叫,这时候,就不再符合代码复用的要求。

叫的方法已经不再通用,最自然的想法就是将叫这个方法变为抽象方法,然后由其子类去实现,这样做虽然解决了第一个问题,但是会造成代码冗余的问题,如这里的分裂鸟和火箭鸟中的叫方法也会样,也就是第二个问题更加突出。要解决上述问题,最理想的方式就是使用接口。

初识接口

1.生活中的接口

在现实生活中,USB接口实际上是某些企业和组织制定的一种约定 或标准,规定了接口的大小、形状等。按照该约定设计的各种设备,如U盘、USB风扇正常工作。USB接口相关工作是按照如下步骤进行的:
(1)约定USB接口标准。
(2)制作符合USB接口约定的各种具体设备。
(3)把USB设备插到USB接口上进行工作。

Java中接口的作用和生活中的接口类似,它提供一种约定,使得实现接口的类(或结构)在形式上保持一致。

如果抽象类中所有的方法都是抽象方法,就可以使用Java提供的接口来表示。从这个角度来讲,接口可以看作是种特殊的 “抽象类” ,但是采用与抽象类完全不同的语法来表示,两者的设计理念也不同。

2.定义和实现一个简单的接口

简单地说,接口是一个不能实例化的类型。接口类型的定义类似于类的定义,语法格式如下。

public interface 接口名{
//接口成员
}

➢和抽象类不同,定义接口使用interface修饰符,访问修饰符只能是public,且可选。
➢接口成员可以是全局常量和公共的抽象方法。

与抽象类一样, 使用接口也必须通过子类,子类通过implements关键字实现接口。实现接口的语法格式如下。

public 类名implements 接口名{
实现方法
普通方法
}

(1)实现用接口使用implements关键字。
(2)实现接口的类必须实现接口中定义的所有抽象方法。接口的实现类允许包含普通方法。

示例3
定义和实现USB接口,进行数据传输。
关键代码:
```javascript
//定义USB接口
public interface UsbInterface {
    //数据传输抽象方法
    void service();

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

示例 4
定义USB风扇类,实现USB接口,获得电流,让风扇转动。
关键代码:

public class UsbFan implements UsbInterface {
    public void service() {
        System.out.println("连接USB口,获得电流, 风扇开始转动。 ");
    }
}

示例5
编写测试类,实现U盘传输数据,实现USB风扇转动。
关键代码:

public class Test {
    public static void main(String[] args) {
        // (1)U盘
        Usblnterface uDisk = new UDisk();
        uDisk.service();
// (2)USB 风扇
        Usblnterface usbFan = new UsbFan();
        usbFan.service();
    }
}

3.更复杂的接口

接口本身也可以继承接口。
接口继承的语法格式如下。

[修饰符] interface 接口名 extends 父接口1, 父接口2.....{
        常量定义
        方法定义
}

一个普通类只能继承一个父类,但能同时实现多个接口,也可以同时继承抽象类和实现多个接口。
实现多个接口的语法格式如下。

class 类名 extends 父类名 implements 接口1,接口2....{
类的成员
}

关于定义和实现接口,需要注意以下几个方面的内容。
➢接口和类、抽象类是一个层次的概念,命名规则相同。
➢修饰符如果是public,则该接口在整个项目中可见。如果省略修饰符,该接口只在当前包中可见。
➢接口中可以定义常量,不能定义变量。接口中的属性都默认用“public static final”修饰,即接口中的属性都是全局静态常量。接口中的常量必须在定义时指定初始值,举例如下。

public static final int PI=3.14;
int PI=3.14;//在接口中,这两个定 义语句的效果完全相同
int PI;//错误,在接口中定 义时必须指定初始值,如果在类中定义会有默认值

➢接口中的所有方法都是抽象方法,接口中的方法都默认为public.
➢和抽象类一样,接口同样不能实例化,接口中不能有构造方法。
➢接口之间可以通过extends实现继承关系,一个接口可以继承多个接口,但接口不能继承类。
➢类只能继承一个父类,但可以通过implements实现多个接口。一个类必须实现接口的全部方法定义,否则必须定义为抽象类。若一个类在继承父类的同时又实现了多个接口,extends必须位于implements之前。

使用接口的优势

为解决“愤怒的小鸟”设计中使用抽象类存在的问题,可以尝试使用接口。首先定义一个鸟叫的接口,如示例6所示。
示例6
定义鸟叫的接口。
关键代码:

public interface ShoutAbility{
public void shout();//鸟叫的抽象方法
}

接下来确定接口的实现类。最直接的想法就是让各个鸟类实现接口,但这样做法还是会导致代码冗余。实际的做法是将各种鸟叫的方式作为接口的实现类。示例77定义了“嗷嗷”叫、“喳喳 ”叫两种叫的方式。
示例7
定义类来描述“嗷嗷”叫、“喳喳” 叫两种鸟叫的方式,并实现鸟叫的接口。
关键代码:

/*嗷嗷叫*/
public class AoShout implements ShoutAbility {
    public void shout() {
        System.out.println("嗷_ _");
    }
}

/*喳喳叫*/
public class ZhaShout implements ShoutAbility {
    public void shout() {
        System.out.println("喳喳! ");

    }
}

然后,在鸟的抽象类中添加shoutAbility类型的属性,表示鸟叫的方式。
示例8
在鸟的抽象类中将接口作为属性,通过属性调用该接口的方法。
关键代码:

public abstract class Bird {
    ShoutAbility shout_ability;//鸟叫的方式

    //鸟类的构造方法,用来初始化鸟叫的行为
    public Bird(ShoutAbility shout_ability) {
        this.shout_ability = shout_ability;
    }

    //叫
    public void shout() {
        shout_ability.shout();//调用接口的方法
    }

    //飞行
    public void fly() {
        System.out.println("弹射飞");
    }

    public abstract void attck();//攻击

    //炸弹鸟
    public class BombBird extends Bird {
        public BombBird(ShoutAbility shout_Abilty) {
            super(shout_Ability);
        }

        //重写攻击方法
        public void attack() {
            System.out.println("炸弹攻击! ");
        }
    }

    //分裂鸟
    public class SplitBird extends Bird {
        public SplitBird(ShoutAbility shout_Ability) {
            super(shout_Ability);
        }

        // 重写攻击方法
        public void attack() {
            System.out.println(" 分裂攻击! ");
        }
    }

    //测试类
    public class Test {
        public static void main(String[] args) {
            ShoutAbility ao_shout = new AoShout();//嗷嗷叫
            ShoutAbility zha_shout = new ZhaShout();    //喳喳叫
            Bird bomb = new BombBird(zha_shout);
            Bird split = new SplitBird(ao_shout);
            bomb.shout();
            splitshout();
            //……省略其他代码
        }
    }
}

在示例8的代码中,抽象类Bird定义了ShoutAbility类型的属性,表示鸟叫的方式,并且在构造方法中对其初始化,在各个子类中调用父类的构造方法,实现对叫的方式的初始化。

面向对象设计的原则

在实际开发过程中,遵循以下原则,会让代码更具灵活性,更能适应变化。
1.摘取代码中变化的行为,形成接口
例如,在“愤怒的小鸟”游戏中,鸟叫的行为变化性很大,有的鸟叫,有的鸟不叫,各种鸟的叫声也不一样,这种行为最好定义为接口。
2.多用组合,少用继承
在“愤怒的小鸟”游戏中,通过在抽象类鸟中包含鸟叫的属性来实现组合,有效地减少了代码冗余。
3.针对接口编程,不依赖于具体实现
如果对一个类型有依赖,应该尽量依赖接口,尽量少依赖子类。因为子类一旦变化,代的变动的可能性大,而接口要稳定得多。在具体的代码实现中,体现在方法参数尽量使用接口,方法的返回值尽量使用接口,属性类型尽量使用接口等。
4.针对扩展开放,针对改变关闭
如果项目中的需求发生了变化,应该添加一个新的接口或者类,而不要去修改原有的代码。

猜你喜欢

转载自blog.csdn.net/sanjiang521/article/details/107557866
今日推荐