初始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);
    }
}

        程序执行的结果是:

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

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

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

        从上图可以发现,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(" "));
    }
}

        程序执行的结果如下:

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

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.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关键字,生成一个新的类,这个新的类会实现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() + " ");
    }
}

        程序执行的结果是:

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

接口中的字段

        因为接口中的任何字段都是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());
    }
}

        程序执行的结果是:

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

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

        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; // 不可转换
    }
}

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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值