十九、日期和时间

日期和时间

本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记

5.1 Date类

DateJDK 1.0java.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表示时区,它是一个抽象类,有静态方法用于获取其实例。获取当前的默认时区,代码为:

    @Test
    public void testTimeZone(){
        assertTrue("Asia/Shanghai".equals( TimeZone.getDefault().getID()));
    }

获取默认时区,并输出其ID,每个人的电脑可能根据实际情况不同,并不一定都是Asia/Shanghai

默认时区是在哪里设置的呢?可以更改吗?

更改时区可以使用TimeZone.setDefaultJVM参数指定。

使用TimeZone.setDefault设置默认时区代码如下:

    @Test
    public 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,英文语言的代码是enLocale类中定义了一些静态变量,表示常见的Locale,比如:

  • Locale.US:表示美国英语。
  • Locale.ENGLISH:表示所有英语。
  • Locale.TAIWAN:表示中国台湾地区所用的中文。
  • Locale.CHINESE:表示所有中文。
  • Locale.SIMPLIFIED_CHINESE:表示中国内地所用的中文。

TimeZone类似,Locale也有静态方法获取默认值,如:

    @Test
    public void testLocale(){
        Locale locale = Locale.getDefault();
        assertTrue("zh_CN".equals(locale.toString()));
    }

5.4 Calendar

java.util.Calendar类是日期和时间操作中的主要类,它表示与TimeZoneLocale相关的日历信息,可以进行各种相关的运算。我们先来看下它的内部组成,与Date类似,Calendar内部也有一个表示时刻的毫秒数,定义为:

protected long time;

除此之外,Calendar内部还有一个数组,表示日历中各个字段的值,定义为:

protected int fields[];

这个数组的长度为17,保存一个日期中各个字段的值,都有哪些字段呢?Calendar类中定义了一些静态变量,表示这些字段,主要有:

  1. Calendar.YEAR:表示年。
  2. Calendar.MONTH:表示月, 1 1 1月是 0 0 0Calendar同样定义了表示各个月份的静态变量,如Calendar.JULY表示 7 7 7月。
  3. Calendar.DAY_OF_MONTH:表示日,每月的第一天是 1 1 1
  4. Calendar.HOUR_OF_DAY:表示小时,为 0 ~ 23 0~23 023
  5. Calendar.MINUTE:表示分钟,为 0 ~ 59 0~59 059
  6. Calendar.SECOND:表示秒,为 0 ~ 59 0~59 059
  7. Calendar.MILLISECOND:表示毫秒,为 0 ~ 999 0~999 0999
  8. Calendar.DAY_OF_WEEK:表示星期几,周日是 1 1 1,周一是 2 2 2,周六是 7 7 7Calenar同样定义了表示各个星期的静态变量,如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);
}

最终调用的方法都是需要TimeZoneLocale的,如果没有,则会使用上面介绍的默认值。getInstance方法会根据TimeZoneLocale创建对应的Calendar子类对象,在中文系统中,子类一般是表示公历的GregorianCalendargetInstance方法封装了Calendar对象创建的细节。TimeZoneLocale不同,具体的子类可能不同,但都是Calendar

来看代码,输出当前时间的各种信息:

    @Test
    public 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内部,会将表示时刻的毫秒数,按照TimeZoneLocale对应的年历,计算各个日历字段的值,存放在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做了一项非常烦琐的工作,根据TimeZoneLocale,在绝对时间毫秒数和日历字段之间自动进行转换,且对不同日历字段的修改进行自动同步更新。除了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)

formatDate转换为字符串,parse将字符串转换为DateDate的字符串表示与TimeZoneLocale都是相关的,除此之外,还与两个格式化风格有关,一个是日期的格式化风格,另一个是时间的格式化风格。DateFormat定义了4个静态变量,表示 4 4 4种风格:SHORTMEDIUMLONGFULL;还定义了一个静态变量DEFAULT,表示默认风格,值为MEDIUM,不同风格输出的信息详细程度不同。

Calendar类似,DateFormat也是抽象类,也用工厂方法创建对象,提供了多个静态方法创建DateFormat对象,有三类方法:

public static final DateFormat getDateInstance();
public static final DateFormat getDateTimeInstance();
public static final DateFormat getTimeInstance();

getDateTimeInstance方法既处理日期也处理时间,getDateInstance方法只处理日期,getTimeInstance方法只处理时间。看下面的代码:

    @Test
    public void testDateFormat() {
        Calendar calendar = Calendar.getInstance();
        // 2016-08-15 14:15:20
        calendar.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)

比如,看下面的代码:

    @Test
    public void testDataFormatStyleLocale() {
        Calendar calendar = Calendar.getInstance();
        // 2016-08-15 14:15:20
        calendar.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

SimpleDateFormatDateFormat的子类,相比DateFormat,它的一个主要不同是,它可以接受一个自定义的模式(pattern)作为参数,这个模式规定了Date的字符串形式。

    @Test
    public void testSimpleDateFormatFormat() {
        Calendar calendar = Calendar.getInstance();
        // 2016-08-15 14:15:20
        calendar.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中含义如下表格所示:

LetterDate or Time ComponentPresentationExamples
GEra designatorTextAD
yYearYear1996; 96
YWeek yearYear2009; 09
MMonth in year (context sensitive)MonthJuly; Jul; 07
LMonth in year (standalone form)MonthJuly; Jul; 07
wWeek in yearNumber27
WWeek in monthNumber2
DDay in yearNumber189
dDay in monthNumber10
FDay of week in monthNumber2
EDay name in weekTextTuesday; Tue
uDay number of week (1 = Monday, …, 7 = Sunday)Number1
aAm/pm markerTextPM
HHour in day (0-23)Number0
kHour in day (1-24)Number24
KHour in am/pm (0-11)Number0
hHour in am/pm (1-12)Number12
mMinute in hourNumber30
sSecond in minuteNumber55
SMillisecondNumber978
zTime zoneGeneral time zonePacific Standard Time; PST; GMT-08:00
ZTime zoneRFC 822 time zone-0800
XTime zoneISO 8601 time zone-08; -0800; -08:00

常见例子如下所示:

Date and Time PatternResult
"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,如下所示代码:

    @Test
    public 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/SimpleDateFormatDate和字符串之间进行相互转换。这些API存在着一些局限性,如下:

  1. 可变性:像日期和时间这样的类应该是不可变的,某一个日期时间对象都只能代表某一个特定的瞬间。
  2. 偏移性:Date类中的年份是从 1900 1900 1900开始的,月份都是从 0 0 0开始的,这不符合常规编程习惯。
  3. 格式化:用于日期格式化及解析的SimpleDateFormat只对Date类有用,Calendar类则不行。
  4. 线程不安全性。
  5. 不能处理闰秒:由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),所以在世界时(民用时)和原子时之间相差超过到 ± 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纠正了过去的缺陷,引入的类主要有:

  1. Instant:表示时刻,不直接对应年月日信息,需要通过时区转换;
  2. LocalDateTime:表示与时区无关的日期和时间,不直接对应时刻,需要通过时区转换;
  3. ZoneId/ZoneOffset:表示时区;
  4. LocalDate:表示与时区无关的日期,与LocalDateTime相比,只有日期,没有时间信息;
  5. LocalTime:表示与时区无关的时间,与LocalDateTime相比,只有时间,没有日期信息;
  6. ZonedDateTime:表示特定时区的日期和时间;
  7. Duration:持续时间;

5.9 Instant

Instant表示时刻,获取当前时刻,代码为:

Instant now = Instant.now();

可以根据Epoch Time(纪元时)创建Instant。比如,另一种获取当前时刻的代码可以为:

Instant now = Instant.ofEpochMilli(System.currentTimeMillis());

我们知道,Date也表示时刻,InstantDate可以通过纪元时相互转换,代码为:

    @Test
    public void testInstantDate(){
        Instant instant = Instant.ofEpochMilli(1709045743587L);
        // Instant 转为 Date
        Date d = Date.from(instant);
        assertTrue("Tue Feb 27 22:55:43 CST 2024".equals(d.toString()));
        // Date 转为 Instant
        Instant 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();

LocalDateTimeLocalDateLocalTime构成,LocalDate加上时间可以构成LocalDateTimeLocalTime加上日期可以构成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,转换需要一个参数ZoneOffsetZoneOffset表示相对于格林尼治的时区差,北京是 + 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")

ZoneOffsetZoneId的子类,可以根据时区差构造。

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方法,比如:

    @Test
    public 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方法,比如:

    @Test
    public 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有很多plusXXXminusXXX方法,分别用于相对增加和减少时间。

今天 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 (2460601000)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是一个接口,表示日期或时间对象,InstantLocalDateTimeLocalDate等都实现了它,这个接口就是对日期或时间进行调整,还有一个专门的类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)

一些例子如下:

    @Test
    public 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中表示时间段的类主要有两个:PeriodDurationPeriod表示日期之间的差,用年月日表示,不能表示时间;Duration表示时间差,用时分秒等表示,也可以用天表示,一天严格等于 24 24 24小时,不能用年月表示。下面看一些例子。计算两个日期之间的差,看个Period的例子:

    @Test
    public 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点算迟到,迟到要统计迟到的分钟数,怎么计算呢?看代码:

    @Test
    public 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;
  }

  1. 马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎

  2. 尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎

  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值