- 环境准备:如何添加 POI 依赖。
- 核心概念:
Workbook,Sheet,Row,Cell的关系。 - 完整代码示例:一个可运行的 Java 程序,读取
.xlsx文件内容并打印。 - 常见操作:如何获取不同类型的单元格数据(数字、字符串、日期等)。
- 处理
.xls文件:与.xlsx的区别。 - 注意事项:内存优化、公式处理等。
环境准备 (Maven)
如果你使用 Maven,在 pom.xml 文件中添加以下依赖,POI 的模块化设计使得你可以只引入需要的部分,以减小项目体积。

对于较新的 .xlsx (OOXML) 格式,你需要 poi-ooxml 依赖:
<dependencies>
<!-- POI 核心库 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version> <!-- 建议使用较新版本 -->
</dependency>
<!-- 用于处理 .xlsx (OOXML) 格式 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- POI 依赖的 XML 处理库 -->
<dependency>
<groupId>org.apache.xmlbeans</groupId>
<artifactId>xmlbeans</artifactId>
<version>5.1.1</version>
</dependency>
<!-- 用于处理 OOXML 包中的条目 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.5</version>
</dependency>
<!-- POI 依赖的 ZIP 压缩库 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.1</version>
</dependency>
</dependencies>
注意:poi-ooxml 会自动引入 poi 和其他必需的依赖,所以如果你只处理 .xlsx 文件,理论上只加 poi-ooxml 即可,但为了清晰和兼容性,通常会把核心库也加上。
核心概念
理解 POI 的对象模型是解析 Excel 的关键,它们之间是典型的树形结构关系:
Workbook(工作簿):代表整个 Excel 文件,一个 Excel 文件就是一个Workbook对象。- 对于
.xls文件,使用HSSFWorkbook。 - 对于
.xlsx文件,使用XSSFWorkbook。
- 对于
Sheet(工作表):代表Workbook中的一个工作表,"Sheet1", "Sheet2"。Row(行):代表Sheet中的一行,行号从 0 开始。Cell(单元格):代表Row中的一个单元格,列号也从 0 开始。
关系图:
Workbook -> Sheet -> Row -> Cell

完整代码示例 (解析 .xlsx 文件)
假设我们有一个名为 data.xlsx 的文件,内容如下:
| 姓名 | 年龄 | 入职日期 | 薪资 |
|---|---|---|---|
| 张三 | 28 | 2025-01-15 | 50 |
| 李四 | 32 | 2025-05-20 | 00 |
| 王五 | 25 | 2025-03-10 | 00 |
下面是一个完整的 Java 程序来读取这个文件:
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
public class ExcelReader {
public static void main(String[] args) {
// 1. 定义要读取的文件路径
String excelFilePath = "data.xlsx";
try (// 2. 使用 try-with-resources 自动关闭流
FileInputStream fis = new FileInputStream(excelFilePath);
// 3. 根据 .xlsx 文件创建 XSSFWorkbook 对象
Workbook workbook = new XSSFWorkbook(fis)
) {
// 4. 获取第一个工作表 (Sheet)
Sheet sheet = workbook.getSheetAt(0);
// 5. 遍历工作表中的每一行 (从第一行开始,跳过表头)
// sheet.iterator() 会获取所有行,包括空行
// sheet.lastRowNum() 获取最后一行的索引
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
Row row = sheet.getRow(rowNum);
if (row == null) {
continue; // 如果某一行是空的,则跳过
}
// 6. 获取单元格数据
// getCell(0) 获取第一列,注意列号从0开始
Cell nameCell = row.getCell(0);
Cell ageCell = row.getCell(1);
Cell dateCell = row.getCell(2);
Cell salaryCell = row.getCell(3);
// 7. 处理不同类型的单元格数据 (非常重要!)
String name = getCellValueAsString(nameCell);
int age = (int) getCellValueAsNumeric(ageCell);
String joinDate = getCellValueAsString(dateCell);
double salary = getCellValueAsNumeric(salaryCell);
// 8. 打印读取到的数据
System.out.println("姓名: " + name + ", 年龄: " + age + ", 入职日期: " + joinDate + ", 薪资: " + salary);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将单元格内容转换为字符串
* @param cell 单元格对象
* @return 单元格内容的字符串表示
*/
private static String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
// 处理日期类型的数字
if (DateUtil.isCellDateFormatted(cell)) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(cell.getDateCellValue());
} else {
// 普通数字,防止出现科学计数法
return String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
// 如果单元格包含公式,则计算公式的值
return cell.getCellFormula();
case BLANK:
return "";
default:
return "";
}
}
/**
* 将单元格内容转换为数字
* @param cell 单元格对象
* @return 单元格内容的 double 值
*/
private static double getCellValueAsNumeric(Cell cell) {
if (cell == null) {
return 0.0;
}
switch (cell.getCellType()) {
case NUMERIC:
return cell.getNumericCellValue();
case STRING:
try {
// 尝试将字符串形式的数字转为 double
return Double.parseDouble(cell.getStringCellValue().trim());
} catch (NumberFormatException e) {
return 0.0;
}
case BOOLEAN:
return cell.getBooleanCellValue() ? 1.0 : 0.0;
case FORMULA:
return cell.getNumericCellValue();
case BLANK:
return 0.0;
default:
return 0.0;
}
}
}
代码解析:
try-with-resources:FileInputStream和Workbook都实现了AutoCloseable接口,使用try-with-resources可以确保它们在使用后被自动关闭,避免资源泄漏。new XSSFWorkbook(fis):通过文件输入流创建一个XSSFWorkbook对象,这个对象代表了整个data.xlsx文件。workbook.getSheetAt(0):获取文件中的第一个工作表,索引从 0 开始。sheet.getLastRowNum():获取最后一行的索引,我们使用这个来确定循环的次数。row.getCell(0):获取指定行、指定列的单元格,列索引从 0 开始。getCellValueAsString()方法:这是最关键的部分,直接使用cell.getStringCellValue()或cell.getNumericCellValue()是不安全的,因为单元格的类型可能和你预期的不一样。cell.getCellType():返回单元格的类型,如CellType.STRING,CellType.NUMERIC,CellType.BOOLEAN等。DateUtil.isCellDateFormatted(cell):一个非常有用的工具方法,用于判断一个数字类型的单元格是否实际上是日期格式。case FORMULA:如果单元格包含公式(如=SUM(A1:A3)),cell.getCellType()会返回FORMULA,你可以调用cell.getCellFormula()获取公式字符串,或者调用cell.getNumericCellValue()/cell.getStringCellValue()来获取公式计算后的结果。
处理 .xls 文件
处理旧版的 .xls 文件与处理 .xlsx 文件几乎完全相同,唯一区别在于创建 Workbook 的方式。

将上面代码中的:
Workbook workbook = new XSSFWorkbook(fis);
替换为:
Workbook workbook = new HSSFWorkbook(fis);
其他所有逻辑(遍历 Sheet, Row, Cell 以及获取单元格内容)都完全通用。
注意事项与最佳实践
a. 内存优化 (SAX 模式)
对于非常大的 Excel 文件(超过 100MB),使用 XSSFWorkbook 或 HSSFWorkbook 会将整个文件加载到内存中,可能导致 OutOfMemoryError。
POI 提供了 SAX (Event Model) 模式来解决这个问题,它不会一次性加载整个文件,而是像解析 XML 一样,事件驱动地逐行读取。
使用 XSSF 和 SAX 模式 (op sax):
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFReader.SheetIterator;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
// ... 其他 import
public class LargeExcelReader {
public static void main(String[] args) throws Exception {
String excelFilePath = "very_large_file.xlsx";
try (OPCPackage pkg = OPCPackage.open(excelFilePath)) {
XSSFReader reader = new XSSFReader(pkg);
SharedStringsTable sst = reader.getSharedStringsTable();
XMLReader parser = XMLReaderFactory.createXMLReader();
ContentHandler handler = new SheetHandler(sst); // 需要自定义一个 ContentHandler
parser.setContentHandler(handler);
SheetIterator sheets = (SheetIterator) reader.getSheetsData();
while (sheets.hasNext()) {
System.out.println("Processing sheet: " + sheets.getSheetName());
InputSource source = new InputSource(sheets.next());
parser.parse(source);
}
}
}
}
// 你需要自己实现一个继承自 DefaultHandler 的 ContentHandler
// 来处理具体的 XML 事件 (startElement, characters, endElement 等)
// 这个过程比较复杂,但对于大文件是必须的。
class SheetHandler extends org.xml.sax.helpers.DefaultHandler {
// ... 实现细节略 ...
}
SAX 模式更复杂,但内存占用非常低,如果你的文件不大,建议还是使用简单易用的 XSSFWorkbook。
b. 处理公式单元格
如前所述,当 cell.getCellType() 返回 CellType.FORMULA 时,单元格的值是其公式的计算结果,你可以直接调用 getNumericCellValue() 或 getStringCellValue() 来获取结果,POI 会帮你计算。
c. 空单元格和合并单元格
- 空单元格:
sheet.getRow(i).getCell(j)可能会返回null,表示该单元格不存在或为空,在访问前务必检查。 - 合并单元格:POI 会将合并区域左上角的单元格作为“主单元格”,其他被合并的单元格在读取时内容为空或为 null,如果你需要知道哪些单元格被合并了,可以使用
sheet.getMergedRegions()方法获取所有合并区域。
// 获取所有合并区域
List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
for (CellRangeAddress region : mergedRegions) {
System.out.println("合并区域: " + region.formatAsString());
// region.getFirstRow(), region.getLastRow() 等
}
| 任务 | .xls (HSSF) |
.xlsx (XSSF) |
|---|---|---|
创建 Workbook |
new HSSFWorkbook(fis) |
new XSSFWorkbook(fis) |
| 获取单元格类型 | cell.getCellType() |
cell.getCellType() |
| 判断是否为日期 | HSSFDateUtil.isCellDateFormatted(cell) |
DateUtil.isCellDateFormatted(cell) |
| 处理大文件 | HSSF.eventusermodel |
XSSF.eventusermodel (SAX) |
掌握 POI 解析 Excel 的核心在于理解 Workbook-Sheet-Row-Cell 的结构,并熟练使用 getCellType() 和 DateUtil 来安全地获取不同类型的单元格数据,对于绝大多数应用场景,标准的事件驱动模型已经足够强大和高效。
