文章目录
第三章的内容是ADT(抽象数据类型)与OOP(面向对象编程),内容很多,因此也是分成几节的内容。这篇文章主要介绍前两节——对数据的定义和使用(第一节)和对函数、方法的定义和使用(第二节)
一.数据类型与类型检验
1.JAVA语言中的数据类型
在JAVA中,有两大类数据类型:基本数据类型与对象数据类型。一般而言,基本数据类型的类型名称都为小写(int,float等),而对象数据类型的类型名称(其实就是类名,接口名,标准等)首字母一般大写(String,BigInteger等等)
两者的区别如下图所示。
对象类型可以通过继承,从而形成层次结构,Object类是所有类的根类,所有类默认继承Object类。
每个基本类型都有对应的对象类型,一般编译器可以进行自动转换,因此我们不必要关心转换问题,就把它当作一个固定格式即可。用处:集合类中要求必须是对象类型,因此必须将基本类型进行包装;但其余时候基本不用,因为对象类型会降低性能。
运算符可以进行重载,例如String类型中的“+”运算符就是用来连接字符串。
2.数据类型检查
检查分为静态检查(编译阶段)和动态检查(运行阶段)。像我们使用eclipse时,写完的错误语句被画上红色波浪线,其实就是静态检查的结果;而那些运行过程中所抛出的异常,一般就是动态检查的结果。一般来说对数据类型的检查都是静态检查。
JAVA的变量在运行前就以及确定类型了,因此JAVA属于静态类型语言。但注意,静态类型语言与静态检查并不是一回事,同样都可以进行静态检查和动态检查,只是静态类型的语言已经可以对数据类型进行静态的检查。
一般来说,由于静态检查不需要真正执行程序,因此可以避免因程序错误所带来的各种后果,要比动态检查安全。
当然,很多时候还是有很多其他陷阱,但是编译器无法检查出来的,例如溢出等等,需要程序员自己的知识来避免。
3.可变性与不可变性
不可变的变量:对于基本数据类型和String类型的变量,就是一旦创建,则值(内容)不可再被改变,其中String类型的重新赋值会创建新的String对象并指向该内容,基本数据类型则会直接报错;而对于其他的对象数据类型,都是指向的对象不改变(其实就是地址不改变)。
可变的变量:没有上述的要求,可以对其内容或指向进行改变。
我们可以对变量加上final关键字,来将其强制变为不可变的变量。final关键字的用法如下图所示。
有些类与类之间除了可变性不同,其他的用法一样,比如String类的对象是不可变的,而StringBuilder类的对象是可变的。只有当存在多个引用的时候,他们才会出现区别。下面我们说一下他们各自的优点和缺点。
对于可变类型,拷贝比较少因此效率较高,也适合多个模块间共享数据(类似于全局变量),但是安全性较低,会有各种风险,容易出现很难发现的错误;而不可变类型效率较低,但是安全性更好。
避免可变类型带来的风险:防御性拷贝,对函数调用的返回值,采取新建一个对象并复制我们想要的结果到这个对象的方式,从而增加安全性,但这样做的缺点就是降低了效率。当然,不可变类型不需要防御性拷贝。
4.快照图
正如第一章所说,快照图主要用在运行阶段,瞬时角度的代码层级视图中。目的是更显式的描述出运行时各变量的变化,使得程序员之间的交流更方便,也让程序的运行更清晰易懂。
一般来说,快照图中,基本数据类型指向一个值,其中如果是可变的话就是单箭头,不可变的话就是双箭头;对象位一个椭圆,其中可变的话就是普通椭圆,不可变的话是双线椭圆。
以下为几个例子,总之通俗易懂。
5.数组类型与集合类型
定长数组一般用的少,用的很多的是Collection中的list作为动态数组。
集合类的结构如下图所示,平时用的最多的就是List,Set和Map。这三个本身都是泛型接口,有对应的不同实现;其中List就是变长数组,Set就是无重复的集合,Map就是键值对的映射(字典)。用处非常多,具体的方法不在这里做过多介绍,可以查询手册。
集合类中有多种遍历的方式,比如for循环和显式迭代器,但是当需要遍历且删除多个元素的时候,for循环的删除可能导致错误,因为删除一个元素后,后面元素的“下标”会发生变化,不会出错误的方法是使用迭代器,并在迭代器中(而不是原结构中)删除数据,如下图所示。
6.有用的不可变类型
集合类型中大部分都是可变的,所以会带来一定的风险。我们可以用 Collections.unmodifiableXXX(对象名)来生成对应的不可变对象。
7.空指针
总的来说,尽量不要使用空指针。
空指针只是针对对象数据类型,代表着“不确定”,调用指向null的对象都会导致空指针异常。
要注意的一点是,非空的集合类型中是可以用null空指针的,因此使用时要注意。
二.设计规约
1.JAVA语言中的方法(函数)
方法(函数)就类似程序中的一块积木,调用者关心的是它的用法,基本不关心它内部如何实现。
总的来说,java语言的方法结构如下所示。(当然比如构造方法不存在返回值类型等等,但忽略这些细节)
(关键字集合) 返回值类型 方法名 (参数列表)
{
设计规约
函数的实现(要注意返回值)
}
这节课后面主要讲解设计规约的书写,因为清晰的让调用者知道函数对应的用法很重要。
2.设计规约
(1).编程中的文档
例如JAVA中的API文档,就包含了以下内容:此类实现的接口,继承的类,它的子类是什么,对这个类的简单描述,方法列表(包括构造方法),以及每个方法的简单说明等等;内容很多。
我们在写程序时,利用了一些关键字及数据类型定义,也相当于是蕴含了设计决策。但仅通过代码,表达能力还是有限的,主要是给编译器读;因此我们需要写详细的文档,描述我们的相关思想和内容总结,主要是给我们自己还有使用者读。
(2).函数规约的撰写
总的来说,规约就是实现者和调用者之间的"约定",调用者只需要阅读规约,就应该明白方法的用法和性能。
其中必须要说明的内容:输入/输出的数据类型,功能(必要时根据输入的不同,来分别说明不同功能),正确性以及性能。总的来说,就是只讲实现了什么,不提如何实现。
一个例子如下图所示。每个输入参数都有@param来描述,还有/**,而不是/*,这是规约的特定格式。
(3).行为等价性
行为等价意味着站在调用者的角度来判断两个方法的调用结果是否相同。总之,方法之间是否等价要考虑到规约中的对客户的约束条件。
(4).规约的具体结构
在通用的语言中,总的来说存在两种约束。一种是对调用者的约束,我们称之为前置条件,采用"requires:"来标注;另一种是对开发者的约束,我们称之为后置条件,采用"effects:"来标注。规约所说明的契约就是:如果前置条件满足,那么后置条件就一定满足。
为了程序健壮性和安全,如果输入不遵守规范(前置条件不满足),抛出异常使得程序尽早结束是很好的选择。
在JAVA语言中,我们一般在@param中写前置条件,在@return和@throws写后置条件。因此就有了前文中的例子。
除非在后置条件声明过,否则方法不应该改变参数。
(5).规约的分类
可按照规约的确定性(行为是否唯一),规约的陈述性(是说明如何实现还是实现内容),以及规约的强度来进行划分,我们在下面一一讲解。
首先是规约的强度:如果一个规约和另一个规约相比,前置条件更弱或等价,且后置条件更强或等价,则前者更强,可以替换后者。更强的规约画作图,包围的面积更小(包围面积说明了合法实现的内容)这是考试重点
规约的确定性。不确定的规约有两种:欠定规约和非确定规约。欠定的规约即为同一个输入可能有多个输出情况,但确定好算法后,输出还是固定的;非确定规约即为同一个输入,每次运行输出可能不一样。其他的是确定规约。
规约的陈述性:分为操作式规约(伪代码,大体说明了如何实现)和声明式规约(没有对内部实现的描述,只有“始-终”状态)。
(6).设计好的规约
总的来说,好的规约有以下几个特点:内聚(描述的功能应单一、简单、易理解),信息丰富(对多种情况都说明结果,不要让客户端产生歧义),强弱要合适(太强了对实现者很困难,太弱了对客户端不友好,要做平衡),规约中最好使用抽象类型,正确使用前提条件(一般如果检查代价很大,或者是public方法,那么很推荐以前提条件的形式去约束)