Java学习之旅第二季-16:接口
16.1 接口概述
接口是声明了一系列抽象方法的抽象数据类型,其设计旨在支持在运行时进行动态方法解析。通常情况下,要使一个方法能够从一个类中访问到另一个类,那么这两个类在编译时就必须都存在,以便 Java 编译器能够进行检查,确保方法的声明是正确的、兼容的。这一要求本身就会导致类的结构变得僵化且无法扩展。
接口的设计就是为了避免这种问题。它们将方法的定义与继承层次结构分离开来。由于接口处于与类不同的层次结构中,因此即使在类层次结构上毫无关联的类也可以实现相同的接口,这就是接口真正威力的体现所在。
在实际开发中,接口主要用于以下几点:
- 约定调用方式,实现解耦:在复杂的项目中,通常需要进行分层设计,层之间的调用采用针对接口编程就是最佳实践
- 表示某种能力:这种JDK的API中有很多例子,最典型的莫过于序列化接口了(java.io.Serializable)
从感官上接口可以认为是纯粹的抽象类,它没有实例变量、构造方法、代码块;在Java 8之前,接口只能只包含静态常量及抽象方法,但随着 Java 版本的更新,接口中允许更多形式的成员。
16.2 接口声明
声明接口使用 interface 关键字,语法形式如下:
<修饰符> [abstract] interface 接口名{常量属性声明方法声明嵌套类型声明
}
语法说明:
- 接口的命名规范与类的命名规范相同,采用 Pascal 命名法
- abstract关键字是可省的,默认就是抽象的,所以也不能使用 final 修饰接口
- 修饰符中如果是访问控制修饰符,则与类声明时可用的修饰符一致,只能是 public 或默认
- 接口中的成员包括:常量属性(final修饰),方法(抽象方法,默认方法,静态方法,私有方法),嵌套类型(内部类/接口等)
接口声明示例如下:
public interface Interface1 {
}
上面的接口中没有声明任何成员,也没有从父接口中继承成员,这种接口可以称为标记接口,主要作用是:
- 统一子类的类型,从类型层次上看,所有实现了接口的子类,其类型都是该接口类型
- 用于标记子类在特定的场景中具有特定的意义(能力)。JDK中比较常见的标记接口包括:java.lang.Cloneable,java.io.Serializable,java.rmi.Remote。不过Java 5中引入的注解可以实现相同的功能。
16.3 接口成员
常量属性
在接口中所有的属性都是 public static final 修饰的常量,就算声明时没有显式使用这几个关键字。如:
public interface Interface1 {int num = 0;public static final String name = "";
}
上述成员中声明的 num,修饰符显式写出来与下面的name声明是相同的。
这里有几个注意点:
- 在 Java 中常量的命名规范是全部字母大写,多个单词则使用下划线分隔,所以应该以NUM、NAME命名
- 由于属性是final修饰的,所以必须赋初始值,且接口中没有其他成员,只能在声明的同时赋初始值
- 接口中的成员是static的,访问时使用接口名加点运算符即可,但要注意接口的修饰符可能是包访问级别的,所以public常量有可能访问不到
这种在接口中声明常量的做法之前非常流行,但是随着 Java 5 引入了枚举,推荐使用枚举保存一组相关的值
抽象方法
接口中声明的方法大部分都是抽象的,abstract可省,其默认是public的。在 Java 8之前只有这一种方法。它们是留给子类重写实现的。
public interface Interface1 {/*** 计算两个整数之和** @param num1 第一个整数* @param num2 第二个整数* @return 两个整数之和*/public abstract int add(int num1, int num2);/*** 判断一个整数是否是偶数** @param num 整数* @return 如果给定的整数是偶数,返回true;否则返回false*/boolean isEven(int num);
}
对于上述的两个方法,第一个方法前的修饰符是全的,第二个方法前没有修饰符,但是这两个方法是一样的声明,都是public abstract 修饰,在IDEA中会有提示说它们是多余的且颜色显示为灰色。所以开发中基本都采用第二种较为简单的写法。
静态方法
接口中的静态方法是 Java 8 增加的语法,使用 static 关键字修饰方法,默认是 public 修饰,也只能是 public 修饰。
public interface Interface1 {/*** 将一个整数翻倍** @param num 整数* @return 翻倍后的结果*/static int doubleNum(int num) {return num * 2;}
}
可以在静态方法中实现一些处理逻辑,它不能使用 abstract 或 final 修饰,且该方法不会被实现类或子接口继承。
访问此方法使用接口名加点运算符即可。
默认方法
默认方法也是 Java 8 新增,使用 default 关键字修饰方法,默认是 public 修饰,也只能是 public 修饰。
/*** 将一个整数减半** @param num 整数* @return 减半后的结果,取整*/
default int halfNum(int num) {return num / 2;
}
默认方法对所有实现了该接口的类都提供了默认实现,保证了向后兼容,在子类的实例中可以访问到接口中的默认方法。
不能使用 abstract、final 和 static 修饰。
在子类中可以重写默认方法,子类使用"接口名.super.方法名"访问接口中的默认方法。
默认方法不能与Object中的方法相同,如:不能使用 toString( )。
私有方法
私有方法是 Java 9 新增的,使用 private 修饰的非抽象方法,但可以是静态方法。
/*** 判断一个整数是否是奇数** @param num 整数* @return 如果给定的整数是奇数,返回true;否则返回false*/
private boolean isOdd(int num) {return num % 2 != 0;
}
私有方法顾名思义只能在本接口被访问,静态私有方法可以被本接口中的默认方法及静态方法访问,非静态私有方法则只能被本接口中的默认方法访问。可以减少本接口多个方法中的重复代码。
lc修饰
l可以重写默认方法,子类使用"接口名.super.方法名"访问接口中的默认方法
l不能与Object中的方法相同
16.4 函数式接口
函数式接口(Functional Interface)是 Java 8 新增的语法,与Lambda表达式及Stream API关系密切,本小节只关注其语法特征。
从语法上讲函数式接口是指只有一个抽象方法的接口;接口声明时可以使用可选的@FunctionalInterface注解来表示该接口是否是一个合法的函数式接口,与@Override类似,它并不是必须的,就算没有@FunctionalInterface注解,只有声明是合法则不会有任何语法问题。比如下面的接口就是函数式接口:
@FunctionalInterface
public interface Interface2 {void method1();
}
就算其中的成员有多个,但是有且只有一个抽象方法才是唯一判断标准,如下面的接口也是函数式接口:
@FunctionalInterface
public interface Interface2 {int num = 0;void method1();default void method2() {}static void method3() {}private void method4() {}
}
但是这个不算:
public interface Interface2 {
}
或者有多个抽象方法的也不算函数式接口:
public interface Interface2 {void method1();void method1(int num);
}
另外接口是可以被继承的,所以在继承的接口中方法可能有父接口中的抽象方法,那么算下来如果超过一个也不能算是函数式接口。
16.5 接口的使用
声明完接口之后,如何使用接口有两种不同的场景,第一种是继承接口,第二种是实现接口。至于将接口作为数据类型使用,我们后续再介绍。
继承接口
接口可以被接口继承,在声明子接口时同样使用 extends 关键字继承一个或多个接口,多个接口之间使用逗号分隔。语法如下:
interface 接口名 extends [接口1 [,接口2…]]{}
可以被继承成员包括:常量属性,抽象方法和默认方法;其他的则不可被继承,包括:静态方法及私有方法。示例:
public interface ParentInterface1 {void method1();
}
public interface ParentInterface2 {void method2();
}
public interface ChildInterface extends ParentInterface1, ParentInterface2 {
}
上述的 ChildInterface 接口就同时继承了两个接口,也同时拥有了两个不同的抽象方法,在它的具体子类实现该接口时,要同时实现这两个方法。
实现接口
抽象类和具体类都可以使用 implements 关键字实现一个或多个接口,多个接口之间使用逗号分隔。语法如下:
[abstract] class 类名 implements [接口1[,接口2…]]{}
实现了接口的具体类需要重写接口中所有的抽象方法:
public class Class1 implements ParentInterface1,ParentInterface2{@Overridepublic void method1() {}@Overridepublic void method2() {}
}
实现了接口的抽象类可以重写接口中的部分抽象方法,也可以不重写任何抽象方法。
public abstract class Class2 implements ParentInterface1, ParentInterface2 {@Overridepublic void method1() {}
}
那么抽象子类的具体子类就要实现抽象方法了。
上面两种场景下,有以下的特殊情况:
1、多个父接口中有完全相同的方法声明:子接口/子类就认一个,无所谓从哪里继承的
2、多个父接口中有相同的属性,比如都声明了属性 name:这在声明子接口/子类时没有什么问题,但是在使用子接口/子类加点运算符访问该属性时会出现问题,因为不知道 name 属性从哪个父接口继承而来。IDEA中提示如下:
Reference to 'name' is ambiguous, both 'ParentInterface1.name' and 'ParentInterface2.name' match
3、多个父接口中有相同的默认方法,则子接口或子类必须重写该方法,否则会有语法错误。
4、当多个接口有相同方法名但不同参数的默认方法时,按照方法重载的规则调用
5、当多个接口有除返回值之外都完全相同的方法声明时,不允许同时实现多个接口。如下面两个接口就不能被同时继承或实现:
public interface ParentInterface1 {void method1();
}
public interface ParentInterface2 {int method1();
}
16.6 混合使用类和接口
经过前面的语法介绍,我们知道接口不能使用 extends 继承类,类不能使用 extends 继承接口。但是类可以同时继承一个类和实现若干接口,示例如下:
public interface ParentInterface3 {void method1();
}
public interface ParentInterface4 {void method2();
}
public class ParentClass {}
public class Child extends ParentClass implements ParentInterface3, ParentInterface4 {@Overridepublic void method1() {}@Overridepublic void method2() {}
}
这种语法中 extends 在前,且只能继承一个类,implements在后,可以任意实现接口。
如果父类已经实现了某接口的方法,则子类可以不用重写该方法。
当父类与接口中有相同的方法时,父类中的方法优先。
16.7 小结
本小节详细介绍了Java接口的概念、特性及使用方法。接口作为一种抽象数据类型,通过声明抽象方法支持动态方法解析,有效解决类层次结构僵化问题。文章阐述了接口的声明语法、成员类型(包括常量属性、抽象方法、静态方法、默认方法和私有方法)。最后介绍了接口的两种使用方式:接口继承(extends)和类实现(implements),并说明具体类和抽象类在实现接口时的不同要求。全文系统性地梳理了Java接口的核心知识点,为理解面向对象编程中的接口机制提供了完整参考。