前言

在实际项目开发中,我们经常需要处理复杂的 Excel 报表。不同于标准的“第一行为列头”的表格结构,有些 Excel 文件采用了 双重表头设计

  • 首行作为列头(横向)
  • 首列作为行头(纵向)

这种结构常见于统计报表、配置表、交叉分析表等场景。例如:

Q1 Q2 Q3 Q4
北京 100 120 130 140
上海 90 110 115 135
广州 80 95 105 120

此时,我们想获取“上海”在“Q3”的销售额,就需要定位到 行头 = 上海,列头 = Q3 的交叉单元格。

本文将教你如何使用 Apache POI 结合 Map 数据结构,优雅、高效、可复用 地实现这一功能。


准备工作:引入 Apache POI

首先确保你的项目中引入了 poi-ooxml 依赖(以 Maven 为例):

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

✅ 支持 .xlsx 格式文件(推荐使用 XSSF)


核心思路:构建二维映射

我们采用 Map 嵌套结构 来存储数据:

Map<String, Map<String, String>> data = new HashMap<>();
// 外层:行头 -> 内层映射
// 内层:列头 -> 单元格值

这样就可以通过 data.get("上海").get("Q3") 直接获取目标值,简洁高效!


 完整代码实现

1. 工具类:ExcelCrossReader

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class ExcelCrossReader {

    /**
     * 读取带有双重表头的 Excel 文件,构建行列交叉映射
     *
     * @param filePath Excel 文件路径
     * @return Map<行头, Map<列头, 值>>
     * @throws IOException 文件读取异常
     */
    public static Map<String, Map<String, String>> loadCrossData(String filePath) throws IOException {
        Map<String, Map<String, String>> dataMap = new HashMap<>();

        try (FileInputStream fis = new FileInputStream(filePath);
             Workbook workbook = new XSSFWorkbook(fis)) {

            Sheet sheet = workbook.getSheetAt(0);
            Row headerRow = sheet.getRow(0); // 第一行是列头

            // 构建列头映射:列头名称 -> 列索引
            Map<String, Integer> colHeaderMap = new HashMap<>();
            for (int i = 1; i < headerRow.getLastCellNum(); i++) {
                Cell cell = headerRow.getCell(i);
                if (cell != null) {
                    colHeaderMap.put(getCellValueAsString(cell), i);
                }
            }

            // 遍历数据行(从第2行开始)
            for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
                Row row = sheet.getRow(rowIndex);
                if (row == null) continue;

                Cell rowHeaderCell = row.getCell(0);
                if (rowHeaderCell == null) continue;
                String rowHeader = getCellValueAsString(rowHeaderCell);

                Map<String, String> rowMap = new HashMap<>();
                for (Map.Entry<String, Integer> entry : colHeaderMap.entrySet()) {
                    String colHeader = entry.getKey();
                    int colIndex = entry.getValue();
                    Cell cell = row.getCell(colIndex);
                    rowMap.put(colHeader, getCellValueAsString(cell));
                }
                dataMap.put(rowHeader, rowMap);
            }
        }
        return dataMap;
    }

    /**
     * 安全获取单元格字符串值
     */
    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:
                return cell.getCellFormula();
            default:
                return "";
        }
    }

    /**
     * 获取交叉值(核心方法)
     */
    public static String getValue(Map<String, Map<String, String>> data, String rowHeader, String colHeader) {
        Map<String, String> row = data.get(rowHeader);
        return row != null ? row.get(colHeader) : null;
    }
}

2. 使用示例

public class Main {
    public static void main(String[] args) {
        try {
            String filePath = "sales.xlsx";
            Map<String, Map<String, String>> data = ExcelCrossReader.loadCrossData(filePath);

            // 查询:上海 在 Q3 的销售额
            String value = ExcelCrossReader.getValue(data, "上海", "Q3");
            System.out.println("上海 Q3 销售额:" + value); // 输出:115

            // 批量查询
            System.out.println("广州 Q4:" + ExcelCrossReader.getValue(data, "广州", "Q4"));

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

 优势总结

优势 说明
结构清晰 使用 Map<String, Map<String, String>> 映射行列,语义明确
查询高效 O(1) 时间复杂度查找交叉值
易于扩展 可封装为通用工具,支持多 Sheet、类型转换等
健壮性强 处理空值、不同数据类型、异常情况
可读性高 代码逻辑清晰,适合团队协作

进阶建议

  1. 支持多级表头?
    可以通过合并单元格判断或读取多行构建复合列头(如 "华东-上海")。

  2. 大数据量?
    使用 SXSSFWorkbookStreamingReader 避免内存溢出。

  3. 类型自动转换?
    可返回 Object 类型,并根据业务判断是 IntegerDouble 还是 String

  4. 缓存机制?
    对于频繁读取的 Excel,可将 dataMap 缓存到内存或 Redis。

 结语

通过本文,我们掌握了如何使用 Apache POI + Map 结构 优雅地处理 Excel 中的 双重表头交叉数据。这种设计不仅提升了代码的可维护性,也极大简化了数据查询逻辑。

核心思想:将表格数据“建模”为键值映射,让查找变得更像“字典查询”


 欢迎交流

如果你在使用过程中遇到问题,或有更优的实现方案,欢迎在评论区留言讨论!
也欢迎点赞、收藏、转发,让更多开发者看到这篇实用教程!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐