Java 小记 - 时间的处理与探究
前言
时间的处理与日期的格式转换几乎是所有应用的基础职能之一,几乎所有的语言都会为其提供基础类库。作为曾经 .NET 的重度使用者,赖其优雅的语法,特别是可扩展方法这个神级特性的存在,我几乎没有特意关注过这些个基础类库,他们如同空气一般,你呼吸着,却不用感受其所在何处。煽情结束,入坑 Java 后甚烦其时间处理方式,在此做个总结与备忘。
1. Date 制造的麻烦
1.1 SimpleDateFormat 存在的问题
初级阶段,我仍对基础类库保留着绝对的信任,时间类型毫不犹豫地使用了 Date
,并且使用 SimpleDateFormat
类去格式化日期,介于项目中会频繁使用他们,我做了类似如下的封装:
public class DateUtils {
public static SimpleDateFormat DATE_FORMAT;
static {
DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
}
public static String getFormatDate(Date date) {
return DATE_FORMAT.format(date);
}
public static Date parseSimpleDate(String strDate) throws ParseException {
return DATE_FORMAT.parse(strDate);
}
}
单元测试跑过之后我便如数应用了:
@Test
public void formatDateTest() throws ParseException {
Date date = DateUtils.parseSimpleDate("2018-07-12");
boolean result = DateUtils.getFormatDate(date).equals("2018-07-12");
Assert.assertTrue(result);
}
然而项目上线后频繁报 java.lang.NumberFormatException
异常,被好一顿吐槽,一查资料才知道 SimpleDateFormat
竟然是线程不安全的。看了下源码,定位到问题所在:
protected Calendar calendar;
// 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;
}
血槽已空,就这么使用了内部变量,回过头看下了注释,人家早已友情提示,呵呵:
/**
* Date formats are not synchronized.
* It is recommended to create separate format instances for each thread.
* If multiple threads access a format concurrently, it must be synchronized
* externally.
*/
单元测试中复现:
1.2 SimpleDateFormat 线程不安全的解决方案
最简单,最不负责任的方法就是加锁:
synchronized (DATE_FORMAT) {
return DATE_FORMAT.format(strDate);
}
因格式化日期常会应用在列表数据的遍历处理中,弃之。还有一种较好的解决方案就是线程内独享,代码修改如下:
public class DateUtils {
private static ThreadLocal<DateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String getFormatDate(Date date) {
return THREAD_LOCAL.get().format(date);
}
public static Date parseSimpleDate(String strDate) throws ParseException {
return THREAD_LOCAL.get().parse(strDate);
}
}
看起来还算不错,兼顾了线程安全与效率,但,好死不死的,当初把 DATE_FORMAT
定义为 public
,并且在某些特殊的场景中直接使用了该静态变量,总之不能通过只改一个工具类解决所有问题,左右都是麻烦,于是乎干脆直接抛弃 SimpleDateFormat
换 org.apache.commons.lang3.time.DateFormatUtils
取而代之,更改代码如下:
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
public static String DATE_PATTERN = "yyyy-MM-dd";
public static String getFormatDate(Date date) {
return DateFormatUtils.format(date, DATE_PATTERN);
}
public static Date parseSimpleDate(String strDate) throws ParseException {
return parseDate(strDate, DATE_PATTERN);
}
}
1.3 烦人的 Calendar
除了日期格式的转换,应用中的对时间处理的另一大需求就是计算,感激 org.apache.commons.lang3.time.DateUtils
这个工具类为我们做了绝大部分的封装,能想到的一些基础的计算都可以直接 “无脑” 使用了。但有时仍然免不了要传 Calendar
中的各种参数进入,特别是那一堆烦人的常量:
public final static int ERA = 0;
public final static int YEAR = 1;
public final static int MONTH = 2;
public final static int WEEK_OF_YEAR = 3;
public final static int WEEK_OF_MONTH = 4;
public final static int DATE = 5;
public final static int DAY_OF_MONTH = 5;
public final static int DAY_OF_YEAR = 6;
public final static int DAY_OF_WEEK = 7;
public final static int DAY_OF_WEEK_IN_MONTH = 8;
public final static int AM_PM = 9;
public final static int HOUR = 10;
public final static int HOUR_OF_DAY = 11;
public final static int MINUTE = 12;
public final static int SECOND = 13;
public final static int MILLISECOND = 14;
public final static int ZONE_OFFSET = 15;
public final static int DST_OFFSET = 16;
public final static int FIELD_COUNT = 17;
public final static int SUNDAY = 1;
public final static int MONDAY = 2;
public final static int TUESDAY = 3;
public final static int WEDNESDAY = 4;
public final static int THURSDAY = 5;
public final static int FRIDAY = 6;
public final static int SATURDAY = 7;
public final static int JANUARY = 0;
public final static int FEBRUARY = 1;
public final static int MARCH = 2;
public final static int APRIL = 3;
public final static int MAY = 4;
public final static int JUNE = 5;
public final static int JULY = 6;
public final static int AUGUST = 7;
public final static int SEPTEMBER = 8;
public final static int OCTOBER = 9;
public final static int NOVEMBER = 10;
public final static int DECEMBER = 11;
public final static int UNDECIMBER = 12;
public final static int AM = 0;
public final static int PM = 1;
public static final int ALL_STYLES = 0;
可能存在即合理,我定然是没有资格评判什么,我只是不喜欢。大而全的方法固然得存在,但是不是得和常用的方案区别开呢,或许会有声音说:“你可以自己动手抽离呀”,是啊,例:
public static Date getBeginOfMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DATE, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
public static Date getEndOfMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));
calendar.set(Calendar.HOUR_OF_DAY, calendar.getActualMaximum(Calendar.HOUR_OF_DAY));
calendar.set(Calendar.MINUTE, calendar.getActualMaximum(Calendar.MINUTE));
calendar.set(Calendar.SECOND, calendar.getActualMaximum(Calendar.SECOND));
calendar.set(Calendar.MILLISECOND, calendar.getActualMaximum(Calendar.MILLISECOND));
return calendar.getTime();
}
即使这些代码只会存在于工具类中,但,只能说不喜欢吧,他们不该从我的手里写出来。
2. Instant 的救赎
Java8 中新增的日期核心类如下:
Instant
LocalDate
LocalTime
LocalDateTime
其余的还有一些时区的以及计算相关的类会在后续的代码示例中提及,这儿主要说下 Instant
,查看源码可看到其仅包含两个关键字段:
/**
* The number of seconds from the epoch of 1970-01-01T00:00:00Z.
*/
private final long seconds;
/**
* The number of nanoseconds, later along the time-line, from the seconds field.
* This is always positive, and never exceeds 999,999,999.
*/
private final int nanos;
/**
* Gets the number of seconds from the Java epoch of 1970-01-01T00:00:00Z.
* <p>
* The epoch second count is a simple incrementing count of seconds where
* second 0 is 1970-01-01T00:00:00Z.
* The nanosecond part of the day is returned by {@code getNanosOfSecond}.
*
* @return the seconds from the epoch of 1970-01-01T00:00:00Z
*/
public long getEpochSecond() {
return seconds;
}
/**
* Gets the number of nanoseconds, later along the time-line, from the start
* of the second.
* <p>
* The nanosecond-of-second value measures the total number of nanoseconds from
* the second returned by {@code getEpochSecond}.
*
* @return the nanoseconds within the second, always positive, never exceeds 999,999,999
*/
public int getNano() {
return nanos;
}
秒和纳秒组合的绝对时间差不多是现在公认的最好的时间处理方式了吧,全世界各地的绝对时间都是相同的,所以可以先把烦人的时区还有那矫情的夏令时丢一边,是一个非常好的中间值设计。
2.1 Instant 与 LocalDateTime 的互转
由于 Instant
不包含时区信息,因此转换时需要指定时区,我们来看看以下示例:
@Test
public void timeZoneTest() {
Instant instant = Instant.now();
LocalDateTime timeForChina = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));
LocalDateTime timeForAmerica = LocalDateTime.ofInstant(instant, ZoneId.of("America/New_York"));
long dif = Duration.between(timeForAmerica, timeForChina).getSeconds() / 3600;
Assert.assertEquals(dif, 12L);
}
上海用的是东八区的时间,纽约用的是西五区的时间,地理时差应为 13 个小时,但美国使用了夏令时,因此实际时差为 12 个小时,以上单元测试能通过证明 LocalDateTime
已经帮帮我们处理了夏令时问题。源代码如下:
public static LocalDateTime ofInstant(Instant instant, ZoneId zone) {
Objects.requireNonNull(instant, "instant");
Objects.requireNonNull(zone, "zone");
ZoneRules rules = zone.getRules();
ZoneOffset offset = rules.getOffset(instant);
return ofEpochSecond(instant.getEpochSecond(), instant.getNano(), offset);
}
...
可看出获取时间偏移量的关键类为:ZoneRules
,由此反过来转换也非常简单,参照源码中的写法:
@Test
public void instantTest() {
LocalDateTime time = LocalDateTime.parse("2018-07-13T00:00:00");
ZoneRules rules = ZoneId.of("Asia/Shanghai").getRules();
Instant instant = time.toInstant(rules.getOffset(time));
LocalDateTime timeBack = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));
Assert.assertEquals(time, timeBack);
}
2.2 再谈格式化
新增的日期格式转换类为 DateTimeFormatter
,虽然还达不到如 C#
那般随心所欲,但至少是线程安全了,可以放心使用,其次,好歹也预置了几个常用的格式模板,为对其进一步地封装提供了一些便利性。
常用的字符串转日期方式如下:
@Test
public void parse() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
LocalDateTime time = LocalDateTime.parse("2018-07-13 12:05:30.505",formatter);
System.out.println(time);
}
以上示例代码哟有三点让人十分不爽,其一,我需要获得的是一个时间类型,他不存在格式的问题,显式指定模板接而进行转换看起来很傻;其二,时间的显示格式是正则轻易可穷尽的,就那么几种,还需要显式传入模板,看起来很傻;其三,LocalDateTime.parse()
不支持 LocalDate
格式的模板,看起来很傻;
因此我对其做了一个简易的封装,示例如下:
public class DateUtils {
public static HashMap<String, String> patternMap;
static {
patternMap = new HashMap<>();
// 2018年7月13日 12时5分30秒,2018-07-13 12:05:30,2018/07/13 12:05:30
patternMap.put("^\\d{4}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{3}\\D*$",
"yyyy-MM-dd-HH-mm-ss-SSS");
patternMap.put("^\\d{4}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D+\\d{2}\\D*$",
"yyyy-MM-dd-HH-mm-ss");
patternMap.put("^\\d{4}\\D+\\d{2}\\D+\\d{2}\\D*$", "yyyy-MM-dd");
// 20180713120530
patternMap.put("^\\d{14}$", "yyyyMMddHHmmss");
patternMap.put("^\\d{8}$", "yyyyMMdd");
}
public static LocalDateTime parse(String text) {
for (String key : patternMap.keySet()) {
if (Pattern.compile(key).matcher(text).matches()) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(patternMap.get(key));
text = text.replaceAll("\\D+", "-")
.replaceAll("-$", "");
return parse(formatter, text);
}
}
throw new DateTimeException("can't match a suitable pattern!");
}
public static LocalDateTime parse(DateTimeFormatter formatter, String text) {
TemporalAccessor accessor = formatter.parseBest(text,
LocalDateTime::from,
LocalDate::from);
LocalDateTime time;
if (accessor instanceof LocalDate) {
LocalDate date = LocalDate.from(accessor);
time = LocalDateTime.of(date, LocalTime.MIDNIGHT);
} else {
time = LocalDateTime.from(accessor);
}
return time;
}
}
测试:
@Test
public void parse() {
String[] array = new String[]{
"2018-07-13 12:05:30",
"2018/07/13 12:05:30.505",
"2018年07月13日 12时05分30秒",
"2018年07月13日 12时05分30秒505毫秒",
"2018-07-13",
"20180713",
"20180713120530",
};
System.out.println("-------------------------");
for (String s : array) {
System.out.println(DateUtils.parse(s));
}
System.out.println("-------------------------");
}
以上示例应该够满足大部分的应用场景了,有特殊的情况出现继而往 patternMap
中添加即可。
反过来日期转字符串,这时候传入 pattern
是说的过去的,因为对此场景而言显示格式成为了核心业务,例:
@Test
public void format() {
LocalDateTime time = LocalDateTime.of(2018, 7, 13, 12, 5, 30);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
Assert.assertEquals(time.format(formatter), "2018-07-13 12:05:30.000");
}
当然,对于常用的格式也应当封装入工具类中。
结语
到此暂时告一段落,后续若有遇到更复杂的应用场景再继续补充。这篇文章拖了好几天才匆匆收尾,期间经历了一些事情可谓是...,前些阶段接二连三地上了好几个 java 项目,本以为紧绷的弦可以松一松,喘口气,花点时间做些总结,写写文章。
哪晓得,项目上线的第二天公司倒闭了,来了一堆特勤又是盘问又是查封,接着所有员工全部遣散回家。那几天整个很懵X,所有管理层没有一个出来解释情况(ps:貌似进去的进去,失联的失联),网上一爬才晓得互金界集体暴雷,然后,就完了。昨日助其黄袍加身,今日恨不能解其体,食其肉,一夜之间灰飞烟灭,我也算亲眼见识到了这幅场景。正好卡在交社保的关头,看着炸锅的维权群和各类小道消息,归就一声长叹。
无奈又得重新出发,虽可借塞翁失马宽慰之,但终免不了些许落寞。若君有良机,肯请告知~(能吃苦,自我驱动强,学习能力尚可
Java 小记 - 时间的处理与探究的更多相关文章
- Java实现时间动态显示方法汇总
这篇文章主要介绍了Java实现时间动态显示方法汇总,很实用的功能,需要的朋友可以参考下 本文所述实例可以实现Java在界面上动态的显示时间.具体实现方法汇总如下: 1.方法一 用TimerTask: ...
- Java 对时间和日期的相关处理
1. 获取当前系统时间和日期并格式化输出 import java.util.Date; import java.text.SimpleDateFormat; public class NowStrin ...
- java中时间的获取(二)
java中时间的获取2 /** * 获取数据库操作记录时间 */ public static String getOpreateDbTime() { Calendar c = Calendar.get ...
- Java 日期时间
Java 日期时间 标签 : Java基础 Date java.util.Date对象表示一个精确到毫秒的瞬间; 但由于Date从JDK1.0起就开始存在了,历史悠久,而且功能强大(既包含日期,也包含 ...
- JAVA格式化时间日期
JAVA格式化时间日期 import java.util.Date; import java.text.DateFormat; /** * 格式化时间类 * DateFormat.FULL = 0 * ...
- Java日期时间使用(转)
Java日期时间使用总结 转自:http://lavasoft.blog.51cto.com/62575/52975/ 一.Java中的日期概述 日期在Java中是一块非常复杂的内容,对于一个 ...
- Java格式化时间
Java格式化时间 将秒或者毫秒值格式化成指定格式的时间 效果图 工具类 工具类里我只列出了一种格式的格式化方式,可以根据自己的需求,修改"yyyy-MM-dd hh:mm:ss" ...
- java Date时间的各种转换方式和Mysql存时间类型字段的分析
一:各种Date之间的转换方法 public class TimeTest { public static void main(String[] args) { Date date = new Dat ...
- Java日期时间处理
Java 日期时间处理 一.时间相关类 java.lang.System java.util.Date java.util.Calendar java.util.GregorianCalendar j ...
随机推荐
- struts2从认识到细化了解
目录 Struts2的介绍与执行流程 介绍: 执行流程: 运行环境搭建 基础示例 Action类的编写 介绍: 访问servlet API 补充: 配置文件 常见配置文件: 常量的配置: struts ...
- Scala之Calendar,SimpleDateFormat简单用法
package com.dingxin.entrance import java.text.SimpleDateFormat import java.util.{Calendar, Date} /** ...
- C#-类(九)
类的定义 类是描述具有相同特征与行为的事物的抽象,类内部包含类的特征和类的行为 类支持继承 类的定义是关键字class为标志 类的格式 访问标识符 class 类名 { 类主体 } 访问标识符:指定了 ...
- DELL OME监控服务器安装配置
介绍 OME软件配合DELL的SA可以对服务器硬件进行监控,并且如果服务器出问题会自动联系DELL报修,方便我们管理维护,具体安装要求就不多写了,我用的机器4核8G内存200G硬盘空间,创建在Hype ...
- AI学习---深度学习&TensorFlow安装
深度学习 深度学习学习目标: 1. TensorFlow框架的使用 2. 数据读取(解决大数据下的IO操作) + 神经网络基础 3. 卷积神经网络的学习 + 验证码识别的案例 机器学习与深度学 ...
- Python中关于with open file as 的用法
最近用到python来处理文本文件了,然后需要处理文件.发现python中提供的with open as 这个还是用的不错的!好的,废话不多说了,看下例子: with open('./sig ...
- AOP的底层实现:JDK动态代理与Cglib动态代理
转载自 https://www.cnblogs.com/ltfxy/p/9872870.html SpringAOP底层的实现原理: JDK动态代理:只能对实现了接口的类产生代理.(实现接口默认JDK ...
- nginx stream 日志设置(Version 1.9.0 +)
nginx自1.9.0开始提供tcp/udp的反向代理功能,直到1.11.4才开始提供session日志功能. 启用stream日志配置文件 主配置文件/etc/nginx/nginx.conf增加内 ...
- TCP长连接保持连接状态
对于TCP长连接保活是十分必要的,原因如下: 1.系统多在OA网和外网间有防火墙隔离,很多防火墙对一段时间内没有报文活动的socket会自动关闭. 2.对于非正常断开的连接系统并不能侦测到,比如防火墙 ...
- angular5 组件通信(一)
用了两年angular1,对1的组件通信比较熟练,最直接的就是直接使用scope的父子节点scope就可以实现,且基本都是基于作用域实现的通信:还有就是emit,broadcast,on这几个东西了. ...