java的接口详解



接口定义了一种规范,实现了同一接口的类都必须遵守这种规范。由于java8新增了接口的默认方法和类方法,java9又增加了私有方法,让接口的概念不那么好理解。我认为应该先不提这部分内容来理解接口。
去掉后面新增的默认方法、类方法和私有方法,接口的普通实例方法必须是抽象方法
所以,可以把接口看成一个百分百抽象的类(接口实际和抽象类还是有区别),它只是定义了一套规则,但完全不限制具体的实现。实现接口的类必须要按照这套规则实现这些抽象的实例方法,但具体如何实现,接口不管。



接口的定义

接口的通用语法为:
[public可选] interface 接口名 extends 父接口1,父接口2,父接口3……
{
零个或多个常量定义…
零个或多个抽象实例方法定义…
零个或多个默认方法类方法私有方法定义…
零个或多个内部类内部接口内部枚举定义…
}

接口的修饰符只有public一种或者省略,表示公共访问权限或者包访问权限
接口相当于特殊的类,所以和类的规则类似,一个java源文件里最多只能有一个public接口,而且有public接口的java源文件的主文件名必须和public接口名相同。
接口成员里面,实例变量只能是静态常量普通实例方法只能是抽象实例方法。在java8之后,接口多了默认方法类方法,在java9之后,接口多了私有方法。这些方法是为了让接口的方法内部也能有方法逻辑,后面再讲。

接口和类一样,可以有内部类、内部接口和内部枚举

接口和类相比,去掉了初始化块和构造器。初始化块和构造器都是为了初始化和创建实例对象设计的成员,但接口不能创建实例对象,也没有需要在初始阶段赋值的实例变量(接口的实例变量都是在定义阶段显式赋值的常量),所以就舍弃了这2个成员。



接口的成员介绍

接口的静态常量

接口的实例变量默认带有public static final三个修饰符,而且由于接口没有初始化块和构造器,所以接口的实例变量必须在定义时直接显式初始化。比如:int NUM = 10; 等价于public static final int NUM = 10;
注意:由于是静态常量,按照命名规范,最好全用大写字母。
所以说,接口的实例变量只能是静态常量

接口的抽象实例方法

接口的所有普通实例方法都默认有public abstract修饰符,也就是公共的抽象实例方法。
这些抽象方法只是为实现接口的多个类制定共同的规范,并没有任何方法的实现。
所以,写接口的普通实例方法应该省略掉public abstract修饰符。
比如:
public abstract void methodA();
void methodA(); // 更推荐的写法
这两种写法是一样的含义。

接口的默认方法

默认方法其实是为了弥补接口所有实例方法都是抽象方法的设定。java8认为,接口也可以写出有行为逻辑(也就是有方法体)的方法,让实现接口的类继承这些有行为逻辑的方法。但又不能推翻以前实例方法都是抽象方法的设定。所以添加了一个默认方法。
默认方法必须有修饰符default,默认方法必须拥有方法逻辑(也就是必须带花括号)。
默认方法的访问权限都是public,public可以省略不写。
默认方法由于有方法体,所以实现类并不需要强制重写接口的默认方法
如果实现类实现了多个接口,多个接口有相同方法签名的默认方法,为了解决多继承的歧义问题,实现类必须重写这种默认方法。

接口的类方法

接口的类方法和普通类的类方法其实是一样的概念,就是与接口直接绑定的方法。
类方法必须用static修饰,调用也应该直接用接口名.类方法的语法调用。
接口的类方法默认带有public访问修饰符,public可以省略不写。
和类的类方法不同,接口的类方法不支持接口引用变量名.类方法的语法。因为本身这种语法在普通类方法的调用中就不推荐,所以接口这里就直接被禁止了。
接口的类方法不能被子接口和实现类继承(不同于普通类,普通类的类方法可以被继承,但不能被重写),所以也必须具有方法体。

调用方式总结:接口的类方法只能用接口名.类方法的语法调用,其他方式都是错误的。

接口的私有方法

因为java8推出了默认方法和类方法(其实就是等价于普通类中的实例方法和类方法的功能)。那当出现多个默认方法或者多个类方法有一段公用的行为逻辑时,最好的解决方法就是用私有的工具方法来实现这段逻辑,达到代码复用的效果。
所以,java9推出了接口的私有方法用private修饰的方法就是私有方法。它有2种版本,私有的类方法和私有的实例方法,根据有没有static修饰符决定。私有的类方法就是接口中类方法的工具方法,私有的实例方法就是接口中默认方法的工具方法

接口的内部类、内部接口、内部枚举

接口的内部类、内部接口和内部枚举都默认采用public static两个修饰符,也就是这些成员都属于接口本身,但可以被子接口或者实现类继承。

接口成员的修饰符

接口是多个类共同的公共行为规范,所以接口里面绝大部分成员都是public访问权限(只有私有方法是private权限,因为这是为接口内部定义的工具方法),所以,除了私有方法外,接口内部其他成员都可以省略也推荐省略掉public访问修饰符

接口的实例变量默认带有public static final修饰符,而且必须在声明时指定默认值,也就是只能是静态常量。

接口的普通实例方法默认带有public abstract修饰符,也就是只能是公有的抽象方法。

接口的默认方法默认带有public修饰符,必须显式标记default修饰符,是必须有方法体的能被实现类继承的方法(作用和普通类中的实例方法一样)。

接口的类方法自带public修饰符,必须显式标记static修饰符,是和接口本身绑定的类方法(作用和普通类的类方法基本一样)。

接口的私有方法必须显式标记private修饰符,根据是哪种类型的工具方法决定是否带有static修饰符,私有方法就是类方法或者默认方法的工具方法。

接口的内部类、内部接口和内部枚举默认带有public static修饰符,和接口直接绑定,但可以被子接口和实现类继承。

接口与构造器

接口没有构造器,因为接口不需要实例化。而且接口的状态只有静态常量,所以连辅助实现类实例化的作用都不需要有(抽象类虽然也不能实例化,但是抽象类有构造器,用于辅助子类实例化)。实现类实现接口后,实现类的构造器会调用其直接父类构造器(有写明直接父类就调用写明的直接父类,没有写直接父类就调用Object类的构造器)。



接口与类之间的各种关系

  1. 接口可以继承多个父接口,接口只能继承接口,不能继承类
  2. 一个类可以实现多个接口,同时还可以继承一个父类
    比如:public class SonClass extends FatherClass implements interA,interB,interC {类具体定义}
  3. 类与类之间只能单继承,但是可以多层单继承。比如C类继承自B类,B类继承自A类。

继承和实现接口,都可以得到父接口的静态常量、抽象方法、默认方法、内部类、内部接口和内部枚举。虽然内部类、内部接口和内部枚举是类相关的,但是子接口和实现类本身可以继承。
但是父接口的类方法属于父接口本身,子接口或者实现类无法继承。
父接口的私有方法是private修饰的,权限属于父接口内部,不允许继承。

可以理解为,接口是为了弥补java只能单继承的缺陷的一种设计。

接口与instanceof运算符

可以用Instanceof运算符检查一个对象是否实现了某个特定的接口。
语法为:
对象引用名 instanceof 接口名

比如:if (printer instanceof Output) {} ,如果为真,表示printer指向的对象是Output接口的一个实现类的实例。



接口的作用

接口体现的是规范和实现分离的设计哲学,是一种松耦合的设计。
一个例子是显卡接口,比如HDMI接口,只要实现了HDMI接口的显卡,就可以插入到HDMI接口的插槽里,实现显卡和主机板的通信,但至于是哪个厂商的哪款显卡,具体用什么方式实现的,接口并不关心。

同一个程序的多个模块之间,也应该通过接口来制定耦合标准,这是一种松耦合。接口一旦制定,最好不要修改,一旦修改,整个系统的大部分类都需要改写。但如果接口不变,模块内部具体实现方式的改变并不会影响其他模块,因为接口定义的规则并没有改变。

多个软件之间也应该采用接口进行耦合,接口就是多个软件之间的通信标准

接口规定了实现者必须向外提供的服务和调用者可以调用的服务。它只是制定服务的规则,但每个服务如何实现,接口并不关心。



接口的使用

接口用3个用途:

  1. 定义引用变量,主要用于多态
  2. 定义类的规范,被其他类实现
  3. 调用接口中的常量

前两条是最核心的用途。

类实现接口的通用语法为:
[修饰符] class 类名 extends 父类 implements 接口1,接口2…
{
类体
}
一个类只能继承一个直接父类,但是可以实现多个接口。

类实现接口和继承父类相似,实现类对象可以获得接口的静态常量,抽象方法和默认方法私有方法、内部类、内部接口和枚举是static类型,会被实现类本身所继承。而接口的类方法很特殊,它属于接口本身,不能被子接口和实现类继承。

一个类实现了接口后,要么实现接口定义的所有抽象方法,要么自身定义为抽象类,留给子类继续实现。

类实现抽象方法的访问权限必须是public,因为接口的抽象方法都默认是public权限,重写接口的方法权限必须大于等于public。

类可以重写所实现接口的默认方法(因为接口的默认方法本质就是带方法体的实例方法),也可以重载继承到的默认方法

接口类型的引用变量必须指向接口实现类的对象,因为接口本身不能创建实例,接口类型的引用变量主要目的就是多态。
所有接口类型的引用变量都可以直接赋给Object类型的引用变量。这是因为接口引用变量指向的对象必定是一个类,它一定是Object类的子类。所以是合理的向上转型



接口与抽象类

接口和抽象类有很多相似点

  1. 抽象类和接口都不能被实例化。
  2. 抽象类和接口都可以包含抽象方法。

但接口和抽象类本质的作用完全不同:
接口体现的是一种规范。相当于系统的总纲,各个程序或者模块应该遵守的标准。接口是不能随便改变的,一改动会辐射到整个系统。

抽象类是一套模板。是实现具体子类过程中的中间产品。这个中间产品实现了子类的公共部分,但更细节更特殊的部分得交给具体的子类分别实现。

抽象类和接口在语法上也是截然不同的

  1. 完全可以把普通类定义为抽象类,抽象类只是多了定义抽象方法的能力,同时失去了实例化的能力。但接口的普通实例方法只能是抽象方法,而为了弥补功能的欠缺,又增加了默认方法、类方法和私有方法。
  2. 接口只能定义静态常量,但抽象类的成员变量没有任何限制。
  3. 接口不包含构造器和初始化块;而抽象类没有限制。只是抽象类的构造器是用于子类调用。而实现接口的类只会调用其直接父类的构造器(没显式写明直接父类就调用Object类的构造器),不需要接口提供构造器。
  4. 一个类只能有一个直接父类,包括抽象类;但一个类可以有多个接口。


接口如何解决多继承的冲突问题

1.如果一个实现类继承了多个接口,每个接口都有同名的常量,则实现类自身必须也定义该同名常量
这解决了多重继承关于实例变量的问题。假设类A和类B都有变量i,而如果类C可以同时继承类A和类B,那类C继承到的i的值到底是多少呢?所以,多继承就会带来矛盾。
所以,java规定,这种情况实现类必须定义同名的变量。这样实现类访问该变量时,直接访问自身定义的同名变量。

在这里插入图片描述
2.普通实例方法只能是抽象方法。
这解决了多重继承方法签名相同的问题。假设类A和类B都有相同的方法签名void test(); {},但两个test方法的方法体不同。而类C可以同时继承类A和类B,那类C继承的test方法到底是类A还是类B的呢? 这就是多继承的矛盾。
而接口的普通实例方法只能是抽象方法,实现类必须重写抽象方法,最后实现类拥有的方法是自己重写后的方法。

3.如果实现类继承了2个接口相同签名的默认方法,那实现类必须重写该默认方法。
其实java接口的默认方法就是弥补接口没有带方法体实例方法的缺陷。但这就带来了继承方法签名相同的2个方法的矛盾。解决方案是:实现类必须重写这个默认方法,这样重写后的方法体才是实现类拥有该默认方法的方法体。

4.假如实现类父类某个实例方法签名和父接口某个默认方法方法签名相同,直接屏蔽掉父接口的默认方法,只继承父类的相同方法签名的实例方法。
java这里的处理非常简单粗暴,父类和接口出现了相同方法签名,直接屏蔽掉接口的默认方法即可。



接口与设计模式

简单工厂模式

接口是一种规范,规范的好处是只设计规则,不管具体实现。所以,用接口来耦合,即使实现改变了,程序之间的连接也不会受到影响。
用简单工厂模式来举例:
先定义一个Output的接口,它用于处理输出相关的问题,先设计Output这个接口,也就是制定输出的规范:

interface Output {
    
    
	// 输出设备的最大缓存数量
    int MAX_CACHE_LINE = 50;
	// 用于输出的抽象方法
    void output();
	// 用于获取数据的抽象方法
    void getData(String msg);
	// 用于打印多段字符串的默认方法
    default void print(String... messages) {
    
    
        for (var message : messages) {
    
    
            System.out.println(message);
        }
    }
}

这个Output接口定义了获取数据的getData方法和打印输出的output方法规范。只是给出规范,但没有具体的实现。
下面是Printer类,它实现了Output接口:

public class Printer implements Output{
    
    
	// 这个字符串数组用于存储得到的字符串
    private final String[] printData = new String[MAX_CACHE_LINE];
    private int dataNum = 0;
    
    @Override
    // 每次打印一行字符串,直到打印完所有的字符串
    public void output() {
    
    
        while (dataNum > 0) {
    
    
            System.out.println("打印机打印:" + printData[0]);
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }
	
    @Override
    // 如果缓存没满,就存入获得的字符串
    public void getData(String msg) {
    
    
        if (dataNum > MAX_CACHE_LINE) {
    
    
            System.out.println("输出队列已满,添加数据失败!");
        } else {
    
    
            printData[dataNum++] = msg;
        }
    }
}

现在,我们有一个Computer类,它需要组合一个输出的设备。核心是,我们需要让接口Output和computer组合,而不是Printer。
因为即使以后改变了Printer实现类的代码,或者用更好的类实现了Output接口,Computer类都不会受到影响。因为Computer类是与接口组合,而接口只是制定了规范,具体实现方式改变并不会影响这种松耦合

public class Computer {
    
    
	// 核心代码,组合的是Output类型的接口,而不是Printer类型的对象
    private Output out;
	
    public Computer(Output out) {
    
    
        this.out = out;
    }

    // 用于模拟输入
    public void keyIn(String msg) {
    
    
        out.getData(msg);
    }

    // 用于模拟打印
    public void print() {
    
    
        out.output();
    }
}

这相当于通过Output接口,让Computer类和Printer类实现了松耦合

接下来,我们需要让产生接口的实现类的位置固定,只能有一个产生实现类的位置。
我们创建一个OutputFactory类来实现这个目的,它就像一个生产Output实现类的工厂:

public class OutputFactory {
    
    
	// 用于产生Output的实现类,如果以后有更好的Output实现类,只需要重构这个方法
    public Output getOutput() {
    
    
        return new Printer();
    }

    public static void main(String[] args) {
    
    
        OutputFactory factory = new OutputFactory();
        Computer cmp = new Computer(factory.getOutput());
        cmp.keyIn("Cauchy");
        cmp.keyIn("LinqQi");
        cmp.print();
    }
}

这就是简单工厂模式,利用接口来实现模块之间的耦合,再给出唯一的生成实现类的方式。假如有了更好的实现类,只需要改变这一处生成实现类的方法,就做到一改全改。尽管实现方式改变了,但由于耦合利用的是接口,所以模块的连接不会受到任何影响。

假如以后有更好的输出接口的实现类,比如BetterOutput类,那只需要改变一处的代码:

public Output getOutput() {
    
    
        return new BetterPrinter();		// 原本是 return new Printer();
    }

这样就做到一改全改。

命令模式

假如某个方法需要完成某个行为,但是具体如何完成这个行为一开始并不确定,必须得等到执行该方法时才可以确定。
这就意味着行为本身得作为参数传递进方法,才可以实现相同的方法执行不同的处理行为
这里的方案是利用接口类型作为某个参数类型,因为接口中的普通方法都是抽象方法,接口类型作为参数,传入的实际是实现类的索引。不同的实现类,实现接口抽象方法的方式不一样,这样就等于传入了不同的行为。
举例:
假如某个方法需要遍历某个整型数组的数组元素,但是不确定具体的遍历方式(也就是遍历方式得作为参数传入进去)。
下面定义处理数组的类:

public class ProcessArray {
    
    
	// 核心是下面这行,target是要处理的目标数组,而第二个参数是一个接口类型的参数,可以传入不同类型的实现类
	// 不同类型的实现类的process方法会不同,这样就相当于把行为作为参数传入,实现了同一个方法在具体执行时可以完成不同的行为
    public void processMode(int[] target, Command cmd) {
    
    
        for (int t : target) {
    
    
            cmd.process(t);
        }
    }
}

定义一个名为Command的接口:
这个接口有个process方法,不同的实现类会重写不同的process方法。

public interface Command {
    
    
    void process(int element);
}

我们可以简单写2个实现类,一个直接输出传入的整型参数,一个输出整型参数的平方。
PrintComand实现类的process方法直接输出目标元素:

public class PrintCommand implements Command{
    
    
    public void process(int element) {
    
    
        System.out.println("目标数组的元素是" + element);
    }
}

SquareComand实现类的process方法输出目标元素的平方:

public class SquareCommand implements Command{
    
    
    public void process(int element) {
    
    
        System.out.println("数组元素的平方是" + element * element);
    }
}

这样就完成了命令模式的定义,核心是这一段代码。
根据传入的实现类不同,会得到不同的process方法,这样数组会以不同的方式遍历:

public void processMode(int[] target, Command cmd) {
    
    
        for (int t : target) {
    
    
            cmd.process(t);
        }
    }

写一个简单的测试程序:

public class CommandTest {
    
    
    public static void main(String[] args) {
    
    
        Command cmd1 = new PrintCommand();
        Command cmd2 = new SquareCommand();
        var pa = new ProcessArray();
        int[] target = {
    
    4, -5, 3, 2};
        pa.processMode(target, cmd1);
        pa.processMode(target, cmd2);
    }
}

对于相同的目标数组,传入不同的实现类对象,就会执行不同的process方法,遍历数组的方式就发生了改变。这就相当于同一个方法可以执行不同的行为逻辑,行为本身作为参数传递。

命令模式利用了接口的实现类对同一个抽象方法,可以有不同的实现。把接口类型作为方法参数,调用接口实现类对象的同一个方法,可能得到不同的实现版本。这就相当于把不同的行为作为参数传入了方法,实现了同一个方法执行不同的行为逻辑。只有在调用方法时,才能根据实参确定方法具体的行为逻辑。

猜你喜欢

转载自blog.csdn.net/qq_983030560/article/details/131562140