软构复习(2)

第四章 数据类型和类型检验

基本数据类型/对象数据类型
int, long, byte, short, char,float, double, boolean Immutable 在栈中分配内存
Classes, interfaces, arrays, enums,annotations Some mutable, some not 在堆中分配内存

静态/动态类型检查

(Java是静态类型检查,在编译阶段进行检查,Java不进行动态类型检测)
静态类型检查:错误在程序运行之前就被自动发现了。可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性。
语法(Syntax errors)、类名/函数名(Wrong names)、参数数目(Wrong number of arguments)、参数类型(Wrong argument types)、返回值类型(Wrong return types)
动态类型检查:在执行代码时自动发现错误。
非法的参数值(Illegal argument values)、非法的返回值(Unrepresentable return values)、越界(Out-of-range indexes)、空指针(Null Point Exception)

静态检查:关于“类型”的检查,不考虑值
动态检查:关于“值”的检查
注意List<String>和List<Object>是在静态类型检测中报错
没有检查:语言完全不能帮助你找到错误。你必须自己注意,否则就会得到错误的答案

mutable/immutable 可变和不可变的

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

不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用

不变性 immutabilty:重要设计原则
不变数据类型:一旦被创建,其值不能改变
如果是引用类型,也可以是不变的:一旦确定其指向的对象,不能再被改变指向其他对象
编译器进行静态类型检查时,如判断final变量首次赋值后发生了改变,会提示错误
尽量使用final变量作为方法的输入参数、作为局部变量。
final特性: ①final 限定的是引用不变(如果mutable改变值不会报错)②final类无法派生子类 ③final方法无法被子类重写。

String是不可变类型
String对象总是表示相同的字符串。
由于String是不可变的,一旦创建,String对象总是具有相同的值。
要在字符串的末尾添加一些东西,你必须创建一个新字符串对象
StringBuilder是可变类型
它具有删除字符串部分、插入或替换字符等方法。
该类有改变对象值的方法,而不是仅仅返回新值

可变类型的优势
使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收)。可变类型因为最少化拷贝,可以提高效率
不可变类型的优势
不可变类型更“安全”,在其他质量指标上表现更好。 可变性使得难以理解程序正在做什么,更难满足方法的规约。
传递可变对象是一个潜在的错误源泉,一旦被无意中改变,则这种错误非常难于跟踪和发现

使用mutable可获得更好的性能,也适合多个模块间共享数据,但不够安全!
Date也是mutable类!避免使用!
可以使用java.time包中的其他immutable类型的类:LocalDateTime, Instant

immutable拷贝时间 O( n 2 n^2 n2)
传参数尽量用immutable类型(保证参数不变性)
如果传mutable参数可先进行defensive copying 防御式拷贝(考试经常考)
必须通过类中的方法来改变类中的属性(防止信息泄露)

//防御式拷贝
return new Date(groundhogAnswer.getTime());

Snapshot diagram 快照图(重点)

快照图用于描述程序运行时的内部状态

Primitive values 基本类型的值:

Object values 对象类型的值:

不可变对象:用双线椭圆

不可变的引用(用final修饰的变量):用双线箭头

//针对不可变值的可变引用
String s1 = new String("abc");
List<String> list = new ArrayList<String>();
list.add(s1);
s1 = s1.concat("d");
System.out.println(list.get(0));//输出:abc
String s2 = s1.concat("e");
list.set(0, s2);
System.out.println(list.get(0));//输出:abcde

复杂的数据类型:Array and Collections

Iterator
mutable类型迭代器
有两种方法:next()和hasNext(),next()方法是mutate的
需要注意,当用Iterator迭代List中元素,涉及到remove时,由于remove后List内元素索引会发生改变,会出现错误。

Collections
基本类型及其封装对象类型都是immutable
List、Map、ArrayList等都是mutable
可以利用Collections类提供的方法将mutable类包装成immutable
Collections.unmodifiableList Collections.unmodifiableSet Collections.unmodifiableMap
这种包装器得到的结果是不可变的,只能看,不能修改(其实就是disabled了一些mutate方法或者让其抛出异常)
这种”不可变“是在运行阶段获得的,编译阶段无法对此进行静态检查
虽然不能用包装后的对象对其进行修改,但依旧能用包装前的对象进行修改

第五章 设计规约Specification

规约(Specifications)
规约不要给出任何方法的实现。
规约不能被程序进行检测(×) ——函数描述可以被检测(参数类型),注释不能被检测。
规约注释包含:功能描述、输入数据限制、返回值

行为等价性(Behavioral equivalence)
一般站在用户(客户端)的角度看(可能会给一定前提),可根据规约判断是否行为等价

前置条件和后置条件
前置条件,关键词requires ,对客户端的约束,在使用方法时必须满足的条件;
后置条件,关键词effects,对开发者的约束,方法结束时必须满足的条件
如果前置条件满足,后置条件必须满足;
如果前置条件不满足,后置条件想干什么可以
契约:如果前置条件满足了,后置条件必须满足

Java中的规约
Java中的静态类型声明是一种规约,可据此进行静态类型检查
方法前的注释也是一种规约,但需要人工判定其是否满足
前置条件在 @param中,后置条件在 @return和@throws中
@return中不能包含具体类型,如@return boolean
如果方法对输入的参数做了改变,一定要在规约中说明

设计规约

规约的强弱(本节重点)
规约强度 S 2 > = S 1 S_2 >= S_1 S2>=S1 : 前置条件不强于后置条件
spec变强:更放松的前置条件 + 更严格的后置条件(考试常出无法比较的规约)
越强的规约,意味着实现者的自由度和责任越重,而用户的责任越轻。

**Diagramming specifications **
每一个点代表一个方法的实现。
如果某个具体实现满足规约,就落在其范围内;否则在其之外。
更强的规约,表示为更小的区域。(实现的自由度小,面积小)
更强的后置条件意味着实现的自由度更低了➔在图中的面积更小
更弱的前置条件意味着实现时要处理更多的可能输入,实现的自由度低了➔面积更小

设计一个好的规约
内聚的:Spec描述的功能应单一、简单、易理解
信息丰富的:不能让客户端产生理解的歧义
应该足够强(否则客户不敢用)、也应该足够弱(否则难以实现)
应该使用抽象类型: 在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度
客户端不喜欢太强的前置条件,不满足前置条件的输入会导致失败。
惯用做法是:不限定太强的前置条件,而是在后置条件中抛出异常:输入不合法

第六章 抽象数据类型(ADT

本节常考大题,包括是否出现表示暴露,AF和RI等
ADT是由操作定义的,与其内部实现无关

ADT 的特性:表示泄漏、抽象函数 AF 、表示不变量 RI
基于数学的形式对 ADT 的这些核心特征进行描述并应用于设计中

ADT四种操作类型
构造器(Creator):创建一个该类型的新对象(可能实现为构造函数或静态函数)(Integer.valueOf())
生产器(Producer):从一个类型的旧对象创建一个新对象(如String中concat方法)(BigInteger.mod(),String.toUpperCase(),Collections.unmodifiableList())
观察器(Observer):返回一个不同类型的对象(如List中的size方法)(Set.contains(),Map.keySet())
变值器(Mutator):改变对象属性的方法(List.addAll(),BufferedReader.readLine())
c r e a t o r : t ∗ → T p r o d u c e r : T + , t ∗ → T o b s e r v e r : T + , t ∗ → t m u t a t o r : T + , t ∗ → v o i d ∣ t ∣ T creator : t* → T\\ producer : T+, t* → T\\ observer : T+, t* → t\\ mutator : T+, t* → void | t | T creator:tTproducer:T+,tTobserver:T+,ttmutator:T+,tvoidtT
▪ Each T is the abstract type itself;
▪ Each t is some other type.
▪ The + marker indicates that the type may occur one or more times in that part of the signature.
▪ The * marker indicates that it occurs zero or more times.
▪ The | indicates or.

工厂方法 (factory method)
如果一个构造器是用静态方法来实现的,通常称为工厂方法:如Java中String类的String.valueOf(Object obj)方法
Mutators通常返回void,但也可以返回非空类型,如Set.add()返回的类型为boolean
immutable类型的ADT无变值器(Mutator)
判断是哪种操作类型,首先需要确定是mutable还是immutable
注:Collections.unmodifiableList() 是 producer; 如果一个方法既改变了对象属性,也返回了不同类型的对象,它是变值器Mutator

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

不变性(Invariants)
总是要假设client 有“恶意”破坏ADT不变量的行为—defensive programming
ADT来负责其不变量,与client端的任何行为无关**(考试必考表示泄露)**
表示泄露出现情况:
1.public 类型的数据 -> private final
2.mutable 类型共享引用
3.不应该包含mutate方法
当复制代价很高时,可在规约中强加条件(但是不推荐!)

Rep Invariant and Abstraction Function 表示不变性和 抽象函数

R:表示值构成的空间:实现者看到和使用的值
A:抽象值构成的空间:client看到和使用的值
ADT开发者关注表示空间R,client关注抽象空间A

Abstraction Function 抽象函数
R和A之间映射关系的函数,即如何将R中的每一个值解释为A中的每一个值。
AF : R → A
R(rep value 表示值) -> A(abstract value 抽象值) 的映射:
1.满射:所有抽象值都要有一个rep value
2.未必单射:一个抽象值可能有多个表示
3.未必双射:不是所有的表示值都有对应的抽象值

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

rep exposure safety argument 表示泄漏的安全声明
给出理由,证明代码并未对外泄露其内部表示——自证清白
选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值

设计ADT
(1) 选择R和A;
(2) RI — 合法的表示值;
(3) 如何解释合法的表示值——映射AF

How to establish invariants 如何实现不变性
在对象的初始状态不变量为true,在对象发生变化时,不变量也要为true
构造器和生产器在创建对象时要确保不变量为true
变值器和观察器在执行时必须保持不变性。
在每个方法return之前,用checkRep()检查不变量是否得以保持。
表示泄漏的风险:一旦泄露,ADT内部表示可能会在程序的任何位置发生改变(而不是限制在ADT内部),从而无法确保ADT的不变量是否能够始终保持为true。

用ADT不变量取代复杂的前置条件,相当于将复杂的前置条件封装到了ADT内部。

Checking the Rep Invariant 随时检查RI是否满足
应该调用**checkRep()**来在每个创建或改变代表(创建者、生产者和变异者)的操作结束时断言代表不变式。在所有可能改变rep的方法内都要检查。
Observer方法可以不用,但建议也要检查,以防止你的“万一”

Documenting AF and RI 在文档中记录AF和RI
在代码中用注释形式记录AF和RI
要精确的记录RI:针对Rep的每一个field以及多个fields之间的关系,进行条件限定,要精确
要精确记录AF:给出client看到的A值是什么,是对每一个Rep值的“数学运算”

有益的可变性(Beneficent mutation)
(该部分大概率会出选择)
immutable的属性是可变的,但是要保证用户角度是一样的
例如:[1, 2] 和 [2, 4]在A空间可均表示1/2

书写AF和RI
可用ADT的不变量来代替前置条件(相当于将复杂的precondition封装到了ADT内部)

第七章 面向对象的编程(OOP)

静态/实例方法
在类中使用static修饰的静态方法会随着类的定义而被分配和装载入内存中;而非静态方法属于对象的具体实例,只有在类的对象创建时在对象的内存中才有这个方法的代码段
编译器只为整个类创建了一个静态变量的副本,也就是只分配一个内存空间,虽然可能有多个实例,但这些实例共享该内存

接口(Interface)
接口之间可以继承与扩展,一个类可以实现多个接口,一个接口可以有多种实现类
接口:确定ADT规约; 类:实现ADT
Java的接口中不能含有constructors,但是从Java 8开始接口中可以含有static工厂方法,可用其替代constructors

default
通过default方法,可以在接口中统一实现某些功能,无需在各个类中重复实现它。
default 方法的典型使用方式:以增量式的为接口增加额外的功能而不破坏已实现的类

重写(Overriding)
严格继承:子类只能添加新方法,无法重写超类中的方法
如果想要一个java中方法不能被重写,必须要加上前缀final
父类型中的被重写函数体不为空:意味着对其大多数子类型来说,该方法是可以被直接复用的。对某些子类型来说,有特殊性,故重写父类型中的函数,实现自己的特殊要求
如果父类型中的某个函数实现体为空,意味着其所有子类型都需要这个功能,但各有差异,没有共性,在每个子类中均需要重写
重写时,可以利用super()来复用父类型中函数的功能

public class Advice extends Thought {
    @Override // @Override annotation in Java 5 is optional but helpful.
    public void message() {
        System.out.println("Advice.");
        super.message();  // Invoke parent's version of method.
    }
}

抽象类(Abstract Class)
抽象方法:只有声明没有具体实现的方法。用关键词abstract来定义
抽象类:如果一个类含有至少一个抽象方法,则被称为抽象类
接口:一个只含有抽象方法的抽象类
如果某些操作是子类型都共有,但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写

接口和抽象类都不能实例化

多态、子类型、重载(Polymorphism, subtyping and overloading)

(考试经常出现重载和重写的对比考察)

多态的三种类型
特殊多态:重载overload(一个方法可以有多个同名的实现)
参数化多态:泛型( 一个类型名字可以代表多个类型(泛型编程))
子类型多态、包含多态:继承(一个变量名字可以代表多个类的实例)

特殊多态和重载(Overloading)
重载:多个方法具有同样的名字,但有不同的参数列表或返回值类型
重载是一种静态多态,根据参数列表进行"最佳匹配",进行静态类型检查
重载的解析在编译阶段(与之相反,重写的方法是在运行阶段进行动态类型检查)

  • 参数列表必须不同
    相同/不同的返回值类型
    相同/不同的public/private/protected
    可以声明新的异常

参数多态和泛型(Generic)
泛型擦除:运行时泛型类型消除(如:List<String>运行时是不知道String的)
所以,不能使用泛型数组(如: Pair <String>[] foo = new Pair < String >[42]
通配符(Wildcards)
只在使用泛型的时候出现,不能在定义中出现。 如:List< ? extends Animal >
?extends T?super T 分别表示T和它的所有子/父类

子类型多态、继承

  1. 重写时,子类的规约要强于父类的规约(更弱的前置条件,更强的后置条件)

  2. 子类的可见性要强于父类(即父类如果是public,子类不能为private)

  3. 子类不能比父类抛出更多的异常(详情见LSP原则)

    注:Java无法检测1,但是可以检测出2、3

子类型多态:不同类型的对象可以统一的处理而无需区分。

instanceof
instanceof()判断对象运行时的类型
注:其父类也会判为true,如 a instanceof Object 始终为true
getclass()获取当前类型
List<Object>不是List<String>的父类
List<String>是ArrayList<String>的父类
List<?> 是 List<String>的父类
注:重写equal()方法时,需要注意参数类型,必须也是Object类型

第八章 ADTOOP中的相等性

相等关系
相等关系是一种等价关系,即满足自反、对称、传递
可以用"是否为等价关系"来检验equals()是否正确
Reflexive 自反: t==t, ∀t∈T
Symmetric 对称: t==u ⇒ u==t
Transitive 传递: t==u ∧ u==v ⇒ t==v

immutable类型的相等
判相等要从A空间来看(用户角度) AF映射到相同结果,则等价
站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价的。

== vs. equals()
== 表示的是引用等价性(一般用于基本数据类型的相等判定)
equals()表示的是对象等价性 (用于对象类型相等判定)
在自定义ADT时,需要重写Object 的 equals() 方法

equals()方法的实现
在Objects中实现的缺省equals()是在判断引用相等性(相当于==)
instanceof操作可以判断对象是否是一种特殊的类型(用instanceof是一种动态类型检查,而不是静态类型检查)
注意:不能在父类中用instanceof判断子类类型
等价的对象必须拥有相同的hashCode;不相等的对象也可以映射为同样的hashCode,但是性能会变差
重写equals方法必须要重写hashCode方法(除非能保证你的ADT不会被放入到Hash类型的集合中)

mutable类型的相等
观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致
行为等价性:调用对象的任何方法都展示出一致的结果
对于mutable类型来说,往往倾向于实现严格的观察等价性(但是在有些时候,观察等价性可能导致bug,甚至破坏RI)
注意:如果某个mutable的对象包含在Set集合类中,当其发生改变后,集合类的行为不确定!
Collections 使用的是观察等价性,但是其他的mutable类(如 StringBuilder)使用的是行为等价性

mutable类型,实现行为等价性即可。也就是说只有指向同样内存空间的objects,才是相等的,所以对mutable类型来说,无需重写这两个函数,直接调用Object的两个方法即可。(如果一定要判断两个对象"看起来"是否一致,最好定义一个新方法,e.g. similar()
1.immutable类型必须重写equals()hashCode()
2.mutable类型可以不重写,直接继承自Object

clone()
clone()创建并返回对象的一个copy
浅拷贝:对于基本数据类型,无影响;对于数组或对象数据类型,浅拷贝只是将内存地址赋值给了新变量,它们指向同一个内存空间。改变其中一个对另一个也会产生影响。
Java中的clone实现的是浅拷贝。
要避免一些问题,建议使用深拷贝。

Autoboxing
Integer 和 int 注意区别
false(Numbers between -128 and 127 are true.)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值