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

server/2024/9/23 2:28:08/

目录

继承的设计

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

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

如何对继承类进行测试

如何禁止继承

复合的设计

什么是复合

复合的缺点


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

继承的设计

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

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);}@Overridepublic 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 runat 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类,继承自HashSetimport java.util.Collection;
import java.util.HashSet;public class CountHashSet<T> extends HashSet<T>{private int count;public CountHashSet(){super();count = 0;}@Overridepublic boolean add(T o){count++;return super.add(o);}@Overridepublic boolean addAll(Collection<? extends T> sets){count += sets.size();return super.addAll(sets);}public int getCount(){return count;}}//Application.javaimport 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;}@Overridepublic int size() {return s.size();}@Overridepublic boolean isEmpty() {return s.isEmpty();}@Overridepublic boolean contains(Object o) {return s.contains(o);}@Overridepublic Iterator<T> iterator() {return s.iterator();}@Overridepublic Object[] toArray() {return s.toArray();}@Overridepublic <T> T[] toArray(T[] a) {return s.toArray(a);    }@Overridepublic boolean add(T e) {return s.add(e);}@Overridepublic boolean remove(Object o) {return s.remove(o);}@Overridepublic boolean containsAll(Collection<?> c) {return s.containsAll(c);    }@Overridepublic boolean addAll(Collection<? extends T> c) {return s.addAll(c);    }@Overridepublic boolean retainAll(Collection<?> c) {return s.retainAll(c);    }@Overridepublic boolean removeAll(Collection<?> c) {return s.removeAll(c);    }@Overridepublic 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);}@Overridepublic boolean add(T t){addCount++;return super.add(t);}@Overridepublic 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.ppmy.cn/server/93697.html

相关文章

【AWS基础】AWS服务介绍与基本使用

AWS基础&#xff1a;AWS服务介绍与基本使用 目录 引言AWS概述AWS的核心服务 计算服务存储服务数据库服务网络服务管理和监控服务 AWS的基本使用 创建AWS账户使用EC2实例使用S3存储配置RDS数据库设置VPC网络 AWS的优势AWS的应用场景结论 引言 亚马逊网络服务&#xff08;AWS&…

C++基础知识:构造函数的分类和调用,有参构造和无参构造,有参构造和无参构造,三种调用方式:括号法,显示法,隐式转换法,以及相关代码演示和注意事项

1.构造函数的分类及调用: 2.两种分类方式: 按参数分为: 有参构造和无参构造 按类型分为:有参构造和无参构造 3.三种调用方式: 括号法 显示法 隐式转换法 2.调用方法代码演示 1.括号法代码演示&#xff1a; #include<iostream>using namespace std;//1.构造函数的分类和…

Linux系统编程-多路IO套接字

目录 有限状态机 多路IO Select IO 1.select 2.FD_SET 3.FD_ISSET 4.FD_CLR 5.FD_ZERO 6. pselect Poll IO Epoll IO 1.epoll_create 2.epol_create1 3.epoll_ctl 4.epoll_wait 5.epoll_pwait 6.readv 7.writev 内存映射 文件锁 网络套接字 1.socket …

计算word文件打印页数 VBA实现

目录 场景复现环境说明实现原理计算当前文件夹下所有word文件页数总和利用递归计算当前文件夹所有work文件页面数量几个BUG计算结果软件报价后话 场景复现 最近需要帮我弟打印高考资料&#xff0c;搜集完资料去网上打印&#xff0c;商家发出了这个计算页数的界面。我就好奇怎么…

详细分析nohup后台运行命令

目录 1. 基本知识2. Demo 1. 基本知识 Unix/Linux 命令&#xff0c;用于在后台运行程序&#xff0c;并确保它在用户退出或注销后继续运行 nohup 的主要作用是使程序在终端会话结束后继续运行&#xff0c;这对需要长时间执行的任务特别有用 基本的用法如下&#xff1a; nohu…

使用Prometheus监控Java应用性能

使用Prometheus监控Java应用性能 大家好&#xff0c;我是微赚淘客系统3.0的小编&#xff0c;是个冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天&#xff0c;我们将探讨如何使用Prometheus监控Java应用的性能。 一、引入Prometheus客户端库 在Java应用中使用…

Docker安装rocketMq

一、概述 RocketMQ是阿里巴巴开源的一款分布式消息中间件&#xff0c;用于处理大规模消息传输与存储。它使用Java语言编写&#xff0c;是阿里巴巴内部历经双十一等高并发场景考验的成熟产品。2016年开源后&#xff0c;RocketMQ捐赠给Apache&#xff0c;并成为了Apache的一个顶…

<数据集>棉花识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;13765张 标注数量(xml文件个数)&#xff1a;13765 标注数量(txt文件个数)&#xff1a;13765 标注类别数&#xff1a;4 标注类别名称&#xff1a;[Partially opened, Fully opened boll, Defected boll, Flower] 序…