You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Apache POI行重排问题:交换Excel行且不破坏引用公式

解决Apache POI中行交换且保留公式引用的问题

我明白你现在的困扰——用Apache POI交换Excel行后公式引用乱了,之前试了shift方法没搞定对吧?针对你要把Sheet1的1-4行倒序成4-3-2-1的需求,咱们不能直接用shiftRows硬搬(因为多次移位会打乱行索引,公式引用也会跟着错位),得用「先备份行数据→调整公式里的行号映射→按新顺序重写行」的思路来处理,具体步骤和代码示例如下:

1. 定义行号映射关系

首先得明确原行和新行的对应关系(注意Excel行号是1-based,POI的Row对象是0-based,要做好转换):

  • 原Excel行1 → 新Excel行4(POI索引0 → 3)
  • 原Excel行2 → 新Excel行3(POI索引1 → 2)
  • 原Excel行3 → 新Excel行2(POI索引2 → 1)
  • 原Excel行4 → 新Excel行1(POI索引3 → 0)

咱们可以用两个Map分别存储Excel行号和POI索引的映射:

// Excel行号(1-based)的映射:原行 → 新行
Map<Integer, Integer> excelRowMap = new HashMap<>();
excelRowMap.put(1, 4);
excelRowMap.put(2, 3);
excelRowMap.put(3, 2);
excelRowMap.put(4, 1);

// 转换为POI行索引(0-based)的映射
Map<Integer, Integer> poiRowMap = new HashMap<>();
for (Map.Entry<Integer, Integer> entry : excelRowMap.entrySet()) {
    poiRowMap.put(entry.getKey() - 1, entry.getValue() - 1);
}

2. 备份原行的完整数据

先把需要交换的4行的所有单元格数据(包括公式、值、样式)备份到临时结构里,避免操作原工作表时数据被覆盖:

// 自定义一个CellData类存储单元格的类型、值/公式、样式
class CellData {
    private CellType cellType;
    private Object value;
    private String formula;
    private CellStyle cellStyle;

    // 省略getter和setter方法
}

XSSFSheet sheet = workbook.getSheet("Sheet1");
List<Map<Integer, CellData>> tempCellDataList = new ArrayList<>();

// 遍历原POI行索引0-3,备份每行的单元格数据
for (int originalPoiRow = 0; originalPoiRow < 4; originalPoiRow++) {
    XSSFRow row = sheet.getRow(originalPoiRow);
    Map<Integer, CellData> cellDataMap = new HashMap<>();
    if (row != null) {
        for (int cellNum = 0; cellNum < row.getLastCellNum(); cellNum++) {
            XSSFCell cell = row.getCell(cellNum);
            if (cell != null) {
                CellData data = new CellData();
                data.setCellType(cell.getCellType());
                data.setCellStyle(cell.getCellStyle());
                
                switch (cell.getCellType()) {
                    case FORMULA:
                        data.setFormula(cell.getCellFormula());
                        break;
                    case NUMERIC:
                        data.setValue(cell.getNumericCellValue());
                        break;
                    case STRING:
                        data.setValue(cell.getStringCellValue());
                        break;
                    case BOOLEAN:
                        data.setValue(cell.getBooleanCellValue());
                        break;
                    default:
                        data.setValue(cell.toString());
                }
                cellDataMap.put(cellNum, data);
            }
        }
    }
    tempCellDataList.add(cellDataMap);
}

3. 删除原有的4行

为了避免行索引混乱,要从下往上删除原行(从POI索引3到0):

for (int i = 3; i >= 0; i--) {
    XSSFRow row = sheet.getRow(i);
    if (row != null) {
        sheet.removeRow(row);
    }
}

4. 按新顺序插入行并调整公式

现在根据新的行顺序,把备份的数据写入对应位置,同时调整公式里的行号引用:

// 新行的POI索引顺序:3(原行4)、2(原行3)、1(原行2)、0(原行1)
List<Integer> newPoiRowOrder = Arrays.asList(3, 2, 1, 0);
// 构建反向映射:新POI索引 → 原POI索引
Map<Integer, Integer> reversePoiMap = new HashMap<>();
for (Map.Entry<Integer, Integer> entry : poiRowMap.entrySet()) {
    reversePoiMap.put(entry.getValue(), entry.getKey());
}

for (int newPoiRow : newPoiRowOrder) {
    XSSFRow newRow = sheet.createRow(newPoiRow);
    int originalPoiRow = reversePoiMap.get(newPoiRow);
    Map<Integer, CellData> cellDataMap = tempCellDataList.get(originalPoiRow);
    
    for (Map.Entry<Integer, CellData> entry : cellDataMap.entrySet()) {
        int cellNum = entry.getKey();
        CellData data = entry.getValue();
        XSSFCell newCell = newRow.createCell(cellNum);
        newCell.setCellStyle(data.getCellStyle());
        
        switch (data.getCellType()) {
            case FORMULA:
                // 调用工具方法调整公式里的行号
                String adjustedFormula = adjustFormulaRowReferences(data.getFormula(), excelRowMap);
                newCell.setCellFormula(adjustedFormula);
                break;
            case NUMERIC:
                newCell.setCellValue((Double) data.getValue());
                break;
            case STRING:
                newCell.setCellValue((String) data.getValue());
                break;
            case BOOLEAN:
                newCell.setCellValue((Boolean) data.getValue());
                break;
            default:
                newCell.setCellValue(data.getValue().toString());
        }
    }
}

5. 实现公式行号调整的工具方法

这个方法会用正则匹配公式里的行号,替换成映射后的新行号:

private static String adjustFormulaRowReferences(String formula, Map<Integer, Integer> rowMap) {
    // 正则匹配列字母后的数字行号(比如A1中的1,避免匹配其他无关数字)
    Pattern pattern = Pattern.compile("(?<=[A-Za-z])[0-9]+(?![0-9])");
    Matcher matcher = pattern.matcher(formula);
    
    StringBuffer sb = new StringBuffer();
    while (matcher.find()) {
        String rowStr = matcher.group();
        int originalRow = Integer.parseInt(rowStr);
        Integer newRow = rowMap.get(originalRow);
        // 如果是我们要交换的行号,就替换,否则保持原样
        matcher.appendReplacement(sb, newRow != null ? String.valueOf(newRow) : rowStr);
    }
    matcher.appendTail(sb);
    return sb.toString();
}

关键注意事项

  • 如果你的工作表有合并单元格,需要额外遍历合并区域,调整合并单元格的行范围(用sheet.getMergedRegions()获取合并区域,修改后再设置回去)。
  • 样式是工作簿共享的,直接复用原单元格样式即可,不需要重新创建。
  • 测试时要重点验证公式,比如原行2的=SUM(A1:A4)会被调整为=SUM(A4:A1),Excel会自动识别这个范围并计算正确结果。

内容的提问来源于stack exchange,提问作者madireddy

火山引擎 最新活动