初始Java 8-2 接口和抽象类

目录

抽象类和接口

完全解耦

组合多个接口

通过继承扩展接口

适配接口

接口中的字段

嵌套接口

接口和工厂

新特性:接口的private方法

新特性:密封类和密封接口


本笔记参考自: 《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);
    }
}

        程序执行的结果是:

初始Java 8-2 接口和抽象类_第1张图片

        上述的Applicator.apply()方法可以任何类型的Processor,将接收的类型转型为Object类型,并输出最终结果。像这样,创建一个方法,这个方法可以根据传递的参数对象表现出不同的行为,这就是策略设计模式。

    方法包含了算法的固定部分,而策略包含了算法变化的部分。

        现在,假设有一组更好用的类:

初始Java 8-2 接口和抽象类_第2张图片

        从上图可以发现,FilterProcessor有相同的接口元素process,但因为Filter没有继承Processor,所有无法将Filter类型的对象传递到Applicator.apply()中进行使用。这种情况下,认为Applicator.apply()Processor之间的耦合超过了所需的程度

    在这组新的类中,process()方法的输入和输出参数都是Waveform

        若Processor是一个接口,因为约束足够宽松,就可以复用参数为Processor接口类型的Applicator.apply()方法了。下面是ProcessorApplicator的修改:

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(" "));
    }
}

        程序执行的结果如下:

初始Java 8-2 接口和抽象类_第3张图片

        但也有上述这种处理方式应付不了的情况。因为库一般是被发现而不是被创建的,在这种情况下,就会需要使用到适配器设计模式:

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类型
    }
}

        注意:当通过上述这种方式结合具体的类和接口时,具体的类必须在前面,然后才是接口

        上述程序中,CanFightActionCharacter包含了同样签名的方法fight(),并且Hero中并没有为fight()提供具体的定义。但是在创建一个对象时,所有的定义都必须是已经存在的。此处之所以没有触发报错,是因为ActionCharacter提供了fight()的定义。

        使用接口的两个原因:

  1. 向上转型为多个基类型,并且利用这样做带来的灵活性。
  2. 防止客户程序员创建此类的对象,并且明确表示这只是一个接口。

    若可以在没有任何方法定义或成员变量的情况下创建基类,就应该使用接口而不是抽象类。

通过继承扩展接口

        可以使用继承向接口中添加新的方法声明,也可以通过继承组合多个接口。这两种方式最终都会得到一个新的接口:

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关键字可以用来关联多个父接口。注意接口名称要用逗号分隔。

组合接口时的名称冲突

        在之前CanFightActionCharacter的例子中,接口和类具有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 8-2 接口和抽象类_第4张图片

    因此,在接口中应该尽量避免使用相同的方法名称。

适配接口

        引入接口的又一个原因是,接口可以允许同一个接口有多个实现。这可以体现为一个接收接口的方法,调用者实现该接口,并将接口作为对象传递给方法。这就回到了之前说的策略设计模式,这种方法灵活、通用并且有更高的可复用性。

        例如,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 8-2 接口和抽象类_第5张图片

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

初始Java 8-2 接口和抽象类_第6张图片

        可以向这个参数中通过各种方法添加数据,或者当没有输入(此时返回-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关键字,生成一个新的类,这个新的类会实现RandomDoubleReadable

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() + " ");
    }
}

        程序执行的结果是:

初始Java 8-2 接口和抽象类_第7张图片

        任何现有类都可以通过这种适配器的方式进行接口的添加,这意味着把接口作为参数的方法可以让任何类适应它。

接口中的字段

        因为接口中的任何字段都是staticfinal的,所有接口也是创建一组常量值的便捷工具:

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());
    }
}

        程序执行的结果是:

初始Java 8-2 接口和抽象类_第8张图片

        通过工厂方法进行额外层的添加,这种做法可以用来创建框架。假设所需实现的方法更加复杂,框架的存在就会更加方便对代码的复用。

新特性:接口的private方法

        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)。通过这种方式,我们可以确保自己的任何代码只需要考虑D1D2

        也可以对接口和抽象类进行密封:

// 密封接口
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子句允许我们在单独的文件夹中定义子类:

初始Java 8-2 接口和抽象类_第9张图片

        sealed类的子类只允许使用下列的某个修饰符进行定义:

  • final:不允许有进一步的子类。
  • sealed:允许有一个密封子类。
  • no-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; // 不可转换
    }
}

    最后:接口在程序设计中,往往是处于用来进行优化的角色。若在程序一开始就使用接口,最终可能会使程序变得太过复杂。接口应该是在必要时用来重构的工具。因此,可以这么说:应该优先使用类而不是接口。

你可能感兴趣的:(Java,java,开发语言)