一、背景
在Java应用程序开发中,处理日期和时间是一项非常常见及麻烦的场景。尽管Java标准库提供了基本的日期和时间操作类以供使用,但它们的使用常常不够直观和灵活,而偶然间了解并学习到Joda-Time, 在工作中用起来比较顺手,其又是一个强大的日期和时间库,提供了丰富的API,用于简化日期和时间的操作。因此,本文对Joda-Time相关知识做一个介绍及总结。
二、Joda-Time基本介绍
Joda-Time是一个开源的Java日期和时间库,由Stephen Colebourne创建,旨在替代Java标准库中的 java.util.Date 和java.util.Calendar 类
。它提供了丰富的API,用于处理日期、时间、时间段、时区等。也是由于Joda-Time良好的设计和风格,很多核心思想被引入到了Java8的java.time
包中,Joda-Time的作者Stephen Colebourne和Oracle一起共同参与了这些API的设计和实现。这里额外值得一提的是,Joda-Time在 Golang 中有对应的实现 go-joda,后续文章中会介绍。
2.1 Joda-Time的核心类
- Instant类
- 不可变的类,用来表示时间轴上一个瞬时的点。
- DateTime类
- 不可变的类,用来替换JDK的Calendar类。
- LocalDate类
- 不可变的类,表示一个本地的日期,而不包含时间部分(没有时区信息)。
- LocalTime类
- 不可变的类,表示一个本地的时间,而不包含日期部分(没有时区信息)。
- LocalDateTime类
- 不可变的类,表示一个本地的日期-时间(没有时区信息)。
- DateTimeFormatter类
- DateTimeFormatter用于日期时间的格式化和解析,它提供了多种预定义的格式化模式,也可以自定义格式化模式。
- DateTimeZone类
- 用于处理时区,它提供了多种预定义的时区,也可以自定义时区。
- Period类
- 用于表示一个时间段(如几天、几月、几年)。
- Duration类
- 用于表示一个时间长度(如几秒、几分钟)。它们提供了丰富的API,用于计算和操作时间段和时间长度。
2.2 Joda-Time的特性
Joda-Time 提供了直观易用的 API 和丰富的功能,极大地简化了日期和时间的处理。Joda-Time 的主要特性包括:
- 支持多个时间带
- 提供丰富的日期和时间操作方法
- 支持格式化和解析日期时间字符串
- 提供时间间隔和周期的计算
三、时间工具类对比
3.1 JDK8前SimpleDateFormat与Joda-Time对比
本节主要是将jdk7之前用到比较多的时间工具类进行对比,SimpleDateFormat就是重点对比对象,其与Joda-Time相比,就存在线程安全问题。
3.1.1 问题复现
下面是复现 SimpleDateFormat中的线程不安全问题的代码:
// (1)创建单例实例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
// (2)创建多个线程,并启动
for (int i = 0; i < 4; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {
// (3)使用单例日期实例解析文本
System.out.println(sdf.parse("2025-01-17 16:19:27"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
// (4)启动线程
thread.start();
}
}
运行以上代码会抛出以下异常:
3.1.2 问题分析
点进到SimpleDateFormat源码可知每个SimpleDateFormat实例里面有一个Calendar对象,从后面会知道其实SimpleDateFormat之所以是线程不安全的就是因为Calendar是线程不安全的,后者之所以是线程不安全的是因为其中存放日期数据的变量都是线程不安全的,比如里面的fields,time等。
3.1.3 解决方案
3.1.3.1 方案一:每次都实例化
每次使用时候new一个SimpleDateFormat的实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其它引用,就会需要被回收,开销会很大。
3.1.3.2 方案二:使用synchronized同步
究其原因是因为多线程下步骤(3)(4)(5)三个步骤不是一个原子性操作,那么容易想到的是对其进行同步,让(3)(4)(5)成为原子操作,可以使用synchronized进行同步,具体如下:
// (1)创建单例实例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
//(2)创建多个线程,并启动
for (int i = 0; i < 4; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {
synchronized (sdf) {
// (3)使用单例日期实例解析文本
System.out.println(sdf.parse("2025-01-17 16:19:27"));
}
} catch (ParseException e) {
e.printStackTrace();
}
}
});
// (4)启动线程
thread.start();
}
}
但是加上同步锁之后会使得线程要竞争锁,在高并发场景下会导致系统响应性能下降。
3.1.3.3 方案三:使用ThreadLocal
使用ThreadLocal的方式也可以解决并发问题,这样每个线程只需要使用一个SimpleDateFormat实例,相比第一种方式大大节省了对象的创建销毁开销,并且不需要对多个线程直接进行同步。使用ThreadLocal方式代码如下:
// (1)创建threadlocal实例
static ThreadLocal<DateFormat> safeSdf = new ThreadLocal<DateFormat>(){
@Override
protected SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args) {
//(2)创建多个线程,并启动
for (int i = 0; i < 4; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {
// (3)使用单例日期实例解析文本
System.out.println(safeSdf.get().parse("2025-01-17 16:19:27"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
// (4)启动线程
thread.start();
}
}
3.1.3.4 方案四:使用Joda-Time
因为Joda-Time是线程安全的,可以直接使用Joda-Time替换SimpleDateFormat,代码如下:
// (1)创建DateTimeFormatter实例
static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
//(2)创建多个线程,并启动
for (int i = 0; i < 4; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
// (3)使用单例日期实例解析文本
System.out.println(dateTimeFormatter.parseDateTime("2025-01-17 16:19:27"));
}
});
thread.start();//(4)启动线程
}
}
3.2 JDK8中LocalDateTime与Joda-Time对比
在 JDK8 以前,时间和日期的类库很难用,而且有线程安全等诸多问题,但是在 JDK8 时,增加了 java.time 包,经过借鉴Joda-Time优秀的设计思想后,JDK8 中的LocalDateTime与Joda-Time功能相差不大了,具体对比可以参考这篇文章:《Java基础之如何取舍Joda与 Java8 日期库》,本文只对Joda-Time的微弱优势做个简单总结:
3.2.1 本地化差异
对于场景:想获得 “星期三” 这个字符串,Joda-Time中使用dt.dayOfWeek().getAsShortText();
这样获得,而在 java.time 包中使用localDateTime.getDayOfWeek().name();
只能获取到英文WEDNESDAY,这样能明显感受到Joda-Time的本地化更强一点。
// 会控制台打印今天是星期几
// joda-time
DateTime dt = new DateTime();
String shortText = dt.dayOfWeek().getAsShortText();
// 星期三
System.out.println(shortText);
// java8
Clock clock = Clock.systemDefaultZone();
LocalDateTime localDateTime = LocalDateTime.now(clock);
String name = localDateTime.getDayOfWeek().name();
// WEDNESDAY
System.out.println(name);
3.2.2 API功能缺失
Joda-Time中有API表示两个 instant 之间的间隔,左闭右开,而java time 中没有提供类似的 API,因为 JSR-310 标准中没有这个概念。
DateTime dt = new DateTime();
DateTime dt1 = new DateTime();
Interval interval = new Interval(dt.toInstant(), dt1.toInstant());
System.out.println(interval);
四、Joda-Time使用
4.1 引入依赖
众所周知,要想使用其工具,首先肯定需要引入相关依赖。这里介绍两种引入方式,如果项目中使用的是Maven管理依赖,那么可以在pom.xml文件中引入以下依赖:
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>
</dependency>
如果项目中使用的是Gradle管理依赖,那么可以在build.gradle文件中引入以下依赖:
implementation 'joda-time:joda-time:2.10.10'
4.2 使用方式
4.2.1 构造对象
如果直接看源码,会看到这个类有很多构造方法,方便开发者按照需求快速构造相应的日期对象,而经常使用的构造DateTime 对象的方法有如下4种:
// 这个无参的构造方法会创建一个在当前系统所在时区的当前时间,精确到毫秒
DateTime dt2 = new DateTime();
System.out.println(dt1);
// 这个构造方法方便快速地构造一个指定的时间,这里精确到秒,类似地其它构造方法也可以传入毫秒
DateTime dt2 = new DateTime(2025,1,26,20,51,20);
System.out.println(dt2);
// 这个构造方法创建出来的实例,是通过一个long类型的时间戳,它表示这个时间戳距1970-01-01T00:00:00Z的毫秒数。使用默认的时区
DateTime dt3 = new DateTime(1737895880000L);
System.out.println(dt3);
// 这个构造方法可以通过一个Object对象构造一个实例。这个Object对象可以是这些类型:ReadableInstant, String, Calendar和Date
DateTime dt4 = new DateTime(new Date());
System.out.println(dt4);
4.2.2 时间格式化
对时间进行格式化的方式有如下两种:
// 方式一
DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
DateTime dt = new DateTime();
System.out.println(dt.toString(dateTimeFormatter));
// 方式二
String dataFormat = "yyyy-MM-dd HH:mm:ss";
System.out.println(dt.toString(dataFormat));
4.2.3 时间加减运算
plus/minus
开头的方法(比如:plusDay, minusMonths):用来做日期时间的加减运算,返回在DateTime实例上增加或减少一段时间后的实例。
DateTime dateTime = new DateTime();
DateTime plusYears = dateTime.plusYears(1);// 加 - 年
System.out.println(plusYears);
DateTime plusMonths = dateTime.plusMonths(1);// 加 - 月
System.out.println(plusMonths);
DateTime plusWeeks = dateTime.plusWeeks(1);// 加 - 周
System.out.println(plusWeeks);
DateTime plusDays = dateTime.plusDays(1);// 加 - 天
System.out.println(plusDays);
DateTime plusHours = dateTime.plusHours(1);// 加 - 时
System.out.println(plusHours);
DateTime plusMinutes = dateTime.plusMinutes(1);// 加 - 分
System.out.println(plusMinutes);
DateTime plusSeconds = dateTime.plusSeconds(1);// 加 - 秒
System.out.println(plusSeconds);
DateTime plusMillis = dateTime.plusMillis(1);// 加 - 毫秒
System.out.println(plusMillis);
dateTime.minusYears(1);// 减 - 年
dateTime.minusMonths(1);// 减 - 月
dateTime.minusWeeks(1);// 减 - 周
dateTime.minusDays(1);// 减 - 天
dateTime.minusHours(1);// 减 - 时
dateTime.minusMinutes(1);// 减 - 分
dateTime.minusSeconds(1);// 减 - 秒
dateTime.minusMillis(1);// 减 - 毫秒
4.2.4 获取时间间隔
xxxBetween方法常被用来获取时间间隔,详细案例如下:
DateTime startTime = new DateTime(2023,8,8, 15,0,0);
DateTime now = new DateTime();
// 时间间隔 【年】
int years = Years.yearsBetween(startTime, now).getYears();
System.out.println(years);
// 时间间隔 【月】
int months = Months.monthsBetween(startTime, now).getMonths();
System.out.println(months);
// 时间间隔 【周】
int weeks = Weeks.weeksBetween(startTime, now).getWeeks();
System.out.println(weeks);
// 时间间隔 【天】
int days = Days.daysBetween(startTime, now).getDays();
System.out.println(days);
// 时间间隔 【时】
int hours = Hours.hoursBetween(startTime, now).getHours();
System.out.println(hours);
// 时间间隔 【分】
int minutes = Minutes.minutesBetween(startTime, now).getMinutes();
System.out.println(minutes);
// 时间间隔 【秒】
int seconds = Seconds.secondsBetween(startTime, now).getSeconds();
System.out.println(seconds);
4.2.5 获取年、月、日、星期几
of
比如dayOfYear、dayOfMonth等,获取一个时间的某个日期属性。
DateTime now = new DateTime();
// 今年的第几天
int dayOfYear = now.getDayOfYear();
System.out.println("这是今年的第 " + dayOfYear + " 天");
// 这个月的第几天
int dayOfMonth = now.getDayOfMonth();
System.out.println("这是这个月的第 " + dayOfMonth + " 天");
// 这周的第几天
int dayOfWeek = now.getDayOfWeek();
System.out.println("这是这周的第 " + dayOfWeek + " 天");
// 今天的第几分钟
int minuteOfDay = now.getMinuteOfDay();
System.out.println("这是今天的第 " + minuteOfDay + " 分钟");
// 这个小时的第几分钟
int minuteOfHour = now.getMinuteOfHour();
System.out.println("这是这个小时的第 " + minuteOfHour + " 分钟");
4.2.6 日期时间比较
DateTime类提供了多种方法,用于比较日期时间,例如,isBefore、isAfter、isEqual
等方法,可以方便地比较两个日期时间的先后顺序。
DateTime now = new DateTime();
DateTime future = now.plusDays(1);
boolean isBefore = now.isBefore(future);
boolean isAfter = now.isAfter(future);
boolean isEqual = now.isEqual(future);
System.out.println("现在是否在将来之前: " + isBefore);
System.out.println("现在是否在将来之后: " + isAfter);
System.out.println("现在是否等于将来: " + isEqual);
4.2.7 处理时间带
Joda-Time 提供了对多个时间带的支持,使得处理不同时间带的日期和时间变得更加简单。
// 设置时区
DateTimeZone timeZone = DateTimeZone.forID("America/New_York");
DateTime dateTime = new DateTime(timeZone);
System.out.println("New York DateTime: " + dateTime);
// 转换时区
DateTime utcDateTime = dateTime.withZone(DateTimeZone.UTC);
System.out.println("UTC DateTime: " + utcDateTime);
4.2.8 计算时间间隔和周期
Joda-Time 提供了 Duration 和 Period 类用于计算时间间隔和周期。
DateTime start = new DateTime(2024, 6, 13, 10, 0, 0);
DateTime end = new DateTime(2024, 6, 13, 12, 30, 0);
// 计算时间间隔
Duration duration = new Duration(start, end);
// 输出 "150 minutes"
System.out.println("Duration: " + duration.getStandardMinutes() + " minutes");
// 计算时间周期
Period period = new Period(start, end);
// 输出 "2 hours and 30 minutes"
System.out.println("Period: " + period.getHours() + " hours and " + period.getMinutes() + " minutes");
五、总结
通过本文的讲解,我们对比了解了Joda-Time与部分java中日期时间工具的区别,然后也学习到了Joda-Time的基本使用方法和核心类。Joda-Time提供了一个优雅、易用、线程安全的日期和时间处理方式,解决了Java原生日期时间API的许多问题。在此,希望通过本文的总结,能帮助到您在实际开发中灵活运用,提升日期时间处理的效率和质量。
六、参考资料
https://juejin.cn/post/6844904077583712263#heading-1
https://blog.csdn.net/qq_37687594/article/details/122028516