自从 14 年发布 Java 8 以后,我们古老 java.util.Date 终于不再是我们 Java 里操作日期时间的唯一的选择,今天小编就来聊一聊关于java日期设计?接下来我们就一起去研究一下吧!
java日期设计
Java 中的时间日期 API自从 14 年发布 Java 8 以后,我们古老 java.util.Date 终于不再是我们 Java 里操作日期时间的唯一的选择。
其实 Java 里的日期时间的相关 API 一直为世猿诟病,不仅在于它设计分上工不明确,往往一个类既能处理日期又能处理时间,很混乱,还在于某些年月日期的数值映射存储反人类,例如:0 对应月份一月,11 对应月份十二月,118 对应年份 2018(1900 118)等。
往往我们得到某个年月值还需要再做相应的运算才能得到准确的年月日信息,直到我们的 Java 8 ,借鉴了第三方开源库 Joda-Time 的优秀设计,重新设计了一个日期时间 API,相比之前,可以说好用百倍,相关 API 接口全部位于包 java.time 下。
古老的日期时间接口1)表示时刻信息的 Date
世界上所有的计算机内部存储时间都使用一个 long 类型的整数,而这个整数的值就是相对于英国格林尼治标准时间(1970年1月1日0时0分0秒)的毫秒数。例如:
public static void main(String[] args){
//January 1, 1970 00:00:00 GMT.
Date date = new Date(1000);
System.out.println(date);
}
输出结果:
//1970-1-1 8:00:01
Thu Jan 01 08:00:01 CST 1970
很多人可能会疑惑,1000 表示的是距离标准时间往后 1 秒,那为什么时间却多走了 八个小时?
这和「时区」有关系,如果你位于英国的格林尼治区,那么结果会如预想一样,但是我们位于中国东八区,时间要早八个小时,所以不同时区基于的基础值不同。
Date 这个类以前真的扮演过很多角色,从它的源码就可以看出来,有可以操作时刻的方法,有可以操作年月日的方法,甚至它还能管时区。可以说,日期时间的相关操作有它一个人就足够了。
但这个世界就是这样,你管的东西多了,自然就不能面面俱到,Date 中很多方法的设计并不是很合理,之前我们也说了,甚至有点反人类。所以,现在的 Date 类中接近百分之八十的方法都已废弃,被标记为 @Deprecated。
sun 公司给 Date 的目前定位是,唯一表示一个时刻,所以它的内部应该围绕着那个整型的毫秒,而不再着重于各种年历时区等信息。
Date 允许通过以下两种构造器实例化一个对象:
private transient long fastTime;
public Date() {
this(System.currentTimeMillis());
}
public Date(long date) {
fastTime = date;
}
这里的 fastTime 属性存储的就是时刻所对应的毫秒数,两个构造器还是很简单,如果调用的是无参构造器,那么虚拟机将以系统当前的时刻值对 fastTime 进行赋值。
还有几个为数不多没有被废弃的方法:
- public long getTime() :返回内部存储的毫秒数
- public void setTime(long time):重新设置内存的毫秒数
- public boolean before(Date when):比较给定的时刻是否早于当前 Date 实例
- public boolean after(Date when):比较给定的时刻是否晚于当前 Date 实例
还有两个方法是 jdk1.8 以后新增的,用于向 Java 8 新增接口的转换,待会介绍。
2)描述年历的 Calendar
Calendar 用于表示年月日等日期信息,它是一个抽象类,所以一般通过以下四种工厂方法获取它的实例对象。
public static Calendar getInstance()
public static Calendar getInstance(TimeZone zone)
public static Calendar getInstance(Locale aLocale)
public static Calendar getInstance(TimeZone zone,Locale aLocale)
其实内部最终会调用同一个内部方法:
private static Calendar createCalendar(TimeZone zone,Locale aLocale)
该方法需要两个参数,一个是时区,一个是国家和语言,也就是说,构建一个 Calendar 实例最少需要提供这两个参数信息,否则将会使用系统默认的时区或语言信息。
因为不同的时区与国家语言对于时刻和年月日信息的输出是不同的,所以这也是为什么一个 Calendar 实例必须传入时区和国家信息的一个原因。看个例子:
public static void main(String[] args){
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getTime());
Calendar calendar1 = Calendar.getInstance
(TimeZone.getTimeZone("GMT"), Locale.ENGLISH);
System.out.println( calendar1.get(Calendar.YEAR) ":"
calendar1.get(Calendar.HOUR) ":"
calendar1.get(Calendar.MINUTE));
}
输出结果:
Sat Apr 21 10:32:20 CST 2018
2018:2:32
可以看到,第一个输出为我们系统默认时区与国家的当前时间,而第二个 Calendar 实例我们指定了它位于格林尼治时区(0 时区),结果也显而易见了,相差了八个小时,那是因为我们位于东八区,时间早于 0 时区八个小时。
可能有人会疑惑了,为什么第二个 Calendar 实例的输出要如此复杂的拼接,而不像第一个 Calendar 实例那样直接调用 getTime 方法简洁呢?
这涉及到 Calendar 的内部实现,我们一起看看:
protected long time;
public final Date getTime() {
return new Date(getTimeInMillis());
}
和 Date 一样,Calendar 的内部也维护着一个时刻信息,而 getTime 方法实际上是根据这个时刻构建了一个 Date 对象并返回的。
而一般我们构建 Calendar 实例的时候都不会传入一个时刻信息,所以这个 time 的值在实例初始化的时候,程序会根据系统默认的时区和当前时间计算得到一个毫秒数并赋值给 time。
所以,所有未手动修改 time 属性值的 Calendar 实例的内部,time 的值都是当时系统默认时区的时刻数值。也就是说,getTime 的输出结果是不会理会当前实例所对应的时区信息的,这也是我觉得 Calendar 设计的一个缺陷所在,因为这样会导致两个不同时区 Calendar 实例的 getTime 输出值只取决于实例初始化时系统的运行时刻。
Calendar 中也定义了很多静态常量和一些属性数组:
public final static int ERA = 0;
public final static int YEAR = 1;
public final static int MONTH = 2;
public final static int WEEK_OF_YEAR = 3;
public final static int WEEK_OF_MONTH = 4;
public final static int DATE = 5;
....
protected int fields[];
protected boolean isSet[];
...
有关日期的所有相关信息都存储在属性数组中,而这些静态常量的值往往表示的就是一个索引值,通过 get 方法,我们传入一个属性索引,返回得到该属性的值。例如:
Calendar myCalendar = Calendar.getInstance();
int year = myCalendar.get(Calendar.YEAR);
这里的 get 方法实际上就是直接取的 fields[1] 作为返回值,而 fields 属性数组在 Calendar 实例初始化的时候就已经由系统根据时区和语言计算并赋值了,注意,这里会根据你指定的时区进行计算,它不像 time 始终是依照的系统默认时区。
个人觉得 Calendar 的设计有优雅的地方,也有不合理的地方,毕竟是个「古董」了,终将被替代。
3)DateFormat 格式化转换
从我们之前的一个例子中可以看到,Calendar 想要输出一个预期格式的日期信息是很麻烦的,需要自己手动拼接。而我们的 DateFormat 就是用来处理格式化字符串和日期时间之间的转换操作的。
DateFormat 和 Calendar 一样,也是一个抽象类,我们需要通过工厂方式产生其实例对象,主要有以下几种工厂方法:
//只处理时间的转换
public final static DateFormat getTimeInstance()
//只处理日期的转换
public final static DateFormat getDateInstance()
//既可以处理时间,也可以处理日期
public final static DateFormat getDateTimeInstance()
当然,它们各自都有各自的重载方法,具体的我们待会儿看。
DateFormat 有两类方法,format 和 parse。
public final String format(Date date)
public Date parse(String source)
format 方法用于将一个日期对象格式化为字符串,parse 方法用于将一个格式化的字符串装换为一个日期对象。例如:
public static void main(String[] args){
Calendar calendar = Calendar.getInstance();
DateFormat dateFormat = DateFormat.getDateTimeInstance();
System.out.println(dateFormat.format(calendar.getTime()));
}
输出结果:
2018-4-21 16:58:09
显然,使用工厂构造的 DateFormat 实例并不能够自定义输出格式化内容,即输出的字符串格式是固定的,不能满足某些情况下的特殊需求。一般我们会直接使用它的一个实现类,SimpleDateFormat。
SimpleDateFormat 允许在构造实例的时候传入一个 pattern 参数,自定义日期字符的输出格式。例如:
public static void main(String[] args){
DateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日");
System.out.println(dateFormat.format(new Date()));
}
输出结果:
2018年04月21日
其中,
- yyyy:年份用四位进行输出
- MM:月份用两位进行输出
- dd:两位表示日信息
- HH:两位来表示小时数
- mm:两位表示分钟数
- ss:两位来表示秒数
- E:表示周几,如果 Locale 在中国则会输出 星期x,如果在美国或英国则会输出英文的星期
- a:表示上午或下午
当然,对于字符串转日期也是很方便的,允许自定义模式,但必须遵守自己制定的模式,否则程序将无法成功解析。例如:
public static void main(String[] args){
String str = "2018年4月21日 17点17分 星期六";
DateFormat sDateFormat = new SimpleDateFormat("yyyy年M月dd日 HH点mm分 E");
sDateFormat.parse(str);
System.out.println(sDateFormat.getCalendar().getTime());
}
输出结果:
Sat Apr 21 17:17:00 CST 2018
显然,程序是正确的解析的我们的字符串并转换为 Calendar 对象存储在 DateFormat 内部的。
总的来说,Date、Calendar 和 DateFormat 已经能够处理一般的时间日期问题了,但是不可避免的是,它们依然很繁琐,不好用。
新式的时间日期处理接口1)表示时刻的 Instant
Instant 和 Date 一样,表示一个时间戳,用于描述一个时刻,只不过它较 Date 而言,可以描述更加精确的时刻。并且 Instant 是时区无关的。
Date 最多可以表示毫秒级别的时刻,而 Instant 可以表示纳秒级别的时刻。例如:
- public static Instant now():根据系统当前时间创建一个 Instant 实例,表示当前时刻
- public static Instant ofEpochSecond(long epochSecond):通过传入一个标准时间的偏移值来构建一个 Instant 实例
- public static Instant ofEpochMilli(long epochMilli):通过毫秒数值直接构建一个 Instant 实例
看看代码:
public static void main(String[] args){
//创建 Instant 实例
Instant instant = Instant.now();
System.out.println(instant);
Instant instant1 = Instant.ofEpochSecond(20);
System.out.println(instant1);
Instant instant2 = Instant.ofEpochSecond(30,100);
System.out.println(instant2);
Instant instant3 = Instant.ofEpochMilli(1000);
System.out.println(instant3);
}
输出结果:
2018-04-23T02:43:10.973Z
1970-01-01T00:00:20Z
1970-01-01T00:00:30.000000100Z
1970-01-01T00:00:01Z
可以看到,Instant 和 Date 不同的是,它是时区无关的,始终是格林零时区相关的,也即是输出的结果始终格林零时区时间。
2)处理日期的 LocalDate
不同于 Calendar 既能处理日期又能处理时间,java.time 的新式 API 分离开日期和时间,用单独的类进行处理。LocalDate 专注于处理日期相关信息。
LocalDate 依然是一个不可变类,它关注时间中年月日部分,我们可以通过以下的方法构建和初始化一个 LocalDate 实例:
- public static LocalDate now():截断当前系统时间的年月日信息并初始化一个实例对象
- public static LocalDate of(int year, int month, int dayOfMonth):显式指定年月日信息
- public static LocalDate ofYearDay(int year, int dayOfYear):根据 dayOfYear 可以推出 month 和 dayOfMonth
- public static LocalDate ofEpochDay(long epochDay):相对于格林零时区时间的日偏移量
看看代码:
public static void main(String[] args){
//构建 LocalDate 实例
LocalDate localDate = LocalDate.now();
System.out.println(localDate);
LocalDate localDate1 = LocalDate.of(2017,7,22);
System.out.println(localDate1);
LocalDate localDate2 = LocalDate.ofYearDay(2018,100);
System.out.println(localDate2);
LocalDate localDate3 = LocalDate.ofEpochDay(10);
System.out.println(localDate3);
}
输出结果:
2018-04-23
2017-07-22
2018-04-10
1970-01-11
需要注意一点,LocalDate 会根据系统中当前时刻和默认时区计算出年月日的信息。
除此之外,LocalDate 中还有大量关于日期的常用方法:
- public int getYear():获取年份信息
- public int getMonthValue():获取月份信息
- public int getDayOfMonth():获取当前日是这个月的第几天
- public int getDayOfYear():获取当前日是这一年的第几天
- public boolean isLeapYear():是否是闰年
- public int lengthOfYear():获取这一年有多少天
- public DayOfWeek getDayOfWeek():返回星期信息
- 等等
这些方法都见名知意,此处不再赘述。
3)处理时间的 LocalTime
类似于 LocalDate,LocalTime 专注于时间的处理,它提供小时,分钟,秒,毫微秒的各种处理,我们依然可以通过类似的方式创建一个 LocalTime 实例。
- public static LocalTime now():根据系统当前时刻获取其中的时间部分内容
- public static LocalTime of(int hour, int minute):显式传入小时和分钟来构建一个实例对象
- public static LocalTime of(int hour, int minute, int second):通过传入时分秒构造实例
- public static LocalTime of(int hour, int minute, int second, int nanoOfSecond):传入时分秒和毫微秒构建一个实例
- public static LocalTime ofSecondOfDay(long secondOfDay):传入一个长整型数值代表当前日已经过去的秒数
- public static LocalTime ofNanoOfDay(long nanoOfDay):传入一个长整型代表当前日已经过去的毫微秒数
同样的,LocalTime 默认使用系统默认时区处理时间,看代码:
public static void main(String[] a){
LocalTime localTime = LocalTime.now();
System.out.println(localTime);
LocalTime localTime1 = LocalTime.of(23,59);
System.out.println(localTime1);
LocalTime localTime2 = LocalTime.ofSecondOfDay(10);
System.out.println(localTime2);
}
输出结果:
13:59:03.723
23:59
00:00:10
当然,LocalTime 中也同样封装了很多好用的工具方法,例如:
- public int getHour()
- public int getMinute()
- public int getSecond()
- public int getNano()
- public LocalTime withHour(int hour):修改当前 LocalTime 实例中的 hour 属性并重新返回一个新的实例
- public LocalTime withMinute(int minute):类似
- public LocalTime withSecond(int second)
- 等等
LocalDateTime 类则是集成了 LocalDate 和 LocalTime,它既能表示日期,又能表述时间信息,方法都类似,只是有一部分涉及时区的转换内容,我们待会说。
4)时区相关的日期时间处理 ZonedDateTime
无论是我们的 LocalDate,或是 LocalTime,甚至是 LocalDateTime,它们基本是时区无关的,内部并没有存储时区属性,而基本用的系统默认时区。往往有些场景之下,缺乏一定的灵活性。
ZonedDateTime 可以被理解为 LocalDateTime 的外层封装,它的内部存储了一个 LocalDateTime 的实例,专门用于普通的日期时间处理。此外,它还定义了 ZoneId 和 ZoneOffset 来描述时区的概念。
ZonedDateTime 和 LocalDateTime 的一个很大的不同点在于,后者内部并没有存储时区,所以对于系统的依赖性很强,往往换一个时区可能就会导致程序中的日期时间不一致。
而后者则可以通过传入时区的名称,使用 ZoneId 进行匹配存储,也可以通过传入与零时区的偏移量,使用 ZoneOffset 存储时区信息。
所以,构建一个 ZonedDateTime 实例有以下几种方式:
- public static ZonedDateTime now():系统将以默认时区计算并存储日期时间信息
- public static ZonedDateTime now(ZoneId zone):指定时区
- public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone):指定日期时间和时区
- public static ZonedDateTime of(LocalDateTime localDateTime, ZoneId zone)
- public static ZonedDateTime ofInstant(Instant instant, ZoneId zone):通过时刻和时区构建实例对象
- 等等
看代码:
public static void main(String[] a){
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(zonedDateTime);
LocalDateTime localDateTime = LocalDateTime.now();
ZoneId zoneId = ZoneId.of("America/Los_Angeles");
ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime,zoneId);
System.out.println(zonedDateTime1);
Instant instant = Instant.now();
ZoneId zoneId1 = ZoneId.of("GMT");
ZonedDateTime zonedDateTime2 = ZonedDateTime.ofInstant(instant,zoneId1);
System.out.println(zonedDateTime2);
}
输出结果:
2018-04-23T16:10:29.510 08:00[Asia/Shanghai]
2018-04-23T16:10:29.511-07:00[America/Los_Angeles]
2018-04-23T08:10:29.532Z[GMT]
简单解释一下,首先第一个输出应该没什么问题,系统保存当前系统日期和时间以及默认的时区。
第二个小例子,LocalDateTime 实例保存了时区无关的当前日期时间信息,也就是这里的年月日时分秒,接着构建一个 ZonedDateTime 实例并传入一个美国时区(西七区)。你会发现输出的日期时间为西七区的 16 点 29 分。
像这种关联了时区的日期时间就很能够解决那种,换时区导致程序中时间错乱的问题。因为我关联了时区,无论你程序换到什么地方运行了,日期 时区 本就已经唯一确定了某个时刻,就相当于我在存储某个时刻的时候,我说明了这是某某时区的某某时间,即便你换了一个地区,你也不至于把这个时间按自己当前的时区进行解析并直接使用了吧。
第三个小例子就更加的直接明了了,构建 ZonedDateTime 实例的时候,给定一个时刻和一个时区,而这个时刻值就是相对于给定时区的标准时间所经过的毫秒数。
有关 ZonedDateTime 的其他日期时间的处理方法和 LocalDateTime 是一样的,因为 ZonedDateTime 是直接封装了一个 LocalDateTime 实例对象,所以所有相关日期时间的操作都会间接的调用 LocalDateTime 实例的方法,我们不再赘述。
5)格式化日期时间
Java 8 的新式日期时间 API 中,DateTimeFormatter 作为格式化日期时间的主要类,它与之前的 DateFormat 类最大的不同就在于它是线程安全的,其他的使用上的操作基本类似。我们看看:
public static void main(String[] a){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(formatter.format(localDateTime));
String str = "2008年08月23日 23:59:59";
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
LocalDateTime localDateTime2 = LocalDateTime.parse(str,formatter2);
System.out.println(localDateTime2);
}
输出结果:
2018年04月23日 17:27:24
2008-08-23T23:59:59
格式化主要有两种情况,一种是将日期时间格式化成字符串,另一种则是将格式化的字符串转换成日期时间对象。
DateTimeFormatter 提供将 format 方法将一个日期时间对象转换成格式化的字符串,但是反过来的操作却建议使用具体的日期时间类自己的 parse 方法,这样可以省去类型转换的步骤。
6)时间差
现实项目中,我们也经常会遇到计算两个时间点之间的差值的情况,最粗暴的办法是,全部幻化成毫秒数并进行减法运算,最后再转换回日期时间对象。
但是 java.time 包中提供了三个日期时间之间的差值的计算方法,我们一起看看。
关于时间差的计算,主要涉及到三个类:
- Period:处理两个日期之间的差值
- Duration:处理两个时间之间的差值
- ChronoUnit:可用于在单个时间单位内测量一段时间,例如天数或秒
例如:
public static void main(String[] args){
LocalDate date = LocalDate.of(2017,7,22);
LocalDate date1 = LocalDate.now();
Period period = Period.between(date,date1);
System.out.println(period.getYears() "年"
period.getMonths() "月"
period.getDays() "天");
LocalTime time = LocalTime.of(20,30);
LocalTime time1 = LocalTime.of(23,59);
Duration duration = Duration.between(time,time1);
System.out.println(duration.toMinutes() "分钟");
LocalDateTime startDate = LocalDateTime.of(2017, Month.JUNE, 17,12,1,1);
System.out.println("开始时间 : " startDate);
LocalDateTime endDate = LocalDateTime.of(2017, Month.JUNE, 18,13,1,1);
System.out.println("结束时间 : " endDate);
long hourDiff = ChronoUnit.HOURS.between(startDate, endDate);
System.out.println("两天之间的差几个小时 : " hourDiff);
}
输出结果:
0年9月1天
209分钟
开始时间 : 2017-06-17T12:01:01
结束时间 : 2017-06-18T13:01:01
两天之间的差几个小时 : 25
显然,年月日的日期间差值的计算使用 Period 类足以,而时分秒毫秒的时间的差值计算则需要使用 Duration 类或者ChronoUnit类。
转载自
https://www.cnblogs.com/yangming1996/p/8902484.html
https://www.cnblogs.com/yangming1996/p/8921381.html
,