Chapter 3: Abstract Data Type (ADT) and Object-Oriented Programming (OOP)
抽象数据类型(ADT)与面向对象编程(OOP)
前两章回答了:什么是“高质量的软件”、
如何从不同维度刻画软件、软件构造的基本过程和步骤
本章:软件构造的理论基础——ADT
软件构造的技术基础——OOP
3.1 Data Type and Type Checking
数据类型与类型检验
-
Get to know basic knowledge about data type, and static and
dynamic type checking in programming language, especially Java.
——静态/动态类型检查 -
Understand mutability and mutable objects ——可变/不变的数据类型
-
Identify aliasing and understand the dangers of mutability
——可变数据类型的危险性 -
Use immutability to improve correctness, clarity and changeability
——不变数据类型的优越性 -
Use snapshot diagram to demonstrate the state of specific time
during a program’s execution. ——用Snapshot图理解数据类型 -
Use Arrays, Collections and Enum to deal with complex data types
——用集合类表达复杂数据类型 -
Know the harm of Null references and avoid it
-
Data type in programming languages
编程语言中的数据类型
Types and Variables
数据类型 和 变量:用特定数据类型定义,可存储满足类型约束的值
Types in Java
primitive types 基本数据类型
object types 引用(对象)数据类型
for example:
– String represents a sequence of characters.
– BigInteger represents an integer of arbitrary size.
根据Java约定,基本类型是小写的,而对象类型以大写字母开头。
Hierarchy of object types 对象类型形成层次结构
根是对象(所有non-primitives都是对象)
所有的类除了对象都有一个父类,用一个extends子句class Guitar extends Instrument{…}
如果忽略了extends子句,则默认为Object
类是所有父类的一个实例 继承关系
-从超类继承可见字段和方法
-可以重写方法来更改它们的行为
Boxed primitives 包装基本数据类型
将基本类型包装为对象类型
– Boolean, Integer, Short, Long, Character, Float, Double
通常是在定义集合类型的时候使用它们
一般情况下,尽量避免使用
Language does autoboxing and auto-unboxing 一般可以自动转换
字符串连接
– String text = “hello” + " world";
– text = text + " number " + 5; // text = “hello world number 5”
Operations 操作
操作是接受输入并产生输出的函数(有时还会更改值本身)。
-作为中缀、前缀或后缀运算符。例如,a + b调用操作+:int×int→int。
-作为一个对象的方法。例如,bigint1.add(bigint2)调用add: BigInteger×BigInteger→BigInteger操作。
-作为函数。例如,Math.sin(theta)调用操作sin: double→double。在这里,数学不是一个对象。它是包含sin函数的类。
Overloading operators/operations 重载
重载,简单说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
同样的操作名可用于不同的数据类型
对于Java中的数字基本数据类型,算术运算符+、-、*和/被严重重载。
方法也可以重载。大多数编程语言都有一定程度的重载。
2 Static vs. dynamic data type checking
静态和动态数据类型检查
Conversion by casting 类型转换
Static Typing vs. Dynamic Typing
Java 是一种静态类型语言
-所有变量的类型在编译时(程序运行之前)是已知的,因此编译器也可以推断出所有表达式的类型。
如果a和b被声明为整型,那么编译器就会得出a+b也是整型的结论。
-实际上,Eclipse环境在您编写代码时就会这样做,所以在您仍然在键入时就会发现许多错误。
– 在编译阶段进行类型检查
动态类型语言
在像Python这样的动态类型语言中,这种检查被延迟到运行时(当程序运行时)
– 在运行阶段进行类型检查
Static Checking and Dynamic Checking 静态类型检查和动态类型检查
一种语言可以提供三种自动检查:
-静态检查:甚至在程序运行之前就自动发现了错误。静态类型检查
-动态检查:执行代码时自动发现bug。动态类型检查
-没有检查:语言根本不能帮你找到错误。你必须自己注意,否则就会得到错误的答案。无检查
不用说,静态地捕获bug比动态地捕获bug要好,而动态地捕获bug比完全不捕获bug要好。
Mismatched Types 不匹配的类型
Java验证类型是否总是匹配:
Static checking 静态类型检查
静态类型检查:可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性
静态检查意味着在编译时检查bug。
bug是编程的祸根。
静态类型可以防止大量的bug感染您的系统
程序:准确地说,将操作应用于错误类型的参数所导致的错误。
如果您编写了一个断行代码,如:“5”*“6”,它试图将两个字符串相乘,那么静态类型将在您仍在编程时捕获此错误,而不是等到执行过程中到达该行为止。
Syntax errors 语法错误,
Wrong names 类名/函数名错误,
Wrong number of arguments 参数数目错误,
Wrong argument types 参数类型错误, l
Wrong return types 返回值类型错误,
语法错误,如多余的标点符号或虚假的单词。甚至像Python这样的动态类型语言也做这种静态检查。
错误的名称,比如math .sin(2)。(正确的名字是Math.sin)
参数数量错误,如Mathsin(30,20)。
错误的参数类型,如Math.sin(“30”)。
返回类型错误,如返回“30”;从声明为返回整型数的函数中。
Dynamic checking 动态类型分许
Illegal argument values 非法的参数值.
Unrepresentable return values 非法的返回值,
Out-of-range indexes 越界,
Calling a method on a null object reference. 空指针
-非法参数值。例如,整数表达式x/y只在y实际为零时是错误的;否则就会成功。在这个表达式中,除0不是一个静态误差,而是一个动态误差。
-无法表示的返回值,即,当无法在类型中表示特定的返回值时。
-超出范围的索引,例如,对字符串使用负数或过大的索引。
-对空对象引用调用方法。空指针
Static vs. Dynamic Checking
静态检查:关于“类型”的检查,不考虑值
动态检查:关于“值”的检查
静态检查往往与类型有关,即与变量的特定值无关的错误。
-静态类型保证了一个变量会有一些来自那个集合的值,但是我们直到运行时才知道它到底有哪个值。
-所以如果错误只由某些值引起,比如被0整除或索引超出范围,那么编译器就不会产生关于它的静态错误。
相比之下,动态检查往往与特定值引起的错误有关。
局部变量和全局变量 initialized初始化
3 Mutability and Immutability 可变性和不变性
Assignment 赋值
Changing a variable or its value
改变一个变量、改变一个变量的值,二者有何区别?
改变一个变量:将该变量指向另一个值的存储空间
改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值。
变化是“罪恶”,但程序不能没有变化; 尽可能避免变化,以避免副作用
Immutability 不变性
不变性:重要设计原则
不变数据类型:一旦被创建,其值不能改变
如果是引用类型,也可以是不变的:一旦确定其指向的对象,不能再被改变
如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分。
所以,尽量使用final变量作为方法的输入参数、作为局部变量。
final表明了程序员的一种“设计决策”
final类无法派生子类
final变量无法改变值/引用
final方法无法被子类重写
Mutability and Immutability
不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用
有区别吗?最终的值都是一样的
-当只有一个引用指向该值,没有区别
-有多个引用的时候,差异就出现了
当另一个变量t指向与s相同的String对象,另一个变量tb指向与sb相同的StringBuilder时,不可变对象和可变对象之间的差异就会变得更加明显。
String as an immutable type
String是不可变类型的一个例子。
字符串对象总是表示相同的字符串。
由于String是不可变的,所以一旦创建了String对象,它总是具有相同的值。
要添加一些东西到字符串的末尾,你必须创建一个新的字符串对象:
s.concat(“b”) 在字符串s后面接上b
StringBuilder as a mutable type
StringBuilder是可变类型的一个例子。
它有删除字符串部分、插入或替换字符等方法。
这个类有改变对象值的方法,而不只是返回新值:
Advantage of mutable types
使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收)
可变类型最少化拷贝以提高效率
使用可变数据类型,可获得更好的性能
也适合于在多个模块之间共享数据
s.trim 去掉字符串两端多余的空格
Risks of mutation 突变的风险
既然如此,为何还要用不可变类型?
不可变类型更“安全”,在其他质量指标上表现更好
可变性使您更难理解程序在做什么,也更难执行契约。
折中,看你看重哪个质量指标
Risky example #1: passing mutable values 传递可变值
该函数超出了spec范畴,因为它改变了输入参数的值!
这种错误非常难于跟踪和发现
对其他程序员来说,也难以理解
set()
Risky example #2: returning mutable values 返回可变值
在这两个例子中——列表和日期——如果列表和日期是不可变类型,那么问题就完全可以避免。
这些bug在设计上是不可能的。
永远不要使用Date !(Date是一个内置的Java类)
-使用java包中的一个类。时间:LocalDateTime、Instant等。
-所有的保证,在他们的规格,他们是不可变的。
How to modify the code? 如何修改代码?
In Example 1:
通过防御式拷贝,给客户端返回一个全新的Date对象
大部分时候该拷贝不会被客户端修改,
可能造成大量的内存浪费
如果使用不可变类型,
则节省了频繁复制的代价
Aliasing is what makes mutable types risky
安全的使用可变类型:局部变量,不会涉及共享;只有一个引用
如果有多个引用(别名),使用可变类型就非常不安全
- Snapshot diagram
as a code-level, run-time, and moment view
用于描述程序运行时的内部状态
便于程序员之间的交流
便于刻画各类变量随时间变化
便于解释设计思路
Mutating values vs. reassigning variables可变值与重新分配变量
快照图为我们提供了一种可视化改变变量和改变值之间的区别的方法:
当你赋值给一个变量或字段时,你是在改变变量的箭头指向的位置。你可以把它指向一个不同值。
当你赋值给可变值的内容时,例如数组或列表,你在改变该值内部的引用。
Primitive and Object values in Snapshot Diagram
基础数据类型由裸常量表示。传入箭头是对变量或对象字段的值的引用。
引入数据类型是按其类型标记的圆。
-当我们想要显示更多的细节,我们在里面写字段名,箭头指向它们的值。对于更详细的信息,字段可以包括它们声明的类型
Reassignment and immutable values 重新分配和不可变值
String是不可变类型的一个例子,这种类型的值一旦创建就永远不会更改。
不可变对象(由它们的设计者设计,以始终表示相同的值)在快照图中由双边框表示,就像图中的字符串对象一样。
Mutable values 可变值
这两个快照图看起来非常不同,这很好:可变性和不可变性之间的区别将在确保代码不受bug影响方面发挥重要作用。
Unreassignable/Immutable references 不可重分配/不可变的引用
如果Java编译器不相信您的最终变量只会在运行时分配一次,那么它将产生编译器错误。因此finaL
为不可变引用提供了一个静态检查。
在快照图中,不可重分配引用(final)用双箭头表示。
引用是不可变的,但指向的值却可以是可变的
可变的引用,也可指向不可变的值
5 Complex data types: Arrays and Collections
复杂数据类型:数组和集合
Array 数组
数组类型int[]包含所有可能的数组值,但是一个特定的数组值一旦创建,就不能改变它的长度。
数组类型的操作包括:
List 列表
注1:List是一个接口。
注2:列表中的成员必须是对象。
Iterating 迭代
Set 集合
集合是零个或多个惟一对象的无序集合。
对象不能在集合中出现多次。要么进要么出。
- s1.contains(e)测试集合是否包含元素
-s1.containsAll (s2)测试是否为s1包含 s2 - s1.removeAll(s2)从s1中删除s2
Set是一个抽象接口
Map 图
地图类似于字典(键值)
-map.put(key, val)添加映射键→val
- map.get(key)获取键的值
- map. containskey (key)测试映射是否有密钥
- map.remove(key)删除映射
Map是一个抽象接口
Map 接口定义的集合又称为查找表,用于存储所谓“key-value"映射对。Key可以看成是Value 的索引,作为key的对象在集合中不可重复。
根据内部数据结构的不同,Map 接口有多种实现类,其中常用的有内部为hash 表实现的 HashMap 和内部为排序二叉树实现的TreeMap.
Building a list from an Array
Arrays.asList(new String[] { “a”, “b”, “c” })
Declaring List, Set, and Map variables
声明列表、集合和映射变量
使用Java集合,我们可以限制集合中包含的对象的类型。
添加项时,编译器可以执行静态检查,以确保只添加适当类型的项。
然后,当我们提取一个条目时,我们可以保证它的类型将是我们所期望的。
Creating List, Set, and Map variables
Java有助于区分
-规格的一个类型-它做什么?抽象接口
-实现-代码是什么?具体类
List、Set和Map都是接口:
-它们定义了这些类型的工作方式,但不提供实现代码。
-优点:用户有权在不同的情况下选择不同的实现。
List、Set和Map的实现:
- List: ArrayList和LinkedList
- Set: HashSet
- Map: HashMap
Using List, Set, and Map variables
Iteration 迭代
Iterator是一个对象,它遍历元素集合并逐个返回元素。
当您使用for(…循环遍历列表或数组。
迭代器有两种方法:
- next()返回集合中的下一个元素——这是一个mutator方法!
- hasNext()测试迭代器是否已经到达集合的末尾。
Mutation(突变) undermines (暗中破坏) an iterator
假设我们有一个用字符串表示的主题列表。我们想要一个dropCourse6方法,可以从列表中删除这门课的6个科目,把其他科目留在后面。
首先编写规范(将在下一讲中介绍)
然后测试用例
实现
6 Useful immutable types 有用的不可变类型
Useful immutable types
The primitive types and primitive wrappers are all immutable. 基本类型及其封装对象类型都是不可变的
如果需要使用大数进行计算,BigInteger和BigDecimal是不可变的。
不要使用可变日期Date,使用适当的java不可变类型。根据所需的计时粒度确定时间。
Java集合类型(List、Set、Map)的通常实现都是可变的:ArrayList、HashMap等等。
集合实用程序类具有获取这些可变集合的不可修改视图的方法:
——Collections.unmodifiableList
——Collections.unmodifiableSet
——Collections.unmodifiableMap
Immutable Wrappers around Mutable Data Types
围绕可变数据类型的不可变包装器
这种包装器得到的结果是不可变的:只能看
但是这种“不可变”是在运行阶段获得的,编译阶段无法据此进行静态检查
如果您尝试排序()这个不可修改列表,Java不会在编译时警告您。
-你会在运行时得到一个异常。
-但这总比什么都不做要好,所以使用不可修改的列表、映射和集合是降低bug风险的好方法。
Unmodifiable Wrappers 无法改变的包装
不可修改的包装器通过拦截将修改集合的所有操作并抛出一个UnsupportedOperationException来取消修改集合的能力。
不可修改的包装有两个主要用途,如下:
-使集合一旦建立就不可变。在这种情况下,最好不要维护对支持集合的引用。这绝对保证了不变性。
-允许某些客户只读访问您的数据结构。您保留对支持集合的引用,但将引用分发给包装器。通过这种方式,客户端可以查看但不能修改,而您可以维护完全访问。
Static methods for unmodifiable collections用于不可修改集合的静态方法
若要从某些已知值创建不可变集合,请使用
另外,使用列表。创建可变集合的不可修改的浅拷贝。
7 Null references 空引用
在Java中,对对象和数组的引用也可以采用特殊的Null值,这意味着引用不指向对象。空值是Java类型系统中一个不幸的漏洞。
基本数据类型Primitive不能为空,编译器将拒绝这种尝试与静态错误:
可以将null赋值给任何非基本数据non-primitive变量,编译器在编译时欣然接受这段代码。但是在运行时,您会得到错误,因为您不能调用任何方法或使用这些引用之一的任何字段
null is not the same as an empty string “” or an empty array
non-primitive的Array和List之类的集合可能是非空的,但是包含null作为值
一旦有人试图使用集合的内容,这些null就可能导致错误。
null值非常麻烦且不安全,因此建议您将它们从设计词汇表中删除。
null值在参数和返回值中被隐式禁用。
不小心使用null会导致各种各样的错误。
研究谷歌代码基,我们发现95%的集合中不应该有任何null值,让这些集合快速失败而不是默默地接受null对开发人员是有帮助的。
此外,null的歧义令人不快。
null返回值的含义很少很明显——例如,map. get(key)可以返回null,因为map中的值是null,或者该值不在映射中。零可以意味着失败,可以意味着成功,几乎可以意味着任何事情。
使用非null语句可以清楚地表达您的意思。
静态:
动态:
空指针异常
总结
静态类型检查Static type checking:
-远离虫子。通过在运行前捕获类型错误和其他bug,静态检查有助于提高安全性。
-容易理解。它有助于理解,因为类型是在代码中显式声明的。
-准备好改变。通过标识需要同步更改的其他位置,静态检查可以更容易地更改代码。例如,当您更改变量的名称或类型时,编译器会立即在使用该变量的所有位置显示错误,并提醒您更新它们。
可变性对于性能和便捷性很有用,但它也会带来bug的风险,因为它要求使用对象的代码在全局级别上表现良好,这极大地增加了我们必须进行的推理和测试的复杂性,以确保其正确性。
确保您理解了不可变对象(如字符串)和不可变引用(如最终变量)之间的区别。
快照图可以帮助理解这一点。
-对象是值,由快照图中的圆圈表示,不可变的对象有一个双边框,表示它从不更改其值。
-引用是指向对象的指针,用快照图中的箭头表示,不可变引用是一个带有双线的箭头,表示箭头不能移动到另一个对象。
关键的设计原则是不可变性:尽可能多地使用不可变对象和不可变引用。
-远离虫子。不可变对象不容易受到混叠引起的bug的影响。不可变引用总是指向同一个对象。
-容易理解。因为不可变的对象或引用总是意味着相同的东西,所以代码的读者更容易推理
-他们不需要跟踪所有的代码来找到对象或引用可能被更改的所有位置,因为它不能被更改。
-准备好改变。如果在运行时不能更改对象或引用,那么依赖于该对象或引用的代码就不必在程序更改时进行修改。
3.1(a)
ABD AD B CDEF 56;编译错误
3.1(b)
D D 3;1;ac;abbc ABCD C TRUE;FALSE;TRUE