前言
计算机中日期时间是一个很大的概念,现有的系统基本都是利用从1970.1.1 00:00:00 到当前时间的毫秒数进行计时,这个1970.1.1 00:00:00 UTC称为epoch Time,也就是所谓的“纪元时”。JDK中关于日期时间的api的修复可谓是一段漫长的填坑之旅,从最早惨不忍睹的Date,到勉强能用的Calendar,再到1.8发布的java.time包中的全新日期时间API,可以明显感到api对开发人员的友好度大幅增强,本文意在记录上述三个日期时间标志性API,仅用于个人学习,如有认识不足之处,请各位见谅,欢迎给作者留言,共同进步。
java.util.Date(非线程安全)
Date作为我们最早接触到的关于日期时间的api,是JDK1.1之前的主流日期时间表达方式。
正如JDK API文档中的描述,Date允许将日期解释为年、月、日、小时、分钟和秒值,还可以格式化和解析日期字符串。不幸的是这些功能的API不适合国际化。 从JDK 1.1开始, Calendar类应该用于在日期和时间字段之间进行转换,并且DateFormat类应用于格式化和解析日期字符串。 在相应的方法Date被弃用。
Date内部有一个不可序列化的long值来存储距离纪元时的毫秒数。Date类为可变的,在多线程并发环境中会有线程安全问题。
public class Date
implements java.io.Serializable, Cloneable, Comparable{
private transient long fastTime;
Date(){
this(System.currentTimeMillis());
}
Date(long date){
fastTime = date;
}
//……
}
java.util.Calendar(非线程安全)
Calendar是一个可以操作日期和时间的抽象类,和Date一样,有一个表示从Epoch Time到当前瞬时偏移量的毫秒值。此外,Calendar还定义了一个count=17的数组,用于表示日历中的各个字段(如年份、月份、本年度的周数 etc.)。
proteted long time;
proteted int fields[];
简单来说Calendar是一个工具类,将瞬时时间的毫秒值自动转化为各个日历字段fileds。 并提供了日期、月份、天数相加减等多种方法。Calendar的子类为可变的,在多线程并发环境中会有线程安全问题。
Calendar还有一个很有意思的坑,简单记录一下,当系统时间为10月31日时,由于代码里面没有设定日期,所以此时calendar对象的日期是31,但是11月的最大天数为30天,所以导致当前calendar的时间向后顺延了一天,月份变为了12月份,代码最后获取到的当月最大天数就是31日。所以构建Calendar的开始时间时应该 设置具体开始日期。
public static void main(String[] args) {
int year = 2019;
int month = 10;
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR,year);
calendar.set(Calendar.MONTH,month);
calendar.getTime();
int date =calendar.getActualMaximum(Calendar.DATE);
System.out.println("date = " + date);
}
// 系统时间为2019年11月1号
date = 30
// 系统时间为2019年10月31号
date = 31
// 系统时间为2019年10月01号
date = 30
jdk1.8全新日期时间API(线程安全)
jdk1.8之前API存在的问题
- 有关时间日期的操作,会用到Date;
- 有关日期、时间的计算操作,会用到Calendar;
- 关于时间日期的格式化,会用到SimpleDateFormat或DateFormat下的其他子类;
但是上述对象都是可变的、线程不安全的,而且存在设计差,时区处理复杂等问题!
1.非线程安全 − Date 和 Calendar 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
2.设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
3.时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
包介绍
- java.time包:这是新的Java日期/时间API的基础包,所有的主要基础类都是这个包的一部分,如:LocalDate, LocalTime, LocalDateTime, Instant, Period, Duration等等。所有这些类都是不可变的和线程安全的,在绝大多数情况下,这些类能够有效地处理一些公共的需求。
- java.time.chrono包:这个包为非ISO的日历系统定义了一些泛化的API,我们可以扩展AbstractChronology类来创建自己的日历系统。
- java.time.format包:这个包包含能够格式化和解析日期时间对象的类,在绝大多数情况下,我们不应该直接使用它们,因为java.time包中相应的类已经提供了格式化和解析的方法。
- java.time.temporal包:这个包包含一些时态对象,我们可以用其找出关于日期/时间对象的某个特定日期或时间,比如说,可以找到某月的第一天或最后一天。你可以非常容易地认出这些方法,因为它们都具有“withXXX”的格式。
- java.time.zone包:这个包包含支持不同时区以及相关规则的类。
主要类介绍
- LocalDate:表示没有时区的日期(只含年月日的日期对象),不可变并且线程安全的
- LocalTime:表示没有时区的时间(只含时分秒的时间对象),不可变并且线程安全的
- LocalDateTime:表示没有时区的日期时间(同时包含年月日时分秒的日期对象),不可变并且线程安全的
- ZoneId:时区ID,用来确定Instant和LocalDateTime互相转换的规则
- ZonedDateTime:一个带时区的完整时间
- Instant:用来表示时间线上的一个点(瞬时)
- Clock:获取某个时区下当前的瞬时时间,日期或者时间
- Duration:表示一个绝对的精确跨度,用于计算时间间隔,使用秒或纳秒为单位
- Period:这个类表示与 Duration 相同的概念,但是以人们比较熟悉的单位表示,比如年、月、日、周
- DateTimeFormatter:提供时间格式化的类型
- TemporalAdjusters:获得指定日期时间等,如当月的第一天、今年的最后一天等
- ChronoUnit:时间单位枚举,用于加减操作
-
ChronoField:字段枚举,用于设置字段值。
全新API的优点
- 不可变,线程安全:新的日期/时间API中,所有的类都是final修饰的,都是不可变的,线程安全的。
public final class LocalDateTime
implements Temporal, TemporalAdjuster, ChronoLocalDateTime, Serializable {
- 类细化:新的API将格式化的日期时间和机器时间(unix timestamp)明确分离,它为日期(Date)、时间(Time)、日期时间(DateTime)、时间戳(unix timestamp)以及时区都定义了不同的类。不同时间分解成了各个类,比如:LocalDate, LocalTime, LocalDateTime,Instant,Year,Month,YearMonth,MonthDay,DayOfWeek等,满足各种不同场景使用需求。
- 方法统一:在所有的类中,方法都被明确定义用以完成相同的行为。比如所有的类中都定义了format()和parse()方法,而不是像以前那样专门有一个独立的类(SimpleDateFormat)。方法作用明确,清晰,统一,方便好记。
-
封装常用方法:所有新的日期/时间API类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分,等等。
- 自定义日期变化:可以使用TemporalAdjuster自定义复杂日期操作,更灵活地处理日期。
- 对比老的Date和Calendar的优化细节:
1.new Date(2020,01,01)实际是3920年2月。因为Date的构造函数 的年份表示的始于1900年的差值。
LocalDate localDate = LocalDate.of(2020, 1, 1);
2.老的month是从0开始的。LocalDate month是从1开始的。
LocalDate localDate = LocalDate.of(2020, 1, 1);
// 输出结果 1
System.out.println(localDate.getMonthValue());
3.老的 DAY_OF_WEEK 的取值,是从周日(1)开始的。LocalDate week是从周一(1)开始的。
LocalDate localDate = LocalDate.of(2020, 1, 1);
// 输出结果 WEDNESDAY
System.out.print(localDate.getDayOfWeek());
// 输出结果 3
System.out.print(localDate.getDayOfWeek().getValue());
4.Date如果不格式化,打印出的日期可读性差。LocalDate的输出默认格式化。
LocalDate localDate = LocalDate.of(2020, 1, 1);
// 输出结果 2020-01-01
System.out.println(localDate.toString());
5.老的日期类并不提供国际化,没有时区支持。新的时间类都支持了时区操作。
//服务器所在时间,输出时不包含时区
LocalDateTime ldt = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
// 输出结果 2020-09-22T22:33:11.011
System.out.println(ldt);
//意大利罗马时间,输出时包含时区
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Rome"));
// 输出结果 2020-09-22T15:33:11.015+01:00[Europe/Rome]
System.out.println(zdt);
其他时间相关的类(了解即可)
- java.util.TimeZone
TimeZone 表示时区偏移量,也可以计算夏令时。通常可以使用getDefault获取程序运行的默认时区,了解即可,目前大部分公司的国际化业务均会部署当地的服务器,所以不需要特别的时区处理 。 - java.util.Locale
Locale对象代表具体的地理,政治或文化地区,因为不同的区域,时间表示方式都不同。同样Locale.getDefault()可以获取默认的地区。 - java.util.DateFormat(非线程安全)
DateFormat是日期/时间格式化子类的抽象类,它以语言无关的方式格式化和分析日期或时间。 日期的字符串展现形式与TimeZone、Locale以及格式化风格有关。格式化风格主要分为日期格式化风格以及时间格式化风格。
// 使用静态工厂方法创建DateFormat对象来格式化时间,getTimeInstance只处理时间,getDateInstance只处理日期,getDateTimeInstance处理日期和时间。
DateFormat.getTimeInstance();
DateFormat.getDateInstance();
DateFormat.getDateTimeInstance();
Calendar calendar = Calendar.getInstance();
// 格式化结果为 23:11:23
DateFormat.getTimeInstance().format(calendar.getTime());
// 格式化结果为 2010-09-20
DateFormat.getDateInstance().format(calendar.getTime());
// 格式化结果为 2010-09-20 23:11:39
DateFormat.getDateTimeInstance().format(calendar.getTime());
DateFormat及其子类SimpleDateFormat都不是线程安全的,DateFormat中设置的calendar是共享变量。
public abstract class DateFormat extends Format {
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* DateFormat
.
* @serial
*/
protected Calendar calendar;
- java.util.SimpleDateFormat(非线程安全)
SimpleDateFormat是DateFormat的子类,可以自定义日期格式,较DateFormat提供了更精确的日期格式化控制。
// 日期和时间模式字符串
String pattern = "yyyy-MM-dd";
// 格式化结果为 2010-09-20
String sysDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
SimpleDateFormat的格式化可以由日期和时间模式字符串指定。 在日期和时间模式字符串中,从'A'到'Z'和从'a'到'z'的非引号的字母被解释为表示日期或时间字符串的组件的模式字母。
SimpleDateFormat的另一个特点是线程不安全,从format方法的实现中可以发现,date是共享变量,并且没有做线程安全控制。
当多个线程同时使用相同的SimpleDateFormat对象【如用static修饰的SimpleDateFormat】调用format方法时,多个线程会同时调用calendar.setTime方法,可能一个线程刚设置好time值另外的一个线程马上把设置的time值给修改了导致返回的格式化时间可能是错误的。同样parse方法也不是线程安全的。
// 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);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
多线程并发保证SimpleDateFormat线程安全的方法
1.避免线程之间共享一个SimpleDateFormat对象,每个线程使用时都创建一次SimpleDateFormat对象 -> 创建和销毁对象的开销大
2.对使用format和parse方法的地方进行加锁 -> 线程阻塞性能差
3.使用ThreadLocal保证每个线程最多只创建一次,SimpleDateFormat对象 -> 较好的方法(参考下图实现)
4.使用JDK8 DateTimeFormatter 替代 ->最佳方法
public static final ThreadLocal df = new ThreadLocal() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
System.out.println(df.get().format(new Date()));
- java.time.format.DateTimeFormatter
使用旧的Date对象时,可以使用SimpleDateFormat进行格式化显示。而格式化新的LocalDateTime等日期时间对象时,需要使用DateTimeFormatter。和SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。由于SimpleDateFormat不是线程安全的,使用时只能在方法内部创建新的局部变量。而DateTimeFormatter可以只创建一个实例,到处引用。
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
System.out.println(formatter.format(zdt));
var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
System.out.println(zhFormatter.format(zdt));
var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
System.out.println(usFormatter.format(zdt));
}
// 输出结果
2020-09-21T17:14 GMT
2020 9月 21 周一 17:14
Mon, September/21/2020 17:14
- java.sql.Date
java.sql.Date继承于java.util.Date,为了符合SQL DATE ,由java.sql.Date实例包装的毫秒值必须通过在实例关联的特定时区中将小时,分钟,秒和毫秒设置为零来“归一化”,简单来说就是java.sql.Date只保留了日期,而没有后面的时分秒,其他与java.util.Date并无差异。
// 结果为 2010-09-20
long time = System.currentTimeMillis();
java.sql.Date date = new java.sql.Date(time);
- java.sql.Time
java.sql.Time继承于java.util.Date,仅保留了时间。
// 结果为 23:15:44
long time = System.currentTimeMillis();
java.sql.Time sqlTime = new java.sql.Time(time);
- java.sql.Timestamp
java.sql.Timestamp同样继承于java.util.Date,增加了保持SQL TIMESTAMP小数秒的能力,允许将秒数的规格精确到纳秒。
long time = System.currentTimeMillis();
// 结果为 2020-09-20 00:44:48.413
java.sql.Timestamp sqlTimestamp = new java.sql.Timestamp(time);
// 结果为 413000000
sqlTimestamp.getNanos();
- TimeUnit
TimeUnit是一个时间单位枚举类,主要用于并发编程,表示给定的粒度单位的持续时间,并且提供了跨单元转换的实用方法,以及在这些单元中执行定时和延迟操作。TimeUnit不保留时间信息,只能帮助组织和使用可能在不同上下文中单独维护的时间表示。 一纳秒定义为千分之一秒,微秒为千分之一毫秒,毫秒为千分之一秒,一分钟为六十秒,一小时为六十分钟,一天为二十四小时。
// 定义锁的获取时间单元为50毫秒
Lock lock = ...;
if (lock.tryLock(50L, TimeUnit.MILLISECONDS)) ...
xk-time
最后给大家安利一个超强的工具,xk-time 是时间转换,计算,格式化,解析,日历和cron表达式等的工具,使用Java8
,线程安全,简单易用,多达70几种常用日期格式化模板,支持Java8时间类和Date,轻量级,无第三方依赖。
为什么要开发这个工具?
- java8以前的Date API设计不太好,使用不方便,往往会有线程安全问题。
- 常见的DateUtil,往往将时间转换,计算,格式化,解析等功能都放在同一个类中,导致类功能复杂,方法太多,查找不方便。
- 为了将与时间紧密相关的节假日、农历、二十四节气、十二星座、十二生肖、十二时辰和日历等功能集中起来开发成工具,方便使用。
主要功能类
1.日期转换工具类 DateTimeConverterUtil
2.日期计算工具类 DateTimeCalculatorUtil
3.日期格式化和解析工具类 DateTimeFormatterUtil
4.日历工具类 CalendarUtil
5.农历日期类 LunarDate
6.节假日计算类 Holiday
7.Cron表达式工具类 CronExpressionUtil
8.计算耗时工具 CostUtil
参考文章(尊重他人劳动成果)
1.LocalDateTime和Date的比较与区别
2.Java日期时间API系列