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




