
本教程详细讲解如何在 java 中读取文本文件,并解析其中的结构化数据(如学生姓名和成绩)以创建对应的 java 对象。文章对比了两种核心方法:使用 `scanner` 自定义分隔符和结合 `scanner` 逐行读取与 `string.split()`,并提供了详细的代码示例、注意事项以及如何将解析数据集成到自定义 `student` 类中。
在 Java 应用程序开发中,从外部文本文件读取并解析结构化数据是一个常见的需求。例如,我们可能需要从一个包含学生姓名和多门成绩的文本文件中提取信息,然后将其转化为 Java 对象,以便后续进行排序、分析或存储。本文将深入探讨两种主要的 Java 文件读取和数据解析策略,并指导您如何将这些数据封装到自定义的 Student 类中。
文本文件格式示例
假设我们有一个名为 main.txt 的文本文件,其内容包含学生姓名和四门课程的成绩,数据之间以逗号和空格分隔,每行代表一个学生的信息:
John Doe, 30, 25, 70, 10 Jane Doe, 33, 20, 80, 15 Christian Pulisic, 70, 60, 50, 20
我们的目标是读取这些数据,将姓名作为字符串,成绩作为整数,并最终创建 Student 对象。
方法一:使用 Scanner 自定义分隔符
java.util.Scanner 类提供了一种方便的方式来解析基本类型和字符串。通过设置自定义分隔符,我们可以让 Scanner 自动按照特定模式分割输入流中的数据。
立即学习“Java免费学习笔记(深入)”;
核心思想
此方法的核心在于 scan.useDelimiter()。我们需要定义一个正则表达式,它能匹配数据字段之间的分隔符以及行尾的换行符。
- 正确的分隔符: 观察示例文件,数据字段之间是 ", "(逗号后跟一个空格),而不是单纯的 ,。
- 处理行尾: \R 是一个特殊的正则表达式,用于匹配任何类型的行结束符(\n, \r, \r\n)。
- 组合分隔符: 使用 | (或) 运算符将多个分隔符组合起来,即 \\R|,。
代码示例
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.ArrayList;
import java.util.List;
// 假设 Student 类已定义
// class Student {
// String name;
// List grades;
// public Student(String name, List grades) {
// this.name = name;
// this.grades = grades;
// }
// @Override
// public String toString() {
// return "Name: " + name + ", Grades: " + grades;
// }
// }
public class StudentDataReaderDelimiter {
public static void main(String[] args) {
// 使用 try-with-resources 确保 Scanner 资源被正确关闭
try (Scanner scan = new Scanner(new File("main.txt"))) {
// 设置分隔符为换行符或 ", "
scan.useDelimiter("\\R|, ");
List students = new ArrayList<>();
while (scan.hasNext()) {
try {
String name = scan.next();
List grades = new ArrayList<>();
for (int i = 0; i < 4; i++) { // 假设有4门成绩
if (scan.hasNextInt()) {
grades.add(scan.nextInt());
} else {
System.err.println("Error: Expected an integer grade but found non-integer for student " + name);
// 跳过当前非整数 token,或者选择更严格的错误处理
scan.next(); // 尝试跳过错误的 token
break; // 停止读取当前学生的成绩
}
}
students.add(new Student(name, grades));
} catch (Exception e) {
System.err.println("Error processing data: " + e.getMessage());
// 可以在这里添加逻辑来跳过当前行或记录错误
// 注意:当使用 useDelimiter("\\R|, ") 时,很难精确地跳过“当前行”
}
}
// 打印解析出的学生信息
for (Student student : students) {
System.out.println(student);
}
} catch (FileNotFoundException e) {
System.err.println("Error: File not found at specified path. " + e.getMessage());
}
}
} 优缺点分析
-
优点:
- 代码简洁,一旦分隔符设置正确,Scanner 会自动处理字段的提取。
- 对于数据格式相对一致,且字段数量固定的文件,此方法非常高效。
- 在某些情况下,即使数据分布在多行,只要分隔符匹配,也能正确解析。
-
缺点:
- 数据验证不严格: 很难在读取过程中验证每个学生是否恰好有固定数量的字段。如果某行数据不完整或多余,可能会导致解析错误或将下一个学生的数据误认为当前学生的字段。
- 错误处理复杂: 当出现格式错误时,很难精确地定位到错误的行并跳过,因为 Scanner 已经将整个文件看作一个连续的 token 流。
方法二:逐行读取与 String.split() 结合
这种方法通常被认为是处理结构化文本文件更健壮和可控的方式。它将文件读取和行内解析分为两个独立步骤。
核心思想
- 逐行读取: 使用 Scanner 或 BufferedReader 逐行读取文件内容。
- 行内解析: 对每一行读取到的字符串,使用 String.split() 方法根据指定的分隔符将其分割成一个字符串数组(tokens)。
- 类型转换与验证: 遍历字符串数组,将字符串类型的成绩转换为整数,并进行必要的格式验证。
代码示例
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
// 假设 Student 类已定义
// class Student {
// String name;
// List grades;
// public Student(String name, List grades) {
// this.name = name;
// this.grades = grades;
// }
// @Override
// public String toString() {
// return "Name: " + name + ", Grades: " + grades;
// }
// }
public class StudentDataReaderSplit {
public static void main(String[] args) {
// 使用 try-with-resources 确保 Scanner 资源被正确关闭
try (Scanner scan = new Scanner(new File("main.txt"))) {
List students = new ArrayList<>();
int lineNumber = 0;
while (scan.hasNextLine()) {
lineNumber++;
String line = scan.nextLine(); // 读取整行
String[] tokens = line.split(", "); // 使用 ", " 分割行
// 验证 token 数量
if (tokens.length == 5) { // 1个姓名 + 4个成绩 = 5个 token
String name = tokens[0];
List grades = new ArrayList<>();
boolean allGradesValid = true;
for (int i = 1; i < tokens.length; i++) {
try {
grades.add(Integer.parseInt(tokens[i]));
} catch (NumberFormatException e) {
System.err.println("Error on line " + lineNumber + ": Invalid grade format for '" + tokens[i] + "'");
allGradesValid = false;
break; // 跳出当前学生成绩的解析
}
}
if (allGradesValid) {
students.add(new Student(name, grades));
}
} else {
System.err.println("Error on line " + lineNumber + ": Incorrect number of data fields. Expected 5, found " + tokens.length + ". Line: '" + line + "'");
}
}
// 打印解析出的学生信息
for (Student student : students) {
System.out.println(student);
}
} catch (FileNotFoundException e) {
System.err.println("Error: File not found at specified path. " + e.getMessage());
}
}
} 优缺点分析
-
优点:
- 强大的数据验证: 可以轻松地在每行级别进行字段数量、数据类型等验证,从而更准确地识别和处理错误。
- 清晰的错误报告: 当数据格式不正确时,可以报告具体的行号和错误信息。
- 更好的控制: 对每一行数据拥有完全的控制权,可以执行更复杂的解析逻辑。
-
缺点:
- 如果单个逻辑记录(如一个学生的所有信息)跨越多行,此方法需要额外的逻辑来拼接这些行。
构建 Student 类并集成数据
为了更好地组织和管理解析出的数据,我们应该定义一个 Student 类来封装学生姓名和成绩。
Student 类定义
import java.util.List; import java.util.ArrayList; public class Student implements Comparable{ private String name; private List grades; public Student(String name, List grades) { this.name = name; this.grades = new ArrayList<>(grades); // 深度拷贝,防止外部修改 } public String getName() { return name; } public List getGrades() { return new ArrayList<>(grades); // 返回拷贝,防止外部修改 } // 计算平均分(可选) public double getAverageGrade() { if (grades.isEmpty()) { return 0.0; } int sum = 0; for (int grade : grades) { sum += grade; } return (double) sum / grades.size(); } @Override public String toString() { return "Student [Name=" + name + ", Grades=" + grades + ", Average=" + String.format("%.2f", getAverageGrade()) + "]"; } // 实现 Comparable 接口,用于按姓名字母顺序排序 @Override public int compareTo(Student other) { return this.name.compareTo(other.name); } }
在上述的两个数据读取示例中,我们已经将解析出的姓名和成绩列表传递给了 Student 类的构造函数,并创建了 Student 对象。例如,在 StudentDataReaderSplit 类的 main 方法中:
// ... (之前的代码) ...
if (allGradesValid) {
students.add(new Student(name, grades)); // 创建 Student 对象并添加到列表中
}
// ... (之后的代码) ...将这些 Student 对象存储在一个 List
// 在解析完所有学生数据后
import java.util.Collections; // 导入 Collections 类
// ... (在 main 方法的最后) ...
Collections.sort(students); // 按姓名字母顺序排序学生列表
System.out.println("\n--- Sorted Students ---");
for (Student student : students) {
System.out.println(student);
}注意事项与最佳实践
-
资源管理: 始终确保文件读取器(如 Scanner)在使用完毕后被关闭,以释放系统资源。Java 7 引入的 try-with-resources 语句是处理此问题的最佳方式,它能自动关闭实现了 AutoCloseable 接口的资源。
try (Scanner scan = new Scanner(new File("main.txt"))) { // 文件读取和解析逻辑 } catch (FileNotFoundException e) { // 异常处理 } -
异常处理:
- FileNotFoundException: 当指定的文件路径不存在时抛出。务必捕获并处理此异常,告知用户文件缺失。
- NumberFormatException: 当尝试将非数字字符串转换为整数时抛出(例如,成绩字段包含字母)。在解析成绩时,应捕获此异常,并妥善处理(例如,跳过该条记录,或将成绩设为默认值)。
-
数据校验:
- 在解析前,检查每行数据是否包含预期的字段数量(如方法二中的 tokens.length == 5)。
- 对数值类型的数据,除了 NumberFormatException,还可以添加业务逻辑上的校验(例如,成绩是否在 0-100 之间)。
-
字符编码: 默认情况下,Scanner 使用平台的默认字符集。如果您的文本文件使用了不同的编码(例如 UTF-8),您需要在创建 Scanner 时明确指定:
Scanner scan = new Scanner(new File("main.txt"), "UTF-8"); -
性能考量:
- 对于小型到中型文件,Scanner 已经足够高效。
- 对于非常大的文件(GB 级别),考虑使用 BufferedReader 结合 String.split(),因为 BufferedReader 提供了更高效的缓冲读取机制。
总结
在 Java 中读取文本文件并解析结构化数据,主要有两种高效且常用的方法:使用 Scanner 自定义分隔符和结合 Scanner 逐行读取与 String.split()。
- Scanner 自定义分隔符 适用于数据格式非常固定且字段数量一致的场景,代码简洁。
- 逐行读取与 String.split() 结合 提供了更精细的控制和更强大的错误处理能力,尤其适用于需要严格验证每行数据格式的场景,是更推荐的通用方法。
无论选择哪种方法,都应遵循 Java 的最佳实践,包括正确的资源管理、健壮的异常处理和详尽的数据校验,以确保程序的稳定性和数据的准确性。通过将解析出的数据封装到如 Student 这样的自定义类中,可以极大地提高代码的可读性、可维护性和数据操作的便利性。










