Easypoi合并单元格终极指南:横向纵向全搞定(附完整代码示例)

📅 发布时间:2026/7/9 7:26:35 👁️ 浏览次数:
Easypoi合并单元格终极指南:横向纵向全搞定(附完整代码示例)
Easypoi合并单元格实战从基础到高阶的完整解决方案最近在做一个数据报表导出功能时我又一次遇到了那个熟悉的问题——Excel合并单元格。需求文档上写着相同部门合并显示、同一产品系列纵向合并而我的代码生成的Excel却是一行行重复的数据用户体验大打折扣。这让我想起了几年前第一次接触Easypoi时的情景当时为了搞定合并单元格几乎翻遍了所有能找到的资料。Easypoi作为Java生态中处理Excel的利器确实大大简化了我们的开发工作。但合并单元格这个看似简单的需求在实际操作中却有不少门道。横向合并、纵向合并、自动合并、条件合并……每种场景都需要不同的处理方式。今天我就结合自己多年的实战经验分享一套完整的Easypoi合并单元格解决方案。1. 理解Easypoi合并单元格的核心机制在深入代码之前我们需要先理解Easypoi处理合并单元格的基本原理。Easypoi底层基于Apache POI但在合并单元格的处理上做了不少封装和优化。1.1 合并单元格的两种基本类型从技术实现角度看合并单元格主要分为两种横向合并在同一行内将多个连续的单元格合并为一个。这种合并通常用于表头设计比如第一季度需要横跨1月、2月、3月三列。纵向合并在同一列内将多个连续的行合并为一个。这种合并常用于数据分组展示比如同一个部门下的多条员工记录部门名称只需要显示一次。在实际项目中我经常遇到需要同时处理这两种合并的场景。比如一个销售报表横向需要合并季度标题纵向需要合并产品类别。1.2 Easypoi的合并工具类Easypoi提供了PoiMergeCellUtil这个核心工具类来处理合并逻辑。这个类封装了大部分常用的合并操作但理解其内部机制对于处理复杂场景至关重要。// 这是PoiMergeCellUtil的核心方法签名 public static void mergeCells(Sheet sheet, int startRow, int... mergeColumns)注意mergeColumns参数接受的是列索引数组这意味着你可以一次性指定多个列进行合并操作。这个设计在处理多列相同数据合并时特别有用。1.3 合并单元格的性能考量很多人忽略的一点是合并单元格操作对性能的影响。特别是在处理大数据量的Excel文件时不当的合并策略可能导致内存溢出或导出速度缓慢。我在一个项目中遇到过这样的问题导出10万行数据时如果逐行判断是否合并程序运行时间从3秒增加到30秒。后来通过优化合并算法先对数据进行排序和分组再批量合并性能提升了10倍。2. 横向合并单元格的实战应用横向合并在实际开发中应用广泛特别是制作复杂的表头结构。下面我通过几个实际案例来展示不同的实现方式。2.1 基础横向合并固定区域合并最简单的横向合并场景是合并固定的单元格区域。比如我们需要将A1到C1这三个单元格合并为一个用于显示基本信息这个标题。// 创建Workbook对象这里假设已经通过Easypoi生成了基本的Excel结构 Workbook workbook ExcelExportUtil.exportExcel(params, entityList, dataList); // 获取第一个sheet Sheet sheet workbook.getSheetAt(0); // 创建合并区域从第0行到第0行从第0列到第2列 CellRangeAddress mergeRegion new CellRangeAddress(0, 0, 0, 2); // 添加合并区域到sheet sheet.addMergedRegion(mergeRegion); // 设置合并后单元格的内容和样式 Row row sheet.getRow(0); Cell cell row.getCell(0); cell.setCellValue(基本信息); // 设置居中显示 CellStyle style workbook.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); cell.setCellStyle(style);这里有几个关键点需要注意行列索引从0开始Excel的行列在代码中都是从0开始计数的合并后内容在左上角单元格合并后只有左上角单元格的内容和样式有效样式需要单独设置合并操作不会自动应用样式2.2 动态横向合并基于数据内容的合并更常见的需求是根据数据动态决定合并范围。比如我们有一个产品列表需要将相同类别的产品名称横向合并显示。// 假设我们有一个产品数据列表 ListProduct products getProductList(); // 先对产品按类别分组 MapString, ListProduct groupedByCategory products.stream() .collect(Collectors.groupingBy(Product::getCategory)); // 遍历每个分组进行横向合并 int currentRow 1; // 从第1行开始0是表头 for (Map.EntryString, ListProduct entry : groupedByCategory.entrySet()) { String category entry.getKey(); ListProduct sameCategoryProducts entry.getValue(); if (sameCategoryProducts.size() 1) { // 计算合并的列范围 int startCol 0; // 产品名称在第0列 int endCol 0; // 只合并名称列 // 计算合并的行范围 int startRow currentRow; int endRow currentRow sameCategoryProducts.size() - 1; // 创建合并区域 CellRangeAddress mergeRegion new CellRangeAddress( startRow, endRow, startCol, endCol ); sheet.addMergedRegion(mergeRegion); // 设置合并单元格的值 Row row sheet.getRow(startRow); Cell cell row.getCell(startCol); cell.setCellValue(category); } currentRow sameCategoryProducts.size(); }这种动态合并的关键在于先分组后合并。通过数据预处理我们可以准确知道每个分组的大小和位置。2.3 复杂表头设计多层横向合并在制作复杂报表时经常需要设计多层表头。比如一个销售报表的表头结构可能是这样的层级合并范围内容第一层A1:E12024年销售数据第二层A2:C2, D2:E2上半年, 下半年第三层A3:B3, C3:C3, D3:D3, E3:E3Q1, Q2, Q3, Q4实现这种多层表头的代码示例如下// 第一层合并年度标题 sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 4)); setCellValue(sheet, 0, 0, 2024年销售数据); // 第二层合并上下半年 sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 2)); sheet.addMergedRegion(new CellRangeAddress(1, 1, 3, 4)); setCellValue(sheet, 1, 0, 上半年); setCellValue(sheet, 1, 3, 下半年); // 第三层季度 sheet.addMergedRegion(new CellRangeAddress(2, 2, 0, 1)); sheet.addMergedRegion(new CellRangeAddress(2, 2, 2, 2)); sheet.addMergedRegion(new CellRangeAddress(2, 2, 3, 3)); sheet.addMergedRegion(new CellRangeAddress(2, 2, 4, 4)); setCellValue(sheet, 2, 0, Q1); setCellValue(sheet, 2, 2, Q2); setCellValue(sheet, 2, 3, Q3); setCellValue(sheet, 2, 4, Q4); // 辅助方法设置单元格值 private void setCellValue(Sheet sheet, int rowNum, int colNum, String value) { Row row sheet.getRow(rowNum); if (row null) { row sheet.createRow(rowNum); } Cell cell row.getCell(colNum); if (cell null) { cell row.createCell(colNum); } cell.setCellValue(value); }提示在设计复杂表头时建议先用Excel手动设计好表头结构确定每个单元格的合并范围然后再用代码实现。这样可以避免行列计算错误。3. 纵向合并单元格的高级技巧纵向合并是数据处理中最常见的需求之一。Easypoi提供了多种方式来实现纵向合并每种方式适用于不同的场景。3.1 使用PoiMergeCellUtil进行自动合并Easypoi的PoiMergeCellUtil.mergeCells()方法是最方便的自动合并工具。它会自动检测指定列中连续相同的数据并进行合并。// 首先使用Easypoi导出基础Excel ExportParams params new ExportParams(员工信息表, 员工数据); params.setType(ExcelType.HSSF); // 使用.xls格式 // 定义表头 ListExcelExportEntity entityList new ArrayList(); entityList.add(new ExcelExportEntity(部门, dept)); entityList.add(new ExcelExportEntity(姓名, name)); entityList.add(new ExcelExportEntity(职位, position)); // 准备数据 ListMapString, Object dataList getEmployeeData(); // 生成Workbook Workbook workbook ExcelExportUtil.exportExcel(params, entityList, dataList); Sheet sheet workbook.getSheetAt(0); // 对第0列部门列进行自动合并 // 参数说明sheet对象, 开始行(从0开始), 需要合并的列数组 PoiMergeCellUtil.mergeCells(sheet, 1, 0); // 从第1行开始跳过表头 // 如果需要合并多列可以传入多个列索引 int[] mergeColumns {0, 2}; // 合并部门列(0)和职位列(2) PoiMergeCellUtil.mergeCells(sheet, 1, mergeColumns);这种方法的最大优点是简单快捷特别适合处理已经按合并字段排序好的数据。但它的局限性也很明显只能合并连续相同的值。3.2 复杂场景多列关联合并在实际项目中我经常遇到这样的需求只有当多列的值都相同时才进行合并。比如员工信息表中只有部门和职位都相同的员工才合并显示。// 准备数据时先按合并条件排序 ListEmployee employees getEmployees(); employees.sort(Comparator .comparing(Employee::getDept) .thenComparing(Employee::getPosition)); // 导出Excel后进行智能合并 Workbook workbook exportToExcel(employees); Sheet sheet workbook.getSheetAt(0); // 使用Map定义复杂的合并规则 MapInteger, int[] mergeMap new HashMap(); // 定义第2行到第10行合并第0列和第2列 // 键行索引值需要合并的列数组 for (int row 2; row 10; row) { mergeMap.put(row, new int[]{0, 2}); } // 执行合并 PoiMergeCellUtil.mergeCells(sheet, mergeMap, 1);这种方式的灵活性更高但需要提前规划好合并的范围。在实际使用中我通常会写一个工具方法来动态生成这个mergeMap。3.3 性能优化大数据量下的合并策略当处理大量数据比如超过1万行时合并操作可能成为性能瓶颈。下面是我在实践中总结的优化方案方案一分批合并// 将大数据集分成多个批次处理 int batchSize 1000; int totalRows sheet.getLastRowNum(); for (int start 1; start totalRows; start batchSize) { int end Math.min(start batchSize - 1, totalRows); // 只处理当前批次的数据 MapInteger, int[] batchMergeMap calculateMergeMap(sheet, start, end); if (!batchMergeMap.isEmpty()) { PoiMergeCellUtil.mergeCells(sheet, batchMergeMap, start); } }方案二预处理数据减少合并判断// 在数据准备阶段就标记出需要合并的行 ListEmployee employees getEmployees(); ListMergeInfo mergeInfos new ArrayList(); String lastDept null; int mergeStart -1; int mergeCount 0; for (int i 0; i employees.size(); i) { Employee emp employees.get(i); if (emp.getDept().equals(lastDept)) { mergeCount; } else { if (mergeCount 1) { mergeInfos.add(new MergeInfo(mergeStart, mergeStart mergeCount - 1, 0)); } lastDept emp.getDept(); mergeStart i 1; // 1是因为Excel行从1开始且第0行是表头 mergeCount 1; } } // 导出后再根据mergeInfos进行合并 for (MergeInfo info : mergeInfos) { CellRangeAddress range new CellRangeAddress( info.getStartRow(), info.getEndRow(), info.getColumn(), info.getColumn() ); sheet.addMergedRegion(range); }注意当数据量非常大时建议先导出不合并的Excel然后在客户端或使用专门的Excel处理工具进行合并。这样可以避免服务端内存溢出。4. 横向与纵向混合合并实战最复杂的场景是同时需要横向和纵向合并。这种需求在制作矩阵式报表时特别常见。4.1 交叉报表的合并处理假设我们需要生成一个销售数据交叉报表行是产品类别列是时间维度年-季度单元格内是销售额。目标效果产品类别列需要纵向合并相同类别年度标题需要横向合并季度列季度标题各自独立// 生成基础报表数据 Workbook workbook generateSalesReport(); Sheet sheet workbook.getSheetAt(0); // 第一步处理产品类别的纵向合并 ListProductCategory categories getCategoriesGrouped(); int currentDataRow 2; // 数据从第2行开始0是主标题1是时间标题 for (ProductCategory category : categories) { int productCount category.getProductCount(); if (productCount 1) { // 纵向合并产品类别列第0列 CellRangeAddress verticalMerge new CellRangeAddress( currentDataRow, currentDataRow productCount - 1, 0, 0 ); sheet.addMergedRegion(verticalMerge); // 设置合并单元格的值和样式 Row row sheet.getRow(currentDataRow); Cell cell row.getCell(0); cell.setCellValue(category.getName()); centerAlignCell(cell); } currentDataRow productCount; } // 第二步处理时间维度的横向合并 // 假设我们有2023、2024两年每年4个季度 int startYearCol 1; // 时间数据从第1列开始 // 合并年度标题 for (int year 0; year 2; year) { int yearStartCol startYearCol (year * 4); int yearEndCol yearStartCol 3; CellRangeAddress yearMerge new CellRangeAddress(1, 1, yearStartCol, yearEndCol); sheet.addMergedRegion(yearMerge); // 设置年度标题 Cell yearCell sheet.getRow(1).getCell(yearStartCol); yearCell.setCellValue(2023 year); centerAlignCell(yearCell); } // 第三步设置季度标题不需要合并 String[] quarters {Q1, Q2, Q3, Q4}; for (int year 0; year 2; year) { for (int quarter 0; quarter 4; quarter) { int col startYearCol (year * 4) quarter; Cell quarterCell sheet.getRow(2).getCell(col); quarterCell.setCellValue(quarters[quarter]); centerAlignCell(quarterCell); } } // 样式设置辅助方法 private void centerAlignCell(Cell cell) { CellStyle style cell.getSheet().getWorkbook().createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); cell.setCellStyle(style); }4.2 动态报表合并策略在实际项目中报表结构往往是动态的。用户可能选择不同的维度组合这就需要我们实现动态的合并逻辑。我设计了一个通用的合并处理器可以应对各种动态合并需求public class DynamicMergeProcessor { private final Sheet sheet; private final ListMergeRule mergeRules; public DynamicMergeProcessor(Sheet sheet) { this.sheet sheet; this.mergeRules new ArrayList(); } // 添加合并规则 public void addRule(MergeRule rule) { mergeRules.add(rule); } // 执行所有合并规则 public void execute() { // 按优先级排序规则 mergeRules.sort(Comparator.comparingInt(MergeRule::getPriority)); for (MergeRule rule : mergeRules) { applyRule(rule); } } private void applyRule(MergeRule rule) { switch (rule.getType()) { case VERTICAL: applyVerticalMerge(rule); break; case HORIZONTAL: applyHorizontalMerge(rule); break; case CROSS: applyCrossMerge(rule); break; } } // 垂直合并实现 private void applyVerticalMerge(MergeRule rule) { // 根据规则计算合并范围 ListCellRangeAddress ranges calculateVerticalRanges(rule); for (CellRangeAddress range : ranges) { sheet.addMergedRegion(range); } } // 水平合并实现 private void applyHorizontalMerge(MergeRule rule) { // 实现逻辑类似 } // 交叉合并实现 private void applyCrossMerge(MergeRule rule) { // 组合水平和垂直合并 } } // 合并规则定义 Data class MergeRule { public enum MergeType { VERTICAL, HORIZONTAL, CROSS } private MergeType type; private int priority; // 优先级数值越小优先级越高 private int targetColumn; // 目标列 private String groupField; // 分组字段 private PredicateObject condition; // 合并条件 }使用这个处理器我们可以灵活地定义各种合并规则// 使用示例 DynamicMergeProcessor processor new DynamicMergeProcessor(sheet); // 规则1按部门纵向合并 processor.addRule(new MergeRule( MergeRule.MergeType.VERTICAL, 1, // 优先级 0, // 第0列部门列 dept, value - true // 无条件合并 )); // 规则2按职位纵向合并优先级低于部门 processor.addRule(new MergeRule( MergeRule.MergeType.VERTICAL, 2, // 优先级较低 2, // 第2列职位列 position, value - !实习生.equals(value) // 实习生不合并 )); // 执行合并 processor.execute();这种设计模式的优点是高度可配置和易于维护。当业务需求变化时只需要调整合并规则而不需要修改核心合并逻辑。5. 实战中的常见问题与解决方案在多年的Easypoi使用经验中我积累了一些处理合并单元格的实用技巧和避坑指南。5.1 合并后样式丢失问题这是最常见的问题之一合并单元格后原本的样式边框、背景色、字体等消失了。解决方案先合并后设置样式// 错误的顺序先设置样式后合并 Cell cell row.createCell(0); cell.setCellValue(测试); cell.setCellStyle(style); // 设置样式 sheet.addMergedRegion(new CellRangeAddress(0, 2, 0, 0)); // 合并后样式丢失 // 正确的顺序先合并后设置样式 sheet.addMergedRegion(new CellRangeAddress(0, 2, 0, 0)); // 先合并 Cell cell row.getCell(0); // 获取合并后的单元格 cell.setCellValue(测试); cell.setCellStyle(style); // 再设置样式更完整的样式处理方案public void mergeWithStyle(Sheet sheet, int firstRow, int lastRow, int firstCol, int lastCol, String value, CellStyle style) { // 1. 先合并 CellRangeAddress region new CellRangeAddress(firstRow, lastRow, firstCol, lastCol); sheet.addMergedRegion(region); // 2. 获取合并区域的左上角单元格 Row row sheet.getRow(firstRow); if (row null) { row sheet.createRow(firstRow); } Cell cell row.getCell(firstCol); if (cell null) { cell row.createCell(firstCol); } // 3. 设置值和样式 cell.setCellValue(value); cell.setCellStyle(style); // 4. 可选为合并区域的其他单元格设置边框 // 合并后只有左上角单元格的样式有效但边框需要单独处理 for (int r firstRow; r lastRow; r) { Row currentRow sheet.getRow(r); if (currentRow null) { currentRow sheet.createRow(r); } for (int c firstCol; c lastCol; c) { if (r firstRow c firstCol) { continue; // 跳过左上角单元格 } Cell borderCell currentRow.getCell(c); if (borderCell null) { borderCell currentRow.createCell(c); } // 只设置边框样式 CellStyle borderStyle sheet.getWorkbook().createCellStyle(); borderStyle.cloneStyleFrom(style); borderCell.setCellStyle(borderStyle); } } }5.2 合并区域重叠问题当多个合并区域有重叠时Excel会抛出异常。这在动态生成合并规则时很容易发生。检测和避免重叠的算法public class MergeRegionValidator { private final ListCellRangeAddress existingRegions new ArrayList(); public boolean canMerge(CellRangeAddress newRegion) { for (CellRangeAddress existing : existingRegions) { if (regionsOverlap(existing, newRegion)) { return false; } } return true; } public void addRegion(CellRangeAddress region) { if (canMerge(region)) { existingRegions.add(region); } else { throw new IllegalArgumentException(合并区域重叠: region.formatAsString()); } } private boolean regionsOverlap(CellRangeAddress r1, CellRangeAddress r2) { return r1.getFirstRow() r2.getLastRow() r1.getLastRow() r2.getFirstRow() r1.getFirstColumn() r2.getLastColumn() r1.getLastColumn() r2.getFirstColumn(); } // 智能合并如果发现重叠尝试调整合并范围 public CellRangeAddress findNonOverlappingMerge(int startRow, int endRow, int startCol, int endCol) { CellRangeAddress proposed new CellRangeAddress(startRow, endRow, startCol, endCol); if (canMerge(proposed)) { return proposed; } // 尝试调整只合并不重叠的部分 for (CellRangeAddress existing : existingRegions) { if (proposed.getFirstRow() existing.getLastRow() proposed.getLastRow() existing.getFirstRow()) { // 行重叠尝试调整列范围 if (startCol existing.getFirstColumn()) { endCol Math.min(endCol, existing.getFirstColumn() - 1); } else if (endCol existing.getLastColumn()) { startCol Math.max(startCol, existing.getLastColumn() 1); } } if (startCol endCol) { // 无法合并 return null; } } return new CellRangeAddress(startRow, endRow, startCol, endCol); } }5.3 性能优化批量合并操作当需要合并大量单元格时单个合并操作的性能开销会累积。下面是一个批量合并的优化方案public class BatchMergeOptimizer { public void optimizeMergeOperations(Sheet sheet, ListCellRangeAddress regions) { // 1. 按行分组合并区域 MapInteger, ListCellRangeAddress regionsByRow regions.stream() .collect(Collectors.groupingBy(CellRangeAddress::getFirstRow)); // 2. 对每行的合并区域进行优化 for (Map.EntryInteger, ListCellRangeAddress entry : regionsByRow.entrySet()) { ListCellRangeAddress rowRegions entry.getValue(); // 按列排序 rowRegions.sort(Comparator.comparingInt(CellRangeAddress::getFirstColumn)); // 3. 合并相邻的合并区域 ListCellRangeAddress mergedRegions mergeAdjacentRegions(rowRegions); // 4. 应用合并 for (CellRangeAddress region : mergedRegions) { try { sheet.addMergedRegion(region); } catch (IllegalArgumentException e) { // 处理可能的冲突 System.err.println(合并失败: region.formatAsString() , 原因: e.getMessage()); } } } } private ListCellRangeAddress mergeAdjacentRegions(ListCellRangeAddress regions) { ListCellRangeAddress result new ArrayList(); if (regions.isEmpty()) { return result; } CellRangeAddress current regions.get(0); for (int i 1; i regions.size(); i) { CellRangeAddress next regions.get(i); // 检查是否相邻且可以合并 if (canMergeAdjacent(current, next)) { current mergeTwoRegions(current, next); } else { result.add(current); current next; } } result.add(current); return result; } private boolean canMergeAdjacent(CellRangeAddress r1, CellRangeAddress r2) { // 检查是否在同一行且列相邻 return r1.getFirstRow() r2.getFirstRow() r1.getLastRow() r2.getLastRow() r1.getLastColumn() 1 r2.getFirstColumn(); } private CellRangeAddress mergeTwoRegions(CellRangeAddress r1, CellRangeAddress r2) { return new CellRangeAddress( r1.getFirstRow(), r1.getLastRow(), r1.getFirstColumn(), r2.getLastColumn() ); } }5.4 导出后的格式兼容性问题不同的Excel查看器对合并单元格的渲染可能不一致。特别是在使用旧版本Excel或在线Excel工具时可能会出现显示问题。兼容性处理建议明确指定文件格式// 对于需要最好兼容性的场景使用HSSF.xls格式 ExportParams params new ExportParams(); params.setType(ExcelType.HSSF); // 兼容性最好 // 对于需要更多功能和大数据量的场景使用XSSF.xlsx格式 params.setType(ExcelType.XSSF); // 功能更强大测试不同环境下的显示效果我通常会准备一个测试矩阵测试环境预期效果实际效果问题记录Excel 2016合并正常正常无WPS Office合并正常边框异常需要单独设置边框在线预览合并正常部分合并失效避免复杂嵌套合并手机端查看显示完整内容截断调整列宽提供格式检查工具public class ExcelFormatChecker { public static ListString checkMergeRegions(Sheet sheet) { ListString issues new ArrayList(); int mergedRegionsCount sheet.getNumMergedRegions(); for (int i 0; i mergedRegionsCount; i) { CellRangeAddress region sheet.getMergedRegion(i); // 检查1合并区域是否为空 if (isRegionEmpty(sheet, region)) { issues.add(合并区域 region.formatAsString() 为空); } // 检查2合并区域是否过大可能影响性能 int cellCount (region.getLastRow() - region.getFirstRow() 1) * (region.getLastColumn() - region.getFirstColumn() 1); if (cellCount 100) { issues.add(合并区域 region.formatAsString() 过大包含 cellCount 个单元格); } // 检查3合并区域是否重叠理论上不应该发生 for (int j i 1; j mergedRegionsCount; j) { CellRangeAddress other sheet.getMergedRegion(j); if (regionsOverlap(region, other)) { issues.add(合并区域重叠: region.formatAsString() 与 other.formatAsString()); } } } return issues; } private static boolean isRegionEmpty(Sheet sheet, CellRangeAddress region) { for (int r region.getFirstRow(); r region.getLastRow(); r) { Row row sheet.getRow(r); if (row ! null) { for (int c region.getFirstColumn(); c region.getLastColumn(); c) { Cell cell row.getCell(c); if (cell ! null cell.getCellType() ! CellType.BLANK) { return false; } } } } return true; } }5.5 内存优化技巧处理大型Excel文件时合并操作可能消耗大量内存。以下是一些优化建议使用SXSSF处理大文件// 使用流式处理避免内存溢出 ExportParams params new ExportParams(); params.setType(ExcelType.XSSF); params.setStyle(ExcelStyleConstants.NO_STYLE); // 简化样式减少内存 // 启用SXSSF流式Excel SXSSFWorkbook workbook new SXSSFWorkbook(100); // 保留100行在内存中 Sheet sheet workbook.createSheet(); // 处理数据... // 注意SXSSF对合并单元格的支持有限需要特殊处理分批处理和合并public void exportLargeDataWithMerge(ListData allData, int batchSize) { int totalBatches (int) Math.ceil((double) allData.size() / batchSize); for (int batch 0; batch totalBatches; batch) { int fromIndex batch * batchSize; int toIndex Math.min(fromIndex batchSize, allData.size()); ListData batchData allData.subList(fromIndex, toIndex); // 导出当前批次 Workbook batchWorkbook exportBatch(batchData, batch); // 合并到主文件 mergeToMainWorkbook(batchWorkbook, batch); // 及时清理 batchWorkbook.close(); } }优化样式对象创建// 避免为每个单元格创建新样式 MapString, CellStyle styleCache new HashMap(); public CellStyle getOrCreateStyle(Workbook workbook, String styleKey) { return styleCache.computeIfAbsent(styleKey, key - { CellStyle style workbook.createCellStyle(); // 根据key配置样式 configureStyle(style, key); return style; }); }处理Easypoi合并单元格时最大的体会是理解需求比实现技术更重要。在开始编码之前一定要先明确合并的业务逻辑哪些数据需要合并合并的规则是什么合并后如何影响数据的可读性有一次我为了追求完美合并实现了一个非常复杂的合并算法结果生成的报表虽然格式漂亮但业务人员反而看不懂了。后来才明白有时候适当的重复数据比过度合并更有助于数据理解。对于性能优化我的经验是不要过早优化。先确保功能正确再处理性能问题。大多数情况下Easypoi的默认性能已经足够好只有在处理真正的大数据量10万行以上时才需要考虑高级优化技巧。最后记得多测试不同环境下的显示效果。特别是如果用户可能使用不同的办公软件查看Excel一定要在WPS、在线Excel、手机端等多个平台测试合并效果。有时候在Excel 2019上显示完美的合并在WPS上可能会有边框错位的问题这时候就需要针对性地调整样式设置。