Java 编程问题:三、使用日期和时间3

简介: Java 编程问题:三、使用日期和时间

66 使用 UTC 和 GMT 获取所有时区


UTC 和 GMT 被认为是处理日期和时间的标准参考。今天,UTC 是首选的方法,但是 UTC 和 GMT 在大多数情况下应该返回相同的结果。


为了获得 UTC 和 GMT 的所有时区,解决方案应该关注 JDK8 前后的实现。所以,让我们从 JDK8 之前有用的解决方案开始。



JDK8 之前


解决方案需要提取可用的时区 ID(非洲/巴马科、欧洲/贝尔格莱德等)。此外,每个时区 ID 都应该用来创建一个TimeZone对象。最后,解决方案需要提取特定于每个时区的偏移量,并考虑到夏令时。绑定到本书的代码包含此解决方案。



从 JDK8 开始


新的 Java 日期时间 API 为解决这个问题提供了新的工具。

在第一步,可用的时区 id 可以通过ZoneId类获得,如下所示:

Set<String> zoneIds = ZoneId.getAvailableZoneIds();


在第二步,每个时区 ID 都应该用来创建一个ZoneId实例。这可以通过ZoneId.of(String zoneId)方法实现:

ZoneId zoneid = ZoneId.of(current_zone_Id);


在第三步,每个ZoneId可用于获得特定于所识别区域的时间。这意味着需要一个“实验室老鼠”参考日期时间。此参考日期时间(无时区,LocalDateTime.now())通过LocalDateTime.atZone()与给定时区(ZoneId)组合,以获得ZoneDateTime(可识别时区的日期时间):

LocalDateTime now = LocalDateTime.now();
ZonedDateTime zdt = now.atZone(ZoneId.of(zone_id_instance));


atZone()方法尽可能地匹配日期时间,同时考虑时区规则,例如夏令时。

在第四步,代码可以利用ZonedDateTime来提取 UTC 偏移量(例如,对于欧洲/布加勒斯特,UTC 偏移量为+02:00):

String utcOffset = zdt.getOffset().getId().replace("Z", "+00:00");


getId()方法返回规范化区域偏移 ID,+00:00偏移作为Z字符返回;因此代码需要快速将Z替换为+00:00,以便与其他偏移对齐,这些偏移遵循+hh:mm+hh:mm:ss格式。

现在,让我们将这些步骤合并到一个辅助方法中:

public static List<String> fetchTimeZones(OffsetType type) {
  List<String> timezones = new ArrayList<>();
  Set<String> zoneIds = ZoneId.getAvailableZoneIds();
  LocalDateTime now = LocalDateTime.now();
  zoneIds.forEach((zoneId) -> {
    timezones.add("(" + type + now.atZone(ZoneId.of(zoneId))
      .getOffset().getId().replace("Z", "+00:00") + ") " + zoneId);
  });
  return timezones;
}


假设此方法存在于DateTimes类中,则获得以下代码:

List<String> timezones 
  = DateTimes.fetchTimeZones(DateTimes.OffsetType.GMT);
Collections.sort(timezones); // optional sort
timezones.forEach(System.out::println);


此外,还显示了一个输出快照,如下所示:

(GMT+00:00) Africa/Abidjan
(GMT+00:00) Africa/Accra
(GMT+00:00) Africa/Bamako
...
(GMT+11:00) Australia/Tasmania
(GMT+11:00) Australia/Victoria
...




67 获取所有可用时区中的本地日期时间


可通过以下步骤获得此问题的解决方案:


  1. 获取本地日期和时间。
  2. 获取可用时区。
  3. 在 JDK8 之前,使用SimpleDateFormatsetTimeZone()方法。
  4. 从 JDK8 开始,使用ZonedDateTime



JDK8 之前

在 JDK8 之前,获取当前本地日期时间的快速解决方案是调用Date空构造器。此外,还可以使用Date在所有可用的时区中显示,这些时区可以通过TimeZone类获得。绑定到本书的代码包含此解决方案。



从 JDK8 开始


从 JDK8 开始,获取默认时区中当前本地日期时间的一个方便解决方案是调用ZonedDateTime.now()方法:

ZonedDateTime zlt = ZonedDateTime.now();


最后,代码可以循环zoneIds,对于每个区域 ID,可以调用ZonedDateTime.withZoneSameInstant(ZoneId zone)方法。此方法返回具有不同时区的此日期时间的副本,并保留以下瞬间:

public static List<String> localTimeToAllTimeZones() {
  List<String> result = new ArrayList<>();
  Set<String> zoneIds = ZoneId.getAvailableZoneIds();
  DateTimeFormatter formatter 
    = DateTimeFormatter.ofPattern("yyyy-MMM-dd'T'HH:mm:ss a Z");
  ZonedDateTime zlt = ZonedDateTime.now();
  zoneIds.forEach((zoneId) -> {
    result.add(zlt.format(formatter) + " in " + zoneId + " is "
      + zlt.withZoneSameInstant(ZoneId.of(zoneId))
        .format(formatter));
  });
  return result;
}


此方法的输出快照可以如下所示:

2019-Feb-26T14:26:30 PM +0200 in Africa/Nairobi 
  is 2019-Feb-26T15:26:30 PM +0300
2019-Feb-26T14:26:30 PM +0200 in America/Marigot 
  is 2019-Feb-26T08:26:30 AM -0400
...
2019-Feb-26T14:26:30 PM +0200 in Pacific/Samoa 
  is 2019-Feb-26T01:26:30 AM -1100




68 显示航班的日期时间信息


本节提供的解决方案将显示有关从澳大利亚珀斯到欧洲布加勒斯特的 15 小时 30 分钟航班的以下信息:


  • UTC 出发和到达日期时间
  • 离开珀斯的日期时间和到达布加勒斯特的日期时间
  • 离开和到达布加勒斯特的日期时间

假设从珀斯出发的参考日期时间为 2019 年 2 月 26 日 16:00(或下午 4:00):


LocalDateTime ldt = LocalDateTime.of(
  2019, Month.FEBRUARY, 26, 16, 00);



首先,让我们将这个日期时间与澳大利亚/珀斯(+08:00)的时区结合起来。这将产生一个特定于澳大利亚/珀斯的ZonedDateTime对象(这是出发时珀斯的时钟日期和时间):

// 04:00 PM, Feb 26, 2019 +0800 Australia/Perth
ZonedDateTime auPerthDepart 
  = ldt.atZone(ZoneId.of("Australia/Perth"));


此外,让我们在ZonedDateTime中加上 15 小时 30 分钟。结果ZonedDateTime表示珀斯的日期时间(这是抵达布加勒斯特时珀斯的时钟日期和时间):

// 07:30 AM, Feb 27, 2019 +0800 Australia/Perth
ZonedDateTime auPerthArrive 
  = auPerthDepart.plusHours(15).plusMinutes(30);


现在,让我们计算一下布加勒斯特的日期时间和珀斯的出发日期时间。基本上,以下代码表示从布加勒斯特时区的珀斯时区出发的日期和时间:

// 10:00 AM, Feb 26, 2019 +0200 Europe/Bucharest
ZonedDateTime euBucharestDepart 
  = auPerthDepart.withZoneSameInstant(ZoneId.of("Europe/Bucharest"));


最后,让我们计算一下到达布加勒斯特的日期和时间。以下代码表示布加勒斯特时区珀斯时区的到达日期时间:

// 01:30 AM, Feb 27, 2019 +0200 Europe/Bucharest
ZonedDateTime euBucharestArrive 
  = auPerthArrive.withZoneSameInstant(ZoneId.of("Europe/Bucharest"));


如下图所示,从珀斯出发的 UTC 时间是上午 8:00,而到达布加勒斯特的 UTC 时间是晚上 11:30:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rdxM6AQc-1657077517351)(img/09ecaf27-f809-42b1-8858-ecf44aa33d5f.png)]

这些时间可以很容易地提取为OffsetDateTime,如下所示:


// 08:00 AM, Feb 26, 2019
OffsetDateTime utcAtDepart = auPerthDepart.withZoneSameInstant(
  ZoneId.of("UTC")).toOffsetDateTime();
// 11:30 PM, Feb 26, 2019
OffsetDateTime utcAtArrive = auPerthArrive.withZoneSameInstant(
  ZoneId.of("UTC")).toOffsetDateTime();




69 将 Unix 时间戳转换为日期时间


对于这个解决方案,假设下面的 Unix 时间戳是 1573768800。此时间戳等效于以下内容:


   11/14/2019 @ 10:00pm (UTC)

   ISO-8601 中的2019-11-14T22:00:00+00:00

   Thu, 14 Nov 2019 22:00:00 +0000,RFC 822、1036、1123、2822

   Thursday, 14-Nov-19 22:00:00 UTC,RFC 2822

   2019-11-14T22:00:00+00:00在 RFC 3339 中


为了将 Unix 时间戳转换为日期时间,必须知道 Unix 时间戳的分辨率以秒为单位,而java.util.Date需要毫秒。因此,从 Unix 时间戳获取Date对象的解决方案需要将 Unix 时间戳乘以 1000,从秒转换为毫秒,如下两个示例所示:

long unixTimestamp = 1573768800;
// Fri Nov 15 00:00:00 EET 2019 - in the default time zone
Date date = new Date(unixTimestamp * 1000L);
// Fri Nov 15 00:00:00 EET 2019 - in the default time zone
Date date = new Date(TimeUnit.MILLISECONDS
  .convert(unixTimestamp, TimeUnit.SECONDS));


从 JDK8 开始,Date类使用from(Instant instant)方法。此外,Instant类附带了ofEpochSecond(long epochSecond)方法,该方法使用1970-01-01T00:00:00Z的纪元的给定秒数返回Instant的实例:

// 2019-11-14T22:00:00Z in UTC
Instant instant = Instant.ofEpochSecond(unixTimestamp);
// Fri Nov 15 00:00:00 EET 2019 - in the default time zone
Date date = Date.from(instant);


上一示例中获得的瞬间可用于创建LocalDateTimeZonedDateTime,如下所示:

// 2019-11-15T06:00
LocalDateTime date = LocalDateTime
  .ofInstant(instant, ZoneId.of("Australia/Perth"));
// 2019-Nov-15 00:00:00 +0200 Europe/Bucharest
ZonedDateTime date = ZonedDateTime
  .ofInstant(instant, ZoneId.of("Europe/Bucharest"));



70 查找每月的第一天/最后一天


这个问题的正确解决将依赖于 JDK8、TemporalTemporalAdjuster接口。


Temporal接口位于日期和时间的表示后面。换句话说,表示日期和/或时间的类实现了这个接口。例如,以下类只是实现此接口的几个类:


   LocalDate(ISO-8601 日历系统中没有时区的日期)

   LocalTime(ISO-8601 日历系统中无时区的时间)

   LocalDateTime(ISO-8601 日历系统中无时区的日期时间)

   ZonedDateTime(ISO-8601 日历系统中带时区的日期时间),依此类推

   OffsetDateTime(在 ISO-8601 日历系统中,从 UTC/格林威治时间偏移的日期时间)

   HijrahDate(希吉拉历法系统中的日期)


TemporalAdjuster类是一个函数式接口,它定义了可用于调整Temporal对象的策略。除了可以定义自定义策略外,TemporalAdjuster类还提供了几个预定义的策略,如下所示(文档包含了整个列表,非常令人印象深刻):


   firstDayOfMonth()(返回当月第一天)

   lastDayOfMonth()(返回当月最后一天)

   firstDayOfNextMonth()(次月 1 日返回)

   firstDayOfNextYear()(次年第一天返回)


注意,前面列表中的前两个调整器正是这个问题所需要的。


考虑一个修正-LocalDate:

LocalDate date = LocalDate.of(2019, Month.FEBRUARY, 27);


让我们看看二月的第一天/最后一天是什么时候:

// 2019-02-01
LocalDate firstDayOfFeb 
  = date.with(TemporalAdjusters.firstDayOfMonth());
// 2019-02-28
LocalDate lastDayOfFeb 
  = date.with(TemporalAdjusters.lastDayOfMonth());


看起来依赖预定义的策略非常简单。但是,假设问题要求您查找 2019 年 2 月 27 日之后的 21 天,也就是 2019 年 3 月 20  日。对于这个问题,没有预定义的策略,因此需要自定义策略。此问题的解决方案可以依赖 Lambda 表达式,如以下辅助方法中所示:

public static LocalDate getDayAfterDays(
    LocalDate startDate, int days) {
  Period period = Period.ofDays(days);
  TemporalAdjuster ta = p -> p.plus(period);
  LocalDate endDate = startDate.with(ta);
  return endDate;
}


如果此方法存在于名为DateTimes的类中,则以下调用将返回预期结果:

// 2019-03-20
LocalDate datePlus21Days = DateTimes.getDayAfterDays(date, 21);



遵循相同的技术,但依赖于static工厂方法ofDateAdjuster(),下面的代码片段定义了一个静态调整器,返回下一个星期六的日期:

static TemporalAdjuster NEXT_SATURDAY 
    = TemporalAdjusters.ofDateAdjuster(today -> {
  DayOfWeek dayOfWeek = today.getDayOfWeek();
  if (dayOfWeek == DayOfWeek.SATURDAY) {
    return today;
  }
  if (dayOfWeek == DayOfWeek.SUNDAY) {
    return today.plusDays(6);
  }
  return today.plusDays(6 - dayOfWeek.getValue());
});


我们将此方法称为 2019 年 2 月 27 日(下一个星期六是 2019 年 3 月 2 日):

// 2019-03-02
LocalDate nextSaturday = date.with(NEXT_SATURDAY);



最后,这个函数式接口定义了一个名为adjustInto()abstract方法。在自定义实现中,可以通过向该方法传递一个Temporal对象来覆盖该方法,如下所示:

public class NextSaturdayAdjuster implements TemporalAdjuster {
  @Override
  public Temporal adjustInto(Temporal temporal) {
    DayOfWeek dayOfWeek = DayOfWeek
      .of(temporal.get(ChronoField.DAY_OF_WEEK));
    if (dayOfWeek == DayOfWeek.SATURDAY) {
      return temporal;
    }
    if (dayOfWeek == DayOfWeek.SUNDAY) {
      return temporal.plus(6, ChronoUnit.DAYS);
    }
    return temporal.plus(6 - dayOfWeek.getValue(), ChronoUnit.DAYS);
  }
}


下面是用法示例:

NextSaturdayAdjuster nsa = new NextSaturdayAdjuster();
// 2019-03-02
LocalDate nextSaturday = date.with(nsa);



71 定义/提取区域偏移


通过区域偏移,我们了解需要从 GMT/UTC 时间中添加/减去的时间量,以便获得全球特定区域(例如,澳大利亚珀斯)的日期时间。通常,区域偏移以固定的小时和分钟数打印:


+02:00-08:30+0400UTC+01:00,依此类推。


因此,简而言之,时区偏移量是指时区与 GMT/UTC 之间的时间差。




JDK8 之前


在 JDK8 之前,可以通过java.util.TimeZone定义一个时区,有了这个时区,代码就可以通过TimeZone.getRawOffset()方法得到时区偏移量(原始部分来源于这个方法不考虑夏令时)。绑定到本书的代码包含此解决方案。



从 JDK8 开始


从 JDK8 开始,有两个类负责处理时区表示。首先是java.time.ZoneId,表示欧洲雅典等时区;其次是java.time.ZoneOffset(扩展ZoneId),表示指定时区的固定时间(偏移量),以 GMT/UTC 表示。

新的 Java 日期时间 API 默认处理夏令时;因此,使用夏令时的夏-冬周期区域将有两个ZoneOffset类。


UTC 区域偏移量可以很容易地获得,如下所示(这是+00:00,在 Java 中用Z字符表示):


// Z
ZoneOffset zoneOffsetUTC = ZoneOffset.UTC;



系统默认时区也可以通过ZoneOffset类获取:

// Europe/Athens
ZoneId defaultZoneId = ZoneOffset.systemDefault();


为了使用夏令时进行分区偏移,代码需要将日期时间与其关联。例如,关联一个LocalDateTime类(也可以使用Instant),如下所示:

// by default it deals with the Daylight Saving Times
LocalDateTime ldt = LocalDateTime.of(2019, 6, 15, 0, 0);
ZoneId zoneId = ZoneId.of("Europe/Bucharest");
// +03:00
ZoneOffset zoneOffset = zoneId.getRules().getOffset(ldt);


区域偏移量也可以从字符串中获得。例如,以下代码获得+02:00的分区偏移:

ZoneOffset zoneOffsetFromString = ZoneOffset.of("+02:00");


这是一种非常方便的方法,可以将区域偏移快速添加到支持区域偏移的Temporal对象。例如,使用它将区域偏移添加到OffsetTime和OffsetDateTime(用于在数据库中存储日期或通过电线发送的方便方法):

OffsetTime offsetTime = OffsetTime.now(zoneOffsetFromString);
OffsetDateTime offsetDateTime 
  = OffsetDateTime.now(zoneOffsetFromString);


我们问题的另一个解决方法是依赖于从小时、分钟和秒来定义ZoneOffset。ZoneOffset的一个助手方法专门用于:

// +08:30 (this was obtained from 8 hours and 30 minutes)
ZoneOffset zoneOffsetFromHoursMinutes 
  = ZoneOffset.ofHoursMinutes(8, 30);


在ZoneOffset.ofHoursMinutes()旁边有ZoneOffset.ofHours()、ofHoursMinutesSeconds()和ofTotalSeconds()。

最后,每个支持区域偏移的Temporal对象都提供了一个方便的getOffset()方法。例如,下面的代码从前面的offsetDateTime对象获取区域偏移:

// +02:00
ZoneOffset zoneOffsetFromOdt = offsetDateTime.getOffset();



72 在日期和时间之间转换


这里给出的解决方案将涵盖以下Temporal类—InstantLocalDateLocalDateTimeZonedDateTimeOffsetDateTimeLocalTimeOffsetTime

相关文章
|
3月前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
3月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
2月前
|
Java API 调度
Java 日期与时间处理:精准掌控时间流转
Java 8引入了全新的日期和时间API,解决了旧版`java.util.Date`和`Calendar`类设计不佳、操作繁琐的问题。新API包括`LocalDate`、`LocalTime`和`LocalDateTime`类,操作简洁直观,符合日常思维习惯。同时提供了`Period`和`Duration`处理时间间隔,以及`DateTimeFormatter`进行格式化输出。这些改进使开发者能更高效、准确地处理日期和时间,极大提升了开发效率与代码质量。 (239字符)
76 6
|
3月前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
92 12
|
3月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
248 2
|
4月前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
3月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
3月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
76 3
|
3月前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
93 5
|
4月前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
102 1

热门文章

最新文章