万字博文教你搞懂java源码的日期和时间相关用法

x33g5p2x  于2021-12-18 转载在 其他  
字(24.5k)|赞(0)|评价(0)|浏览(324)

介绍

本篇文章主要介绍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的源码,先看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+支持)
DateTimeFormatter介绍

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的。

支持版本及以上

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);

相关文章

目录