日期和时间
本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
5.1 Date类
Date是JDK 1.0中java.util包下提供类,Date表示时刻,内部主要是一个long类型的值,表示特定的瞬间,可以精确到毫秒,如下所示:
private transient long fastTime;
Date有两个构造方法:
public Date(long date) {fastTime = date;
}
public Date() {this(System.currentTimeMillis());
}
第一个构造方法是根据传入的毫秒数进行初始化;第二个构造方法是默认构造方法,它根据System.currentTimeMillis()的返回值进行初始化。System.currentTimeMillis()是一个常用的方法,它返回当前时刻距离纪元时的毫秒数。Date中的大部分方法都已经过时了,其中没有过时的主要方法有下面这些:
public long getTime() // 返回毫秒数
public boolean equals(Object obj) // 主要就是比较内部的毫秒数是否相同
public int compareTo(Date anotherDate) // 与其他Date进行比较,如果当前Date的毫秒数小于参数中的返回-1,相同返回0,否则返回1
public boolean before(Date when) // 判断是否在给定日期之前
public boolean after(Date when) // 判断是否在给定日期之后
public int hashCode() // 哈希值算法与Long类似
5.2 TimeZone
TimeZone表示时区,它是一个抽象类,有静态方法用于获取其实例。获取当前的默认时区,代码为:
    @Testpublic void testTimeZone(){assertTrue("Asia/Shanghai".equals( TimeZone.getDefault().getID()));}
获取默认时区,并输出其ID,每个人的电脑可能根据实际情况不同,并不一定都是Asia/Shanghai。
默认时区是在哪里设置的呢?可以更改吗?
更改时区可以使用TimeZone.setDefault和JVM参数指定。
使用TimeZone.setDefault设置默认时区代码如下:
    @Testpublic void testTimeZoneSetDefault(){TimeZone.setDefault(TimeZone.getTimeZone("GMT+08:00"));assertTrue("GMT+08:00".equals( TimeZone.getDefault().getID()));}
使用JVM参数修改如下所示:
java -Duser.timezone=Asia/Shanghai
TimeZone也有静态方法,可以获得任意给定时区的实例。比如,获取美国东部时区:
TimeZone tz = TimeZone.getTimeZone("US/Eastern");
ID除了可以是名称外,还可以是GMT形式表示的时区,如:
TimeZone tz = TimeZone.getTimeZone("GMT+08:00");
5.3 Locale
Locale表示国家(或地区)和语言,它有两个主要参数:一个是国家(或地区);另一个是语言,每个参数都有一个代码,不过国家(或地区)并不是必需的。比如,中国内地的代码是CN,中国台湾地区的代码是TW,美国的代码是US,中文语言的代码是zh,英文语言的代码是en。Locale类中定义了一些静态变量,表示常见的Locale,比如:
- Locale.US:表示美国英语。
- Locale.ENGLISH:表示所有英语。
- Locale.TAIWAN:表示中国台湾地区所用的中文。
- Locale.CHINESE:表示所有中文。
- Locale.SIMPLIFIED_CHINESE:表示中国内地所用的中文。
与TimeZone类似,Locale也有静态方法获取默认值,如:
    @Testpublic void testLocale(){Locale locale = Locale.getDefault();assertTrue("zh_CN".equals(locale.toString()));}
5.4 Calendar
java.util.Calendar类是日期和时间操作中的主要类,它表示与TimeZone和Locale相关的日历信息,可以进行各种相关的运算。我们先来看下它的内部组成,与Date类似,Calendar内部也有一个表示时刻的毫秒数,定义为:
protected long time;
除此之外,Calendar内部还有一个数组,表示日历中各个字段的值,定义为:
protected int fields[];
这个数组的长度为17,保存一个日期中各个字段的值,都有哪些字段呢?Calendar类中定义了一些静态变量,表示这些字段,主要有:
- Calendar.YEAR:表示年。
- Calendar.MONTH:表示月, 1 1 1月是 0 0 0,- Calendar同样定义了表示各个月份的静态变量,如- Calendar.JULY表示 7 7 7月。
- Calendar.DAY_OF_MONTH:表示日,每月的第一天是 1 1 1。
- Calendar.HOUR_OF_DAY:表示小时,为 0 ~ 23 0~23 0~23。
- Calendar.MINUTE:表示分钟,为 0 ~ 59 0~59 0~59。
- Calendar.SECOND:表示秒,为 0 ~ 59 0~59 0~59。
- Calendar.MILLISECOND:表示毫秒,为 0 ~ 999 0~999 0~999。
- Calendar.DAY_OF_WEEK:表示星期几,周日是 1 1 1,周一是 2 2 2,周六是 7 7 7,- Calenar同样定义了表示各个星期的静态变量,如- Calendar.SUNDAY表示周日。
Calendar是抽象类,不能直接创建对象,它提供了多个静态方法,可以获取Calendar实例,比如:
public static Calendar getInstance()
{Locale aLocale = Locale.getDefault(Locale.Category.FORMAT);return createCalendar(defaultTimeZone(aLocale), aLocale);
}public static Calendar getInstance(TimeZone zone,Locale aLocale)
{return createCalendar(zone, aLocale);
}
最终调用的方法都是需要TimeZone和Locale的,如果没有,则会使用上面介绍的默认值。getInstance方法会根据TimeZone和Locale创建对应的Calendar子类对象,在中文系统中,子类一般是表示公历的GregorianCalendar。getInstance方法封装了Calendar对象创建的细节。TimeZone和Locale不同,具体的子类可能不同,但都是Calendar。
来看代码,输出当前时间的各种信息:
    @Testpublic void testCalendar() {Calendar calendar = Calendar.getInstance();calendar.setTimeInMillis(1709045743587L);assertTrue("year: 2024 month: 1 day: 27 hour: 22 minute: 55 second: 43 millisecond: 587 day_of_week: 3".equals("year: " + calendar.get(Calendar.YEAR) + " month: " + calendar.get(Calendar.MONTH) + " day: "+ calendar.get(Calendar.DAY_OF_MONTH) + " hour: " + calendar.get(Calendar.HOUR_OF_DAY)+ " minute: "+ calendar.get(Calendar.MINUTE) + " second: " + calendar.get(Calendar.SECOND) + " millisecond: "+ calendar.get(Calendar.MILLISECOND) + " day_of_week: " + calendar.get(Calendar.DAY_OF_WEEK)));}
Calendar内部,会将表示时刻的毫秒数,按照TimeZone和Locale对应的年历,计算各个日历字段的值,存放在fields数组中,Calendar.get方法获取的就是fields数组中对应字段的值。调用函数:setTimeInMillis(long millis)和setTime(Date date),Calendar支持根据Date或毫秒数设置时间,也支持根据年月日等日历字段设置时间,比如:
public final void set(int year, int month, int date)
public final void set(int year, int month, int date, int hourOfDay, int minute, int second)
public void set(int field, int value)
除了直接设置,Calendar支持根据字段增加和减少时间:
public void add(int field, int amount)
amount为正数表示增加,负数表示减少。比如,如果想设置Calendar为第二天的下午 2 2 2点 15 15 15,代码可以为:
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 14);
calendar.set(Calendar.MINUTE, 15);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Calendar的这些方法中一个比较方便和强大的地方在于,它能够自动调整相关的字段。比如,我们知道 2 2 2月最多有 29 29 29天,如果当前时间为 1 1 1月 30 30 30号,对Calendar.MONTH字段加 1 1 1,即增加一月,Calendar不是简单的只对月字段加 1 1 1,那样日期是 2 2 2月 30 30 30号,是无效的,Calendar会自动调整为 2 2 2月最后一天,即 2 2 2月 28 28 28日或 29 29 29日。再如,设置的值可以超出其字段最大范围,Calendar会自动更新其他字段,如:
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR_OF_DAY, 48);
calendar.add(Calendar.MINUTE, -120);
相当于增加了 46 46 46小时。
内部,根据字段设置或修改时间时,Calendar会更新fields数组对应字段的值,但一般不会立即更新其他相关字段或内部的毫秒数的值,不过在获取时间或字段值的时候, Calendar会重新计算并更新相关字段。
简单总结下,Calenar做了一项非常烦琐的工作,根据TimeZone和Locale,在绝对时间毫秒数和日历字段之间自动进行转换,且对不同日历字段的修改进行自动同步更新。除了add方法,Calendar还有一个类似的方法:
calendar.roll(Calendar.MINUTE, 3);
与add方法的区别是,roll方法不影响时间范围更大的字段值。比如:
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 13);
calendar.set(Calendar.MINUTE, 59);
calendar.add(Calendar.MINUTE, 3);
calendar首先设置为13:59,然后分钟字段加 3 3 3,执行后的calendar时间为14:02。如果add改为roll,即:
calendar.roll(Calendar.MINUTE, 3);
则执行后的calendar时间会变为13:02,在分钟字段上执行roll方法不会改变小时的值。Calendar可以方便地转换为Date或毫秒数,方法是:
public final Date getTime()
public long getTimeInMillis()
与Date类似,Calendar之间也可以进行比较,也实现了Comparable接口,相关方法有:
public boolean equals(Object obj)
public int compareTo(Calendar anotherCalendar)
public boolean after(Object when)
public boolean before(Object when)
5.5 DateFormat
DateFormat类主要在Date和字符串表示之间进行相互转换,它有两个主要的方法:
public final String format(Date date)
public Date parse(String source)
format将Date转换为字符串,parse将字符串转换为Date。Date的字符串表示与TimeZone和Locale都是相关的,除此之外,还与两个格式化风格有关,一个是日期的格式化风格,另一个是时间的格式化风格。DateFormat定义了4个静态变量,表示 4 4 4种风格:SHORT、MEDIUM、LONG和FULL;还定义了一个静态变量DEFAULT,表示默认风格,值为MEDIUM,不同风格输出的信息详细程度不同。
与Calendar类似,DateFormat也是抽象类,也用工厂方法创建对象,提供了多个静态方法创建DateFormat对象,有三类方法:
public static final DateFormat getDateInstance();
public static final DateFormat getDateTimeInstance();
public static final DateFormat getTimeInstance();
getDateTimeInstance方法既处理日期也处理时间,getDateInstance方法只处理日期,getTimeInstance方法只处理时间。看下面的代码:
    @Testpublic void testDateFormat() {Calendar calendar = Calendar.getInstance();// 2016-08-15 14:15:20calendar.set(2016, 07, 15, 14, 15, 20);assertTrue("2016年8月15日 14:15:20".equals(DateFormat.getDateTimeInstance().format(calendar.getTime())));assertTrue("2016年8月15日".equals(DateFormat.getDateInstance().format(calendar.getTime())));assertTrue("14:15:20".equals(DateFormat.getTimeInstance().format(calendar.getTime())));}
每类工厂方法都有两个重载的方法,接受日期和时间风格以及Locale作为参数:
public static final DateFormat getDateTimeInstance(int dateStyle, int timeStyle);
public static final DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale)
比如,看下面的代码:
    @Testpublic void testDataFormatStyleLocale() {Calendar calendar = Calendar.getInstance();// 2016-08-15 14:15:20calendar.set(2016, 07, 15, 14, 15, 20);assertTrue("2016年8月15日 CST 14:15:20".equals(DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.CHINESE).format(calendar.getTime())));}
DateFormat的工厂方法里,我们没看到TimeZone参数,不过,DateFormat提供了一个setter方法,可以设置TimeZone:
    public void setTimeZone(TimeZone zone){calendar.setTimeZone(zone);}
DateFormat虽然比较方便,但如果我们要对字符串格式有更精确的控制,则应该使用SimpleDateFormat这个类。
5.6 SimpleDateFormat
SimpleDateFormat是DateFormat的子类,相比DateFormat,它的一个主要不同是,它可以接受一个自定义的模式(pattern)作为参数,这个模式规定了Date的字符串形式。
    @Testpublic void testSimpleDateFormatFormat() {Calendar calendar = Calendar.getInstance();// 2016-08-15 14:15:20calendar.set(2016, 07, 15, 14, 15, 20);SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 E HH时mm分ss秒");assertTrue("2016年08月15日 周一 14时15分20秒".equals(sdf.format(calendar.getTime())));}
SimpleDateFormat有个构造方法,可以接受一个pattern作为参数,上面例子pattern是:yyyy年MM月dd日 E HH时mm分ss秒。
pattern中含义如下表格所示:
| Letter | Date or Time Component | Presentation | Examples | 
|---|---|---|---|
| G | Era designator | Text | AD | 
| y | Year | Year | 1996;96 | 
| Y | Week year | Year | 2009;09 | 
| M | Month in year (context sensitive) | Month | July;Jul;07 | 
| L | Month in year (standalone form) | Month | July;Jul;07 | 
| w | Week in year | Number | 27 | 
| W | Week in month | Number | 2 | 
| D | Day in year | Number | 189 | 
| d | Day in month | Number | 10 | 
| F | Day of week in month | Number | 2 | 
| E | Day name in week | Text | Tuesday;Tue | 
| u | Day number of week (1 = Monday, …, 7 = Sunday) | Number | 1 | 
| a | Am/pm marker | Text | PM | 
| H | Hour in day (0-23) | Number | 0 | 
| k | Hour in day (1-24) | Number | 24 | 
| K | Hour in am/pm (0-11) | Number | 0 | 
| h | Hour in am/pm (1-12) | Number | 12 | 
| m | Minute in hour | Number | 30 | 
| s | Second in minute | Number | 55 | 
| S | Millisecond | Number | 978 | 
| z | Time zone | General time zone | Pacific Standard Time;PST;GMT-08:00 | 
| Z | Time zone | RFC 822 time zone | -0800 | 
| X | Time zone | ISO 8601 time zone | -08;-0800;-08:00 | 
常见例子如下所示:
| Date and Time Pattern | Result | 
|---|---|
| "yyyy.MM.dd G 'at' HH:mm:ss z" | 2001.07.04 AD at 12:08:56 PDT | 
| "EEE, MMM d, ''yy" | Wed, Jul 4, '01 | 
| "h:mm a" | 12:08 PM | 
| "hh 'o''clock' a, zzzz" | 12 o'clock PM, Pacific Daylight Time | 
| "K:mm a, z" | 0:08 PM, PDT | 
| "yyyyy.MMMMM.dd GGG hh:mm aaa" | 02001.July.04 AD 12:08 PM | 
| "EEE, d MMM yyyy HH:mm:ss Z" | Wed, 4 Jul 2001 12:08:56 -0700 | 
| "yyMMddHHmmssZ" | 010704120856-0700 | 
| "yyyy-MM-dd'T'HH:mm:ss.SSSZ" | 2001-07-04T12:08:56.235-0700 | 
| "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" | 2001-07-04T12:08:56.235-07:00 | 
| "YYYY-'W'ww-u" | 2001-W27-3 | 
除了将Date转换为字符串,SimpleDateFormat也可以方便地将字符串转化为Date,如下所示代码:
    @Testpublic void testSimpleDateFormatParse() throws ParseException {String str = "2016-08-15 14:15:20.456";SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");try {Date date = sdf.parse(str);SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年M月d h:m:s.S a");System.out.println(sdf2.format(date));assertTrue("2016年8月15 2:15:20.456 下午".equals(sdf2.format(date)));} catch (ParseException e) {throw e;}}
代码将字符串解析为了一个Date对象,然后使用另外一个格式进行了输出,这里SSS表示三位的毫秒数。需要注意的是,parse会抛出一个受检异常,异常类型为ParseException,调用者必须进行处理。
5.7 局限性
Date表示时刻,与年月日无关,Calendar表示日历,与时区和Locale相关,可进行各种运算,是日期时间操作的主要类,DateFormat/SimpleDateFormat在Date和字符串之间进行相互转换。这些API存在着一些局限性,如下:
- 可变性:像日期和时间这样的类应该是不可变的,某一个日期时间对象都只能代表某一个特定的瞬间。
- 偏移性:Date类中的年份是从 1900 1900 1900开始的,月份都是从 0 0 0开始的,这不符合常规编程习惯。
- 格式化:用于日期格式化及解析的SimpleDateFormat只对Date类有用,Calendar类则不行。
- 线程不安全性。
- 不能处理闰秒:由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),所以在世界时(民用时)和原子时之间相差超过到 ± 0.9 ±0.9 ±0.9秒时,人们就把协调世界时向前拨 1 1 1秒(负闰秒,最后一分钟为 59 59 59秒)或向后拨 1 1 1秒(正闰秒,最后一分钟为 61 61 61秒)。目前,全球已经进行了 27 27 27次闰秒,均为正闰秒。最近一次闰秒是北京时间 2017 2017 2017年 1 1 1月 1 1 1日 7 7 7时 59 59 59分 59 59 59秒。
5.8 Java 8的日期和时间API
Java 8中引入的java.time纠正了过去的缺陷,引入的类主要有:
- Instant:表示时刻,不直接对应年月日信息,需要通过时区转换;
- LocalDateTime:表示与时区无关的日期和时间,不直接对应时刻,需要通过时区转换;
- ZoneId/ZoneOffset:表示时区;
- LocalDate:表示与时区无关的日期,与- LocalDateTime相比,只有日期,没有时间信息;
- LocalTime:表示与时区无关的时间,与- LocalDateTime相比,只有时间,没有日期信息;
- ZonedDateTime:表示特定时区的日期和时间;
- Duration:持续时间;
5.9 Instant
Instant表示时刻,获取当前时刻,代码为:
Instant now = Instant.now();
可以根据Epoch Time(纪元时)创建Instant。比如,另一种获取当前时刻的代码可以为:
Instant now = Instant.ofEpochMilli(System.currentTimeMillis());
我们知道,Date也表示时刻,Instant和Date可以通过纪元时相互转换,代码为:
    @Testpublic void testInstantDate(){Instant instant = Instant.ofEpochMilli(1709045743587L);// Instant 转为 DateDate d = Date.from(instant);assertTrue("Tue Feb 27 22:55:43 CST 2024".equals(d.toString()));// Date 转为 InstantInstant i = d.toInstant();assertTrue("2024-02-27T14:55:43.587Z".equals(i.toString()));}
5.10 LocalDateTime
LocalDateTime表示与时区无关的日期和时间,获取系统默认时区的当前日期和时间,代码为:
LocalDateTime ldt = LocalDateTime.now();
还可以直接用年月日等信息构建LocalDateTime。比如,表示 2017 2017 2017年 7 7 7月 11 11 11日 20 20 20点 45 45 45分 5 5 5秒,代码可以为:
LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);
LocalDateTime有很多方法,可以获取年月日时分秒等日历信息,比如:
public int getYear()
public int getMonthValue()
public int getDayOfMonth()
public int getHour()
public int getMinute()
public int getSecond()
public DayOfWeek getDayOfWeek()
5.11 LocalDate/LocalTime
可以认为LocalDateTime由两部分组成,一部分是日期LocalDate,另一部分是时间LocalTime。它们的用法也很直观,比如:
//表示2017年7月11日
LocalDate ld = LocalDate.of(2017, 7, 11);
//当前时刻按系统默认时区解读的日期
LocalDate now = LocalDate.now();
//表示21点10分34秒
LocalTime lt = LocalTime.of(21, 10, 34);
//当前时刻按系统默认时区解读的时间
LocalTime time = LocalTime.now();
LocalDateTime由LocalDate和LocalTime构成,LocalDate加上时间可以构成LocalDateTime,LocalTime加上日期可以构成LocalDateTime,比如:
LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);
LocalDate ld = ldt.toLocalDate(); //2017-07-11
LocalTime lt = ldt.toLocalTime(); // 20:45:05
//LocalDate加上时间,结果为2017-07-11 21:18:39
LocalDateTime ldt2 = ld.atTime(21, 18, 39);
//LocalTime加上日期,结果为2016-03-24 20:45:05
LocalDateTime ldt3 = lt.atDate(LocalDate.of(2016, 3, 24));
5.12 ZoneId/ZoneOffset
LocalDateTime不能直接转为时刻Instant,转换需要一个参数ZoneOffset,ZoneOffset表示相对于格林尼治的时区差,北京是 + 08 : 00 +08:00 +08:00。比如,转换一个LocalDateTime为北京的时刻,方法为:
public static Instant toBeijingInstant(LocalDateTime ldt) {return ldt.toInstant(ZoneOffset.of("+08:00"));
}
给定一个时刻,使用不同时区解读,日历信息是不同的,Instant有方法根据时区返回一个ZonedDateTime:
public ZonedDateTime atZone(ZoneId zone)
默认时区是ZoneId.systemDefault(),可以这样构建ZoneId:
// 北京时区
ZoneId bjZone = ZoneId.of("GMT+08:00")
ZoneOffset是ZoneId的子类,可以根据时区差构造。
5.13 ZonedDateTime
ZonedDateTime表示特定时区的日期和时间,获取系统默认时区的当前日期和时间,代码为:
ZonedDateTime zdt = ZonedDateTime.now();
LocalDateTime.now也是获取默认时区的当前日期和时间,有什么区别呢?LocalDateTime内部不会记录时区信息,只会单纯记录年月日时分秒等信息,而ZonedDateTime除了记录日历信息,还会记录时区,它的其他大部分构建方法都需要显式传递时区,比如:
// 根据 Instant 和时区构建 ZonedDateTime
public static ZonedDateTime ofInstant(Instant instant, ZoneId zone)
// 根据 LocalDate、LocalTime 和 ZoneId 构造
public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone)
ZonedDateTime可以直接转换为Instant,比如:
ZonedDateTime ldt = ZonedDateTime.now();
Instant now = ldt.toInstant();
5.14 格式化
Java 8中,主要的格式化类是java.time.format.DateTimeFormatter,它是线程安全的,看个例子:[插图]输出为:[插图]将字符串转化为日期和时间对象,可以使用对应类的parse方法,比如:
    @Testpublic void testDateTimeFormatterFormat(){DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");LocalDateTime ldt = LocalDateTime.of(2016,8,18,14,20,45);assertTrue("2016-08-18 14:20:45".equals(formatter.format(ldt)));}
将字符串转化为日期和时间对象,可以使用对应类的parse方法,比如:
    @Testpublic void testDateTimeFormatterParse(){DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String str = "2016-08-18 14:20:45";LocalDateTime ldt = LocalDateTime.parse(str, formatter);assertTrue("2016-08-18T14:20:45".equals(ldt.toString()));}
5.15 设置和修改时间
修改时期和时间有两种方式,一种是直接设置绝对值,另一种是在现有值的基础上进行相对增减操作,Java 8的大部分类都支持这两种方式。另外,Java 8的大部分类都是不可变类,修改操作是通过创建并返回新对象来实现的,原对象本身不会变。我们来看一些例子。
调整时间为下午 3 3 3点 20 20 20分,代码为:
LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.withHour(15).withMinute(20).withSecond(0).withNano(0);
还可以为:
LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.toLocalDate().atTime(15, 20);
3 3 3小时 5 5 5分钟后,示例代码为:
LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusHours(3).plusMinutes(5);
LocalDateTime有很多plusXXX和minusXXX方法,分别用于相对增加和减少时间。
今天 0 0 0点,可以为:
LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.with(ChronoField.MILLI_OF_DAY, 0);
ChronoField是一个枚举,里面定义了很多表示日历的字段,MILLI_OF_DAY表示在一天中的毫秒数,值从 0 0 0到 ( 24 ∗ 60 ∗ 60 ∗ 1000 ) − 1 (24 * 60 * 60 * 1000)-1 (24∗60∗60∗1000)−1。还可以为:
LocalDateTime ldt = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
LocalTime.MIN表示 00 : 00 00:00 00:00。也可以为:
LocalDateTime ldt = LocalDate.now().atTime(0, 0);
下周二上午 10 10 10点整,可以为:
LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusWeeks(1).with(ChronoField.DAY_OF_WEEK, 2).with(ChronoField.MILLI_OF_DAY, 0).withHour(10);
上面下周二指定是下周,如果是下一个周二呢?这与当前是周几有关,如果当前是周一,则下一个周二就是明天,而其他情况则是下周,代码可以为:
LocalDate ld = LocalDate.now();
if(! ld.getDayOfWeek().equals(DayOfWeek.MONDAY)){ld = ld.plusWeeks(1);
}
LocalDateTime ldt = ld.with(ChronoField.DAY_OF_WEEK, 2).atTime(10, 0);
针对这种复杂一点的调整,Java 8有一个专门的接口TemporalAdjuster,这是一个函数式接口,定义为:
public interface TemporalAdjuster {Temporal adjustInto(Temporal temporal);
}
Temporal是一个接口,表示日期或时间对象,Instant、LocalDateTime和LocalDate等都实现了它,这个接口就是对日期或时间进行调整,还有一个专门的类TemporalAdjusters,里面提供了很多TemporalAdjuster的实现。比如,针对下一个周几的调整,方法是:
public static TemporalAdjuster next(DayOfWeek dayOfWeek)
针对上面的例子,代码可以为:
LocalDate ld = LocalDate.now();
LocalDateTime ldt = ld.with(TemporalAdjusters.next(DayOfWeek.TUESDAY)).atTime(10, 0);
TemporalAdjusters中还有很多方法,部分方法如下:
public static TemporalAdjuster firstDayOfMonth()
public static TemporalAdjuster lastDayOfMonth()
public static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek)
public static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek)
public static TemporalAdjuster previous(DayOfWeek dayOfWeek)
public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek)
一些例子如下:
    @Testpublic void testTemporalAdjusters() {LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);// 下周二上午 10 点整LocalDateTime nextThu = ldt.with(TemporalAdjusters.next(DayOfWeek.TUESDAY)).toLocalDate().atTime(10, 0);assertTrue("2017-07-18T10:00".equals(nextThu.toString()));// 本月最后一天最后一刻LocalDateTime thisMonthLastDayLastTime = ldt.with(TemporalAdjusters.lastDayOfMonth()).toLocalDate().atTime(LocalTime.MAX);assertTrue("2017-07-31T23:59:59.999999999".equals(thisMonthLastDayLastTime.toString()));long maxDayOfMonth = ldt.range(ChronoField.DAY_OF_MONTH).getMaximum();LocalDateTime thisMonthLastDayLastTime2 = ldt.withDayOfMonth((int) maxDayOfMonth).toLocalDate().atTime(LocalTime.MAX);assertTrue("2017-07-31T23:59:59.999999999".equals(thisMonthLastDayLastTime2.toString()));// 下个月第一个周一的下午5点整LocalDateTime nextMonthFirstMondaySeventeen = ldt.plusMonths(1).with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)).toLocalDate().atTime(17, 0);assertTrue("2017-08-07T17:00".equals(nextMonthFirstMondaySeventeen.toString()));}
5.16 时间段的计算
Java 8中表示时间段的类主要有两个:Period和Duration。Period表示日期之间的差,用年月日表示,不能表示时间;Duration表示时间差,用时分秒等表示,也可以用天表示,一天严格等于 24 24 24小时,不能用年月表示。下面看一些例子。计算两个日期之间的差,看个Period的例子:
    @Testpublic void testPeriod() {LocalDate ld1 = LocalDate.of(2016, 3, 24);LocalDate ld2 = LocalDate.of(2017, 7, 12);Period period = Period.between(ld1, ld2);assertTrue("1年3月18天".equals(period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天"));}
根据生日计算年龄,示例代码可以为:
LocalDate born = LocalDate.of(1990,06,20);
int year = Period.between(born, LocalDate.now()).getYears();
计算迟到分钟数,假定早上 9 9 9点是上班时间,过了 9 9 9点算迟到,迟到要统计迟到的分钟数,怎么计算呢?看代码:
    @Testpublic void testDuration() {long lateMinutes = Duration.between(LocalTime.of(9, 0), LocalDateTime.of(2017, 7, 11, 20, 45, 5)).toMinutes();assertTrue(705 == lateMinutes);}
5.17 与 Date/Calendar 对象的转换
Java 8的日期和时间API没有提供与老的Date/Calendar相互转换的方法,但在实际中,我们可能是需要的。前面介绍了Date可以与Instant通过毫秒数相互转换,对于其他类型,也可以通过毫秒数/Instant相互转换。比如,将LocalDateTime按默认时区转换为Date,代码可以为:
public static Date toDate(LocalDateTime ldt){return new Date(ldt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
将ZonedDateTime转换为Calendar,代码可以为:
public static Calendar toCalendar(ZonedDateTime zdt) {TimeZone tz = TimeZone.getTimeZone(zdt.getZone());Calendar calendar = Calendar.getInstance(tz);calendar.setTimeInMillis(zdt.toInstant().toEpochMilli());return calendar;
}
Calendar保持了ZonedDateTime的时区信息。
将Date按默认时区转换为LocalDateTime,代码可以为:
public static LocalDateTime toLocalDateTime(Date date) {return LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()),ZoneId.systemDefault());
}
将Calendar转换为ZonedDateTime,代码可以为:
public static ZonedDateTime toZonedDateTime(Calendar calendar) {ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(calendar.getTimeInMillis()),calendar.getTimeZone().toZoneId());return zdt;}
- 马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎ 
- 尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎