介绍
本篇文章主要介绍java源码中提供了哪些日期和时间的类
日期和时间的两套API
java提供了两套处理日期和时间的API
1、旧的API,放在java.util 这个包下的:比较常用的有Date和Calendar等
2、新的API是java 8新引入的,放在java.time 这个包下的:LocalDateTime,ZonedDateTime,DateTimeFormatter和Instant等
为什么会有两套日期时间API,这个是有历史原因的,旧的API是jdk刚开始就提供的,随着版本的升级,逐渐发现原先的api不满足需要,暴露了一些问题,所以在java 8 这个版本中,重新引入新API。
这两套API都要了解,为什么呢?
因为java 8 发布时间是2014年,很多之前的系统还是沿用旧的API,所以这两套API都要了解,同时还要掌握两套API相互转化的技术。
一:Date
支持版本及以上
JDK1.0
介绍
Date类说明
Date类负责时间的表示,在计算机中,时间的表示是一个较大的概念,现有的系统基本都是利用从1970.1.1 00:00:00 到当前时间的毫秒数进行计时,这个时间称为epoch(时间戳)
package java.util; public class Date implements java.io.Serializable, Cloneable, Comparable<Date> { ... private transient long fastTime; .... } 复制代码
java.u是java提供表示日期和时间的类,类里有个long 类型的变量fastTime,它是用来存储以毫秒表示的时间戳。
date常用的用法
import java.u; ----------------------------------------- //获取当前时间 Date date = new Date(); Sy("获取当前时间:"+date); //获取时间戳 Sy("获取时间戳:"+da()); // date时间是否大于afterDate 等于也为false Date afterDate = new Date(da()-3600*24*1000); Sy("after:"+da(afterDate)); Sy("after:"+da(date)); // date时间是否小于afterDate 等于也为false Date beforeDate = new Date(da()+3600*24*1000); Sy("before:"+da(beforeDate)); Sy("before:"+da(date)); //两个日期比较 Sy("compareTo:"+da(date)); Sy("compareTo:"+da(afterDate)); Sy("compareTo:"+da(beforeDate)); //转为字符串 Sy("转为字符串:"+da()); //转为GMT时区 toGMTString() java8 中已废弃 Sy("转为GMT时区:"+da()); //转为本地时区 toLocaleString() java8 已废弃 Sy("转为本地时区:"+da()); 复制代码
自定义时间格式-SimpleDateFormat
date的toString方法转成字符串,不是我们想要的时间格式,如果要自定义时间格式,就要使用SimpleDateFormat
//获取当前时间 Date date = new Date(); Sy("获取当前时间:"+date); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Sy(date)); SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒"); Sy(date)); 复制代码
SimpleDateFormat也可以方便的将字符串转成Date
//获取当前时间 String str = "2021-07-13 23:48:23"; try { Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(str); Sy(date); } catch (ParseException e) { e.printStackTrace(); } 复制代码
日期和时间格式化参数说明
yyyy:年 MM:月 dd:日 hh:1~12小时制(1-12) HH:24小时制(0-23) mm:分 ss:秒 S:毫秒 E:星期几 D:一年中的第几天 F:一月中的第几个星期(会把这个月总共过的天数除以7) w:一年中的第几个星期 W:一月中的第几星期(会根据实际情况来算) a:上下午标识 k:和HH差不多,表示一天24小时制(1-24)。 K:和hh差不多,表示一天12小时制(0-11)。 z:表示时区 复制代码
SimpleDateFormat线程不安全原因及解决方案
SimpleDateFormat线程为什么是线程不安全的呢?
来看看SimpleDateFormat的源码,先看format方法:
// Called from Format after creating a FieldDelegate private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); ... } 复制代码
问题就出在成员变量calendar,如果在使用SimpleDateFormat时,用static定义,那SimpleDateFormat变成了共享变量。那SimpleDateFormat中的calendar就可以被多个线程访问到。
SimpleDateFormat的parse方法也是线程不安全的:
public Date parse(String text, ParsePosition pos) { ... Date parsedDate; try { parsedDate = calb.establish(calendar).getTime(); // If the year value is ambiguous, // then the two-digit year == the default start year if (ambiguousYear[0]) { if (parsedDa(defaultCenturyStart)) { parsedDate = calb.addYear(100).establish(calendar).getTime(); } } } // An IllegalArgumentException will be thrown by Calendar.getTime() // if any fields are out of range, e.g., MONTH == 17. catch (IllegalArgumentException e) { = start; = oldStart; return null; } return parsedDate; } 复制代码
由源码可知,最后是调用**parsedDate = calb.establish(calendar).getTime();**获取返回值。方法的参数是calendar,calendar可以被多个线程访问到,存在线程不安全问题。
我们再来看看**calb.establish(calendar)**的源码
calb.establish(calendar)方法先后调用了cal.clear()和cal.set(),先清理值,再设值。但是这两个操作并不是原子性的,也没有线程安全机制来保证,导致多线程并发时,可能会引起cal的值出现问题了。
验证SimpleDateFormat线程不安全
public class SimpleDateFormatDemoTest { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { //1、创建线程池 ExecutorService pool = Execu(5); //2、为线程池分配任务 ThreadPoolTest threadPoolTest = new ThreadPoolTest(); for (int i = 0; i < 10; i++) { (threadPoolTest); } //3、关闭线程池 (); } static class ThreadPoolTest implements Runnable{ @Override public void run() { String dateString = (new Date()); try { Date parseDate = (dateString); String dateString2 = (parseDate); Sy().getName()+" 线程是否安全: "+da(dateString2)); } catch (Exception e) { Sy().getName()+" 格式化失败 "); } } } } 复制代码
出现了两次false,说明线程是不安全的。而且还抛异常,这个就严重了。
解决方案
这个是阿里巴巴 java开发手册中的规定:
1、不要定义为static变量,使用局部变量
2、加锁:synchronized锁和lock锁
3、使用ThreadLocal方式
4、使用DateTimeFormatter代替SimpleDateFormat(DateTimeFormatter是线程安全的,java 8+支持)
5、使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,不受限于java版本)
解决方案1:不要定义为static变量,使用局部变量
就是要使用SimpleDateFormat对象进行format或parse时,再定义为局部变量。就能保证线程安全。
public class SimpleDateFormatDemoTest1 { public static void main(String[] args) { //1、创建线程池 ExecutorService pool = Execu(5); //2、为线程池分配任务 ThreadPoolTest threadPoolTest = new ThreadPoolTest(); for (int i = 0; i < 10; i++) { (threadPoolTest); } //3、关闭线程池 (); } static class ThreadPoolTest implements Runnable{ @Override public void run() { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateString = (new Date()); try { Date parseDate = (dateString); String dateString2 = (parseDate); Sy().getName()+" 线程是否安全: "+da(dateString2)); } catch (Exception e) { Sy().getName()+" 格式化失败 "); } } } } 复制代码
由图可知,已经保证了线程安全,但这种方案不建议在高并发场景下使用,因为会创建大量的SimpleDateFormat对象,影响性能。
解决方案2:加锁:synchronized锁和Lock锁
加synchronized锁: SimpleDateFormat对象还是定义为全局变量,然后需要调用SimpleDateFormat进行格式化时间时,再用synchronized保证线程安全。
public class SimpleDateFormatDemoTest2 { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { //1、创建线程池 ExecutorService pool = Execu(5); //2、为线程池分配任务 ThreadPoolTest threadPoolTest = new ThreadPoolTest(); for (int i = 0; i < 10; i++) { (threadPoolTest); } //3、关闭线程池 (); } static class ThreadPoolTest implements Runnable{ @Override public void run() { try { synchronized (simpleDateFormat){ String dateString = (new Date()); Date parseDate = (dateString); String dateString2 = (parseDate); Sy().getName()+" 线程是否安全: "+da(dateString2)); } } catch (Exception e) { Sy().getName()+" 格式化失败 "); } } } } 复制代码
如图所示,线程是安全的。定义了全局变量SimpleDateFormat,减少了创建大量SimpleDateFormat对象的损耗。但是使用synchronized锁, 同一时刻只有一个线程能执行锁住的代码块,在高并发的情况下会影响性能。但这种方案不建议在高并发场景下使用 加Lock锁: 加Lock锁和synchronized锁原理是一样的,都是使用锁机制保证线程的安全。
public class SimpleDateFormatDemoTest3 { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static Lock lock = new ReentrantLock(); public static void main(String[] args) { //1、创建线程池 ExecutorService pool = Execu(5); //2、为线程池分配任务 ThreadPoolTest threadPoolTest = new ThreadPoolTest(); for (int i = 0; i < 10; i++) { (threadPoolTest); } //3、关闭线程池 (); } static class ThreadPoolTest implements Runnable{ @Override public void run() { try { lock.lock(); String dateString = (new Date()); Date parseDate = (dateString); String dateString2 = (parseDate); Sy().getName()+" 线程是否安全: "+da(dateString2)); } catch (Exception e) { Sy().getName()+" 格式化失败 "); }finally { lock.unlock(); } } } } 复制代码
由结果可知,加Lock锁也能保证线程安全。要注意的是,最后一定要释放锁,代码里在finally里增加了lock.unlock();,保证释放锁。 在高并发的情况下会影响性能。这种方案不建议在高并发场景下使用
解决方案3:使用ThreadLocal方式
使用ThreadLocal保证每一个线程有SimpleDateFormat对象副本。这样就能保证线程的安全。
public class SimpleDateFormatDemoTest4 { private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static void main(String[] args) { //1、创建线程池 ExecutorService pool = Execu(5); //2、为线程池分配任务 ThreadPoolTest threadPoolTest = new ThreadPoolTest(); for (int i = 0; i < 10; i++) { (threadPoolTest); } //3、关闭线程池 (); } static class ThreadPoolTest implements Runnable{ @Override public void run() { try { String dateString = ().format(new Date()); Date parseDate = ().parse(dateString); String dateString2 = ().format(parseDate); Sy().getName()+" 线程是否安全: "+da(dateString2)); } catch (Exception e) { Sy().getName()+" 格式化失败 "); }finally { //避免内存泄漏,使用完threadLocal后要调用remove方法清除数据 (); } } } } 复制代码
使用ThreadLocal能保证线程安全,且效率也是挺高的。适合高并发场景使用。
解决方案4:使用DateTimeFormatter代替SimpleDateFormat
使用DateTimeFormatter代替SimpleDateFormat(DateTimeFormatter是线程安全的,java 8+支持) DateTimeFormatter介绍
public class DateTimeFormatterDemoTest5 { private static DateTimeFormatter dateTimeFormatter = Da("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { //1、创建线程池 ExecutorService pool = Execu(5); //2、为线程池分配任务 ThreadPoolTest threadPoolTest = new ThreadPoolTest(); for (int i = 0; i < 10; i++) { (threadPoolTest); } //3、关闭线程池 (); } static class ThreadPoolTest implements Runnable{ @Override public void run() { try { String dateString = da()); TemporalAccessor temporalAccessor = da(dateString); String dateString2 = da(temporalAccessor); Sy().getName()+" 线程是否安全: "+da(dateString2)); } catch (Exception e) { e.printStackTrace(); Sy().getName()+" 格式化失败 "); } } } } 复制代码
使用DateTimeFormatter能保证线程安全,且效率也是挺高的。适合高并发场景使用。
解决方案5:使用FastDateFormat 替换SimpleDateFormat
使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,不受限于java版本)
public class FastDateFormatDemo6 { private static FastDateFormat fastDateFormat = Fa("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { //1、创建线程池 ExecutorService pool = Execu(5); //2、为线程池分配任务 ThreadPoolTest threadPoolTest = new ThreadPoolTest(); for (int i = 0; i < 10; i++) { (threadPoolTest); } //3、关闭线程池 (); } static class ThreadPoolTest implements Runnable{ @Override public void run() { try { String dateString = (new Date()); Date parseDate = (dateString); String dateString2 = (parseDate); Sy().getName()+" 线程是否安全: "+da(dateString2)); } catch (Exception e) { e.printStackTrace(); Sy().getName()+" 格式化失败 "); } } } } 复制代码
使用FastDateFormat能保证线程安全,且效率也是挺高的。适合高并发场景使用。
FastDateFormat源码分析
Apache Commons Lang 3.5 复制代码
//FastDateFormat @Override public String format(final Date date) { return (date); } @Override public String format(final Date date) { final Calendar c = Calendar.getInstance(timeZone, locale); c.setTime(date); return applyRulesToString(c); } 复制代码
源码中 Calender 是在 format 方法里创建的,肯定不会出现 setTime 的线程安全问题。这样线程安全疑惑解决了。那还有性能问题要考虑?
我们来看下FastDateFormat是怎么获取的
Fa(); Fa(CHINESE_DATE_TIME_PATTERN); 复制代码
看下对应的源码
/** * 获得 FastDateFormat实例,使用默认格式和地区 * * @return FastDateFormat */ public static FastDateFormat getInstance() { return CACHE.getInstance(); } /** * 获得 FastDateFormat 实例,使用默认地区<br> * 支持缓存 * * @param pattern 使用{@link java.} 相同的日期格式 * @return FastDateFormat * @throws IllegalArgumentException 日期格式问题 */ public static FastDateFormat getInstance(final String pattern) { return CACHE.getInstance(pattern, null, null); } 复制代码
这里有用到一个CACHE,看来用了缓存,往下看
private static final FormatCache<FastDateFormat> CACHE = new FormatCache<FastDateFormat>(){ @Override protected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) { return new FastDateFormat(pattern, timeZone, locale); } }; // abstract class FormatCache<F extends Format> { ... private final ConcurrentMap<Tuple, F> cInstanceCache = new ConcurrentHashMap<>(7); private static final ConcurrentMap<Tuple, String> C_DATE_TIME_INSTANCE_CACHE = new ConcurrentHashMap<>(7); ... } 复制代码
在getInstance 方法中加了ConcurrentMap 做缓存,提高了性能。且我们知道ConcurrentMap 也是线程安全的。
实践
/** * 年月格式 {@link FastDateFormat}:yyyy-MM */ public static final FastDateFormat NORM_MONTH_FORMAT = Fa(NORM_MONTH_PATTERN); 复制代码
//FastDateFormat public static FastDateFormat getInstance(final String pattern) { return CACHE.getInstance(pattern, null, null); } 复制代码
如图可证,是使用了ConcurrentMap 做缓存。且key值是格式,时区和locale(语境)三者都相同为相同的key。
问题
1、tostring()输出时,总以系统的默认时区格式输出,不友好。
2、时区不能转换
3、日期和时间的计算不简便,例如计算加减,比较两个日期差几天等。
4、格式化日期和时间的SimpleDateFormat对象是线程不安全的
5、Date对象本身也是线程不安全的
public class Date implements java.io.Serializable, Cloneable, Comparable<Date> { ... } 复制代码
二:Calendar
支持版本及以上
JDK1.1
介绍
Calendar类说明
Calendar类提供了获取或设置各种日历字段的各种方法,比Date类多了一个可以计算日期和时间的功能。
Calendar常用的用法
// 获取当前时间: Calendar c = Calendar.getInstance(); int y = c.ge); int m = 1 + c.ge); int d = c.ge); int w = c.ge); int hh = c.ge); int mm = c.ge); int ss = c.ge); int ms = c.ge); Sy("返回的星期:"+w); Sy(y + "-" + m + "-" + d + " " + " " + hh + ":" + mm + ":" + ss + "." + ms); 复制代码
如上图所示,月份计算时,要+1;返回的星期是从周日开始计算,周日为1,1~7表示星期;
Calendar的跨年问题和解决方案
问题
背景:在使用Calendar 的api getWeekYear()读取年份,在跨年那周的时候,程序获取的年份可能不是我们想要的,例如在2019年30号时,要返回2019,结果是返回2020,是不是有毒
// 获取当前时间: Calendar c = Calendar.getInstance(); c.clear(); String str = "2019-12-30"; try { c.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(str)); int y = c.getWeekYear(); Sy(y); } catch (ParseException e) { e.printStackTrace(); } 复制代码
分析原因
老规矩,从源码入手
Calendar类 ------------------------- //@since 1.7 public int getWeekYear() { throw new UnsupportedOperationException(); } 复制代码
这个源码有点奇怪,getWeekYear()方法是java 7引入的。它的实现怎么是抛出异常,但是执行时,又有结果返回。
断点跟进,通过Calendar.getInstance()获取的Calendar实例是GregorianCalendar
GregorianCalendar -------------------------------------- public int getWeekYear() { int year = get(YEAR); // implicitly calls complete() if (internalGetEra() == BCE) { year = 1 - year; } // Fast path for the Gregorian calendar years that are never // affected by the Julian-Gregorian transition if (year > gregorianCutoverYear + 1) { int weekOfYear = internalGet(WEEK_OF_YEAR); if (internalGet(MONTH) == JANUARY) { if (weekOfYear >= 52) { --year; } } else { if (weekOfYear == 1) { ++year; } } return year; } ... } 复制代码
方法内获取的年份刚开始是正常的
在JDK中会把前一年末尾的几天判定为下一年的第一周,因此上面程序的结果是1
解决方案
使用Calendar类 ge)获取年份
问题
1、读取月份时,要+1
2、返回的星期是从周日开始计算,周日为1,1~7表示星期
3、Calendar的跨年问题,获取年份要用c.ge),不要用c.getWeekYear();
4、获取指定时间是一年中的第几周时,调用cl.ge),要注意跨年问题,跨年的那一周,获取的值为1。离跨年最近的那周为52。
三:LocalDateTime
支持版本及以上
jdk8
介绍
LocalDateTime类说明
表示当前日期时间,相当于:yyyy-MM-ddTHH:mm:ss
LocalDateTime常用的用法
获取当前日期和时间
LocalDate d = LocalDa(); // 当前日期 LocalTime t = LocalTime.now(); // 当前时间 LocalDateTime dt = LocalDa(); // 当前日期和时间 Sy(d); // 严格按照ISO 8601格式打印 Sy(t); // 严格按照ISO 8601格式打印 Sy(dt); // 严格按照ISO 8601格式打印 复制代码
由运行结果可行,本地日期时间通过now()获取到的总是以当前默认时区返回的
获取指定日期和时间
LocalDate d2 = LocalDa(2021, 07, 14); // 2021-07-14, 注意07=07月 LocalTime t2 = LocalTime.of(13, 14, 20); // 13:14:20 LocalDateTime dt2 = LocalDa(2021, 07, 14, 13, 14, 20); LocalDateTime dt3 = LocalDa(d2, t2); Sy("指定日期时间:"+dt2); Sy("指定日期时间:"+dt3); 复制代码
日期时间的加减法及修改
LocalDateTime currentTime = LocalDa(); // 当前日期和时间 Sy("------------------时间的加减法及修改-----------------------"); 的加减法包含了LocalDate和LocalTime的所有加减,上面说过,这里就只做简单介绍 Sy("3.当前时间:" + currentTime); Sy("3.当前时间加5年:" + curren(5)); Sy("3.当前时间加2个月:" + curren(2)); Sy("3.当前时间减2天:" + curren(2)); Sy("3.当前时间减5个小时:" + curren(5)); Sy("3.当前时间加5分钟:" + curren(5)); Sy("3.当前时间加20秒:" + curren(20)); //还可以灵活运用比如:向后加一年,向前减一天,向后加2个小时,向前减5分钟,可以进行连写 Sy("3.同时修改(向后加一年,向前减一天,向后加2个小时,向前减5分钟):" + curren(1).minusDays(1).plusHours(2).minusMinutes(5)); Sy("3.修改年为2025年:" + curren(2025)); Sy("3.修改月为12月:" + curren(12)); Sy("3.修改日为27日:" + curren(27)); Sy("3.修改小时为12:" + curren(12)); Sy("3.修改分钟为12:" + curren(12)); Sy("3.修改秒为12:" + curren(12)); 复制代码
LocalDateTime和Date相互转化
Date转LocalDateTime
Sy("------------------方法一:分步写-----------------------"); //实例化一个时间对象 Date date = new Date(); //返回表示时间轴上同一点的瞬间作为日期对象 Instant instant = da(); //获取系统默认时区 ZoneId zoneId = ZoneId.systemDefault(); //根据时区获取带时区的日期和时间 ZonedDateTime zonedDateTime = in(zoneId); //转化为LocalDateTime LocalDateTime localDateTime = zonedDa(); Sy("方法一:原Date = " + date); Sy("方法一:转化后的LocalDateTime = " + localDateTime); Sy("------------------方法二:一步到位(推荐使用)-----------------------"); //实例化一个时间对象 Date todayDate = new Date(); (long l)使用1970-01-01T00:00:00Z的纪元中的毫秒来获取Instant的实例 LocalDateTime ldt = In(todayDa()).atZone()).toLocalDateTime(); Sy("方法二:原Date = " + todayDate); Sy("方法二:转化后的LocalDateTime = " + ldt); 复制代码
LocalDateTime转Date
Sy("------------------方法一:分步写-----------------------"); //获取LocalDateTime对象,当前时间 LocalDateTime localDateTime = LocalDa(); //获取系统默认时区 ZoneId zoneId = ZoneId.systemDefault(); //根据时区获取带时区的日期和时间 ZonedDateTime zonedDateTime = localDa(zoneId); //返回表示时间轴上同一点的瞬间作为日期对象 Instant instant = zonedDa(); //转化为Date Date date = Da(instant); Sy("方法一:原LocalDateTime = " + localDateTime); Sy("方法一:转化后的Date = " + date); Sy("------------------方法二:一步到位(推荐使用)-----------------------"); //实例化一个LocalDateTime对象 LocalDateTime now = LocalDa(); //转化为date Date dateResult = Da()).toInstant()); Sy("方法二:原LocalDateTime = " + now); Sy("方法二:转化后的Date = " + dateResult); 复制代码
线程安全
网上大家都在说JAVA 8提供的LocalDateTime是线程安全的,但是它是如何实现的呢
今天让我们来挖一挖
public final class LocalDateTime implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable { ... } 复制代码
由上面的源码可知,LocalDateTime是不可变类。我们都知道一个Java并发编程规则:不可变对象永远是线程安全的。
对比下Date的源码 ,Date是可变类,所以是线程不安全的。
public class Date implements java.io.Serializable, Cloneable, Comparable<Date> { ... } 复制代码
四:ZonedDateTime
支持版本及以上
jdk8
介绍
ZonedDateTime类说明
表示一个带时区的日期和时间,ZonedDateTime可以理解为LocalDateTime+ZoneId
从源码可以看出来,ZonedDateTime类中定义了LocalDateTime和ZoneId两个变量。
且ZonedDateTime类也是不可变类且是线程安全的。
public final class ZonedDateTime implements Temporal, ChronoZonedDateTime<LocalDate>, Serializable { /** * Serialization version. */ private static final long serialVersionUID = -6260982410461394882L; /** * The local date-time. */ private final LocalDateTime dateTime; /** * The time-zone. */ private final ZoneId zone; ... } 复制代码
ZonedDateTime常用的用法
获取当前时间+带时区+时区转换
// 默认时区获取当前时间 ZonedDateTime zonedDateTime = ZonedDa(); // 用指定时区获取当前时间,Asia/Shanghai为上海时区 ZonedDateTime zonedDateTime1 = ZonedDa("Asia/Shanghai")); //withZoneSameInstant为转换时区,参数为ZoneId ZonedDateTime zonedDateTime2 = zonedDa("America/New_York")); Sy(zonedDateTime); Sy(zonedDateTime1); Sy(zonedDateTime2); 复制代码
LocalDateTime+ZoneId变ZonedDateTime
LocalDateTime localDateTime = LocalDa(); ZonedDateTime zonedDateTime1 = localDa()); ZonedDateTime zonedDateTime2 = localDa("America/New_York")); Sy(zonedDateTime1); Sy(zonedDateTime2); 复制代码
上面的例子说明了,LocalDateTime是可以转成ZonedDateTime的。
五:DateTimeFormatter
支持版本及以上
jdk8
介绍
DateTimeFormatter类说明
DateTimeFormatter的作用是进行格式化显示,且DateTimeFormatter是不可变类且是线程安全的。
public final class DateTimeFormatter { ... } 复制代码
说到时间的格式化显示,就要说老朋友SimpleDateFormat了,之前格式化Date就要用上。但是我们知道SimpleDateFormat是线程不安全的,还不清楚的,请看这里-->
DateTimeFormatter常用的用法
ZonedDateTime zonedDateTime = ZonedDa(); DateTimeFormatter formatter = Da("yyyy-MM-dd'T'HH:mm ZZZZ"); Sy(zonedDateTime)); DateTimeFormatter usFormatter = Da("E, MMMM/dd/yyyy HH:mm", Locale.US); Sy(zonedDateTime)); DateTimeFormatter chinaFormatter = Da("yyyy MMM dd EE HH:mm", Locale.CHINA); Sy(zonedDateTime)); 复制代码
DateTimeFormatter的坑
1、在正常配置按照标准格式的字符串日期,是能够正常转换的。如果月,日,时,分,秒在不足两位的情况需要补0,否则的话会转换失败,抛出异常。
DateTimeFormatter DATE_TIME_FORMATTER = Da("yyyy-MM-dd HH:mm:ss.SSS"); LocalDateTime dt1 = LocalDa("2021-7-20 23:46:43.946", DATE_TIME_FORMATTER); Sy(dt1); 复制代码
会报错:
java.: Text '2021-7-20 23:46:43.946' could not be parsed at index 5 复制代码
分析原因:是格式字符串与实际的时间不匹配
"yyyy-MM-dd HH:mm:ss.SSS"
"2021-7-20 23:46:43.946"
中间的月份格式是MM,实际时间是7
解决方案:保持格式字符串与实际的时间匹配
DateTimeFormatter DATE_TIME_FORMATTER = Da("yyyy-MM-dd HH:mm:ss.SSS"); LocalDateTime dt1 = LocalDa("2021-07-20 23:46:43.946", DATE_TIME_FORMATTER); Sy(dt1); 复制代码
2、YYYY和DD谨慎使用
LocalDate date = LocalDa(2020,12,31); DateTimeFormatter formatter = Da("YYYYMM"); // 结果是 202112 Sy( (date)); 复制代码
Java’s DateTimeFormatter pattern “YYYY” gives you the week-based-year, (by default, ISO-8601 standard) the year of the Thursday of that week. 复制代码
YYYY是取的当前周所在的年份,week-based year 是 ISO 8601 规定的。2020年12月31号,周算年份,就是2021年
private static void tryit(int Y, int M, int D, String pat) { DateTimeFormatter fmt = Da(pat); LocalDate dat = LocalDa(Y,M,D); String str = (dat); Sy("Y=%04d M=%02d D=%02d " + "formatted with " + "\"%s\" -> %s\n",Y,M,D,pat,str); } public static void main(String[] args){ tryit(2020,01,20,"MM/DD/YYYY"); tryit(2020,01,21,"DD/MM/YYYY"); tryit(2020,01,22,"YYYY-MM-DD"); tryit(2020,03,17,"MM/DD/YYYY"); tryit(2020,03,18,"DD/MM/YYYY"); tryit(2020,03,19,"YYYY-MM-DD"); } 复制代码
Y=2020 M=01 D=20 formatted with "MM/DD/YYYY" -> 01/20/2020 Y=2020 M=01 D=21 formatted with "DD/MM/YYYY" -> 21/01/2020 Y=2020 M=01 D=22 formatted with "YYYY-MM-DD" -> 2020-01-22 Y=2020 M=03 D=17 formatted with "MM/DD/YYYY" -> 03/77/2020 Y=2020 M=03 D=18 formatted with "DD/MM/YYYY" -> 78/03/2020 Y=2020 M=03 D=19 formatted with "YYYY-MM-DD" -> 2020-03-79 复制代码
最后三个日期是有问题的,因为大写的DD代表的是处于这一年中那一天,不是处于这个月的那一天,但是dd就没有问题。
例子参考于:www.cnblogs.com/tonyY/p/121…
所以建议使用yyyy和dd。
六:Instant
支持版本及以上
jdk8
介绍
Instant类说明
public final class Instant implements Temporal, TemporalAdjuster, Comparable<Instant>, Serializable { ... } 复制代码
Instant也是不可变类且是线程安全的。其实Java.time 这个包是线程安全的。
Instant是java 8新增的特性,里面有两个核心的字段
... private final long seconds; private final int nanos; ... 复制代码
一个是单位为秒的时间戳,另一个是单位为纳秒的时间戳。
是不是跟**Sy()**返回的long时间戳很像,Sy()返回的是毫秒级,Instant多了更精确的纳秒级时间戳。
Instant常用的用法
Instant now = In(); Sy("now:"+now); Sy()); // 秒 Sy()); // 毫秒 复制代码
Instant是没有时区的,但是Instant加上时区后,可以转化为ZonedDateTime
Instant ins = In(); ZonedDateTime zdt = ins.atZone()); Sy(zdt); 复制代码
long型时间戳转Instant
要注意long型时间戳的时间单位选择Instant对应的方法转化
//1626796436 为秒级时间戳 Instant ins = In(1626796436); ZonedDateTime zdt = ins.atZone()); Sy("秒级时间戳转化:"+zdt); //1626796436111l 为秒级时间戳 Instant ins1 = In(1626796436111l); ZonedDateTime zdt1 = in()); Sy("毫秒级时间戳转化:"+zdt1); 复制代码
Instant的坑
In()获取的时间与北京时间相差8个时区,这是一个细节,要避坑。
看源码,用的是UTC时间。
public static Instant now() { return Clock.systemUTC().instant(); } 复制代码
解决方案:
Instant now = In().plusMilli(8)); Sy("now:"+now); 复制代码
链接: