HIT Software Construction Review Notes (chapter three)

1. 基本数据类型,对象数据类型

  • Java有几种基本数据类型,其中包括:

- int(对于像5-200这样的整数,但是限制于±2^31或大约±20亿的范围)。

- long(对于上下限为±2^63的整数)。

- booleantruefalse)。

- double(双精度浮点数,代表实数的一个子集)。

- char(用于单个字符,如a$)。

  • Java也有对象数据类型,例如:

- String表示一系列字符。

- BigInteger表示一个任意大小的整数。

  • 按照Java的约定,基本类型是小写字母,而对象类型以大写字母开头。

基本数据类型

对象引用类型

int, long, byte, short, char, float, double, boolean

Classes, interfaces, arrays, enums, annotations(注解)

除了值之外没有其他特性

具有除了值之外的特性

不变的

有些是变化的,有些不是

在栈中,只有使用时才存在

在堆中(垃圾收集)

无法实现表达的统一

利用泛型实现表达的统一

非常“便宜

“代价昂贵”

  • 对象类型的层次结构

i.Object类是所有类的父类

- 除了Object之外的所有类都有一个父类,使用extends指定。

如:class Guitar extends Instrument {...}

ii.定义Java类时如果没有显式的指明父类,那么就默认继承了Object类。例如:public class Demo {...}实际上是public class Demo extends Object {...}的简写形式。

iii.一个类是其所有超类的一个实例。

- 从其超类继承可见的字段和方法。

- 可以重写方法来改变它们的行为。

  • 基本数据类型的包装类

i.将基本数据类型封装为对象。

- 例如:Boolean,Integer, Short, Long, Character, Float, Double

ii.典型用例是集合。

iii.除非不得不,否则不要用包装类,效率低。

iiii.编译器会自动进行转换

iiiii.例子:List<Integer>list = new ArrayList<Integer>();

      list.add(1);list.add(50);

      1和50不是对象,但编译器能够通过,后台自动转换,但是会降低效率。

     list.add(Integer.valueOf(1)); list.add(Integer.valueOf(50));

 

2. 静态类型检查、动态类型检查

 

  • 静态检查和动态检查

i.一种语言可以提供的三种自动检查:

- 静态检查:在程序运行之前,错误会自动被发现。

- 动态检查:执行代码时会自动发现错误。

- 不检查:该语言根本不会帮助你找到错误,你必须亲自观察,否则最终就会以错误的答案告终。

ii.显然,静态的捕获一个bug要比动态的捕获它好,动态的捕获它要比根本没有捕获它好。Static checking > Dynamic checking > No checking

  • 类型转换

- int a = 2; // a = 2

- double a = 2; // a = 2.0(隐式)

- int a = 18.7; // ERROR

- int a = (int)18.7; // a = 18

- double a = 2/3; // a = 0.0

- double a = (double)2/3; // a = 0.6666...

  • 静态检查

i.静态检查意味着在编译时检查错误。

ii.静态检查可以避免因使用错误类型的操作而产生的bug

iii.静态检查可以检查出如下的几种错误:

- 语法错误:例如额外的标点符号。即使是像Python这样的动态类型语言也可以进行这种静态检查

- 错误的名字:例如Math.sine(2)。(正确的名字应该是Math.sin(2))

- 错误的参数数量:例如Math.sin(30,20)。

- 错误的参数类型:例如Math.sin(30)。

- 错误的返回类型:例如在要求返回一个int时返回30

  • 动态检查

i.非法的参数值:例如,只有当y实际上为0时,整数表达式x/y才是错误的;否则它是可以工作的。所以在这个表达式中,除零不是一个静态错误,而是一个动态错误。

ii.不可表示的返回值:即特定的返回值无法在类型中表示

iii.超出范围的索引:例如,在一个字符串上使用负值或太大的索引

iiii.在空的对象引用上调用方法

  • 静态检查vs 动态检查

i.静态检查往往是关于类型的,与变量具有的特定值无关的错误。

- 静态类型保证了一个变量会从该集合中获得一些值,但是我们只有运行时才知道它确切的具有哪个值

- 所以如果错误只会由某些值引起,比如被零除或者索引超出范围,那么编译器不会引发关于它的静态错误

ii.相比之下,静态检查往往是由特定值引起的错误

 

3. Mutable/Immutable

 

  • Immutability不变性

i.不变性是一个主要的设计原则

ii.不变数据类型创建完后,值不可修改

- 不变引用,一旦指定引用位置,不可再次指定

iii.要使一个引用是不可变的,用关键字final来声明它

- final int n = 5;

iiii.如果Java编译器不确定你的final变量只会在运行时分配一次,那么它将产生编译错误。即final给予了你对于不变引用的静态检查

iiiii.最好使用final来声明一个方法的参数和尽可能多的局部变量

iiiiii.像变量的类型一样,这些声明是重要的记录,对代码的读者来说很有用,并且由编译器进行静态检查

iiiiiii.注意:

- 一个final类声明意味着它不能被继承

- 一个final变量意味着它始终包含相同的值/引用,但不能更改

- 一个final方法意味着它不能被子类重写

 

  • 可变性和不变性

i.对象是不可变的:一旦创建,它们总是表示相同的值

ii.对象是可变的:它们具有可以改变对象的值的方法

 

  • 不可变类型——String

i.String是不可变类型的一个例子

ii.一个String对象总是表示相同的字符串

iii.由于Strnig是不可变的,一旦创建,String对象就始终具有相同的值

iiii.要将某些内容添加到String的末尾,你必须创建一个新的String对象

  • 可变类型——StringBuilder

i.StringBuilder是可变类型的一个例子

ii.它具有删除部分字符串,插入或替换字符等方法

iii.该类具有更改对象值的方法,而不仅仅是返回新值

  • 可变性和不变性

i.所以呢?在这两种情况下,最后都会出现ssb,指向的是字符串ab

- 当只有一个对象的引用时,可变性与不变性之间的区别并不重要

ii.但是当对象有其他引用时,它们的行为方式会有很大的的差异

- 例如,当另一个变量t指向与s相同的String对象;而另一个变量tb指向与sb相同的StringBuilder时,那么不可变对象与可变对象之间的差异就会变得更加明显。

  • 可变类型的优点

i.使用不可变的字符串,这会产生大量的临时副本

- 字符串的第一个数字(0)实际上在构建最终字符串的过程中被复制n次,第二个数字被复制n-1次,以此类推

- 它实际上花费O(n²)的时间来完成所有的拷贝,尽管我们只连接了n个元素

ii.StringBuilder旨在最大限度的减少这种复制

- 它使用一个简单但聪明的内部数据结构来避免进行任何复制,直到最后,当您用toString()调用请求最终的String

iii.获得良好的性能是我们使用可变对象的一个原因

iiii.另一个是方便共享:通过共享一个常见的可变数据结构,您的程序的两个部分可以很方便的进行通信

  •  可变的风险

i.由于可变类型似乎比不可变类型强大许多,为什么你会选择不可变类型?

- StringBuilder应该能够完成String可以做的所有事情,以及set()和append()等等

ii.答案是,从bugs的角度,不可变类型可以更安全。而且更容易理解,更容易进行更改。

- 可变性使得你的程序正在做什么难以理解,而更难执行合同

 

4. 值的改变、引用的改变

 

  • 改变一个变量或它的值

i.改变变量和改变值有什么区别?

- 当你对一个变量进行分配时,你正在改变变量的箭头指向的地方,你可以将其指向不同的值

- 当你对可变值的内容(例如数组或列表)进行分配时,你将在该值内更改引用

5. 防御式拷贝



6. Snapshot diagram

 

  • 快照图

i.为了理解微妙的问题,我们可以绘制运行时发生的事情的图片

ii.快照图表示运行时程序的内部状态——堆(正在进行的方法和局部变量)以及堆(当前存在的对象)

iii.为什么我们使用快照图?

- 通过图片互相交流

- 为了说明基本类型与对象类型,不可变值与不可变引用,指针别名,栈与堆,抽象与具体表示等概念

- 帮助解释你在一个团队项目中的设计(彼此之间以及和你的技术援助)

iiii.快照图给我们提供了一种可视化更改变量与更改值之间的区别的办法:

- 当您分配对象或字段时,您将更改变量的箭头指向的位置,您可以将其指向不同的值

- 当您分配可变值的内容(例如数组或列表)时,您将在该值内更改引用

 

  • 快照图中的原始值和对象值

i.原始值

- 原始值由裸常量表示,传入的箭头是对来自变量或对象字段的值的引用

ii.对象值

- 对象值是由其类型标记的圆。当我们想要显示更多细节时,我们在其中写入字段名称,箭头指向它们的值。有关更详细的信息,这些字段可以包含它们的声明类型。有些人更愿意写x:int而不是int x,但这两种都可以。


  • 重新分配和不可变值

i.例如,如果我们有一个字符串变量s,我们可以将其从a值重新分配给ab

- String s = "a"; s = s +"b";

ii.字符串是不可变类型的一个例子,一种类型的值一旦被创建就永远不会被改变

iii.不变对象(它们的设计者打算始终表示相同的值)在快照图中用双边框表示,就像我们图中的String对象一样

  •  可变值

i.相比之下,StringBuilder(一个内置的Java类)是一个可变对象,表示一串字符,并且它具有更改对象值的方法:

- StringBuilder sb = newStringBuilder("a"); sb.append("b");

ii.这两个快照图看起来非常不同,这是很好的:可变性与不可变性之间的差异将在使代码免受错误影响方面发挥重要的作用


  • 不可变参考

i.Java也为我们提供了不可变的引用:一次赋值后永不重新赋值的变量。为了使引用不可变,用关键字final来声明它:

- final int n = 5;

ii.如果Java编译器不确定你的final变量只会在运行时分配一次,那么它将产生编译错误。即final给予了你对于不变引用的静态检查

iii.在快照图中,不可变引用(final)y用双箭头表示

iiii.这是一个对象,其id从不改变(它不能被重新分配到不同的数字),但是其age可以改变

iiiii.我们可以拥有一个对不变值的引用(例如:final StringBuilder sb),即使我们指向同一个对象,它的值也会改变

iiiiii.我们也可以拥有一个对不可变值(如String s)的可变引用,其中变量的值可以更改,因为它可以重新指向不同的对象

 

7. Specification、前置/后置条件

 

(1)编程文档化

  • 一个例子:Java API文档

  • 记录假设

(2)Specification and Contract (of a method)

  • Specification

i.Java中Integer类的add()方法


ii.一个对象与其用户之间的协议

- 方法签名(类型规格)

- 功能和正确性预期

- 性能预期

iii.该方法做了什么,而不是如何去做

- 接口(API),而不是实现

3)规范结构(前置条件和后置条件)

  • 规范结构

i.一个方法的规范由几个子句组成:

- 前置条件,由关键字require表示

- 后置条件,由关键字effects表示

- 异常行为,违反先决条件时会发生什么

ii.前置条件是客户(即方法的调用者)的义务。这是一个方法被调用时的状态

iii.后置对象是该方法实施者的义务。如果前置条件适用于调用状态,则方法必须遵守后置条件,通过返回适当的值,抛出指定的异常,修改或不修改对象等等。

iiii.整体结构是一个逻辑蕴含:如果前置条件在方法被调用时成立,那么当方法完成时后置条件必须成立。

iiiii.如果在调用方法时前置条件不成立,则实现不受后置条件的限制。它可以自由的做任何事情,包括不终止,抛出异常,返回任意结果,进行任意修改等。


  • Java中的规范

i.Java的静态类型声明实际上是方法的前置条件和后置条件的一部分,该部分被编译器自动检查和执行

ii.合同的其余部分必须在方法之前的comment中进行描述,并且通常依靠人来检查它.保证它。

iii.参数由@param语句描述,结果由@return@throws语句描述

iiii.将前置条件放放入@param中,将后置条件@return@throws

iiiii.Java API文档是从Java标准库源代码中的Javadoc注释生成的。在Javadoc中记录您的规范允许Eclipse向您(和您的代码的客户端)显示有用的信息,并允许您以与Java API文档相同的格式生成HTML文档。

 

  •  可变方法的规范

i.如果没有明确说明,那么默认输入值是不可变的

ii.惯例:

- 除非另有说明,否则不允许变化

- 没有输入的变化

iii.可变对象使得简单的规约变得非常复杂

iiii.黑盒测试:以独立于实现的方式检查测试的程序是否遵循指定的规范。

8.行为等价性

 

  • 行为等价

i.要确定行为等价性,问题是我们是否可以用一个实现代替另一个实现

ii.为了使一个实现代替另一个实现成为可能,并且知道何时是可接受的,我们需要一个规范来说明客户端依赖什么

iii.注意:规范不应该有关于方法的局部变量或方法类的私有字段

9.规约的强度

 

  • 哪个规约更好?

i.规约的确定性

- 确定的规约:给定一个满足前置条件的输入,其输出是唯一、明确的

- 欠确定性的规约:同一个输入可能有多个输出

ii.规约的陈述性

- 操作式规约(Operational specs),例如:伪代码

- 声明式规约(Declarative specs),没有内部实现的描述,只有“初——终”状态

- 声明式规约更有价值

iii.规约的强度

- 若规约的强度S2>=S1S2前置条件更弱,后置条件更强。就可以用S2来代替S1

10.ADT操作的四种类型

 

  • Creators(构造器):创建某个类型的新对象

- 构造器可以将对象作为参数,但不是正在构建的类型的对象

- 实现为静态方法的构造器通常称为工厂方法

  • Producers(生产器):从该类型的旧的对象创建新对象

- 例如,String的concat()方法是一个生产器:它需要两个字符串并产生一个代表它们串联的新字符串

  • Observers(观察器):获取抽象类型的对象并返回不同类型的对象

- 例如,List的size()方法返回一个int

  • Mutators(变值器):改变对象的属性

- 例如,List的add()方法通过在结尾添加一个元素来改变list


11.表示独立性

 

  • 表示独立性

i.一个好的抽象数据类型应该是表示独立的

- 这意味着抽象类型的使用与其表示(实际的数据结构或用于实现它的数据字段)无关,因此表示的变化对抽象类型本身之外的代码没有影响。

ii.例如,List提供的操作与列表是以链表还是数组表示无关

iii.除非通过前置条件和后置条件充分指定ADT的操作,否则您将无法更改ADT的表示,以便于客户知道要依赖哪些内容,并且可以安全的知道要更改哪些内容

Example: DifferentRepresentations for Strings

  • MyString的一个简单表示

i.现在,让我们看一下MyString的一个简单表示:只是一个字符数组,恰好是字符串的长度,最后没有额外的空间。以下是如何声明内部表示的方法,作为类中的一个实际变量:

- privatechar[] a;

ii.通过选择这种表示形式,这些操作将以直接的方式实施:

iii.由于这种数据类型是不可变的,因此子串操作并不需要将字符复制到新数组中。它可以指向原始的MyString对象的字符数组,并跟踪新的子串对象所代表的startend

iiii.为了实现这个优化,我们可以将这个类的内部表示改为:

iiiii.由于MyString的现有客户端依赖于其公共方法的规范,而不依赖其私有字段,因此我们可以在不检查和更改所有客户端代码的情况下进行更改

 

12.表示泄露

13.不变量、表示不变量RI

14.表示空间、抽象空间、AF

15.以注释的形式撰写AF、RI

 

  • 一个ADT的不变量

i.一个好的抽象数据类型最重要的属性是它保留了自己的不变量

ii.对于程序每种可能的运行时状态,不变量总是程序的一个属性

- 不变性是一个至关重要的不变量:一旦创建,一个不可变对象在整个生命周期中都应该表示相同的值

iii.ADT保留自己的不变量意味着ADT负责确保自己的不变量保持不变

- 它不依赖于客户的良好行为

- 正确性不依赖于其他模块

iiii.不变量由构造器建立

- 由公共方法调用来维护

- 在方法执行期间可能会暂时失效

  • 两个值的空间


i.R:表示值(rep值)的空间,由实际实现实体的值组成。

- 在简单情况下,抽象类型将作为单个对象来实现,但更常见的是需要一个小型的对象网络,所以这个值实际上往往是相当复杂的。

ii.A:抽象值的空间,client看到和使用的值

- ADT的实现者关注表示空间R,用户关注抽象空间A

  • R和A之间的映射

i.每个抽象值被映射到一些rep值(满射)

ii.一些抽象值被映射到多个rep值(未必单射)

iii.并非所有的rep值都被映射(未必双射)

  • 抽象函数

i.一个抽象函数将rep值映射到它们表示的抽象值

- AF : R →A

  • 表示不变量RI

i.将rep值映射到布尔值

- RI : R →boolean

ii.对于一个rep值r,当且仅当r由AF映射时,RI(r)才为真

iii.换句话说,RI告诉我们给定的rep值是否良好

iiii.或者可以将RI看作是一个集合:它是定义AF的rep值的一个子集

  • 记录RI和AF

i.表示不变量和抽象函数都应该记录在代码中,就在rep本身的声明旁边:

ii.比较下面两组:


- 即使对于相同类型的rep值空间和相同的表示不变量RI,我们也可以有不同的抽象函数

  • 设计ADT

i.选择RARI:合法的表示值。如何解释合法的表示值:映射AF——作出具体的解释,每个rep value是如何映射到abstract value的,把这种解释明确的写到注释中。

  • 检查表示不变量

i.你应该调用checkRep()来在创建或改变rep(构造器,生产器,变值器)的每个操作结束时声明表示不变量

ii. NoNull Values in the Rep

  • 记录AFRI以及防止表示泄露

i.记录AFRI。在代码中用注释形式记录AFRI是很好的习惯,另外需要记录的是表示泄露的安全声明,需要给出理由证明代码没有想外泄露其内部表示



16. 接口、抽象类、具体类

 

  • 面向对象的标准

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

ii.泛型:“ready for change” and“design for/with reuse”。可以编写具有代表任意类型的正式泛型参数的类

iii.继承:可以将一个类定义为从另一个继承,以控制潜在的复杂性

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

iiiii.动态分配/绑定:在一个实体上调用一个特性应该总是触发与所连接的运行时对象的类型相对应的特性,这在调用的不同执行过程中不一定是相同的。

 

  • 接口

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

- Java中的接口是方法签名的列表,但没有方法体

- 如果一个类在它的implements子句中声明接口并为所有接口的方法提供方法体,则该类将实现一个接口

- 一个接口可以扩展其它接口

- 一个类可以实现多个接口

  • 接口和实现

i.API的多个实现可以共存

- 多个类可以实现相同的API

- 它们可以在性能和行为上有所不同

ii.Java中,API由接口或类指定

- 接口只提供一个API

- 接口定义但不实现API

- Class实现了一个API和一个实现

- 一个类可以实现多个接口


iii.如此实现的问题:破坏了通过抽象建立的屏障

- 客户必须知道具体表示类的名称

- 因为Java中的接口不能包含构造函数,所以它们必须直接调用其中一个具体类的构造函数

- 该构造函数的规范不会出现在接口的任何地方,所以没有任何静态的保证,即不同的实现甚至会提供相同的构造函数

  • 使用静态工厂而不是构造函数

i.Java8中,接口允许包含静态方法,因此我们可以在接口MyString中将创建者操作valueOf实现为静态工厂方法:



抽象方法和抽象类‘’

i.抽象方法:

- 具有签名但没有实现的方法(也成为抽象操作)

- 由关键字abstract定义

ii.抽象类:

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

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

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

iiii.具体类 → 抽象类 → 接口

 

17.继承、override

 

  • 继承和子类型

i.继承是为了代码重用           Class A extends B

- Writecode once and only once

- 超类的功能在子类中隐式可用

ii.子类型是用于多态的           Class A implements I

- 以相同的方式访问对象,但获得不同的行为

- 子类型可以替代超类型

iii.接口定义了客户的期望/承诺

iiii.一个类满足了接口的期望

- 一个抽象类是一个方便的混合

- 一个子类专用于一个类的实现

  • Overriding (覆盖/重写)

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

- 子类中的实现通过与父类中的方法具有相同名称,相同参数或签名以及相同返回类型的方法来覆盖(替换)父类中的实现

- 执行的方法的版本将由用于调用它的对象决定

- 如果父类中的对象用于调用方法,则会执行父类中的版本,但如果使用子类中的对象调用该方法,则将执行子类中的版本


ii.子类中可以通过super关键字来调用父类中被重写的方法

 

  • 可重写的方法和严格的继承

i.可重写的方法:允许重新实现的方法

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

ii.严格的继承

- 子类只能向超类添加新方法,不能去覆盖它们

- 如果一个方法不能在Java程序中被覆盖,那么必须以关键字final为前缀


18. 多态、overload

 

  • 多态的三种类型:(多态类型,其操作可以用于其他类型的值)

i.特殊多态:一个函数可以有多个同名的实现

ii.参数多态性:一个类型名字可以代表多个类型

iii.子类型多态、包含多态:一个变量名字可以代表多个类的实例

  • 特殊多态

i.特殊多态是在函数适用于多种不同类型(可能不具有通用结构)时获得的,并且可能以不相关的方式表现每种类型


  • Overloading 重载

i.重载的方法允许你在类中重复使用相同的方法名称,但使用不同的参数(以及可选的不同的返回类型)

ii.重载方法通常意味着对于那些调用方法的人来说会好一些,因为代码承担了应对不同参数类型的负担,而不是在调用方法之前强制调用者执行转换

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

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

iiii.重载是一种静态的多态

- 函数调用使用“最佳匹配技术”解决,即函数根据参数列表解析

- 函数调用中有静态类型检查

- 在编译时确定使用这些方法的哪一个

  • 重载规则

i.函数重载规则:重载函数必须依据参数数量或数据类型而有所不同。

- 重载的方法必须改变参数列表(MUST

- 重载的方法可以改变返回类型(CAN

- 重载的方法可以改变访问修饰符(CAN

- 重载的方法可以声明新的或更广泛的检查异常(CAN

- 一个方法可以在同一个类或子类中重载


  • Overriding vs. Overloading

i.不要混淆派生类中重写一个方法与重载一个方法名称

- 当方法被重写时,派生类中给出的新方法定义与基类中的参数数量和类型完全相同

- 当派生类中的方法与基类中的拥有不同的签名时,即重载

- 需要注意的是,当派生类重载原始方法时,它仍然继承基类中的原始方法


19.泛型

 

  • 参数多态性

i.参数多态性是函数在一系列类型上一致工作时获得的,这些类型通常具有一定的结构

- 它能够以通用的方式定义函数和类型,以便于它在运行时传递参数的基础上工作,即允许在没有完全指定类型的情况下进行静态类型检查。

- 这在Java中被称作泛型

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

  • Java中的泛型

i.类型变量是一个无限制的标识符

- 它们由泛型类声明,泛型接口声明,泛型方法声明和泛型构造函数声明引入。(范型的四种使用方式)

ii.如果一个类声明了一个或多个类型变量,则该类是泛型的

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

- 它定义了一个或多个作为参数的类型变量

- 泛型类声明定义了一组参数化类型,用于每个可能的类型参数部分的调用

- 所有这些参数化类型在运行时共享相同的类

iii.如果一个接口声明了一个或多个类型变量,则它是泛型的

- 这些类型变量被称为接口的类型参数

- 它定义了一个或多个作为参数的类型变量

- 一个泛型接口声明定义了一组类型,每种类型参数部分的每个可能的调用都有一个类型

- 所有参数化类型在运行时共享相同的接口

iiii.如果一个方法声明一个或多个类型变量,则该方法是泛型的

- 这些类型变量被称为方法的形式类型参数

- 形式类型参数列表的形式与类或接口的类型参数列表相同

20.等价性equals()和==

21. equals()的自反、传递、对称性

22. hashCode()

23. 可变对象的观察等价性、行为等价性

 

(1)What is and why equality?

  • ADT上的等价操作

i.ADT是通过创建以操作为特征而不是以表示为特征的类型的数据抽象

ii.对于一个抽象数据类型,抽象函数解释了如何将具体表示值解释为抽象类型的值。并且我们看到了抽象函数的选择如何确定如何编写实现每个ADT操作的代码

iii.抽象函数(AF)提供了一种方法来轻松的定义一个ADT上的等价操作

 

(2)三种等价的方式

  • 使用AF或使用一个关系

i.使用一个抽象函数。回想一下抽象函数AF:R -> A将数据类型的具体实例映射到它们相应的抽象值。为了使用AF作为等价性的定义,仅仅当AF(a)=AF(b)时,我们会说a equals b

ii.使用一个关系。等价性是一个关系E T x T:

- 自反性:E(t,t) t∈T

- 对称性:E(t,u) E(u,t)

- 传递性:E(t,u) ∧ E(u,v) E(t,v)

- 要用E作为等价性的定义,我们可以说a equals b当且仅当E(a,b)

 

(3)== vs. equals()

  • == vs. equals()

i.Java有两种不同的操作,用于测试等价性,具有不同的语义

- ==运算符比较引用。它测试引用的等价性。如果它们指向内存中的相同存储,那么这两个引用就是==的。就快照图而言,如果它们的箭头指向相同的对象气泡,则两个引用是==的

- equals()操作比较对象内容。换句话说,对象等价性

ii.必须为每个抽象数据类型适当的定义equals操作

iii.当我们定义一个新的数据类型时,我们有责任决定对象等价对数据类型的值意味着什么,并适当的实现equals()操作

  • ==运算符与equals方法

i.对于原始数据类型,你必须使用==

ii.对于对象引用类型

- ==运算符提供标识语义

- Java中,==只是测试引用标识,不会比较对象内容

- Object.equals完全一样,即使Object.equals已经被覆盖

- 这很少会是你想要的

- 你应该(几乎)总是使用.equals


(可参考http://www.cnblogs.com/dolphin0520/p/3592500.html,作者海子)


5The Object contract

  • Objectequals()的规约

i.当你重写Objectequals方法时,你必须遵守其general contract

- equals必须定义一个等价关系——即一个自反、对称和传递的关系

- equals必须保持一致:对方法的重复调用必须产生相同的结果,前提是未修改对象的equals比较中使用的信息

- 对于一个非空引用xx.equals(null)应该返回false

- 对于equals方法认定相等的两个对象,hashCode必须产生相同的结果

ii.equals方法实现等价关系

- 自反:对于任何非空引用值xx.equals(x)必须返回true

- 对称:对于任何非空引用值xyx.equals(y) must return true if and onlyif y.equals(x) returns true

- 传递:对于任何非空引用值xyzif x.equals(y)returns true and y.equals(z) returns true, then x.equals(z) must return true

- 一致性:对于任何非空引用值xy,如果只修改了在对象的等值比较中没有使用的信息,x.equalsy)的多个调用就始终返回true或始终返回false

- 对于任何非空引用值xx.equalsnull)必须返回false

iii.equals是所有对象的全局等价关系

  • hashCode的规约

i.只要在应用程序执行过程中多次调用同一对象时,hashCode方法必须返回相同的整数,前提是未修改用于对象的equals比较的信息

- 该整数不需要从应用程序的一次执行到同一应用程序的另一次执行保持一致

ii.如果两个对象根据equals(Object)方法相等,则对这两个对象中的每一个调用,hashCode方法都必须产生相同的整数结果

- 如果两个对象根据equals(Object)方法不相等,上述条件就是不需要的了,但分别对这两个对象调用的hashCode方法必须产生不同的整数结果。程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高散列表的性能

  • 重写hashCode()

i.确保满足规约的一个简单但强悍的方法是让hashCode始终返回某个常量值,因此每个对象的hash code都是相同的

- 这满足Object的规约,但它会带来灾难性的性能影响,因为每个key都会存储在同一个slot中,并且每个查找都会退化为沿着长列表的线性搜索

ii.标准是计算用于确定相等性的对象的每个组件的hash code(通常通过调用每个组件的hashCode方法),然后组合这些hash code,引入几个算术运算

Alwaysoverride hashCode() when you override equals().

 

(6)可变类型的等价

  • 可变类型的等价

i.当两个对象无法通过观察区分时,它们是等价的

ii.对于可变对象,有两种解释方法:

- 观察等价性意味着两个引用不能通过不改变任何一个对象的状态的代码来区别,即通过只调用observer,producer,和creator 方法。这测试两个引用在程序的当前状态中是否“看起来相同”

- 行为等价性意味着两个引用不能通过任何代码加以区分,即使在一个代码上调用了mutator,而另外一个代码不会调用它。这将测试这两个引用在现在和所有“将来的状态”中是否会“表现”相同

iii.注意:对于不可变的对象,观察等价性和行为等价性是相同的,因为没有任何增变方法。


参考资料:HIT、MIT软件构造课程;http://www.cnblogs.com/dolphin0520/p/3592500.html,作者海子






















猜你喜欢

转载自blog.csdn.net/qq_36163296/article/details/80733230