Date
-Instant
为了从Date
转换到Instant
,可采用Date.toInstant()
方法求解。可通过Date.from(Instant instant)
方法实现反转:
Date
到Instant
可以这样完成:
Date date = new Date(); // e.g., 2019-02-27T12:02:49.369Z, UTC Instant instantFromDate = date.toInstant();
Instant到Date可以这样完成:
Instant instant = Instant.now(); // Wed Feb 27 14:02:49 EET 2019, default system time zone Date dateFromInstant = Date.from(instant);
请记住,Date
不是时区感知的,但它显示在系统默认时区中(例如,通过toString()
)。Instant
是 UTC 时区。
让我们快速地将这些代码片段包装在两个工具方法中,它们在一个工具类DateConverters
中定义:
public static Instant dateToInstant(Date date) { return date.toInstant(); } public static Date instantToDate(Instant instant) { return Date.from(instant); }
此外,让我们使用以下屏幕截图中的方法来丰富此类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VC4DDDjb-1657077517352)(img/ae07c212-3c31-4d8f-91b1-b494d5b9d393.png)]
屏幕截图中的常量DEFAULT_TIME_ZONE
是系统默认时区:
public static final ZoneId DEFAULT_TIME_ZONE = ZoneId.systemDefault();
Date
–LocalDate
Date
对象可以通过Instant
对象转换为LocalDate
。一旦我们从给定的Date
对象中获得Instant
对象,解决方案就可以应用于它系统默认时区,并调用toLocaleDate()
方法:
// e.g., 2019-03-01 public static LocalDate dateToLocalDate(Date date) { return dateToInstant(date).atZone(DEFAULT_TIME_ZONE).toLocalDate(); }
从LocalDate
到Date
的转换应该考虑到LocalDate
不包含Date
这样的时间成分,所以解决方案必须提供一个时间成分作为一天的开始(关于这个问题的更多细节可以在“一天的开始和结束”问题中找到):
// e.g., Fri Mar 01 00:00:00 EET 2019 public static Date localDateToDate(LocalDate localDate) { return Date.from(localDate.atStartOfDay( DEFAULT_TIME_ZONE).toInstant()); }
Date
–LocalDateTime
从Date
到DateLocalTime
的转换与从Date
到LocalDate
的转换是一样的,只是溶液应该调用toLocalDateTime()
方法如下:
// e.g., 2019-03-01T07:25:25.624 public static LocalDateTime dateToLocalDateTime(Date date) { return dateToInstant(date).atZone( DEFAULT_TIME_ZONE).toLocalDateTime(); }
从LocalDateTime
到Date
的转换非常简单。只需应用系统默认时区并调用toInstant()
:
// e.g., Fri Mar 01 07:25:25 EET 2019 public static Date localDateTimeToDate(LocalDateTime localDateTime) { return Date.from(localDateTime.atZone( DEFAULT_TIME_ZONE).toInstant()); }
Date
–ZonedDateTime
Date
到ZonedDateTime
的转换可以通过从给定Date
对象获取Instant
对象和系统默认时区来完成:
// e.g., 2019-03-01T07:25:25.624+02:00[Europe/Athens] public static ZonedDateTime dateToZonedDateTime(Date date) { return dateToInstant(date).atZone(DEFAULT_TIME_ZONE); }
将ZonedDateTime转换为Date就是将ZonedDateTime转换为Instant:
// e.g., Fri Mar 01 07:25:25 EET 2019 public static Date zonedDateTimeToDate(ZonedDateTime zonedDateTime) { return Date.from(zonedDateTime.toInstant()); }
Date
–OffsetDateTime
从Date
到OffsetDateTime
的转换依赖于toOffsetDateTime()
方法:
// e.g., 2019-03-01T07:25:25.624+02:00 public static OffsetDateTime dateToOffsetDateTime(Date date) { return dateToInstant(date).atZone( DEFAULT_TIME_ZONE).toOffsetDateTime(); }
从OffsetDateTime
到Date
的转换方法需要两个步骤。首先将OffsetDateTime
转换为LocalDateTime
;其次将LocalDateTime
转换为Instant
,对应偏移量:
// e.g., Fri Mar 01 07:55:49 EET 2019 public static Date offsetDateTimeToDate( OffsetDateTime offsetDateTime) { return Date.from(offsetDateTime.toLocalDateTime() .toInstant(ZoneOffset.of(offsetDateTime.getOffset().getId()))); }
Date
–LocalTime
将Date
转换为LocalTime
可以依赖LocalTime.toInstant()
方法,如下所示:
// e.g., 08:03:20.336 public static LocalTime dateToLocalTime(Date date) { return LocalTime.ofInstant(dateToInstant(date), DEFAULT_TIME_ZONE); }
将LocalTime
转换为Date
应该考虑到LocalTime
没有日期组件。这意味着解决方案应将日期设置为 1970 年 1 月 1 日,即纪元:
// e.g., Thu Jan 01 08:03:20 EET 1970 public static Date localTimeToDate(LocalTime localTime) { return Date.from(localTime.atDate(LocalDate.EPOCH) .toInstant(DEFAULT_TIME_ZONE.getRules() .getOffset(Instant.now()))); }
Date
-OffsetTime
将Date
转换为OffsetTime
可以依赖OffsetTime.toInstant()
方法,如下所示:
// e.g., 08:03:20.336+02:00 public static OffsetTime dateToOffsetTime(Date date) { return OffsetTime.ofInstant(dateToInstant(date), DEFAULT_TIME_ZONE); }
将OffsetTime
转换为Date
应该考虑到OffsetTime
没有日期组件。这意味着解决方案应将日期设置为 1970 年 1 月 1 日,即纪元:
// e.g., Thu Jan 01 08:03:20 EET 1970 public static Date offsetTimeToDate(OffsetTime offsetTime) { return Date.from(offsetTime.atDate(LocalDate.EPOCH).toInstant()); }
73 迭代一系列日期
假设范围是由开始日期 2019 年 2 月 1 日和结束日期 2019 年 2 月 21 日界定的。这个问题的解决方案应该循环【2019 年 2 月 1 日,2019 年 2 月 21 日】间隔一天,并在屏幕上打印每个日期。基本上要解决两个主要问题:
- 一旦开始日期和结束日期相等,就停止循环。
- 每天增加开始日期直到结束日期。
JDK8 之前
在 JDK8 之前,解决方案可以依赖于Calendar
工具类。绑定到本书的代码包含此解决方案。
从 JDK8 开始
首先,从 JDK8 开始,可以很容易地将日期定义为LocalDate
,而不需要Calendar
的帮助:
LocalDate startLocalDate = LocalDate.of(2019, 2, 1); LocalDate endLocalDate = LocalDate.of(2019, 2, 21);
一旦开始日期和结束日期相等,我们就通过LocalDate.isBefore(ChronoLocalDate other)方法停止循环。此标志方法检查此日期是否早于给定日期。
使用LocalDate.plusDays(long daysToAdd)方法逐日增加开始日期直到结束日期。在for循环中使用这两种方法会产生以下代码:
for (LocalDate date = startLocalDate; date.isBefore(endLocalDate); date = date.plusDays(1)) { // do something with this day System.out.println(date); }
输出的快照应如下所示:
2019-02-01 2019-02-02 2019-02-03 ... 2019-02-20
从 JDK9 开始
JDK9 可以用一行代码解决这个问题。由于新的LocalDate.datesUntil(LocalDate endExclusive)
方法,这是可能的。此方法返回Stream<LocalDate>
,增量步长为一天:
startLocalDate.datesUntil(endLocalDate).forEach(System.out::println);
如果增量步骤应以天、周、月或年表示,则依赖于LocalDate.datesUntil(LocalDate endExclusive, Period step)。例如,1 周的增量步骤可以指定如下:
startLocalDate.datesUntil(endLocalDate, Period.ofWeeks(1)).forEach(System.out::println);
输出应为(第 1-8 周,第 8-15 周),如下所示:
2019-02-01 2019-02-08 2019-02-15
74 计算年龄
可能最常用的两个日期之间的差异是关于计算一个人的年龄。通常,一个人的年龄以年表示,但有时应提供月,甚至天。
JDK8 之前
在 JDK8 之前,试图提供一个好的解决方案可以依赖于Calendar
和/或SimpleDateFormat
。绑定到本书的代码包含这样一个解决方案。
从 JDK8 开始
更好的方法是升级到 JDK8,并依赖以下简单的代码片段:
LocalDate startLocalDate = LocalDate.of(1977, 11, 2); LocalDate endLocalDate = LocalDate.now(); long years = ChronoUnit.YEARS.between(startLocalDate, endLocalDate);
由于Period
类的原因,将月和日添加到结果中也很容易实现:
Period periodBetween = Period.between(startLocalDate, endLocalDate);
现在,可以通过periodBetween.getYears()、periodBetween.getMonths()、periodBetween.getDays()获得以年、月、日为单位的年龄。
例如,在当前日期 2019 年 2 月 28 日和 1977 年 11 月 2 日之间,我们有 41 年 3 个月 26 天。
75 一天的开始和结束
在 JDK8 中,可以通过几种方法来找到一天的开始/结束。
让我们考虑一下通过LocalDate
表达的一天:
LocalDate localDate = LocalDate.of(2019, 2, 28);
找到 2019 年 2 月 28 日一天的开始的解决方案依赖于一个名为atStartOfDay()的方法。此方法从该日期午夜 00:00 返回LocalDateTime:
// 2019-02-28T00:00 LocalDateTime ldDayStart = localDate.atStartOfDay();
或者,该溶液可以使用of(LocalDate date, LocalTime time)
方法。该方法将给定的日期和时间组合成LocalDateTime
。因此,如果经过的时间是LocalTime.MIN
(一天开始时的午夜时间),则结果如下:
// 2019-02-28T00:00 LocalDateTime ldDayStart = LocalDateTime.of(localDate, LocalTime.MIN);
一个LocalDate物体的一天结束时间至少可以用两种方法得到。一种解决方案是依靠LocalDate.atTime(LocalTime time)。得到的LocalDateTime可以表示该日期与一天结束时的组合,如果解决方案作为参数传递,LocalTime.MAX(一天结束时午夜前的时间):
// 2019-02-28T23:59:59.999999999 LocalDateTime ldDayEnd = localDate.atTime(LocalTime.MAX);
或者,该解决方案可以通过atDate(LocalDate date)方法将LocalTime.MAX与给定日期结合:
// 2019-02-28T23:59:59.999999999 LocalDateTime ldDayEnd = LocalTime.MAX.atDate(localDate);
由于LocalDate没有时区的概念,前面的例子容易出现由不同的角落情况引起的问题,例如夏令时。有些夏令时会在午夜(00:00 变为 01:00 AM)更改时间,这意味着一天的开始时间是 01:00:00,而不是 00:00:00。为了缓解这些问题,请考虑以下示例,这些示例将前面的示例扩展为使用夏令时感知的ZonedDateTime:
// 2019-02-28T00:00+08:00[Australia/Perth] ZonedDateTime ldDayStartZone = localDate.atStartOfDay(ZoneId.of("Australia/Perth")); // 2019-02-28T00:00+08:00[Australia/Perth] ZonedDateTime ldDayStartZone = LocalDateTime .of(localDate, LocalTime.MIN).atZone(ZoneId.of("Australia/Perth")); // 2019-02-28T23:59:59.999999999+08:00[Australia/Perth] ZonedDateTime ldDayEndZone = localDate.atTime(LocalTime.MAX) .atZone(ZoneId.of("Australia/Perth")); // 2019-02-28T23:59:59.999999999+08:00[Australia/Perth] ZonedDateTime ldDayEndZone = LocalTime.MAX.atDate(localDate) .atZone(ZoneId.of("Australia/Perth"));
现在,我们来考虑一下-LocalDateTime
,2019 年 2 月 28 日,18:00:00:
LocalDateTime localDateTime = LocalDateTime.of(2019, 2, 28, 18, 0, 0);
显而易见的解决方案是从LocalDateTime中提取LocalDate,并应用前面的方法。另一个解决方案依赖于这样一个事实,Temporal接口的每个实现(包括LocalDate)都可以利用with(TemporalField field, long newValue)方法。主要是,with()方法返回这个日期的一个副本,其中指定的字段ChronoField设置为newValue。因此,如果解决方案将ChronoField.NANO_OF_DAY(一天的纳秒)设置为LocalTime.MIN,那么结果将是一天的开始。这里的技巧是通过toNanoOfDay()将LocalTime.MIN转换为纳秒,如下所示:
// 2019-02-28T00:00 LocalDateTime ldtDayStart = localDateTime .with(ChronoField.NANO_OF_DAY, LocalTime.MIN.toNanoOfDay());
这相当于:
LocalDateTime ldtDayStart = localDateTime.with(ChronoField.HOUR_OF_DAY, 0);
一天的结束是非常相似的。只需通过LocalTime.MAX而不是MIN:
// 2019-02-28T23:59:59.999999999 LocalDateTime ldtDayEnd = localDateTime .with(ChronoField.NANO_OF_DAY, LocalTime.MAX.toNanoOfDay());
这相当于:
LocalDateTime ldtDayEnd = localDateTime.with( ChronoField.NANO_OF_DAY, 86399999999999L);
与LocalDate
一样,LocalDateTime
对象不知道时区。在这种情况下,ZonedDateTime
可以帮助:
// 2019-02-28T00:00+08:00[Australia/Perth] ZonedDateTime ldtDayStartZone = localDateTime .with(ChronoField.NANO_OF_DAY, LocalTime.MIN.toNanoOfDay()) .atZone(ZoneId.of("Australia/Perth")); // 2019-02-28T23:59:59.999999999+08:00[Australia/Perth] ZonedDateTime ldtDayEndZone = localDateTime .with(ChronoField.NANO_OF_DAY, LocalTime.MAX.toNanoOfDay()) .atZone(ZoneId.of("Australia/Perth"));
作为奖励,让我们看看 UTC 一天的开始/结束。除了依赖于with()
方法的解决方案外,另一个解决方案可以依赖于toLocalDate()
,如下所示:
// e.g., 2019-02-28T09:23:10.603572Z ZonedDateTime zdt = ZonedDateTime.now(ZoneOffset.UTC); // 2019-02-28T00:00Z ZonedDateTime dayStartZdt = zdt.toLocalDate().atStartOfDay(zdt.getZone()); // 2019-02-28T23:59:59.999999999Z ZonedDateTime dayEndZdt = zdt.toLocalDate() .atTime(LocalTime.MAX).atZone(zdt.getZone());
由于java.util.Date
和Calendar
存在许多问题,因此建议避免尝试用它们实现此问题的解决方案。
76 两个日期之间的差异
计算两个日期之间的差值是一项非常常见的任务(例如,请参阅“计算年龄”部分)。让我们看看其他方法的集合,这些方法可以用来获得以毫秒、秒、小时等为单位的两个日期之间的差异。
JDK8 之前
建议通过java.util.Date和Calendar类来表示日期时间信息。最容易计算的差异用毫秒表示。绑定到本书的代码包含这样一个解决方案。
从 JDK8 开始
从 JDK8 开始,建议通过Temporal
(例如,DateTime
、DateLocalTime
、ZonedDateTime
等)来表示日期时间信息。
假设两个LocalDate
对象,2018 年 1 月 1 日和 2019 年 3 月 1 日:
LocalDate ld1 = LocalDate.of(2018, 1, 1); LocalDate ld2 = LocalDate.of(2019, 3, 1);
计算这两个Temporal对象之间差异的最简单方法是通过ChronoUnit类。除了表示一组标准的日期周期单位外,ChronoUnit还提供了几种简便的方法,包括between(Temporal t1Inclusive, Temporal t2Exclusive)。顾名思义,between()方法计算两个Temporal对象之间的时间量。让我们看看计算ld1和ld2之间的差值的工作原理,以天、月和年为单位:
// 424 long betweenInDays = Math.abs(ChronoUnit.DAYS.between(ld1, ld2)); // 14 long betweenInMonths = Math.abs(ChronoUnit.MONTHS.between(ld1, ld2)); // 1 long betweenInYears = Math.abs(ChronoUnit.YEARS.between(ld1, ld2));
或者,每个Temporal
公开一个名为until()
的方法。实际上,LocalDate
有两个,一个返回Period
作为两个日期之间的差,另一个返回long
作为指定时间单位中两个日期之间的差。使用返回Period
的方法如下:
Period period = ld1.until(ld2); // Difference as Period: 1y2m0d System.out.println("Difference as Period: " + period.getYears() + "y" + period.getMonths() + "m" + period.getDays() + "d");
使用允许我们指定时间单位的方法如下:
// 424 long untilInDays = Math.abs(ld1.until(ld2, ChronoUnit.DAYS)); // 14 long untilInMonths = Math.abs(ld1.until(ld2, ChronoUnit.MONTHS)); // 1 long untilInYears = Math.abs(ld1.until(ld2, ChronoUnit.YEARS));
ChronoUnit.convert()
方法也适用于LocalDateTime
的情况。让我们考虑以下两个LocalDateTime
对象:2018 年 1 月 1 日 22:15:15 和 2019 年 3 月 1 日 23:15:15:
LocalDateTime ldt1 = LocalDateTime.of(2018, 1, 1, 22, 15, 15); LocalDateTime ldt2 = LocalDateTime.of(2018, 1, 1, 23, 15, 15);
现在,让我们看看ldt1
和ldt2
之间的区别,用分钟表示:
// 60 long betweenInMinutesWithoutZone = Math.abs(ChronoUnit.MINUTES.between(ldt1, ldt2));
并且,通过LocalDateTime.until()
方法以小时表示的差异:
// 1 long untilInMinutesWithoutZone = Math.abs(ldt1.until(ldt2, ChronoUnit.HOURS));
但是,ChronoUnit.between()
和until()
有一个非常棒的地方,那就是它们与ZonedDateTime
一起工作。例如,让我们考虑欧洲/布加勒斯特时区和澳大利亚/珀斯时区的ldt1
,加上一小时:
ZonedDateTime zdt1 = ldt1.atZone(ZoneId.of("Europe/Bucharest")); ZonedDateTime zdt2 = zdt1.withZoneSameInstant( ZoneId.of("Australia/Perth")).plusHours(1);
现在,我们用ChronoUnit.between()
来表示zdt1
和zdt2
之间的差分,用ZonedDateTime.until()
来表示zdt1
和zdt2
之间的差分,用小时表示:
// 60 long betweenInMinutesWithZone = Math.abs(ChronoUnit.MINUTES.between(zdt1, zdt2)); // 1 long untilInHoursWithZone = Math.abs(zdt1.until(zdt2, ChronoUnit.HOURS));
最后,让我们重复这个技巧,但是对于两个独立的ZonedDateTime
对象:一个为ldt1
获得,一个为ldt2
获得:
ZonedDateTime zdt1 = ldt1.atZone(ZoneId.of("Europe/Bucharest")); ZonedDateTime zdt2 = ldt2.atZone(ZoneId.of("Australia/Perth")); // 300 long betweenInMinutesWithZone = Math.abs(ChronoUnit.MINUTES.between(zdt1, zdt2)); // 5 long untilInHoursWithZone = Math.abs(zdt1.until(zdt2, ChronoUnit.HOURS));
77 实现象棋时钟
从 JDK8 开始,java.time包有一个名为Clock的抽象类。这个类的主要目的是允许我们在需要时插入不同的时钟(例如,出于测试目的)。默认情况下,Java 有四种实现:SystemClock、OffsetClock、TickClock和FixedClock。对于每个实现,Clock类中都有static方法。例如,下面的代码创建了FixedClock(一个总是返回相同Instant的时钟):
Clock fixedClock = Clock.fixed(Instant.now(), ZoneOffset.UTC);
还有一个TickClock
,它返回给定时区整秒的当前Instant
滴答声:
Clock tickClock = Clock.tickSeconds(ZoneId.of("Europe/Bucharest"));
还有一种方法可以用来在整分钟内打勾tickMinutes()
,还有一种通用方法tick()
,它允许我们指定Duration
。
Clock
类也可以支持时区和偏移量,但是Clock
类最重要的方法是instant()
。此方法返回Clock
的瞬间:
// 2019-03-01T13:29:34Z System.out.println(tickClock.instant());
还有一个millis()方法,它以毫秒为单位返回时钟的当前时刻。
假设我们要实现一个时钟,它充当象棋时钟:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QaOdoWVZ-1657077517353)(img/ad6496ac-4425-407c-9a5f-c922283d6bcb.png)]
为了实现一个Clock类,需要遵循以下几个步骤:
扩展Clock类。
执行Serializable。
至少覆盖从Clock继承的抽象方法。
Clock类的框架如下:
public class ChessClock extends Clock implements Serializable { @Override public ZoneId getZone() { ... } @Override public Clock withZone(ZoneId zone) { ... } @Override public Instant instant() { ... } }
我们的ChessClock
将只与 UTC 一起工作;不支持其他时区。这意味着getZone()
和withZone()
方法可以实现如下(当然,将来可以修改):
@Override public ZoneId getZone() { return ZoneOffset.UTC; } @Override public Clock withZone(ZoneId zone) { throw new UnsupportedOperationException( "The ChessClock works only in UTC time zone"); }
我们实现的高潮是instant()方法。难度在于管理两个Instant,一个是左边的玩家(instantLeft),一个是右边的玩家(instantRight)。我们可以将instant()方法的每一次调用与当前玩家已经执行了一个移动的事实相关联,现在轮到另一个玩家了。所以,基本上,这个逻辑是说同一个玩家不能调用instant()两次。实现这个逻辑,instant()方法如下:
public class ChessClock extends Clock implements Serializable { public enum Player { LEFT, RIGHT } private static final long serialVersionUID = 1L; private Instant instantStart; private Instant instantLeft; private Instant instantRight; private long timeLeft; private long timeRight; private Player player; public ChessClock(Player player) { this.player = player; } public Instant gameStart() { if (this.instantStart == null) { this.timeLeft = 0; this.timeRight = 0; this.instantStart = Instant.now(); this.instantLeft = instantStart; this.instantRight = instantStart; return instantStart; } throw new IllegalStateException( "Game already started. Stop it and try again."); } public Instant gameEnd() { if (this.instantStart != null) { instantStart = null; return Instant.now(); } throw new IllegalStateException("Game was not started."); } @Override public ZoneId getZone() { return ZoneOffset.UTC; } @Override public Clock withZone(ZoneId zone) { throw new UnsupportedOperationException( "The ChessClock works only in UTC time zone"); } @Override public Instant instant() { if (this.instantStart != null) { if (player == Player.LEFT) { player = Player.RIGHT; long secondsLeft = Instant.now().getEpochSecond() - instantRight.getEpochSecond(); instantLeft = instantLeft.plusSeconds( secondsLeft - timeLeft); timeLeft = secondsLeft; return instantLeft; } else { player = Player.LEFT; long secondsRight = Instant.now().getEpochSecond() - instantLeft.getEpochSecond(); instantRight = instantRight.plusSeconds( secondsRight - timeRight); timeRight = secondsRight; return instantRight; } } throw new IllegalStateException("Game was not started."); } }
因此,根据哪个玩家调用了instant()
方法,代码计算出该玩家在执行移动之前思考所需的秒数。此外,代码会切换播放器,因此下一次调用instant()
将处理另一个播放器。
让我们考虑一个从2019-03-01T14:02:46.309459Z
开始的国际象棋游戏:
ChessClock chessClock = new ChessClock(Player.LEFT); // 2019-03-01T14:02:46.309459Z Instant start = chessClock.gameStart();
此外,玩家执行以下一系列动作,直到右边的玩家赢得游戏:
Left moved first after 2 seconds: 2019-03-01T14:02:48.309459Z Right moved after 5 seconds: 2019-03-01T14:02:51.309459Z Left moved after 6 seconds: 2019-03-01T14:02:54.309459Z Right moved after 1 second: 2019-03-01T14:02:52.309459Z Left moved after 2 second: 2019-03-01T14:02:56.309459Z Right moved after 3 seconds: 2019-03-01T14:02:55.309459Z Left moved after 10 seconds: 2019-03-01T14:03:06.309459Z Right moved after 11 seconds and win: 2019-03-01T14:03:06.309459Z
看来时钟正确地记录了运动员的动作。
最后,比赛在 40 秒后结束:
Game ended:2019-03-01T14:03:26.350749300Z Instant end = chessClock.gameEnd(); Game duration: 40 seconds // Duration.between(start, end).getSeconds();
总结
任务完成了!本章提供了使用日期和时间信息的全面概述。广泛的应用必须处理这类信息。因此,将这些问题的解决方案放在你的工具带下不是可选的。从Date、Calendar到LocalDate、LocalTime、LocalDateTime、ZoneDateTime、OffsetDateTime、OffsetTime、Instant——它们在涉及日期和时间的日常任务中都是非常重要和有用的。