目录
前言
这篇文章是我在学习软件构造课程系列的第一篇文章,围绕Java编程与测试基础章节,对已学内容进行自我总结以供未来使用。
主要内容包括:
-
- Java项目的创建、文件结构。
-
- Java中如何处理基本的输入。
-
- 如何在Eclipse中使用JUnit进行单元测试。
-
- 使用git向github提交代码。
示例代码是习题课的项目代码,由我自己编写。
题目如下:
一、Java项目的创建和结构
在Eclipse中创建Java项目
在Eclipse主页面的左上角选择new->Java Project创建项目,如图所示:
Java项目的文件结构
下面是我针对习题课的题目自己创建的项目,其文件结构如图:
由此可见,Java项目的核心文件结构应该是:
Project file -> Source folder -> Package -> Class
对应到图中,就是
Hindex -> src -> main -> Main.java
注意:
在Eclipse中,默认的工作路径为项目文件夹,所以如果代码中涉及到文件操作,采用相对路径时要从项目文件夹开始写起。
上图中的文件"Array.txt"就位于默认的路径下,所以代码中只需要写"Array.txt"即可访问该文件。
二、Java中对输入的基本处理方法
从控制台输入
读取用户的控制台输出,可以采用字符串转换的方法。也就是现将用户的输入视为字符串直接读取,然后再对该字符串进行一系列后续操作来提取有效数据。
首先,需要以系统输入为参数创建Scanner类的实例
Scanner scanner = new Scanner(System.in);
然后,使用Scanner类的 .nextLine() 方法等待并读取用户输入。此方法用于读取一行的输入,返回值是一个字符串:
String input = scanner.nextLine();
之后,便可以对输入的字符串input进行处理了。这里选用习题课的例子,需要对以逗号‘,’分割的一串整数进行读取。
对于分隔符",",使用 .split() 方法,接收分隔符为参数,返回一个字符串数组,每一个元素为分割之后得到的字符串。
对于字符串到int型数据的转换,使用 Integer.parseInt() 方法,接收字符串返回该字符串表示的整数。
代码示例如下:
public static int[] readInt_0(String input) throws NumberFormatException, NumberFormatException,NegativeNumberException {
String[] c_numbers = input.split(",");
int[] numbers = new int[c_numbers.length];
for(int i = 0; i < numbers.length; i++) {
numbers[i] = Integer.parseInt(c_numbers[i]);
if(numbers[i] < 0) {
System.out.printf("输入的第%d个数为负!\n", i + 1);
throw new NegativeNumberException();
}
}
return numbers;
}
注意:我在编写输入控制的方法时,发现如果将scanner定义在方法的内部,在测试时会出现一系列的问题。所以尽量将scanner定义在主方法main中,将读取的字符串作为参数进行传递。同时也不要忘记在输入结束后关闭scanner。
从文件读入
在java中,其实有多种文件读入类可供选择:
- FileInputStream:字节流,逐字节的读入,适合读取二进制文件。
- FileReader:字符流,以系统默认编码读取字符,适合读取文本文件。
- BufferedReader:具有缓冲区,可一次读取多个字符(比如读取一行),性能较高,通常用这个。
下面选用BufferedReader实现对整行
从文件读入的操作,大致分为三步:
- 1.创建BufferedReader对象。构造方法需要接受FileReader对象作为参数。
- 2.使用.readLine()方法即可读入一行
- 3.处理完毕后,要及时检查null并关闭BufferedReader对象。
BufferedReader r = new BufferedReader(new FileReader("Array.txt"));
String line = r.readLine();
这里FileReader接收文件地址作为参数,同时自己本身也作为参数构造BufferedReader的实例。
.readLine()会从文件中读取一行,返回一个字符串,后续对字符串进行与上面相似的处理即可。
但是由于java中文件的读写等操作,很可能会出现IOException异常。
此异常属于检查性异常,无法避免,要求必须使用try和catch进行处理,否则无法编译。
可以先编写核心代码,然后再套入try和catch结构。
对于函数的返回值等,需要在try结构外创建,try结构外返回,以免出现错误。
关闭Reader需要在finally中,同时也要注意null检查以及异常检查。
示例如下:
public class FileRead
{
/**
* 从文件中读取整数数组
* @return 从文件中读取的数组
*/
public static int[] readFromFile()
{
//创建BufferedReader对象用于读取文件,接收FileReader指定文件地址作为构造参数
//为了要处理异常,构造FileReader时需要在try结构内进行。
BufferedReader r = null;
String line;
int[] numbers = null;
try
{
r = new BufferedReader(new FileReader("Array.txt"));
//当读到文件末尾时,.readLine会返回null.
//这里要求只读取一行
System.out.println("文件存储数组为:");
line = r.readLine();
System.out.println(line);
String[] c_numbers = line.split(",");
numbers = new int[c_numbers.length];
for(int i=0;i<numbers.length;i++)
{
numbers[i] = Integer.parseInt(c_numbers[i]);
}
}
catch(IOException io_1)
{
io_1.printStackTrace();
}
finally
{
if(r!=null)
{
try
{
r.close();
}
catch(IOException io_2)
{
io_2.printStackTrace();
}
}
}
return numbers;
}
}
三、使用JUnit进行单元测试
基本步骤
Eclipse中有自带的JUnit插件可供使用,但是需要进行一些操作才能在项目中运行JUnit测试。如果该项目并非使用Eclipse构建,同样也可以在Eclipse平台上使用JUnit进行测试,但是需要根据项目的配置说明,手动导入JUnit库并设置。
其整体步骤如下:
- 1.在项目中添加JUnit库:
选择项目->Build Path->选择Class Path->Add Library->添加JUnit - 2.在项目中新建Source Folder:test
- 3.建立测试类:
选中需要进行测试的java类文件,选择new->other…->JUnit Test Case->选择需要测试的方法
建立完成后,便可以在test源文件夹中的同名包下找到类文件的测试类文件。 - 4.编写测试用例:
使用assertEquals(<期望值>,<方法返回值>) 来进行返回值测试。
使用assertArrayEquals(<期望数组>,<方法返回的数组>) 来进行返回值为数组的测试。(完全一致才通过)
使用assertThrows(<异常类型.class>,<函数式接口>) 来进行异常抛出的测试。
注意
在导入JUnit时,必须在Class Path中添加,且项目中不要保留module.info文件,不然会出错!
正确操作的结果如下图所示:
我在测试代码时也发现,测试文件不需要导入被测试代码中的被测试类,测试仍然可以正常进行。(猜测)这是因为按照正常操作下来,测试代码的包与主代码的包同名,尽管它们处于不同的文件位置,但java将二者视为同一个包,这可以使测试时不必导入,方便快捷。
异常测试与处理
JUnit提供了了assertThrows()方法来进行异常抛出的测试,经过学习,我总结如下:
- assertThrows() 方法:
- JUnit提供的测试异常抛出的方法。
- 第一个参数是预期的异常类型。这里需要加后缀.class,这是因为Class对象表示了一个类的元信息,而非对象实例。
- 我们关心的是异常类型是否符合预期,而不关系异常的实例,因为异常的实例会随输出而变化。
- 第二个参数是一个函数式接口的实现,因为用于测试需要输入测试用例,所以一般可以写为:
()->{调用需要测试的方法,比如Class.methodName(x,y)}。即使用空参数的Lambda表达式来实现。
这里涉及到了“函数式接口”的概念,通过与ChatGPT一番友好交流后, 我总结如下:
- 简而言之,函数式接口的最大特征就是其一般只有一个抽象方法。函数式接口用于快速创建一个输入->输出的函数。
- 函数式接口的实现可以采用方法引用和Lambda表达式,二者一般就用在这里,且是等效的。
- 函数式接口一旦被实现,就可以在任何地方引用,而不需要像传统的那样先创建类,再在类中写方法,更加方便快速。
- Lambda表达式:形式为 (<参数>,…)->{代码块}。代码块相当于一个函数的主体部分,可以使用return语句。
- 方法引用:形式为 <类名>::<方法名>。一般用于直接采用已经写好的方法来实现函数式接口。不能使用return语句。
下面对readInt_0()进行测试。主代码如下:
public static int[] readInt_0(String input) throws NumberFormatException, NumberFormatException,NegativeNumberException {
String[] c_numbers = input.split(",");
int[] numbers = new int[c_numbers.length];
for(int i = 0; i < numbers.length; i++) {
numbers[i] = Integer.parseInt(c_numbers[i]);
if(numbers[i] < 0) {
System.out.printf("输入的第%d个数为负!\n", i + 1);
throw new NegativeNumberException();
}
}
return numbers;
}
测试代码如下:
@Test
void testReadInt0() throws NumberFormatException, NegativeNumberException
{
//返回值的测试,使用数组测试
assertArrayEquals(new int[]{1,2,3,4}, Main.readInt_0("1,2,3,4"));
assertArrayEquals(new int[]{1,1,1,1}, Main.readInt_0("1,1,1,1"));
assertArrayEquals(new int[]{0}, Main.readInt_0("0"));
//异常抛出的测试
assertThrows(NumberFormatException.class, ()->{Main.readInt_0("1,2,p,4");});
assertThrows(NumberFormatException.class, ()->{Main.readInt_0("@qdsaf");});
assertThrows(NegativeNumberException.class, ()->{Main.readInt_0("1,1,-1,1");});
}
在进行异常抛出的测试时,出现了许多bug,经过多次修改后,最终通过了测试。一句话总结,就是:测试方法的异常抛出在方法之外而不是方法之内。 也就是说要使用throws关键字,将异常抛出后交给调用该方法的代码段来处理。这样才能使assertThrows()接收到抛出的异常,否则异常将会在方法内部处理完毕,无法抛出。
调用该方法的代码块如下:
Scanner scanner = new Scanner(System.in);
System.out.println("请输入数组");
int[] numbers = null;
int flag = 1;
while(flag==1)
{
try
{
numbers = readInt_0(scanner.nextLine());
flag = 0;
}
catch (NumberFormatException e1)
{
System.out.println("非法输入,请重新输入!");
flag = 1;
}
catch(NegativeNumberException e2)
{
System.out.println("输入了负数,请重新输入!");
flag = 1;
}
}
在这里,我实际上自定义了一个异常NegativeNumberException
class NegativeNumberException extends Exception {
public NegativeNumberException() {
super("输入不能为负数!");//这里是设置异常消息
}
}
这里的异常抛出有两种方式,为了便于区分我称之为:
- 1.显式抛出:throw new Exception()。一般用于自定义的异常。
- 2.自动抛出:比如在这里,如果有非法字符那么就会自动抛出NumberFormatException。
异常抛出后方法会立刻终止,try中后续代码也不会执行,而是直接跳到catch块中。这样就可以利用标识变量flag控制for循环,使用户反复输入直到输入完全合法。
四、使用git向github提交代码
在老师的要求下,之后的每次实验都要在github上提交,所以我学习了一些git与github的基本知识(主要通过git中文使用指南)。当然,git作为一个强大的版本控制系统是本专业必须要学习使用的工具。
这里只简要的说明一下如何创建远程仓库内并关联到本地仓库,以及如何将自己的代码通过git命令进行提交。
如何创建远程仓库并关联到本地仓库
- 先在github上创建一个仓库,获取URL。
- 在本地创建仓库,
并使用:git init
将本地仓库关联到远程仓库git remote add <shortname> <URL>
- 使用命令
便可以拉取仓库的信息。git fetch <shortname>
拉取后会看到有:* [new branch]的提示
这表示本地仓库跟踪了新的远程分支。
此时使用命令
找到该远程分支,假如它的名字为。git branch -r
那么执行
即可在本地创建一个新的分支并追踪远程分支。git checkout -b <local_branch_name> <RemoteBranch>
- 上传文件
使用
以及git add <filename>
将文件提交到本地仓库。git commit
然后再使用
就可以将本地项目上传到远程仓库了。git push <remote> <branch>
一个比较方便的操作
偶然间发现Eclipse中自动检测到了git bash的终端。实际上现在很多IDE都集成了各种终端。在Eclipse中,单击项目,右键选择Show in Local Terminal->Git Bash,即可在页面下方显示git bash界面。
如图所示: