JavaSE笔记——抽象类和接口


前言

接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。


一、抽象类和方法

在前面例子中,基类 Instrument 中的 play() 方法往往是不会用到的方法。其创建它的目的是为派生类创建一个通用方法。在前面例子中,创建这个通用方法的唯一理由是,不同的子类可以用不同的方式来实现。这个通用方法建立了一个基本形式,以此表达所有派生类的共同部分。另一种说法把 Instrument 称为抽象基类,或简称抽象类。

对于像 Instrument 那样的抽象类来说,它的对象几乎总是没有意义的。创建一个抽象类是为了通过通用行为操纵一系列类。因此,Instrument 只是表示一些列相同的行为(方法),不是具体实现,所以创建一个 Instrument 的对象毫无意义,我们可能希望阻止用户这么做。通过让 Instrument 所有的方法产生错误,就可以达到这个目的,但是这么做会延迟到运行时才能得知错误信息,并且需要用户进行可靠、详尽的测试。最好能在编译时捕捉问题。

Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体。下面是抽象方法的声明语法:

abstract void f();

包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,使用 abstract 修饰。否则,编译器会报错。当试图创建抽象类的对象时,编译器也会报错。

public abstract class Basic {
    abstract void unimplemented();

    public static void main(String[] args) {
        Basic basic = new Basic();
    }
}

在这里插入图片描述

如果一个类继承了抽象类并为之创建对象,那么就必须为基类的所有抽象方法提供实现方法。如果不这么做(可以选择不做),这个必须加上 abstract 关键字申明为一个抽象类。

public abstract class Basic1 extends Basic{
    
}

可以将一个不包含任何抽象方法的类指明为 abstract,在类中的抽象方法没啥意义但想阻止创建类的对象时,这么做就很有用。在将一个类指明为 abstract 并不强制类中的所有方法必须都是抽象方法。为了创建可初始化的类,就要继承抽象类,并实现所有抽象方法。

public class Basic2 extends Basic {

    @Override
    void unimplemented() {
        System.out.println("我实现了抽象方法");
    }

    public static void main(String[] args) {
        Basic2 basic2 = new Basic2();
        basic2.unimplemented();
    }
}

留意 @Override 的使用。没有这个注解的话,如果你没有定义相同的方法名或签名,抽象机制会认为你没有实现抽象方法从而产生编译时错误。

二、接口创建

使用 interface 关键字创建接口,interface 和 class 一样随处常见。

public interface PureInterface {
    int m1();

    void m2();

    double m3();
}

在 Java 8 之前我们可以这么说:interface 关键字产生一个完全抽象的类,没有提供任何实现。我们只能描述类应该像什么,做什么,但不能描述怎么做,即只能决定方法名、参数列表和返回类型,但是无法确定方法体。接口只提供形式,通常来说没有实现,尽管在某些受限制的情况下可以有实现。

一个接口表示:所有实现了该接口的类看起来都像这样。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。所以,接口被用来建立类之间的协议。

Java 8 中接口稍微有些变化,因为 Java 8 允许接口包含默认方法和静态方法,接口的基本概念仍然没变,介于类型之上、实现之下。

接口同样可以包含属性,这些属性被隐式指明为 static 和 final,用 implements 关键字使一个类遵循某个特定接口(或一组接口)。

public class Implementation implements PureInterface {
    @Override
    public int m1() {
        return 0;
    }

    @Override
    public void m2() {

    }

    @Override
    public double m3() {
        return 0;
    }
}

你可以选择显式地声明接口中的方法为 public,但是即使你不这么做,它们也是public 的。所以当实现一个接口时,来自接口中的方法必须被定义为 public。

1.默认方法

Java 8 为关键字 default 增加了一个新的用途(之前只用于 switch 语句和注解中)。当在接口中使用它时,任何实现接口却没有定义方法的时候可以使用 default 创建的方法体。默认方法比抽象类中的方法受到更多的限制,但是非常有用。

public interface InterfaceWithDefault {
    void firstMethod();

    void secondMethod();

    default void newMethod() {
        System.out.println("newMethod");
    }
}

public class Implementation2 implements InterfaceWithDefault {
    @Override
    public void firstMethod() {
        System.out.println("firstMethod");
    }

    @Override
    public void secondMethod() {
        System.out.println("secondMethod");
    }

    public static void main(String[] args) {
        Implementation2 implementation2 = new Implementation2();
        implementation2.firstMethod();
        implementation2.secondMethod();
        implementation2.newMethod();
    }
}

增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法或虚拟扩展方法。

2.多继承

多继承意味着一个类可能从多个父类型中继承特征和特性。Java 过去是一种严格要求单继承的语言:只能继承自一个类(或抽象类),但可以实现任意多个接口。

public interface One {
    default void first() {
        System.out.println("first");
    }
}

public interface Two {
    default void second() {
        System.out.println("second");
    }
}

public class MI implements One, Two{
    public static void main(String[] args) {
        MI mi = new MI();
        mi.first();
        mi.second();
    }
}

在这里插入图片描述

如果实现的多个接口中方法名和参数类型一样,需要覆写冲突的方法。

3.接口中的静态方法

Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具:

public interface Operations {
    void execute();

    static void show(String msg) {
        System.out.println(msg);
    }
}

public class Operation1 implements Operations {
    @Override
    public void execute() {
        Operations.show("Operation1");
    }
}

public class Operation2 implements Operations{
    @Override
    public void execute() {
        Operations.show("Operation2");
    }
}

public class Machine {
    public static void main(String[] args) {
        new Operation1().execute();
        new Operation2().execute();
    }
}

在这里插入图片描述

三、抽象类和接口

尤其是在 Java 8 引入 default 方法之后,选择用抽象类还是用接口变得更加令人困惑。下面做了明确的区分:

参数抽象类接口
默认的方法实现它可以有默认的方法实现接口完全是抽象的。它根本不存在方法的实现
实现子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现
构造器抽象类可以有构造器接口不能有构造器
与正常Java类的区别除了你不能实例化抽象类之外,它和普通Java类没有任何区别接口是完全不同的类型
访问修饰符抽象方法可以有public、protected和default这些修饰符接口方法默认修饰符是public。你不可以使用其它修饰符。
main方法抽象方法可以有main方法并且我们可以运行它接口没有main方法,因此我们不能运行它。(java8以后接口可以有default和static方法,所以可以运行main方法)
多继承抽象方法可以继承一个类和实现多个接口接口只可以继承一个或多个其它接口
速度它比接口速度要快接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
添加新方法如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。

四、完全解耦

当方法操纵的是一个类而非接口时,它就只能作用于那个类或其子类,没法把方法应用于那个继承层级结构之外的类。接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码。

例如有一个类 Process 有两个方法 name() 和 process()。process() 方法接受输入,修改并输出。把这个类作为基类用来创建各种不同类型的 Processor。

public class Processor {

    public String name() {
        return getClass().getSimpleName();
    }

    public Object process(Object input) {
        return input;
    }
}

public class Upcase extends Processor {
    @Override
    public String process(Object input) {
        return ((String) input).toUpperCase();
    }
}

public class Downcase extends Processor{
    @Override
    public String process(Object input) {
        return ((String) input).toLowerCase();
    }
}

public class Splitter extends Processor{
    @Override
    public String process(Object input) {
        return Arrays.toString(((String) input).split(" "));
    }
}

public class Applicator {
    public static void apply(Processor p, Object s) {
        System.out.println("Using Processor " + p.name());
        System.out.println(p.process(s));
    }

    public static void main(String[] args) {
        String s = "We are such stuff as dreams are made on";
        apply(new Upcase(), s);
        apply(new Downcase(), s);
        apply(new Splitter(), s);
    }
}

在这里插入图片描述

Applicator 的 apply() 方法可以接受任何类型的 Processor,并将其应用到一个Object 对象上输出结果。像本例中这样,创建一个能根据传入的参数类型从而具备不同行为的方法称为策略设计模式。方法包含算法中不变的部分,策略包含变化的部分。策略就是传入的对象,它包含要执行的代码。在这里,Processor 对象是策略,main() 方法展示了三种不同的应用于 String s 上的策略。

假设现在发现了一组电子滤波器,它们看起来好像能使用 Applicator 的 apply() 方法,如下:

public class Waveform {
    private static long counter;
    private final long id = counter++;

    @Override
    public String toString() {
        return "Waveform " + id;
    }
}

public class Filter {
    public String name() {
        return getClass().getSimpleName();
    }

    public Waveform process(Waveform input) {
        return input;
    }
}

public class LowPass extends Filter {
    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}

public class HighPass extends Filter{
    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}

public class BandPass extends Filter{
    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}

Filter 类与 Processor 类具有相同的接口元素,但是因为它不是继承自 Processor,Filter 类的创建者根本不知道你想将它当作 Processor 使用,因此你不能将 Applicator 的 apply() 方法应用在 Filter 类上。主要是因为 Applicator 的 apply() 方法和 Processor 过于耦合,这阻止了 Applicator 的 apply() 方法被复用。

但如果 Processor 是一个接口,那么限制就会变得松动,只要是实现该接口的所有类都能作为方法参数,这就实现 apply() 方法的复用。Applicator 不需要改变,把 Processor 子类修改如下:

public interface Processor {

    default String name() {
        return getClass().getSimpleName();
    }

    Object process(Object input);
}

public class Upcase implements Processor {
    @Override
    public String process(Object input) {
        return ((String) input).toUpperCase();
    }
}

public class Downcase implements Processor{
    @Override
    public String process(Object input) {
        return ((String) input).toLowerCase();
    }
}

public class Splitter implements Processor{
    @Override
    public String process(Object input) {
        return Arrays.toString(((String) input).split(" "));
    }
}

在这里插入图片描述
我们得到了相同的结果,然而我们经常会遇到的情况是已存在且无法修改类。例如在电子滤波器的例子中,类库已经存在而不是我们创建的,在这些情况下,可以使用适配器设计模式。适配器允许代码接受已有的接口产生需要的方法,如下:

public class FilterAdapter implements Processor{

    private Filter filter;

    FilterAdapter(Filter filter) {
        this.filter = filter;
    }

    @Override
    public String name() {
        return filter.name();
    }

    @Override
    public Waveform process(Object input) {
        return filter.process((Waveform) input);
    }
}

public class Applicator {
    public static void apply(Processor p, Object s) {
        System.out.println("Using Processor " + p.name());
        System.out.println(p.process(s));
    }

    public static void main(String[] args) {
//        String s = "We are such stuff as dreams are made on";
//        apply(new Upcase(), s);
//        apply(new Downcase(), s);
//        apply(new Splitter(), s);

        Waveform w = new Waveform();
        Applicator.apply(new FilterAdapter(new LowPass()), w);
        Applicator.apply(new FilterAdapter(new HighPass()), w);
        Applicator.apply(new FilterAdapter(new BandPass()), w);
    }
}

在这里插入图片描述
在这种使用适配器的方式中,FilterAdapter 的构造器接受已有的接口 Filter,继而产生需要的 Processor 接口的对象。你可能还注意到 FilterAdapter 中使用了委托。协变允许我们从 process() 方法中产生一个 Waveform 而非 Object 对象。将接口与实现解耦使得接口可以应用于多种不同的实现,因而代码更具可复用性。

五、使用继承扩展接口

通过继承,可以很容易在接口中增加方法声明,还可以在新接口中结合多个接口。这两种情况都可以得到新接口。

public interface Monster {
    void menace();
}

public interface DangerousMonster extends Monster{
    void destroy();
}

public class DragonZilla implements DangerousMonster{
    @Override
    public void destroy() {
        System.out.println("destroy方法");
    }

    @Override
    public void menace() {
        System.out.println("menace方法");
    }
}

六、接口适配

接口最吸引人的原因之一是相同的接口可以有多个实现。在简单情况下体现在一个方法接受接口作为参数,该接口的实现和传递对象给方法则交由你来做。因此,接口的一种常见用法是前面提到的策略设计模式。编写一个方法执行某些操作并接受一个指定的接口作为参数。可以说:“只要对象遵循接口,就可以调用方法” ,这使得方法更加灵活,通用,并更具可复用性,前面的 Processor 接口就是如此。

public interface Processor {

    default String name() {
        return getClass().getSimpleName();
    }

    Object process(Object input);
}

七、接口字段

因为接口中的字段都自动是 static 和 final 的,所以接口就成为了创建一组常量的方便的工具。

public interface Months {
    int JANUARY = 1;
    int FEBRUARY = 2;
    int MARCH = 3;
    int APRIL = 4;
    int MAY = 5;
    int JUNE = 6;
    int ULY = 7;
    int AUGUST = 8;
    int SEPTEMBER = 9;
    int OCTOBER = 10;
    int NOVEMBER = 11;
    int DECEMBER = 12;
}

注意 Java 中使用大写字母的风格定义具有初始化值的 static final 变量。接口中的字段自动是 public 的,所以没有显式指明这点。

自 Java 5 开始,我们有了更加强大和灵活的关键字 enum,那么在接口中定义常量组就显得没什么意义了。

八、接口和工厂方法模式

接口是多实现的途径,而生成符合某个接口的对象的典型方式是工厂方法设计模式。不同于直接调用构造器,只需调用工厂对象中的创建方法就能生成对象的实现——理论上,通过这种方式可以将接口与实现的代码完全分离,使得可以透明地将某个实现替换为另一个实现。

public interface Service {
    void method1();
    void method2();
}

public class Service1 implements Service {
    @Override
    public void method1() {
        System.out.println("Service1 method1");
    }

    @Override
    public void method2() {
        System.out.println("Service1 method2");
    }
}

public interface ServiceFactory {
    Service getService();
}

public class Service1Factory implements ServiceFactory {
    @Override
    public Service getService() {
        return new Service1();
    }
}

总结

认为接口是好的选择,从而使用接口不用具体类,这具有诱惑性。几乎任何时候,创建类都可以替代为创建一个接口和工厂。

很多人都掉进了这个陷阱,只要有可能就创建接口和工厂。这种逻辑看起来像是可能会使用不同的实现,所以总是添加这种抽象性。这变成了一种过早的设计优化。

任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值