
本文详解如何修正 CSV 读取器中的 NullPointerException,并实现交易总数、借记/贷记分类统计及借记金额汇总等核心功能。
本文详解如何修正 csv 读取器中的 `nullpointerexception`,并实现交易总数、借记/贷记分类统计及借记金额汇总等核心功能。
在开发信用卡交易 CSV 解析器时,一个常见却致命的错误是在 Scanner 实例未初始化前就调用其方法。你的代码中,s 字段被声明为 private Scanner s;,但未在构造或使用前完成实例化——导致 s.nextLine() 触发 NullPointerException。根本问题在于:scanner() 方法开头即进入 while (moreLines) 循环,并立即尝试 s.nextLine(),而此时 s 仍为 null;直到循环结束后才创建 Scanner 实例,逻辑顺序完全颠倒。
✅ 正确初始化顺序:先创建 Scanner,再读取数据
应将 Scanner 初始化移至 scanner() 方法起始处,并确保文件存在且可读。以下是重构后的关键逻辑(已整合统计需求):
Miller (mlr) 是一个命令行工具,用于查询、整形和重新格式化名称索引数据,如 CSV、TSV、JSON 和 JSON Lines。它将 awk、sed、cut、join 和 sort 的功能整合到一个专为结构化数据处理而构建的单一工具中。
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class CsvReader {
private Scanner s;
private int totalTransactions = 0;
private int debitCount = 0;
private int creditCount = 0;
private double totalDebitAmount = 0.0;
public CsvReader() {
// 构造器中不直接调用 scanner(),避免过早执行
// 留待显式调用或按需初始化
}
public void analyzeTransactions() {
try {
s = new Scanner(new File("visadata.txt"));
// 跳过可能存在的表头(如第一行为列名)
if (s.hasNextLine()) s.nextLine();
while (s.hasNextLine()) {
String line = s.nextLine().trim();
if (line.isEmpty()) continue;
String[] fields = getTokens(line);
if (fields.length < 5) continue; // 确保字段足够(如:卡号,日期,类型,金额,描述)
String transactionType = fields[2].trim().toLowerCase(); // 假设第3列为交易类型
String amountStr = fields[3].trim(); // 假设第4列为金额
totalTransactions++;
try {
double amount = Double.parseDouble(amountStr);
if ("debit".equals(transactionType)) {
debitCount++;
totalDebitAmount += amount;
} else if ("credit".equals(transactionType)) {
creditCount++;
}
} catch (NumberFormatException e) {
System.err.println("警告:无效金额格式跳过行 — " + line);
}
}
printSummary();
} catch (FileNotFoundException e) {
System.err.println("❌ 错误:未找到文件 'visadata.txt',请检查路径和文件权限。");
} finally {
if (s != null) s.close(); // 必须关闭资源,防止内存泄漏
}
}
private String[] getTokens(String input) {
String[] tokens = new String[5];
for (int i = 0; i < tokens.length; i++) tokens[i] = "";
int index = 0, pointer = 0;
boolean inQuotes = false;
while (pointer < input.length()) {
char c = input.charAt(pointer);
if (c == '"') {
inQuotes = !inQuotes;
} else if (c == ',' && !inQuotes) {
index++;
if (index >= tokens.length) break;
} else {
tokens[index] += c;
}
pointer++;
}
return tokens;
}
private void printSummary() {
System.out.println("? 交易分析报告");
System.out.println("├─ 总交易数:" + totalTransactions);
System.out.println("├─ 借记交易数:" + debitCount);
System.out.println("├─ 贷记交易数:" + creditCount);
System.out.printf("└─ 借记总金额:$%.2f%n", totalDebitAmount);
}
}⚠️ 关键注意事项
- 资源管理:Scanner 必须在 finally 块中显式关闭,否则可能导致文件句柄泄漏;
- 健壮性增强:添加空行跳过、字段长度校验、金额解析异常捕获,避免单条脏数据中断整个流程;
- 字段映射假设:本示例假定 CSV 第3列为 "debit"/"credit" 类型,第4列为数值金额。实际使用时请根据真实 CSV 结构(如列名、分隔符、引号规则)调整 getTokens() 解析逻辑或改用成熟库(如 Apache Commons CSV);
- 避免静态工具类滥用:StringTokenizer 已过时,且 static 的 tokenizer 字段无实际用途,已移除;所有状态应封装在实例内;
- 构造器职责清晰:构造器仅负责对象初始化,不执行 I/O 操作,提升可测试性与灵活性。
通过以上重构,你不仅解决了 NullPointerException,还构建了一个可扩展、易维护、具备基础金融数据统计能力的 CSV 解析器。后续可轻松扩展为支持多卡类型识别、时间范围过滤或导出分析报表等功能。


















