Java接口interface关键字定义及如何使用、注意事项

在Java里有一个类似于类的东西,我们已知abstract class为抽象类,不能被实例化,只能被继承
而在Java里面,还有一个类似的就是接口了。可以说,绝大多数Java后端程序员,每天的日常任务就是写接口。可见,接口有多重要,那接口到底应该怎么使用呢?

我们创建一个InterfaceTest.java的文件,并编写以下代码

public class InterfaceTest {
 public static void main(String[] args) {

 }
}
interface Lock {

}

这样通过interface关键字,就能定义名为Lock的接口了

但是需要注意的是,在Java里,不能同时存在public类和public接口,只能保留一个(即一个源文件中不能同时存在public顶级类和public接口),所以如果已经有public class的情况下给interface使用访问修饰符public,编译程序会直接报错

接口的命名规范和类是一样的,但命名只有规范式写法,并不是强制性,只是为了增加可读性和更好的维护

如果我们在接口里面定义方法,则每个方法都会被默认在前面加上public abstract,即公开的抽象的方法。也就是说,我们在接口里定义的方法,默认就是抽象方法,也就是说需要被实现这个接口的类实现。但如果实现接口的类是抽象类,则不需要实现接口里定义的方法(抽象方法)

同时也要注意,接口方法默认为public abstract,实现类重写时访问权限必须是public,可显式写成public,但写成private/protected会导致编译报错

那么接口到底有什么用呢?

1. 定规矩
规定一个类必须有哪些方法。比如MC插件接口规定:插件必须写onEnable() 、 onDisable() ,服务器才认可
2. 解耦合
只规定“要做什么”,不规定“怎么做”。换存储方式、换录屏逻辑,只要接口不变,服务器主程序不用改
3. 支持多实现
Java不能多继承,但一个类可以实现多个接口,灵活扩展功能
4. 统一调用
不管是哪个玩家的录屏、哪种备份逻辑,只要实现同一个接口,代码就能统一管理

那接口和抽象类有什么区别呢?

  1. 能不能写普通方法
    接口:以前只能写抽象方法,Java 8+可以加默认方法,但主要还是规范行为
    抽象类:可以写普通成员方法、构造方法、成员变量,能直接实现逻辑
  2. 能不能多继承
    接口:一个类可以实现多个接口,用来补多种能力
    抽象类:一个类只能继承一个抽象类,单继承
  3. 成员变量
    接口:变量默认都是public static final(常量)
    抽象类:可以有普通成员变量、私有变量
  4. 用途不一样
    接口:表示「拥有某种能力」,比如Serializable可序列化、Listener事件监听
    抽象类:表示「属于某一类事物」,是同类对象的父类模板

但是!我们有一点需要注意

一个常见陷阱:接口字段虽然是常量(public static final),但不能被子接口或实现类“覆盖”,只是隐藏,多态不生效

既然我们知道了接口是什么,那我们就来写一个实例来亲自感受一下接口的魅力吧

接口表示一种能力,那我们就来写一个防盗门的程序吧。继续使用InterfaceTest.java来编写我们的代码

public class InterfaceTest {
 public static void main(String[] args) {

 }
}

之后我们来写一个Door类,用于之后实现接口

class Door {

}

然后我们接下来有Door的类了,就相当于我们有一个门了。但这个门只是一个普普通通的门,并不具备关门锁门的能力,我们要实现这个能力怎么办呢?这时候我们就可以使用接口了

我们来写一个关门锁门的接口,让实现这个接口的Door类具备这个能力

//用于上锁
interface LockUp {
 //这里在编译器编译的时候会自动写成public abstract void lockup();
 public void lockup();
}
//用于解锁
interface Unlock {
 public void unlock();
}

这里我们定义了两个接口,一个用于上锁,一个用于解锁。既然我们有了这两个接口,我们该怎么使用类去实现这两个接口呢?这时候我们就得用到implements关键字了,我们将以上这三行代码整理到一起,并使用implements关键字去实现

public class InterfaceTest {
 public static void main(String[] args) {
  //创建一个名为door的对象
  Door door = new Door();
  //调用Door类实现的方法
  door.lockup();
  door.unlock();
 }
}
class Door implements LockUp, Unlock {
 public void lockup() {
  System.out.println("已上锁");
 }
 public void unlock() {
  System.out.println("已解锁");
 }
}
//用于上锁
interface LockUp {
 //这里在编译器编译的时候会自动写成public abstract void lockup();
 public void lockup();
}
//用于解锁
interface Unlock {
 public void unlock();
}

没错,当我们需要继承多个接口时,每个接口名需要使用“,”也就是英文的逗号隔开。如果这个类同时又继承了一个父类,那么implements关键字需要写在extends关键字的后面

那么,此时控制台就会输出

已上锁
已解锁

[Program finished]

到这里,相信你已经学会了如何去写接口和使用接口。其实接口也是可以继承的,和类继承一样,接口的继承使用的是extends关键字。并且和类不同的是,接口可以多继承,也就是说一个接口可以继承多个接口。具体用法和类继承是一样的,在继承多接口的时候,也是使用“,”英文的逗号来分开

interface A extends B, C, D {}
interface B {}
interface C {}
interface D {}

但是关于继承接口,有几个点需要注意

  1. 如果B、C里有同名默认方法(default),会直接编译报错,必须手动解决冲突
  2. 继承后会合并所有规则
    子接口会把父接口的所有抽象方法、常量、默认方法全部继承下来
    实现子接口的类,必须实现所有父接口 + 子接口的抽象方法
  3. 常量重复定义会覆盖
    父接口里有常量,子接口再定义同名常量,会直接覆盖父接口的,容易出隐藏bug
  4. 默认方法冲突必须手动处理
    两个父接口有同名default方法,子接口必须重写这个方法,否则报错
    可以用父接口名.super.方法() 指定用谁的实现
  5. 抽象方法同名不算冲突,只会保留一个
    多个父接口有一模一样的抽象方法,不报错,最终只算一个方法,实现类只需要实现一次
  6. 接口不能继承抽象类 / 普通类
    接口只能extends接口,不能继承类
  7. 不要滥用多继承
    继承层数太多、接口乱继承,会导致代码难读、方法来源混乱,维护起来很痛苦

我们注意到,上面的注意事项提到了默认方法,也就是default关键字。那这是什么呢?原来在Java 8后引入了default关键字,default是Java 8专门给接口加的关键字,用来写带实现的默认方法,解决以前接口一改动,所有实现类都要跟着改的噩梦

特点是

  1. 有方法体,不是抽象方法
  2. 实现类可以直接调用,也可以选择重写覆盖
  3. 只能出现在接口里

其核心作用是,让继承的接口和实现接口的类不用重写使用了default关键字修饰的方法,相当于“干掉了”abstract关键字。也就是说使用了default关键字修饰的方法不再是抽象方法,实现的类可以直接调用,也可以选择重写覆盖,可以拥有自己的方法体

interface PlayerRecorder {
    //无default修饰的:抽象方法,必须实现。编译时编译器自动在前面加上public abstract
    void start();

    //默认方法:有实现,实现类可写可不写
    default void log(String msg) {
        System.out.println("[Record] " + msg);
    }
}

class UserRecorder implements PlayerRecorder {
    @Override
    public void start() {
        log("开始录制"); //直接用接口里的default方法
    }
}

另外提一嘴,在重写方法时可以加上@Override注解,重写接口方法时加该注解,编译器会校验方法名、参数是否写错,避免拼写错误(比如把lockup写成lock)却查不到问题。但不强制,不加也能运行。加了这个之后,我接口有一个lockup()方法需要重写,但是我错误地写成了lockUp()或者lackup(),导致编译器查不到有lockUp()或者lackup()方法可以被重写,编译器会直接报错。或者我的方法是lockUp(String a)我却重写成了lockUp(String b)或者lockUp(int a)也会直接报错。如果不加的话若发生笔误,则会直接写一个新方法,而无法达到重写方法的目的

注意:@Override注解只能写在需要重写的方法之前,写在方法体里或类的前面,亦或者其他的地方则会直接报错,具体写法可以参考上面代码的写法

回归正题,以前Java程序员在接口里加一个方法,所有实现类都要改一遍,现在用了default关键字可以

  1. 安全扩展接口功能
  2. 不破坏已有代码
  3. 提供通用工具方法(日志、格式处理等)

既然我们知道了default关键字有这么多用处,那我们就顺便来认识一个static静态方法吧。没错,接口里面也是可以使用静态方法的,那接口里面的静态方法有什么用呢?

  1. 专属工具方法,归类更清晰
    比如一个录屏接口ReplayRecorder,需要通用的文件名生成、文件大小校验、路径拼接,直接写在接口里当静态方法,不用新建RecorderUtils工具类
public interface ReplayRecorder {
    // 静态工具方法
    static String generateFileName(String playerName) {
        return playerName + "-" + System.currentTimeMillis() + ".mcpr";
    }
}
  1. 不污染实现类,无需实现/继承
    静态方法属于接口本身,实现类不用管它,不会强制重写,也不会被继承过去
  2. 直接调用,无需实例化
    直接用接口名.方法名()调用,不用new对象,写插件时调用工具逻辑极方便
  3. 避免多实现冲突
    不像default方法会有同名冲突,static方法只归接口自己,互不干扰

那我们该怎么使用呢?我们创建一个Test.java的文件,我们先定义一个接口

//定义一个接口
interface Animal {

    //普通抽象方法:相当于规矩,谁实现谁就要写
    void say();

    //static方法:接口自己的工具方法,直接能用
    static void help() {
        System.out.println("这是动物接口的帮助方法");
    }
}

这里我们定义了一个Animal接口,里面有一个say()方法和一个help()方法。不同的是一个是使用了static修饰的方法,一个则是一个普通的无返回类型的方法。那我们接下来继续写一个类来实现这个接口

//类实现接口
class Dog implements Animal {

    //必须重写接口里的普通方法
    @Override
    public void say() {
        System.out.println("小狗:汪汪");
    }
}

可以看到,我们重写了Animal里面的say()方法,那这个static方法为什么没有重写呢?稍安勿躁,接下来我们来调用一下就知道了。我们在Test类的主方法中来实现这些方法

public class Test {
    public static void main(String[] args) {

        //重点:直接用 接口名.静态方法() 调用
        Animal.help();

        //创建对象,调用普通方法
        Dog dog = new Dog();
        dog.say();
    }
}

接下来我们来看一下控制台的输出

这是动物接口的帮助方法
小狗:汪汪

[Program finished]

可以看到没有报错,是因为static方法可以被直接调用,它只属于接口。因此我们不用专门写一个类来实现这个接口,我们可以直接通过接口名.静态方法名()来调用对应的方法,且静态方法可以具有方法体

所以我们可以总结出使用static的方法有以下特点

  1. 接口里的static方法,直接用接口名字就能调用
  2. 不需要创建对象,也不需要实现类
  3. 它就是接口自带的一个小工具,和实现类没关系

但是使用static修饰方法也必须要注意以下几点

  1. 只能用「接口名」调用,不能用实现类/实例
    错误写法:
public class Test {
 public static void main(String[] args) {
  Dog dog = new Dog();
  dog.help(); //直接报错
 }
}

正确写法

public class Test {
 public static void main(String[] args) {
  Animal.help(); //正确可用
 }
}
  1. 不能被重写(覆盖),没有多态
    实现类写同名静态方法,不是重写,只是自己的新方法,和接口无关
  2. 静态方法里不能调用抽象方法/default方法
    因为静态方法属于接口,没有实例对象,而抽象/default方法都是实例级别的
  3. 不参与继承
    子接口、实现类都不会继承父接口的静态方法,想调用必须用原接口名
  4. 不能和接口里的抽象方法同名
    语法上不报错,但逻辑极度混乱,属于规范禁忌

一句话区分staticdefault

static:接口自己用,不继承、不重写,纯工具方法
default:给实现类用,可继承、可重写,实例方法

defaultstaticprivate对比

default:实例调用,可被重写
static:接口名调用,不能重写
private(Java9+):给接口内部复用,外部不能用

简单总结就是

  1. 接口里的static方法,直接用接口名字就能调用
  2. 不需要创建对象,也不需要实现类
  3. 它就是接口自带的一个小工具,和实现类没关系

最后再来补充一点,在Java 9以后,接口支持写私有方法。接口里可以写private void(),给default方法复用代码,外部不能访问

接口里不能有构造方法、不能实例化

当然还有一个叫函数式接口(FunctionalInterface)的东西
只有一个抽象方法的接口,可以用Lambda简化写法
比如事件监听、异步任务,大量用Lambda

总而言之,本文章只介绍interface接口的使用方法和注意事项,用于帮助一些Java初学者的辅助学习以及知识补充、查漏补缺等,至于一些更深入的知识就不过多介绍了

上一篇