看山聊Java:开始使用 Java8 中的时间类

简介: ava8 之前,我们常用的时间类有java.util.date、java.util.Calendar和java.util.Timezone。还会有一些不那么常用的,java.sql.Date、java.sql.TimeStamp。

image.png

该图片由Erik Karits在Pixabay上发布


你好,我是看山。


前面聊了聊 Java8 新版时间 API 的类,然后又说了说怎么与旧版时间 API 的转换,今天来聊聊怎样通过新 API 实现老 API 的功能,这样我们就可以逐步替换掉旧版 API,与时俱进。


Java8 之前,我们常用的时间类有java.util.date、java.util.Calendar和java.util.Timezone。还会有一些不那么常用的,java.sql.Date、java.sql.TimeStamp。


这些类可以实现一些简单的功能,但是要实现复杂的功能,我们只能自己实现很多工具类,或者借助第三方类库,其中比较著名的第三方库是 joda-time,后来 Java8 吸收其设计,直接让 joda-time 转正了。


因为这个系列说的都是时间库,后文没有特殊说明,使用“新版 API”指代JSR 310提供的时间 API,使用“旧版 API”指代 Java 8 之前提供的官方时间 API。


新版 API 的优势

新版 API 的优势,对应的就是旧版 API 的劣势。


语义明确

这点非常重要。


我们在定义一个实体的时候,能够做到语义明确没有歧义非常重要。


比如,我们需要定义出生日期,只需要年月日,在以前只能用java.util.Date,但是这个类包含时分秒。


或者,我们需要定义生日,正常只需要月日就行,连年都不需要,java.util.Date更加做不到。


在这些场景中,我们只能通过注释或者属性名做一些松散约束,很容易出错。


经验告诉我们,人是不可靠的,约定是不可靠的,只要有犯错的可能,就一定会犯错。


新版 API 完美解决这个问题,提供具有不同语义且语义明确的类。如果想表示日期可以用LocalDate,如果想表示生日,可以用MonthDay。这样,我们就能够借助编译器增强约束力。


符合自然规律

我们来通过一段代码感受下(测试日期是 2021 年 6 月 28 日),执行结果放在每行结尾处:

Date date = new Date();
System.out.println(date);// Mon Jun 28 21:41:25 CST 2021
System.out.println(date.getYear());// 121
System.out.println(date.getMonth());// 5
System.out.println(date.getDay());// 1
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
System.out.println(calendar.get(Calendar.YEAR));// 2021
System.out.println(calendar.get(Calendar.MONTH));// 5
System.out.println(calendar.get(Calendar.DAY_OF_MONTH));// 28
LocalDate localDate = LocalDate.now();
System.out.println(localDate.getYear());// 2021
System.out.println(localDate.getMonth());// JUNE
System.out.println(localDate.getDayOfMonth());// 28

仅仅一个年月日的获取,旧版 API 已经是存在各种含义。


java.util.Date#getYear表示的是当前年份减去 1900;java.util.Date#getDay返回的是当前是一周中的第几天,下标从 0 开始。当然,这几个 API 在 JDK1.1 的时候已经标记废弃了,但是知道现在也没有删除。相信很多初次使用旧版 API 的同学,都可能踩过坑。


更甚的是,java.util.Date和java.util.Calendar的返回的月份,都是从 0 开始计数的。但是年和日是从 1 开始计数,实在没有搞懂当时开发人员的脑回路是怎样的。


再看新版 API,完全符合我们的认知:年是自然年,月使用Month枚举(枚举的 value 值是自然月),日是自然日,一切都是根据我们对自然规律的理解。


而且,新版 API 很贴心的考虑了夏令时,在使用ZonedDateTime进行不同区域时间转换时,会自动计算。可以说,关于时间的逻辑,新版 API 已经都包含了。


功能完善

旧版 API 提供的时间类,只能实现一些简单的逻辑,想要做一些复杂的操作,就需要自己实现。新版 API 提供了灵活的时间功能 API。


Instant:表示时间戳,也就是一个时间点。

LocalDate:表示日期,年、月、日

LocalTime:表示时间,时、分、秒、纳秒

LocalDateTime:表示日期+时间,内部也是通过LocalDate和LocalTime存储数据的

OffsetDateTime:带有时间偏移的LocalDateTime

OffsetTime:带有时间偏移的LocalTime

ZonedDateTime:比OffsetDateTime多了时区,也就是带有时区、带有时间偏移的LocalDateTime

MonthDay:表示月、日,不包含年和时间

YearMonth:表示年、月,不包含日和时间

Duration:表示纳秒、秒的时间量,可以度量天、小时、分、秒、毫秒、纳秒。

Period:表示年、月、日的时间量,可以度量年、月、日、周。

有了上面这些类,我们可以在操作时间时横着走了。


不可变(线程安全)

上面提到的这些类中,都是不可修改的,这些类的成员变量都使用final修饰,所有需要修改数据的方法,都是返回一个新实例。新版 API 通过这种方式实现了时间类的不可变性。


我们都知道,一个对象是不可变的,那它就是线程安全的。


旧版 API 的则不是这样,数据可变且对并发敏感。当然,这只能算是设计初衷不同。


新版 API 表示时间的类是线程安全的,可以对时间格式化的DateTimeFormatter也是时间安全的。


旧版的SimpleDateFormat也是类似的功能,但却不是线程安全的。这点就不是设计初衷不同了,而是缺少设计。相信很多没有被这个类毒打过的小白,会定义一些工具类,里面是static定义的SimpleDateFormat,然后对时间进行格式化,测试的时候没有一点问题,放在线上就出现各种诡异错误。


开启征程

我们通过一些常用场景,展示新旧版本 API 的实现方式。


获取当前时间

旧版 API:


Date now = new Date();

新版 API:


// 没有偏移、没有时区概念的当前时间
LocalDateTime now = LocalDateTime.now();
// 有偏移和时区的当前时间
ZonedDateTime now2 = ZonedDateTime.now();

指定日期

旧版 API:


Date birthday = new GregorianCalendar(1988, Calendar.AUGUST, 20).getTime();

新版 API:


LocalDate birthday = LocalDate.of(1988, Month.AUGUST, 20);
LocalDate birthday2 = LocalDate.of(1988, 8, 20);

获取指定量

旧版 API:


int month = new GregorianCalendar().get(Calendar.MONTH);

新版 API:


Month month = LocalDateTime.now().getMonth();

调整时间

比如,获取当前之间 5 小时前的时间。


旧版 API:


GregorianCalendar calendar = new GregorianCalendar();
calendar.add(Calendar.HOUR_OF_DAY, -5);
Date fiveHoursBefore = calendar.getTime();

新版 API:


LocalDateTime fiveHoursBefore = LocalDateTime.now().minusHours(5);

调整指定时间量

比如,将时间调整到 6 月。


旧版 API:


GregorianCalendar calendar = new GregorianCalendar();
calendar.set(Calendar.MONTH, Calendar.JUNE);
Date inJune = calendar.getTime();

新版 API:


LocalDateTime inJune = LocalDateTime.now().withMonth(Month.JUNE.getValue());

截断时间量

我们有时候会将时间对象某个单位后面的所有时间量设置为 0,这个操作形象的称为截断。比如,将分、秒、毫秒设置为零。


旧版 API:


Calendar now = Calendar.getInstance();
now.set(Calendar.MINUTE, 0);
now.set(Calendar.SECOND, 0);
now.set(Calendar.MILLISECOND, 0);
Date truncated = now.getTime();

新版 API:

LocalTime truncated = LocalTime.now().truncatedTo(ChronoUnit.HOURS);

时区转换

旧版 API:

GregorianCalendar calendar = new GregorianCalendar();
calendar.setTimeZone(TimeZone.getTimeZone("CET"));
Date centralEastern = calendar.getTime();

新版 API:


ZonedDateTime centralEastern = LocalDateTime.now().atZone(ZoneId.of("CET"));

计算时间跨度

旧版 API:

GregorianCalendar calendar = new GregorianCalendar();
Date now = new Date();
calendar.add(Calendar.HOUR, 1);
Date hourLater = calendar.getTime();
long elapsed = hourLater.getTime() - now.getTime();

新版 API:


LocalDateTime now = LocalDateTime.now();
LocalDateTime hourLater = LocalDateTime.now().plusHours(1);
Duration span = Duration.between(now, hourLater);

Duration是跨度较小,可以度量天、小时、分、秒、毫秒、纳秒。如果跨度较大,可以使用Period,可以度量年、月、日、周。


时间格式化和解析

新版 API 使用DateTimeFormatter,这个类是线程安全的。而且相较于SimpleDateFormat提供了很多额外的功能。


旧版 API:


SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date now = new Date();
String formattedDate = dateFormat.format(now);
Date parsedDate = dateFormat.parse(formattedDate);

新版 API:


DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate now = LocalDate.now();
String formattedDate = now.format(formatter);
LocalDate parsedDate = LocalDate.parse(formattedDate, formatter);

其他一些小功能

比如,计算一个月有几天:


旧版 API:


Calendar calendar = new GregorianCalendar(1990, Calendar.FEBRUARY, 20);
int daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);

新版 API:


int daysInMonth = YearMonth.of(1990, 2).lengthOfMonth();

新旧版 API 的转换

新版 API 虽然好用,但是罗马不是一天建成的。旧版 API 不会一夜消失,可能需要共用。Java 8 提供了很多方法,用来在新旧版本 API 之间进行转换。比如:


// 从 Calendar 转换 Instant
Instant instantFromCalendar = GregorianCalendar.getInstance().toInstant();
// 从 Calendar 转换 ZonedDateTime
ZonedDateTime zonedDateTimeFromCalendar = new GregorianCalendar().toZonedDateTime();
// 从 Instant 转换 Date
Date dateFromInstant = Date.from(Instant.now());
// 从 ZonedDateTime 转换 Calendar
GregorianCalendar calendarFromZonedDateTime = GregorianCalendar.from(ZonedDateTime.now());
// 从 Date 转换 Instant
Instant instantFromDate = new Date().toInstant();
// 从 TimeZone 转换 ZoneId
ZoneId zoneIdFromTimeZone = TimeZone.getTimeZone("PST").toZoneId();

更多的内容可以看看该系列的另一篇:Java8 的时间库(2):Date 与 LocalDate 或 LocalDateTime 互相转换。


文末总结

在本文中,我们讲解了旧版 API 的缺点、新版 API 的优点。新版 API 提供了很多友好的方法,而且不同类之间操作几乎一致,所以很多例子虽然只是说了一个类的使用,但是其他类也可以采用相似方式实现。


推荐阅读

Java8 的时间库(1):介绍 Java8 中的时间类及常用 API

Java8 的时间库(2):Date 与 LocalDate 或 LocalDateTime 互相转换

Java8 的时间库(3):开始使用 Java8 中的时间类

未完待续……

目录
相关文章
|
2月前
|
Java 开发者
在 Java 中,一个类可以实现多个接口吗?
这是 Java 面向对象编程的一个重要特性,它提供了极大的灵活性和扩展性。
164 57
|
17天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
2月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
68 8
|
2月前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
81 17
|
2月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
2月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
143 4
|
2月前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
90 2
|
2月前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。
79 4
|
2月前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
59 5
|
2月前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
127 5