哈尔滨工业大学软件构造课程笔记第三章第四节

3.4 面向对象的编程(OOP)

1. 基本概念:对象、类、属性和方法

对象
现实世界中的对象有两个共同特征:它们都有状态和行为。
识别现实对象的状态和行为是开始考虑面向对象编程的好方法。

对于你看到的每一个对象,问你自己两个问题,这些现实世界的观察都转化为OOP的世界:
状态有哪些?
行为有哪些?

对象是状态和行为的集合
状态——对象中包含的数据。
-在Java中,这些是对象的字段

行为——对象支持的操作
-在Java中,这些被称为方法
-方法只是功能的代名词
-调用一个方法=调用一个函数


每个对象都有一个类
—类定义了方法和字段
—方法和字段统称为成员

哈尔滨工业大学软件构造课程笔记第三章第四节_第1张图片

类定义了类型和实现
-类型≈对象可以使用的地方
-实现≈对象如何做事情

一般来说,类的方法就是它的应用程序
编程接口(API)
-定义用户如何与实例交互

类的静态和实例变量/方法
类成员变量:与类关联的变量,而不是与类的实例关联的变量。您还可以将方法与类关联——类方法
-要引用类变量和方法,请将类的名称和类方法或类变量的名称与句号(’.’)连接起来。

方法和变量不类方法或类的变量被称为实例方实法和实例成员变量。
-要引用实例方法和变量,必须引用类实例中的方法和变量

总结:
类变量和类方法与一个类相关联,每个类发生一次。使用它们不需要创建对象。
实例方法和变量在一个类的每个实例中出现一次。

静态方法不与类的任何特定实例关联,而实例方法(声明时没有Static关键字)必须在特定对象上调用。

哈尔滨工业大学软件构造课程笔记第三章第四节_第2张图片

2. 接口和枚举

接口
Java的接口是设计和表达一个ADT的有用的语言机制,它的实现作为实现该接口的类。

  • Java中的接口是一个方法签名列表,但没有方法主体。
    如果一个类在它的implements子句中声明了接口,并且为接口的所有方法提供了方法体,那么它就实现了接口。
    -一个接口可以扩展一个或多个其他接口
    一个类可以实现多个接口

Java接口和类
接口:确定ADT规约;
类:实现ADT

也可以不需要接口直接使用类作为ADT,既
有ADT定义也有ADT实现

实际中更倾向于使用接口来定义变量

为什么多个实现?
▪不同的性能
-选择最适合您使用的实现
▪不同的行为
-选择做你想做的实现
-行为必须符合接口规范(“合同”)
▪通常的表现和行为都是不同的
-提供一个功能-性能权衡
-例子:HashSet, TreeSet

枚举
有时类型有一个小的、有限的不可变值集

当值集很小且有限时,将所有值定义为命名的常量(称为枚举)是有意义的。Java有enum结构。

4.封装和信息隐藏

信息隐藏
内部数据和实现细节的隐藏程度是模块化设计质量评价的最重要标准

设计良好的代码隐藏了所有的实现细节
API同实现分离
模块间只通过API通讯
他们不知道对方的内部运作

封装和信息隐藏是软件设计的基本原则

信息隐藏的好处
▪对构成系统的类进行解耦
-允许它们独立开发、测试、优化、使用、理解和修改
▪加快系统开发
-类可以并行开发
▪减轻维护负担
类可以被更快地理解和调试,而不用担心会伤害到其他模块
▪有效地进行性能调整
-“热”类可以单独优化
▪增加软件重用
-松耦合类通常在其他上下文中很有用

用接口信息隐藏
使用接口类型声明变量
客户端仅使用接口中定义的方法
客户端代码无法直接访问属性

这只是到此为止
-客户端可以直接访问非接口成员
-本质上,这是自愿的信息隐藏

成员的可见性修饰符
▪private-只能从宣布类
▪protected-可从声明类的子类(和包装内)访问
▪public-可从任何地方访问

哈尔滨工业大学软件构造课程笔记第三章第四节_第3张图片
信息隐藏的最佳实践
▪仔细设计你的API
▪只提供客户要求的功能,所有其他成员应是private的
▪你可以在不破坏客户的情况下public一个private成员,不能进行相反操作,会破坏调用者的使用

5. 继承和覆盖

继承
继承是为了代码重用
只写一次代码
-超类特性(公共的,受保护的)在子类中隐式可用

Class A extends B

哈尔滨工业大学软件构造课程笔记第三章第四节_第4张图片

(1)覆盖

可重写的方法和严格的继承
可重写方法:允许重新实现的方法。
-在Java方法是可重写的默认,即没有特殊的关键字。

严格继承:子类只能添加新方法,无法重写超类中的方法
-如果一个方法在Java程序中不能被覆盖,那么它必须以关键字final作为前缀

final
final域:防止初始化后重新分配到字段
final方法:防止覆盖方法
final类:防止扩展类

覆盖
方法覆盖是一种语言特性,它允许子类或子类提供一个方法的特定实现,该方法已经由它的超类或父类之一提供。
重写的函数:完全同样的signature
实际执行时调用哪个方法,运行时决定
哈尔滨工业大学软件构造课程笔记第三章第四节_第5张图片
父类型中的被重写函数体不为空:意味着对其大多数子类型来说,该方法是可以被直接复用的。

对某些子类型来说,有特殊性,故重写父类型中的函数,实现自己的特殊要求

如果父类型中的某个函数实现体为空,意味着其所有子类型都需要这个功能,但各有差异,没有共性,在每个子类中均需要重写。

当子类包含重写超类方法的方法时,它还可以通过使用关键字super来调用超类方法。

重写的时候,不要改变原方法的本意

重写方法的tips
如果你想覆盖一个方法:
-确保签名匹配
-使用@Override(编译器会检查覆盖方法和被
覆盖的方法签名是否完全一致)——做复制粘贴声明(或者让IDE这样做)
-能见度可以保持不变或增加,但不能减少。

(2)抽象类

抽象方法和抽象类
抽象方法:
-有签名但没有实现的方法(也称为抽象操作)
-由关键字abstract定义

抽象类:
一个包含至少一个抽象方法的类叫做抽象类
-它不能被实例化!
继承某个抽象类的子类在实例化时,所有父类中的抽象方法必须已经实现

接口:只有抽象方法的抽象类
-接口主要用于系统或子系统的规范。实现由子类或其他机制提供。
▪具体类→抽象类→界面

如果某些操作是所有子类型都共有,但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写

所有子类型完全相同的操作,放在父类型中实现,子类型中无需重写。

有些子类型有而其他子类型无的操作,不要在父类型中定义和实现,而应在特定子类型中实现

6. 多态、子类型、重载

(1)多态的三种类型

多态的三种类型
特殊多态:当一个函数表示不同且可能是异构的实现时,依赖于单个指定类型和组合的有限范围。使用功能重载
一个方法可以有多个同名的实现(方法重载)

参数化多态:在编写代码时没有提到任何特定类型,因此可以透明地与任意数量的新类型一起使用。在面向对象的编程社区中,这通常称为泛型或泛型编程
一个类型名字可以代表多个类型(泛型编程)

子类型多态、包含多态:当一个名称表示许多不同的类的实例相关的一些常见的超类。
一个变量名字可以代表多个类的实例(子类型)

(2)特殊多态性和重载

特殊多态性
当一个函数作用于几个不同的类型(它们可能不具有公共结构),并且可能以不相关的方式对每个类型进行操作时,可以获得特殊多态性

重载
重载方法允许您在类中重用相同的方法名,但使用不同的参数(可选地,使用不同的返回类型)。

重载一个方法通常意味着你对那些调用你的方法的人更友好一些,因为你的代码承担了处理不同参数类型的负担,而不是在调用你的方法之前强迫调用者进行转换

函数重载是通过不同的实现来创建多个同名方法的能力。
-对重载函数的调用将运行与调用上下文相适应的函数的特定实现,允许一个函数调用根据上下文执行不同的任务。

重载是一种静态多态
根据参数列表进行最佳匹配
静态类型检查
在编译阶段时决定要具体执行哪个方法 (static type checking)
与之相反,overridden methods则是在run-time进行dynamic checking!

重载的规则
函数重载中的规则:重载的函数必须根据特性或数据类型有所不同
-必须更改参数列表。
-可以改变返回类型。
-可以声明新的或更广泛的检查异常。
-一个方法可以在同一个类或子类中重载。

要调用的方法的重写版本在运行时根据对象类型决定,而要调用的方法的重载版本则基于在编译时传递的参数的引用类型。

重写和重载
不要混淆覆盖派生类中的一个方法(与重载方法的名字吗
——当一个方法被覆盖,新方法定义派生类中给出相同的参数的数量和类型的基类,重写时父类和子类中的方法具有相同的签名
——当派生类中的一个方法有不同的签名从基类的方法,这是重载,签名不同时则为重载
——注意,当派生类重载的原始方法,它仍然继承了原方法从基类子类重载了父类的方法后,子类仍然继承了被重载的方法

重载 重写
变元表 必须改变 不能改变
返回类型 可以改变 不改变或子类型
异常 可以改变 可以减少或消除 必须不抛出新的或更广泛的已检查异常
数据库 可以改变 不能有更多的限制(可以少一些限制)
调用 引用类型决定选择哪个重载版本(基于声明的参数类型)。在编译时发生。实际调用的方法仍然是在运行时发生的虚拟方法调用,但是编译器始终知道要调用的方法的签名。所以在运行时,参数匹配已经被确定,只是不是方法所在的实际类 对象类型(换句话说,堆上实际实例的类型)决定选择哪个方法在运行时发生

(3)参数多态性和泛型编程

参数多态性
参数多态性是指方法针对多种类型时具有同样的行为(这里的多种类型具有通用的结构),此时可使用统一的类型表达多种类型

在运行时根据具体指定类型确定(编译成class文件时,会用指定类型替换类型变量“擦除”)

这是所谓的“泛型”在Java中。

泛型编程是一种编程风格,其中数据类型和函数是根据待指定的类型编写的,随后在需要时根据参数提供的特定类型进行实例化

泛型编程围绕“从具体进行抽象”的思想,将采用不同数据表示的算法进行抽象,得到泛型化的算法,可以得到复用性、通用性更强的软件

类型变量和泛型类
类型变量是无限制的标识符
使用泛型变量的三种形式:泛型类、泛型接口和泛型方法

类中如果声明了一个或多个泛型变量,则为泛型类

这些类型变量称为类的类型参数

它定义一个或多个作为参数的类型变量。
-泛型类声明定义了一组参数化类型,每一个可能的类型参数部分调用一个泛型类声明。
-所有这些参数化类型在运行时共享同一个类

类型变量
使用diamond操作符<>来帮助声明类型变量。

List<Integer> ints = new ArrayList<Integer>();

public interface List<E>

public class Entry<KeyType, ValueType>

java中的泛型接口
如果接口声明一个或多个类型变量,那么它就是泛型的。
-这些类型变量称为接口的类型参数。
-它定义一个或多个作为参数的类型变量。
-泛型接口声明定义了一组类型,其中一个用于类型参数部分的每个可能调用。
-所有参数化类型在运行时共享相同的接口。

interface InterfaceName <T,E,>{
     
public T methodName();
.....
}

另一个例子:Java Set
集合是其它类型E元素的有限集合的ADT。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200504174748101.png
Set是泛型类型的一个示例:该类型的规范是占位符类型,稍后将填充该类型。

而不是编写单独的规范和实现
Set,Set,等等,我们设计并实现了一个Set。

假设我们要实现通用集接口。
方法1:通用接口,非通用实现:实现
为特定类型E为Set。

方法2: 泛型接口,泛型的实现类。
我们还可以实现通用Set接口,而不为E选择类型。
-在这种情况下,我们编写的代码无视客户端为E选择的实际类型。

  • Java的HashSet就是这样做的。

一些Java泛型细节
可以有多个类型参数
-例如,Map, Map

通配符
– List list = new ArrayList();
– List
– List

泛型类型信息被擦除(即仅在编译时)
-不能使用instanceof()检查泛型类型,运行时泛型消失了!

无法创建泛型数组

  • Pair[] foo = new Pair[42];/ /不会编译

(4)子类型多态性

继承和子类型:层次结构的一瞥
Java集合API
哈尔滨工业大学软件构造课程笔记第三章第四节_第6张图片
继承/子类型的好处:代码的重用,建模的灵活性
▪Java:每个类只能直接扩展一个父类;一个类可以实现多个接口。

子类型
▪“B是a的子类型”意味着“每个B都是a”。
▪在规范方面:“每一个B都满足a的规范。”

  • B只是a的一个子类型,如果B的规格至少和a的规格一样强。
    -当我们声明一个实现接口的类时,Java编译器会自动执行这一需求的一部分:它确保a中的每个方法都出现在B中,并具有兼容的类型签名。
  • B类如果不实现A中声明的所有方法,就不能实现接口A。

子类型的静态检查
▪但是编译器不能检查我们是否在其他方面削弱了规范:
-加强对某种方法的某些投入的先决条件
-弱化后置条件
-削弱接口抽象类型对客户端广告的保证。
▪如果你在Java中声明一个子类型(例如,实现一个接口),那么你必须确保子类型的规范至少和超类型的规范一样严格。
▪子类型的规约不能弱化超类型的规约。

子类型多态
子类型多态:不同类型的对象可以统一的处理而无需区分

每个对象根据其类型进行操作(例如,如果您添加新的帐户类型,客户端代码不会更改),从而隔离了“变化”

Liskov代换原理(LSP):
-如果S是T的子类型,那么可以用S类型的对象替换T类型的对象(即,可以用S子类型的任何对象替换T类型的对象),而不改变T的任何理想属性。

instanceof
测试对象是否属于给定类的操作符

建议:尽可能避免使用instanceof(),并且永远不要(?)在超类中使用instanceof()来检查子类的类型。

类型强制
▪有时你想要一个不同的类型比你有
double pi = 3.14;
int indianaPi = (int) pi;
▪有用的,如果你知道你有一个更具体的亚型:
Account acct = …;
CheckingAccount checkingAcct = (CheckingAccount) acct;
long fee = checkingAcct.getFee();
▪但是如果类型不兼容,它会得到一个ClassCastException异常
▪建议:
-避免向下类型
-从不(?)在超类中向下转换为子类

10. Java中一些重要的对象方法

覆盖对象方法
▪equals()-如果两个对象是“相等的”,则为真
▪hashCode() -用于哈希映射的哈希代码
▪toString()—一个可打印的字符串表示

toString()——很难看,而且没有提供足够的信息
-你知道你的目标是什么,所以你可以做得更好
-总是覆盖,除非你知道在不会被调用

▪equals和hashCode-身份语义
-你必须重写如果你想要的值语义
,否则不

11. 设计好类

不可变类的优点
▪简单
▪固有的线程安全的
▪可以免费共享
▪不需要防御副本
▪优秀的建筑模块

如何编写不可变类
▪不要提供任何修改器
▪确保不覆盖任何方法
▪确保所有场地进入决赛
▪所有领域的私人
确保任何可变组件的安全性(避免重复暴露)
▪实现toString()、hashCode()、clone()、equals()等。

何时使类不可变
▪总是,除非有很好的理由不去
▪总是使小的“价值类”不可变!
-例子:Color, PhoneNumber, Unit
-Date和Point是错误的!
-专家们经常用long代替Date

何时使类可变
▪类代表其状态变化的实体
-真实世界-银行账户,红绿灯
-摘要-迭代器,匹配器,集合Iterator, Matcher, Collection
进程类-线程,定时器Thread, Timer
▪如果类必须是可变的,尽量减少易变性
-构造函数应该完全初始化实例
-避免重新初始化方法

12. OOP的历史

模拟和面向对象编程的起源
▪1960年代:Simula 67是第一个面向对象的语言开发
挪威计算机公司的Kristin Nygaard和Ole-Johan Dahl
中心,支持离散事件仿真。(类、对象、继承等)▪“面向对象编程”最初是由
施乐帕洛阿尔托研究中心的Smalltalk语言。
▪1980年代:面向对象编程已经成为一个突出的因素,这是主要的因素
c++。
▪尼克劳斯编写的模块编程和数据抽象,与
奥伯龙和Modula-2;
▪埃菲尔和Java

13. 总结

▪面向对象的准则
▪基本概念:对象、类、属性、方法和接口
▪OOP的独特特点
封装和信息隐藏
继承和覆盖
多态性、子类型和重载
静态和动态调度
▪Java中一些重要的对象方法
▪编写不可变类
▪OOP的历史

你可能感兴趣的:(哈工大,软件构造,学习笔记,多态,设计模式,java,编程语言)