Java 8的日期时间类


时区问题

1. Date 并无时区问题,世界上任何一台计算机使用 new Date() 初始化得到的时间都一样。Date 中保存的是 UTC 时间,UTC 是以原子钟为基础的统一时间,不以太阳参照计时,并无时区划分。
2. Date 中保存的是一个时间戳,代表的是从 1970 年 1 月 1 日 0 点(Epoch 时间)到现在的毫秒数。
System.out.println(new Date(0));
System.out.println(TimeZone.getDefault().getID() + ":" + TimeZone.getDefault().getRawOffset()/3600000);
//Thu Jan 01 08:00:00 CST 1970
//Asia/Shanghai:8 上海时区相比UTC时差+8小时
方式一,以 UTC 保存,保存的时间没有时区属性,是不涉及时区时间差问题的世界统一时间。我们通常说的时间戳,或 Java 中的 Date 类就是用的这种方式,这也是推荐的方式。
方式二,以字面量保存,比如年 / 月 / 日 时: 分: 秒,一定要同时保存时区信息。Calendar 是有时区概念,所以我们通过不同的时区初始化 Calendar,得到了不同的时间。

正确处理时区

1. 使用Java 8 推出了新的时间日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter;
2. 注意得问题:
  -- 不同时区的人转换成 Date 会得到不同的时间(时间戳);
  -- 同一个 Date,在不同的时区下格式化得到不同的时间表示;
3. 正确处理时区,在于存进去和读出来两方面即存使用正确得当前时区保证UTC时间正确,读也用正确设置本地时区,把UTC时间转为正确得本地时间;

//一个时间表示
String stringDate = "2020-01-02 22:00:00";
//初始化三个时区
ZoneId timeZoneSH = ZoneId.of("Asia/Shanghai");
ZoneId timeZoneNY = ZoneId.of("America/New_York");
ZoneId timeZoneJST = ZoneOffset.ofHours(9);
//格式化器
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ZonedDateTime date = ZonedDateTime.of(LocalDateTime.parse(stringDate, dateTimeFormatter), timeZoneJST);
//使用DateTimeFormatter格式化时间,可以通过withZone方法直接设置格式化使用的时区
DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");//Asia/Shanghai2020-01-02 21:00:00 +0800
System.out.println(timeZoneSH.getId() + outputFormat.withZone(timeZoneSH).format(date));//America/New_York2020-01-02 08:00:00 -0500
System.out.println(timeZoneNY.getId() + outputFormat.withZone(timeZoneNY).format(date));//+09:002020-01-02 22:00:00 +0900
System.out.println(timeZoneJST.getId() + outputFormat.withZone(timeZoneJST).format(date));
//总结:使用ZonedDateTime保存时间,使用设置了ZoneId的DateTimeFormatter配合ZonedDateTime进行时间格式化得到本地时间表示。

日期时间格式化和解析

//问题一:小写 y 是年,而大写 Y 是 week year,也就是所在的周属于哪一年。导致格式化后提前跨年。
Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
System.out.println("defaultLocale:" + Locale.getDefault());//defaultLocale:zh_CN
Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.DECEMBER, 29,0,0,0);
SimpleDateFormat YYYY = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("格式化: " + YYYY.format(calendar.getTime()));//格式化: 2020-12-29
System.out.println("weekYear:" + calendar.getWeekYear());//weekYear:2020
System.out.println("firstDayOfWeek:" + calendar.getFirstDayOfWeek());//firstDayOfWeek:1
System.out.println("minimalDaysInFirstWeek:" + calendar.getMinimalDaysInFirstWeek());//minimalDaysInFirstWeek:1
//一年第一周的判断方式是,从 getFirstDayOfWeek() 开始,完整的 7 天,并且包含那一年至少 getMinimalDaysInFirstWeek() 天。
//这个计算方式和区域相关,对于当前 zh_CN 区域来说,2020 年第一周的条件是,从周日开始的完整 7 天,2020 年包含 1 天即可。
//显然,2019 年 12 月 29 日周日到 2020 年 1 月 4 日周六是 2020 年第一周,得出的 week year 就是 2020 年。
//问题二:定义的 static 的 SimpleDateFormat 可能会出现线程安全问题;
//问题三:当需要解析的字符串和格式不匹配的时候,SimpleDateFormat 表现得很宽容,还是能得到结果,需要注意;
避免以上三个问题:
使用 Java 8 中的 DateTimeFormatter,先使用DateTimeFormatterBuilder来定义格式化字符串,然后解析即可
private static DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR) //
.appendLiteral("/")
.appendValue(ChronoField.MONTH_OF_YEAR) //
.appendLiteral("/")
.appendValue(ChronoField.DAY_OF_MONTH) //
.appendLiteral(" ")
.appendValue(ChronoField.HOUR_OF_DAY) //
.appendLiteral(":")
.appendValue(ChronoField.MINUTE_OF_HOUR) //
.appendLiteral(":")
.appendValue(ChronoField.SECOND_OF_MINUTE) //
.appendLiteral(".")
.appendValue(ChronoField.MILLI_OF_SECOND) //毫秒
.toFormatter();

//这里DateTimeFormatter是线程安全的,定义为static使用;最后DateTimeFormatter解析比较严格,需要解析的字符串和格式不匹配会直接报错;
//使用刚才定义的DateTimeFormatterBuilder构建的DateTimeFormatter来解析这个时间
LocalDateTime localDateTime = LocalDateTime.parse("2020/1/2 12:34:56.789", dateTimeFormatter);
//解析成功
System.out.println(localDateTime.format(dateTimeFormatter));
//使用yyyyMM格式解析20160901是否可以成功呢?
String dt = "20160901";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM");
System.out.println("result:" + dateTimeFormatter.parse(dt));

日期时间的计算

1. 使用时间戳进行时间计算会出现int溢出问题,导致时间计算不正确;
//wrong
Date today = new Date();Date nextMonth = new Date(today.getTime() + 30 * 1000 * 60 * 60 * 24);
//right
Date today = new Date();Date nextMonth = new Date(today.getTime() + 30L * 1000 * 60 * 60 * 24);
2. java8之前建议使用Calendar:
Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.DAY_OF_MONTH, 30);
3. 使用Java8的日期时间类型之间进行各种计算(API功能强大):
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.plusDays(30));
  -- 可以使用各种minus和plus方法直接对日期进行加减操作,如加/减一天或一月
System.out.println(LocalDate.now()
        .minus(Period.ofDays(1))
        .plus(1, ChronoUnit.DAYS)
        .minusMonths(1)
        .plus(Period.ofMonths(1)));
 -- 通过with方法进行快捷时间调节
使用 TemporalAdjusters.firstDayOfMonth 得到当前月的第一天;
使用 TemporalAdjusters.firstDayOfYear() 得到当前年的第一天;
使用 TemporalAdjusters.previous(DayOfWeek.SATURDAY) 得到上一个周六;
使用 TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY) 得到本月最后一个周五。

System.out.println("//本月的第一天");
System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()));

System.out.println("//今年的程序员日");
System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfYear()).plusDays(255));

System.out.println("//今天之前的一个周六");
System.out.println(LocalDate.now().with(TemporalAdjusters.previous(DayOfWeek.SATURDAY)));

System.out.println("//本月最后一个工作日");
System.out.println(LocalDate.now().with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)));
  -- 直接使用lambda表达式进行自定义的时间跳转,如为当前时间增加100天以内的随即天数:

System.out.println(LocalDate.now().with(temporal -> temporal.plus(ThreadLocalRandom.current().nextInt(100), ChronoUnit.DAYS)));
-- 使用query方法查询是否匹配条件:
LocalDate.now().query(CommonMistakesApplication::isFamilyBirthday);
-- Java8中有一个专门的累Period定义了日期间隔,通过Period.between得到了两个LocalDate的差,
//返回的是两个日期差几年零几个月零几天。如果希望得知两个日期之间差几天,直接调用Period的getDays()方法
//得到的只是最后的“零几天”,而不是算总的间隔天数。

LocalDateTime转化为Date
LocalDateTime localDateTime = LocalDateTime.now().plusHours(24);//当前时间添加24小时
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

 Date的toString()方法

Date的toString()方法会输出类似CST之类的时区字样
// toString中相关源码,可以看到会获取当前时区
// 如果取不到则显示GMT进行格式化
// 因此,这里仅仅是时区,并不代表Date类内置了时区信息

mysql中数据类型datetime和timestamp区别

主要体现在占用空间、表示的时间范围和时区三个方面:
1. 占用空间:
//datetime占用8字节;timestamp占用4字节;
2. 时间范围:
//datetime表示的范围是从“1000-01-01 00:00:00.000000”到“9999-12-31 23:59:59.999999”;
//timestamp表示的范围是从“1970-01-01 00:00:01.000000”到“2038-01-19 03:14:07.999999”。
3. 时区:
//timestamp保存的时候根据当前时区转换为UTC,查询的时候再根据前时区从UTC转回来;
//而datetime就是一个死的字符串时间(仅仅对mysql本身而言)表示。
注意:
//datetime不包含时区是固定的时间表示,仅仅是指mysql本身。
//使用timestamp,需要考虑java进程的时区和mysql连接的时区。
//而使用datetime类型,则只需要考虑Java进程的时区(
//因为mysql datetime没有时区信息了,j
//dbc时间戳转换成mysql datetime,
//会根据mysql的serverTimezone做一次转换)。

相关