目录
本笔记参考自: 《On Java 中文版》
在Java 8添加了默认方法后,如何选择抽象类和接口也成了一个问题。下列表格会将两者进行区分:
| 特性 | 接口 | 抽象类 |
|---|---|---|
| 组合 | 可以在新类中组合多个接口 | 只能继承一个抽象类 |
| 状态 | 不能包含字段(除静态字段,但静态字段无法表示对象状态) | 可以包含字段,非抽象类可以使用这些字段 |
| 默认方法和抽象方法 | 默认方法不需要在子类型中实现 | 抽象方法必须在子类型中实现 |
| 构造器 | 不能有构造器 | 可以有构造器 |
| 访问权限控制 | 方法默认被public static abstract修饰 常量默认被public static final修饰 | 可以设置,默认是包访问权限 |
经验法则告诉我们:在合理的范围内尽可能抽象。当然,除非必要,否则抽象类和接口都还是不用为好,因为常规类已经足够解决问题。
一个方法若只存在于常规的类中,那么这个方法只能被这个类及其子类调用。一旦想要让这个方法脱离这一继承层次结构,就会发现这是难以做到的。接口就放宽了这种限制,使得代码可以更加容易被复用。
- import java.util.Arrays;
-
- class Processor {
- public String name() {
- return getClass().getSimpleName();
- }
-
- public Object process(Object input) {
- return input;
- }
- }
-
- class Upcase extends Processor {
- @Override
- public String process(Object input) { // 返回类型是协变的
- return ((String) input).toUpperCase();
- }
- }
-
- class Downcase extends Processor {
- @Override
- public String process(Object input) {
- return ((String) input).toLowerCase();
- }
- }
-
- class Splitter extends Processor {
- @Override
- public String process(Object input) {
- // split()方法可以分割字符串
- return Arrays.toString(((String) input).split(" "));
- }
- }
-
- public class Applicator {
- public static void apply(Processor p, Object s) {
- System.out.println("使用方法:" + p.name());
- System.out.println(p.process(s));
- System.out.println();
- }
-
- public static void main(String[] args) {
- String s = "We are Programmer";
- apply(new Upcase(), s);
- apply(new Downcase(), s);
- apply(new Splitter(), s);
- }
- }
程序执行的结果是:

上述的Applicator.apply()方法可以任何类型的Processor,将接收的类型转型为Object类型,并输出最终结果。像这样,创建一个方法,这个方法可以根据传递的参数对象表现出不同的行为,这就是策略设计模式。
方法包含了算法的固定部分,而策略包含了算法变化的部分。
现在,假设有一组更好用的类:

从上图可以发现,Filter和Processor有相同的接口元素process,但因为Filter没有继承Processor,所有无法将Filter类型的对象传递到Applicator.apply()中进行使用。这种情况下,认为Applicator.apply()与Processor之间的耦合超过了所需的程度。
在这组新的类中,process()方法的输入和输出参数都是Waveform。
若Processor是一个接口,因为约束足够宽松,就可以复用参数为Processor接口类型的Applicator.apply()方法了。下面是Processor和Applicator的修改:
- package interfaceprocessor;
-
- public interface Processor {
- default String name() {
- return getClass().getSimpleName();
- }
-
- Object process(Object input);
- }
- package interfaceprocessor;
-
- public class Applicator {
- public static void apply(Processor p, Object s) {
- System.out.println("使用方法:" + p.name());
- System.out.println(p.process(s));
- }
- }
一种复用代码的方式是,调用者可以编写符合这个接口的类:
- package interfaceprocessor;
-
- import java.util.Arrays;
-
- interface StringProcessor extends Processor {
- // @Override不是必要的,但它可以指出返回类型发生了从Object到String的协变
- @Override
- String process(Object input);
-
- String s = "You are an Programmer"; // 在接口内定义的字段是static和final的
-
- public static void main(String[] args) { // main()方法的定义也是被允许的
- Applicator.apply(new Upcase(), s);
- Applicator.apply(new Downcase(), s);
- Applicator.apply(new Splitter(), s);
- }
- }
-
- class Upcase implements StringProcessor {
- @Override
- public String process(Object input) { // 返回类型是协变的
- return ((String) input).toUpperCase();
- }
- }
-
- class Downcase implements StringProcessor {
- @Override
- public String process(Object input) {
- return ((String) input).toLowerCase();
- }
- }
-
- class Splitter implements StringProcessor {
- @Override
- public String process(Object input) {
- return Arrays.toString(((String) input).split(" "));
- }
- }
程序执行的结果如下:

但也有上述这种处理方式应付不了的情况。因为库一般是被发现而不是被创建的,在这种情况下,就会需要使用到适配器设计模式:
- package interfaceprocessor;
-
- import filters.*;
-
- class FilterAdapter implements Processor {
- Filter filter;
-
- FilterAdapter(Filter filter) {
- this.filter = filter;
- }
-
- @Override
- public String name() { // 使用了委托
- return filter.name();
- }
-
- public Waveform process(Object input) { // 返回类型是协变的,这允许我们产生一个Waveform
- return filter.process((Waveform) input);
- }
- }
-
- public class FilterProcessor {
- public static void main(String[] args) {
- Waveform w = new Waveform();
- Applicator.apply(new FilterAdapter(new LowPass(1.0)), w);
- Applicator.apply(new FilterAdapter(new HighPass(2.0)), w);
- Applicator.apply(new FilterAdapter(new BandPass(3.0, 4.0)), w);
- }
- }
接口与实现的解耦允许我们将一个接口应用于多个不同的实现。
接口没有实现,也就是说,没有与接口有关联的储存储。这为多个接口组合在一起提供了合理性。
Java没有强制要求一个子类的基类是抽象的或是具体的。一个子类只能继承一个非接口,但同时,这个子类也可以继承复数的接口(这些接口名都应该被放置在implement关键字之后,并用逗号隔开)。例如:
- // 一组接口
- interface CanFight {
- void fight();
- }
-
- interface CanSwim {
- void swim();
- }
-
- interface CanFly {
- void fly();
- }
-
- // 一个基类
- class ActionCharacter {
- public void fight() {
- };
- }
-
- class Hero extends ActionCharacter
- implements CanFight, CanSwim, CanFly {
- // 此处没有为fight提供定义
- @Override
- public void swim() {
- }
-
- @Override
- public void fly() {
- }
- }
-
- public class Adventure {
- public static void t(CanFight x) {
- x.fight();
- }
-
- public static void u(CanSwim x) {
- x.swim();
- }
-
- public static void v(CanFly x) {
- x.fly();
- }
-
- public static void w(ActionCharacter x) {
- x.fight();
- }
-
- public static void main(String[] args) {
- Hero h = new Hero();
- t(h); // 当作一个Canfight类型
- u(h); // 把Hero当作一个CanSwim类型
- v(h); // 同样进行了转型
- w(h); // 当作一个ActionCharacter类型
- }
- }
注意:当通过上述这种方式结合具体的类和接口时,具体的类必须在前面,然后才是接口。
上述程序中,CanFight和ActionCharacter包含了同样签名的方法fight(),并且Hero中并没有为fight()提供具体的定义。但是在创建一个对象时,所有的定义都必须是已经存在的。此处之所以没有触发报错,是因为ActionCharacter提供了fight()的定义。
使用接口的两个原因:
若可以在没有任何方法定义或成员变量的情况下创建基类,就应该使用接口而不是抽象类。
可以使用继承向接口中添加新的方法声明,也可以通过继承组合多个接口。这两种方式最终都会得到一个新的接口:
- interface Monster {
- void menace();
- }
-
- interface DangerousMonster extends Monster {
- void destroy();
- }
-
- interface Lethal {
- void kill();
- }
-
- class DragonZilla implements DangerousMonster {
- @Override
- public void menace() {
- }
-
- @Override
- public void destroy() {
- }
- }
-
- interface Vampire extends DangerousMonster, Lethal {
- void drinkBlood();
- }
-
- class VeryBadVampire implements Vampire {
- @Override
- public void menace() {
- }
-
- @Override
- public void destroy() {
- }
-
- @Override
- public void kill() {
- }
-
- @Override
- public void drinkBlood() {
- }
- }
-
- public class HorroShow {
- static void u(Monster b) {
- b.menace();
- }
-
- static void v(DangerousMonster d) {
- d.menace();
- d.destroy();
- }
-
- static void w(Lethal l) {
- l.kill();
- }
-
- public static void main(String[] args) {
- DangerousMonster barney = new DragonZilla();
- u(barney);
- v(barney);
-
- Vampire vlad = new VeryBadVampire();
- u(vlad);
- v(vlad);
- w(vlad);
- }
- }
在进行新接口的创建时,extends关键字可以用来关联多个父接口。注意接口名称要用逗号分隔。
组合接口时的名称冲突
在之前CanFight和ActionCharacter的例子中,接口和类具有void fight()方法。因为ActionCharacter提供了定义,因此没有任何问题。但如果方法的签名或返回类型不同,情况就会发生改变。
- // 3个接口
- interface I1 {
- void f();
- }
-
- interface I2 {
- int f(int i);
- }
-
- interface I3 {
- int f();
- }
-
- // 提供了一个声明
- class C {
- public int f() {
- return 1;
- }
- }
-
- // 对不同的接口进行组合
- class C2 implements I1, I2 {
- @Override
- public void f() {
- }
-
- @Override
- public int f(int i) { // 发生重载
- return 2;
- }
- }
-
- class C3 extends C implements I2 {
- @Override
- public int f(int i) { // 发生重载
- return 3;
- }
- }
-
- class C4 extends C implements I3 { // 两者的f()方法定义完全相同,可以直接使用
- }
-
- // 下面是无法组合的情况:方法只有返回类型不同
- // class C5 extends C implements I1 {
- // }
-
- // interface I4 extends I1, I3 {
- // }
上述程序中,最后的两种组合将重写、实现和重载混在了一起,若取消注释并尝试编译,会引发报错:

因此,在接口中应该尽量避免使用相同的方法名称。
引入接口的又一个原因是,接口可以允许同一个接口有多个实现。这可以体现为一个接收接口的方法,调用者实现该接口,并将接口作为对象传递给方法。这就回到了之前说的策略设计模式,这种方法灵活、通用并且有更高的可复用性。
例如,java.util包提供了一个Scanner类,这个类的构造器会接收一个Readable接口作为参数。Readable是一个专门为Scanner创建的接口,这样Scanner的参数就不会受到类型的约束。若想要让一个类能够和Scanner一起被使用,只需要让这个新类实现Readable接口即可:
- import java.nio.CharBuffer;
- import java.util.Random;
- import java.util.Scanner;
-
- public class RandomStrings implements Readable {
- private static Random rand = new Random(47);
- private static final char[] CAPITALS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
- private static final char[] LOWERS = "abcdefghijklmnopqrstuvwxyz".toCharArray();
- private static final char[] VOWELS = "aeiou".toCharArray();
-
- private int count;
-
- public RandomStrings(int count) {
- this.count = count;
- }
-
- @Override
- public int read(CharBuffer cb) {
- if (count-- == 0) // 若输入已经结束
- return -1;
- cb.append(CAPITALS[rand.nextInt(CAPITALS.length)]);
- for (int i = 0; i < 4; i++) {
- cb.append(VOWELS[rand.nextInt(VOWELS.length)]);
- cb.append(LOWERS[rand.nextInt(LOWERS.length)]);
- }
-
- cb.append(" ");
- return 10;
- }
-
- public static void main(String[] args) {
- Scanner s = new Scanner(new RandomStrings(10));
- while (s.hasNext())
- System.out.println(s.next());
- }
- }
程序执行的结果是:

通过java.lang提供的Readable接口可以实现一个read()方法。read()方法的参数列表是一个CharBuffer类型的参数,可以在文档中找到关于这个类型的描述:

可以向这个参数中通过各种方法添加数据,或者当没有输入(此时返回-1)。
但若一个类型没有实现Readable,要让其能够与Scanner一起工作,就会需要使用到多重继承。现在,假设有一个没有实现Readable的接口RandomDoubles:
- import java.util.Random;
-
- public interface RandomDoubles {
- Random RAND = new Random(47);
-
- default double next() {
- return RAND.nextDouble();
- }
-
- public static void main(String[] args) {
- RandomDoubles rd = new RandomDoubles() {
- };
-
- for (int i = 0; i < 7; i++) {
- System.out.println(rd.next() + " ");
- }
- }
- }
此时,就可以使用适配器模式,组合两个不同的接口来创建一个适配的类。现在,使用interface关键字,生成一个新的类,这个新的类会实现RandomDouble和Readable:
- import java.nio.CharBuffer;
- import java.util.Scanner;
-
- public class AdaptedRandomDoubles
- implements RandomDoubles, Readable {
- private int count;
-
- public AdaptedRandomDoubles(int count) {
- this.count = count;
- }
-
- @Override
- public int read(CharBuffer cb) {
- if (count-- == 0)
- return -1;
-
- String result = Double.toString(next()) + " ";
- cb.append(result);
- return result.length();
- }
-
- public static void main(String[] args) {
- Scanner s = new Scanner(new AdaptedRandomDoubles(7)); // 使用Scanner
- while (s.hasNextDouble())
- System.out.println(s.nextDouble() + " ");
- }
- }
程序执行的结果是:

任何现有类都可以通过这种适配器的方式进行接口的添加,这意味着把接口作为参数的方法可以让任何类适应它。
因为接口中的任何字段都是static和final的,所有接口也是创建一组常量值的便捷工具:
- public class Months {
- int JABUARY = 1,
- FABRUARY = 2;
- // ...
- }
注意:Java中具有常量初始值的static final字段的命名全部使用大写字符(并且使用下划线分隔单词)。
在Java 5之前,Java经常通过这种方式实现枚举。
初始化接口中的字段
接口中的定义字段不能是“空白的final”,但可以通过非常量表达式进行初始化:
- import java.util.Random;
-
- public interface RandVals {
- Random RAND = new Random();
- int RANDOM_INT = RAND.nextInt(10);
- long RANDOM_LONG = RAND.nextLong() * 10;
- float RANDOM_FLOAT = RAND.nextFloat() * 10;
- double RANDOM_DOUBLE = RAND.nextDouble() * 10;
- }
这些字段都是静态的,它们会在接口第一次被加载时初始化。简单地看看:
- public class TestRandVals {
- public static void main(String[] args) {
- System.out.println(RandVals.RANDOM_DOUBLE);
- System.out.println(RandVals.RANDOM_DOUBLE);
- }
- }
程序执行的结果是:
![]()
接口中定义的字段不是接口的一部分。这些字段的值会储存在接口的静态存储区中。
接口可以嵌套在类和其他接口中:
- package nesting;
-
- class A {
- interface B {
- void f();
- }
-
- public class BImp implements B {
- @Override
- public void f() {
- }
- }
-
- private class BImp2 implements B {
- @Override
- public void f() {
- }
- }
-
- private interface C {
- void f();
- }
-
- private class CImp implements C {
- @Override
- public void f() {
- }
- }
-
- public class CImp2 implements C {
- @Override
- public void f() {
- }
- }
-
- public C getC() {
- return new CImp2();
- }
-
- private C cRef;
-
- public void receiveC(C c) {
- cRef = c;
- cRef.f();
- }
- }
-
- interface D {
- interface E {
- void f();
- }
-
- public interface F { // 此处可以省略public
- void f();
- }
-
- void g();
-
- // 不能在接口中使用private
- // private interface H {
- // }
- }
-
- public class NestingInterfaces {
- public class BImp implements A.B {
- @Override
- public void f() {
- }
- }
-
- // private的接口只能在定义的类中实现
- // class DImp implements A.D {
- // public void f() {
- // };
- // }
-
- class DImp implements D {
- @Override
- public void g() {
- };
-
- class DE implements D.E {
- @Override
- public void f() {
- }
- }
- }
-
- public static void main(String[] args) {
- A a = new A();
-
- // A.C无法访问:
- // A.C ac = a.getC();
-
- // 只能返回A.C:
- // A.CImp2 ci2 = a.getC(); // 无法接收返回值
-
- // 无法访问接口C中的方法
- // a.getC().f();
-
- // 需要使用到第二个A对象,才能处理getC()
- A a2 = new A();
- a2.receiveC(a.getC());
- }
- }
在类中进行接口嵌套的语句与正常使用几乎没有区别。它们都可以具有public或是包访问权限。
值得一提的是,接口也可以是private的,就像A.C一样。这种接口会被用于:① 实现像CImp一样的私有内部类;② 像CImp2一样的public类,这种类只有自己的类型,在外界看来其与接口C无关,这种做法限制了接口C中的方法定义,也就是说,private的接口不允许任何的向上转型。
上述程序中a.getC()的使用无疑是特殊的:这个方法的返回值必须传递给一个有权使用它的对象,也就是另一个A。
所有的接口元素都必须是public的,所以嵌套在其他接口中的接口默认也是public的(也只能是)。
通过接口,可以进行多种的实现。若想要生成适合某个接口的对象,就可以采取工厂方法设计模式:不直接调用构造器,而是在工厂对象上调用创建方法,这种创建方法可以产生接口实现。
- interface Service {
- void method1();
-
- void method2();
- }
-
- interface ServiceFactory {
- Service getService();
- }
-
- // 1号服务
- class Service1 implements Service {
- Service1() { // 将构造器限定为包访问,不允许外部使用
- }
-
- @Override
- public void method1() {
- System.out.println("1号服务:方法1");
- }
-
- @Override
- public void method2() {
- System.out.println("1号服务:方法2");
- }
- }
-
- class Service1Factory implements ServiceFactory {
- @Override
- public Service getService() {
- return new Service1();
- }
- }
-
- // 2号服务
- class Service2 implements Service {
- Service2() { // 具有包访问权限的构造器
- }
-
- @Override
- public void method1() {
- System.out.println("2号服务:方法1");
- }
-
- @Override
- public void method2() {
- System.out.println("2号服务:方法2");
- }
- }
-
- class Service2Factory implements ServiceFactory {
- @Override
- public Service getService() {
- return new Service2();
- }
- }
-
- public class Factories {
- public static void serviceConsumer(ServiceFactory fact) {
- Service s = fact.getService();
- s.method1();
- s.method2();
- }
-
- public static void main(String[] args) {
- // 通过“工厂”,调用不同的服务
- serviceConsumer(new Service1Factory());
- System.out.println();
- serviceConsumer(new Service2Factory());
- }
- }
程序执行的结果是:

通过工厂方法进行额外层的添加,这种做法可以用来创建框架。假设所需实现的方法更加复杂,框架的存在就会更加方便对代码的复用。
JDK 9最终确定,可以将接口中的方法转换为private方法:
- interface JDK9 {
- private void fd() { // private方法默认是default的
- System.out.println("JDK9::fd()");
- }
-
- private static void fs() {
- System.out.println("JDK::fs()");
- }
-
- default void f() {
- fd();
- }
-
- static void g() {
- fs();
- }
- }
-
- class ImplJDK9 implements JDK9 {
- }
-
- public class PrivateInterfaceMethods {
- public static void main(String[] args) {
- new ImplJDK9().f();
- JDK9.g();
- }
- }
程序运行的结果如下:
![]()
JDK 17最终确定引入密封类(sealed)和密封接口,这样基类或接口就可以限制自己能派生的类:
- sealed class Base permits D1, D2 {}
-
- final class D1 extends Base {}
-
- final class D2 extends Base {}
-
- // 这是非法的:
- //final class D3 extends Base {}
若继承了未在permits子句中列出的子类,就会发生报错(如:D3)。通过这种方式,我们可以确保自己的任何代码只需要考虑D1和D2。
也可以对接口和抽象类进行密封:
- // 密封接口
- sealed interface Ifc permits Imp1, Imp2 {}
-
- final class Imp1 implements Ifc {}
-
- final class Imp2 implements Ifc {}
-
- // 密封抽象类
- sealed abstract class AC permits X {}
-
- final class X extends AC {}
若需要继承基类的子类都在同一个文件夹中,就不需要permit子句:
- sealed class Shape {}
-
- final class Circle extends Shape {}
-
- final class Triangle extends Shape {}
而permits子句允许我们在单独的文件夹中定义子类:

sealed类的子类只允许使用下列的某个修饰符进行定义:
注意:一个sealed类有至少一个子类。而sealed的子类会保持对层次结构的严格控制。
record
JDK 16的record也可以实现接口的密封。record是隐式的final,因此它不需要与final并用:
- package interfaces;
-
- sealed interface Employee permits CLevel, Programer { }
-
- record CLevel(String type)
- implements Employee { }
-
- record Programer(String experience)
- implements Employee{}
编译器会阻止我们从密封层次结构中向下转型为非法类型:
- sealed interface II permits JJ {}
-
- final class JJ implements II {}
-
- class Something {}
-
- public class CheckedDowncast {
- public void f() {
- II i = new JJ(); // 向上转型
- JJ j = (JJ) i; //强制类型转换
-
- // Something s = (Something) i; // 不可转换
- }
- }
![]()
最后:接口在程序设计中,往往是处于用来进行优化的角色。若在程序一开始就使用接口,最终可能会使程序变得太过复杂。接口应该是在必要时用来重构的工具。因此,可以这么说:应该优先使用类而不是接口。