哈工大2022春软件构造学习记录(二)(ch4、5)

前面几讲主要介绍了软件构造的三个视角八个维度,回答了什么是“高质量的软件”、如何从不同维度刻画软件的问题,介绍了测试优先的编程方法、软件版本控制方法和软件构造的阶段模式和生命周期等等,从第四讲开始就要进入软件构造课程的另一个重要部分:ADT以及OOP了。

Ch4

主要介绍数据类型的基本知识,包括可变和不可变数据类型及它们各自的优缺点,静态和动态类型检查,用Snapshot图理解数据类型等几个方面。 

1.java数据类型

java中的数据类型分为基本数据类型对象数据类型。原始数据类型例如int、long、float、boolean等只有值没有ID,是不可变的数据类型,而对象数据类型既有值又有ID,可以与其他对象进行区分,同时根据具体实现可以为可变或不可变类型。

可以将基本类型包装成对象类型,一般在容器类型中这样使用,平时一般不用、因为会增大性能开销。现在的jdk都能自动进行基本类型和对象类型的转换。

2.类型检查

Java是静态类型语言,变量类型在编译时已知,所以编译阶段就进行类型检查,不符合语法的表达式或类型转换等会被发现;而动态类型的语言例如python一般需要在运行时才能检查数据类型(除了类型外的其他语法错误也会在编译时检查)。

静态检查可以在编译阶段就发现错误,相比运行时才检查错误,提高了程序的正确性和健壮性。但是静态检查只考虑“类型”而不考虑值,因此像除0或者数组越界并不能被检查出来,相反,动态检查会检查由“值”引起的错误。

需要注意的是,机器数是有局限的,基本数据类型毕竟不是真正的数,所以有一些实际上错误的运算并不会被编译器发现,这里有几个例子:

3.可变性和不可变性

“改变一个变量”和“改变一个变量的值”,二者是有区别的。改变一个变量:将该变量指向另一个存储空间;改变一个变量的值:将该变量当前指向的存储空间中写入一个新的值。

在程序中尽量避免“可变性”,因为可变很可能在某个地方就会带来副作用,产生不好的后果。也就是说不变性是一个重要的设计原则,不变性包括这两个方面:1.不变数据类型:创建后值不能改变。2.不变引用类型:确定指向的对象后不能再更改为指向其他对象。这样的不变性可以通过final关键字实现。尽量使用final变量作为方法的输入参数、作为局部变量,它体现了程序员的一种设计决策。

final关键字的一些特性:

      String类是一个典型的不可变数据类型,它对应一个可变类型StringBuilder。我们可以通过它们的快照图看出二者的差别:

在上图所示的情况中,它们似乎结果不会产生差别。但事实上,在一些场景中它们是有所差异的:

这就涉及到它们的具体实现原理了,String不可变,每次更改它的值实际上是把这个String变量的引用指向了另一个位置,在这个位置有创建的新的一个String对象(因此对不可变类型的频繁修改会产生大量的临时拷贝);而StringBuilder可以在它指向的空间内直接进行修改,因此可变类型的性能更好,也适合在多个模块间进行数据的共享。

但即便在性能上如此,我们还是建议尽可能使用“不变性”,因为不可变类型更加安全,而对可变对象的传递和使用很可能在不知道什么位置就发生了不正确的修改而造成程序出错,这就需要程序员进行折中。

两个可变类型被改变引发错误的例子:

1.修改了被传递过来的可变类型的值

2.修改了全局变量

如何避免这样的问题?

我们可以通过进行防御式拷贝,传递一个内容相同但是新的对象,这样即使这个对象被修改了也不会产生什么影响。但是大多数时候该拷贝不会被客户端修改,可能造成大量的内存浪费。
一个一劳永逸的方法就是使用不可变类型替代可变类型,这样省去了复制的代价。

在这样的情况下使用可变类型是安全的:
局部变量,不会涉及共享;只有一个引用。

4.快照图

快照图用来描述程序运行时的内部状态、程序员进行交流、刻画各类变量随时间如何变化、解释设计思路非常方便。

它的基本规则:

基本类型和对象类型的表示:

不可变的对象用双线椭圆,不可变的引用用双线箭头。

注意:有些情况下引用是不可变的,但指向的值却可以是可变的(例如final StringBuilder sb);可变的引用,也可指向不可变的值(例如String)。

5.复杂数据结构的快照图

6.迭代器

迭代器是一个对象,它遍历一组元素并逐个返回元素。for(…:…)形式的遍历,调用的是被遍历对象所实现的迭代器。需要注意在迭代器运行的时候不要对被遍历的对象内容进行修改,会产生ConcurrentModificationException异常。

7.不可变类型的使用

基本类型及其封装类型都是不可变的。

容器的包装:得到不可变的对象。(这种不可变是运行阶段获得的)

Ch5 设计规约

上一节关注编程语言中的“数据类型:、”变量“、”值“,尤其是mutable/immutable的类型/值/引用,这一节转向”方法/函数/操作“如何定义——编程中的”动词“、规约。

1.规约

在代码中其实已经体现了一部分的设计决策,例如方法的参数和返回值类型、final修饰的变量等等,但是这还远远不够。我们需要明确地用注释形式写出“设计决策“,方便自己和其他人阅读,一方面在编写时方便和团队沟通协作,另一方面能和客户端达成一致,规定好双方的责任,也有助于客户端轻松地使用方法。但是需要注意不能在规约中写明这个方法具体是如何实现的,这涉及到后面课程中提到的”表示泄露“问题。

下图给出了规约的一些好处:

规约通常包含这几部分:输入/输出的数据类型,功能和正确性,性能表现。

行为等价性:

不同的方法可能有不同的实现,但是如果它们能做到的功能一样或者说输入方式和操作结果一样,那么对客户端来讲,这两个方法就没什么差别。这实际上与“规约“的特性不谋而合。如果两个方法都符合一个相同的规约,那么它们的行为就是等价的。

2.规约的强度

规约有“强弱“之分,这是通过对规约的前置条件和后置条件的定义判断的。

前置条件:对客户端的约束,在使用方法时必须满足的条件

后置条件:对开发者的约束,方法结束时必须满足的条件

规约在程序编写者和客户之间建立一个契约:如果前置条件满足了,后置条件必须满足。这隐含了一个约定:如果前置条件没有满足,那么这个方法可以做任何事情。

强度更强的规约可以替换强度弱的规约。只有一个规约S2的前置条件更弱或相等(不强于)且后置条件更强或相等(不弱于)于另一个规约S1,才能说强度S2>=S1,这个时候就可以用S2替换S1。总结来说,更强的规约有着更宽松的前置条件和更严格的后置条件。如果只满足对前置和后置两个条件约束的其中一个或都不满足,则这两个规约没有强弱之分。

除了强度以外,规约的确定性(描述的输出是否确定)和规约的陈述性(只是描述了输出,还是描述了如何计算输出)也是评价规约的重要因素。

3.关于规约和编写方法的一些注意事项

静态类型声明是一种规约,可据此进行静态类型检查static checking。方法前的注释也是一种规约,但需人工判定其是否满足。

在编写程序的时候要严格按照规约来,有一些情况要特别注意:1. 非在后置条件里声明过,否则方法内部不应该改变输入参数。应尽量遵循此规则,尽量不设计mutating的spec,否则就容易引发bugs。2.尽量使用不可变类型,除非规约中规定要返回可变类型。程序中可能有很多变量指向同一个可变对象(别名),无法强迫类的实现体和客户端不保存可变变量的“别名”,也就无法保证客户端不会对这些变量进行更改。虽然可以在规约中规定客户不能修改,但我们不能完全靠类实现者或客户的“良心“来编写代码,而应该编程时就采取手段防止这一事件。

4.设计好的规约

一个方法设计的好不好,很大程度上取决于你的spec设计的如何,要让客户和开发者使用都舒服。

1.规约应该是内聚的

Spec描述的功能应单一、简单、易理解,如果一个规约中描述它做了两件事,应当分开形成两个方法。

2.规约应该是信息丰富的

Spec的描述不能让客户端产生理解的歧义。

3.你的规约应该有足够的强度

太弱的规约很难让客户放心的使用,因为你没有给出足够的承诺。开发者应尽可能考虑各种特殊情况,在post-condition给出处理措施。

4.你的规约也不应该过强

太强的规约在很多特殊情况下难以达到,给开发者增加了实现的难度。

5.在规约里使用抽象类型

这样可以给方法的实现体和客户端更大的自由度,而不是限定它们一定使用某个特定的类的实现方案。

6.关于前置条件的强度

惯用的做法是:不限定太强的precondition,而是在postcondition中抛出异常:输入不合法,原则是尽量在错误的根源处fail以避免其大规模扩散难以发现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值