软件构造 lab4-Debugging, Exception Handling, and Defensive Programming
1 实验目标概述
本次实验重点训练学生面向健壮性和正确性的编程技能,利用错误和异常处理、断言与防御式编程技术、日志/断点等调试技术、黑盒测试编程技术,使程序可在不同的健壮性/正确性需求下能恰当的处理各种例外与错误情况,在出错后可优雅的退出或继续执行,发现错误之后可有效的定位错误并做出修改。
实验针对 Lab 3 中写好的 ADT 代码和基于该 ADT 的三个应用的代码,使用以下技术进行改造,提高其健壮性和正确性:
错误处理
异常处理
Assertion 和防御式编程
日志
调试技术
黑盒测试及代码覆盖度
2 实验环境配置
略
3 实验过程
请仔细对照实验手册,针对每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
3.1 Error and Exception Handling
3.1.1 处理输入文本中的三类错误
3.1.1.1 不符合语法规范错误
3.1.1.1.1 DataPatternException
正则表达式时由于数据的常量错误而没有匹配到单个元素
if (!matcher.find())
{
try {
throw new DataPatternException(“数据:\n” + stringInfo + " \n不符合输入规范!"); //正则表达式是否能够匹配
} catch (DataPatternException e) {
flightlogger.log(Level.SEVERE, e.getMessage(), e); //日志
throw new DataPatternException(“数据:\n” + stringInfo + " \n不符合输入规范!");
}
}
3.1.1.1.2 EntryNumberFormatException
计划项编号不符合规则
public static void checkEntryNumber(String planningEntryNumber) throws EntryNumberFormatException
{
if(Character.isUpperCase(planningEntryNumber.charAt(0)) && Character.isUpperCase(planningEntryNumber.charAt(1)))
{
for(int i = 2; i < planningEntryNumber.length(); i++)
{
if(!Character.isDigit(planningEntryNumber.charAt(i)))
throw new EntryNumberFormatException("航班号 " + planningEntryNumber + " 格式错误!");
}
}
else
throw new EntryNumberFormatException("航班号 " + planningEntryNumber + " 格式错误!");
}
3.1.1.1.3 SameAirportException
起飞和到达机场相同引起的错误
public static void checkAirport(String location1, String location2) throws SameAirportException
{
if (location1.equals(location2))
{
throw new SameAirportException(location1 + ": 起始机场与降落机场相同!");
}
}
3.1.1.1.4 TimeOrderException
起飞时间应该在到达时间之前(不能相等)
public static void checkTime(String departureTime, String arrivalTime) throws TimeOrderException {
LocalDateTime dt = null, at = null;
try {
dt = LocalDateTime.parse(departureTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
at = LocalDateTime.parse(arrivalTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
} catch (Exception e) {
throw new DateTimeParseException("输入时间不符合规范!", departureTime + arrivalTime, 0);
} finally {
if(dt != null && at != null)
{
if (!dt.isBefore(at))
throw new TimeOrderException("起飞时间 " + departureTime + " 不在降落时间 " + arrivalTime + " 之前.");
}
}
}
3.1.1.1.5 PlaneNumberFormatException
飞机编号不符合格式。
public static void checkPlaneNumber(String planeNumber) throws PlaneNumberFormatException
{
if(planeNumber.charAt(0) == 'N' || planeNumber.charAt(0) == 'B')
{
for(int i = 1; i < planeNumber.length(); i++)
{
if(!Character.isDigit(planeNumber.charAt(i)))
throw new PlaneNumberFormatException("飞机编号:" + planeNumber + " 格式错误!");
}
}
else
throw new PlaneNumberFormatException("飞机编号:" + planeNumber + " 格式错误!");
}
3.1.1.1.6 PlaneTypeException
飞机类型不符合格式
public static void checkPlaneType(String planeType) throws PlaneTypeException
{
for(int i = 0; i < planeType.length(); i++)
{
if(!Character.isDigit(planeType.charAt(i)) && !Character.isUpperCase(planeType.charAt(i)))
throw new PlaneTypeException("飞机型号:" + planeType + " 格式错误!");
}
}
3.1.1.1.7 PlaneSeatRangeException
飞机座位数范围错误。
public static void checkPlaneSeat(String planeSeat) throws PlaneSeatRangeException
{
int seats = 0;
try {
seats = Integer.valueOf(planeSeat);
} catch (Exception e) {
throw new NumberFormatException(“输入座位数 " + planeSeat + " 不符合规范!”);
} finally {
if(seats < 50 || seats > 600)
{
throw new PlaneSeatRangeException(“输入座位数 " + planeSeat + " 不符合规范!”);
}
}
}
3.1.1.1.8 PlaneAgeFormatException
飞机年龄非一位小数或整数,且介于0-30之间
public static void checkPlaneAge(String planeAge) throws PlaneAgeFormatException
{
double age = 0;
if(planeAge.charAt(0) == '.')
throw new PlaneAgeFormatException("输入机龄 " + planeAge + " 不符合规范!1");
String[] str = planeAge.split("\\.");
try {
age = Double.valueOf(planeAge);
} catch (Exception e) {
throw new NumberFormatException("输入机龄 " + planeAge + " 不符合规范!2");
} finally {
if(age < 0 || age > 30)
{
throw new PlaneAgeFormatException("输入机龄 " + planeAge + " 不符合规范!3");
}
if(str.length == 2 || str.length == 1)
{
if(str[0].length() > 1 && str[0].charAt(0) == '0')
throw new PlaneAgeFormatException("输入机龄 " + planeAge + " 不符合规范!4");
else
{
for(int i = 0; i < str[0].length(); i++)
{
if(!Character.isDigit(planeAge.charAt(i)))
throw new PlaneAgeFormatException("输入机龄 " + planeAge + " 不符合规范!5");
}
}
if(str.length == 2 && !(str[1].length() == 1 && Character.isDigit(str[1].charAt(0))))
{
throw new PlaneAgeFormatException("输入机龄 " + planeAge + " 不符合规范!6");
}
}
else
throw new PlaneAgeFormatException("输入机龄 " + planeAge + " 不符合规范!7");
}
}
3.1.1.2 元素相同错误
SameEntrySameDayException
相同计划项名称问题
之前已经考虑过,直接在原基础上throw就行
3.1.1.3 依赖关系不正确错误
3.1.1.3.1 HugeTimeGapException
起飞时间和到达时间超过一天。
public static void checkTimeHuge(String departureTime, String arrivalTime) throws HugeTimeGapException {
LocalDateTime dt = null, at = null;
try {
dt = LocalDateTime.parse(departureTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
at = LocalDateTime.parse(arrivalTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
} catch (Exception e) {
throw new DateTimeParseException("输入时间不符合规范!", departureTime + arrivalTime, 0);
} finally {
if(dt != null && at != null)
{
if (dt.plusDays(1).isBefore(at))
throw new HugeTimeGapException("起飞时间 " + departureTime + " 提前降落时间 " + arrivalTime + " 一天以上.");
}
}
}
3.1.1.3.2 EntryInconsistentInfoException
相同航班号的航班信息(起降地点/时间)不一致。
public static boolean checkEntryConsistentInfo(String d1, String d2, String a1, String a2, String lo1, String lo2, String lt1, String lt2) throws EntryInconsistentInfoException {
boolean b1, b2, b;
b1 = d1.equals(d2) && a1.equals(a2);
b2 = lo1.equals(lo2) && lt1.equals(lt2);
b = b1 && b2;
return b;
}
只是一个判断函数,异常根据返回值来抛出
3.1.1.3.3 PlaneInconsistentInfoException
不同的航班中出现相同的飞机。
for(Resource r:resource)
{
if(r.getNumber().equals(number))
{
Plane R = (Plane) r;
if(!(R.getModel().equals(strType) && R.getSeats() == seats && R.getYear() == year))
try {
throw new PlaneInconsistentInfoException(“编号为 " + number + " 多次出现但各属性不完全相同!”);
} catch (PlaneInconsistentInfoException e) {
flightlogger.log(Level.SEVERE, e.getMessage(), e); //日志
throw new PlaneInconsistentInfoException(“编号为 " + number + " 多次出现但各属性不完全相同!”);
} //飞机编号相同的飞机属性是否相同
}
}
3.1.1.3.4 SameDepartureTimeException
起飞时间与计划项时间不一致
public static void checkTimeSame(String planningTime, String departureTime) throws SameDepartureTimeException, ParseException {
String[] Str = departureTime.split(" ");
SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd");
try {
format.parse(planningTime);
format.parse(Str[0]);
}catch (ParseException e) {
throw new ParseException("输入时间不符合规范!", 0);
} finally {
if(Str.length != 2)
throw new SameDepartureTimeException("起飞时间 " + departureTime + " 不合规范");
if (!planningTime.equals(Str[0]))
throw new SameDepartureTimeException("起飞时间 " + departureTime + " 与计划项时间 " + planningTime + " 不在同一天。");
}
}
3.1.2 处理客户端操作时产生的异常
3.1.2.1 DeleteAllocatedResourceException
在删除某资源的时候,如果有尚未结束的计划项正在占用该资源。
判断计划项状态是否是running或allocated
3.1.2.2 DeleteOccupiedLocationException
在删除某地点的时候,如果有尚未结束的计划项正在占用该地点。
同上面一样,只是需要考虑waitting
3.1.2.3 UnableCancelException
在取消某计划项的时候,如果该计划项的当前状态不允许取消。
通过判断cancel方法
3.1.2.4 ResourceSharedException
只有flight和train需要考虑,在为某计划项分配某资源的时候,如果分配后会导致与已有的其他计划项产生“资源独占冲突”。
分配资源时遍历查找判断
3.1.2.5 LocationSharedException
只有activity需要考虑,在为某计划项变更位置的时候,如果变更后会导致与已有的其他计划项产生“位置独占冲突”。
同上,在修改位置时遍历查找判断
3.2 Assertion and Defensive Programming
3.2.1 checkRep()检查rep invariants
3.2.2 Assertion/异常机制来保障pre-/post-condition
3.2.3 你的代码的防御式策略概述
客户端和API之间,需要基于用户输入参数进行功能控制,因此用户输入的内容正确性决定了API功能实现的正确性。客户端的输入方法或API的方法起始阶段需要对用户输入进行检查。
在API操作完成之后,在客户端或API中需要对结果进行正确性的大致检查,避免一下明显错误情况;若API操作不当,可能在程序中引入隐式错误。所以每次使用完清空之前保留数据。
在API的操作会对ADT进行影响,若ADT为可变的,则要求Setter()参数正确。检查参数正确可以在API的方法中,也可以在ADT的方法中。
3.3 Logging
建立并初始化日志
private final static Logger flightlogger = Logger.getLogger(“FlightEntry Log”);
{
Locale.setDefault(new Locale(“ch”, “China”));
FileHandler fileHandler = null;
flightlogger.setLevel(Level.INFO);
try {
fileHandler = new FileHandler(“src/log/FlightEntryLog.txt”, true);
} catch (SecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
fileHandler.setFormatter(new SimpleFormatter());
flightlogger.addHandler(fileHandler);
}
3.3.1 异常处理的日志功能
基本如上图
3.3.2 应用层操作的日志功能
3.3.3 日志查询功能
Pattern pattern1 = Pattern.compile("(.?) apps\.(.?)App (.?)\.");
Pattern pattern2 = Pattern.compile("([A-Z]?): (.*?)\.+");
通过文件读取日志信息并且通过正则表达式进行划分
然后针对用户输入的时间段进行一一匹配,判断是否符合,然后返回给用户
3.4 Testing for Robustness and Correctness
3.4.1 Testing strategy
测试时靠考虑从无到有,从未分配到已分配。
3.4.2 测试用例设计
为每个exception类都设计测试用例,将测试数据保存在test/txt里。
有些exception只有一组数据,有些为了保证测试可靠性有多个文件从不同方向进行测试。
不符合语法规范错误每个文件里只有一个数据。
而像冲突异常里就会有多组数据。
运行结果
3.4.3 测试运行结果与EclEmma覆盖度报告
Instruction Counters
Branch Counters
Complexity
3.5 SpotBugs tool
没有Spot到值得注意的Bugs。。
3.6 Debugging
3.6.1 EventManager程序
该程序要求若干时间区间交集数量的最大值。算法是标记+搜索。将区间的每一个整数点进行标记(用Map),查询是查找Map.values()最大值。
通过看规约可以知道主要想使用前缀和的思想,通过写测试首先发现空指针异常,没有对treemap里是否存在对应key值进行判断。所以加了判断并进行初始化,并由于前缀和思想,每次遇到start对对应value值加1,遇到end对对应value值-1,然后通过有序的遍历,进行最大值的筛选,可以得到一年中的最大k值。
最后将日期换算成小时,并加入异常判断。
3.6.2 LowestPrice程序
该程序想要求在有special offer的情况下最优价格。算法思想是贪心。首先假设最低代价为所有均用零售,然后每次将一个special offer加入“购物车”,更新需求,再用新需求迭代求解。
主要就是迭代,通过测试可以得知如果差值为负数应该直接break,而不是continue,并且等于0时可以继续进行,由于特殊价格链表本身就比商品长度多出,并且多出来的正好是价格,所以迭代是不用-1。
最后加入异常判断。
3.6.3 FlightClient/Flight/Plane程序
该程序通过枚举每个航班,尝试安排飞机,确保没有与其他已经分配的航班冲突,最后确认是否能所有同时分配成功。
根据经验将plane和flight中关于字符串的==与替换为.equals
根据编译器的静态检查得知sort并不能直接对List使用,所以重写compare函数并创建新的比较器调用。根据编译器的静态检查还能发现Calendar类型不能直接通过 < , > 比较,所以改成before()以及after()
并由于之前冲突判断经验发现没有考虑等于的情况。
再由测试发现并没有可以更改bFeasible的语句,并且发现循环在程序会返回false的情况下无法结束,经过调试可以发现即使遍历完所有飞机,但是只要不分配飞机就无法跳出while循环,又由于经过对飞机的随机选择,无法确定是否遍历完所有飞机再跳出while。所以对随机选择飞机进行修改,调用过的飞机将他排除在下一次随机选择范围外,并计数,达到飞机个数时跳出while。这样我们就有了2种跳出方法,但仍然无法确定是成功分配而跳出还是遍历完所有飞机都无法分配而跳出,这时发现bAllocated没有使用,所以将其当做标志变量,在分配完后不使用break跳出循环,而选择更改bAllocated的值为true,在在最外层循环判断,如果发现一次bAllocated的值为false,就可以知道分配失败,就可以将bFeasible改为false并返回。