杰瑞科技汇

Java POI如何高效操作Excel?

目录

  1. Apache POI 简介
  2. 环境搭建
  3. 核心概念:XSSFWorkbook vs HSSFWorkbook
  4. 基本操作(读取和写入)
    • 1 创建 Excel 文件
    • 2 读取 Excel 文件
    • 3 修改 Excel 文件
  5. 高级操作
    • 1 样式设置(字体、颜色、边框、对齐)
    • 2 合并单元格
    • 3 列宽和行高
    • 4 处理日期
    • 5 处理公式
  6. 大文件处理(SXSSF)
  7. 最佳实践与注意事项
  8. 完整示例代码

Apache POI 简介

Apache POI (Poor Obfuscation Implementation) 是一个开源的 Java 库,由 Apache 软件基金会维护,它提供了 API,允许 Java 程序读取和写入 Microsoft Office 格式的文件,如 .xls (Excel 97-2003) 和 .xlsx (Excel 2007+)。

Java POI如何高效操作Excel?-图1
(图片来源网络,侵删)
  • HSSF: 操作 .xls 格式,属于旧版格式,行数限制为 65,536 行,列数为 256 列。
  • XSSF: 操作 .xlsx 格式,属于新版格式,理论上只受限于内存大小。
  • SXSSF: 基于 XSSF 的大文件流式 API,用于处理非常大的 Excel 文件,以减少内存消耗。
  • HWPF: 操作 .doc 格式。
  • XWPF: 操作 .docx 格式。

强烈建议: 除非有特殊兼容性要求,否则请优先使用 .xlsx 格式(XSSF),因为它功能更强大、性能更好。


环境搭建

你需要在你的项目中添加 POI 的依赖,如果你使用 Maven,在 pom.xml 中添加以下依赖:

<!-- 使用最新的稳定版本,请访问 Maven Central 查找最新版本 -->
<dependencies>
    <!-- 核心 POI 库 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>5.2.5</version>
    </dependency>
    <!-- 用于操作 .xlsx 格式 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.5</version>
    </dependency>
    <!-- 如果需要处理 XML,可能需要这个 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml-lite</artifactId>
        <version>5.2.5</version>
    </dependency>
</dependencies>

如果你使用 Gradle,在 build.gradle 中添加:

implementation 'org.apache.poi:poi:5.2.5'
implementation 'org.apache.poi:poi-ooxml:5.2.5'

核心概念:XSSFWorkbook vs HSSFWorkbook

这是使用 POI 时第一个需要做出的重要选择。

Java POI如何高效操作Excel?-图2
(图片来源网络,侵删)
特性 HSSFWorkbook XSSFWorkbook
文件格式 .xls (BIFF 格式) .xlsx (OOXML 格式)
版本 Excel 97-2003 Excel 2007 及更高版本
内存占用 所有数据加载到内存 所有数据加载到内存
行/列限制 65,536 行 / 256 列 1,048,576 行 / 16,384 列
性能 相对较快 相对较慢(文件结构更复杂)
功能 功能有限 功能丰富(支持更多样式、图表等)

如何选择?

  • 如果你的用户可能还在使用非常古老的 Excel 版本(2003或更早),或者生成的文件必须兼容这些旧版本,使用 HSSFWorkbook
  • 对于所有新项目,强烈推荐使用 XSSFWorkbook

基本操作

1 创建 Excel 文件

下面是一个创建 .xlsx 文件并写入数据的完整示例。

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileOutputStream;
import java.io.IOException;
public class CreateExcelExample {
    public static void main(String[] args) {
        // 1. 创建一个 XSSFWorkbook 对象,代表一个 Excel 工作簿
        try (Workbook workbook = new XSSFWorkbook()) {
            // 2. 创建一个工作表
            Sheet sheet = workbook.createSheet("员工信息");
            // 3. 创建行 (从0开始)
            Row headerRow = sheet.createRow(0);
            // 4. 创建单元格并设置值
            Cell headerCell1 = headerRow.createCell(0);
            headerCell1.setCellValue("姓名");
            Cell headerCell2 = headerRow.createCell(1);
            headerCell2.setCellValue("年龄");
            Cell headerCell3 = headerRow.createCell(2);
            headerCell3.setCellValue("部门");
            // 5. 创建数据行
            Row dataRow1 = sheet.createRow(1);
            dataRow1.createCell(0).setCellValue("张三");
            dataRow1.createCell(1).setCellValue(28);
            dataRow1.createCell(2).setCellValue("技术部");
            Row dataRow2 = sheet.createRow(2);
            dataRow2.createCell(0).setCellValue("李四");
            dataRow2.createCell(1).setCellValue(32);
            dataRow2.createCell(2).setCellValue("市场部");
            // 6. 写入文件输出流
            try (FileOutputStream fileOut = new FileOutputStream("employees.xlsx")) {
                workbook.write(fileOut);
                System.out.println("Excel 文件 'employees.xlsx' 创建成功!");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2 读取 Excel 文件

读取文件是创建文件的逆过程。

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.IOException;
public class ReadExcelExample {
    public static void main(String[] args) {
        String filePath = "employees.xlsx";
        try (FileInputStream fileIn = new FileInputStream(filePath);
             Workbook workbook = new XSSFWorkbook(fileIn)) {
            // 1. 获取第一个工作表
            Sheet sheet = workbook.getSheetAt(0); // 或者 getSheet("员工信息")
            // 2. 遍历行 (从0开始,跳过标题行)
            for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row == null) {
                    continue; // 跳过空行
                }
                // 3. 获取单元格并读取值
                // getCell(0) 是第一列,getCell(1) 是第二列
                Cell nameCell = row.getCell(0);
                Cell ageCell = row.getCell(1);
                Cell deptCell = row.getCell(2);
                // 处理单元格类型,避免 getCellValue() 返回类型不确定的问题
                String name = nameCell.getStringCellValue();
                int age = (int) ageCell.getNumericCellValue(); // 年龄是数字
                String department = deptCell.getStringCellValue();
                System.out.println("姓名: " + name + ", 年龄: " + age + ", 部门: " + department);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3 修改 Excel 文件

修改的本质是“读取 -> 修改 -> 写入”。注意: 直接在原文件上修改通常不可靠,最好先读取到内存,修改后写出到一个新文件。

Java POI如何高效操作Excel?-图3
(图片来源网络,侵删)
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ModifyExcelExample {
    public static void main(String[] args) {
        String inputFilePath = "employees.xlsx";
        String outputFilePath = "employees_modified.xlsx";
        try (FileInputStream fileIn = new FileInputStream(inputFilePath);
             Workbook workbook = new XSSFWorkbook(fileIn);
             FileOutputStream fileOut = new FileOutputStream(outputFilePath)) {
            Sheet sheet = workbook.getSheetAt(0);
            // 修改第二行第二列的年龄
            Row rowToModify = sheet.getRow(1); // 第二行
            if (rowToModify != null) {
                Cell cellToModify = rowToModify.getCell(1); // 第二列
                if (cellToModify != null) {
                    cellToModify.setCellValue(99); // 把年龄改成99
                } else {
                    // 如果单元格不存在,可以创建一个
                    rowToModify.createCell(1).setCellValue(99);
                }
            }
            // 添加一个新行
            int lastRowNum = sheet.getLastRowNum();
            Row newRow = sheet.createRow(lastRowNum + 1);
            newRow.createCell(0).setCellValue("王五");
            newRow.createCell(1).setCellValue(25);
            newRow.createCell(2).setCellValue("人事部");
            // 将修改后的内容写入新文件
            workbook.write(fileOut);
            System.out.println("Excel 文件修改成功,已保存为 '" + outputFilePath + "'");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

高级操作

1 样式设置

样式通过 CellStyle 对象来创建和设置,然后应用到单元格。

// 在创建工作簿后
CellStyle style = workbook.createCellStyle();
// 1. 设置字体
Font font = workbook.createFont();
font.setFontName("Arial");
font.setFontHeightInPoints((short) 12);
font.setBold(true);
font.setColor(IndexedColors.RED.getIndex());
style.setFont(font);
// 2. 设置背景色
style.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 3. 设置边框
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 4. 设置对齐方式
style.setAlignment(HorizontalAlignment.CENTER); // 水平居中
style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直居中
// 应用样式到单元格
Cell cell = row.createCell(0);
cell.setCellValue("带样式的文本");
cell.setCellStyle(style);

2 合并单元格

使用 SheetaddMergedRegion 方法。

// 参数 CellRangeAddress 构造函数: (起始行, 结束行, 起始列, 结束列)
// 行和列都是从0开始索引
sheet.addMergedRegion(new CellRangeAddress(0, 2, 0, 0)); // 合并第1到第3行,第1列
// 在合并区域的第一个单元格写入内容
Row row = sheet.getRow(0);
if (row == null) {
    row = sheet.createRow(0);
}
row.createCell(0).setCellValue("合并的单元格");

3 列宽和行高

// 设置列宽 (参数: 列索引, 宽度,宽度是字符宽度的1/256)
sheet.setColumnWidth(0, 20 * 256); // 设置第一列宽度为20个字符
// 自动调整列宽 (基于内容)
// sheet.autoSizeColumn(0); // 对第一列进行自适应
// 注意:autoSizeColumn 在处理大数据量时可能很慢,建议只在少量数据或导出前调用。
// 设置行高 (参数: 行索引, 高度,高度是1/20个点)
Row row = sheet.createRow(1);
row.setHeightInPoints(30); // 设置行高为30个点

4 处理日期

POI 内部使用 double 类型来存储日期和时间(从1900年1月1日开始的天数小数),你需要使用 DataFormat 来正确显示。

// 创建一个日期单元格
CellStyle dateStyle = workbook.createCellStyle();
DataFormat format = workbook.createDataFormat();
dateStyle.setDataFormat(format.getFormat("yyyy-MM-dd"));
Row row = sheet.createRow(3);
Cell dateCell = row.createCell(0);
java.util.Date date = new java.util.Date(); // 获取当前日期
dateCell.setCellValue(date);
dateCell.setCellStyle(dateStyle);

5 处理公式

只需将公式字符串设置到单元格即可。

Cell formulaCell = row.createCell(4);
formulaCell.setCellFormula("SUM(B1:B2)"); // 计算B1和B2的和

大文件处理(SXSSF)

当处理超过 10,000 行数据时,使用 XSSFWorkbook 会导致内存溢出(OOM)。SXSSF (Streaming Usermodel API) 通过将部分数据写入临时文件来解决此问题,从而显著降低内存使用。

核心特点:

  • 低内存消耗:只保留当前可见行(window size)在内存中。
  • 只写SXSSF 是一个只写 API,不支持随机读取和修改。
  • 临时文件:数据会先写入一个临时文件,最后再合并到最终文件中,需要清理临时文件。
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.FileOutputStream;
import java.io.IOException;
public class LargeFileExample {
    public static void main(String[] args) {
        // windowSize 是内存中保留的行数,默认为100
        try (Workbook workbook = new SXSSFWorkbook(100)) { 
            Sheet sheet = workbook.createSheet("大数据");
            // 写入10万行数据
            for (int i = 0; i < 100000; i++) {
                Row row = sheet.createRow(i);
                row.createCell(0).setCellValue("数据 " + i);
                row.createCell(1).setCellValue(i * 10);
            }
            // 写入文件
            try (FileOutputStream out = new FileOutputStream("large_data.xlsx")) {
                workbook.write(out);
            }
            // 重要:清理由 SXSSF 创建的临时文件
            ((SXSSFWorkbook) workbook).dispose();
            System.out.println("大文件生成完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

最佳实践与注意事项

  1. 使用 try-with-resourcesWorkbook, Sheet, Row, FileInputStream, FileOutputStream 等资源都应该放在 try-with-resources 语句块中,以确保它们被正确关闭,防止内存泄漏。
  2. 优先选择 .xlsx:除非有兼容性要求,否则总是使用 XSSFWorkbook
  3. 处理大数据量用 SXSSF:如果行数超过 5 万,务必考虑使用 SXSSFWorkbook
  4. 不要在循环中创建样式:样式对象创建开销较大,应该在循环外部创建好样式,然后在循环中重复使用。
  5. 单元格类型安全:读取单元格时,使用 getCellType() 判断类型,然后调用对应的 getXXXCellValue() 方法,避免 ClassCastException
  6. 注意日期格式:日期在 Excel 中是特殊类型,务必使用 DataFormat 来设置显示格式。
  7. 异常处理:文件操作可能会抛出 IOException,务必进行捕获和处理。

完整示例代码

这是一个综合了读取、样式设置、合并单元格、自动调整列宽和写入的完整示例。

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileOutputStream;
import java.io.IOException;
public class ComprehensiveExample {
    public static void main(String[] args) {
        String filePath = "comprehensive_report.xlsx";
        try (Workbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet("销售报表");
            // 1. 创建标题样式
            CellStyle titleStyle = workbook.createCellStyle();
            Font titleFont = workbook.createFont();
            titleFont.setBold(true);
            titleFont.setFontHeightInPoints((short) 16);
            titleStyle.setFont(titleFont);
            titleStyle.setAlignment(HorizontalAlignment.CENTER);
            // 2. 创建表头样式
            CellStyle headerStyle = workbook.createCellStyle();
            Font headerFont = workbook.createFont();
            headerFont.setBold(true);
            headerStyle.setFont(headerFont);
            headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            headerStyle.setBorderTop(BorderStyle.THIN);
            headerStyle.setBorderBottom(BorderStyle.THIN);
            headerStyle.setBorderLeft(BorderStyle.THIN);
            headerStyle.setBorderRight(BorderStyle.THIN);
            // 3. 创建数据样式
            CellStyle dataStyle = workbook.createCellStyle();
            dataStyle.setBorderTop(BorderStyle.THIN);
            dataStyle.setBorderBottom(BorderStyle.THIN);
            dataStyle.setBorderLeft(BorderStyle.THIN);
            dataStyle.setBorderRight(BorderStyle.THIN);
            dataStyle.setAlignment(HorizontalAlignment.RIGHT);
            // 4. 创建合并的标题行
            Row titleRow = sheet.createRow(0);
            titleRow.createCell(0).setCellValue("2025年第四季度销售报告");
            titleRow.getCell(0).setCellStyle(titleStyle);
            sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 3)); // 合并第一行的5列
            // 5. 创建表头
            String[] headers = {"产品名称", "销售数量", "单价", "总销售额"};
            Row headerRow = sheet.createRow(1);
            for (int i = 0; i < headers.length; i++) {
                Cell cell = headerRow.createCell(i);
                cell.setCellValue(headers[i]);
                cell.setCellStyle(headerStyle);
            }
            // 6. 创建数据
            Object[][] data = {
                    {"笔记本电脑", 120, 5999.99},
                    {"无线鼠标", 350, 99.50},
                    {"机械键盘", 80, 459.00},
                    {"显示器", 45, 1299.00}
            };
            int rowNum = 2;
            for (Object[] rowData : data) {
                Row row = sheet.createRow(rowNum++);
                for (int i = 0; i < rowData.length; i++) {
                    Cell cell = row.createCell(i);
                    if (rowData[i] instanceof String) {
                        cell.setCellValue((String) rowData[i]);
                    } else if (rowData[i] instanceof Integer) {
                        cell.setCellValue((Integer) rowData[i]);
                    } else if (rowData[i] instanceof Double) {
                        cell.setCellValue((Double) rowData[i]);
                    }
                    cell.setCellStyle(dataStyle);
                }
                // 计算总销售额
                Cell totalSalesCell = row.createCell(3);
                totalSalesCell.setCellFormula("B" + (rowNum) + "*C" + (rowNum));
                totalSalesCell.setCellStyle(dataStyle);
            }
            // 7. 自动调整列宽
            for (int i = 0; i < headers.length; i++) {
                sheet.autoSizeColumn(i);
            }
            // 8. 写入文件
            try (FileOutputStream fileOut = new FileOutputStream(filePath)) {
                workbook.write(fileOut);
            }
            System.out.println("综合示例 Excel 文件 '" + filePath + "' 创建成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
分享:
扫描分享到社交APP
上一篇
下一篇