《软件构造》第3章复习摘要

总览:

        

第一部分(讲义3-1)

        Java基本数据类型,例如:

                int:范围[-2^31, 2^31]

                long:范围[-2^63, 2^63]

                boolean

                double

                char:单个字符

        Java对象数据类型,例如:

                String、BigInteger……

        习惯上,基本数据类型都是小写字母,而对象数据类型以大写字母开头。

        基本数据类型与对象数据类型的比较:

        

        可以将基本类型包装为对象类型:如Boolean、Integer、Short、Long、Character、Float、Double。通常是在定义集合类型的时候使用它们。一般情况下,尽量避免使用。一般可以自动转换。

        静态/动态类型检查:

                静态类型检查:可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序的正确性/健壮性。主要检查语法错误、类名/函数名错误、参数数目错误、参数类型错误、返回值类型错误,是关于“类型”的检查。

                动态类型检查:主要检查非法的参数值、非法的返回值、越界、空指针,是关于“值”的检查。

        Mutable/Immutable:

                首先理解“改变一个变量”和“改变一个变量的值”(“引用的改变”和“值的改变”)的区别

                改变一个变量:将该变量指向另一个值的存储空间;

                改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值。

                

                Immutable数据类型:一旦被创建,其值不能改变。

                Immutable引用类型:一旦确定其指向的对象,不能再被改变。如果要使一个引用是Immutable,可以用final关键字来声明它。

                如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分。尽量使用final变量作为方法的输入参数,作为局部变量。final表明了程序员的一种“设计决策”。注意:final类无法派生子类,final变量无法改变值/引用,final方法无法被子类重写。

                Immutable对象:一旦被创建,始终指向同一个值/引用,如String类型;

                Mutable对象:拥有方法可以修改自己的值/引用,如StringBuilder类型。

                Mutable类型的优点:使用Immutable类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收),Mutable类型最少化拷贝以提高效率,获得更好的性能,Mutable类型也适合于在多个模块之间共享数据。

                而Immutable类型更“安全”,在其它质量指标上表现更好。

                因此,使用Mutable类型还是Immutable类型,要在性能和安全上进行折中,看你看重哪个质量指标。

        防御式拷贝:防止客户端修改全局Mutable类型变量。

                通过防御式拷贝,给客户端返回一个全新的对象。大部分时候该拷贝不会被客户端修改,可能造成大量的内存浪费。如果使用Immutable类型,则节省了频繁复制的代价。

                安全的使用Mutable类型:局部变量,不会涉及共享;只有一个引用。

                如果有多个引用(别名),使用Mutable类型就非常不安全。

        Snapshot diagram:用于描述程序运行时的内部状态。便于程序员之间的交流;便于刻画各类变量随时间变化;便于解释设计思路。

                Immutable对象:用双线椭圆;

                Mutable对象:用单线椭圆;

                Immutable引用:用双线剪头;

                Mutable引用:用单线剪头;

                (Immutable引用指向的值可以是可变的,Mutable引用指向的值可以是不可变的)。

        一些有用的Immutable类型:

                基本类型及其封装对象类型都是不可变的;

                不可修改的集合类unmodifiableXXX是不可变的(但这种“不可变”是在运行阶段获得的,编译阶段无法据此进行静态检查)。

        总之,尽可能地使用Immutable对象和Immutable引用!


第二部分(讲义3-2)

        行为等价性

                站在客户端视角看行为等价性,若两个函数对用户来说是等价的,则可相互替换。

                根据规约判断是否行为等价,若两个函数均符合某个规约,则它们等价。

                注意:行为等价性与函数的具体实现无关!

        Specification的结构:

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

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

                如果前置条件满足了,后置条件必须满足;如果前置条件不满足,则方法可做任何事情。

                通常把前置条件放在@param中,把后置条件放在@return和@throws中,在规约的开头写明函数的功能。


                欠定的规约:同一个输入可以有多个输出;

                非确定的规约:同一个输入,多次执行得到的输出可能不同。

                为避免混乱,欠定的规约 == 非确定的规约。

                欠定的规约通常有确定的实现。

                


                操作式规约:例如伪代码;

                声明式规约:没有内部实现的描述,只有“初-终”状态。

                声明式规约更有价值。

                通常情况下,内部实现的细节不在规约里呈现,放在代码实现体内部注释里呈现。

        规约的强度

                若规约S2的前置条件比S1更弱,且S2的后置条件比S1更强,则规约的强度S2>=S1,此时就可以用S2替代S1。

                (spec变强:更放松的前置条件 + 更严格的后置条件)

                几个易混淆的例子:

                

                

                

                越强的规约,意味着implementor的自由度和责任越重,而client的责任越轻。

        Diagramming specifications:

                某个具体实现,若满足规约,则落在其范围内,否则,在其之外。

                程序员可以在规约的范围内自由选择实现方式,客户端无需了解具体使用了哪个实现。

                更强的规约,表达为更小的区域。

                

        在规约中是否使用前置条件?

                


第三部分(讲义3-3)

        (可变类型的对象:提供了可改变其内部数据的值的操作;

        不变数据类型:其操作不改变内部值,而是构造新的对象。)

        ADT操作的四种类型Creators(构造器)、Producers(生产器)、Observers(观察器)、Mutators(变值器,改变对象属性的方法,不可变数据类型没有这个操作)。注意区分,见下图:

                

        表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示(rep)的变化不应影响外部spec和客户端。

        除非ADT的操作指明了具体的pre-和post-condition,否则不能改变ADT的内部表示——spec规定了client和implementor之间的契约。

        ADT总结:

                

        Invariant(不变量):在任何时候总是true。由ADT来负责其不变量,与client端的任何行为无关。用来保持程序的“正确性”,容易发现错误。

        避免表示泄露Safety from Rep Exposure:防御式拷贝、使用immutable类型(包括unmodifiableXXX)、private权限修饰。

        

        保持不变量和避免表示泄露,是ADT最重要的一个Invarient!

        表示空间:内部表示(rep)构成的空间。

        抽象空间:抽象值构成的空间,client看到和使用的值。

        ADT实现者关注表示空间R,用户关注抽象空间A。R到A的映射是满射,但未必是单射,也未必是双射。

        AF(Abstraction Function,抽象函数):表征R到A的映射关系的函数。

        RI(Rep Invariant,表示不变量):某个具体的“表示”是否是“合法的”。可以将RI看作所有表示值的一个子集,包含了所有合法的表示值。也可以将RI看作一个条件,描述了什么是“合法”的表示值。

        自行设计checkRep()方法(private权限)来在运行时检查RI。

        不同的内部表示,需要设计不同的AF和RI。

        选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值。

        即使是同样的R、同样的RI,也可能有不同的AF,即“解释不同”。

        对immutable的ADT来说,它在A空间的abstract value应是不变的,但其内部表示的R空间中的取值则可以是变化的。

        设计ADT:(1)选择R和A;(2)RI——合法的表示值;(3)如何解释合法的表示值——映射AF,即每个合法的rep value如何映射到abstract value。

        以注释的形式撰写AF、RI、Safety from Rep Exposure

                ADT的规约里只能使用client可见的内容来写,包括参数、返回值、异常等。如果规约里需要提及“值”,只能使用A空间中的“值”。ADT的规约里也不应谈及任何内部表示的细节,以及R空间中的任何值。ADT的内部表示(私有属性)对外部都应严格不可见。

                因此,在代码中应以注释的形式(只能由开发者看)写出AF、RI、Safety from Rep Exposure,而不能写在Javadoc文档中,防止被外部看到而破坏表示独立性/信息隐藏。

                


第四部分(讲义3-4)

        接口抽象类具体类继承多态方法的重写与重载泛型,这些都是Java编程语言的基本知识了,这边不再过分强调,只说说几个容易混的点。

        多态有三种形式:

                

        可以在同一个类中重载,也可在子类中重载。

        重载与重写的区别:

                

                对象向上转型后,利用转型后的对象调用方法时,若子类和父类都有该同名方法,欲确定调用的是父类中的方法还是子类中的方法:首先明确是方法的重载还是重写;由于重写是动态绑定(采用动态类型检查)的,所以调用的是子类的方法;而重载是静态绑定(采用静态类型检查)的,所以调用的是父类的方法。但这个规则对static修饰的方法不适用,见如下例子:

                

                即:对于static或final或private修饰的方法,不管是重写还是重载,都是静态绑定的。

        不能用instanceof()来检查泛型的具体类型,也不能创建包含泛型的对象的数组。

        泛型接口可以有泛型的实现类,也可以有非泛型的实现类。

                

                

        泛型中通配符的使用具体见5-2。

        接口确定ADT规约,类实现ADT。


第五部分(讲义3-5)

        ==:引用等价性

        equals():对象等价性

        对基本数据类型,使用==判定相等;对对象类型,使用equals()判定相等,若用==,则是在判定两个对象的身份标识ID是否相等(指向内存里的同一段空间)。

        在Object中实现的缺省equals()是在判断引用等价性。这通常不是程序员所期望的,因此需要重写。

        equals()必须确定一个等价关系,即自反、传递、对称。除非对象被修改了,否则调用多次equals()应是同样的结果。对于任意的非空引用值x,x.equals(null)一定返回false。

        重写equals()时,也必须要重写hashCode(),并且两者应相对应        

        两个equal的对象,其hashCode()的结果必须一致,但hashCode()值一样的两个对象不一定equal,即,不相等的对象,也可以映射为同样的hashCode,但性能会变差。

        hashCode()主要用于集合中重复元素的判断,从而提高性能(减少使用equals的次数)。只有当两个对象的hashCode()值相等时,才会调用equals()进行再次确认;对于hashCode()不同的两个对象,它们一定不相等,所以就不用再调用equals()方法来确认了,此时性能就提高了。

        除非对象被修改了,否则调用多次hashCode()应是同样的结果。

        hashCode()的几种写法:

                

                

        Mutable对象的等价性:

                观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致;

                行为等价性:调用对象的任何方法都展示出一致的结果。

        对immutable类型来说,实现的其实是行为等价性。

        对mutable类型来说,往往倾向于实现严格的观察等价性。但在有些时候,观察等价性可能导致bug,甚至可能破坏RI,见下面这个例子:

                

                因此,如果某个mutable对象包含在集合类中,当其发生改变后,集合类的行为不确定,务必小心。

        在JDK中,不同的mutable类使用不同的等价性标准。

        对mutable类型,实现行为等价性即可。也就是说,只有指向同样内存空间的对象,才是相等的。所以,对可变类型来说,无需重写equals()和hashCode(),直接继承Object对象的这两个方法即可。如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法来判断。

        对immutable类型,必须重写equals()和hashCode()。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值