强大易用的日期和时间库 Joda Time 详细用法

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.html

https://programtalk.com/vs/joda-time/src/test/java/org/joda/time/format/TestPeriodFormatterBuilder.java/


赞(52) 打赏
未经允许不得转载:优客志 » JAVA开发
分享到:

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏