【编程练习】java简易学生管理系统

上周一个数据库作业,用文件读写的方式来实现学生信息的读写,从而与数据库编程的方式进行对比。

在这个练习中,我主要是打算熟悉一下 java 的文件操作,因为我发现我学了 java 之后基本没有写过文件读写。

本文主要总结一下本练习用到的一些知识点,方便下次使用。

本文对应的 github 库

参考链接

题目

  • 1.建两个文本文件
  • 2.插入学生信息
  • 3.查询学生对应的奖励
  • 4.增删改信息

文件 1 学生基本信息

学号 姓名 性别 年龄 专业 位置 长度
2017901006 杨啸 21 软件工程 0 30
…… …… …… …… …… …… ……
  • 位置:奖励文件内对应的位置
  • 长度:奖励文件内对应的奖励字段的长度

文件 2 奖励

记录学生获得的奖励

奖励
2011 校奖学金,2012 国家奖学金
2012 校优秀学生

结构

  • model 包
    • Gender 枚举类:枚举值为“男”和“女”
    • StudentInfo 类:作为学生信息结点,存储学生信息的六个字段。后来写着写着觉得这个类名太长了,不该加 Info 后缀的,写起来怪怪的。
    • StudentInfoSystem 类:用于实现整个系统的逻辑功能,包括录入信息,查找信息,保存数据,读取数据等。
  • view 包
    • ConsoleMenu 类:控制台菜单类,作为与用户交互的界面。

知识点总结

Gender 枚举类

在录入学生性别的时候,会涉及到输入什么的问题,输入 0 或 1?输入“男”或“女”?如果用整数或者字符串都可能会产生非法数据,导致一些问题,这些问题在很久以前我拿 C++写的时候就遇到过一些。

最好的方式不是用布尔类型这种二值类型,而是使用枚举类型,因为有可能用到别的值,比如 UNSET(未设置)。

在这次练习之前,我会的 java 枚举大概是下面这样:

1
2
3
4
5
public enum Gender {
MALE,
FAMALE
}

如同 C++枚举一样列出枚举值,然后当成常量一样使用。

后面变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//Gender.java
package model;

public enum Gender {
MALE("男"),
FAMALE("女");

private String m_genderString;
private Gender(String gender) {
m_genderString = gender;
}

public String getGenderString() {
return m_genderString;
}

/**
* 根据字符串的值返回对应的枚举值
* TODO:应该可以优化成不用手动写switch的
* @param genderString
* @return 返回字符串对应的枚举值,找不到则返回null
*/
public static Gender newGender(String genderString) {
switch (genderString) {
case "男":
return Gender.MALE;
case "女":
return Gender.FAMALE;

default:
break;
}
return null;
}

}

百度了几篇博客之后(当时没收藏,找不到了),知道了枚举类一点点原理。

拿上面这个代码举例:写MALE("男")其实像是为 Gender 类的构造方法传入一个值为“男”的字符串,而默认的构造方法是没有参数的,所以你需要写一个以字符串为参数的构造方法。

为了保留下这个传入的字符串,需要定义一个字符串类型的属性;为了获取它,再定义一个 getter。

这样做的目的是,在输出学生性别时,可以调用性别属性的getGenderString()方法获取对应的字符串。

最下面那个newGender()方法则是在录入学生性别时使用。

File 类

java 中的 File 类是对文件的抽象,它可以是文件,也可以是目录。

1
2
File dir = new File(pathName);
if(!dir.isDirectory()) return -1;//如果传入的不是目录字符串,则返回

它拥有的方法主要是用于检测文件的信息的,比如它是文件还是目录,是否存在,绝对路径是什么等等,它不能直接读写文件(创建倒是可以)。

读写文件得需要其他类,这一部分的类太多了,我找了挺久才找到合适的类。

FileOutputStream 类

首先是保存数据到文件中。用到了这个类,它是文件输出流,以字节流的形式输出到文件。意思就是给他传入的参数是字节数组而不是字符串。不过字符串转字节数组非常简单,用字符串的getBytes()就好。

打开文件输出流:

1
2
3
4
5
6
7
8
File dir = new File(pathName);
if(!dir.isDirectory()) return -1;//如果传入的不是目录字符串,则返回

File infoFile = new File(pathName + "/StudentInfoList.csv");//学生信息文件

if(!infoFile.exists()) infoFile.createNewFile();

FileOutputStream infoFOS = new FileOutputStream(infoFile);

随后拼接好学生信息的字符串,转换为字节数组,传入即可:

1
2
byte[] infoBuf = studentInfoString.getBytes();
infoFOS.write(infoBuf);//写入文件

最后记得 close

1
infoFOS.close();

FileReader 类和 BufferedReader 类

一行一个学生信息,所以读取时打算直接 readline,需要BufferedReader类,进而需要FileReader类。

1
2
3
4
5
BufferedReader bufReader = new BufferedReader(new FileReader(infoFile));//打开缓冲字符流
String tmpString = null;
while((tmpString = bufReader.readLine()) != null) {

}

因为基本信息文件中保存的是奖励在奖励文件中的位置与长度,所以在读取时需要设置文件指针。

这个需要用到RandomAccessFile类。

1
RandomAccessFile randomAccessFile = new RandomAccessFile(rewardFile, "r");//打开随机读写

设置文件指针到指定位置,读取对应长度的字节。

1
2
3
4
5
//从Reward文件中读取奖励信息
randomAccessFile.seek(position);
byte[] tmpBytes = new byte[1024];
randomAccessFile.read(tmpBytes,0, rewardLen);//读取指定长度的奖励信息
String rewardString = new String(tmpBytes);

写到这里我还遇到了一个问题,就是读取到字符串后,无法判断字符串是否为空,如果用rewardString.equals("")或者``rewardString.isEmpty()`,发现字符串的长度是和我设置的缓冲区大小有关的。虽然全部是空白,但是并不为空字符串。用长度判断也不行。

后来找到了一个方法:

1
String rewardString = new String(tmpBytes).trim();//转换为字符串

trim()可以去掉首尾空白,那么 1024 长度的空字符串就会变成 0 长度的普通的空字符串,就可以用刚刚的方式来判断学生是否有奖励了。

录入学生信息

java 的控制台输入我也是没弄过,这次就来试试。

会了之后发现蛮简单的。创建一个Scanner对象,传入源输入流,要从控制台输入,所以输入流设置为System.in

1
Scanner scanner = new Scanner(System.in);

需要读取一行数据,可以使用next()或者nextLine()方法,区别详情见百度

录入普通字符串搞定:

1
2
3
4
5
System.out.println("请输入学生的学号");
String studentId = scanner.nextLine();

System.out.println("请输入学生的姓名");
String name = scanner.nextLine();

录入性别则需要判断合法性(其实上面的学号,姓名也需要判断合法性,不过不是本次练习的核心,就没弄了)

1
2
3
4
5
6
7
String genderString = "";
Gender gender = null;
do {
System.out.println("请输入学生的性别,输入\"男\" 或者\"女\"");
genderString = scanner.nextLine();
gender = Gender.newGender(genderString);
}while(gender == null);

这里用了一个 do-while 循环,如果输入的不是枚举类里面有的值,就要求再次输入。

接着是年龄,年龄是一个正整数,需要nextInt()来获取整数,如果输入的不是整数,就会抛出异常。处理完这个异常之后继续要求输入,直到输入正确的年龄为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
System.out.println("请输入学生的年龄");
int age = 0;
while (true) {
try {
age = scanner.nextInt();
if(age<=0) {
System.out.println("年龄必须是正数");
}
else {
break;
}
} catch (Exception e) {
System.out.println("年龄必须是整数");
scanner.next();//清空错误数据
}
}

在输入完整数类型的年龄后,需要输入字符串类型的专业了,这里会遇到和 C++输入一样的情况,那就是你还没输入就当你输入了,最后得到的是空字符串。这是因为在输入整数后按下回车键确定时,这个换行符还留存在输入缓冲区,下一个nextLine()将其当成了结束标志,从而结束字符串的输入。所以需要清空一下缓冲区,清空方式就是读取一个值并丢弃:

1
2
3
4
scanner.nextLine();//清除缓冲区空行

System.out.println("请输入学生的专业");
String major = scanner.nextLine();

最后是录入学生的奖励,这个部分加深了我对“String 的相等比较不能用==而要用equals()”的知识点的印象。

1
2
3
4
5
6
7
8
9
10
System.out.println("请输入学生的专业");
String major = scanner.nextLine();

System.out.println("请输入学生的奖励,每输入完一项换行,输入\"done\"结束输入");
String rewardString = "";
Vector<String> reward = new Vector<String>();
do {
rewardString = scanner.nextLine();
if(!rewardString.equalsIgnoreCase("done")) reward.add(rewardString);
} while (!rewardString.equalsIgnoreCase("done"));

核心代码

保存数据到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 保存到文件中
* @param pathName 数据文件所在的路径字符串
* @return 保存的记录数,如果出错返回-1
*/
public int saveData(String pathName) {


try {
File dir = new File(pathName);
if(!dir.isDirectory()) return -1;//如果传入的不是目录字符串,则返回

File infoFile = new File(pathName + "/StudentInfoList.csv");//学生信息文件
File rewardFile = new File(pathName + "/Rewards.csv");//学生奖励文件

if(!infoFile.exists()) infoFile.createNewFile();
if(!rewardFile.exists()) rewardFile.createNewFile();

FileOutputStream infoFOS = new FileOutputStream(infoFile);
FileOutputStream rewardFOS = new FileOutputStream(rewardFile);

int curPosition = 0;//“奖励”文件指针当前位置
for(StudentInfo studentInfo:m_studentInfoList) {

String studentInfoString = String.format("%s,%s,%s,%d,%s,",
studentInfo.getStudentId(),
studentInfo.getName(),
studentInfo.getGender().getGenderString(),
studentInfo.getAge(),
studentInfo.getMajor()
);


String rewardString = String.join(",", studentInfo.getReward());//拼接奖励字符串
if(!rewardString.isEmpty()) {
//奖励字符串不为空,则添加换行符
rewardString += "\n";
}



byte[] rewardBuf = rewardString.getBytes();//转换为字节数组



studentInfoString += String.format("%s,%s\n", curPosition,rewardBuf.length);
curPosition += rewardBuf.length;//计算下一个位置

byte[] infoBuf = studentInfoString.getBytes();

//写入文件
infoFOS.write(infoBuf);
rewardFOS.write(rewardBuf);
}

System.out.printf("成功保存%d条记录到以下文件中:\n[%s]\n[%s]\n",
m_studentInfoList.size(),
infoFile.getCanonicalPath(),
rewardFile.getCanonicalPath()
);

rewardFOS.close();
infoFOS.close();


return m_studentInfoList.size();

} catch (IOException e) {
e.printStackTrace();
}

return 0;
}

从文件中加载数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* 从文件中加载
* @param pathName 数据文件所在的路径字符串
* @param clearFlag 是否清空内存中原有数据
* @return 读取的记录数,如果出错返回-1
*/
public int loadData(String pathName,boolean clearFlag) {
try {
if(clearFlag) m_studentInfoList.clear();

File dir = new File(pathName);
if(!dir.isDirectory()) return -1;//如果传入的不是目录字符串,则返回

File infoFile = new File(pathName + "/StudentInfoList.csv");//学生信息文件
File rewardFile = new File(pathName + "/Rewards.csv");//学生奖励文件


BufferedReader bufReader = new BufferedReader(new FileReader(infoFile));//打开缓冲字符流
RandomAccessFile randomAccessFile = new RandomAccessFile(rewardFile, "r");//打开随机读写

String tmpString = null;
int counter = 0;
while((tmpString = bufReader.readLine()) != null) {

//按行读取
String[] infoStrings = tmpString.split(",");//按照分隔符分割

if(infoStrings.length != 7) {
return -1;//如果字段数对不上,说明文件格式有问题
}

//从StudentInfo文件中读取学生信息
String studentId = infoStrings[0];
String name = infoStrings[1];
Gender gender = Gender.newGender(infoStrings[2]);
if(gender == null) return -1;

int age = Integer.parseInt(infoStrings[3]);
String major = infoStrings[4];

long position = Long.parseLong(infoStrings[5]);
int rewardLen = Integer.parseInt(infoStrings[6]);

//从Reward文件中读取奖励信息
randomAccessFile.seek(position);
byte[] tmpBytes = new byte[1024];
randomAccessFile.read(tmpBytes,0, rewardLen);//读取指定长度的奖励信息

String rewardString = new String(tmpBytes).trim();//转换为字符串


Vector<String> rewardList = null;
if(!rewardString.isEmpty()) {
//如果奖励不为空则添加
rewardList = new Vector<String>();
String[] rewardArr = rewardString.split(",");
for(String reward : rewardArr) {
rewardList.add(reward);
}

}


StudentInfo newStudentInfo = new StudentInfo(studentId, name, gender, age, major, rewardList);

m_studentInfoList.add(newStudentInfo);
counter++;
}


bufReader.close();
randomAccessFile.close();


return counter;



} catch (Exception e) {
e.printStackTrace();
return -1;
}




}

【编程练习】java简易学生管理系统

https://yxchangingself.xyz/posts/java_simple_studentInfoSystem/

作者

憧憬少

发布于

2020-03-03

更新于

2020-03-03

许可协议