《疯狂java讲义》学习(17):接口

版权声明:本文为博主原创文章,如若转载请注明出处 https://blog.csdn.net/tonydz0523/article/details/86539157

更彻底的抽象:接口

抽象类是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类”——接口(interface),接口里不能包含普通方法,接口里的所有方法都是抽象方法

接口的概念

同一个类的内部状态数据、各种方法的实现细节完全相同,类是一种具体实现体。而接口定义了一种规范,接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可满足实际需要。
接口是从多个相似类中抽象出来的规范,接口不提供任何实现。接口体现的是规范和实现分离的设计哲学。
接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着接口里通常是定义一组公用方法。

接口的定义

和类定义不同,定义接口不再使用class关键字,而是使用inferface关键字。接口定义的基本语法如下:

[修饰符] interface 接口名 extends 父接口1, 父接口2...
{
    零个到多个常量定义...
    零个到多个抽象方法定义...
}

上面语法的详细说明如下:

  • 修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口。
  • 接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可;如果要遵守Java可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无须任何分隔符。
  • 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。

由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含Field(这能是常量)、方法(只能是抽象实例方法)、内部类(包括内部接口、枚举)定义。
对比接口和类的定义方式,不难发现接口的成员比类里的成员少了两种,而且接口里的Field只能是常量,接口里的方法只能是抽象方法。
前面已经说过了,接口里定义的是多个共同的公共行为规范,因此接口里的所有成员,包括常量、方法、内部类和枚举类都是public访问权限。定义接口成员时,可以省略访问控制修饰符,如果指定访问控制修饰符,则只能使用public访问控制修饰符。
对于接口里定义的常量Field而言,它们是接口相关的,而且它们只能是常量,因此系统会自动为这些Field增加static和final两个修饰符。也就是说,在接口中定义Field时,不管是否使用public static final修饰符,接口里的Field总将使用这三个修饰符来修饰。而且,接口里没有构造器和初始化块,因此接口里定义的Field只能在定义时指定默认值。
接口里定义Field采用如下两行代码的结果完全一样:

//系统自动为接口里定义的Field增加public static final修饰符
int MAX_SIZE=50;
public static final int MAX_SIZE=50;

对于接口里定义的方法而言,他们只能抽象方法,因此系统会自动为其增加abstract修饰符;由于接口里的方法全部是抽象方法,因此接口里不允许定义静态方法,即不可使用static修饰接口里定义的方法。不管定义接口里方法时是否使用public abstract修饰符,接口里的方法总是使用public abstract来修饰。
接口里定义的内部类、接口、枚举类默认都采用public static两个修饰符,不管定义时是否制定这两个修饰符,系统都会自动使用public static对他们进行修饰。
下面是一个接口:

package lee;

public interface Output {
    //接口里定义的Field只能是常量
    int MAX_CACHE_LINE = 50;
    //接口里定义的只能是public的抽象实例方法
    void out();
    void getData(String msg);
}

上面定义了一个Output接口,这个接口里包含了一个常量Field。除此之外,这个接口还定义了两个方法:表示取得数据的getData方法和表示输出的out方法。这就定义了Output接口的规范:只要某个类能取得数据,并可以将数据输出,那它就是一个输出设备,值域这个设备的实现细节,这里暂时不关心。
接口里的Field默认是使用public static final修饰的,因此即使另一个类处于不同包下,也可以通过接口来访问接口里的常量Field。例如下面程序:

package yeeku;

public class OutputPropertyTest {
    public static void main(String[] args) {
        //访问另一个包中的Output接口的MAX_CACHE_LINE
        System.out.println(lee.Output.MAX_CACHE_LINE);
        //下面语句将引起“为final变量赋值”的编译异常
//        Output.MAX_CACHE_LINE=20;
    }
}

从上面main方法中可以看出,OutputPropertyTest与Output处于不同包下,但可以访问Output的MAX_CACHE_LINE常量,这表明该Field是public访问权限的,而且可通过接口来访问该Field,表明这个Field是一个静态Field;当为这个Field赋值时引发"为final变量赋值"的编译异常,表明这个Field使用了final修饰。

接口的继承

接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量Field、内部类和枚举类定义。
一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号(,)隔开。下面程序定义了三个接口,第三个接口继承了前面两个接口,下面是个例子:

interface interfaceA
{
    int PROP_A=5;
    void testA();
}
interface interfaceB
{
    int PROP_B=6;
    void testB();
}
interface interfaceC extends interfaceA, interfaceB{
    int PROP_C=7;
    void testC();
}
public class InterfaceExtendsTest
{
    public static void main(String[] args)
    {
        System.out.println(interfaceC.PROP_A);
        System.out.println(interfaceC.PROP_B);
        System.out.println(interfaceC.PROP_C);
    }
}

上面程序中的interfaceC接口继承了interfaceA和interfaceB,所以interfaceC中获得了它们的常量Field,因此在main方法中看到通过interfaceC来访问PROP_A、PROP_B和PROP_C常量.

使用接口

接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,设个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。
一个类可以实现一个或多个接口,继承使用extends关键字,实现则使用implements关键字。因此一个类可以实现多个接口,这也是Java为单继承灵活性不足所做的补充。类实现接口的语法格式如下:

[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
    类体部分
}

实现接口与继承父类相似,一样可以获得所实现接口里定义的常量Field、抽象方法、内部类和枚举类定义。
让类实现接口需要类定义后增加implements部分,当需要实现多个接口时,多个接口之间以英文逗号(,)隔开,一个类可以继承一个父类,同时实现多个接口。
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类,下面是一个实现接口的类:

import lee.Output;
//定义一个Product接口
interface Product
{
    int getProduceTime();
}
//让Printer类实现Output接口和Product接口
public class Printer implements Output , Product
{
    private String[] printData=new String[MAX_CACHE_LINE];
    //用以记录当前需打印的作业数
    private int dataNum=0;
    public void out()
    {
        //只要还有作业,就继续打印
        while(dataNum > 0)
        {
            System.out.println("打印机打印:" + printData[0]);
            //把作业队列整体前移一位,并将剩下的作业数减1
            System.arraycopy(printData , 1, printData, 0, --dataNum);
        }
    }
    public void getData(String msg)
    {
        if (dataNum >=MAX_CACHE_LINE)
        {
            System.out.println("输出队列已满,添加失败");
        }
        else
        {
            //把打印数据添加到队列里,已保存数据的数量加1
            printData[dataNum++]=msg;
        }
    }
    public int getProduceTime()
    {
        return 45;
    }
    public static void main(String[] args)
    {
        //创建一个Printer对象,当成Output使用
        Output o=new Printer();
        o.getData("轻量级Java EE企业应用实战");
        o.getData("疯狂Java讲义");
        o.out();
        o.getData("疯狂Android讲义");
        o.getData("疯狂Ajax讲义");
        o.out();
        //创建一个Printer对象,当成Product使用
        Product p=new Printer();
        System.out.println(p.getProduceTime());
        //所有接口类型的引用变量都可直接赋给Object类型的变量
        Object obj=p;
    }
}

从上面程序中可以看出,Printer类实现类Output接口和Product接口,因此Printer对象既可以直接赋给Output变量,也可直接赋给Product变量。
实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口里的方法时只能使用public访问权限。
接口不能显示继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。

接口和抽象类

接口和抽象类很想,他们都具有如下特征:

  • 都不能被实例化,都位于继承树的顶端,用于被其他类实现和继承。
  • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

但接口和抽象类之间的差别非常大,这种差别主要体现在二者的设计目的上。
当在一个程序中使用接口时,接口时多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口时多个程序之间的通信标准。
从某种程度上来看,接口类似于整个系统的“总纲”,它制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
除此之外,接口和抽象类在用法上也存在如下差别:

  • 接口里只能包含抽象方法,不包含已经提供实现的方法;抽象类则完全可以包含普通方法。
  • 接口类不能定义静态方法;抽象类里可以定义静态方法
  • 接口里只能定义静态常量Field,不能定义普通Field;抽象类里则既可以定义普通Field,也可以定义静态常量Field
  • 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
  • 接口里不能包含初始化块;但抽象类则完全可以包含初始化块
  • 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足

面向接口编程

接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。
基于这种原则,很多软件架构设计理论都倡导“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。下面介绍两种常用场景来示范面向接口编程的优势

1.简单工厂模式

有一个场景:假设程序中有个Computer类需要组合一个输出设备,现在有两个选择:直接让Computer类组合一个Printer,或者让Computer类组合一个Output,那么到底采用哪种方式更好呢?假设让Computer类组合一个Printer对象,如果有一天系统需要重构,需要使用BetterPrinter来代替Printer,于是我们需要打开Computer类源代码进行修改。如果系统中只有一个Computer类组合了Printer还好,但如果系统中有100个类组合了Printer,甚至1000个、10000个……将意味着我们要打开100个、1000个、10000个类进行修改,这是多么大的工作量!
为了避免这个问题,我们让Computer类组合一个Output类型的对象,将Computer类与Printer类完全分离。Computer对象实际组合的是Printer对象还是BetterPrinter对象,对Computer而言完全透明。当Printer对象切换到BetterPrinter对象时,系统完全不受影响。下面是这个Computer类的定义代码:

public class Computer {
    private Output out;
    public Computer(Output out){
        this.out=out;
    }
    //定义一个模拟获取字符串输入的方法
    public void KeyIn(String msg){
        out.getData(msg);
    }
    //定义一个模拟打印的方法
    public void print(){
        out.out();
    }
}

上面的Computer类已经完全与Printer类分离,只是与Output接口耦合。Computer不再负责创建Output对象,系统提供一个Output工厂来负责生成Output对象。这个OutputFactory工厂代码如下:

public class OutputFactory {
    public Output getOutput(){
        return new Printer();
    }
    public static void main(String[] args){
        OutputFactory of=new OutputFactory();
        Computer c=new Computer(of.getOutput());
        c.KeyIn("ffzs" );
        c.KeyIn("泛泛之素");
        c.print();
    }
}

该类OutputFactory类中包含了一个getOutput方法,该方法返回一个Output实现类的实例,该方法负责创建Output实例,具体创建哪一个实现类的对象由该方法决定(具体有该方法中的粗体部分控制,当然也可以增加更复杂的控制逻辑)。如果系统需要将Printer给为BetterPrinter实现类,只需让BetterPrinter实现Output接口,并改变OutputFactory类中的getOutput方法即可。
下面是BetterPrinter实现类的代码,BetterPrinter只是对原有的Printer进行简单修改,以模拟系统重构后的改进。

public class BetterPrinter implements Output
{
    private String[] printData
            =new String[MAX_CACHE_LINE * 2];
    //用以记录当前需打印的作业数
    private int dataNum=0;
    public void out()
    {
        //只要还有作业,就继续打印
        while(dataNum > 0)
        {
            System.out.println("高速打印机正在打印:" + printData[0]);
            //把作业队列整体前移一位,并将剩下的作业数减1
            System.arraycopy(printData , 1, printData, 0, --dataNum);
        }
    }
    public void getData(String msg)
    {
        if (dataNum >=MAX_CACHE_LINE * 2)
        {
            System.out.println("输出队列已满,添加失败");
        }
        else
        {
            //把打印数据添加到队列里,已保存数据的数量加1
            printData[dataNum++]=msg;
        }
    }
}

上面的BetterPrinter类也实现了Output接口,因此也可以当成Output对象使用,于是我们需要把OutputFactory工厂类的getOutput方法改为如下代码:

return new BetterPrinter();

通过这种方式,我们把所有生成Output对象的逻辑集中在OutputFactory工厂类中管理,而所有需要使用Output对象的类只需与Output接口耦合,而不是与具体的实现类耦合。即使系统中有很多类使用了Printer对象,只要OutputFactory类的getOutput方法生成的Output对象是BetterPrinter对象,则它们全部都会改为使用BetterPrinter对象,而所有程序无须修改,只需要修改OutputFactory工厂类的getOutput方法实现即可。

2.命令模式

考虑这样一个场景:某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组的数组元素,但无法确定在便利数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。
这个要求看起来有点奇怪:这个方法不仅需要普通数据可以变化,甚至还有方法执行体也需要变化,难道我们能把“处理行为”作为一个参数传入该方法?
对这样一个需求,我们必须把“处理行为”作为参数传入该方法,因为Java不允许代码块单独存在,因此我们使用一个Command接口来定义一个方法,用这个方法来封装“处理行为”:

public interface Command {
    //接口里定义的process方法用于封装“处理行为”
    void process(int[] target);
}

上面的Command接口里定义了一个process方法,这个方法用于封装“处理行为”,但这个方法没有方法体——因为现在还无法确定这个处理行为。
下面是需要处理数组的处理类,在这个处理类中包含一个process方法,这个方法无法确定处理数组的处理行为,所以定义该方法时使用了一个Command参数,这个Command参数负责对数组的处理行为。该类的程序代码如下:

public class ProcessArray {
    public void process(int[] target, Command cmd){
        cmd.process(target);
    }
}

通过一个Command接口,就实现了让ProcessArray类和具体“处理行为”的分离,程序使用Command接口代表了对数组的处理行为。Command接口也没有提供真正的处理,只有等到需要调用ProcessArray对象的process方法时,才真正传入一个Command对象,才确定对数组的处理行为,下面分别是PrintCommand类和AddCommand类的代码:
PrintCommand

public class PrintCommand implements Command {
    public void process(int[] target){
        for (int tmp:target){
            System.out.println("迭代输出目标数组的元素:"+tmp);
        }
    }
}

AddCommand:

public class AddCommand implements Command{
    public void process(int[] target){
        int sum=0;
        for (int tmp:target){
            sum +=tmp;
        }
        System.out.println("数组元素的总和是:"+sum);
    }
}

下面程序示范了对数组的两种处理方法:

public class CommandTest {
    public static void main(String[] args){
        ProcessArray pa=new ProcessArray();
        int[] target={3,-4,6,4};
        //第一次处理数组,具体行为取决于PrintCommand
        pa.process(target, new PrintCommand());
        System.out.println("------------------------");
        //第二次处理数组,具体处理取决于AddCommand
        pa.process(target,new AddCommand());
    }
}

运行程序,结果如下:

迭代输出目标数组的元素:3
迭代输出目标数组的元素:-4
迭代输出目标数组的元素:6
迭代输出目标数组的元素:4
------------------------
数组元素的总和是:9

对于PrintCommand和AddCommand两个实现类而言,实际有意义的部分就是process(int[] target)方法,该方法的方法体就是传入ProcessArray类里的process方法的“处理行为”,通过这种方式就可实现process方法和“处理行为”的分离。

java实例练习

使用策略模式保存图片类型

在使用图像处理软件处理图片后,需要选择一种格式进行保存,然而各种格式在底层实现的算法并不相同,这刚好适合策略模式。本实例将为大家演示如何使用策略模式与简单工厂模式组合进行实例的开发。

1.

新建项目StrategyPattern,并在其中创建一个ImageSaver.java文件。在该类中编写接口ImageSaver,并在该接口中定义save()方法。核心代码如下所示:

package StrategyPattern;

public interface ImageSaver {
    //定义save()方法
    void save();
}

2.

再创建一个类GIFSaver接口,在实现save()方法时将图片保存为GIF格式。对于将图片保存成其他格式与存储为GIF格式类似。核心代码如下所示:
GIF:

package StrategyPattern;

public class GIFSaver implements ImageSaver{
    @Override
    public void save(){
        //实现save()方法
        System.out.println("将图片保存成GIF格式");
    }
}

JPEG:

package StrategyPattern;

public class JPEGSaver implements ImageSaver{
    @Override
    public void save() {
        System.out.println("将图片保存成JPEG格式");
    }
}

PNG:

package StrategyPattern;

public class PNGSaver implements ImageSaver{
    @Override
    public void save() {
        System.out.println("将图片保存成PNG格式");
    }
}

3.

接着我们编写一个TypeChooser,该类根据用户提供的图片类型来选择合适的图片存储方式。其核心代码如下所示:

package StrategyPattern;

public class TypeChooser {
    public static ImageSaver getSaver(String type){
        if (type.equalsIgnoreCase("GIF")){
            //使用if else 语句来判断图片的类型
            return new GIFSaver();
        }
        else if (type.equalsIgnoreCase("JPEG")){
            return new JPEGSaver();
        }
        else if (type.equalsIgnoreCase("PNG")){
            return new PNGSaver();
        }
        else {
            return null;
        }
    }
}

4.

最后创建一个测试类StrategyPattern,该类模拟用户的操作,为类型选择器提供图片的类型。核心代码如下所示:

package StrategyPattern;

public class StrategyPattern {
    public static void main(String[] args){
        System.out.print("用户选择GIF模式:");
        ImageSaver saver=TypeChooser.getSaver("GIF");
        // 获取保存图片为GIF类型对象
        saver.save();
        System.out.print("用户选择了JPEG格式:");
        saver=TypeChooser.getSaver("JPEG");
        saver.save();
        System.out.print("用户选择了PNG格式:");
        saver=TypeChooser.getSaver("PNG");
        saver.save();
    }
}

策略模式,又称算法簇模式,就是定义了不同的算法簇,并且之间可以互相替换,此模式让算法的变化独立于使用算法的客户。策略模式的好处在于可以动态的改变对象的行为。策略模式属于对象行为型模式,主要针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。通常,策略模式适用于当一个应用程序需要实现一种特定的服务或者功能,而且该程序有多种实现方式时使用。
策略模式中有三个对象:

  • 环境对象:该类中实现了对抽象策略中定义的接口或者抽象类的引用。
  • 抽象策略对象:它可由接口或抽象类来实现。
  • 具体策略对象:它封装了实现不同功能的不同算法。

利用策略模式构建应用程序,可以根据用户配置等内容,选择由不同算法来实现应用程序的功能。具体的选择由环境对象来完成。采用这种方式可以避免由于使用条件语句而带来的代码混乱,从而提高应用程序的灵活性与条理性。

猜你喜欢

转载自blog.csdn.net/tonydz0523/article/details/86539157