1.2 《算法》之数据抽象

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lwz45698752/article/details/84951050

数据抽象

  • 数据类型即一组值和一组对这些值操作的集合
  • 定义和使用数据类型来抽象一对象,该过程称为数据抽象(函数抽象风格的补充)
  • Java编程基础主要是使用class关键字构造引用数据类型,该编程风格称为面向对象编程(核心概念是对象,即保存了某个数据类型的值的实体)
  • 抽象数据类型是一种能对使用者隐蔽数据表示的数据类型,用Java类实现抽象数据类型类比用静态方法实现函数库。抽象数据类型将数据和函数的实现关联,并将数据的表示方式隐藏起来。
  • 使用抽象数据类型时主要关注API的描述的操作上而不关心数据的表示,而在实现抽象数据类型时,关注数据本身及实现对其操作
  • 本书通过抽象数据类型用API的实现描述算法和数据结构且以适用于多用途的API准确定义问题

使用抽象数据类型

  • 要使用一种数据类型,不需要知道其内部实现细节
  • 抽象数据类型的基础是一组值的集合,API(方法)定义操作而不是说明这些值的意义
  • 使用API说明类的行为,继承的方法在API中显示为灰色(便于discern)
  • 抽象数据类型静态方法库的区别(见书P39,共六点)

继承

  • ToString()默认返回字符串表示的该数据类型值的内存地址,当将任意数据类型值与字符串值连接时调用该方法,一般重载该方法
  • 将程序组织为独立模块的机制可应用于所有Java类

对象

  • 对象是能够承载数据类型的值的实体,对象有三大特性:状态,标识和行为
  • 对象的状态即数据类型中的值,标识用于区分不同对象(认为标识是对象在内存中位置),行为即操作方法
  • 实现一数据类型的唯一职责是维护一个对象的身份,使用该数据类型时只需遵守描述对象行为的API,而不用关注对象状态的表示方法。
  • 引用是访问对象的方式,不同的Java实现中引用的实现细节也不同,但可以认为引用就是内存地址

创建对象

  • 每种数据类型的值存储在对象中
  • 构造函数无返回值,因为其总是返回其对应数据类型对象的引用,调用构造函数创建对象
  • 调用new,则(1)系统为对象分配内存空间(2)初始化对象中值(3)返回对象引用
  • 原始数据类型:变量直接与值相关联,而对于引用数据类型,变量与指向对象的引用关联
  • 引用数据类型用例隐藏了对象值的表示细节(private),所以不能编写依赖于任何特定表示方法的代码
  • 静态方法是实现函数,而实例方法是实现对数据类型的操作(对比,见P41的表1.2.2)

使用引用数据类型对象

  • 从引用角度而非值的角度去考虑问题,包括:
    1. 赋值:使用引用类型的赋值语句,创建该引用的一个副本,不创建新对象,只是创建另一个指向该对象的引用(称为别名),即复制的是引用的值(参照上文的关联解释
    2. 传递引用类型对象:如参数为counter对象,本质上传递的是一个名称和一个计数器,实际只需要指定一个引用类型变量,因为调用一带参方法的动作效果相当于每个实参在赋值语句右侧,而形参在左侧,这样可理解为上述赋值语句的行为,Java将参数值的一个副本从调用端传递给了方法端(称为值传递机制),对于primitive,复制了变量的实际值,方法无法改变调用端的值,而对于引用数据类型,复制了引用的值,可改变传入的对象的值
    3. 返回引用类型对象
      方法可以返回参数对象(作为形参的对象),弥补了返回值只能为一的缺陷
    4. 创建并使用该类型对象的数组
      非primitive的值都是对象,即数组也是引用数据类型,所以传入或返回的数组变量都是传入或返回数组引用的副本
      引用数据类型的数组是对象的引用组成的数组,而不是对象本身组成的

对象小结

  • 运用数据抽象思想(定义和使用数据类型,将数据类型的值封装在对象中)编写代码的方式称为面向对象编程
  • 数据类型(类)指的是一组值和对值的操作的集合,我们会将数据类型实现在独立的类模块中并编写使用示例
  • 对象是能存储任意该数据类型的值的实体,或类的实例
  • 对象有三大特性:状态,标识和行为
  • 调用new,则(1)系统为对象分配内存空间(2)初始化对象中值(3)返回对象引用
  • 原始数据类型:变量直接与值相关联,而对于引用数据类型,变量与指向对象的引用关联
  • 引用数据类型的数组是对象的引用组成的数组,而不是对象本身组成的

抽象数据类型举例

  • 编写Java程序:实现某种抽象数据类型或静态方法库
  • 开发,组织–》轻易使用
  • 通过描述性前缀区分不同抽象数据类型的不同实现,从整体上来说,抽象数据类型说明组织并理解数据结构是编程中的重要成分。
  1. Java lang包中的标准系统抽象数据类型,可被任意Java程序调用
  2. Java标准库中的类,与第1项相比但需要import语句
  3. I/O处理类:处理多个输入输出流
  4. 面向数据类的抽象数据类型:主要作用是通过封装数据的表示来简化数据的组织和处理,如平面点
  5. 集合类抽象数据类型,简化对同一类型一组数据的操作
  6. 面向操作的抽象数据类型,用于分析算法,如计数器
  7. 图算法相关的抽象数据类型,包括用来封装各种图表示的面向数据类的抽象数据类型,和提供图的处理算法的面向操作的抽象数据类型

几何对象

  • 为基本几何对象定义抽象数据类型,如点,矩形等

信息处理

  • 应用的核心是组织和处理信息,抽象数据类型是组织信息的一种自然方式
  • 虽然未给出细节,两份API展示了商业应用程序的一种典型做法
  • 信息处理程序的典型做法:定义和真实世界中物体相对应的对象
  • 好处在于用例不需要知道数据的表示方法(封装数据),不去深究组织信息的方式,只要注意这种做法
  • 用抽象数据类型方式组织数据能将一个对象和其相关数据变成一个整体(如将日期的年月日属性整合),然后维护一个date对象
  • 实现从Object类继承下的某些方法能使我们算法处理任意类型的数据,如在数据结构中包含ToString方法的重写来打印一由对象值组成的一个字符串(习惯用法)
  • 遇到逻辑相关的不同类型的数据,考虑定义抽象数据类型来组织数据(数据抽象),简化代码

字符串

  • 一个string值是一串可用索引访问的char值
  • 可使用字符串字面量来创建和初始化一个字符串
  • 不用字符数组取代字符串:为了代码简洁清晰,无需关心字符串实现方式
  • split("\s+")表示一个或多个制表符,空格,换行符或回车
  • 关注典型的字符串处理代码(待补P50)

再谈输入输出(处理多输入输出流)

  • StdIn等标准库缺点在于只能处理单个输入流,输出流,通过面向对象编程,定义类似机制来实现多输入输出流的处理
  • 本书标准库定义了数据类型In/Out/Draw:带参的表示来源为文件或网站,空参的表示来源为标准输入

抽象数据类型的实现

  • 和静态方法库一样,通过class关键字实现抽象数据类型
  • 定义数据类型的值的实例变量(声明实例变量),定义构造函数和实例方法
  • 单元测试用例main函数用于测试

实例变量

  • 声明实例变量来定义数据类型的值(每个对象的状态)
  • 通过private隐藏抽象数据类型的数据表示(抽象性的体现)

构造函数

  • 每个类至少包含一个构造函数,用于初始化实例变量
  • 默认构造函数为空参类型,且各实例变量默认初始化
  • 使用new关键字-》触发构造函数

实例方法(对象的行为)

  • 类比实现静态方法的代码
  • 签名:指定了方法名,所有参数变量的类型和名称
  • 实例方法的特性基本都和静态方法相同,除了它可以访问并操作实例变量
  • 在实例方法中对实例变量的引用时调用该方法的对象的实例变量
  • 调用实例方法改变实例变量,与调用静态方法仅仅是语法上的区别,但颠覆思维方式

作用域

实例方法中共包含如下三种变量:

  • 参数变量:作用域为整个方法
  • 局部变量:声明和初始化都在方法体内,作用域为定义后的方法体内
  • 实例变量:为该类的对象保存了数据类型的值,作用域为整个类,如有歧义,用this前缀来区分实例变量

API及用例和实现

  • API及用例和实现是实现和使用抽象数据类型所需要理解的基本部分
  • 用例通常独立成为含有main方法的类,并将main方法预留为一个用于开发和最小单元测试的用例
  • 三步走:1.定义API 2.用一个Java类实现API的定义 3.创建测试用例验证

抽象数据类型的实例

日期

  • 定义了两种日期类的实现,都满足API中的定义(API不指定对实现的要求)
  • 两种实现各有优缺点,体现在对时间和空间的利用上

维护多个实现

采用如下命名约定

  • 前缀的描述性修饰符:BasicDate,SmartDate
  • 维护一个无前缀的参考实现

累加器

  • 累加器API定义了一种能计算一组数据实时平均值的抽象数据类型‘
  • 该实现未保存所有数据的值,避免用光内存,所以可应用处理大规模数据(即使在无法全部保存数据的设备上)
  • 可视化的累加器:其实现继承了累加器类,添加了可视化的实例方法
  • 仔细而完整地设计API
  • 不愿改动API(影响用例代码),则可添加一个新的构造函数来取得某些功能(保证原方法还能调用)

数据类型的设计

  • 抽象数据类型是一种向用例隐藏内部表示的数据类型。该思想强有力地影响了现代编程
  • 例子-》研究抽象数据类型的高级特性和Java实现打下了基础

封装性

  • 利用数据类型的实现来封装数据,简化实现和隔离用例开发,封装实现了模块化编程,其允许我们
    1. 支持尚未编写的程序(API起指南作用)
    2. 隔离了对数据类型的值的操作(可在实现中添加一致性检查等调试工具)
  • 大型程序分解为独立开发和调试的小型模块,各模块独立,API作为用例和实现之间唯一的依赖

设计API

  • 按照能复用的方式编写程序
  • 说明书问题(判断实现与API相符与否):一份说明书应该用一种类似于编程语言的形式语言编写,而从数学上可证明,判定这样两个程序进行的计算是否相同是不可能的
  • 为了验证设计,在API附近的正文中给出用例代码
  • 设计API陷阱,如太粗略(无法提供有效的抽象)或太详细(抽象过于细致或发散)或依赖于某种特定的数据表示(用例代码无法从数据表示的细节中分离出来)
  • 总而言之,API只为用例提供它们所需要的

算法与抽象数据类型

  • 数据抽象天生适合算法研究,因为其能够为我们提供一个框架,在框架中能够准确地说明一个算法的目的和用法
  • 算法一般是某个抽象数据类型的一个实例方法的实现
  • 白名单例子很自然地实现为一个抽象数据类型的用例,进行了如下操作:
    1. 由一组给定值构造一个set对象
    2. 判定某值是否存在于集合中
  • 将上述操作封装在抽象数据类型中,StaticSETofInts是更一般也更有用的符号表抽象数据类型的一种特殊情况,二分法是较为适合用于实现符号表抽象数据类型的一种,同BinarySearch相比,StaticSETofInts确保数组在rank()方法调用前被排序
  • 每个Java程序都是一组静态方法和一种数据类型的实现的集合,关注实例方法和隐藏数据表示
  • 利用类继承机制来支持数据抽象

Java继承机制

接口继承

  • Java为定义对象之间的关系提供了支持,称为接口,广泛使用该机制,如比较和迭代
  • 接口机制又称为子类型机制
  • 不使用非正式的API,为Date声明一个接口(在Date实现中引用该接口,编译器会检查该实现是否与接口相符
  • 该方式称为接口继承,因为实现类继承的是接口
  • 可以在更多非正式的API中使用接口继承

实现继承

  • 子类继承被广泛用于编写可扩展的库,来有效重用代码

字符串表示的习惯

  • 当连接符的一个操作数是字符串时,Java自动将另一个操作数也转换为字符串,这个约定是这种自动转换的基础,若该对象的数据类型未实现tostring方法,则调用object类的默认实现(返回一个含有该对象内存地址的字符串),一般为每个类实现并重写tostring方法(重写时只需隐式调用,即通过“+”,调用每个实例变量的tostring方法)

封装类型

  • Java提供了一些内置的引用数据类型,称为封装类型,每个原始数据类型都有一个对应的封装类型
  • 必要时,Java会自动装箱和拆箱,如int值+string值,则int类型自动转换为Integer并调用tostring方法

等价性(对象相等性问题)

考虑两个对象相等与否?

  • ==: 若用相同类型的两个引用变量进行等价性测试,则等等号表示检测标识,即引用是否相同
  • 若想检测数据类型的值(对象的状态)或自定义规则来判断等价性,则要重载equals方法(等价性测试方法)
  • 一些标准数据类型(封装类型及string)和复杂数据类型(file,URL)已重写了equals方法,可直接使用内置的实现
  • Java规定equals必须是一种等价关系,具有自反性,对称性,传递性,一致性,非空型(P64)
  • equals实现步骤:
    1. 若该对象引用同参数对象引用相同,返回true
    2. 参数为空,根据约定,返回false(避免后续代码空引用)
    3. 两对象的类不同,返回false(使用==来判断Class类型的对象是否相等,因为同一种类的所有对象的getclass方法一定能返回相同的引用)
    4. 将参数对象类型从object转换为date(前项测试通过,转换必然成功)
    5. 若任意实例变量的值不同,返回false

内存管理

  • 没有引用指向某对象会某对象离开作用域后成为孤儿对象
  • 必要时分配内存,不必要时释放内存
  • 内存管理
    1. 对于primitive数据类型,内存分配所需要的所有信息在编译阶段就能够获取,声明变量时预留内存空间,离开作用域释放内存空间
    2. 对于引用数据类型,系统在创建对象时则分配内存,但程序执行的动态性决定对象何时成为孤儿对象,并不能准确知道何时释放一个对象内存
    3. 在C++中分配和释放内存由程序员操作,而Java自动内存管理(这种回收内存的方式称为垃圾回收),记录孤儿对象并将它们的内存释放在内存池中,所以Java采用不允许修改引用的策略,使得其能高效自动垃圾回收

不可变性

  • 不可变数据类型,即创建某类型的对象后其实例变量不能改变,如date,string类(按照约定,不使用子类继承的代码中)
  • final强制保证不可变性,变量为final,则其只能被赋值一次(不然产生编译时错误)
  • 不可变性基于应用场景决定,如要封装不变的值,以便将其和primitive数据类型一样用于赋值语句等
  • string是不可变的,而数组是可变的,如将string传递给方法,方法改变不了实参string,而若传入数组,则可改变
  • 我们希望string的值不变,而数组可变,但有时希望使用可变字符串(stringbuilder类)和不可变数组(vector类)
  • 使用不可变类型的代码更简单,因为容易确保在用例中使用它们的变量的状态前后一致,而要时刻关注可变数据类型的值变化情况
  • 不可变意味着要为每个值创建一个新对象,但开销能接受
  • final只能保证primitive数据类型的不可变性,对于引用数据类型则不行(只能让其永远指向同一个对象,但该对象的值本身还是可变的)
  • 设计数据类型要考虑不变性

契约式设计

  • 程序运行时检验程序状态的机制
    1. 异常:控制不可预见的错误
      破坏性事件,如Java系统方法抛出的异常:下标越界等,最简单的异常:

      throw new RuntimeException(“Error occurs”) //中断程序执行,打印错误信息

      提倡 “快速出错”的常规编程practice,即一旦出错则抛出异常,更早定位错误位置

    2. 断言:验证假设
      用于确认为true的布尔表达式,若为false,则终止程序并报错,使用断言确定程序的正确性和记录我们意图,比如判断数组索引

      assert index>=0 : “Negative index”

      默认未启用断言,使用-ea启用断言,程序正常操作时不依赖断言,因为它们可能会被禁用
      契约式编程模型思想:使用断言使得程序不会被错误终止或进入死循环,设计数据类型时说明调用方法的前提条件,说明方法在返回时必须达到的要求(后置条件)和副作用(方法对对象状态产生的影响),这些条件都可以用断言进行测试。

      扫描二维码关注公众号,回复: 4868680 查看本文章

小结

  • 本节所讨论的语言机制说明数据类型在设计中所遇到的问题
  • 设计数据类型是主要目标,使得大部分工作在抽象层次完成,且和实际问题匹配。

答疑

  1. 数据抽象:help us write dependable and accurate code
  2. 原始数据类型比引用数据类型运行更快
  3. 可使用私有实例方法在公有方法中共享代码
  4. 允许直接访问实例变量的好处don’t outweigh 对数据的特定表达方式依赖所带来的坏处
  5. 创建有N个对象的数组要使用N+1次new关键字
  6. printIn(Object):自动调用该对象的tostring方法
  7. 指针和引用的辨析:
    指针类比Java的引用,可看作机器地址,C语言中指针是一种primitive数据类型,可通过各种方法操作它,但指针编程易出错,需要精心设计指针类的操作来避免犯错,Java将此观点发挥到极致,在Java中创建引用的方法只有new,改变引用的方法也只有赋值语句,即程序员对引用进行的操作只有创建和复制,所以Java引用被称为安全指针(Java能保证每个引用都会指向某种类型的对象,且能找出孤儿对象并将其回收)指针类比Java的引用,可看作机器地址,C语言中指针是一种primitive数据类型,可通过各种方法操作它,但指针编程易出错,需要精心设计指针类的操作来避免犯错,Java将此观点发挥到极致,在Java中创建引用的方法只有new,改变引用的方法也只有赋值语句,即程序员对引用进行的操作只有创建和复制,所以Java引用被称为安全指针(Java能保证每个引用都会指向某种类型的对象,且能找出孤儿对象并将其回收)
  8. 哪里找到Java实现引用和进行垃圾回收的细节?
    Java系统的实现各有不同,例如,实现引用的一种自然方式是使用指针(机器地址),另一种则是句柄(指针的指针),前者访问数据的速度更快,后者能更好地实现垃圾回收
  9. 子类继承的问题
    阻碍模块化编程:首先子类完美依赖于父类(fragile的基类问题),其次子类代码可以访问所有实例变量(子类会误改父类实例变量)
  10. 如何使一个类不可变?
    保证含有一个可变类型的实例变量的数据类型的不可变性,需要得到一个本地副本(称为保护性复制),且保证没有任何实例方法能改变数据的值
  11. 引用null表示不指向任何对象对的字面量,
  12. 所有类都含有一个main静态方法。此外,涉及多个对象操作,若它们(多个对象)都不是触发该方法(操作多个对象)的合适对象,则添加一个静态方法,来简化代码
  13. static表示静态变量,也称为全局变量(作用域为全局),不和具体对象关联(类的对象共享)

感言

大致复习了一遍Java基本语法,下章正式学习算法,啦啦啦啦啦啦啦!

猜你喜欢

转载自blog.csdn.net/lwz45698752/article/details/84951050
1.2