同学们经过一个学期的Java学习,顺利的参加了期末考试,并取得了不错的成绩。软件工程系里安排了几位同学统计大家的成绩,由于同学们的宿舍住得比较分散,负责统计分数的同学记录下了每个宿舍同学的班级、姓名和分数并记录在一个文本文件上,每位同学的班级、姓名和分数记录为一行,以单个空格分隔。几位同学统计完成后所有的文件放在了D:\javatest
目录下,该文件夹下只有分数统计文件,文件名为class1.txt、class2.txt、…,文件数量不超过9个,具体数量不确定。
其实背景是狗子我要做期末作业了~
设计并开发一个工程项目,统计各班的平均分,并将班级和对应的平均分依次存入名为test数据库中名为classScore的表中。classScore表包含两个字段,分别为班级名和平均分,一共有4个班级,名称分别为“软件1班”、“软件2班”、“软件3班”、“软件4班”,平均分保留两位小数。
类名 | 描述 |
---|---|
UserInterface |
图形用户界面,用于接收文件存放在目录,以及整个年级的平均分显示。该图形用户界面实现了事件处理,可以响应用户的操作。 |
DirectoryHandler |
用于接收用户界面传入的目录,通过文件相关操作循环处理该目录下的全部txt文件。DirectoryHandler 类包含一个名为directoryHandler 的方法。 |
fileHandler |
用于处理文件中的数据。 |
DatabaseHandler |
用于将被directoryHandler() 方法和databaseHandler() 处理过的数据存放到数据库中. |
Student |
用于封装学生考试成绩表中的基本信息 |
JdbcUtil |
用于配置数据库 |
现有以下需要注意的点:
到MySQL驱动官网下载对应的驱动 – > 传送门,这里我下载的是mysql-connector-java-5.1.47.jar,大家可以参考一下。
对应的MySQL安装教程可观看往期博客:MySQL-V5.7 压缩包版安装教程
在下载之后直接复制到IDEA中,建议根目录下新建一个lib文件夹,再将驱动复制进去,最后需要在该文件夹鼠标右击,选择Add as Library。这里需要注意的是直接将下载的文件复制进去不需要进行其余操作。
本类是用于封装学生考试成绩表中的基本信息,其主要封装了学生的名字、班级以及成绩,同时具有下列方法
get/set
方法;equals()/hashCode()/toString()
方法。/* 省略了上述方法 */
private String className; // 班级名称
private String stuName; //学生名字
private double grade; //学生成绩
本类搭建了基本的图形用户界面,用于接收文件存放在目录,以及整个年级的平均分显示。该图形用户界面实现了事件处理,可以响应用户的操作,而在这里主要使用了Swing
来编写界面,因此在该类中继承的是JFrame
类。
private JTextField filePathChoose = null; // 文件夹所在路径的文本框
private JTextField resultText = null; // 结果输出框
在上面的图片中我们可以有一个很明确的思路,就是从上到下的内容分别用四个容器进行存放,再将四个容器添加到窗体中即可。图片自己画的多少有点丑(๑´ㅂ`๑)
// 用于存放选择文件提示语的文本框并左对齐
JPanel pathTextJPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 15));
// 用于存放选择文件的组件并居中
JPanel filePathJPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
// 用于存放显示结果的文本框并左对齐
JPanel resultTextJPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 15));
// 用于存放确认按钮并居中
JPanel confirmButtonJPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 5));
在第一个容器中我们可以看出存放的是一段文字,因此我们这里可以使用文本框JTextField
或者一个标签JLabel
进行编写,需要注意的是,如果使用的是文本框的话需要将其设置文本框不可写以及去除文本边框。
// 定义存放选择文件提示语的文本框
JTextField pathText = new JTextField("请输入你要解析的文件所在的路径:");
// 设置文本框不可写
pathText.setEditable(false);
// 取消文本边框
pathText.setBorder(null);
在第二个容器中存放的是一个文本框,在要求中这里是需要我们输入文件路径从而读取该路径下的数据,因此可以使用文本框JTextField
// 定义文本框
filePathChoose = new JTextField();
// 设置大小
filePathChoose.setPreferredSize(new Dimension(300,25));
// 设置边框
filePathChoose.setBorder(new MatteBorder(2, 2, 1, 1, Color.DARK_GRAY));
But,在编写测试的过程中,狗子我觉得每次都要手动输入这个路径是不是有点不够智能==(ΘへΘ),因此我在这个基础上进行了改进添加了一个文件选择器在文本框的旁边,这样是不是就高级多了呢┐( ‾᷅㉨‾᷅ )┌==
// 定义存放选择文件的组件
filePathChoose = new JTextField();
filePathChoose.setPreferredSize(new Dimension(300,25));
filePathChoose.setBorder(new MatteBorder(2, 2, 1, 1, Color.DARK_GRAY));
// 定义打开文件夹的组件
JButton jbtOpen = new JButton("打开文件夹");
// 使用匿名内部类为“打开文件夹”按钮添加事件
jbtOpen.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 定义一个文件选择器
JFileChooser jFileChooser = new JFileChooser();
// 获得FileSystemView的一个实例
FileSystemView fileSystemView = FileSystemView.getFileSystemView();
// 获取桌面的路径并将其设置为打开文件选择器的默认路径
jFileChooser.setCurrentDirectory(fileSystemView.getHomeDirectory()); // 设置只能选择文件夹
jFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
/*
若showOpenDialog中参数为null,则打开的对话框处于电脑显示屏的中央
若showOpenDialog中参数为this,则打开的对话框处于编写的程序屏幕中央
showOpenDialog调用之后会返回一个值
1. 返回0,代表已经选择了某个文件或文件夹
2. 返回1,代表选择了取消按钮或直接关闭了窗口
3. JFileChooser.APPROVE_OPTION为一个常量,代表着情况1
*/
if (jFileChooser.showOpenDialog(userInterface.this) == JFileChooser.APPROVE_OPTION) {
// 获取已经选中的文件夹
File selectedFile = jFileChooser.getSelectedFile();
// 将选中的文件的绝对路径同步到文本框中
filePathChoose.setText(selectedFile.getAbsolutePath());
}
}
});
第三个容器和第一个容器相差无几,同样需要设置不可写和去除边框
// 定义存放统计结果的的文本框
resultText = new JTextField("统计结果为:", 20);
// 设置文本框不可写
resultText.setEditable(false);
// 取消文本边框
resultText.setBorder(null);
第四个容器即最后一个容器是一个JButton按钮,并对按钮绑定点击事件,在这里使用的是匿名内部类的方式。由于在这里我们还没有数据进行读取处理,因此这里暂时是 null
// 定义确定按钮
JButton confirmButton = new JButton("确定");
// 通过构件设置按钮的大小
confirmButton.setPreferredSize(new Dimension(150,30));
// 绑定点击事件进行计算
confirmButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Double result = null;
System.out.println("统计结果为:" + result);
}
});
在编写完成上述四个容器内的组件后,便需要将其分别添加到对应的容器中去了
/* 分别将四个组件添加到JPanel容器中 */
pathTextJPanel.add(pathText); // 选择文件提示语
filePathJPanel.add(filePathChoose); // 文件选择组件
filePathJPanel.add(jbtOpen); // 文件夹选择器
resultTextJPanel.add(resultText); // 统计结果文本框
confirmButtonJPanel.add(confirmButton); // 确认按钮
获取窗体容器并将四个容器添加到其中,同时设置窗体的对应属性
// 分别将四个组件添加到JPanel容器中
pathTextJPanel.add(pathText); // 选择文件提示语
filePathJPanel.add(filePathChoose); // 文件选择组件
filePathJPanel.add(jbtOpen); // 文件夹选择器
resultTextJPanel.add(resultText); // 统计结果文本框
confirmButtonJPanel.add(confirmButton); // 确认按钮
// 获取一个容器并设置其布局管理器
Container container = this.getContentPane();
container.setLayout(new GridLayout(4, 1));
// 分别将四个JPanel容器添加到container容器中
container.add(pathTextJPanel); // 添加存放选择文件提示语的文本框的JPanel容器
container.add(filePathJPanel); // 添加存放选择文件的组件的JPanel容器
container.add(resultTextJPanel); // 添加存放显示结果的文本框的JPanel容器
container.add(confirmButtonJPanel); // 添加存放确认按钮的JPanel容器
// 设置窗体大小并固定、默认关闭方式及可视化
this.setResizable(false);
this.setBounds(500, 300, 450, 230);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setVisible(true);
好了,到这里userInterface
类就告辞一段落了,下面是编写的界面效果图。
本类用于接收用户界面传入的目录,通过文件相关操作循环处理该目录下的全部txt文件。
由于班级数目是给定的,并且txt文件的结构如上图所示,因此定义的集合的键值类型分别为String-->Double
和String-->Integer
static final int CLASS_NUMBER = 4; // 年级中班级的总数
private int allClassStudentCount = 0; // 所有班级成员的个人
private double allClassStudentScore = 0.0; // 整个年级的成绩总和
private File directoryFile = null; // 存放目标目录
private HashMap<String, Double> hmClassScore = null; // 用于存放班级对应的成绩
private HashMap<String, Integer> hmClassMemberNum = null; // 用于存放班级对应的成员人数
方法名 | 方法描述 |
---|---|
directoryHandler() |
对txt文件进行处理,并通过调用DatabaseHandler类 中的databaseHandler() 方法将数据存放到数据库中 |
在方法的开始,我们应该对前面定义的部分变量进行赋值和实例化,并对集合进行初始化,使得变量的初始值是确定的
// 新建文件夹对象
directoryFile = new File(directory);
// 文件夹下的文件形成的数组
File[] files = directoryFile.listFiles();
if (files == null) {
return null;
}
// 用于存放每个班级对应的平均分
hmClassScore = new HashMap<String, Double>(CLASS_NUMBER);
// 用于存放每个班级对应的人数
hmClassMemberNum = new HashMap<String, Integer>(CLASS_NUMBER);
// 利用数组进行循环初始化集合
String[] allClass = {"软件1班", "软件2班", "软件3班", "软件4班"};
for (int i = 0; i < CLASS_NUMBER; i++) {
// 设置每个班的初始成绩是0
hmClassScore.put(allClass[i], 0.0);
// 设置每个班的初始人数是0
hmClassMemberNum.put(allClass[i], 0);
}
通过传递选中文件夹的路径参数并调用FileHandler类下的fileHandler()方法,实现数据的读取,同时更新Map集合中的数据。在此类过程中可能会存在异常抛出,因此需要对异常进行捕获处理。
for(File file : files) {
try {
// 通过逐个调用fileHandler方法获得目录下文件中的内容
List<Student> students = new FileHandler().fileHandler(file.getAbsolutePath());
if (students == null) {
new ErrorDialog("文件夹为空!").setVisible(true);
return null;
}
for (Student student : students) {
// 通过班级key对班级人数进行更新
hmClassMemberNum.put(student.getClassName(), hmClassMemberNum.get(student.getClassName()) + 1);
// 通过班级key对班级成绩进行更新
hmClassScore.put(student.getClassName(), hmClassScore.get(student.getClassName()) + student.getGrade());
allClassStudentCount++;
allClassStudentScore += student.getGrade();
}
} catch (IOException e) {
System.out.println(e.getMessage());
// 实例化错误信息提示弹窗,并返回null值
new ErrorDialog("文件打开错误!").setVisible(true);
return null;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage());
// 实例化错误信息提示弹窗,并返回null值
new ErrorDialog("文件已损坏!").setVisible(true);
return null;
} catch (NumberFormatException e) {
System.out.println(e.getMessage());
// 实例化错误信息提示弹窗,并返回null值
new ErrorDialog("文件打开错误!").setVisible(true);
return null;
}
}
调用DatabaseHandler类中的databaseHandler方法将数据存入数据库,并返回保留两位小数的平均值
// 调用DatabaseHandler类中的databaseHandler方法将数据存入数据库
new DatabaseHandler().databaseHandler(this);
// 返回保留两位小数的平均值
return Double.parseDouble(new DecimalFormat("0.00")
.format(allClassStudentScore/allClassStudentCount));
该类用于用于处理文件中的数据,其内含一个fileHandler()方法。
//BufferedReader: 用于读取数据
private BufferedReader bis = null;
//fileConcent: 用于容纳学生对象
private List<Student> fileConcent = null;
方法名 | 方法描述 | 备注 |
---|---|---|
List |
接收由文件目录和文件名组成的字符串参数,返回一个List泛型对象,List中存储的每一个Student对象就是一个学生实体对象,对应txt文件中一条学生记录。 | 存在异常IOException抛出 |
在方法的开始,同样需要对定义的变量进行实例化。这里需要注意的是在实例化BufferedReader时需要对其设置编码,避免乱码的情况出现。
// 设置编码GBK,否则可能出现编码错误
bis = new BufferedReader(new InputStreamReader(new FileInputStream(fileName), "GBK"));
fileConcent = new ArrayList<Student>();
读取文件的核心思路如下:
try {
// 存放文件中的每一行数据
String read = null;
while ((read = bis.readLine()) != null) {
// 读取到的每一行信息根据空格进行切割并放到数组中
String[] studentInfos = read.split(" ");
// 容纳班级名称的临时变量
String stuClassName = null;
// 容纳学生名字的临时变量
String stuName = null;
// 容纳学生成绩的临时变量
double stuGrade = 0;
try {
// 单条记录中学生的班级
stuClassName = studentInfos[0];
// 单条记录中学生的名字
stuName = studentInfos[1];
// 单条记录中学生的分数
stuGrade = Double.parseDouble(studentInfos[2]);
} catch (ArrayIndexOutOfBoundsException e) {
throw new NumberFormatException("请选择正确的文件");
} catch (NumberFormatException e) {
throw new NumberFormatException("请选择正确的文件");
}
// 将单条数据封装成学生对象添加到集合中
fileConcent.add(new Student(stuClassName, stuName, stuGrade));
}
} catch (FileNotFoundException e) {
throw new FileNotFoundException("文件可能已损坏!");
} finally {
try {
// 关闭文件流
if(bis != null) {
bis.close();
}
} catch (IOException e) {
throw new IOException("文件关闭出错!");
}
}
return fileConcent;
本类用于配置数据库。
private static String driver = null;
private static String url = null;
private static String username = null;
private static String password = null;
static {
try {
// 读取dp.properties文件中的配置信息
InputStream resourceAsStream = JdbcUtil.class.getClassLoader().getResourceAsStream("dp.properties");
Properties properties = new Properties();
properties.load(resourceAsStream);
// 获取数据库链接驱动
driver = properties.getProperty("driver");
// 获取数据库连接URL地址
url = properties.getProperty("url");
// 获取数据库连接用户名
username = properties.getProperty("username");
// 获取数据库连接密码
password = properties.getProperty("password");
// 加载数据库驱动
Class.forName(driver);
} catch (Exception e) {
e.printStackTrace();
}
}
方法名 | 方法描述 | 备注 |
---|---|---|
Connection getConnection() |
获取数据库连接对象获取数据库连接对象 | 存在异常SQLException 抛出 |
void release(Connection connection, Statement statement, ResultSet resultSet) |
释放资源 | 无 |
/**
* 获取数据库连接对象获取数据库连接对象
* @Author xBaozi
* @Date 16:26 2021/12/14
* @Param []
* @return java.sql.Connection
**/
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
/**
* 释放资源
* @Author xBaozi
* @Date 16:25 2021/12/14
* @Param [connection, statement, resultSet]
* @return void
**/
public static void release(Connection connection, Statement statement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
本类用于将被DirectoryHandler类处理过的数据存放到数据库中,其内含一个databaseHandler()方法。
在本项目中,狗子我将数据库的配置文件单独写了出来放在了src下的dp.properties文件,再通过工具类进行获取即可。
driver = com.mysql.jdbc.Driver
# jdbc:mysql://所连接接口名:端口号/所需要连接的数据库库名?【配置设置】
url = jdbc:mysql://localhost:3306/spms_final_exam?useUnicode=true&characterEncoding=utf8&useSSL=true
username = root
password = 123456
// 定义一个数据库连接
Connection connection = null;
// 定义一个负责执行SQL语句的preparedStatement对象
PreparedStatement preparedStatement = null;
// 定义一个结果集
ResultSet resultSet = null;
方法名 | 方法描述 |
---|---|
databaseHandler(DirectoryHandler directoryHandler) |
用于处理HashMap中的数据,将其存入数据库当中 |
在这里的思路都很统一,通过工具类获取数据库连接,循环遍历集合获取班级信息,通过SQL语句将班级信息进行插入或更新,在完成之后对资源进行释放。
// 定义decimalFormat对象用于浮点数格式控制
DecimalFormat decimalFormat = new DecimalFormat("0.00");
// 获取班级对应人数的集合
HashMap<String, Integer> hmClassMemberNum = directoryHandler.getHmClassMemberNum();
// 获取班级对应成绩的集合
HashMap<String, Double> hmClassScore = directoryHandler.getHmClassScore();
try {
// 获取一个数据库连接
connection = JdbcUtil.getConnection();
// 通过for-each循环读取集合中的数据
for (Map.Entry<String, Double> entry : hmClassScore.entrySet()) {
// 班级名字
String className = entry.getKey();
// 班级人数
int classMemberNumber = hmClassMemberNum.get(entry.getKey());
// 班级成绩
double classAvgScore = Double.parseDouble(decimalFormat
.format(entry.getValue()/classMemberNumber));
// 控制台同步输出插入信息
System.out.println("{" + className + "," +
classAvgScore + "," +
classMemberNumber + "}");
// 定义要执行的SQL语句
String sql = "INSERT INTO class_score(class_name, class_avg_score, class_menber_number) VALUES(?,?,?) ";
// 创建一个prepareStatement对象
preparedStatement = connection.prepareStatement(sql);
// 为SQL语句中的第一个?设置值
preparedStatement.setString(1, className);
// 为SQL语句中的第二个?设置值
preparedStatement.setDouble(2, classAvgScore);
// 为SQL语句中的第三个?设置值
preparedStatement.setInt(3, classMemberNumber);
int status = 0;
try {
// 执行SQL语句并返回插入的行数
status = preparedStatement.executeUpdate();
} catch (MySQLIntegrityConstraintViolationException throwables) {
// 定义要执行的SQL语句
sql = "UPDATE class_score " +
"SET class_avg_score = ?, class_menber_number = ? " +
"WHERE class_name = ?";
// 创建一个prepareStatement对象
preparedStatement = connection.prepareStatement(sql);
// 为SQL语句中的第一个?设置值
preparedStatement.setDouble(1, classAvgScore);
// 为SQL语句中的第二个?设置值
preparedStatement.setInt(2, classMemberNumber);
// 为SQL语句中的第三个?设置值
preparedStatement.setString(3, className);
status = preparedStatement.executeUpdate();
}
// 判断是否更新成功
if (status > 0) {
System.out.println("更新成功");
} else {
System.out.println("数据出现错误!");
}
}
} catch (SQLException throwables) {
throwables.printStackTrace();
new ErrorDialog("数据更新出错").setVisible(true);
} finally {
JdbcUtil.release(connection, preparedStatement, resultSet);
}
到这里就很happy的说明,已经完成了本次项目的开发,下面将会看到一系列的结果图片演示。
这个项目断断续续写了两天,但是如果专注写的话,应该一个下午左右就可以写完了(只是狗子我自己的一个速度猜测),收获还是挺多的,当然这个项目还是有很多地方可以进行改进的:
在最后,放上gitee源码仓库,需要的小伙伴可自行叉走 --> 传送门
完结撒花,走人!