Effective Java 学习笔记--第18、19条继承与复合

news2024/11/13 9:06:48

目录

继承的设计

对用于继承的类可覆盖方法的说明

被继承类还需要遵循的约束

如何对继承类进行测试

如何禁止继承

复合的设计

什么是复合

复合的缺点


这两条的关系较强,核心都是继承,但是更强调继承的脆弱性,而且给出了继承的一个更优替代--复合。

继承的设计

继承是实现代码重用的有效手段,但是最为关键的是要关注类中可覆盖方法的自用,如果存在这种自用情况,那就要提供详细的文档来向调用者说明,要么就完全消除这种情况。那什么是可覆盖方法的自用,它又会给继承带来什么问题呢?我们可以通过一个例子来说明。

worker类有run、getup和gotowork三个可覆盖方法,其中run和getup可以作为gotowork的自用类,disabled类继承了worker的特性,但是由于无法跑步便通过覆盖禁用了run方法,这就导致所继承的gotowork方法发生了异常:

//worker类
public class worker {
    private String name;
    public worker(String name){
        this.name = name;
    }

    public void gotoWork(){
        getup();
        run();
        System.out.println(this.name+"is working now");
    }

    public void run(){
        System.out.println(this.name+"is running");
    }

    public void getup(){
        System.out.println(this.name+"has woken up");
    }

}


//disabled类
public class disabled extends worker {
    public disabled(String name){
        super(name);
    }

    @Override
    public void run(){
        throw new RuntimeException("I cannot run");
    }
}


//客户端程序
public class Application {
    public static void main(String[] args) {
        disabled tom = new disabled("Tom");
        tom.gotoWork();
    }
}

结果在调用gotowork的时候发生了异常:

(base) MacBook-Pro:test5 $ java test5/Application
Tom has woken up
Exception in thread "main" java.lang.RuntimeException: I cannot run
        at test5.disabled.run(disabled.java:11)
        at test5.worker.gotoWork(worker.java:11)
        at test5.Application.main(Application.java:7)

这里从调用者的视角来看,由于没有文档说明gotowork的实现细节,所以默认是直接继承worker的实现,但实际上gotowork自用了worker的run方法,子类对其的禁用会直接导致所继承gotowork方法的异常。所以,作者强调除非可以保证可覆盖方法完全不自用,否则一定要对于有自用场景的可覆盖方法做出说明,那么需要如何说明才算具体呢?

对用于继承的类可覆盖方法的说明

用于继承的类必须要对于可覆盖方法的自用性进行详细说明,必须要指明:

  • 该方法调用了哪些可覆盖的方法
  • 调用的顺序和过程是什么样的
  • 每一个调用的结果又是如何影响后续处理过程的

下面截一个Oracle关于Java.util.AbstractCollection类中remove方法的规范解释:

其中的implementation一段就指出了其依赖于iterator的remove方法来实现,如果其没有被实现将会抛出异常。

被继承类还需要遵循的约束

构造器不能调用可被覆盖的方法:一旦子类覆盖了有关方法可能会导致构造函数运行失败,

类构造器(比如Cloneable接口的clone方法和Serializable接口的readObject方法)也不能调用可被覆盖的方法:这些问题与第一条类似,但是clone方法调用失败会损害被克隆对象本身,所以会有更严重的问题。

如何对继承类进行测试

测试继承类的最佳方式就是通过子类(一般编写3个即可),如果遗漏了关键的受保护成员,尝试编写子类就会使得遗漏所带来的痛苦变得更加明显。同时,如果编写多个子类并没有使用到受保护的成员,或许就应该把它作为私有的。总之,在子类构建的实践中可以对于每一个方法的共享范围有一个深刻的印象。

如何禁止继承

继承有这样大的风险,所以对于不是专门为继承设计的类就尽量设置为禁止继承,主要有两种方法:

  • 类设计为final
  • 所有的构造器私有,通过静态工厂方法来构造类实例。

复合的设计

从前文中已经可以发现继承虽然是一种实现代码重用的有效手段,但是有着很明显的缺陷。

破坏封装:要使用继承就必须对于类中可覆盖方法做详细的了解,这在一定程度上破坏了类的封装

父类与子类的强耦合性:部分子类方法强依赖父类,如果父类方法发生变化则会破坏整个子类

这里通过HashSet类的例子来说,HashSet类是Java集合框架中的一个实现类它实现了Set接口,其中addAll方法的实现调用了它的另一个方法add,这里就体现了add方法的自用性。当我们需要有一个新类继承HashSet同时要加入对于新增元素的计数功能时就会出现问题:

//CountHashSet类,继承自HashSet

import java.util.Collection;
import java.util.HashSet;

public class CountHashSet<T> extends HashSet<T>{
    private int count;
    public CountHashSet(){
        super();
        count = 0;
    }
    
    @Override
    public boolean add(T o){
        count++;
        return super.add(o);
    }

    @Override
    public boolean addAll(Collection<? extends T> sets){
        count += sets.size();
        return super.addAll(sets);
    }

    public int getCount(){
        return count;
    }
    
}


//Application.java

import java.util.ArrayList;
import test6.CountHashSet;
public class Application {
    public static void main(String[] args) {
        CountHashSet sets = new CountHashSet();
        ArrayList list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);

        sets.addAll(list);
        System.out.println(sets.getCount());
    }
}

这里客户端预期计数为3,但是实际输出为6,因为自用的add方法会进行重复计数。

所以有自用性方法的类就要仔细去分析它的实现规则,来判断是否符合继承的要求,这就破坏了封装,同时子类方法的实现强依赖与父类,这也体现了强耦合性。

什么是复合

复合本身也是一种实现代码重用的方式,是将需要引入的类(引入类)以类实例的方式作为另一个类(目标类)的私有域,实现将引入类的特性添加到目标类上。这其中有一种特殊的复合方式称为转发,即目标类的方法直接包装引入类的对应方法,并且返回引入类方法原本的返回值(就是单纯做一个包装),而把引入类所有方法都进行包装的类成为引入类的转发类

举个例子,上面的CountHashSet类如果以复合的方法来实现,可以简单的把Set接口包装成一个转发类:

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

public class ForwardingSet<T> implements Set<T> {
    private final Set<T> s;

    public ForwardingSet(Set<T> s){
        this.s = s;
    }

    @Override
    public int size() {
        return s.size();
    }

    @Override
    public boolean isEmpty() {
        return s.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return s.contains(o);
    }

    @Override
    public Iterator<T> iterator() {
       return s.iterator();
    }

    @Override
    public Object[] toArray() {
       return s.toArray();
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return s.toArray(a);    
    }

    @Override
    public boolean add(T e) {
        return s.add(e);
    }

    @Override
    public boolean remove(Object o) {
        return s.remove(o);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return s.containsAll(c);    
    }

    @Override
    public boolean addAll(Collection<? extends T> c) {
        return s.addAll(c);    
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        return s.retainAll(c);    
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        return s.removeAll(c);    
    }

    @Override
    public void clear() {
        s.clear();    
    }

    
}

可以看到Set接口的所有方法都被ForwardingSet的方法实现了转发(单纯包装)。如果再想实现计数等扩展功能就可以基于转发类来实现:

import java.util.Collection;
import java.util.Set;
import InstruSet.ForwardingSet;

public class CustomInstrumentedSet<T> extends ForwardingSet<T> {
    private int addCount=0;

    public CustomInstrumentedSet(Set<T> s){
        super(s);
    }

    @Override
    public boolean add(T t){
        addCount++;
        return super.add(t);
    }

    @Override
    public boolean addAll(Collection<? extends T> set){
        addCount += set.size();
        return super.addAll(set);
    }

    public int showAddCount(){
        return addCount;
    }
}

这样实现首先将Set接口与CustomInstrumentedSet实现类解耦开了,无论在客户端中被包装的是哪一种Set接口的实现类(比如HashSet),都与CustomInstrumentedSet无关。同时也加强了被包装类的封装性,因为CustomInstrumentedSet的设计者不再需要去关注每种可覆盖方法的实现逻辑了。

复合的缺点

复合唯一的缺点是涉及到回调机制的时候,当引入类方法有返回其自身的功能实现时,就只能返回引入类实例自身,而无法返回包装类,因为引入类并不知道它包装类是谁,这就是复合的SELF问题。

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

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

相关文章

【云原生】Helm来管理Kubernetes集群的详细使用方法与综合应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

香港电讯亮相2024算网融合产业发展大会,荣获“SD-WAN优秀产品奖”

秉承“开放、创新、融合、共赢”的发展战略&#xff0c;中国通信标准化协会算网融合产业及标准推进委员会&#xff08;CCSATC621&#xff09;联合中国信息通信研究院&#xff0c;于2024年7月10日共同召开“2024年算网融合产业发展大会”。本次大会发布了多项算网融合领域最新研…

SpringBoot 日志:从基础到高级的全面指南

&#x1f4da; SpringBoot 日志&#xff1a;从基础到高级的全面指南 &#x1f50d; &#x1f4da; SpringBoot 日志&#xff1a;从基础到高级的全面指南 &#x1f50d;摘要引言正文内容一、日志概述 &#x1f4dc;二、日志使用 &#x1f4dd;2.1 打印日志 &#x1f4e3;2.2 日志…

主文件表遗失:数据恢复策略与实战指南

深入解析&#xff1a;无法恢复主文件表的困境 在数字化时代&#xff0c;数据不仅是信息的载体&#xff0c;更是企业运营和个人生活的核心。然而&#xff0c;当遭遇“无法恢复主文件表”的困境时&#xff0c;整个数据系统仿佛被按下了暂停键&#xff0c;让人措手不及。主文件表…

数据集成是什么意思?方法有哪些?数据集成三种方法介绍

1 数据集成是什么 数据集成(Data Intergration)&#xff0c;也称为数据整合&#xff0c;是通过将分布式环境中的异构数据集成起来&#xff0c;为用户提供统一透明的数据访问方式。该定义中的集成是指从整体层面上维护数据的一致性&#xff0c;并提高对数据的利用和共享&#x…

智能语音转Markdown的神器

嘿&#xff0c;技术大咖们&#xff0c;今天我要给你们安利一个超酷炫的智能语音转Markdown笔记系统&#xff0c;它融合了前沿的语音识别技术和强大的AI大模型&#xff0c;绝对是记录和整理信息的神器&#xff01; 打造了一个语音转Markdown的神器 智能语音生成Markdown笔记 这…

芋道源码yudao-cloud 二开日记(商品sku数据归类为规格属性)

商品的每一条规格和属性在数据库里都是单一的一条数据&#xff0c;从数据库里查出来后&#xff0c;该怎么归类为对应的规格和属性值&#xff1f;如下图&#xff1a; 在商城模块&#xff0c;商品的单规格、多规格、单属性、多属性功能可以说是非常完整&#xff0c;如下图&#x…

Java新手启航:JDK 21 版本安装,开启编程之行

在Java开发前&#xff0c;JDK是必不可少的环境&#xff0c;接下来&#xff0c;让我们一起完成JDK 21版本的下载和安装&#xff01; 种一棵树最好的时间是10年前&#xff0c;其次就是现在&#xff0c;加油&#xff01; …

【Redis 进阶】事务

Redis 的事务和 MySQL 的事务概念上是类似的&#xff0c;都是把一系列操作绑定成一组&#xff0c;让这一组能够批量执行。 一、Redis 的事务和 MySQL 事务的区别 1、MySQL 事务 原子性&#xff1a;把多个操作打包成一个整体。&#xff08;要么全都做&#xff0c;要么都不做&am…

实时渲染云交互助力汽车虚拟仿真新体验!

汽车虚拟仿真是指利用软件和数学模型&#xff0c;模拟汽车的设计、制造、测试和运行等过程&#xff0c;以及汽车与环境、驾驶员、乘客等的交互。汽车虚拟仿真可以帮助汽车工程师快速验证方案&#xff0c;优化性能&#xff0c;降低成本&#xff0c;提高安全性和可靠性。 ​ 汽车…

S32G3系列芯片Serial Boot功能详解!

《S32G3系列芯片——Boot详解》系列——S32G3系列芯片Serial Boot功能详解&#xff01;★★★ 一、Serial Boot模式概述二、串行下载协议2.1 基于UART和CAN的下载协议概述2.2 基于FlexCAN的Serial Boot2.2.1 IO配置2.2.2 时钟配置2.2.3 通信波特率2.2.4 基于FlexCAN的Serial Bo…

精心准备的高水平的博客【点评语】,来抄啊!

大家好&#xff0c;我是一名_全栈_测试开发工程师&#xff0c;已经开源一套【自动化测试框架】和【测试管理平台】&#xff0c;欢迎大家关注我&#xff0c;和我一起【分享测试知识&#xff0c;交流测试技术&#xff0c;趣聊行业热点】。 第 1 条 这篇博客文章如同灯塔般照亮了技…

ElementPlus 覆盖默认样式的探索

文章目录 问题解决:global 解释改进一下在研究一下 问题 解决 使用 :global(.el-header) :global(.el-header) {padding: 0; } :global(.el-menu--horizontal) {justify-content: center; }:global 解释 在Vue中&#xff0c;:global() 是一个特殊的 CSS 选择器&#xff0c;用…

在Windows中使用VS Code连接远程服务器

①首先生成自己的密钥 ssh-keygen ②打开VS Code的扩展&#xff0c;安装连接工具 Remote-SSH Remote - SSH: Editing Configuration Files ③点击左侧远程资源管理器&#xff0c;之后点击SSH右侧齿轮&#xff0c;选择一个配置文件 注意&#xff1a;此部分的Host名字要与生成…

【Python机器学习系列】一文教你实现决策树模型可视化(案例+源码)

这是我的第335篇原创文章。 一、引言 决策树是一个有监督分类模型&#xff0c;本质是选择一个最大信息增益的特征值进行输的分割&#xff0c;直到达到结束条件或叶子节点纯度达到阈值。根据分割指标和分割方法&#xff0c;可分为&#xff1a;ID3、C4.5、CART算法。每一种颜色代…

GitLab安装方式

一、什么是GitLab GitLab是一个利用Ruby on Rails开发的开源应用程序&#xff0c;实现一个自托管的Git项目仓库&#xff0c;可通过Web界面进行访问公开或者私人项目。它拥有与Github类似的功能&#xff0c;能够浏览源代码&#xff0c;管理缺陷和注释&#xff0c;可以管理团队对…

动态代理对象在 IronPython 中的实现

动态代理对象是一种设计模式&#xff0c;允许在运行时动态地创建对象&#xff0c;并在这些对象上拦截和处理方法调用。它常用于 AOP&#xff08;面向方面编程&#xff09;、日志记录、权限控制等场景。应用非常广泛&#xff0c;下面跟着我来聊一聊我遇到的问题。 1、问题背景 …

通过ProSave对西门子触摸屏进行OS更新的具体操作方法(恢复出厂设置)

通过ProSave对西门子触摸屏进行OS更新的具体操作方法(恢复出厂设置) 首先,打开电脑的控制面板,将右上角的查看方式修改为“大图标”,如下图所示,找到“设置PG/PC接口”, 如下图所示,在弹出的窗口中上方的应用程序访问点的下拉菜单中选择 “S7ONLINE(STEP7)”,并在下…

【深度学习实战(49)】目标检测损失函数:IoU、GIoU、DIoU、CIoU、EIoU、alpha IoU、SIoU、WIoU原理及Pytorch实现

前言 损失函数是用来评价模型的预测值和真实值一致程度&#xff0c;损失函数越小&#xff0c;通常模型的性能越好。不同的模型用的损失函数一般也不一样。损失函数主要是用在模型的训练阶段&#xff0c;如果我们想让预测值无限接近于真实值&#xff0c;就需要将损失值降到最低…

kernel-devel导致的linux网卡驱动安装异常

引言 安装包下载&#xff1a;iso镜像文件解压后进入package路径&#xff0c;可以找到所有想要的rpm安装包 1.检查gcc gcc -v&#xff1a;检查gcc编译程序是否安装&#xff0c;如果已经成功安装直接执行步骤3 2.安装gcc & gcc-c gcc程序准备&#xff0c;拷贝到centos后进入…