八、复用(1)

news2024/9/25 7:25:33

本章概要

  • 组合语法
  • 继承语法
    • 初始化基类
    • 带参数的构造函数
  • 委托

代码复用是面向对象编程(OOP)最具魅力的原因之一。

对于像 C 语言等面向过程语言来说,“复用”通常指的就是“复制代码”。任何语言都可通过简单复制来达到代码复用的目的,但是这样做的效果并不好。Java 围绕“类”(Class)来解决问题。我们可以直接使用别人构建或调试过的代码,而非创建新类、重新开始。

如何在不污染源代码的前提下使用现存代码是需要技巧的。学习以下两种方式来达到这个目的:

  1. 第一种方式直接了当。在新类中创建现有类的对象。这种方式叫做“组合”(Composition),通过这种方式复用代码的功能,而非其形式。
  2. 第二种方式更为微妙。创建现有类类型的新类。照字面理解:采用现有类形式,又无需在编码时改动其代码,这种方式就叫做“继承”(Inheritance),编译器会做大部分的工作。继承是面向对象编程(OOP)的重要基础之一。更多功能相关将在多态(Polymorphism)章节中介绍。

组合与继承的语法、行为上有许多相似的地方(这其实是有道理的,毕竟都是基于现有类型构建新的类型)。在本章中,你会学到这两种代码复用的方法。

组合语法

在前面的学习中,“组合”(Composition)已经被多次使用。你仅需要把对象的引用(object references)放置在一个新的类里,这就使用了组合。例如,假设你需要一个对象,其中内置了几个 String 对象,两个基本类型(primitives)的属性字段,一个其他类的对象。对于非基本类型对象,将引用直接放置在新类中,对于基本类型属性字段则仅进行声明。

class WaterSource {
    private String s;

    WaterSource() {
        System.out.println("WaterSource()");
        s = "Constructed";
    }

    @Override
    public String toString() {
        return s;
    }
}

public class SprinklerSystem {
    private String valve1, valve2, valve3, valve4;
    private WaterSource source = new WaterSource();
    private int i;
    private float f;

    @Override
    public String toString() {
        return "valve1 = " + valve1 + " " +
                        "valve2 = " + valve2 + " " +
                        "valve3 = " + valve3 + " " +
                        "valve4 = " + valve4 + "\n" +
                        "i = " + i + " " + "f = " + f + " " +
                        "source = " + source; // [1]
    }

    public static void main(String[] args) {
        SprinklerSystem sprinklers = new SprinklerSystem();
        System.out.println(sprinklers);
    }
}
WaterSource()
valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = Constructed

这两个类中定义的一个方法是特殊的: toString()。每个非基本类型对象都有一个 toString() 方法,在编译器需要字符串但它有对象的特殊情况下调用该方法。因此,在 [1] 中,编译器看到你试图“添加”一个 WaterSource 类型的字符串对象 。因为字符串只能拼接另一个字符串,所以它就先会调用 toString()source 转换成一个字符串。然后,它可以拼接这两个字符串并将结果字符串传递给 System.out.println()。要对创建的任何类允许这种行为,只需要编写一个 toString() 方法。在 toString() 上使用 @Override ** 注解来告诉编译器,以确保正确地覆盖。@Override ** 是可选的,但它有助于验证你没有拼写错误 (或者更微妙地说,大小写字母输入错误)。类中的基本类型字段自动初始化为零,正如 object Everywhere 一章中所述。但是对象引用被初始化为 null,如果你尝试调用其任何一个方法,你将得到一个异常(一个运行时错误)。方便的是,打印 null 引用却不会得到异常。

编译器不会为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销。初始化引用有四种方法:

  1. 当对象被定义时。这意味着它们总是在调用构造函数之前初始化。
  2. 在该类的构造函数中。
  3. 在实际使用对象之前。这通常称为_延迟初始化_。在对象创建开销大且不需要每次都创建对象的情况下,它可以减少开销。
  4. 使用实例初始化。

以上四种实例创建的方法例子在这:

class Soap {
    private String s;

    Soap() {
        System.out.println("Soap()");
        s = "Constructed";
    }

    @Override
    public String toString() {
        return s;
    }
}

public class Bath {
    private String // Initializing at point of definition:
            s1 = "Happy",
            s2 = "Happy",
            s3, s4;
    private Soap castille;
    private int i;
    private float toy;

    public Bath() {
        System.out.println("Inside Bath()");
        s3 = "Joy";
        toy = 3.14f;
        castille = new Soap();
    }

    // Instance initialization:
    {
        i = 47;
    }

    @Override
    public String toString() {
        if (s4 == null) // Delayed initialization:
        {
            s4 = "Joy";
        }
        return "s1 = " + s1 + "\n" +
                        "s2 = " + s2 + "\n" +
                        "s3 = " + s3 + "\n" +
                        "s4 = " + s4 + "\n" +
                        "i = " + i + "\n" +
                        "toy = " + toy + "\n" +
                        "castille = " + castille;
    }

    public static void main(String[] args) {
        Bath b = new Bath();
        System.out.println(b);
    }
}

在这里插入图片描述

Bath 构造函数中,有一个代码块在所有初始化发生前就已经执行了。当你不在定义处初始化时,仍然不能保证在向对象引用发送消息之前执行任何初始化——如果你试图对未初始化的引用调用方法,则未初始化的引用将产生运行时异常。

当调用 toString() 时,它将赋值 s4,以便在使用字段的时候所有的属性都已被初始化。

继承语法

继承是所有面向对象语言的一个组成部分。事实证明,在创建类时总是要继承,因为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)。

组合的语法很明显,但是继承使用了一种特殊的语法。当你继承时,你说,“这个新类与那个旧类类似。你可以在类主体的左大括号前的代码中声明这一点,使用关键字 extends 后跟基类的名称。当你这样做时,你将自动获得基类中的所有字段和方法。这里有一个例子:

class Cleanser {
    private String s = "Cleanser";

    public void append(String a) {
        s += a;
    }

    public void dilute() {
        append(" dilute()");
    }

    public void apply() {
        append(" apply()");
    }

    public void scrub() {
        append(" scrub()");
    }

    @Override
    public String toString() {
        return s;
    }

    public static void main(String[] args) {
        Cleanser x = new Cleanser();
        x.dilute();
        x.apply();
        x.scrub();
        System.out.println(x);
    }
}

public class Detergent extends Cleanser {
    // Change a method:
    @Override
    public void scrub() {
        append(" Detergent.scrub()");
        super.scrub(); // Call base-class version
    }

    // Add methods to the interface:
    public void foam() {
        append(" foam()");
    }

    // Test the new class:
    public static void main(String[] args) {
        Detergent x = new Detergent();
        x.dilute();
        x.apply();
        x.scrub();
        x.foam();
        System.out.println(x);
        System.out.println("Testing base class:");
        Cleanser.main(args);
    }
}
Cleanser dilute() apply() scrub()

Cleanser dilute() apply() Detergent.scrub() scrub() foam()
Testing base class:
Cleanser dilute() apply() scrub()

这演示了一些特性。首先,在 Cleanserappend() 方法中,使用 += 操作符将字符串连接到 s,这是 Java 设计人员“重载”来处理字符串的操作符之一 (还有 + )。

第二,CleanserDetergent 都包含一个 main() 方法。你可以为每个类创建一个 main() ; 这允许对每个类进行简单的测试。当你完成测试时,不需要删除 main(); 你可以将其留在以后的测试中。即使程序中有很多类都有 main() 方法,惟一运行的只有在命令行上调用的 main()。这里,当你使用 java Detergent 时候,就调用了 Detergent.main()。但是你也可以使用 java Cleanser 来调用 Cleanser.main(),即使 Cleanser 不是一个公共类。即使类只具有包访问权,也可以访问 public main()

在这里,Detergent.main() 显式地调用 Cleanser.main(),从命令行传递相同的参数(当然,你可以传递任何字符串数组)。

Cleanser 中的所有方法都是公开的。请记住,如果不使用任何访问修饰符,则成员默认为包访问权限,这只允许包内成员访问。因此,如果没有访问修饰符,那么包内的任何人都可以使用这些方法。例如,Detergent 就没有问题。但是,如果其他包中的类继承 Cleanser,则该类只能访问 Cleanser 的公共成员。因此,为了允许继承,一般规则是所有字段为私有,所有方法为公共。(protected成员也允许派生类访问;你以后会知道的。)在特定的情况下,你必须进行调整,但这是一个有用的指南。

Cleanser 的接口中有一组方法: append()dilute()apply()scrub()toString()。因为 Detergent 是从 Cleanser 派生的(通过 extends 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 Detergent 中看到所有这些方法的显式定义。那么,可以把继承看作是复用类。如在 scrub() 中所见,可以使用基类中定义的方法并修改它。在这里,你可以在新类中调用基类的该方法。但是在 scrub() 内部,不能简单地调用 scrub(),因为这会产生递归调用。为了解决这个问题,Java的 super 关键字引用了当前类继承的“超类”(基类)。因此表达式 super.scrub() 调用方法 scrub() 的基类版本。

继承时,你不受限于使用基类的方法。你还可以像向类添加任何方法一样向派生类添加新方法:只需定义它。方法 foam() 就是一个例子。Detergent.main() 中可以看到,对于 Detergent 对象,你可以调用 CleanserDetergent 中可用的所有方法 (如 foam() )。

初始化基类

现在涉及到两个类:基类和派生类。想象派生类生成的结果对象可能会让人感到困惑。从外部看,新类与基类具有相同的接口,可能还有一些额外的方法和字段。但是继承并不只是复制基类的接口。当你创建派生类的对象时,它包含基类的子对象。这个子对象与你自己创建基类的对象是一样的。只是从外部看,基类的子对象被包装在派生类的对象中。

必须正确初始化基类子对象,而且只有一种方法可以保证这一点 : 通过调用基类构造函数在构造函数中执行初始化,该构造函数具有执行基类初始化所需的所有适当信息和特权。Java 自动在派生类构造函数中插入对基类构造函数的调用。下面的例子展示了三个层次的继承:

class Art {
    Art() {
        System.out.println("Art constructor");
    }
}

class Drawing extends Art {
    Drawing() {
        System.out.println("Drawing constructor");
    }
}

public class Cartoon extends Drawing {
    public Cartoon() {
        System.out.println("Cartoon constructor");
    }

    public static void main(String[] args) {
        Cartoon x = new Cartoon();
    }
}
Art constructor
Drawing constructor
Cartoon constructor

构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化。即使不为 Cartoon 创建构造函数,编译器也会为你合成一个无参数构造函数,调用基类构造函数。尝试删除 Cartoon 构造函数来查看这个。

带参数的构造函数

上面的所有例子中构造函数都是无参数的 ; 编译器很容易调用这些构造函数,因为不需要参数。如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 super 关键字和适当的参数列表显式地编写对基类构造函数的调用:

// reuse/Chess.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Inheritance, constructors and arguments

class Game {
  Game(int i) {
    System.out.println("Game constructor");
  }
}

class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    System.out.println("BoardGame constructor");
  }
}

public class Chess extends BoardGame {
  Chess() {
    super(11);
    System.out.println("Chess constructor");
  }
  public static void main(String[] args) {
    Chess x = new Chess();
  }
}
/* Output:
Game constructor
BoardGame constructor
Chess constructor
*/

如果没有在 BoardGame 构造函数中调用基类构造函数,编译器就会报错找不到 Game() 的构造函数。此外,对基类构造函数的调用必须是派生类构造函数中的第一个操作。(如果你写错了,编译器会提醒你。)

委托

Java 不直接支持的第三种重用关系称为委托。这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)。例如,宇宙飞船需要一个控制模块:

// reuse/SpaceShipControls.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.

public class SpaceShipControls {
  void up(int velocity) {}
  void down(int velocity) {}
  void left(int velocity) {}
  void right(int velocity) {}
  void forward(int velocity) {}
  void back(int velocity) {}
  void turboBoost() {}
}

建造宇宙飞船的一种方法是使用继承:

// reuse/DerivedSpaceShip.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.

public class DerivedSpaceShip extends SpaceShipControls {
  private String name;
  public DerivedSpaceShip(String name) {
    this.name = name;
  }
  @Override
  public String toString() { return name; }
  public static void main(String[] args) {
    DerivedSpaceShip protector = new DerivedSpaceShip("NSEA Protector");
    protector.forward(100);
  }
}

然而, DerivedSpaceShip 并不是真正的“一种” SpaceShipControls ,即使你“告诉” DerivedSpaceShip 调用 forward()。更准确地说,一艘宇宙飞船包含了 SpaceShipControls,同时 SpaceShipControls 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:

// reuse/SpaceShipDelegation.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.

public class SpaceShipDelegation {
  private String name;
  private SpaceShipControls controls = new SpaceShipControls();
  public SpaceShipDelegation(String name) {
    this.name = name;
  }
  // Delegated methods:
  public void back(int velocity) {
    controls.back(velocity);
  }
  public void down(int velocity) {
    controls.down(velocity);
  }
  public void forward(int velocity) {
    controls.forward(velocity);
  }
  public void left(int velocity) {
    controls.left(velocity);
  }
  public void right(int velocity) {
    controls.right(velocity);
  }
  public void turboBoost() {
    controls.turboBoost();
  }
  public void up(int velocity) {
    controls.up(velocity);
  }
  public static void main(String[] args) {
    SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
    protector.forward(100);
  }
}

方法被转发到底层 control 对象,因此接口与继承的接口是相同的。但是,你对委托有更多的控制,因为你可以选择只在成员对象中提供方法的子集。

虽然Java语言不支持委托,但是开发工具常常支持。例如,上面的例子是使用 JetBrains Idea IDE 自动生成的。

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

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

相关文章

Hive终端命令行打印很多日志时,如何设置日志级别

示例:use test; 切换到test数据库时,输出很多日志信息不方便看结果,如下图。 解决方法: 退出hive命令行界面(ctrlC)执行“vi /usr/local/apache-hive-3.1.2-bin/conf/log4j.properties”命令,创…

AI lightning学习

真的是没有mmlab的框架好理解,hook调用没问题,就是代码写的不整洁,hook放的到处都是,而且hook的名字和run的名字也不好对应。 又是捧mmengine的一天 😃

《面试1v1》ElasticSearch 集群索引分片

🍅 作者简介:王哥,CSDN2022博客总榜Top100🏆、博客专家💪 🍅 技术交流:定期更新Java硬核干货,不定期送书活动 🍅 王哥多年工作总结:Java学习路线总结&#xf…

React中使用mobx管理状态数据使用样例

MobX 是一个身经百战的库,它通过运用透明的函数式响应编程(Transparent Functional Reactive Programming,TFRP)使状态管理变得简单和可扩展。官网地址:关于 MobX | MobX中文文档 | MobX中文网 安装依赖 mobx-react-…

共享式以太网的退避算法

在使用CSMA/CD协议的共享总线以太网中,正在发送顿的站点一边发送顿一边检测碰撞,当检测到碰撞时就立即停止发送,退避一段随机时间后再重新发送。   截断二进制指数退避算法:退避时间基本退避时间 2 τ 2\tau 2τ随机数r{ 0 , 1 …

Java笔记(三十二):MySQL(下)-- 事务、索引、引擎、视图、MySQL管理

七、事务transaction 1、引入 场景:学工部整个部门解散了,该部门及部门下的员工都需要删除了。 操作: -- 删除学工部 delete from dept where id 1; -- 删除成功-- 删除学工部的员工 delete from emp where dept_id 1; -- 删除失败&…

Java从入门到精通(四)· 面向对象

Java从入门到精通(四) 面向对象 一 静态关键字(static) static表示静态关键字,可以用来修饰成员变量和成员方法。 被 static修饰的成员变量,称之为静态成员变量或者类变量,同理可得&#xff…

前端性能优化介绍与常见方法(二)

这是一个没有套路的前端博主,热衷各种前端向的骚操作,经常想到哪就写到哪,如果有感兴趣的技术和前端效果可以留言~博主看到后会去代替大家踩坑的~ 主页: oliver尹的主页 格言: 跌倒了爬起来就好~ 目录 一、…

条件竞争漏洞Double Fetch

前言 Double Fetch(双取)是一种条件竞争的漏洞,相关的论文发表在USENIX,论文链接:https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-wang.pdf Double Fetch Double Fetch是内核的一种漏洞类型,发生在内…

开发一个RISC-V上的操作系统(六)—— 中断(interrupt)和异常(exception)

目录 往期文章传送门 一、控制流 (Control Flow)和 Trap 二、Exceptions, Traps, and Interrupts Contained Trap Requested Trap Invisible Trap Fatal Trap 异常和中断的异同 三、RISC-V的异常处理 mtvec(Machine Trap-Vector Ba…

【福利】免费白嫖Notability订阅版,无需越狱,支持无限笔记!

转载请注明出处:小锋学长生活大爆炸[xfxuezhang.cn] 视频教程:【福利】免费白嫖Notability订阅版,无需越狱,支持无限笔记!_哔哩哔哩_bilibili 下载爱思助手 连接iPad 搜索并安装12.1.8版本的notability iPad上打开软件…

redis初级redis入门redis数据类型redis常用命令redis持久化机制

Redis 课程内容 Redis入门Redis数据类型Redis常用命令在Java中操作RedisRedis持久化机制 1. Redis入门 1.1 Redis简介 Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件。 **官网:**https://redis.io **中文网&…

SpringBoot多环境切换及JSR303数据校验

多环境切换 profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境; 多配置文件 我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版…

高速公路巡检新手段——道路智能巡检系统

高速公路作为我国公路建设的一项重要成果,其建设和运营对于促进我国经济发展、改善交通运输条件和提高人民生活水平具有重要的意义。 高速公路巡检是确保公路安全的重要措施之一。每年数以万计的车辆在高速公路上穿行,因此高速公路的安全性显得尤为重要。…

【Hbuilder+夜神模拟器】连接过程中遇到的坑

1、安装 HBuilder X 官网戳这里 选择适合你的版本 双击直接运行 夜神模拟器官网戳这里 选择适合你的版本 双击直接运行 2、Hbuilder中配置 打开工具—设置—运行配置 adb路径可以点击右侧的浏览找到bin文件夹下的nox_adb.exe,Android模拟器端口默认是62001&…

释放OMO(线下线上融合)的力量:打造最强私域运营

在零售领域,新技术的出现促进了创新业务战略的发展,其中一种策略是OMO,即线下合并线上,它结合了线上和线下零售的优势,为客户提供无缝的购物体验。由于企业即时消息在在线客户沟通中发挥着重要作用,OMO 随着…

如何在Spring MVC中使用@ControllerAdvice创建全局异常处理器

文章目录 前言一、认识注解:RestControllerAdvice和ExceptionHandler二、使用步骤1、封装统一返回结果类2、自定义异常类封装3、定义全局异常处理类4、测试 总结 前言 全局异常处理器是一种 🌟✨机制,用于处理应用程序中发生的异常&#xff…

使用mybatis-plus的updateById方法更新一个numeric(1)类型字段,sql成功执行,但是updates为0,更新失败的解决办法

使用mybatis-plus的updateById方法更新一个numeric(1)类型字段,sql成功执行,但是updates为0,更新失败的解决办法: 背景:我有一张表,有个启用禁用功能,没有放在编辑页面一起编辑保存,…

使用sysbench对mysql数据库进行基准测试

目录 一、 安装sysbench 二、准备测试表及数据 三、开始测试 1、测试综合TPS 2、只读性能 oltp_read_only 3、删除性能 oltp_delete 4、更新索引字段性能 oltp_update_index 5、更新非索引字段性能 oltp_update_non_index 6、插入性能 oltp_insert 7、写入性能 ol…

SpringBoot Web开发静态资源处理

Web开发探究 简介 其实SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配 使用SpringBoot的步骤: 1、创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好 2、手动…