Java 8 引入的 Date-Time API (java.time 包) 为日期和时间处理带来了显著的改进,解决了 java.util.Date 类的许多痛点:
- 非线程安全
- 时区处理麻烦
- 格式化和时间计算繁琐
- 设计有缺陷,Date 类同时包含日期和时间;还有一个 java.sql.Date,容易混淆。
本文将详细讲解 Java 8 新的 Date-Time API,并通过源码和示例代码对比 Java 8 之前的实现方式,深入剖析其设计的用意和目的。
java.time 主要类
java.util.Date 既包含日期又包含时间,而 java.time 将它们进行了分离:
- LocalDateTime:日期和时间,格式为 yyyy-MM-ddTHH:mm:ss.SSS
- LocalDate:仅日期,格式为 yyyy-MM-dd
- LocalTime:仅时间,格式为 HH:mm:ss
格式化
Java 8 之前:
java
public void oldFormat(){
Date now = new Date();
//格式化 yyyy-MM-dd
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String date = sdf.format(now);
System.out.println(String.format("date format : %s", date));
//格式化 HH:mm:ss
SimpleDateFormat sdft = new SimpleDateFormat("HH:mm:ss");
String time = sdft.format(now);
System.out.println(String.format("time format : %s", time));
//格式化 yyyy-MM-dd HH:mm:ss
SimpleDateFormat sdfdt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String datetime = sdfdt.format(now);
System.out.println(String.format("dateTime format : %s", datetime));
}
Java 8 之后:
java
public void newFormat(){
//格式化 yyyy-MM-dd
LocalDate date = LocalDate.now();
System.out.println(String.format("date format : %s", date));
//格式化 HH:mm:ss
LocalTime time = LocalTime.now().withNano(0);
System.out.println(String.format("time format : %s", time));
//格式化 yyyy-MM-dd HH:mm:ss
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dateTimeStr = dateTime.format(dateTimeFormatter);
System.out.println(String.format("dateTime format : %s", dateTimeStr));
}
字符串转日期格式
Java 8 之前:
java
//已弃用
Date date = new Date("2021-01-26");
//替换为
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date1 = sdf.parse("2021-01-26");
Java 8 之后:
java
LocalDate date = LocalDate.of(2021, 1, 26);
LocalDate.parse("2021-01-26");
LocalDateTime dateTime = LocalDateTime.of(2021, 1, 26, 12, 12, 22);
LocalDateTime.parse("2021-01-26T12:12:22");
LocalTime time = LocalTime.of(12, 12, 22);
LocalTime.parse("12:12:22");
Java 8 之前 的转换需要借助 SimpleDateFormat 类,而 Java 8 之后 只需要使用 LocalDate、LocalTime、LocalDateTime 的 of 或 parse 方法。
日期计算
以下以计算 一周后的日期 为例,其他单位(年、月、日、时等)类似。这些单位都在
java.time.temporal.ChronoUnit 枚举中定义。
Java 8 之前:
java
public void afterDay(){
//一周后的日期
SimpleDateFormat formatDate = new SimpleDateFormat("yyyy-MM-dd");
Calendar ca = Calendar.getInstance();
ca.add(Calendar.DATE, 7);
Date d = ca.getTime();
String after = formatDate.format(d);
System.out.println("一周后日期:" + after);
//算两个日期间隔多少天,计算间隔多少年,多少月方法类似
String dates1 = "2021-12-23";
String dates2 = "2021-02-26";
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
Date date1 = format.parse(dates1);
Date date2 = format.parse(dates2);
int day = (int) ((date1.getTime() - date2.getTime()) / (1000 * 3600 * 24));
System.out.println(dates1 + "和" + dates2 + "相差" + day + "天");
}
Java 8 之后:
java
public void pushWeek(){
//一周后的日期
LocalDate localDate = LocalDate.now();
//方法1
LocalDate after = localDate.plus(1, ChronoUnit.WEEKS);
//方法2
LocalDate after2 = localDate.plusWeeks(1);
System.out.println("一周后日期:" + after);
//算两个日期间隔多少天,计算间隔多少年,多少月
LocalDate date1 = LocalDate.parse("2021-02-26");
LocalDate date2 = LocalDate.parse("2021-12-23");
Period period = Period.between(date1, date2);
System.out.println("date1 到 date2 相隔:" + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天");
//获取总天数
long day = date2.toEpochDay() - date1.toEpochDay();
System.out.println(date1 + "和" + date2 + "相差" + day + "天");
}
获取指定日期
获取特定一个日期,如本月最后一天或第一天。
Java 8 之前:
java
public void getDay() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
//获取当前月第一天:
Calendar c = Calendar.getInstance();
c.set(Calendar.DAY_OF_MONTH, 1);
String first = format.format(c.getTime());
System.out.println("first day:" + first);
//获取当前月最后一天
Calendar ca = Calendar.getInstance();
ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH));
String last = format.format(ca.getTime());
System.out.println("last day:" + last);
//当年最后一天
Calendar currCal = Calendar.getInstance();
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(Calendar.YEAR, currCal.get(Calendar.YEAR));
calendar.roll(Calendar.DAY_OF_YEAR, -1);
Date time = calendar.getTime();
System.out.println("last day:" + format.format(time));
}
Java 8 之后:
java
public void getDayNew() {
LocalDate today = LocalDate.now();
//获取当前月第一天:
LocalDate firstDayOfThisMonth = today.with(TemporalAdjusters.firstDayOfMonth());
//获取当前月最后一天:
LocalDate lastDayOfThisMonth = today.with(TemporalAdjusters.lastDayOfMonth());
//获取下一天:
LocalDate nextDay = lastDayOfThisMonth.plusDays(1);
//获取当年最后一天:
LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear());
//2021年最后一个周日
LocalDate lastSundayOf2021 = LocalDate.parse("2021-12-31").with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));
}
java.time.temporal.TemporalAdjusters 提供了很多便捷的方法来获取特定日期。
JDBC 和 Java 8
现在 JDBC 时间类型和 Java 8 时间类型的对应关系是:
- Date ---> LocalDate
- Time ---> LocalTime
- Timestamp ---> LocalDateTime
而之前统统对应 Date 类型。
时区
java.util.Date 对象实际上存储的是 1970 年 1 月 1 日 0 点(GMT)至 Date 对象所表示时刻所经过的毫秒数。因此,它记录的毫秒数与时区无关,但在使用上需要转换成当地时间,这就涉及到了时间的国际化。java.util.Date 本身并不支持国际化,需要借助 TimeZone。
java
//北京时间
Date date = new Date();
SimpleDateFormat bjSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//北京时区
bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
System.out.println("毫秒数:" + date.getTime() + ", 北京时间:" + bjSdf.format(date));
//东京时区
SimpleDateFormat tokyoSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
System.out.println("毫秒数:" + date.getTime() + ", 东京时间:" + tokyoSdf.format(date));
//直接打印会自动转成当前时区时间
System.out.println(date);
在新特性中引入了 java.time.ZonedDateTime 来表示带时区的时间。它可以看成是 LocalDateTime + ZoneId。
java
//当前时区时间
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println("当前时区时间: " + zonedDateTime);
//东京时间
ZoneId zoneId = ZoneId.of(ZoneId.SHORT_IDS.get("JST"));
ZonedDateTime tokyoTime = zonedDateTime.withZoneSameInstant(zoneId);
System.out.println("东京时间: " + tokyoTime);
// ZonedDateTime 转 LocalDateTime
LocalDateTime localDateTime = tokyoTime.toLocalDateTime();
System.out.println("东京时间转当地时间: " + localDateTime);
//LocalDateTime 转 ZonedDateTime
ZonedDateTime localZoned = localDateTime.atZone(ZoneId.systemDefault());
System.out.println("本地时区时间: " + localZoned);
java.time 包与 Joda-Time 的对比
Java 8 引入的 java.time 包是对日期和时间处理的一次重大改进,它借鉴了许多 Joda-Time 的设计理念,并在此基础上进行了优化。那么,java.time 包与 Joda-Time 相比有哪些优势和劣势呢?
1. 设计理念和 API 风格
Joda-Time
- 设计理念:Joda-Time 旨在提供一个更直观和更强大的日期时间处理库,弥补 java.util.Date 和 java.util.Calendar 的不足。
- API 风格:Joda-Time 的 API 风格非常直观,类名和方法名都非常清晰易懂。例如,DateTime 类表示日期和时间,LocalDate 类表示仅日期。
Java 8 java.time
- 设计理念:java.time 包是基于 JSR 310 的规范实现,吸收了 Joda-Time 的优点,并在此基础上进行了优化和改进。
- API 风格:java.time 包的 API 风格与 Joda-Time 类似,但在命名和结构上更加规范。例如,LocalDateTime 表示日期和时间,LocalDate 表示仅日期,LocalTime 表示仅时间。
2. 集成和兼容性
Joda-Time
- 集成:Joda-Time 是一个独立的第三方库,需要手动添加依赖。
- 兼容性:Joda-Time 与 Java SE 8 之前的版本兼容,适用于所有版本的 Java。
Java 8 java.time
- 集成:java.time 包是 Java SE 8 的一部分,无需额外添加依赖。
- 兼容性:java.time 包仅适用于 Java SE 8 及以上版本,对于 Java 8 之前的版本,需要额外的兼容性库(如 ThreeTen-Backport)。
3. 性能和线程安全
Joda-Time
- 性能:Joda-Time 的性能相对较好,但在某些操作上可能不如 java.time 包高效。
- 线程安全:Joda-Time 的大多数类是不可变的,因此是线程安全的。
Java 8 java.time
- 性能:java.time 包在设计时考虑了性能优化,尤其是在日期计算和格式化等高频操作上性能更优。
- 线程安全:java.time 包中的所有类都是不可变的,因此也是线程安全的。
4. 功能和扩展性
Joda-Time
- 功能:Joda-Time 提供了丰富的功能,支持各种日期时间操作、格式化、解析、时区转换等。
- 扩展性:Joda-Time 提供了一些扩展功能,例如自定义时间单位和字段。
Java 8 java.time
- 功能:java.time 包提供了与 Joda-Time 类似的功能,并在某些方面进行了增强。例如,提供了更丰富的日期调整器(TemporalAdjuster)和更强大的时间量(Duration 和 Period)。
- 扩展性:java.time 包也提供了良好的扩展性,允许用户自定义时间单位和字段。
示例对比
Joda-Time 示例
java
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
public class JodaTimeExample {
public static void main(String[] args) {
// 当前时间
DateTime now = DateTime.now();
System.out.println("当前时间: " + now);
// 格式化
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
String formattedDate = now.toString(formatter);
System.out.println("格式化后的时间: " + formattedDate);
// 解析
DateTime parsedDate = DateTime.parse("2021-01-26 12:12:22", formatter);
System.out.println("解析后的时间: " + parsedDate);
// 日期计算
DateTime oneWeekLater = now.plusWeeks(1);
System.out.println("一周后的时间: " + oneWeekLater);
}
}
Java 8 java.time 示例
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class JavaTimeExample {
public static void main(String[] args) {
// 当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println("当前时间: " + now);
// 格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDate = now.format(formatter);
System.out.println("格式化后的时间: " + formattedDate);
// 解析
LocalDateTime parsedDate = LocalDateTime.parse("2021-01-26 12:12:22", formatter);
System.out.println("解析后的时间: " + parsedDate);
// 日期计算
LocalDateTime oneWeekLater = now.plusWeeks(1);
System.out.println("一周后的时间: " + oneWeekLater);
}
}
总结
优势
- 集成:java.time 包是 Java 8 的一部分,无需额外依赖,集成更方便。
- 性能:java.time 包在性能上进行了优化,尤其是在高频日期时间操作上。
- 线程安全:java.time 包中的所有类都是不可变的,因此线程安全。
- 标准化:java.time 包是基于 JSR 310 的规范实现,更加标准化。
劣势
- 兼容性:java.time 包仅适用于 Java SE 8 及以上版本,对于 Java 8 之前的版本需要额外的兼容性库。
- 学习成本:对于熟悉 Joda-Time 的开发者来说,迁移到 java.time 包可能需要一定的学习成本。
使用 java.time 包中的类处理时间间隔和持续时间
在 Java 8 的 java.time 包中,处理时间间隔和持续时间有两个主要的类:Duration 和 Period。Duration 用于表示基于时间的间隔(以秒和纳秒为单位),而 Period 用于表示基于日期的间隔(以年、月、日为单位)。
1. Duration 类
Duration 类用于表示两个时间点之间的时间间隔,精确到秒和纳秒。它常用于计算精确的时间差,如秒、分钟、小时等。
示例代码
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class DurationExample {
public static void main(String[] args) {
// 当前时间
LocalDateTime startTime = LocalDateTime.now();
System.out.println("开始时间: " + startTime);
// 模拟一个耗时操作
try {
Thread.sleep(3000); // 休眠3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 结束时间
LocalDateTime endTime = LocalDateTime.now();
System.out.println("结束时间: " + endTime);
// 计算时间间隔
Duration duration = Duration.between(startTime, endTime);
System.out.println("时间间隔: " + duration.getSeconds() + " 秒 " + duration.getNano() + " 纳秒");
// 其他常用方法
System.out.println("时间间隔(分钟): " + duration.toMinutes());
System.out.println("时间间隔(毫秒): " + duration.toMillis());
// 使用ChronoUnit计算时间间隔
long secondsBetween = ChronoUnit.SECONDS.between(startTime, endTime);
System.out.println("时间间隔(ChronoUnit): " + secondsBetween + " 秒");
}
}
2. Period 类
Period 类用于表示两个日期之间的时间间隔,精确到年、月、日。它常用于计算日期间的差异,如天数、月数、年数等。
示例代码
java
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class PeriodExample {
public static void main(String[] args) {
// 当前日期
LocalDate startDate = LocalDate.now();
System.out.println("开始日期: " + startDate);
// 目标日期
LocalDate endDate = startDate.plusYears(1).plusMonths(2).plusDays(3);
System.out.println("结束日期: " + endDate);
// 计算日期间隔
Period period = Period.between(startDate, endDate);
System.out.println("日期间隔: " + period.getYears() + " 年 " + period.getMonths() + " 月 " + period.getDays() + " 天");
// 其他常用方法
System.out.println("总月数: " + period.toTotalMonths());
// 使用ChronoUnit计算日期间隔
long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);
System.out.println("日期间隔(ChronoUnit): " + daysBetween + " 天");
}
}
选择 Duration 和 Period 的适用场景
选择 Duration 的场景
- 需要精确到秒和纳秒的时间计算。
- 计时操作,如测量代码执行时间、任务的持续时间等。
- 时区无关的时间计算,如两个时间点之间的间隔。
选择 Period 的场景
- 需要基于年、月、日的日期计算。
- 计算两个日期之间的年、月、日差异。
- 日历相关操作,如计算某个日期之后的某年某月某日。
比较两个日期之间的差异
示例代码
java
import java.time.LocalDate;
import java.time.Period;
public class PeriodDifferenceExample {
public static void main(String[] args) {
// 两个日期
LocalDate startDate = LocalDate.of(2020, 1, 1);
LocalDate endDate = LocalDate.of(2021, 6, 15);
// 计算日期间隔
Period period = Period.between(startDate, endDate);
System.out.println("日期间隔: " + period.getYears() + " 年 " + period.getMonths() + " 月 " + period.getDays() + " 天");
}
}
使用 Duration 进行时区无关的时间计算
示例代码
java
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class ZonedDateTimeExample {
public static void main(String[] args) {
// 纽约时间
ZonedDateTime startDateTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("纽约开始时间: " + startDateTime);
// 东京时间
ZonedDateTime endDateTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("东京结束时间: " + endDateTime);
// 计算时间间隔
Duration duration = Duration.between(startDateTime, endDateTime);
System.out.println("时间间隔: " + duration.toHours() + " 小时");
}
}
使用 ChronoUnit 枚举
ChronoUnit 枚举提供了一系列用于时间单位的常量,支持基于时间和日期的计算。
示例代码
java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class ChronoUnitExample {
public static void main(String[] args) {
// 当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println("当前时间: " + now);
// 计算一周后的时间
LocalDateTime oneWeekLater = now.plus(1, ChronoUnit.WEEKS);
System.out.println("一周后的时间: " + oneWeekLater);
// 计算一年前的日期
LocalDate oneYearAgo = LocalDate.now().minus(1, ChronoUnit.YEARS);
System.out.println("一年前的日期: " + oneYearAgo);
// 计算两个时间点之间的小时数
long hoursBetween = ChronoUnit.HOURS.between(now, oneWeekLater);
System.out.println("时间间隔(小时): " + hoursBetween + " 小时");
}
}
总结
使用 Java 8 java.time 包中的 Duration 和 Period 类可以方便地处理时间间隔和持续时间。Duration 用于表示基于时间的间隔,如秒、分钟、小时等;而 Period 用于表示基于日期的间隔,如年、月、日等。此外,ChronoUnit 枚举提供了一系列用于时间单位的常量,支持基于时间和日期的计算。