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 ...
随机推荐
- LeetCode题解之Binary Tree Right Side View
1.题目描述 2.问题分析 使用层序遍历 3.代码 vector<int> v; vector<int> rightSideView(TreeNode* root) { if ...
- jar、war、ear
附加jboss里面的application.xml <?xml version=”1.0″ encoding=”UTF-8″?> <application xmlns="h ...
- c# 设置MdiClient窗体的背景图片
在窗体的InitializeComponent();方法后面添加下面的代码. MdiClient MC = new MdiClient(); MC.Name = "MdiClientForm ...
- ugui中toggle.isOn的属性笔记
准备知识 toggle:指unity3d引擎中UGUI的 toggle组件 (单选框) 本文使用lua语言描述 事件触发 使用unity的ugui,你如果细心观察会发现toggle在界面被关闭/隐藏( ...
- python设计模式之单例模式(转)
设计模式之单例模式 单例设计模式是怎么来的?在面向对象的程序设计中,当业务并发量非常大时,那么就会出现重复创建相同的对象,每创建一个对象就会开辟一块内存空间,而这些对象其实是一模一样的,那么有没有办法 ...
- 挂载KVM Guest操作系统磁盘
使用虚拟机时, 发现想要修改虚拟机中的文件非常麻烦, 需要启动虚拟机, 然后再登录进去修改. 对于已经关闭的虚拟机, 为了修改一个文件而启动, 非常耽误时间. 对于一个无法启动的虚拟机(比如启动文件损 ...
- 【2018.08.19 C与C++基础】编程语言类型系统简介(草稿)
还是先占坑,等理顺了思路再写,学过的东西总是无法系统化,感觉什么都知道一点,但一深入却是一脸懵逼. 这真的是个问题,看似很努力,却无法成为一个master. 参考链接: 1. 编程语言的类型系统为何如 ...
- spring简述
1. 什么是Spring Spring:SE/EE开发的一站式框架.(有EE开发每一层的解决方案) WEB层:SpringMVC Service层:Spring的Bean管理,Spring的声明式事务 ...
- (转)Spring Boot(二十):使用 spring-boot-admin 对 Spring Boot 服务进行监控
http://www.ityouknow.com/springboot/2018/02/11/spring-boot-admin.html 上一篇文章<Spring Boot(十九):使用 Sp ...
- 设计模式のMementoPattern(备忘录模式)----行为模式
一.产生背景 意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态. 主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态, ...