三、软件构造过程与配置管理
1.传统软件过程模型
两种基本类型
-
线性过程(Linear)
-
迭代过程(Iterative)
现有模型
瀑布模型
就像瀑布一样流经,是线性过程
无法适应变化
增量过程模型 Incremental (non-iterative)
将一个瀑布模型分为多个瀑布模型实现
能适应一定变化的需求
V字模型
可以看作是瀑布模型的一个扩展
即编写代码之后进行各层次的测试,发现问题,马上返回设计阶段重新来过
原型过程 (iterative)
在原型上持续不断的迭代发现用户变化的需求,因此它能很好地适应需求的变化,但是导致了开发周期变长
迭代:开发出来之后由用户试用/评审,发现问题反馈给开发者,开发者修改原有的实现,继续交给用户评审。
螺旋模型
一种风险驱动的过程模型
经过多轮的迭代,而每轮都遵循瀑布模式,每轮迭代有明确的目标,遵循“原型”过程,进行严格的风险分析,方可进入下一轮迭代
2.敏捷开发 Agile Development
敏捷开发:通过快速迭代和小规模的持续改进,以快速适应变化。
敏捷开发的方法:
1. EXtreme Programming (XP,极限编程)
与螺旋模型相近,将复杂的工程转化为一个个小周期,每个小周期经过交流讨论与用户反馈,确定该周期的问题等,通过反复的修改与迭代,写出最终的程序
Kent Beck 曾经说过“开车”就是一个 XP 的范例,即使看上去进行得很顺利,也不要把视线从公路上移开,因为路况的变化,将使得你必须随时做出一些这样那样的调整。而在软件项目中,客户就是司机,他们也没有办法确切地知道软件应该做什么,因此程序员就需要向客户提供方向盘,并且告知我们现在的位置。
3.软件配置管理(SCM)和版本控制系统(VCS)
4.1 软件配置管理(SCM)
SCM的任务是跟踪和控制软件的变化,其核心是版本控制和基线的确立
· 软件配置项(SCI:Life Cycle of a Configuration Item)
是SCM的基本单元,eg:数据、文件、硬件信息、运行环境等
· 基线(Baseline):
基线:软件持续变化过程中的“稳定时刻”(例如:对外发布的版本)
· 配置管理数据库(CMDB):
存储软件的各配置项随时间发生变化的信息和基线信息
4.2 版本控制(Versioning)
- 仓库:相当于于SCM中的CMDB,项目版本的存储
- 工作拷贝:在开发者本地机器上的一份项目拷贝(副本\备份)
- 文件:一个独立的配置项
- 版本:在某个特定时间点的项目内容的记录
- 变化:即code churn,两个版本之间的差异
- HEAD:程序员正在其上工作的版本
版本控制系统(VCS)
1.本地版本控制系统(Local VCS): 仓库存储于开发者本地机器,无法共享和协作
2.集中式版本控制系统(Centralized VCS): 仓库存储于独立的服务器,支持多开发者之间的协作,例如:CVS,SVN(代码版本控制软件)
3.分布式版本控制系统(Distributed VCS): 仓库存储于独立的服务器+每个开发者的本地机器,例如:Git
4.Git
Git库通常由三部分组成:
本地的CMDB、工作目录(本地文件系统)、暂存区(隔离工作目录和Git仓库)
而每个Git中的文件有三个状态:
- 已修改(Modified):本地与git中的不同,未在暂存区
- 已暂存(Staged):文件已修改且加入暂存区
- 已提交(Committed):文件在工作目录和git目录中相同
Git 的所有操作都是在一个图数据结构( 对象图) 上进行
从另一台机器/ 服务器复制git 项目意味着复制整个对象图
Object Graph :版本之间的演化关系图,
一条边A->B 表征了“在版本B 的基础上作出变化,形成了版本A ”
结点:象征着一个文件的Commit
分支:是在版本控制下对对象的复制,以便可以沿两个分支平行进行修改(git checkout)
git的基本命令:
git init 初始化
git add 开始跟踪一个新文件,
一个修改过的且被跟踪的文件,处于暂存状态
运行了git add 之后又对相应文件做了修改,要重新git add 。
git commit -m ""
git clone [url]
git status
要查看具体修改了什么地方,可以用git diff 命令
跳过使用暂存区域 git commit -a
移除文件 git rm
git checkout –b iss53 转换分支
四、数据类型与类型检验
1.数据类型
基本数据类型 | 对象数据类型 |
int, long, byte, short, char, float, double, boolean | Classes, interfaces, arrays, enums, 注解annotations |
只有值,没有ID (与其他值无法区分) | 既有ID,也有值 |
在栈中分配内存,只有使用时存在 | 在堆中分配内存,用垃圾回收机制 |
代价低 | 代价昂贵 |
关于对象类型:
所有类的根是Object,除Object的所有类均有父类,使用继承extends表示
包装类型
将基本类型包装为对象类型:Boolean, Integer, Short, Long, Character, Float, Double
通常是在定义集合容器类型的时候使用它们 eg:Map<Integer,Double> map = new HashMap()
一般可以自动转换
Overloading operations 重载:同样的操作名可用于不同的数据类型
2.静态类型与动态类型检验
静态类型:在编译阶段进行类型检查
关于“类型”的检查,不考虑值
语法错误 | (动态类型检查的语言也会进行静态检查) |
类名/ 函数名错误 | Math.sine(2) . (正确名字是 sin) |
参数数目错误 | Math.sin(30, 20) |
参数类型错误 | Math.sin("30") |
返回值类型错误 | 规定返回int,但返回了String |
动态类型:在运行阶段进行类型检查
关于“值”的检查
非法的参数值 | divide-by-zero 除零错误 |
非法的返回值错误 | |
越界 | 数组越界,集合越界 |
空指针 |
无检查
整数除法 | 5/2 会返回一个被截断的整数2 |
整数溢出 | int big = 200000*200000; |
浮点数溢出 | Not a Number eg:float类型的除数为0 |
静态检查 >> 动态动态 >> 无检查
3.可变性和不可变性(Mutability and Immutability)
改变一个变量:将该变量指向另一个值的存储空间。
改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值。
不变数据类型:一旦被创建,其值不能改变
不变引用类型:一旦确定其指向的对象,不能再改变指向
可用关键字final修饰,使类型不可变:编译器进行静态类型检查时,如判断final 变量首次赋值后发生了改变,会提示错误
尽量使用final 变量作为方法的 输入参数 、作为 局部变量
- final 类无法派生子类
- final变量无法改变值/引用
- final方法无法被子类重写
不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用
eg:
- string是不可变类型:一旦被创建,值就不可变。若要在String后添加,必须创建新的String对象。
- StringBuilder是可变类型:内部存在修改方法
优点
缺点
可变类型
可获得更佳性能,也适合在多个模块间共享数据
不安全
难以理解程序正在做什么,更难满足方法的规约
不可变类型
更“安全”
不可变类型的频繁修改会产生大量垃圾(垃圾回收)
例子1:
返回可变的值出现的问题:
这个方法直接改变了List数组!
例子2:
问题:
partyPlanning
在不知不觉中修改了春天的起始位置,因为partyDate
和groundhogAnswer
指向了同一个可变Date
对象 。更糟糕的是,这个bug可能不会在这里的
partyPlanning()
或startOfSpring()
中出现。而是在另外一个调用startOfSpring()
的地方出现,得到一个错误的值然后继续进行运算。解决方案:
通过防御式拷贝,给客户端返回一个全新的Date对象return new Date(groundhogAnswer.getTime());
但是,大部分时候该拷贝不会被客户端修改,可能造成大量的内存浪费。因此,最好采用不可变类型,避免了客户端修改问题,也不会出现防御式拷贝产生的内存浪费。
安全的使用可变类型:
局部变量,不会涉及共享;
只有一个引用,如果有多个引用(别名),使用可变类型就非常不安全
别名的使用:
- 在
List
例子中,一个相同的列表被list
(在sum
和sumAbsolute
中)和myData
(在main
中)同时索引。一个程序员(sumAbsolute
的)认为更改这个列表是ok的;另一个程序员(main
)希望列表保持原样。由于别名的使用,main
的程序员得到了一个错误的结果。- 而在
Date
的例子中,有两个变量groundhogAnswer
和partyDate
索引到同一个Date
对象。这两个别名出现在程序的不同地方,所以不同的程序员很难知道别人会对这个Date
对象做哪些改变。
4.Java 数组及聚合类型、类与方法、API 文档
1. 数组
数组是一连串类型相同的元素组成的结构,而且它的长度是固定的(元素个数固定)。
其中我们用到了a.length
诸如此类的操作,不加括号。这是由于他不是一个类内的方法调用,与list不同,不能在其后加上括号和参数。
2. 列表 List
List
类型是一个长度可变的序列结构,并且是抽象接口列表可以包含零个或多个对象,而且对象可以出现多次
List
是一个接口,无法进行实例化。List
只能存对象(Interger而不是int),不能存储基本类型,否则将在编译阶段出错。由于 List 是一个接口,这种类型的对象无法直接用 new 来构造,但是它指定了 List 必须提供的操作。 ArrayList 是一个实类型的类( concrete type ),它提供了 List 操作符的具体实现。当然, ArrayList 不是唯一的实现方法(还有 LinkedList 等),但是是最常用的一个。
如果一个
List
是用Arrays.asList
创建的,它的长度就固定了。List<Integer> list = new ArrayList<Integer>(); list.get(2) list.set(2, 0) list.size() list.add(e) list.isEmpty()
3. 映射 Map
Map
是一个二元组,且为抽象接口Map<String,Interger> map= new HashMap<>(); map.put(key, val) //添加映射 key → val map.get(key) //获取 key 映射的值 map.containsKey(key) //测试 key 是否存在 map.remove(key) //删除 key 所在的映射
4. 集合 Set
集合是一种含有零个或多个不重复对象的聚合类型,并且为抽象接口。
我们不用数字索引表示集合的元素(即元素没有顺序的概念)
s1.contains(e) //测试集合中是否含有e s1.containsAll(s2) //测试是否 s1 ⊇ s2 s1.removeAll(s2) //在 s1 中去除 s2 的元素
5. 迭代器 Iterator
Iteration
是可变的迭代器。迭代器使用有两种方法:
next()
返回下一个元素,会修改迭代器的方法(mutator method);它不仅会返回一个元素,而且会改变内部状态,使得下一次使用它的时候会返回下一个元素。hasNext()
测试是否到达末尾for(…:…) 形式的遍历,调用的是被遍历对象所实现的迭代器
List<String> lst = ...; Iterator iter = lst.iterator(); while (iter.hasNext()) { String str = iter.next(); System.out.println(str); }
区别:
第一种形式不可在遍历过程中改变其中的元素,否则会报错。
第二种形式如需要在遍历过程中修改其中元素,需要采用正确方式,否则虽不会报错但会产生错误结果。
应使用iterator中的remove()方法,不能直接从集合类中移除。例子:
原因:remove后迭代器中的元素会向前移,导致遗漏一个元素未删除