前两篇回顾了什么是高质量软件,如何从不同维度刻画软件、软件构造的基本过程和步骤,本篇关注软件构造的理论基础ADT(抽象数据类型)和软件构造技术基础OOP(面向对象编程)
编程语言中的数据类型
Java中的类型与变量
数据类型:一组值以及可以对其执行的操作
变量:用特定数据类型定义,可存储满足类型约束的值
Java语言包括基本数据类型和对象数据类型。
- 基本:int long boolean double等
- 对象:string等
根据Java约定,基本体类型为小写,而对象类型以大写字母开头。
- 原始数据类型:只有值,没有ID(无法区分);不可变的 ;在栈中分配内存;代价低
- 对象数据类型:既有ID,也有值;可变或不可变;在堆中分配内存;代价昂贵
对象类型形成层次结构
所有非原始类型的根类型都为Object
如果省略了extends语句,则默认继承自Object
一个类是其所有父级类的一个具体类型
- 类可以从它的父级类中继承可见的字段和方法
- 类可以覆盖继承的方法来改变它们的行为
包装原始类型
Boolean, Integer, Short, Long, Character, Float, Double
将基本类型包装为对象类型,通常是在定义集合类型的时候使用它们,一般情况下,尽量避免使用。
一般可以自动转换。
操作、操作符
操作符
简单的计算符号:=,+,-,*,/
字符串链接(+)
String text = "hello" + " world";
text = text + " number " + 5;
// text = "hello world number 5"
操作
接受输入产生输出的函数。
作为中缀、前缀或后缀运算符。例如,a + b调用操作=: int × int → int 。
作为一个对象的一种方法。例如,bigint1.add(bigint2)调用操作add:BigInteger × BigInteger → BigInteger 。
作为一个函数。例如,Math.sin(theta)调用操作sin:double → double 。在这里,Math不是一个object。它是一个包含sin函数的类。
重载
有些操作被重载,因为相同的操作名称用于不同类型。
对于Java中的数字原始类型,算术运算符
+、-、*、/被大量重载。
方法也可以被重载。大多数编程语言都有一定程度的重载。
静态与动态数据检查
类型转换
int a = 2; // a = 2
double a = 2; // a = 2.0 (隐式类型转换)
int a = (int) 18.7; // a = 18
double a = (double)2/3; // a = 0.6666…
静态类型/动态类型
- Java是一种静态类型语言:
所有变量类型在编译时已知,因此编译器可以推导表达式类型
IDE在编写代码时就执行静态类型检查,编译阶段进行类型检查
- Python为动态类型语言,在动态类型语言中检查延迟到程序运行时
一种语言可以提供的三种自动检查:
- 静态类型检查:在程序运行之前就会自动发现错误。
- 动态类型检查:在执行代码时会自动发现错误。
- 不检查:该语言根本不能自动找到错误。
静态地捕捉一个bug比动态地捕捉它要好,而动态地捕捉它也比根本不捕捉它要好。
静态类型检查 >> 动态 >> 无检查
静态类型检查
可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性
- 语法错误:比如额外的标点符号或假词。即使是像Python这样的动态类型语言,也会进行这种静态检查。
- 错误地命名了类名/函数名错误,如Math.sine (2)。(正确的名字是sin)
- 参数数量错误,比如Math.sin(30,20)。
- 参数类型错误,如Math.sin(“30”)。
- 返回值类型错误,如从声明返回int的函数返回了“30”。
动态类型检查
- 非法参数值。例如,整数表达式x / y只有在y实际上是0时才是错误的;否则它就能工作。所以在这个表达式中,除以零不是一个静态错误,而是一个动态错误。
- 非法的返回值,即当特定的返回值不能在该类型中表示时。
- 超出范围的索引越界,例如,在字符串上使用一个负的或太大的索引。
- 调用空对象引用上的方法。(空指针)
总的来说,静态检查是关于类型的检查,不考虑具体值;动态检查是关于值的检查。
可变性与不可变性
可变性与不可变性
Java语言中,改变一个变量、改变一个变量的值,二者有区别:前者是将该变量指向另一个存储空间;后者是将该变量当前指向的存储空间中写入一个新的值。
程序编写需要尽可能避免变化。
关于不变性的设计:不变数据类型。一旦被创建,其值不能改变,引用类型也可以不变(指向对象不变)。
不变对象:一旦被创建,始终指向同一个值
可变对象:拥有方法可以修改自己的值(引用)
具体方法:
final
final int n = 5;
final Person a = new Person(“Ross”);
如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分。
所以,尽量使用final变量作为方法的输入参数、作为局部变量。final表明了程序员的一种“设计决策”。
- final类无法派生子类
- final变量无法改变值/引用
- final方法无法被子类重写
String/StringBuilder
String:
是不可变类型,要在末尾添加内容,必须创建一个新的新的字符串对象
StringBuilder:
可变类型,可以删除、替换、插入等,类有更改对象值的方法。
区别、优缺点
可变优点:
使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收)。
可变类型最少化拷贝以提高效率。
使用可变数据类型,可获得更好的性能。
也适合于在多个模块之间共享数据。
不可变优点:
不可变类型更“安全”,在其他质量指标上表现更好。
实际使用时需要折中考虑,取决于看重哪个质量指标。
使用可变数据类型时防范风险
防御式拷贝(安全,但容易造成内存浪费)
在此种情况下使用不可变类型可以防范风险,节省复制代价
安全的使用可变类型:局部变量,不会涉及共享;只有一个引用
如果有多个引用,则使用可变类型则就不安全
快照图
快照图表示一个程序在运行时的内部状态——它的堆栈(正在进行中的方法及其本地变量)和它的堆(当前存在的对象)。
优点:
- 便于程序员之间的交流
- 便于刻画各类变量随时间变化
- 便于解释设计思路
快照图类型:
基本类型:
基本类型值用常数表示。传入的箭头是对变量或对象字段中的值的引用。
对象类型的值是用其类型标记的圆。
重新分配值
- 不可变对象:双线椭圆
- 可变对象:单线椭圆
- 不可重新分配的引用:双箭头
(引用不可变,但指向的值可变;可变的引用指向的值可以不可变)
复杂数据类型:数组和集合
Array:
数组是另一种类型T的固定长度的序列。
int[]数组类型包括所有可能的数组值,但一个特定的数组值一旦创建,就永远不能更改其长度。
对数组类型的操作包括:
indexing: a[2]
assignment: a[2] = 0
length: a.length
List:
列表是另一种类型T的可变长度序列。
对列表的一些操作:
indexing: list.get(2)
assignment: list.set(2, 0)
length: list.size()
Set:
集合是包含零个或多个唯一对象的无序集合。
对象不能在集合中出现多次。
对集合的一些操作:
s1.contains(e) //test if the set contains an element
s1.containsAll(s2) //test whether s1 ⊇ s2
s1.removeAll(s2) //remove s2 from s1
Map:
映射类似于一个字典(键-值)。
对映射的操作:
map.put(key, val) //add the mapping key → val
map.get(key) //get the value for a key
map.containsKey(key) //test whether the map has a key
map.remove(key) //delete a mapping
实用不可变类型:
基本类型及其封装类型都是不可变的。
如不要使用可变的Date,使用与需求匹配的不可变类型。
List, Set, Map的具体实现类都是可变的,Collection类有一些方法来获取这些可变集合的不可修改的视图
用途:
在集合构建后使其不可变。
允许某些客户端只读访问您的数据结构。