HIT Software Construction Review Notes(3-1 Data Type and Type Checking)
第三章 抽象数据类型(ADT)和面向对象编程(OOP)
3.1 数据类型和类型检查
1. 编程语言中的数据类型
类型和变量
- 类型是一组值,以及可对这组值执行的操作
- 例子:
-boolean: Truth value (true or false)
-int: Integer (0, 1, -47)
-double: Real number (3.14, 1.0, -2.1)
-String: Text (“hello”, “example”)
- 变量:存储一个特定类型的值的命名位置
- 形式:类型 名字
- 例子: String foo
Java中的类型
- Java有几种基本数据类型,其中包括:
- int(对于像5和-200这样的整数,但限于±2 ^ 31或大约±20亿的范围)
- long(对于上下限达到±2 ^ 63的整数)
- boolean(对于true或false)
- double(用于浮点数,代表实数的一个子集)
- char(用于单个字符,如'A'和'$')
- Java也有对象数据类型,例如:
- String表示一系列字符
- BigInteger表示一个任意大小的整数
按照Java的约定,基本类型是小写字母,而对象类型以大写字母开头
对象类型的层次结构
- 标准根类是Object
- 除Object以外的所有类都有一个父类,使用extends语句指定
- 如果省略了extends,则默认缺省为Object
- 一个类是其所有超类的一个实例
- 从其超类继承可见的字段和方法
- 可以覆盖方法来改变它们的行为
基本数据类型的包装类
- 将基本数据类型封装为对象
- Boolean,Integer, Short, Long, Character, Float, Double
- 典型的用例是集合类
- 除非不得不,否则不要使用包装类!(效率低)
- 编辑器会进行自动转换
- 例如:
//更正:List.add(50);更改为list.add(50);
运算符
- 运算符:执行简单操作的符号
- 操作顺序:遵循标准的数学规则
- 1.括号优先
- 2.乘除优先
- 3.加减
- 字符串连接(+)
操作
- 操作是接收输入然后产生输出的功能(有时会自行更改这些值)
- 作为中缀,前缀或后缀运算符。 例如,a+ b调用操作+:int×int→int
- 作为一个对象的方法。 例如,bigint1.add(bigint2)调用操作add:BigInteger×BigInteger→BigInteger。
- 作为一个函数。 例如,Math.sin(theta)调用sin:double→double操作。在这里,数学不是一个对象。 这是包含sin函数的类
重载运算符和操作
- 一些操作被重载:相同的操作名称用于不同的类型
- java不支持运算符重载
- 方法也可以重载,大多数编程语言都有一定程度的重载
2. 静态与动态数据类型
静态类型与动态类型
- Java是一种静态类型的语言
- 所有变量的类型在编译时已知(在程序运行之前),因此编译器也可以推导出所有表达式的类型
- 如果a和b被声明为int,那么编译器会得出一个结论:a+b也是一个int
- 事实上,Eclipse环境在编写代码时就会执行此操作,因此您在键入内容时会发现很多错误(译者注:并且沉迷于键入完成后错误消失的快感)
- 在像Python这样的动态类型语言中,这种检查会被推迟到运行时(当程序运行时)
3. 类型检查
静态检查和动态检查
- 一种语言可以提供的三种自动检查:
- 静态检查:在程序运行之前,错误会自动被发现
- 动态检查:执行代码时会自动发现错误
- 没有检查:语言根本不会帮助你发现错误。你必须亲自观察,否则最后就会得到错误的结果
- 静态捕获一个bug要好于动态捕获它,动态捕获一个bug要好于不去捕获
不匹配的类型
- Java验证类型总是要匹配的
类型转换
Implicit:隐式
静态检查
- 静态检查意味着在编译时检查错误
- bugs是编程的灾难
- 避免因使用错误类型的操作而导致的bug
- 举个例子,如果你尝试着将两个字符串相乘(”5”*”6”),那么静态检查就会在你正在编程的时候捕获到这个错误,而不是等到这行代码被执行的时候才发现错误
- 语法错误,例如额外的标点符号,即使是像Python这样的动态类型语言也可以进行这种静态检查
动态检查
- 非法的参数值。例如,整数表达式x/y只当y为0的时候是错误的,否则它可以工作。所以在这个表达式中,除零不是一个动态错误,而是一个静态错误。
- 不可表示的返回值。即特定的返回值无法在类型中表示时。
- 超出范围的索引。例如,在字符串中使用负值或太大的索引
- 在空的对象引用上调用方法
静态 vs 动态检查
- 静态检查往往是关于类型的,与变量具有的特定值无关的错误
- 静态类型保证了一个变量会从集合中获得一些值,但是我们只有运行时才知道它具有哪些值
- 所以如果错误只会由某些值引起,比如被零除或索引超出范围,那么编译器不会引发关于它的静态错误
- 相比之下,动态检查往往是由特定值引起的错误
原始类型不是真正的数字
- Java中的一个陷阱——以及其他所有编程语言——都是它们的原始数字类型具有不像我们习惯使用的整数和实数那样的特殊情况
- 结果就是,真正应该被动态检查的一些错误却根本没有得到检查
- 整数除法:5/2不返回分数,它返回一个截断的整数
- 整数溢出:如果计算结果过大或者过小,无法适应有效范围,那么就会悄然溢出并且返回错误答案。例如,int big = 20000 * 20000
- 浮点类型中的特殊值:NaN(“不是数字”)POSITIVE_INFINITY和NEGATIVE_INFINITY。例如,double a = 7/0
4. 可变性和不变性
赋值
- 使用“=”给变量赋值
- 例子:
- String foo;
- foo = “IAP 6.092”;
- 赋值可以和变量声明结合使用
- 例子:
- double badPi = 3.14;
- boolean isJanuary = true;
更改变量或其值
- 改变一个变量和改变一个值的区别是什么?
- 当你指定一个变量时,你正在改变变量的箭头指向的地方。你可以将将其指向不同的值
- 当你指定一个可变值的内容时——例如数组或列表——你将在该值内更改引用
- Change is a necessary evil
- 好的程序员可以避免变化的事情,因为它们有可能被意外的改变
不变性
- 不变性是一个主要的设计原则
- 不变数据类型创建完后,值不可修改
- Java也给了我们不变的参考:不变引用,一旦指定引用位置,不可再次指定
- 要使引用不可变,请使用关键字final声明它
- final int n = 5;
- 如果Java编译器不确信final变量只会在运行时分配一次,那么它将产生编译器错误。所以final可以提供你不可变引用的静态检查
- 最好使用final来声明方法的参数和尽可能多的局部变量
- 像变量的类型一样,这些声明是重要的记录,对代码的读者很有用,并由编译器来进行静态检查
- 注意:
- 一个final类声明意味着它不能被继承
- 一个final变量意味着它始终包含着相同的值/引用,但不能更改
- 一个final方法意味着它不能被子类覆盖
可变性和不变性
- 对象是不可变的:一旦创建,它们总是表示相同的值
- 对象是可变的:它们具有可以改变对象值的方法
不可变类型——String
- String是不可变类型的一个例子
- 一个String对象总是表示相同的字符串
- 由于String是不可变的,一旦创建,String对象就始终具有相同的值
- 要将某些内容添加到String的末尾,你必须创建一个新的String对象
可变类型——StringBuilder
- StringBuilder是可变类型的一个例子
- 它具有删除部分字符串,插入或替换字符等方法
- 这个类具有改变对象值的方法,而不仅仅是返回新值
可变性和不变性
- 所以呢?在这两种情况下,你都会以s和sb指向字符串”ab”结束
- 当只有一个对象的引用时,可变性与不变性之间的区别并不重要
- 但是当对象有其他的引用时,它们的行为会有很大的差异
- 例如,当另一个变量t指向与s相同的String对象,而另一个变量tb指向与sb相同的StringBuilder时,那么不可变对象和可变对象之间的差异就会变得更加明显
可变类型的优点
- 使用不可变的字符串,这会产生大量的临时副本
- 字符串的第一个数字(“0”)实际上在构建最终字符串的过程中被复制n次,第二个数字被复制n-1次,依此类推。
- 它实际上花费O(n2)时间来完成所有的拷贝,尽管我们只连接了n个元素
- StringBuilder旨在最大限度的减少这种复制
- 它使用一个简单但聪明的内部数据结构来避免进行任何复制,直到最后,当你用toString()调用请求最终的String时
- 获得良好的性能是我们使用可变对象的一个原因
- 另一个是方便共享:通过共享一个常见的可变数据结构,您的程序的两个部分可以更方便的进行通信
- “全局变量”
- 但是你必须知道全局变量的缺点……
可变的风险
- 由于可变类型比不可变类型更强大,为什么选择不可变类型?
- StringBuilder应该能够完成String可以做的所有事情,以及set()和append()等等
- 答案是,不可变的类型在错误中更安全,更容易理解,并且更容易进行更改
- 可变性使得你的程序正在做什么难以被理解,而更难执行变化。
- 性能和安全之间的权衡?
如果我们使用不可变类型,那么程序的不同部分可以安全地在内存*享相同的值,因此需要较少的复制和较少的内存空间。不可变性可以比可变性更有效,因为不可变类型永远不需要被防御性复制。
别名是让可变类型有风险的原因
- 如果你在方法中完全在本地使用它们,并且只有一个对象引用,那么使用可变对象就没有问题。
- 导致这个问题的原因是有多个引用,也称为别名,用于同一个可变对象。
5. code-level,run-time, and moment view——快照图
快照图
- 为了理解微妙的问题,我们可以绘制运行时发生的事情的图片。
- 快照图表示运行时程序的内部状态——堆栈(正在进行的方法及其局部变量)及其堆(当前存在的对象)。
- 我们为什么使用快照图?
- 通过图片互相交流
- 为了说明基本类型与对象类型,不可变值与不可变引用,指针别名,堆栈与堆,抽象与具体表示等概念。
- 帮助解释你的团队项目的设计(彼此之间以及和你的技术援助)。
- 为后续课程中更丰富的设计符号铺平道路
- 快照图给我们提供了一种可视化更改变量和更改值之间区别的方法:
- 当您分配给变量或字段时,您将更改变量的箭头指向的位置。 您可以将其指向不同的值。
- 当您分配可变值的内容时(例如数组或列表),您将在该值内更改引用。
快照图中的原始值和对象值
- 原始值:
- 原始值由裸常量表示。 传入的箭头是对来自变量或对象字段的值的引用。
- 对象值
- 对象值是由其类型标记的圆。 当我们想要显示更多细节时,我们在其中写入字段名称,箭头指向它们的值。 有关更详细的信息,这些字段可以包含它们的声明类型。 有些人更喜欢写x:int而不是int x,但都很好。
重新分配和不可变值
- 例如,如果我们有一个字符串变量s,我们可以将其从“a”值重新分配给“ab”
- 字符串是不可变类型的一个例子,一种类型的值一旦创建就永远不会改变。
- 不变对象(它们的设计者打算始终表示相同的值)在快照图中用双边框表示,就像我们图中的String对象一样。
可变值
- 相比之下,StringBuilder(一个内置的Java类)是一个可变对象,表示一串字符,并且它具有更改对象值的方法:
- 这两个快照图看起来非常不同,这是很好的:可变性和不可变性之间的差异将在使代码免受错误影响方面发挥重要作用
不可变参考
- Java也为我们提供了不可变的引用:一次赋值且永不重新赋值的变量。 为了使引用不可变,用关键字final声明它:
- 如果Java编译器不确定最终变量只会在运行时分配一次,那么它将产生编译器错误。 所以最终给出了对不可变引用的静态检查。
- 在快照图中,不可变引用(final)由双箭头表示。
- 这是一个对象,其ID从不改变(它不能被重新分配到不同的数字),但其年龄可以改变。
- 我们可以拥有一个对可变值的不可变的引用(例如:final StringBuilder sb),即使我们指向同一个对象,它的值也会改变。
- 我们也可以拥有一个对不可变值(如String s)的可变引用,其中变量的值可以更改,因为它可以重新指向不同的对象。
6. 复杂数据类型:数组和集合
数组
- 数组是另一个类型T的固定长度序列。例如,下面是如何声明一个数组变量并构造一个数组值以分配给它:
- int []数组类型包含所有可能的数组值,但是一旦创建了特定的数组值,永远不会改变其长度。
- 数组类型的操作包括:
- 索引:a[2]
- 赋值:a[2]=0
- 长度:a.length
List
- 我们使用List类型来代替固定长度的数组。
- List是另一个类型T的可变长度序列
- 它的一些操作:
- 索引:list.gets(2)
- 赋值:list.set(2, 0)
- 长度:list.size()
- 注1:List是一个接口
- 注2:List中的成员必须是一个对象
迭代
- 迭代一个数组
- 迭代一个List
Set
- Set是多个独一无二的对象的无序集合
- 一个对象不能多次的出现在一个Set中,要么它在,要么它不在
- Set是一个抽象接口
Map
- 一个Map类似于字典(键-值)
- Map是一个抽象接口
从集合中构建一个List
声明List,Set和Map变量
- 使用Java collections,我们可以限制集合中包含的对象的类型。
- 当我们添加一个项目时,编译器可以执行静态检查,以确保我们只添加适当类型的项目
- 然后,当我们拿出一个项目时,我们保证它的类型将是我们所期望的
- 声明:
- 我们不能创建一个原始类型的集合
- 例如,Set<int>是不会工作的
- 但是,int有一个我们可以使用的Integer包装器(例如Set <Integer> numbers)。
- 当使用的时候:
建List,Set和Map变量
- Java有助于区分:
- 一个类型的规范——它有什么作用? 抽象接口
- 实现——代码是什么? 具体类
- List,Set和Map都是接口
- 它们定义了这些相应类型的工作方式,但他们不提供实现代码
- 优点:用户有权在不同情况下选择不同的实施方式。
- List,Set和Map的实现
迭代
迭代器是一个可变类型
- 迭代器是一个对象,它遍历一组元素并逐个返回元素
- 当你使用for(...:...)循环遍历一个List或数组时,迭代器在Java中被使用。
- 迭代器有两个方法:
- next()返回集合中的下一个元素——这是一个增变方法!
- hasNext()测试迭代器是否已到达集合的末尾
ArrayList<String>的迭代器示例
MyIterator的快照图
7. 有用的不可变类型
有用的不可变类型
- 原始类型和原始包装都是不可变的。 如果你需要计算大数,BigInteger和BigDecimal是不可变的。
- 不要使用可变日期,根据您需要的计时粒度,使用java.time中适当的不可变类型。
Java的集合类型(List,Set,Map)的通常实现都是可变的:ArrayList,HashMap等。Collections实用类具有用于获得这些可变集合的不可修改视图的方法
不可修改的包装
- 不可修改的包装器通过拦截所有修改集合并抛出UnsupportedOperationException的操作来取消修改集合。
- 不可修改的包装器有两个主要用途,如下所示:
- 一旦构建一个集合就不可变。 在这种情况下,最好不要保留对后备集合的引用。 这绝对保证了不变性
- 允许某些客户端只读访问您的数据结构。您保留对后备集合的引用,但请分发引用。这样,客户可以看,但不能修改,而你保持完全访问。
参考资料:HIT软件构造课程,MIT软件构造课程
译者注:由于译者时间有限,期末考试临近,从3.2开始,译者将只会去翻译考试会涉及到的内容,其余部分内容暂不做翻译,日后有机会的话译者会再补充