万字博文教你搞懂java源码的日期和时间相关用法
Posted 小虚竹
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了万字博文教你搞懂java源码的日期和时间相关用法相关的知识,希望对你有一定的参考价值。
技术活,该赏
关注+一键三连(点赞,评论,收藏)再看,养成好习惯
hutool日期时间系列文章
2DateUtil(时间工具类)-常用的时间类型Date,DateTime,Calendar和TemporalAccessor(LocalDateTime)转换
9LocalDateTimeUtil(JDK8+中的{@link LocalDateTime} 工具类封装)
10TemporalAccessorUtil{@link TemporalAccessor} 工具类封装
介绍
本篇文章主要介绍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的源码
// 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线程不安全
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{
private volatile int i=0;
@Override
public void run() {
while (i<10){
String dateString = simpleDateFormat.format(new Date());
try {
Date parseDate = simpleDateFormat.parse(dateString);
String dateString2 = simpleDateFormat.format(parseDate);
System.out.println(Thread.currentThread().getName()+" : "+i++);
System.out.println(dateString.equals(dateString2));
System.out.println("-------------------------");
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}
}
出现了两次false,说明线程是不安全的。
解决方案
这个是阿里巴巴 java开发手册中的规定:
1、不要定义为static变量
2、加锁:synchronized(lock)
3、使用DateTimeFormatter代替SimpleDateFormat(DateTimeFormatter是线程安全的,java 8+支持)
问题
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)3hutool源码分析:DateUtil(时间工具类)-获取日期的各种内容
3hutool源码分析:DateUtil(时间工具类)-获取日期的各种内容
4hutool源码分析:DateUtil(时间工具类)-格式化时间(万字长文源码分析,学大佬如何写代码)
4hutool源码分析:DateUtil(时间工具类)-格式化时间(万字长文源码分析,学大佬如何写代码)