Joda-Time 是一个强大、易用、高效的时间 日期处理框架,可以使时间和日期更容易操作和理解,可以完全替代 JDK 的相关 API 。
Joda-Time提供了一组Java类包用于处理包括ISO8601标准在内的date和time。可以利用它把JDK Date和Calendar类完全替换掉,而且仍然能够提供很好的集成。
GitHub地址:https://github.com/JodaOrg/joda-time
Joda-Time 与 JDK API 区别
1、JDK Date和Calendar的API难于使用。
2、JDK存储月份、星期从0开始而不是1。
3、JDK 难于扩展除默认支持以外的日历系统。
4、JDK 不支持一些常见的日期计算,如计算两 个时间之间所相差天数、星期等。
5、需要注意线程安全问题 , 如 DateFormat
Joda-Time 特点
1、令时间和日期更易于管理和操作。
2、提供简单的API,易于使用和扩展。
3、支持 ISO8601、Coptic 等多种日历系统。
4、与 JDK 百分之百互操作,使用 Joda-time 只需替换部分代码。
5、可完全代替 JDK 的时间和日期相关处理类。
Joda-Time 核心概念
1、不可变性 (Immutability)
居于线程安全设计,Joda-time 所有时间、日期操作的 API 都将返回一个全新的 Joda 实例,类似于 String 的各种操作方法的工作方式。
2、瞬间性 (Instant)
表示时间上的某个精确时刻,使用从 epoch 开始计算的毫秒数表示,即在时间线上只出现一次且唯一的时间点,与JDK相同。
3、局部性 (Partial)
指时间 (Instant) 的一部分片断,没有时区,可以在时间上来回“移动”,如 11 月 10 日、05:25:54,它可以是任何一年的时间。
4 、时间间隔
用于描述和计算两个时间之间的跨度,如两时间所相差的天数、月份、星期、时、分、秒等等,Joda-time 通过以下三个类进行描述:Duration、Period、Interval。
5、年表 (Chronology)
日历系统,一种计算时间的特殊方式,一种执行日历算法的框架,Joda-time 支持以下 8 种日历系统。
6、时区(Time Zone)
地球上的区域使用同一个时间定义 , 用于每个国家或不同区域对同一时间的计算
Joda-Time使用
引入依赖包
Maven依赖
<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.3</version> </dependency>
Gradle依赖
dependencies { compile ( "joda-time:joda-time:2.9.3", ) }
核心类介绍
最常用的date-time类:
Instant:不可变的类,用来表示时间轴上一个瞬时的点
DateTime:不可变的类,用来替换JDK的Calendar类
LocalDate:不可变的类,表示一个本地的日期,而不包含时间部分(没有时区信息)
LocalTime:不可变的类,表示一个本地的时间,而不包含日期部分(没有时区信息)
LocalDateTime:不可变的类,表示一个本地的日期-时间(没有时区信息)
TimeZone:类似于 java.util.TimeZone
DateTimeFormatter:时间解析与格式化处理类, 用于日期和时间与字符串之间的转换,类似于 jdk 的 DateFormat
DateTimeFormat:DateTimeFormatter 的工厂类,提供各种创建 DateTimeFormatter 实例方法
DateTimeFormatterBuilder:DateTimeFormatter 构造器,当需要实现自定 义的时间格式时可使用
注意:不可变的类,表明了正如Java的String类型一样,其对象是不可变的。即,不论对它进行怎样的改变操作,返回的对象都是新对象。
Instant:比较适合用来表示一个事件发生的时间戳。不用去关心它使用的日历系统或者是所在的时区。
DateTime:主要目的是替换JDK中的Calendar类,用来处理那些时区信息比较重要的场景。
LocalDate:适合表示出生日期这样的类型,因为不关心这一天中的时间部分。
LocalTime:适合表示一个商店的每天开门/关门时间,因为不用关心日期部分。
时间跨度
joda提供了三种时间跨度类;
Duration:提供了日、时、分、秒、毫秒几个单位的工厂方法来创建。
Period:在Duration基础上增加了年、月、周作为单位。
Interval:这个类表示一个特定的时间跨度,将使用一个明确的时刻界定这段时间跨度的范围。Interval 为半开区间,这表示由 Interval 封装的时间跨度包括这段时间的起始时刻,但是不包含结束时刻。
示例代码
1、创建一个用时间表示的某个随意的时刻 — 比如,2015年12月21日0时0分
DateTime dateTime1 = new DateTime(); System.out.println(dateTime1); // out: 2016-02-26T16:02:57.582+08:00 DateTime dateTime2 = new DateTime(2016, 2, 14, 0, 0, 0); // 年,月,日,时,分,秒,毫秒 System.out.println(dateTime2); // out: 2016-02-14T00:00:00.000+08:00 DateTime dateTime3 = new DateTime(1456473917004L); System.out.println(dateTime3); // out: 2016-02-26T16:05:17.004+08:00 DateTime dateTime4 = new DateTime(new Date()); System.out.println(dateTime4); // out: 2016-02-26T16:07:59.970+08:00 DateTime dateTime5 = new DateTime("2016-02-15T00:00:00.000+08:00"); System.out.println(dateTime5); // out: 2016-02-15T00:00:00.000+08:00
源码查看
/** * Constructs an instance from datetime field values * using <code>ISOChronology</code> in the default time zone. * * @param year the year * @param monthOfYear the month of the year, from 1 to 12 * @param dayOfMonth the day of the month, from 1 to 31 * @param hourOfDay the hour of the day, from 0 to 23 * @param minuteOfHour the minute of the hour, from 0 to 59 * @since 2.0 */ public DateTime( int year, int monthOfYear, int dayOfMonth, int hourOfDay, int minuteOfHour) { super(year, monthOfYear, dayOfMonth, hourOfDay, minuteOfHour, 0, 0); } 更多源码内容自行查看
2、格式化时间输出
DateTime dateTime = new DateTime(2015, 12, 21, 0, 0, 0, 333); System.out.println(dateTime.toString("yyyy/MM/dd HH:mm:ss EE"));
访问DateTime实例
当你有一个DateTime实例的时候,就可以调用它的各种方法,获取需要的信息。
with开头的方法(比如:withYear):用来设置DateTime实例到某个时间,因为DateTime是不可变对象,所以没有提供setter方法可供使用,with方法也没有改变原有的对象,而是返回了设置后的一个副本对象。下面这个例子,将2000-02-29的年份设置为1997。值得注意的是,因为1997年没有2月29日,所以自动转为了28日。
DateTime dateTime2000Year = new DateTime(2000,2,29,0,0,0); System.out.println(dateTime2000Year); // out: 2000-02-29T00:00:00.000+08:00 DateTime dateTime1997Year = dateTime2000Year.withYear(1997); System.out.println(dateTime1997Year); // out: 1997-02-28T00:00:00.000+08:00
plus/minus开头的方法(比如:plusDay, minusMonths):用来返回在DateTime实例上增加或减少一段时间后的实例。下面的例子:在当前的时刻加1天,得到了明天这个时刻的时间;在当前的时刻减1个月,得到了上个月这个时刻的时间。
DateTime now = new DateTime(); System.out.println(now); // out:2018-01-02T17:41:18.474+08:00 DateTime tomorrow = now.plusDays(1); System.out.println(tomorrow); // out: 2018-01-03T17:41:18.474+08:00 DateTime lastMonth = now.minusMonths(1); System.out.println(lastMonth); // out: 2017-12-02T17:41:18.474+08:00
注意,在增减时间的时候,想象成自己在翻日历,所有的计算都将符合历法,由Joda-Time自动完成,不会出现非法的日期(比如:3月31日加一个月后,并不会出现4月31日)。
返回Property的方法:Property是DateTime中的属性,保存了一些有用的信息。我们可以通过不同Property中get开头的方法获取一些有用的信息:
DateTime now = new DateTime(); System.out.println(now); // 2018-01-02T17:46:03.336+08:00 System.out.println(now.monthOfYear().getAsText()); // 一月 System.out.println(now.monthOfYear().getAsText(Locale.KOREAN)); // 1월 System.out.println(now.dayOfWeek().getAsShortText()); // 星期二 System.out.println(now.dayOfWeek().getAsShortText(Locale.CHINESE)); // 星期二
有时我们需要对一个DateTime的某些属性进行置0操作。比如,我想得到当天的0点时刻。那么就需要用到Property中round开头的方法(roundFloorCopy)
其它:还有许多其它方法(比如dateTime.year().isLeap()来判断是不是闰年)。它们的详细含义,请参照Java Doc,现查现用,用需求驱动学习。
DateTime now = new DateTime(); System.out.println(now.dayOfWeek().roundCeilingCopy()); // 2018-01-03T00:00:00.000+08:00 周 System.out.println(now.dayOfWeek().roundFloorCopy()); // 2018-01-02T00:00:00.000+08:00 天 System.out.println(now.minuteOfDay().roundFloorCopy()); // 2018-01-02T18:36:00.000+08:00 分 System.out.println(now.secondOfMinute().roundFloorCopy()); // 2018-01-02T18:36:33.000+08:00 秒
Interval和Period
Joda-Time为时间段的表示提供了支持。
Interval:它保存了一个开始时刻和一个结束时刻,因此能够表示一段时间,并进行这段时间的相应操作
Period:它保存了一段时间,比如:6个月,3天,7小时这样的概念。可以直接创建Period,或者从Interval对象构建。
Duration:它保存了一个精确的毫秒数。同样地,可以直接创建Duration,也可以从Interval对象构建。
虽然,这三个类都用来表示时间段,但是在用途上来说还是有一些差别。请看下面的例子:
DateTime dt = new DateTime(2005, 3, 26, 12, 0, 0, 0); DateTime plusPeriod = dt.plus(Period.days(1)); DateTime plusDuration = dt.plus(new Duration(24L * 60L * 60L * 1000L)); System.out.println(dt); // 2005-03-26T12:00:00.000+08:00 System.out.println(plusPeriod); // 2005-03-27T12:00:00.000+08:00 System.out.println(plusDuration); // 2005-03-27T12:00:00.000+08:00
因为当时那个地区执行夏令时的原因,在添加一个Period的时候会添加23个小时。而添加一个Duration,则会精确地添加24个小时,而不考虑历法。所以,Period和Duration的差别不但体现在精度上,也同样体现在语义上。因为有时候按照有些地区的历法 1天不等于 24小时。
日历系统和时区
Joda-Time默认使用的是ISO的日历系统,而ISO的日历系统是世界上公历的事实标准。然而,值得注意的是,ISO日历系统在表示1583年之前的历史时间是不精确的。
Joda-time 支持以下 8 种日历系统:ISO8601( 默认 ) 、Buddhist、Coptic、Ethiopic、Gregorian、GregorianJulian、Islamic、Julian,未来会支持更多(以下为系统翻译,不是很准)
ISO:事实上的世界日历系统,基于ISO - 8601标准
GJ:历史上精确的历法,与朱利安紧随其后的Gregorian
Gregorian:公历系统一直使用的(无产阶级的)
Buddhist:佛教的历法系统,它是多年来由GJ所抵消的
Coptic:科普特日历系统,定义了30天的月
Ethiopic:埃塞俄比亚日历系统,定义了30天的月
Islamic:伊斯兰教,或希吉里,阴历系统
Joda-Time默认使用的是JDK的时区设置。如果需要的话,这个默认值是可以被覆盖的。
Joda-Time使用可插拔的机制来设计日历系统,而JDK则是使用子类的设计,比如GregorianCalendar。下面的代码,通过调用一个工厂方法获得Chronology的实现:
Chronology coptic = CopticChronology.getInstance();
时区是作为chronology的一部分来被实现的。下面的代码获得一个Joda-Time chronology在东京的时区:
DateTimeZone zone = DateTimeZone.forID("Asia/Tokyo"); Chronology gregorianJuian = GJChronology.getInstance(zone); DateTime dateTime = new DateTime(gregorianJuian); System.out.println(new DateTime()); // 2018-01-03T11:05:44.873+08:00 System.out.println(dateTime); // 2018-01-03T12:05:44.975+09:00
局部时间
如果我们只关心日期或者时间,可以使用LocalDate和LocalTime类。
DateTime dateTime=DateTime.now(); System.out.println(dateTime.toString("yyyy-MM-dd HH:mm:ss")); LocalDate localDate=dateTime.toLocalDate(); System.out.println(localDate.toString()); LocalTime localTime=dateTime.toLocalTime(); System.out.println(localTime.toString());
注意:toString()有bug,需要开发者在编码阶段就去保证使用了正确的格式字符串。如:
System.out.println(localDate.toString("yyyy-MM-dd HH:mm:ss")); // 2018-01-03 ��:��:��
jdk 1.8也引入了这两个类,在java.time包中,但是一如既往的不方便操作仍然存在。他们处理这种格式化字符串的方式是抛 UnsupportedTemporalTypeException 异常.
时间格式化
joda通过ISODateTimeFormat类提供了一些工厂方法来创建不同的格式化,如:
System.out.println(DateTime.now().toString(ISODateTimeFormat.dateHourMinuteSecond())); // 2018-01-03T13:12:03
查看源码注释片段
* <p> * This method examines the fields provided and returns an ISO-style * formatter that best fits. This can be useful for outputting * less-common ISO styles, such as YearMonth (YYYY-MM) or MonthDay (--MM-DD). * <p> * The list provided may have overlapping fields, such as dayOfWeek and * dayOfMonth. In this case, the style is chosen based on the following * list, thus in the example, the calendar style is chosen as dayOfMonth * is higher in priority than dayOfWeek: * <ul> * <li>monthOfYear - calendar date style * <li>dayOfYear - ordinal date style * <li>weekOfWeekYear - week date style * <li>dayOfMonth - calendar date style * <li>dayOfWeek - week date style * <li>year * <li>weekyear * </ul> * The supported formats are: * <pre> * Extended Basic Fields * 2005-03-25 20050325 year/monthOfYear/dayOfMonth * 2005-03 2005-03 year/monthOfYear * 2005--25 2005--25 year/dayOfMonth * * 2005 2005 year * --03-25 --0325 monthOfYear/dayOfMonth * --03 --03 monthOfYear * ---03 ---03 dayOfMonth * 2005-084 2005084 year/dayOfYear * -084 -084 dayOfYear * 2005-W12-5 2005W125 weekyear/weekOfWeekyear/dayOfWeek * 2005-W-5 2005W-5 weekyear/dayOfWeek * * 2005-W12 2005W12 weekyear/weekOfWeekyear * -W12-5 -W125 weekOfWeekyear/dayOfWeek * -W12 -W12 weekOfWeekyear * -W-5 -W-5 dayOfWeek * 10:20:30.040 102030.040 hour/minute/second/milli * 10:20:30 102030 hour/minute/second * 10:20 1020 hour/minute * 10 10 hour * -20:30.040 -2030.040 minute/second/milli * -20:30 -2030 minute/second * -20 -20 minute * --30.040 --30.040 second/milli * --30 --30 second * ---.040 ---.040 milli * * 10-30.040 10-30.040 hour/second/milli * * 10:20-.040 1020-.040 hour/minute/milli * * 10-30 10-30 hour/second * * 10--.040 10--.040 hour/milli * * -20-.040 -20-.040 minute/milli *
不过没有提供我们常用的yyyy-MM-dd HH:mm:ss,需要自己实现
DateTimeFormatter format = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); // 时间解析 DateTime dateTime = DateTime.parse("2012-12-21 23:22:45", format); // 时间格式化,输出==> 2012/12/21 23:22:45 星期五 String string_u = dateTime.toString("yyyy/MM/dd HH:mm:ss EE"); System.out.println(string_u); // 格式化带Locale,输出==> 2012年12月21日 23:22:45 星期五 String string_c = dateTime.toString("yyyy年MM月dd日 HH:mm:ss EE", Locale.CHINESE); System.out.println(string_c);
如果你不嫌麻烦,也可以转换两次拼接
DateTime now = DateTime.now(); System.out.println(now.toString(ISODateTimeFormat.yearMonthDay()) + " " + now.toString(ISODateTimeFormat.hourMinuteSecondMillis())); // 2018-01-03 14:27:37.131
Property
joda为每一个时间类创建一个内部静态类,叫Property,便于访问实例中的各个字段。而Joda更赋予了它强大的能力,就像穿越一样在时间线上游走。
由于它存在于多个类中,如果你希望先构建这个对象再使用,import包会有点小麻烦,因为名字一样存在于不同的类中。如:
DateTime dateTime=DateTime.now(); Property property=dateTime.dayOfYear(); System.out.println(property.get());
这时应该 import org.joda.time.DateTime.Property;
有时使用ide时有可能误操作导入org.joda.time.YearMonth.Property;
等等。
正确的使用姿势是,把这个Property作为一个中间变量。
DateTime dateTime= new DateTime(2017,12,30,10,10,10); System.out.println(dateTime.dayOfWeek().get()); // 6 星期六 System.out.println(dateTime.dayOfMonth().get()); // 30 30日 System.out.println(dateTime.dayOfYear().get()); // 364 第364天 System.out.println(dateTime.dayOfMonth().getMaximumValue()); // 当月最后一天 31 System.out.println(dateTime.dayOfMonth().withMaximumValue()); // 当月最后一天DateTime:2017-12-31T10:10:10.000+08:00
类似的Property有:
yearOfCentury dayOfYear monthOfYear dayOfMonth dayOfWeek
泛泛的说这些不太容易说清楚,看例子:
DateTime dateTime = new DateTime(2017, 2, 21, 0, 0); System.out.println("Hello,我来过:" + dateTime.dayOfMonth() .setCopy(28) // 穿越到2017-02-28 .minusYears(9) // 穿越到9年前 .dayOfMonth() .withMaximumValue() // 穿越到那年那月的最后一天,那天是29日 .dayOfWeek() // .get() //29日那天是星期五 .setCopy(1)); // (不管29日是星期几)穿越到29日那天所在的星期一 // Hello,我来过:2008-02-25T00:00:00.000+08:00
说明一下:setCopy是为前面的属性对应的字段指定一个值,并返回这个DateTime实例;withMaximumValue()是将这个值设置为该字段的最大值,并返回这个DateTime实例,它相当于是setCopy的一个特殊情况,withMinimumValue()同样的道理。
所以可以知道,最后一个setCopy(1)
替换为 withMinimumValue()
结论是一样的。
计算两日期相差的天数
LocalDate start = new LocalDate(2012, 12, 14); LocalDate end = new LocalDate(2012, 12, 15); int days = Days.daysBetween(start, end).getDays(); DateTime start = new DateTime(2005, 3, 26, 12, 0, 0, 0); DateTime end = new DateTime(2005, 3, 28, 12, 0, 0, 0); int days = Days.daysBetween(start, end).getDays();
获取18天之后的某天在下个月的当前周的第一天日期
DateTime now = new DateTime(); String dateStr = now.plusDays(18).plusMonths(1).dayOfWeek().withMinimumValue().toString("yyyy-MM-dd HH:mm:ss"); System.out.println(now); // 2018-01-03T10:35:08.402+08:00 System.out.println(dateStr); // 2018-02-19 10:32:02
到新年还有多少天
LocalDate fromDate = new DateTime(2018,1,3,10,10,10).toLocalDate(); LocalDate newYear = fromDate.plusYears(1).withDayOfYear(1); System.out.println(Days.daysBetween(fromDate, newYear).getDays() + "天"); // 363天
计算间隔和区间
DateTime begin = new DateTime("2018-01-03"); DateTime end = new DateTime("2018-03-01"); // 计算区间毫秒数 Duration d = new Duration(begin, end); long millis = d.getMillis(); // 计算区间天数 Period p = new Period(begin, end, PeriodType.days()); int days = p.getDays(); // 计算特定日期是否在该区间内 Interval interval = new Interval(begin, end); boolean contained = interval.contains(new DateTime("2018-03-01")); //【大于等于begin,小于end】true System.out.println("间隔:" + millis + " 毫秒"); // 间隔:4924800000 毫秒 System.out.println("间隔:" + days + " 天"); // 间隔:57 天 System.out.println(contained); // true 表示包含在区间内
日期比较
DateTime d1 = new DateTime("2015-10-01"); DateTime d2 = new DateTime("2016-02-01"); //和系统时间比 boolean b1 = d1.isAfterNow(); boolean b2 = d1.isBeforeNow(); boolean b3 = d1.isEqualNow(); //和其他日期比 boolean f1 = d1.isAfter(d2); boolean f2 = d1.isBefore(d2); boolean f3 = d1.isEqual(d2);
与JDK互操作
// 通过jdk时间对象构造 Date date = new Date(); DateTime dateTime = new DateTime(date); Calendar calendar = Calendar.getInstance(); dateTime = new DateTime(calendar); // Joda-time 各种操作..... dateTime = dateTime.plusDays(1) // 增加天 .plusYears(1)// 增加年 .plusMonths(1)// 增加月 .plusWeeks(1)// 增加星期 .minusMillis(1)// 减分钟 .minusHours(1)// 减小时 .minusSeconds(1);// 减秒数 // 计算完转换成jdk 对象 Date date2 = dateTime.toDate(); Calendar calendar2 = dateTime.toCalendar(Locale.CHINA);
下面通过一个简单的demo来实战一下,输入一个日期(生日,格式:yyyy-MM-dd Or yyyy-MM-dd HH:mm:ss),计算出今天是你人生的第多少天/小时/分/秒,代码如下:
import org.joda.time.*; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.Scanner; public class DayOfLife { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.println("*********请输入生日(yyyy-MM-dd Or yyyy-MM-dd HH:mm:ss)*********"); while (true) { String line = scanner.nextLine(); if (line.equals("exit")) break; System.out.println(">>>" + line); DateTimeFormatter format = null; if (line.length() == 10) { format = DateTimeFormat.forPattern("yyyy-MM-dd"); } else { format = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); } DateTime startDateTime = null; try { startDateTime = DateTime.parse(line, format); } catch (Exception e) { System.err.println("输入格式错误,请重新输入生日!"); continue; } calDays(startDateTime, new DateTime()); } } catch (Exception e) { e.printStackTrace(); } finally { scanner.close(); } } private static void calDays(DateTime startDateTime, DateTime endDateTime) { Days days = Days.daysBetween(startDateTime, endDateTime); System.out.println("今天是你人生的第" + days.getDays() + "天"); Hours hours = Hours.hoursBetween(startDateTime, endDateTime); System.out.println("今天是你人生的第" + hours.getHours() + "小时"); Minutes minutes = Minutes.minutesBetween(startDateTime, endDateTime); System.out.println("今天是你人生的第" + minutes.getMinutes() + "分钟"); Seconds seconds = Seconds.secondsBetween(startDateTime, endDateTime); System.out.println("今天是你人生的第" + seconds.getSeconds() + "秒"); } }
结语
这篇文章参考了Joda-Time的官方文档:Quick Start,并加上了自己的理解。
涉及到更多的需求和用法(比如“日期时间的格式化”等),可以参考官方文档:User Guide。
参考链接:
http://www.joda.org/joda-time/quickstart.html
http://www.ibm.com/developerworks/cn/java/j-jodatime.html
http://www.docjar.org/docs/api/org/joda/time/format/PeriodFormatterBuilder.htmlhttps://programtalk.com/vs/joda-time/src/test/java/org/joda/time/format/TestPeriodFormatterBuilder.java/