目录
数据类型
分为基本数据类型和对象数据类型两大类。
数据类型检查
分为动态类型检查和静态类型检查两大类。
静态类型检查
- 在编译阶段进行;
- 涉及到的错误类型有:语法错误、类名/函数名错误、参数数目错误、参数类型错误、返回值类型错误等;
- 静态类型检查是关于“类型”的检查。
动态类型检查
- 在运行阶段进行;
- 涉及到的错误类型有:非法的参数值、非法的返回值、越界、空指针等;
- 动态类型检查是关于“值”的检查。
可变性与不可变性
使用fianl关键字可以将可变类型转化为不可变类型。
final类无法派生子类
final变量无法改变值/引用
final方法无法被子类重写
可变类型
- 可变数据类型:拥有方法可以修改自己的值;
- 可变引用类型:拥有方法可以修改自己的引用;
- 可获得更好的性能,也适合在多个模块之间共享数据;
- 可通过防御式复制提高安全性,但可能造成大量的内存浪费。
不可变类型
- 不可变数据类型:一旦被创建,其值不能改变;
- 不可变引用类型:一旦确定其指向的对象,不能再被改变;
- 频繁修改会产生大量的临时拷贝,需要垃圾回收,但更“安全”。
代码快照图
通过描述程序运行时的内部状态,达到便于程序员之间的交流、刻画各类变量随时间变化、解释设计思路的目的。
基本类型
对象类型
不可变类型
- 不可变对象:双线椭圆;
- 不可变引用:双线箭头。
- 不可变的引用,可以指向可变的值,可变的引用也可以指向不可变的值。
规约
包含输入/输出的数据类型、功能和正确性、性能,在开发者和客户端之间充当防火墙的角色。
规约的结构
- 前置条件:对客户端的约束,在使用方法时必须满足的条件,基本放在@param中;
- 后置条件:对开发者的约束,方法结束时必须满足的条件,基本放在@return和@throws中;
- 如果前置条件满足了,后置条件必须满足。
可变方法的规约
- 除非在后置条件里声明过,否则方法内部不应该改变输入参数。
- 尽量避免使用可变对象,因为程序中可能有很多变量指向同一个可变对象,而开发者无法强迫类的实现体和客户端不保存可变变量的“别名”。
规约的强度
如果规约强度S2≥S1,那么意味着S2前置条件更弱,后置条件更强,如此S2就可替代S1。
如果用图来表示规约的强度,那么面积越小说明规约越强。
规约的确定性
- 确定的规约:给定一个满足前置条件的输入,其输出是唯一的、明确的;
- 欠定的规约:同一个输入可以有多个输出;
- 非确定的规约:同一个输入, 多次执行时得到的输出可能不同。
如何设计好的规约
- 内聚的,规约描述的功能应单一、简单、易理解;
- 信息丰富的,不让客户端产生理解的歧义;
- 足够强度的,既满足客户端基本需求,也必须考虑特殊情况;
- 不足够强度的,太强的规约在很多特殊情况下难以达到;
- 使用抽象类型,在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度。
抽象数据类型(ADT)
ADT四种操作
- 构造器(Creator):方法构造新的对象,可能实现为构造函数或静态函数;
- 生产器(Producer):方法从已存在的对象中创建一个新的对象;
- 观察器(Observer):方法返回一个对象的相关值,该值与对象不是一个类型;
- 变值器(Mutator):改变对象属性的方法,通常返回值为void。
设计ADT
- 设计简洁、一致的操作;
- 满足客户端的需要的同时降低操作难度;
- 要么针对抽象设计,要么针对具体应用的设计。
表示独立性
ADT内部表示的变化不应影响规约和客户端。
除非ADT的操作指明了具体的前置或后置条件,否则不能改变ADT的内部表示。
不变量
- 不变量:在任何状态下不变量均为true,由ADT 负责,与client端的任何行为无关,有助于保持程序的“正确性”,更容易发现错误。
- 表示不变量:用于某个具体的“表示”是否是“合法的”。
- 表示泄露:外部类的代码会改变该类内部的表示,不仅影响不变性,也影响了表示独立性,同样也无法在不影响客户端的情况下改变其内部表示。
维护不变量防止表示泄露的方法: - 对于只在该类中使用的函数使用private关键字
- 使用final关键字
- 使用防御式复制,但不可避免地导致空间的浪费
- 使用Collections.unmodifiableSet等方法
抽象函数AF与表示不变量RI
R:包含具体实现的值,ADT实现者关注;
A:包含设计时使用的值,客户端关注;
抽象函数AF:从R映射到A的抽象函数,即如何去解释R中的每一个值为A中的每一个值,具有如下特点:
- 每一个抽象值都是由表示值映射而来 ,即满射;
- 有些抽象值是由多个表示值映射而来,即未必单射;
- 由于有些表示值是非法的,因此不是所有的表示值都能映射到抽象域中,即未必双射。
表示不变量RI:判断某个具体的“表示”是否是“合法的”。
由于RI应该是始终被满足的,所以在所有可能改变rep的方法内都要检查。
如何撰写AF和RI:
- 首先选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值。
- 不同的内部表示,需要设计不同的AF和RI;
- 以注释的形式写在代码中。
面向对象的编程(OOP)
基本概念
对象
对象均具有状态和行为两个特征,比如一只狗是一个对象,他有名字、颜色、品种等状态,以及叫,摇尾巴等行为。
在Java中,字段指代状态,方法指代行为。
类
类是一个模板,描述了一类对象的行为和状态,每个对象都有一个类,其中定义了属性类型以及行为实现。
接口
- 接口是ADT方法的集合,用于确定ADT规约。
- 接口之间可以继承与扩展,一个类可以实现多个接口,一个接口可以有多种实现类。
- 接口内无构造方法、字段和实现,其具体方法的实现在类中,用implements关键字连接接口。
- 由于接口中的每个 方法在所有类中都要实现,所以可以使用default方法,在接口中统一实现某些功能,从而无需在各个类中重复实现它,以减少工作量。
接口的好处:
- 安全性高:当客户端使用接口类型时,静态检查确保他们只使用由接口定义的方法,从而避免表示泄露;
- 易于理解:客户端确切知道在哪查找ADT的规约;
- 易于改变:通过添加和实现接口的类,从而添加新类型的实现。
继承和重写Override
继承
严格继承:子类只能添加新方法,无法重写超类中的方法,若要防止重写超类中的方法,超类中的方法需要用关键字final修饰。
重写Override
- 重写的方法要有完全一样的函数声明,包括参数列表、返回类型、函数名称等。
- 实际执行时调用哪个方法在运行时决定,执行动态类型检查;
- 在重写的方法前,需要使用@Override关键字;
- 若想在重写的函数中使用超类中的方法,使用super关键字;
- 重写的时候,不要改变原方法的本意;
- 父类的成员方法只能被其子类重写;
- 子类访问权限不能比父类中的更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected;
- 声明为final的方法不能被重写,声明为static的方法不能被重写,但是能够被再次声明;
- 子类和父类在同一个包中,那么子类可以重写父类除声明为private和final的方法以外的所有方法;子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法;
- 构造方法不能被重写;
- 如果不能继承一个方法,则不能重写该方法。
多态和重载Overload
多态
- 特殊多态:功能重载;
- 参数化多态:泛型;
- 子类型多态:包含多态。
重载Overload
- 多个方法具有同样的名字,但有不同的参数列表或返回值类型;
- 一种静态多态,在编译阶段时决定具体要执行哪种方法,根据参数列表进行最佳匹配,执行静态类型检查;
- 必须改变参数列表(参数个数或类型不一样);
- 可以改变返回类型、改变访问修饰符、声明新的或更广的检查异常;
- 可在同一个类中或者在一个子类中被重载;
- 无法以返回值类型作为重载函数的区分标准。
重写VS重载
泛型
泛型的本质是参数化类型,即将数据类型指定为一个参数。
- 泛型类:其定义中包含了类型变量的类;
- 泛型接口:定义中包含了类型变量的接口;
- 泛型方法:定义中包含了类型变量的方法。
实现泛型接口的两种方式:
- 泛型接口,非泛型的实现类:
- 泛型接口,泛型的实现类:
通配符(?)只在使用泛型的时候出现,不能再定义中出现,示例如下:
ADT和OOP中的等价性
等价关系满足自反性、对称性和传递性。
不可变对象的等价性
- 若AF映射到同样的结果,则等价;
- 两个对象之间满足自反,传递、对称的关系;
- 站在外部观察者角度发现二者没有区别。
== VS equals()
==:引用等价性,指向相同的内存地址;
equals():对象等价性,在自定义ADT时,需要重写Object的 equals(),可使用关键词instanceof,因为Object内的equals()判定的是引用等价性。
- 等价的对象必须有相同的hashCode,不相等的对象,也可以映射为同样的hashCode,但性能会变差。
- 重写Object中的equals()时,也要重写hashCode()。
对于基本数据类型,使用==判定是否相等;对于对象,使用equals()判定相等。
可变对象的等价性
- 观察等价性:在不改变状态的情况下,两个可变对象是否看起来一致,即调用observer、producer、creater方法时不改变状态;
- 行为等价性:调用对象的任何方法都展示出一致的结果。
对于不可变类型而言,观察等价性和行为等价性一致,因为其中没有mutator方法。
- 对可变类型,实现行为等价性即可,即只有指向同样内存空间的对象才是相等的。
- 因此,对可变类型来说,无需重写equals()和hashCode()方法,直接继承 Object的两个方法即可。
- 如果一定要判断两个可变对象是否具有观察等价性,最好定义一个新的方法。