1

大纲

面向对象的标准
基本概念:对象,类,属性,方法和接口
OOP的独特功能

封装和信息隐藏
继承和重写
多态性,子类型和重载
静态与动态分派

Java中一些重要的Object方法
设计好的类
面向对象的历史
总结

面向对象的标准

面向对象的编程方法/语言应该具有类的概念作为中心概念。
语言应该能够为类和它的特征提供断言(即规范:前置条件,后置条件和不变量)和异常处理,依靠工具生成这些断言中的文档,并且可选地在运行时监视它们 时间。

  • 他们帮助生产可靠的软件;
  • 他们提供系统文件;
  • 它们是测试和调试面向对象软件的核心工具。

静态类型:一个定义良好的类型系统应该通过强制执行一些类型声明和兼容性规则来保证它接受的系统的运行时类型安全。

泛型(Genericity):用于“准备改变”和“为/重用设计”:应该可以编写具有表示任意类型的正式泛型参数的类。

继承(Inheritance):应该可以将一个类定义为从另一个继承,以控制潜在的复杂性。

多态(Polymorphism):在基于继承的类型系统的控制下,应该可以将实体(表示运行时对象的软件文本中的名称)附加到各种可能类型的运行时对象。

动态分派/绑定(Dynamic dispatch / binding):在一个实体上调用一个特性应该总是触发与所附加的运行时对象的类型相对应的特性,这在调用的不同执行过程中不一定是相同的。

基本概念:对象,类,属性和方法

对象

真实世界的物体有两个特征:它们都有状态和行为。
识别真实世界对象的状态和行为是从OOP角度开始思考的好方法。

  • 狗有状态(名称,颜色,品种,饥饿)和行为(吠叫,取出,摇尾巴)。
  • 自行车具有状态(当前档位,当前踏板节奏,当前速度)和行为(更换档位,改变踏板节奏,应用制动器)。

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

  • 这个物体有什么可能的状态?
  • 这个对象有什么可能的行为?

一个对象是一组状态和行为

状态 - 包含在对象中的数据。

  • 在Java中,这些是对象的字段

行为 - 对象支持的操作

  • 在Java中,这些被称为方法
  • 方法只是面向对象的功能
  • 调用一个方法=调用一个函数

每个对象都有一个类

  • 一个类定义方法和字段
  • 统称为成员的方法和领域

类定义了类型和实现

  • 类型≈可以使用对象的位置
  • 执行≈对象如何做事情

松散地说,类的方法是它的应用程序编程接口(API)

  • 定义用户如何与实例进行交互

静态与实例变量/类的方法

类成员变量:与类相关联的变量,而不是类的实例。 您还可以将方法与类关联 - 类方法。

  • 要引用类变量和方法,需要将类的名称和类方法或类变量的名称连同句点('.')一起加入。

不是类方法或类变量的方法和变量称为实例方法和实例成员变量。

  • 要引用实例方法和变量,必须引用类实例中的方法和变量

总结:

  • 类变量和类方法与类相关联,并且每个类都会出现一次。 使用它们不需要创建对象。
  • 实例方法和变量在类的每个实例中出现一次。

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

接口

Java的接口是一种用于设计和表达ADT的有用语言机制,其实现方式是实现该接口的类。

  • Java中的接口是方法签名的列表,但没有方法体。
  • 如果一个类在其implements子句中声明接口并为所有接口的方法提供方法体,则该类将实现一个接口。
  • 一个接口可以扩展一个或多个其他接口
  • 一个类可以实现多个接口
  • 接口和类:定义和实现ADT
  • 接口之间可以继承
  • 一个类可以实现多个接口

接口和实现

API的多个实现可以共存

  • 多个类可以实现相同的API
  • 他们在性能和行为方面可能有所不同

在Java中,API由接口或类指定

  • 接口只提供一个API
  • 一个接口定义但不实现API
  • 类提供了一个API和一个实现
  • 一个类可以实现多个接口

一个接口可以有多种实现

Java接口和类

接口与类

  • 接口:确定ADT规约
  • 类:实现ADT

类确实定义了类型

  • 类似接口方法的公共类方法
  • 可从其他课程直接访问的公共字段

但更喜欢使用接口

  • 除非你知道一个实现就足够,否则使用变量和参数的接口类型。
  • 支持更改实施;
  • 防止依赖于实施细节

问题:打破抽象边界

  • 客户必须知道具体表示类的名称。
  • 因为Java中的接口不能包含构造函数,所以它们必须直接调用其中一个具体类的构造函数。
  • 构造函数的规范不会出现在接口的任何位置,所以没有任何静态的保证,即使不同的实现甚至会提供相同的构造函数。

接口的优点

接口指定了客户端的契约,仅此而已。

  • 接口是客户程序员需要阅读才能理解ADT的全部内容。
  • 客户端不能在ADT的表示上创建无意的依赖关系,因为实例变量根本无法放入接口。
  • 实现完全保持完全分离,完全不同。 抽象数据类型的多个不同表示可以共存于同一个程序中,作为实现接口的不同类。
  • 当一个抽象数据类型被表示为一个单独的类时,如果没有接口,就很难拥有多个表示。

为什么有多个实现

不同的表现

  • 选择最适合您使用的实现

不同的行为

  • 选择你想要的实现
  • 行为必须符合接口规范(“契约”)

性能和行为往往不尽相同

  • 提供功能
  • 性能权衡
  • 示例:HashSet,TreeSet

接口总结

  • 编译器和人员的文档
  • 允许性能权衡
  • 可选的方法
  • 有意识欠定规格的方法
  • 一个类的多个视图
  • 越来越不值得信赖的实现

减少错误保证安全

  • ADT由它的操作定义,而接口就是这样做的。
  • 当客户端使用接口类型时,静态检查确保它们只使用接口定义的方法。
  • 如果实现类暴露其他方法 - 或更糟糕的是,具有可见的表示 - 客户端不会意外地看到或依赖它们。
  • 当我们有一个数据类型的多个实现时,接口提供方法签名的静态检查。

容易明白

  • 客户和维护人员确切知道在哪里查找ADT的规范。
  • 由于接口不包含实例字段或实例方法的实现,因此更容易将实现的细节保留在规范之外。

准备好改变

  • 我们可以通过添加实现接口的类轻松地添加新类型的实现。
  • 如果我们避免使用静态工厂方法的构造函数,客户端将只能看到该接口。
  • 这意味着我们可以切换客户端正在使用的实现类,而无需更改其代码。

封装和信息隐藏

信息隐藏

将精心设计的模块与不好的模块区分开来的唯一最重要的因素是其隐藏内部数据和其他模块的其他实施细节的程度。
设计良好的代码隐藏了所有的实现细节

  • 将API与实施完全分开
  • 模块只通过API进行通信
  • 对彼此的内在运作无知

被称为信息隐藏或封装,是软件设计的基本原则。

信息隐藏的好处

将构成系统的类分开

  • 允许它们独立开发,测试,优化,使用,理解和修改

加速系统开发

  • 类可以并行开发

减轻了维护的负担

  • 可以更快速地理解类并调试,而不必害怕损害其他模块

启用有效的性能调整

  • “热”类可以单独优化

增加软件重用

  • 松散耦合的类通常在其他情况下证明是有用的

通过接口隐藏信息

使用接口类型声明变量
客户端只能使用接口方法
客户端代码无法访问的字段
但我们到目前为止

  • 客户端可以直接访问非接口成员
  • 实质上,它是自愿的信息隐藏

成员的可见性修饰符

private - 只能从声明类访问
protected - 可以从声明类的子类(以及包内)
public - 从任何地方访问

信息隐藏的最佳实践

仔细设计你的API
只提供客户需要的功能,其他所有成员应该是私人的
您可以随时在不破坏客户的情况下让私人成员公开

  • 但反之亦然!

继承和重写

(1)重写

可重写的方法和严格的继承
可重写方法:允许重新实现的方法。

  • 在Java方法中默认是可重写的,即没有特殊的关键字。

严格的继承

  • 子类只能向超类添加新的方法,它不能覆盖它们
  • 如果某个方法不能在Java程序中被覆盖,则必须以关键字final为前缀。

final

final字段:防止初始化后重新分配给字段
final方法:防止重写该方法
final类:阻止继承类

重写

方法重写是一种语言功能,它允许子类或子类提供已由其超类或父类之一提供的方法的特定实现。

  • 相同的名称,相同的参数或签名,以及相同的返回类型。
  • 执行的方法的版本将由用于调用它的对象决定。实际执行时调用哪个方法,运行时决定。
  • 如果父类的对象用于调用该方法,则会执行父类中的版本;
  • 如果使用子类的对象来调用该方法,则会执行子类中的版本。

当子类包含一个覆盖超类方法的方法时,它也可以使用关键字super调用超类方法。
重写的时候,不要改变原方法的本意

(2)抽象类

抽象方法和抽象类

抽象方法:

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

抽象类:

  • 一个包含至少一个抽象方法的类被称为抽象类

接口:只有抽象方法的抽象类

  • 接口主要用于系统或子系统的规范。 该实现由子类或其他机制提供。

具体类⇒抽象类⇒接口

多态性,子类型和重载

(1)三种多态性

特殊多态(Ad hoc polymorphism):当一个函数表示不同且可能不同种类的实现时,取决于单独指定类型和组合的有限范围。 使用函数重载(function overloading)在许多语言中支持特设多态。
参数化多态(parametric polymorphism):当代码被写入时没有提及任何特定类型,因此可以透明地使用任何数量的新类型。 在面向对象的编程社区中,这通常被称为泛型或泛型编程。
子类型多态(也称为子类型多态或包含多态):当名称表示由一些公共超类相关的许多不同类的实例。

(2)特殊多态和重载

当函数适用于几种不同的类型(可能不会显示公共结构)并且可能以不相关的方式表现每种类型时,可以获得特殊多态。

(3)重载

重载的方法允许您在类中重复使用相同的方法名称,但使用不同的参数(以及可选的不同的返回类型)。
重载方法通常意味着对于那些调用方法的人来说会更好一些,因为代码承担了应对不同参数类型的负担,而不是在调用方法之前强制调用方执行转换。

函数重载可以在不同的实现中创建同名的多个方法。

  • 对重载函数的调用将运行适合于调用上下文的该函数的特定实现,允许一个函数调用根据上下文执行不同的任务。

重载是一种静态多态

  • 函数调用使用“最佳匹配技术”解决,即根据参数列表解析函数。
  • 函数调用中的静态类型检查
  • 在编译时确定使用这些方法中的哪一个。(静态类型检查)
  • 与之相反,重写方法则是在运行时进行动态检查!

重载规则

函数重载中的规则:重载函数必须因参数或数据类型而有所不同

  • 必须改变参数列表。
  • 可以改变返回类型。
  • 可以改变访问修饰符。
  • 可以声明新的或更广泛的检查异常。
  • 一个方法可以在同一个类或子类中重载。

重写与重载

不要混淆覆盖派生类中的方法和重载方法名称

  • 当方法被覆盖时,派生类中给出的新方法定义与基类中的参数数量和类型完全相同
  • 当派生类中的方法与基类中的方法有不同的签名时,即重载
  • 请注意,当派生类重载原始方法时,它仍然继承基类中的原始方法

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

当一个函数在一系列类型上统一工作时获得参数多态性; 这些类型通常具有一些共同的结构。

  • 它能够以通用的方式定义函数和类型,以便它可以在运行时传递参数的基础上工作,即允许在没有完全指定类型的情况下进行静态类型检查。
  • 这是Java中所谓的“泛型(Generic)”。

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

泛型编程围绕从具体,高效的算法中抽象出来以获得可与不同数据表示形式结合的泛型算法来生成各种各样有用软件的想法相关。

Java中的泛型

类型变量是一个不合格的标识符。

  • 它们由泛型类声明,泛型接口声明,泛型方法声明和泛型构造函数声明引入。

如果一个类声明一个或多个类型变量,则该类是通用的。

泛型类:其定义中包含了类型变量

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

如果声明了类型变量,则interface是通用的。

  • 这些类型变量被称为接口的类型参数。
  • 它定义了一个或多个作为参数的类型变量。
  • 通用接口声明定义了一组类型,每种类型参数部分的每个可能的调用都有一个类型。
  • 所有参数化类型在运行时共享相同的接口。

如果声明类型变量,则方法是通用的。

  • 这些类型的变量被称为方法的形式类型参数。
  • 形式类型参数列表的形式与类或接口的类型参数列表相同。

类型变量

使用菱形运算符<>来帮助声明类型变量。

一些Java泛型细节

可以有多个类型参数

  • 例如Map <E,F>,Map <String,Integer>

Wildcards通配符,只在使用泛型的时候出现,不能在定义中出现

  • List <?> list = new ArrayList <String>();
  • List<? extends Animal>
  • List<? super Animal>

通用类型信息被擦除(即仅编译时)

  • 不能使用instanceof()来检查通用类型运行时泛型消失了!

无法创建通用数组

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

(4)子类多态性

子类

一个类型是一组值。

  • Java List类型由接口定义。
  • 如果我们考虑所有可能的List值,它们都不是List对象:我们不能创建接口的实例。

相反,这些值都是ArrayList对象或LinkedList对象,或者是实现List的另一个类的对象。
子类型只是超类型的一个子集

  • ArrayList和LinkedList是List的子类型。

继承/子类型的好处:重用代码,建模灵活性
在Java中:每个类只能直接继承一个父类; 一个类可以实现多个接口。

“B是A的子类型”意思是“每个B都是A.”
在规格方面:“每个B都符合A的规格”。

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

静态检查子类型

但编译器无法检查我们是否以其他方式削弱了规范:

  • 加强对某种方法的某些投入的先决条件
  • 弱化后置条件
  • 弱化接口抽象类型通告客户端的保证。

如果你在Java中声明了一个子类型(例如,实现一个接口),那么你必须确保子类型的规范至少和超类型一样强。
子类型的规约不能弱化超类型的规约。

子类型多态

子类型多态:不同类型的对象可以被客户代码统一处理子类型多态:不同类型的对象可以统一的处理而无需区分
每个对象根据其类型行为(例如,如果添加新类型的帐户,客户端代码不会更改)从而隔离了“变化”

Liskov替换原则(LSP):

  • 如果S是T的子类型,那么类型T的对象可以用类型S的对象替换(即,类型T的对象可以用子类型S的任何对象代替)而不改变T的任何期望属性。

instanceof

测试某个对象是否为给定类的运算符
建议:如果可能,避免使用instanceof(),并且从不在超类中使用instanceof()来检查针对子类的类型。

类型转换

有时你想要一种不同于你已有的类型
如果你知道你有一个更具体的子类型,很有用
但是如果类型不兼容,它会得到一个ClassCastException

建议:

  • 避免向下转换类型
  • 从不在超类内向某个子类下降

动态分派

动态分派是选择在运行时调用多态操作的哪个实现的过程。

  • 面向对象的系统将一个问题建模为一组交互对象,这些对象执行按名称引用的操作。
  • 多态现象是这样一种现象,即有些可互换的对象每个都暴露同一名称的操作,但行为可能不同。

确定在运行时要调用哪种方法,即对重写或多态方法的调用可在运行时解决

作为示例,File对象和Database对象都有一个StoreRecord方法,可用于将人员记录写入存储。 他们的实现不同。

一个程序持有一个对象的引用,该对象可能是一个File对象或一个数据库对象。 它可能是由运行时间设置决定的,在这个阶段,程序可能不知道或关心哪一个。
当程序在对象上调用StoreRecord时,需要确定哪些行为被执行。
该程序将StoreRecord消息发送给未知类型的对象,并将其留给运行时支持系统以将消息分派给正确的对象。 该对象实现它实现的任何行为。

动态分派与静态分派形成对比,其中在编译时选择多态操作的实现。

动态分派的目的是为了支持在编译时无法确定多态操作的适当实现的情况,因为它依赖于操作的一个或多个实际参数的运行时类型。

静态分派:编译阶段即可确定要执行哪个具体操作。
重载的方法使用静态分派,而重写的方法在运行时使用动态分派。

动态分派不同于动态绑定(也称为动态绑定)。

  • 选择操作时,绑定会将名称与操作关联。
  • 在决定了名称引用的操作后,分派会为操作选择一个实现。
  • 绑定:将调用的名字与实际的方法名字联系起来(可能很多个);
    分派:具体执行哪个方法(提前绑定→静态分派)
  • 通过动态分派,该名称可以在编译时绑定到多态操作,但不会在运行时间之前选择实现。动态分派:编译阶段可能绑定到多态操作,运行阶段决定具体执行哪个(覆盖和过载均是如此);
  • 虽然动态分派并不意味着动态绑定,但动态绑定确实意味着动态分派,因为绑定决定了可用分派的集合。

提前/静态绑定

每当发生静态,私有和最终方法的绑定时,类的类型由编译器在编译时确定,并且绑定在那里和那里发生。

推迟/动态绑定

重写父类和子类都有相同的方法,在这种情况下,对象的类型决定了要执行哪种方法。 对象的类型在运行时确定。

动态方法分派

1.(编译时)确定要查找哪个类
2.(编译时)确定要执行的方法签名

  • 查找所有可访问的适用方法
  • 选择最具体的匹配方法

3.(运行时)确定接收器的动态类别
4.(运行时)从动态类中,找到要调用的方法

  • 在第2步中找到具有相同签名的方法
  • 否则在超类中搜索等。

10 Java中的一些重要的Object方法

equals() - 如果两个对象“相等”,则为true
hashCode() - 用于哈希映射的哈希代码
toString() - 可打印的字符串表示形式

toString() - 丑陋而无信息

  • 你知道你的目标是什么,所以你可以做得更好
  • 总是覆盖,除非你知道不会被调用

equals&hashCode - 身份语义

  • 如果你想要价值语义,你必须重写
  • 否则不要

设计好的类

不可变类的优点

简单
本质上是线程安全的
可以自由分享
不需要防御式拷贝
优秀的积木

如何编写一个不可变的类

不要提供任何变值器
确保没有方法可能被覆盖
使所有的领域最终
使所有字段保密
确保任何可变组件的安全性(避免重复曝光)
实现toString(),hashCode(),clone(),equals()等。

何时让类不可变
总是,除非有充分的理由不这样做
总是让小型“价值类”永恒不变!

何时让类可变

类表示状态改变的实体

  • 真实世界 - 银行账户,红绿灯
  • 抽象 - 迭代器,匹配器,集合
  • 进程类 - 线程,计时器

如果类必须是可变的,则最小化可变性

  • 构造函数应该完全初始化实例
  • 避免重新初始化方法

OOP的历史

仿真和面向对象编程的起源

20世纪60年代:Simula 67是第一个由Kristin Nygaardand Ole-Johan Dahl在挪威计算中心开发的面向对象语言,用于支持离散事件模拟。 (类,对象,继承等)
“面向对象编程(OOP)”这个术语最早是由施乐PARC在他们的Smalltalk语言中使用的。
20世纪80年代:面向对象已经变得非常突出,而其中的主要因素是C ++。
NiklausWirth用于Oberon和Modula-2的模块化编程和数据抽象;
埃菲尔和Java

总结

面向对象的标准
基本概念:对象,类,属性,方法和接口
OOP的独特功能

封装和信息隐藏
继承和重写
多态性,子类型和重载
静态和动态调度

Java中一些重要的Object方法
编写一个不可变的类
OOP的历史


啥也博士
13 声望2 粉丝