
1. 问题背景:HashMap的无序性
在java中,java.util.hashmap是一种常用的键值对存储结构。然而,hashmap的内部实现是基于哈希表,它不保证元素的迭代顺序。这意味着当你将excel表格中的列名和值存入hashmap时,即使你按照从左到右的顺序插入,hashmap在迭代时也可能以任意顺序返回这些键值对。这对于需要严格保持列顺序的场景(如将数据写回excel或按原顺序处理数据)来说,是一个显著的问题。
例如,一个Excel表格的列顺序是 column1, column2:
column1 column2 value1 value2 value3 value4
如果使用HashMap存储,得到的Map可能呈现如下无序状态:
0 = "column2" -> value2
"column1" -> value1
1 = "column2" -> value4
"column1" -> value3这与我们期望的 column1 -> value1, column2 -> value2 的顺序不符。
2. 解决方案:使用LinkedHashMap保持插入顺序
为了解决HashMap的无序性问题,Java提供了java.util.LinkedHashMap。LinkedHashMap继承自HashMap,并额外维护了一个双向链表,用于记录元素的插入顺序。因此,当你遍历LinkedHashMap时,它会按照键值对被插入的顺序返回它们。这正是我们读取Excel数据并希望保持列顺序所需的特性。
立即学习“Java免费学习笔记(深入)”;
Apache Superset 是一个广泛采用的开源 BI 平台,用于 SQL 探索、图表构建和仪表板交付。当代理需要查询仓库数据、组装仪表板或使用成熟的分析界面解释指标而不是临时笔记本代码时,此技能非常有用。
3. 代码实现与优化
以下是修改后的readExcelSheet方法,它将HashMap替换为LinkedHashMap,以确保列的顺序得到保留。
import org.apache.poi.ss.usermodel.*;
import java.util.*;
public class ExcelReaderUtil {
/**
* 从Excel工作表中读取数据,并以有序的List<Map<String, String>>形式返回。
* 每个Map代表一行数据,Map中的键值对顺序与Excel列的插入顺序一致。
*
* @param sheet 要读取的Excel工作表对象
* @return 包含Excel数据的List,如果工作表为空则返回空列表
*/
public static List<Map<String, String>> readExcelSheet(Sheet sheet) {
// 获取行的迭代器
Iterator<Row> rows = sheet.iterator();
// 如果没有行,则返回空列表
if (!rows.hasNext()) {
return Collections.emptyList();
}
// 读取表头(第一行)作为Map的键
Row header = rows.next();
List<String> keys = new ArrayList<>();
// 遍历表头单元格,获取列名
for (Cell cell : header) {
String value = getCellValueAsString(cell); // 使用辅助方法获取单元格值
if (!value.isEmpty()) {
keys.add(value);
} else {
// 遇到空列名时,可以根据实际需求选择跳出或继续
// 这里选择跳出,认为后续列可能不再是有效表头
break;
}
}
// 初始化结果列表
List<Map<String, String>> result = new ArrayList<>();
// 遍历剩余的每一行数据
while (rows.hasNext()) {
Row row = rows.next();
// 使用LinkedHashMap来保证列的插入顺序
Map<String, String> rowMap = new LinkedHashMap<>();
// 遍历表头键,按顺序填充当前行的数据
for (int i = 0; i < keys.size(); ++i) {
// 获取单元格,如果不存在则创建为空白单元格
Cell cell = row.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
String value = getCellValueAsString(cell); // 使用辅助方法获取单元格值
rowMap.put(keys.get(i), value);
}
// 只有当行不为空时才添加到结果列表
// 判断行是否为空:检查Map中所有值是否都为空字符串
if (!rowMap.values().stream().allMatch(String::isEmpty)) {
result.add(rowMap);
}
}
return result;
}
/**
* 辅助方法:安全地获取单元格的字符串值,处理不同类型的单元格。
*
* @param cell 单元格对象
* @return 单元格的字符串表示,如果单元格为null或空白,则返回空字符串
*/
private static String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
// 对于日期类型,需要额外处理,这里简化为数值
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString(); // 或者格式化为特定日期字符串
} else {
return String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
// 对于公式单元格,可以尝试获取计算后的值
try {
return String.valueOf(cell.getNumericCellValue()); // 尝试获取数值结果
} catch (IllegalStateException e) {
try {
return cell.getStringCellValue(); // 尝试获取字符串结果
} catch (IllegalStateException ex) {
return ""; // 无法获取值
}
}
case BLANK:
return "";
default:
return ""; // 默认返回空字符串
}
}
// 示例用法 (需要Apache POI库)
public static void main(String[] args) throws Exception {
// 假设有一个名为 "example.xlsx" 的Excel文件
// 创建一个模拟的Workbook和Sheet用于测试
Workbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook();
Sheet sheet = workbook.createSheet("Sheet1");
// 创建表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("column1");
headerRow.createCell(1).setCellValue("column2");
headerRow.createCell(2).setCellValue("column3"); // 增加一列测试
// 创建数据行1
Row dataRow1 = sheet.createRow(1);
dataRow1.createCell(0).setCellValue("value1");
dataRow1.createCell(1).setCellValue("value2");
dataRow1.createCell(2).setCellValue(123); // 测试数值类型
// 创建数据行2
Row dataRow2 = sheet.createRow(2);
dataRow2.createCell(0).setCellValue("value3");
dataRow2.createCell(1).setCellValue("value4");
dataRow2.createCell(2).setCellValue(true); // 测试布尔类型
// 创建空行(应被过滤)
sheet.createRow(3);
// 调用读取方法
List<Map<String, String>> data = readExcelSheet(sheet);
// 打印结果,观察列顺序
for (Map<String, String> rowMap : data) {
System.out.println("--- Row ---");
rowMap.forEach((key, value) -> System.out.println(" " + key + " -> " + value));
}
workbook.close();
}
}代码改进说明:
- LinkedHashMap的使用: 最核心的改动是将 Map<String, String> rowMap = new HashMap<>(); 替换为 Map<String, String> rowMap = new LinkedHashMap<>();。这确保了在将列名和值放入rowMap时,它们会按照插入的顺序(即Excel中从左到右的列顺序)进行存储。
- getCellValueAsString辅助方法: 原始代码中直接使用 cell.toString() 来获取单元格值,这可能导致非字符串类型的单元格(如数值、日期、布尔值)在转换时出现问题或不符合预期。新增的getCellValueAsString辅助方法根据单元格类型安全地获取其字符串表示,提高了代码的健壮性。
- 空行判断优化: if (!rowMap.values().stream().allMatch(String::isEmpty)) 能够有效过滤掉所有单元格都为空的行。
4. 其他Map类型选择
除了LinkedHashMap,Java还提供了其他Map实现,它们在特定场景下也可能有用:
- TreeMap: TreeMap实现了SortedMap接口,它会根据键的自然顺序(或自定义的比较器)对键进行排序。如果你希望Excel列数据按照列名的字母顺序(或数字顺序)而不是原始插入顺序进行存储,那么TreeMap可能是一个选择。然而,对于保留原始Excel列顺序的需求,LinkedHashMap是更直接和合适的方案。
5. 注意事项与总结
-
依赖管理: 上述代码使用了Apache POI库来处理Excel文件。请确保你的项目中已添加相应的Maven或Gradle依赖,例如:
<!-- Maven --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.3</version> <!-- 使用最新稳定版本 --> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.3</version> <!-- 处理.xlsx文件需要此依赖 --> </dependency> - 单元格类型处理: getCellValueAsString方法提供了一个基本的单元格类型处理示例。在实际应用中,你可能需要更精细地处理日期格式、公式求值错误等情况,以满足具体的业务需求。
- 性能考量: 对于非常大的Excel文件,一次性将所有数据加载到内存中的List<Map>可能会消耗大量内存。在这种情况下,可以考虑流式处理数据,或者分批处理。
- 空列名处理: 示例代码在遇到空列名时会break跳出表头读取。如果你的Excel文件可能存在中间有空列名但后续仍有有效列名的情况,你可能需要调整此逻辑,例如继续读取所有单元格直到行尾。
通过将HashMap替换为LinkedHashMap,可以有效地解决在Java中读取Excel数据时列顺序混乱的问题,确保数据在内存中保持与源文件一致的结构,从而简化后续的数据处理和回写操作。

















