十、接口(2)

news2024/11/19 9:23:41

本章概要

  • 抽象类和接口
  • 完全解耦
  • 多接口结合
  • 使用继承扩展接口
    • 结合接口时的命名冲突

抽象类和接口

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

特性接口抽象类
组合新类可以组合多个接口只能继承单一抽象类
状态不能包含属性(除了静态属性,不支持对象状态)可以包含属性,非抽象方法可能引用这些属性
默认方法 和 抽象方法不需要在子类中实现默认方法。默认方法可以引用其他接口的方法必须在子类中实现抽象方法
构造器没有构造器可以有构造器
可见性隐式 public可以是 protected 或 “friendly”

抽象类仍然是一个类,在创建新类时只能继承它一个。而创建类的过程中可以实现多个接口。

有一条实际经验:在合理的范围内尽可能地抽象。因此,更倾向使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。

完全解耦

每当一个方法与一个类而不是接口一起工作时(当方法的参数是类而不是接口),你只能应用那个类或它的子类。如果你想把这方法应用到一个继承层次之外的类,是做不到的。接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码。

例如有一个类 Processor 有两个方法 name()process()process() 方法接受输入,修改并输出。把这个类作为基类用来创建各种不同类型的 Processor。下例中,Processor 的各个子类修改 String 对象(注意,返回类型可能是协变类型而非参数类型):

// interfaces/Applicator.java
import java.util.*;

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() divides a String into pieces:
        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);
    }
}

输出:

Using Processor Upcase
WE ARE SUCH STUFF AS DREAMS ARE MADE ON
Using Processor Downcase
we are such stuff as dreams are made on
Using Processor Splitter
[We, are, such, stuff, as, dreams, are, made, on]

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

split()String 类中的方法,它接受 String 类型的对象并以传入的参数作为分割界限,返回一个数组 String[]。在这里用它是为了更快地创建 String 数组。

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

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 {
    double cutoff;
    
    public LowPass(double cutoff) {
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
        return input; // Dummy processing 哑处理
    }
}
public class HighPass extends Filter {
    double cutoff;
    
    public HighPass(double cutoff) {
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}
public class HighPass extends Filter {
    double cutoff;
    
    public HighPass(double cutoff) {
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}
public class BandPass extends Filter {
    double lowCutoff, highCutoff;
    
    public BandPass(double lowCut, double highCut) {
        lowCutoff = lowCut;
        highCutoff = highCut;
    }
    
    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}

Filter 类与 Processor 类具有相同的接口元素,但是因为它不是继承自 Processor —— 因为 Filter 类的创建者根本不知道你想将它当作 Processor 使用 —— 因此你不能将 Applicatorapply() 方法应用在 Filter 类上,即使这样做也能正常运行。主要是因为 Applicatorapply() 方法和 Processor 过于耦合,这阻止了 Applicatorapply() 方法被复用。另外要注意的一点是 Filter 类中 process() 方法的输入输出都是 Waveform

但如果 Processor 是一个接口,那么限制就会变得松动到足以复用 Applicatorapply() 方法,用来接受那个接口参数。下面是修改后的 ProcessorApplicator 版本:

public interface Processor {
    default String name() {
        return getClass().getSimpleName();
    }
    
    Object process(Object 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));
    }
}

复用代码的第一种方式是客户端程序员遵循接口编写类,像这样:

// interfaces/interfaceprocessor/StringProcessor.java
// {java interfaces.interfaceprocessor.StringProcessor}
package interfaces.interfaceprocessor;
import java.util.*;

interface StringProcessor extends Processor {
    @Override
    String process(Object input); // [1]
    String S = "If she weighs the same as a duck, she's made of wood"; // [2]
    
    static void main(String[] args) { // [3]
        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(" "));
    }
}

输出:

Using Processor Upcase
IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
Using Processor Splitter
[If, she, weighs, the, same, as, a, duck,, she's, made, of, wood]

[1] 该声明不是必要的,即使移除它,编译器也不会报错。但是注意这里的协变返回类型从 Object 变成了 String。

[2] S 自动就是 final 和 static 的,因为它是在接口中定义的。

[3] 可以在接口中定义 main() 方法。

这种方式运作得很好,然而你经常遇到的情况是无法修改类。例如在电子滤波器的例子中,类库是被发现而不是创建的。在这些情况下,可以使用_适配器_设计模式。适配器允许代码接受已有的接口产生需要的接口,如下:

// interfaces/interfaceprocessor/FilterProcessor.java
// {java interfaces.interfaceprocessor.FilterProcessor}
package interfaces.interfaceprocessor;
import interfaces.filters.*;

class FilterAdapter implements Processor {
    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 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);
    }
}

输出:

Using Processor LowPass
Waveform 0
Using Processor HighPass
Waveform 0
Using Processor BandPass
Waveform 0

在这种使用适配器的方式中,FilterAdapter 的构造器接受已有的接口 Filter,继而产生需要的 Processor 接口的对象。你可能还注意到 FilterAdapter 中使用了委托。

协变允许我们从 process() 方法中产生一个 Waveform 而非 Object 对象。

将接口与实现解耦使得接口可以应用于多种不同的实现,因而代码更具可复用性。

多接口结合

接口没有任何实现——也就是说,没有任何与接口相关的存储——因此无法阻止结合的多接口。这是有价值的,因为你有时需要表示“一个 x 是一个 a 和一个 b 以及一个 c”。

在这里插入图片描述

派生类并不要求必须继承自抽象的或“具体的”(没有任何抽象方法)的基类。如果继承一个非接口的类,那么只能继承一个类,其余的基元素必须都是接口。需要将所有的接口名称置于 implements 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。下例展示了一个由多个接口组合而成的具体类产生的新类:

// interfaces/Adventure.java
// Multiple interfaces
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 {
    public void swim() {}
    
    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); // Treat it as a CanFight
        u(h); // Treat it as a CanSwim
        v(h); // Treat it as a CanFly
        w(h); // Treat it as an ActionCharacter
    }
}

Hero 结合了具体类 ActionCharacter 和接口 CanFightCanSwimCanFly。当通过这种方式结合具体类和接口时,需要将具体类放在前面,后面跟着接口(否则编译器会报错)。

接口 CanFight 和类 ActionCharacter 中的 fight() 方法签名相同,而在类 Hero 中也没有提供 fight() 的定义。可以扩展一个接口,但是得到的是另一个接口。当想创建一个对象时,所有的定义必须首先都存在。类 Hero 中没有显式地提供 fight() 的定义,是由于该方法在类 ActionCharacter 中已经定义过,这样才使得创建 Hero 对象成为可能。

在类 Adventure 中可以看到四个方法,它们把不同的接口和具体类作为参数。当创建一个 Hero 对象时,它可以被传入这些方法中的任意一个,意味着它可以依次向上转型为每个接口。Java 中这种接口的设计方式,使得程序员不需要付出特别的努力。

记住,前面例子展示了使用接口的核心原因之一:为了能够向上转型为多个基类型(以及由此带来的灵活性)。然而,使用接口的第二个原因与使用抽象基类相同:防止客户端程序员创建这个类的对象,确保这仅仅只是一个接口。这带来了一个问题:应该使用接口还是抽象类呢?如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它(这个主题在本章总结会再次讨论)。

使用继承扩展接口

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

// interfaces/HorrorShow.java
// Extending an interface with inheritance
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 HorrorShow {
    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);
    }
}

接口 DangerousMonsterMonster 简单扩展的一个新接口,类 DragonZilla 实现了这个接口。

Vampire 中使用的语法仅适用于接口继承。通常来说,extends 只能用于单一类,但是在构建接口时可以引用多个基类接口。注意到,接口名之间用逗号分隔。

结合接口时的命名冲突

当实现多个接口时可能会存在一个小陷阱。在前面的例子中,CanFightActionCharacter 具有完全相同的 fight() 方法。完全相同的方法没有问题,但是如果它们的签名或返回类型不同会怎么样呢?这里有一个例子:

// interfaces/InterfaceCollision.java
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 1;  // 重载
    }
}

class C3 extends C implements I2 {
    @Override
    public int f(int i) {
        return 1; // 重载
    }
}

class C4 extends C implements I3 {
    // 完全相同,没问题
    @Override
    public int f() {
        return 1;
    }
}

// 方法的返回类型不同
//- class C5 extends C implements I1 {}
//- interface I4 extends I1, I3 {}

覆写、实现和重载令人不快地搅和在一起带来了困难。同时,重载方法仅根据返回类型是区分不了的。当不注释最后两行时,报错信息如下:

error: C5 is not abstract and does not override abstract
method f() in I1
class C5 extends C implements I1 {}
error: types I3 and I1 are incompatible; both define f(),
but with unrelated return types
interfacce I4 extends I1, I3 {}

当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/886322.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java基础知识实际应用(学生信息管理系统、猜拳小游戏、打印日历)

一、Java学生信息管理系统 这个系统包含了添加、修改、删除、查询和显示所有学生信息等功能。您可以在此基础上进行修改和完善,以适应您的需求。 import java.util.Scanner;public class StudentManagementSystem {private static Scanner scanner new Scanner(S…

C++——oo的魅力之多态

文章目录 多态的概念多态的定义和实现多态的构成条件虚函数重写的两个例外协变(基类和派生类虚函数返回值类型不同)析构函数的重写(基类和派生类析构函数名字不同) c11 override 和 final关键字 重载,重写(覆盖), 隐藏(重定义)对比抽象类(纯虚函数)多态的…

Vivado使用入门之二:网表物理约束

目录 一、背景 二、物理约束 2.1 概念 2.2 网表约束 2.2.1 CLOCK_DEDICATED_ROUTE 2.2.2 MARK_DEBUG 2.2.3 DONT_TOUCH 2.2.4 LOCK_PINS 三、位置约束 四、布线约束 4.1 route 4.2 assign routing mode 五、参考 一、背景 在工程设计中为了保证上板后功能正常&…

【BI看板】Docker-compose安装Superset,安装最新版本2.1.0

软件及环境准备 docker, docker-compose docker-compose安装 字节码安装 #wget https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-linux-x86_64 #mv docker-compose-linux-x86_64 docker-compose #chmod x /usr/local/bin/docker-com…

一、计算机网络体系结构

Content 1. 计算机网络的组成2. 计算机网络的功能3. 计算机网络的分类4. 计算机网络的性能指标5. 计算机网络分层结构OSI模型TCP/IP模型互联网五层模型共同点: 6. 计算机网络提供的服务按三种方式分类面向连接服务和无连接服务可靠服务和不可靠服务有连接服务和无连…

5G+AI数字化智能工厂建设解决方案PPT

导读:原文《5GAI数字化智能工厂建设解决方案》(获取来源见文尾),本文精选其中精华及架构部分,逻辑清晰、内容完整,为快速形成售前方案提供参考。数字化智能工厂定义 智能基础架构协同框架 - 端、边、云、网…

Java课题笔记~ SpringMVC拦截器

SpringMVC 中的 Interceptor 拦截器,它的主要作用是拦截指定的用户请求,并进行相应的预处理与后处理。其拦截的时间点在“处理器映射器根据用户提交的请求映射出了所要执行的处理器类,并且也找到了要执行该处理器类的处理器适配器&#xff0c…

2023华为产品测评官-开发者之声 + 华为云ModelArts试用体验心得

2023华为产品测评官-开发者之声 华为云ModelArts试用体验心得 文章目录 2023华为产品测评官-开发者之声 华为云ModelArts试用体验心得一、活动介绍二、华为云ModelArts简介三、AI Gallery简介步骤1:订阅模型步骤2:使用订阅模型部…

Reids 的整合使用

大家好 , 我是苏麟 , 今天带来强大的Redis . REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。 Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选…

冯·诺依曼计算机

一、定义 冯诺依曼机(von Neumann machine),又称冯诺依曼计算机,根据冯诺依曼提出的存储程序概念设计的计算机。主要特征是:指令与数据都以二进制形式储存在存储器里;指令根据其储存的顺序执行。 冯…

SpringBoot常用注解 - @Controller

Controller : Controller是加在类上面的注解,使得类里面的每个方法都返回一个视图页面 实际开发中,有时候只是让后端的结果返回到前端,而不作为新的视图页面,此时需要结合 ResponseBody,让这个方法返回给前端的不是一个…

三星霸主地位“无可撼动“,DRAM内存市场份额创近 9 年新低仍第一

三星电子在DRAM市场的竞争地位一直备受关注。据报告显示,除了市场份额下降外,三星电子在上半年的销售额也出现了下滑。这主要是由于全球消费电子产品需求下滑,导致三星电子的芯片需求减少。 存储芯片业务所在的设备解决方案部门的营收和利润也…

快速提高写作生产力——使用PicGo+Github搭建免费图床,并结合Typora

文章目录 简述PicGo下载PicGo获取Token配置PicGo结合Typora总结 简述PicGo PicGo: 一个用于快速上传图片并获取图片 URL 链接的工具 PicGo 本体支持如下图床: 七牛图床 v1.0腾讯云 COS v4\v5 版本 v1.1 & v1.5.0又拍云 v1.2.0GitHub v1.5.0SM.MS V2 v2.3.0-b…

Python_数据容器详解

Python数据容器 1. 列表基础语法和操作练习题 2. 列表的循环练习题 3. 元组 tuple4. 元组的循环练习题 5. 字符串6. 切片练习总结 7. set 集合8. 字典 dict字典的嵌套总结 字典常用操作练习 9. 对比总结以及通用操作对比总结通用操作 1. 列表基础语法和操作 """…

蓝桥杯嵌入式省一教程:(二)LCD显示

在嵌入式开发中,屏幕显示是一个非常重要的功能。同时,其移植对于初学者来说较为复杂,需要较好地掌握I2C或SPI等通讯协议。然而,在蓝桥杯中,比赛方已经为我们提供了与LCD有关的库,这让我们能够简单方便地使用…

Nginx 下载、安装与运行

下载地址 Nginx官网 - 下载页面 在Windows电脑,下载Windows版本。 下载的就是一个 zip 压缩包。解压后的文件就是可以直接使用的 Nginx 。 版本说明 选择 Mainline 版本就可以了。 解压到合适的位置 运行Nginx 命令行,进入 Nginx 所在的目录。运…

产品经理:能不能把 Vue 的中文输入法 bug 解决了?

前言 有个挺常见的需求相信大家应该都遇到过&#xff0c;就是一个搜索框&#xff0c;边输入边提示&#xff0c;类似于下面这样&#xff1a; 这玩意在前端也挺好实现的&#xff0c;就 v-model 然后 watch 再做个防抖请求接口呗&#xff01;于是我&#xff1a; <template>…

SAP MM学习笔记25- SAP中 基本数量单位,发注单位,发注价格单位

SAP 的 MM Master 中有 3种单位。 1&#xff0c;基本数量单位&#xff08; 基本订单单位&#xff0c; 库存管理的最小单位&#xff09; 2&#xff0c;发注单位&#xff08;订单单位&#xff09; 3&#xff0c;发注价格单位&#xff08;订单价格单位&#xff09; 管理 SAP 库…

【jenkins】jenkins流水线构建打包jar,生成docker镜像,重启docker服务的过程,在jenkins上一键完成,实现提交代码自动构建的功能

【jenkins】jenkins流水线构建打包jar&#xff0c;生成docker镜像&#xff0c;重启docker服务的过程&#xff0c;在jenkins上一键完成&#xff0c;实现提交代码自动构建&#xff0c;服务重启&#xff0c;服务发布的功能。一键实现。非常的舒服。 1. 启动脚本 shell脚本 这是 s…

测试部门来了个00后卷王之王,老油条感叹真干不过,但是...

在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不是。技术很强的同事&#xff0c;可遇不可求&#xff0c;向他学习还来不及呢。 真正让人反感的&#xff0c;是技术平平&…