技术活,该赏
关注+一键三连(点赞,评论,收藏)再看,养成好习惯
文章目录
SimpleDateFormat线程不安全原因及解决方案
五:DateTimeFormatter
介绍
本篇文章主要介绍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.util.Date 是java提供表示日期和时间的类,类里有个long 类型的变量fastTime,它是用来存储以毫秒表示的时间戳。
date常用的用法
import java. util. Date ;
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
//获取当前时间
Date date = new Date ( ) ;
System . out. println ( "获取当前时间:" + date) ;
//获取时间戳
System . out. println ( "获取时间戳:" + date. getTime ( ) ) ;
// date时间是否大于afterDate 等于也为false
Date afterDate = new Date ( date. getTime ( ) - 3600 * 24 * 1000 ) ;
System . out. println ( "after:" + date. after ( afterDate) ) ;
System . out. println ( "after:" + date. after ( date) ) ;
// date时间是否小于afterDate 等于也为false
Date beforeDate = new Date ( date. getTime ( ) + 3600 * 24 * 1000 ) ;
System . out. println ( "before:" + date. before ( beforeDate) ) ;
System . out. println ( "before:" + date. before ( date) ) ;
//两个日期比较
System . out. println ( "compareTo:" + date. compareTo ( date) ) ;
System . out. println ( "compareTo:" + date. compareTo ( afterDate) ) ;
System . out. println ( "compareTo:" + date. compareTo ( beforeDate) ) ;
//转为字符串
System . out. println ( "转为字符串:" + date. toString ( ) ) ;
//转为GMT时区 toGMTString() java8 中已废弃
System . out. println ( "转为GMT时区:" + date. toGMTString ( ) ) ;
//转为本地时区 toLocaleString() java8 已废弃
System . out. println ( "转为本地时区:" + date. toLocaleString ( ) ) ;
自定义时间格式-SimpleDateFormat
date的toString方法转成字符串,不是我们想要的时间格式,如果要自定义时间格式,就要使用SimpleDateFormat
//获取当前时间
Date date = new Date ( ) ;
System . out. println ( "获取当前时间:" + date) ;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat ( "yyyy-MM-dd HH:mm:ss" ) ;
System . out. println ( simpleDateFormat. format ( date) ) ;
SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat ( "yyyy年MM月dd日 HH时mm分ss秒" ) ;
System . out. println ( simpleDateFormat1. format ( date) ) ;
SimpleDateFormat也可以方便的将字符串转成Date
//获取当前时间
String str = "2021-07-13 23:48:23" ;
try {
Date date = new SimpleDateFormat ( "yyyy-MM-dd HH:mm:ss" ) . parse ( str) ;
System . out. println ( 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 ( parsedDate. before ( 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) {
pos. errorIndex = start;
pos. index = 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 = Executors . newFixedThreadPool ( 5 ) ;
//2、为线程池分配任务
ThreadPoolTest threadPoolTest = new ThreadPoolTest ( ) ;
for ( int i = 0 ; i < 10 ; i++ ) {
pool. submit ( threadPoolTest) ;
}
//3、关闭线程池
pool. shutdown ( ) ;
}
static class ThreadPoolTest implements Runnable {
@Override
public void run ( ) {
String dateString = simpleDateFormat. format ( new Date ( ) ) ;
try {
Date parseDate = simpleDateFormat. parse ( dateString) ;
String dateString2 = simpleDateFormat. format ( parseDate) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 线程是否安全: " + dateString. equals ( dateString2) ) ;
} catch ( Exception e) {
System . out. println ( Thread . currentThread ( ) . 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 = Executors . newFixedThreadPool ( 5 ) ;
//2、为线程池分配任务
ThreadPoolTest threadPoolTest = new ThreadPoolTest ( ) ;
for ( int i = 0 ; i < 10 ; i++ ) {
pool. submit ( threadPoolTest) ;
}
//3、关闭线程池
pool. shutdown ( ) ;
}
static class ThreadPoolTest implements Runnable {
@Override
public void run ( ) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat ( "yyyy-MM-dd HH:mm:ss" ) ;
String dateString = simpleDateFormat. format ( new Date ( ) ) ;
try {
Date parseDate = simpleDateFormat. parse ( dateString) ;
String dateString2 = simpleDateFormat. format ( parseDate) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 线程是否安全: " + dateString. equals ( dateString2) ) ;
} catch ( Exception e) {
System . out. println ( Thread . currentThread ( ) . 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 = Executors . newFixedThreadPool ( 5 ) ;
//2、为线程池分配任务
ThreadPoolTest threadPoolTest = new ThreadPoolTest ( ) ;
for ( int i = 0 ; i < 10 ; i++ ) {
pool. submit ( threadPoolTest) ;
}
//3、关闭线程池
pool. shutdown ( ) ;
}
static class ThreadPoolTest implements Runnable {
@Override
public void run ( ) {
try {
synchronized ( simpleDateFormat) {
String dateString = simpleDateFormat. format ( new Date ( ) ) ;
Date parseDate = simpleDateFormat. parse ( dateString) ;
String dateString2 = simpleDateFormat. format ( parseDate) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 线程是否安全: " + dateString. equals ( dateString2) ) ;
}
} catch ( Exception e) {
System . out. println ( Thread . currentThread ( ) . 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 = Executors . newFixedThreadPool ( 5 ) ;
//2、为线程池分配任务
ThreadPoolTest threadPoolTest = new ThreadPoolTest ( ) ;
for ( int i = 0 ; i < 10 ; i++ ) {
pool. submit ( threadPoolTest) ;
}
//3、关闭线程池
pool. shutdown ( ) ;
}
static class ThreadPoolTest implements Runnable {
@Override
public void run ( ) {
try {
lock. lock ( ) ;
String dateString = simpleDateFormat. format ( new Date ( ) ) ;
Date parseDate = simpleDateFormat. parse ( dateString) ;
String dateString2 = simpleDateFormat. format ( parseDate) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 线程是否安全: " + dateString. equals ( dateString2) ) ;
} catch ( Exception e) {
System . out. println ( Thread . currentThread ( ) . 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 = Executors . newFixedThreadPool ( 5 ) ;
//2、为线程池分配任务
ThreadPoolTest threadPoolTest = new ThreadPoolTest ( ) ;
for ( int i = 0 ; i < 10 ; i++ ) {
pool. submit ( threadPoolTest) ;
}
//3、关闭线程池
pool. shutdown ( ) ;
}
static class ThreadPoolTest implements Runnable {
@Override
public void run ( ) {
try {
String dateString = threadLocal. get ( ) . format ( new Date ( ) ) ;
Date parseDate = threadLocal. get ( ) . parse ( dateString) ;
String dateString2 = threadLocal. get ( ) . format ( parseDate) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 线程是否安全: " + dateString. equals ( dateString2) ) ;
} catch ( Exception e) {
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 格式化失败 " ) ;
} finally {
//避免内存泄漏,使用完threadLocal后要调用remove方法清除数据
threadLocal. remove ( ) ;
}
}
}
}
使用ThreadLocal能保证线程安全,且效率也是挺高的。适合高并发场景使用 。
解决方案4:使用DateTimeFormatter代替SimpleDateFormat
使用DateTimeFormatter代替SimpleDateFormat(DateTimeFormatter是线程安全的,java 8+支持)
public class DateTimeFormatterDemoTest5 {
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter . ofPattern ( "yyyy-MM-dd HH:mm:ss" ) ;
public static void main ( String [ ] args) {
//1、创建线程池
ExecutorService pool = Executors . newFixedThreadPool ( 5 ) ;
//2、为线程池分配任务
ThreadPoolTest threadPoolTest = new ThreadPoolTest ( ) ;
for ( int i = 0 ; i < 10 ; i++ ) {
pool. submit ( threadPoolTest) ;
}
//3、关闭线程池
pool. shutdown ( ) ;
}
static class ThreadPoolTest implements Runnable {
@Override
public void run ( ) {
try {
String dateString = dateTimeFormatter. format ( LocalDateTime . now ( ) ) ;
TemporalAccessor temporalAccessor = dateTimeFormatter. parse ( dateString) ;
String dateString2 = dateTimeFormatter. format ( temporalAccessor) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 线程是否安全: " + dateString. equals ( dateString2) ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 格式化失败 " ) ;
}
}
}
}
使用DateTimeFormatter能保证线程安全,且效率也是挺高的。适合高并发场景使用 。
解决方案5:使用FastDateFormat 替换SimpleDateFormat
使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,不受限于java版本)
public class FastDateFormatDemo6 {
private static FastDateFormat fastDateFormat = FastDateFormat . getInstance ( "yyyy-MM-dd HH:mm:ss" ) ;
public static void main ( String [ ] args) {
//1、创建线程池
ExecutorService pool = Executors . newFixedThreadPool ( 5 ) ;
//2、为线程池分配任务
ThreadPoolTest threadPoolTest = new ThreadPoolTest ( ) ;
for ( int i = 0 ; i < 10 ; i++ ) {
pool. submit ( threadPoolTest) ;
}
//3、关闭线程池
pool. shutdown ( ) ;
}
static class ThreadPoolTest implements Runnable {
@Override
public void run ( ) {
try {
String dateString = fastDateFormat. format ( new Date ( ) ) ;
Date parseDate = fastDateFormat. parse ( dateString) ;
String dateString2 = fastDateFormat. format ( parseDate) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 线程是否安全: " + dateString. equals ( dateString2) ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
System . out. println ( Thread . currentThread ( ) . getName ( ) + " 格式化失败 " ) ;
}
}
}
}
使用FastDateFormat能保证线程安全,且效率也是挺高的。适合高并发场景使用 。
FastDateFormat源码分析
Apache Commons Lang 3.5
//FastDateFormat
@Override
public String format ( final Date date) {
return printer. format ( 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是怎么获取的
FastDateFormat . getInstance ( ) ;
FastDateFormat . getInstance ( CHINESE_DATE_TIME_PATTERN) ;
看下对应的源码
/**
* 获得 FastDateFormat实例,使用默认格式和地区
*
* @return FastDateFormat
*/
public static FastDateFormat getInstance ( ) {
return CACHE. getInstance ( ) ;
}
/**
* 获得 FastDateFormat 实例,使用默认地区<br>
* 支持缓存
*
* @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式
* @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 = FastDateFormat . getInstance ( 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. get ( Calendar . YEAR) ;
int m = 1 + c. get ( Calendar . MONTH) ;
int d = c. get ( Calendar . DAY_OF_MONTH) ;
int w = c. get ( Calendar . DAY_OF_WEEK) ;
int hh = c. get ( Calendar . HOUR_OF_DAY) ;
int mm = c. get ( Calendar . MINUTE) ;
int ss = c. get ( Calendar . SECOND) ;
int ms = c. get ( Calendar . MILLISECOND) ;
System . out. println ( "返回的星期:" + w) ;
System . out. println ( 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();
System.out.println(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类 get(Calendar.YEAR)获取年份
问题
1、读取月份时,要+1
2、返回的星期是从周日开始计算,周日为1,1~7表示星期
3、Calendar的跨年问题,获取年份要用c.get(Calendar.YEAR),不要用c.getWeekYear();
4、获取指定时间是一年中的第几周时,调用cl.get(Calendar.WEEK_OF_YEAR),要注意跨年问题,跨年的那一周,获取的值为1。离跨年最近的那周为52。
三:LocalDateTime
支持版本及以上
jdk8
介绍
LocalDateTime类说明
表示当前日期时间,相当于:yyyy-MM-ddTHH:mm:ss
LocalDateTime常用的用法
获取当前日期和时间
LocalDate d = LocalDate . now ( ) ; // 当前日期
LocalTime t = LocalTime . now ( ) ; // 当前时间
LocalDateTime dt = LocalDateTime . now ( ) ; // 当前日期和时间
System . out. println ( d) ; // 严格按照ISO 8601格式打印
System . out. println ( t) ; // 严格按照ISO 8601格式打印
System . out. println ( dt) ; // 严格按照ISO 8601格式打印
由运行结果可行,本地日期时间通过now()获取到的总是以当前默认时区返回的
获取指定日期和时间
LocalDate d2 = LocalDate . of ( 2021 , 07 , 14 ) ; // 2021-07-14, 注意07=07月
LocalTime t2 = LocalTime . of ( 13 , 14 , 20 ) ; // 13:14:20
LocalDateTime dt2 = LocalDateTime . of ( 2021 , 07 , 14 , 13 , 14 , 20 ) ;
LocalDateTime dt3 = LocalDateTime . of ( d2, t2) ;
System . out. println ( "指定日期时间:" + dt2) ;
System . out. println ( "指定日期时间:" + dt3) ;
日期时间的加减法及修改
LocalDateTime currentTime = LocalDateTime . now ( ) ; // 当前日期和时间
System . out. println ( "------------------时间的加减法及修改-----------------------" ) ;
//3.LocalDateTime的加减法包含了LocalDate和LocalTime的所有加减,上面说过,这里就只做简单介绍
System . out. println ( "3.当前时间:" + currentTime) ;
System . out. println ( "3.当前时间加5年:" + currentTime. plusYears ( 5 ) ) ;
System . out. println ( "3.当前时间加2个月:" + currentTime. plusMonths ( 2 ) ) ;
System . out. println ( "3.当前时间减2天:" + currentTime. minusDays ( 2 ) ) ;
System . out. println ( "3.当前时间减5个小时:" + currentTime. minusHours ( 5 ) ) ;
System . out. println ( "3.当前时间加5分钟:" + currentTime. plusMinutes ( 5 ) ) ;
System . out. println ( "3.当前时间加20秒:" + currentTime. plusSeconds ( 20 ) ) ;
//还可以灵活运用比如:向后加一年,向前减一天,向后加2个小时,向前减5分钟,可以进行连写
System . out. println ( "3.同时修改(向后加一年,向前减一天,向后加2个小时,向前减5分钟):" + currentTime. plusYears ( 1 ) . minusDays ( 1 ) . plusHours ( 2 ) . minusMinutes ( 5 ) ) ;
System . out. println ( "3.修改年为2025年:" + currentTime. withYear ( 2025 ) ) ;
System . out. println ( "3.修改月为12月:" + currentTime. withMonth ( 12 ) ) ;
System . out. println ( "3.修改日为27日:" + currentTime. withDayOfMonth ( 27 ) ) ;
System . out. println ( "3.修改小时为12:" + currentTime. withHour ( 12 ) ) ;
System . out. println ( "3.修改分钟为12:" + currentTime. withMinute ( 12 ) ) ;
System . out. println ( "3.修改秒为12:" + currentTime. withSecond ( 12 ) ) ;
LocalDateTime和Date相互转化
Date转LocalDateTime
System . out. println ( "------------------方法一:分步写-----------------------" ) ;
//实例化一个时间对象
Date date = new Date ( ) ;
//返回表示时间轴上同一点的瞬间作为日期对象
Instant instant = date. toInstant ( ) ;
//获取系统默认时区
ZoneId zoneId = ZoneId . systemDefault ( ) ;
//根据时区获取带时区的日期和时间
ZonedDateTime zonedDateTime = instant. atZone ( zoneId) ;
//转化为LocalDateTime
LocalDateTime localDateTime = zonedDateTime. toLocalDateTime ( ) ;
System . out. println ( "方法一:原Date = " + date) ;
System . out. println ( "方法一:转化后的LocalDateTime = " + localDateTime) ;
System . out. println ( "------------------方法二:一步到位(推荐使用)-----------------------" ) ;
//实例化一个时间对象
Date todayDate = new Date ( ) ;
//Instant.ofEpochMilli(long l)使用1970-01-01T00:00:00Z的纪元中的毫秒来获取Instant的实例
LocalDateTime ldt = Instant . ofEpochMilli ( todayDate. getTime ( ) ) . atZone ( ZoneId . systemDefault ( ) ) . toLocalDateTime ( ) ;
System . out. println ( "方法二:原Date = " + todayDate) ;
System . out. println ( "方法二:转化后的LocalDateTime = " + ldt) ;
LocalDateTime转Date
System . out. println ( "------------------方法一:分步写-----------------------" ) ;
//获取LocalDateTime对象,当前时间
LocalDateTime localDateTime = LocalDateTime . now ( ) ;
//获取系统默认时区
ZoneId zoneId = ZoneId . systemDefault ( ) ;
//根据时区获取带时区的日期和时间
ZonedDateTime zonedDateTime = localDateTime. atZone ( zoneId) ;
//返回表示时间轴上同一点的瞬间作为日期对象
Instant instant = zonedDateTime. toInstant ( ) ;
//转化为Date
Date date = Date . from ( instant) ;
System . out. println ( "方法一:原LocalDateTime = " + localDateTime) ;
System . out. println ( "方法一:转化后的Date = " + date) ;
System . out. println ( "------------------方法二:一步到位(推荐使用)-----------------------" ) ;
//实例化一个LocalDateTime对象
LocalDateTime now = LocalDateTime . now ( ) ;
//转化为date
Date dateResult = Date . from ( now. atZone ( ZoneId . systemDefault ( ) ) . toInstant ( ) ) ;
System . out. println ( "方法二:原LocalDateTime = " + now) ;
System . out. println ( "方法二:转化后的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 = ZonedDateTime . now ( ) ;
// 用指定时区获取当前时间,Asia/Shanghai为上海时区
ZonedDateTime zonedDateTime1 = ZonedDateTime . now ( ZoneId . of ( "Asia/Shanghai" ) ) ;
//withZoneSameInstant为转换时区,参数为ZoneId
ZonedDateTime zonedDateTime2 = zonedDateTime. withZoneSameInstant ( ZoneId . of ( "America/New_York" ) ) ;
System . out. println ( zonedDateTime) ;
System . out. println ( zonedDateTime1) ;
System . out. println ( zonedDateTime2) ;
LocalDateTime+ZoneId变ZonedDateTime
LocalDateTime localDateTime = LocalDateTime . now ( ) ;
ZonedDateTime zonedDateTime1 = localDateTime. atZone ( ZoneId . systemDefault ( ) ) ;
ZonedDateTime zonedDateTime2 = localDateTime. atZone ( ZoneId . of ( "America/New_York" ) ) ;
System . out. println ( zonedDateTime1) ;
System . out. println ( zonedDateTime2) ;
上面的例子说明了,LocalDateTime是可以转成ZonedDateTime的。
五:DateTimeFormatter
支持版本及以上
jdk8
介绍
DateTimeFormatter类说明
DateTimeFormatter的作用是进行格式化显示,且DateTimeFormatter是不可变类且是线程安全的。
public final class DateTimeFormatter {
. . .
}
说到时间的格式化显示,就要说老朋友SimpleDateFormat了,之前格式化Date就要用上。但是我们知道SimpleDateFormat是线程不安全的,还不清楚的,
DateTimeFormatter常用的用法
ZonedDateTime zonedDateTime = ZonedDateTime . now ( ) ;
DateTimeFormatter formatter = DateTimeFormatter . ofPattern ( "yyyy-MM-dd'T'HH:mm ZZZZ" ) ;
System . out. println ( formatter. format ( zonedDateTime) ) ;
DateTimeFormatter usFormatter = DateTimeFormatter . ofPattern ( "E, MMMM/dd/yyyy HH:mm" , Locale . US) ;
System . out. println ( usFormatter. format ( zonedDateTime) ) ;
DateTimeFormatter chinaFormatter = DateTimeFormatter . ofPattern ( "yyyy MMM dd EE HH:mm" , Locale . CHINA) ;
System . out. println ( chinaFormatter. format ( zonedDateTime) ) ;
DateTimeFormatter的坑
1、在正常配置按照标准格式的字符串日期,是能够正常转换的。如果月,日,时,分,秒在不足两位的情况需要补0,否则的话会转换失败,抛出异常。
DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter . ofPattern ( "yyyy-MM-dd HH:mm:ss.SSS" ) ;
LocalDateTime dt1 = LocalDateTime . parse ( "2021-7-20 23:46:43.946" , DATE_TIME_FORMATTER) ;
System . out. println ( dt1) ;
会报错:
java. time. format. DateTimeParseException: 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 = DateTimeFormatter . ofPattern ( "yyyy-MM-dd HH:mm:ss.SSS" ) ;
LocalDateTime dt1 = LocalDateTime . parse ( "2021-07-20 23:46:43.946" , DATE_TIME_FORMATTER) ;
System . out. println ( dt1) ;
2、YYYY和DD谨慎使用
LocalDate date = LocalDate . of ( 2020 , 12 , 31 ) ;
DateTimeFormatter formatter = DateTimeFormatter . ofPattern ( "YYYYMM" ) ;
// 结果是 202112
System . out. println ( formatter. format ( 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 = DateTimeFormatter . ofPattern ( pat) ;
LocalDate dat = LocalDate . of ( Y , M , D ) ;
String str = fmt. format ( dat) ;
System . out. printf ( "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 就没有问题。
例子参考于:https://www.cnblogs.com/tonyY/p/12153335.html
所以建议使用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;
. . .
一个是单位为秒的时间戳,另一个是单位为纳秒的时间戳。
是不是跟**System.currentTimeMillis()**返回的long时间戳很像,System.currentTimeMillis()返回的是毫秒级,Instant多了更精确的纳秒级时间戳。
Instant常用的用法
Instant now = Instant . now ( ) ;
System . out. println ( "now:" + now) ;
System . out. println ( now. getEpochSecond ( ) ) ; // 秒
System . out. println ( now. toEpochMilli ( ) ) ; // 毫秒
Instant是没有时区的,但是Instant加上时区后,可以转化为ZonedDateTime
Instant ins = Instant . now ( ) ;
ZonedDateTime zdt = ins. atZone ( ZoneId . systemDefault ( ) ) ;
System . out. println ( zdt) ;
long型时间戳转Instant
要注意long型时间戳的时间单位选择Instant对应的方法转化
//1626796436 为秒级时间戳
Instant ins = Instant . ofEpochSecond ( 1626796436 ) ;
ZonedDateTime zdt = ins. atZone ( ZoneId . systemDefault ( ) ) ;
System . out. println ( "秒级时间戳转化:" + zdt) ;
//1626796436111l 为秒级时间戳
Instant ins1 = Instant . ofEpochMilli ( 1626796436111l ) ;
ZonedDateTime zdt1 = ins1. atZone ( ZoneId . systemDefault ( ) ) ;
System . out. println ( "毫秒级时间戳转化:" + zdt1) ;
Instant的坑
Instant.now()获取的时间与北京时间相差8个时区,这是一个细节,要避坑。
看源码,用的是UTC时间。
public static Instant now ( ) {
return Clock . systemUTC ( ) . instant ( ) ;
}
解决方案:
Instant now = Instant . now ( ) . plusMillis ( TimeUnit . HOURS. toMillis ( 8 ) ) ;
System . out. println ( "now:" + now) ;
推荐相关文章
hutool日期时间系列文章
1DateUtil(时间工具类)-当前时间和当前时间戳
2DateUtil(时间工具类)-常用的时间类型Date,DateTime,Calendar和TemporalAccessor(LocalDateTime)转换
3DateUtil(时间工具类)-获取日期的各种内容
4DateUtil(时间工具类)-格式化时间
5DateUtil(时间工具类)-解析被格式化的时间
6DateUtil(时间工具类)-时间偏移量获取
7DateUtil(时间工具类)-日期计算
8ChineseDate(农历日期工具类)
9LocalDateTimeUtil(JDK8+中的{@link LocalDateTime} 工具类封装)
10TemporalAccessorUtil{@link TemporalAccessor} 工具类封装
其他
要探索JDK的核心底层源码,那必须掌握native用法