项目地址
GitHub项目地址:https://github.com/littleyysocute/WordCount
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
·Estimate | ·估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | ||
·Analysis | ·需求分析(包括学习新技术) | 50 | 60 |
·Design Spec | ·生成设计文档 | 90 | 100 |
·Design Review | ·设计复审(和同学审核设计文档) | 10 | 10 |
·Coding Standard | ·代码规范(为目前的开发指定合适的规范) | 30 | 20 |
·Design | ·具体设计 | 120 | 150 |
·Coding | ·具体编码 | 1500 | 1200 |
·Code Review | ·代码复审 | 120 | 120 |
·Test | ·测试(自我测试,修改代码,提交修改) | 150 | 200 |
Reporting | 报告 | ||
·Test Report | ·测试报告 | 150 | 120 |
·Size Measurement | ·计算工作量 | 30 | 20 |
·Postmortem & Process Improvement Plan | ·事后总结,并提出过程改进计划 | 60 | 30 |
合计 | 2340 | 2050 |
解题思路
我们需要写出一个单词计数软件,它能够统计文本文件的字符数、单词数和行数。这是一个命令行程序,命令的格式为:
- wc.exe [parameter] [file_name]
其中的参数共有六项,分别是:
- -c,返回文件的字符数
- -w,返回文件中词的数目
- -l,返回文件的行数
- -s,递归处理目录下符合条件的文件
- -a,返回更复杂的数据(代码行/空行/注释行)
- -x,这一参数单独使用,用来显示图形界面,供用户在图形界面上选取文件,而后返回文件的字符数、行数等全部信息
前三项为基本要求,后两项为扩展要求,最后一项为高级要求。
首先处理基本功能。字符数和行数都比较直观,但词的数目需要进行一些区分。在发现JAVA库中自带的相关函数前,我们关于这个问题思索了许久。简单地通过空格和符号判断词的界限和个数显然是不可行或不准确的(对于标准格式的文本文件这样的方式是可行的,但是文件大多格式并不标准)。但后来通过调用JAVA库的函数,这些问题也都迎刃而解。
返回行数信息的功能中,难点在于代码行、空行和注释行的判别。空行相对容易,但代码行和注释行容易混淆。例如“}//注释”的行会被视为注释行,而“i++;//注释”则会被视为代码行,“/*”造成的有换行的注释也会为判别带来困难。
处理目录下符合条件文件的操作,主要在于递归地对文件目录进行遍历,找到满足要求的文件,再对文件进行上述四种操作。
图形界面的操作虽然看起来复杂,但JAVA库中都已经有了相关的方法,因此处理起来相对容易一些。
实现过程
本次实验采用JAVA语言完成。
程序的主函数在Count.java文件内,然后再由CountManager.java文件内的函数对用户的输入进行处理。在这一过程中如果出现问题,如输入指令不存在、找不到文件、用户输入不规范等,都会抛出异常信息。
接下来是对各种指令的处理:
1.字符数
处理字符数指令最简单,只需要在读的同时计数即可:
public class CharCount implements CountFactory {
private int CharNumber;
public CharCount(int CharNumber) {
this.CharNumber = CharNumber;
}
@Override
public void count(File file) throws Exception {
@SuppressWarnings("resource")
BufferedReader bufread = new BufferedReader(new FileReader(file));
while (bufread.read() != -1) {
CharNumber++;
}
System.out.println("文件: " + file.getName() + " 的总字符数为:" + CharNumber);
}
}
2.单词数
这里调用了JAVA的split方法。split方法可以通过给定的令牌字符对正则表达式进行区分,这里采用的令牌字符是“\W”,用于匹配非单词字符(除A ~ Z、 a ~ z、0 ~ 9外的字符)。程序通过逐行地读取文件的内容,统计每一行的次数并进行累计,最后就可以得到文件的单词数。代码如下:
public class WordCount implements CountFactory {
private int WordNumber;
public WordCount(int WordNumber) {
this.WordNumber = WordNumber;
}
@Override
public void count(File file) throws Exception {
String str = "";
String[] AllWord = null;
@SuppressWarnings("resource")
BufferedReader bufread = new BufferedReader(new FileReader(file));
while ((str = bufread.readLine()) != null) {
AllWord = str.split("\\W");
WordNumber += AllWord.length;
AllWord = null;
}
System.out.println("文件: " + file.getName() + " 的总单词数为:" + WordNumber);
}
}
3.行数
计算行数的方法写在LineCount.java文件里。只需要逐行地读取文件,每读取一行就对计数器加一,最后就可以得到行数。代码大多与前面的大同小异,仅对上一部分中的while循环进行修改即可,此处不再贴出。
4.各段行数统计
当指令为-a时,我们需要统计文件中代码行、空行和注释行的数量。这里的重点在于对三种字段的判别。
空行这里用一个正则表达式表示(见下代码),该正则表达式表示“从开头到结尾,全部都是非换行符外的空白符号”,这种情况下会被判断为空行。
接下来主要来判断代码行和注释行。首先看注释采用“/*”写法的情况,这种时候需要单独创建一个布尔值,当发现注释开头的时候将此布尔值置为真,此后找到的所有行都是注释行,直到发现注释结尾。对采用“//”写法的注释还需要单独判断。这里采用了计算字数时使用的split方法,将“/”当作令牌字符,这样划分出来的第一部分就是代码部分(假设代码中不含/),第二部分是空的(即注释用的双斜杠中间),第三部分是注释部分。再对第一部分进行判别,这样就可以得到判断。
代码如下:
public class AllLineCount implements CountFactory {
private int BlankLineNumber;
private int CodeLineNumber;
private int CommentLineNumber;
public AllLineCount(int BlankLineNumber, int CodeLineNumber, int CommentLineNumber) {
this.BlankLineNumber = BlankLineNumber;
this.CodeLineNumber = CodeLineNumber;
this.CommentLineNumber = CommentLineNumber;
}
@Override
public void count(File file) throws Exception {
boolean isCommentLine = false;
String str = "";
@SuppressWarnings("resource")
BufferedReader bufread = new BufferedReader(new FileReader(file));
while ((str = bufread.readLine()) != null) {
str = str.trim();
if (str.matches("^[\\s&&[^\\n]]*$") || str.length() == 1) {
BlankLineNumber++;
} else if (str.startsWith("/*") && !str.endsWith("/*")) {
CommentLineNumber++;
isCommentLine = true;
} else if (isCommentLine) {
CommentLineNumber++;
if (str.endsWith("*/"))
isCommentLine = false;
} else if (str.contains("//") && judge(str)) {
CommentLineNumber++;
} else
CodeLineNumber++;
}
System.out.println("文件:" + file.getName() + " 的代码行数为:" + CodeLineNumber + "; " + "空行数为:" + BlankLineNumber
+ "; " + "注释行数为:" + CommentLineNumber);
}
public boolean judge(String str) {
String[] div;
div = str.split("/");
div[0] = div[0].replaceAll("\\s*", "");
if (div[0].length() <= 1 && div[1].length() == 0) {
return true;
} else {
return false;
}
}
}
5.对子目录文件进行操作
由主方法传入的参数中已经包含了需要进行操作的文件类型,因此我们可以调用JAVA中的方法在文件所在目录与子目录中进行查找。代码如下:
public class Recursive {
private String recursivecommandtype;
private String filetype;
public Recursive(String recursivecommandtype, String filetype) {
this.recursivecommandtype = recursivecommandtype;
this.filetype = filetype;
}
public void count(File file) throws Exception {
if (file.isDirectory()) {
File[] AllFile = file.listFiles();
for (File each_file : AllFile) {
count(each_file);
}
} else {
boolean wrong = false;
String RealType = file.getPath();
char realtype = RealType.charAt(RealType.length() - 1);
if (realtype == filetype.charAt(filetype.length() - 1)) {
CountFactory count = null;
if ("-c".equals(recursivecommandtype)) {
count = new CharCount(0);
} else if ("-w".equals(recursivecommandtype)) {
count = new WordCount(0);
} else if ("-l".equals(recursivecommandtype)) {
count = new LineCount(0);
} else if ("-a".equals(recursivecommandtype)) {
count = new AllLineCount(0, 0, 0);
} else {
wrong = true;
System.out.println("Nonlicet command args.");
}
if (!wrong)
count.count(file);
}
}
}
}
6.图形界面
这里的代码基本就是在调用JAVA库,代码如下:
public class GUI {
private static JFileChooser jfc;
private static File file;
public GUI() {
init();
}
public void init() {
jfc = new JFileChooser();
jfc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
jfc.showDialog(new JLabel(), "选择");
file = jfc.getSelectedFile();
}
public void count() throws Exception {
CountFactory count1 = new CharCount(0);
count1.count(file);
CountFactory count2 = new WordCount(0);
count2.count(file);
CountFactory count3 = new LineCount(0);
count3.count(file);
CountFactory count4 = new AllLineCount(0, 0, 0);
count4.count(file);
}
}
性能优化
这里的代码以文件操作和库方法为主,并没有设计特殊的算法或数据结构,因此在性能方面能做的优化十分有限。经过多次测试,因为未知原因,第一次运行时总是会显得运行比较缓慢,推测是文件流的问题。因为我们并不熟悉JAVA文件流相关的原理,因此程序可能会在这里消耗比较多的时间。由于程序主要以库方法为主,我们改进性能的空间并不大,我们的改进主要是对代码进行了重构。重构之后的代码更加清晰易懂,采用了简单工厂模式。原先版本的程序向每个方法中传入的参数都是路径,需要在方法内转化为文件,经过重构之后直接向方法传递文件,节约了时间。
实验总结
这一实验虽然并不十分复杂,但是依然耗费了一些功夫。主要的收获在于两点:
1.对JAVA的进一步熟练。在实验过程中,我们都收获了JAVA编程的很多经验,熟练了JAVA方法的使用。
2.对软件工程过程的进一步熟悉。不同于个人项目,小组项目相对需要强调组员之间的交流和合作。这使得开发过程变得不同以往,让我们对软件工程的各个步骤以及它们的重要性有了进一步的认识。