下面我将从核心原理、常用库(Apache POI、EasyExcel)的操作方法以及常见问题三个方面,为你详细讲解 Java 如何处理 Excel 的日期格式。
核心原理:Excel 和 Java 的日期本质
理解这一点是解决所有日期问题的关键。
Excel 如何存储日期
- 底层存储:Excel 将日期和时间存储为一个双精度浮点数。
- 整数部分:代表自 1900年1月1日 以来的天数。
1代表 1900/1/2,2代表 1900/1/3,...,44562代表 2025/1/1。
- 小数部分:代表一天中的时间。
5代表中午 12:00,25代表早上 6:00。
- 著名的 Bug:Excel 的日期系统有一个著名的 bug:它错误地认为 1900年是闰年。
1900/2/29(这个日期不存在)在 Excel 中被存储为0,这在处理跨 1900 年的日期时需要注意,但现代应用通常不会涉及。
Java 如何存储日期
java.util.Date:一个传统的类,同时包含日期和时间信息,内部用一个long类型存储自 1970年1月1日 00:00:00 GMT 以来的毫秒数。java.time包 (Java 8+):现代 Java 日期时间 API,提供了更清晰、更安全的类,如LocalDate(仅日期),LocalDateTime(日期和时间),ZonedDateTime(带时区的日期时间)。
两者之间的转换
当使用 Java 库(如 Apache POI)读取 Excel 单元格时,如果单元格被格式化为日期,库会:
- 从 Excel 文件中读取那个双精度浮点数。
- 将这个浮点数转换为 Java 可以理解的日期对象。
转换的关键公式:
Java 日期 = Excel 日期值 - Excel 基准日期的天数 + Java 基准日期的毫秒数
- Excel 基准日期: 1900-01-01
- Java 基准日期: 1970-01-01
POI 等库已经帮我们完成了这个复杂的转换工作,我们只需要正确地调用 API 即可。
使用 Apache POI 处理日期格式
Apache POI 是最流行的 Java 操作 Office 文件的库。
读取 Excel 日期
核心步骤:
- 判断单元格类型是否为
CellType.NUMERIC。 - 使用
DateUtil.isCellDateFormatted(cell)方法判断这个数字是否被格式化为日期。 - 如果是,直接使用
cell.getDateCellValue()获取java.util.Date对象。 - 如果不是,但你知道它应该是日期,你需要手动进行转换(使用
DateUtil.getJavaDate())。
代码示例:
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;
import java.util.Date;
public class PoiDateReader {
public static void main(String[] args) {
String filePath = "your_excel_file.xlsx";
try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = new XSSFWorkbook(fis)) {
Sheet sheet = workbook.getSheetAt(0);
Row row = sheet.getRow(0); // 假设日期在第一行
Cell cell = row.getCell(0);
// 1. 检查单元格类型
if (cell.getCellType() == CellType.NUMERIC) {
// 2. 检查是否是格式化的日期
if (DateUtil.isCellDateFormatted(cell)) {
// 3. 直接获取 Date 对象
Date date = cell.getDateCellValue();
System.out.println("直接读取的 Date 对象: " + date);
// 4. 格式化输出为你想要的字符串格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formattedDate = sdf.format(date);
System.out.println("格式化后的日期字符串: " + formattedDate);
} else {
// 如果单元格是数字但未格式化为日期,44562
double numericValue = cell.getNumericCellValue();
System.out.println("单元格是纯数字,未格式化: " + numericValue);
// 手动将其转换为 Java 日期
Date date = DateUtil.getJavaDate(numericValue);
System.out.println("手动转换后的 Date 对象: " + date);
}
} else {
System.out.println("单元格不是数字类型,无法作为日期处理。");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
写入 Excel 日期
写入日期时,你需要做两件事:
- 设置单元格的值(
setCellValue)。 - 为单元格应用日期格式(
CellStyle)。
代码示例:
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class PoiDateWriter {
public static void main(String[] args) {
String filePath = "output_excel_file.xlsx";
// 1. 创建工作簿和工作表
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("日期示例");
// 2. 创建日期格式
CellStyle dateCellStyle = workbook.createCellStyle();
CreationHelper createHelper = workbook.getCreationHelper();
// 设置日期格式,"yyyy-MM-dd"
dateCellStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd"));
// 3. 创建行和单元格
Row row = sheet.createRow(0);
Cell cell = row.createCell(0);
// 4. 设置单元格值为当前日期
Date currentDate = new Date();
cell.setCellValue(currentDate);
// 5. 应用日期格式到单元格
cell.setCellStyle(dateCellStyle);
// 6. 写入文件
try (FileOutputStream fos = new FileOutputStream(filePath)) {
workbook.write(fos);
System.out.println("Excel 文件已成功创建,并写入了日期。");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
workbook.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用 EasyExcel 处理日期格式
EasyExcel 是阿里巴巴开源的库,以其“简单、节省内存”著称,尤其适合处理大数据量的 Excel 文件。
读取 Excel 日期
EasyExcel 的读取方式是基于模型的(Model-based),你需要定义一个与 Excel 行对应的 Java 实体类。
步骤:
- 定义实体类:在需要映射日期的字段上使用
@DateTimeFormat注解来指定 Excel 中的日期格式。 - 创建监听器:创建一个
AnalysisEventListener来处理读取到的数据。 - 读取文件:调用
EasyExcel.read()方法。
代码示例:
实体类 DateModel.java
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
public class DateModel {
// @ExcelProperty 指定列名
// @DateTimeFormat 指定 Excel 中该列的日期格式
@ExcelProperty("订单日期")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") // 必须与 Excel 文件中的格式匹配
private Date orderDate;
// Getters and Setters
public Date getOrderDate() {
return orderDate;
}
public void setOrderDate(Date orderDate) {
this.orderDate = orderDate;
}
@Override
public String toString() {
return "DateModel{" +
"orderDate=" + orderDate +
'}';
}
}
读取主程序
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import java.util.ArrayList;
import java.util.List;
public class EasyExcelDateReader {
public static void main(String[] args) {
String filePath = "your_excel_file.xlsx";
// 使用监听器来读取数据
EasyExcel.read(filePath, DateModel.class, new AnalysisEventListener<DateModel>() {
// 存储读取的数据
private List<DateModel> dataList = new ArrayList<>();
@Override
public void invoke(DateModel data, AnalysisContext context) {
// 每解析一行,就会调用一次这个方法
dataList.add(data);
System.out.println("解析到一条数据: " + data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 所有数据解析完成后的操作
System.out.println("所有数据解析完成!");
System.out.println("总共解析了 " + dataList.size() + " 条数据。");
}
}).sheet().doRead();
}
}
写入 Excel 日期
写入时,同样使用实体类,但这次用 @WriteDateTimeFormat 注解。
实体类 DateModelForWrite.java
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.WriteDateTimeFormat;
public class DateModelForWrite {
@ExcelProperty("创建时间")
// 指定写入 Excel 时的日期格式
@WriteDateTimeFormat("yyyy年MM月dd日")
private Date createTime;
// Getters and Setters
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
写入主程序
import com.alibaba.excel.EasyExcel;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class EasyExcelDateWriter {
public static void main(String[] args) {
String filePath = "output_easyexcel_file.xlsx";
// 1. 准备数据
List<DateModelForWrite> dataList = new ArrayList<>();
DateModelForWrite data1 = new DateModelForWrite();
data1.setCreateTime(new Date());
dataList.add(data1);
// 2. 写入 Excel
EasyExcel.write(filePath, DateModelForWrite.class).sheet("日期写入").doWrite(dataList);
System.out.println("EasyExcel 文件已成功创建,并写入了日期。");
}
}
常见问题与最佳实践
读取的日期是 Double 类型,而不是 Date
- 原因:最常见的原因是
DateUtil.isCellDateFormatted(cell)返回了false,这可能是因为:- Excel 单元格虽然输入了日期,但被你手动改成了“常规”或“数字”格式。
- 单元格是通过公式生成的,且公式结果没有明确设置为日期格式。
- 解决方案:
- 检查 Excel 文件,确保单元格格式被设置为“日期”。
- 在代码中,如果确定单元格应该是日期,可以强制使用
DateUtil.getJavaDate(cell.getNumericCellValue())进行转换。
读取的日期不正确(相差 4 年或 1 天)
-
原因:
- 相差 4 年:这是因为 POI 默认使用 1900 年日期系统,而你的 Excel 文件可能使用了 1904 年日期系统(在 Excel for Mac 中很常见),1904 年系统以 1904-01-01 为基准,可以避免 1900 年的闰年问题。
- 相差 1 天:通常是 1900 年闰年 bug 导致的。
-
解决方案:
- 在读取
Workbook时,明确指定日期系统。// 对于 .xls 文件 Workbook workbook = new HSSFWorkbook(fis); workbook.setMissingCellPolicy(Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); // 设置使用 1904 日期系统 workbook.set1904(false); // false 是 1900 系统,true 是 1904 系统
// 对于 .xlsx 文件 Workbook workbook = new XSSFWorkbook(fis); // XSSF 没有直接 set1904 的方法,但可以通过底层 OOXML 实现 // ((XSSFWorkbook) workbook).getProperties().getCoreProperties().setModified(new Date()); // 这不是直接方法 // 更简单的方法是检查文件属性,或者在读取前判断,然后使用 POI 的特定 API。 // 如果文件是 .xlsx,且日期不正确,多半是 1904 系统。 // 可以通过以下方式强制设置 if (workbook instanceof XSSFWorkbook) { ((XSSFWorkbook) workbook).getProperties().getExtendedProperties().setApplication("Microsoft Excel"); // 这是一个间接的方法,有时能解决问题,最可靠的方式是提前知道文件使用哪种系统。 }
* **最佳实践**:在读取 Excel 前,最好能确认文件的日期系统设置,如果无法确认,并且遇到日期错乱,可以尝试切换 `set1904` 的值。 - 在读取
最佳实践总结
-
优先使用 Java 8 的
java.time:如果你使用的是 Java 8 或更高版本,强烈建议使用LocalDate和LocalDateTime代替旧的java.util.Date。- 读取转换:
// 假设 cell 是一个日期格式的单元格 Date date = cell.getDateCellValue(); LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
- 写入转换:
LocalDate localDate = LocalDate.now(); Date date = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); cell.setCellValue(date);
- 读取转换:
-
明确格式:无论是读取还是写入,都明确指定日期格式(如
yyyy-MM-dd),避免因系统默认不同导致的问题。 -
检查文件:遇到问题时,首先用 Excel 软件打开文件,检查单元格的格式设置是否正确。
-
选择合适的库:
- Apache POI:功能最全面,支持所有 Office 版本,适合复杂的操作。
- EasyExcel:API 更简洁,内存占用低,适合读取大数据量的 Excel 文件。
