哈工大软件构造课程学习笔记
目录
前言
本文主要包含三部分,对应哈工大软件构造课程第8、9、10章。包括ADT和OOP中的“等价性”、面向复用的软件构造技术、面向可维护性的构造技术。
一、ADT和OOP中的“等价性”
在Java中,判断相等有两种形式,分别是== 和 equals():
==:意为引用等价性,即判断比较的二者是否内存地址是否相等。
equals():意为对象等价性,即判断两个对象是否相等。
需要注意,默认情况,也就是 equals()的内部实现与==,等价,也就是说只有两个对象处于相同地址空间时才会返回true。
但是在更多情况下,我们定义的ADT做等价性比较时,只需要两个ADT内容相等即可,该怎么办呢?答案是要进行重写
这里将ADT分为两类,可变与不可变类型来对equals进行讨论:
对于不可变类型,由于不可变类型往往要进行临时拷贝,导致更改前后的对象处于不同的地址,这时使用equals()几乎都会返回false。所以我们要将euqlas()重写:
注意:
1.equals重写的是Objects类的方法,而不是自己类型的,如果重写的参数是本身类型,就属于重载,此时类中会存在两个equals方法,继承自Objects类的equals方法隐式存在,这样会造成错误。
2.在重写euqals()会常用 instanceof关键字和getclass()方法,作用分别是判断某个对象是不是特定类型(或其子类型) 和 返回对象的类型其中instanceof 动态类型检查,不是静态类型检查,需要注意的是,除了重写equals()之外,尽可能避免使用二者。
3.除了equals()方法之外,经常与之绑定的一个方法是hashcode()方法,hashcode()为对象在哈希表中产生一个哈希值,方便索引,哈希表的知识这里不是讲解重点。这就带来了一个问题,如果重写了equals()而没有重写hashcode(),会导致两个等价的对象的hashcode不同,会出现使用一个与对象等价的对象去索引对象的结果是找不到。所以要求我们只要重写equals()就要重写hashcode()方法。至于重写hashcode()的方式有很多,可以将所有对象hashcode都设置为同一个,但会导致性能下降;还是要根据对象的属性来编写。
对于可变类型,有两种判断等价关系的方式:1.观察等价性(在不改变状态的情况下,两个对象看起来一致)2.行为等价性(调用任何对象的方法都展示出一致的结果)
虽然Java中普遍对与可变类型采用观察等价性的方式(除了如StringBulider等),但是这种方式可能会导致bug,例子如下:
List<String> list = new ArrayList<>(); list.add("a");
Set<List<String>> set = new HashSet<List<String>>(); set.add(list);
此时的快照图如图:
但是如果我们修改这个存入的列表:
list.add("goodbye");
会出现这种情况:
set.contains(list) → false!
原因是对象的hashcode变了,但是HashSet没有更新其在bucket的位置,查找时在新hashcode的位置找不到元素,hash table的RI遭到了破坏。
说明如果某个可变的对象包含在Set集合类中,当其放生改变时,集合类的行为会变得不确定。
总结,不可变类型必须重写equals()和hashcode();可变类型不用重写;对象之间比较等价性都要使用equals()(包括如Integer等的包装类型)。
二、面向复用的软件构造技术
1.复用
基于复用编程就是利用已有的可复用软件搭建应用系统,实际上软件构造过程中的任何实体都可能被复用:代码层面上,可以复用方法属性等;模型层面,可以复用类或接口;库层面可以复用各种内库;结构层面上可以复用框架。
可分为, 白盒复用:源代码可见,可修改和扩展;黑盒复用:源代码不可见,不能修改
在系统级复用也就是架构复用时,如果想要拓展框架,也要分两种考虑:
白盒框架,通过代码层面的继承进行框架扩展
黑盒框架,通过实现特定接口/delegation进行框架扩展
2.LSP
LSP即 Liskov 替换原则,包含以下几条:
1. 子类型可以增加方法,但不可删
2.子类型需要实现抽象类型 (接口、抽象类)中所有未实现的方法
3.子类型中重写的方法必须有相同或子类型的返回值或者符合co-variant(协边)的参数
4.子类型中重写的方法必须使用同样类型的参数或者符合contra-variant(反协变)的参数(此种情况Java目前按照重载overload处理)
5.子类型中重写的方法不能抛出额外的异常,抛出的异常也要协变
总结而言,LSP要求子类对象想要替换父类时,子类必须满足1.前置条件不能强化2.后置条件不能弱化3.不变量要保持4.子类型方法参数:逆变5.子类型方法的返回值:协变6.异常类型:协变
注意:Java中数组支持协变, 泛型是类型不变的。
对于泛型
1.泛型信息只存在于编译阶段,在运行时会被”擦除”,无限定被设定为Objects,有限定,用第1个,getClass()只适用于原始类型
2.泛型想要实现协变需要使用通配符,通配符包含无限定通配符?,逻辑上时所有泛型的父类; <? super A> 下限通配符,逻辑上可以匹配所有A类及其父类; <? extends A> 上限通配符,逻辑上可以匹配所有A及A的子类。
3.委托和比较
比较器
Java中有两种比较器接口,分别是Comparator<T>和Comparable<T>,两者都可以在排序中使用,包含了排序算法。
相同点:都要重写各自的方法(compare()和compareTo())
区别:对于Comparator<T>,实现它的是具体的符合要求的新的比较类,使用时要先实例一个比较类,才能使用;对于Comparable<T>,实现它的是要被比较的类,并且使用时直接使用Collections.sort(对象)即可。
委托
委派/委托意为一个对象请求另一个对象的功能,是复用的一种常见形式
如果子类只需要复用父类中的一小部分方法 ,可以不需要使用继承,而是通过委派机制来实现,从而避免继承大量无用的方法
CRP原则:组合优先于继承,注:组合是委派的一种形式。
还需要注意的是,“委托”发生在object层面,而“继承”发生在class层面。
委托有四种分类:
1.Dependency: 临时性的delegation,通过方法的参数或者在方法的局部中使用发生
联系,被委托的类是在类外定义的,类内无这个被委托类的引用属性
2.Association: 永久性的delegation,类内会有一个被委托的类的对象,该对象的引用可以更改为外部对象
3.Composition(组合): 更强的association,但难以变化,类内会有一个被委托的类的对象,该对象的引用不可以改变
4.Aggregation: 更弱的association,可动态变化,类内会有一个被委托的类的对象的引用,但是实例是在外部创建的,并且可以控制引用指向
Dependency (A use one or multiple B)
Association (A has one or multiple B)
Composition/aggregation (A owns one or multiple B)
三、面向可维护性的构造技术
1.面向可维护性的构造技术
评价可维护性的指标,可以使用圈复杂度、代码行数、继承的层次数、类之间的耦合度、单元测试的覆盖度等,简单来讲就是代码的复杂程度。
可维护性的5个指标:
1.可分解性:将问题分解为各个可独立解决的子问题
2.可组合性:可容易的将模块组合起来形成新的系统
3.可理解性:每个子模块都可被系统设计者容易的理解
4.可持续性:规格说明小的变化将只影响一小部分模块,而不会影响整个体系结构
5.出现异常之后的保护 :运行时的不正常将局限于小范围模块内
面向可维护性编程的5个规则:
1.直接映射:模块的结构与现实世界中问题领域的结构保持一致
2.尽可能少的接口:模块应尽可能少的与其他模块通讯
3.尽可能小的接口:如果两个模块通讯,那么它们应交换尽可能少的信息
4.显式接口:当A与B通讯时,应明显的发 生在A与B的接口之间
5.信息隐藏:经常可能发生变化的设计决策应尽可能隐藏在抽象接口后面
2.面向对象编程的准则:SOLID
▪ (SRP) The Single Responsibility Principle 单一责任原则:ADT中不应该有多于1个原因让其发生变化,否则就拆分开
▪ (OCP) The Open-Closed Principle 开放-封闭原则:模块的 行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化,但模块自身的代码是不应被修改的
▪ (LSP) The Liskov Substitution Principle Liskov替换原则:子类型必须能够替换其基类型,派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异
▪ (ISP) The Interface Segregation Principle 接口聚合原则:不能强迫客户端依赖于它们
不需要的接口:只提供必需的接口,不应该使接口太“胖”
▪ (DIP) The Dependency Inversion Principle 依赖转置原则:高层模块不应该依赖于低层
模块,二者都应该依赖于抽象,抽象不应该依赖于实现细节,实现细节应该依赖于抽象
该部分还涉及了正则表达式的相关学习,这里建议参考链接https://www.runoob.com/java/java-regular-expressions.html
总结
本文详细介绍了三个重要部分:ADT和OOP中的“等价性”、面向复用的软件构造技术、面向可维护性的构造技术,对于提高软件构造质量有重要作用。