不推荐使用Date日期和Calendar日期
Posted 阳阳天天开心
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了不推荐使用Date日期和Calendar日期相关的知识,希望对你有一定的参考价值。
很神奇,为什么java中不推荐使用Date与Calendar日期了,很多时候初学者在刚学习java时,大部分使用的日期都是Date与Calendar,只有及少数个别使用的LocalDateTime日期
Date 与 Calendar存在的共性问题
-
毫秒值与日期直接转换麻烦和繁琐,然后通过毫秒值来计算时间的差额步骤较多,并且还可能存在误差
-
再有一个就是线程问题,我们都知道在创建一个Date日期变量时,它的格式不是我们能看懂的日期格式,这时候就需要使用SimpleDateFormat了,然后
这就引发了一个问题,这个类的线程是不安全的(在做生产总览的时候,需要大量使用日期类,当时每个类我都创建了一个SimpleDateFormat,想着有点
浪费内存空间了,然后在service层最上面定义了一个静态的日期格式类,接着就引发了一个问题,页面报错,然后查询资料才知道,多个线程
对同一个类操作造成了格式化错误,甚至有可能引发内存泄漏,可能导致内存泄漏问题,占用过多的内存资源)
使用日期推荐LocalDate、LocalTime、LocalDateTime这三个日期类
1. LocalDate类是一个不可变的日期时间对象,表示日期,通常被视为年月日
// 获取当前年、月、日 LocalDate today = LocalDate.now(); // 获取年 int year = today.getYear(); // 获取月 int month = today.getMonthValue(); // 获取天 int day = today.getDayOfMonth(); // 获取毫秒值 LocalDateTime startOfDay = localDate.atStartOfDay(); long millisecondsFromDate = startOfDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
2. LocalTime类是一个不可变的日期时间对象,代表一个时间,通常被看作小时-秒,时间表示为纳秒精度
// 获取当前年、月、日 LocalTime time = LocalTime.now(); int hour = time.getHour(); // 获取小时 int minute = time.getMinute(); // 获取分钟 int second = time.getSecond(); // 获取秒 int nano = time.getNano(); // 获取纳秒
3. LocalDateTime是一个不可变的日期时间对象,代表日期时间,通常被视为年-月-日- 时-分-秒
LocalDateTime dateTime = LocalDateTime.now(); int year = dateTime.getYear(); // 获取年份 int month = dateTime.getMonthValue(); // 获取月份 (1-12) int dayOfMonth = dateTime.getDayOfMonth(); // 获取月份中的天数 int hour = dateTime.getHour(); // 获取小时 int minute = dateTime.getMinute(); // 获取分钟 int second = dateTime.getSecond(); // 获取秒 int nano = dateTime.getNano(); // 获取纳秒 // 获取毫秒值 // 需要将LocalDateTime变量转换为ZoneDateTime对象 ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault()); Instant instant = zonedDateTime.toInstant(); long milliseconds = instant.toEpochMilli(); // 日期格式化 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String dayTime = dateTimeFormatter.format(dateTime);
方法名 | 说明 |
public LocalDateTime plusYears(long years) | 添加或者减去年 |
public LocalDateTime plusMonths(long months) | 添加或者减去月 |
public LocalDateTime plusDays(long months) | 添加或者减去日 |
public LocalDateTime plusHours(long hours) | 添加或者减去时 |
public LocalDateTime plusMinutes(long minutes) | 添加或者减去分 |
public LocalDateTime plusSeconds(long seconds) | 添加或者减去秒 |
public LocalDateTime plusWeeks(long weeks) | 添加或者减去周 |
这里要注意!!根据业务需求使用不同的日期,如果前端传值为年-月-日,而后端使用了LocalDateTime日期会报无法转换的问题,这是因为LocalDateTime底层是被final修饰了无法更改,它的格式只能是年-月-日 时:分:秒,所以要使用对应的日期格式
使用示例:(获取一天中的24小时,然后使用String的format方法截取)
// 创建 DateTimeFormatter 对象,指定时间格式为 "yyyy-MM-dd HH:mm:ss" DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); LocalDate date = LocalDate.parse(dayTime); // 获取当前时间,小时取整,分和秒设为0 使用with将日期设定为指定日期,然后分与秒为0 LocalDateTime now = LocalDateTime.now().with(date).withMinute(0).withSecond(0); for(int j = 0; j < 24; j++) TbProductionLineStopDTO tbProductionLineStopDTO = new TbProductionLineStopDTO(); if(j == 23) // 设置当前日期小时为23,并且格式化 startTime = now.withHour(23).format(formatter); // 天数减1,并且小时设置为0 endTime = now.plusDays(1).withHour(0).format(formatter); starHour = String.format("%02d", j); endHour = String.format("%02d", 00); else // 创建 startTime 和 endTime 变量 startTime = now.withHour(j).format(formatter); endTime = now.withHour(j+1).format(formatter); starHour = String.format("%02d", j); endHour = String.format("%02d", j+1); Integer productionCount = realTimeMapper.queryTbProductPassrecord(startTime,endTime,lineCode); if(productionCount == null) productionCount = 0; String time = starHour + "-" + endHour; tbProductionLineStopDTO.setLineCount(productionCount); tbProductionLineStopDTO.setTimeHour(time); tbProductionLineStopDTOList.add(tbProductionLineStopDTO);
填坑:Java 中的日期转换
我们之前讨论过时间,在Java 中有一些方法会出现横线?比如Date 过期方法。
参考文章:知识点:java一些方法会有横线?以Date 过期方法为例
Java中的日期和时间处理方法
- Date类(官方不再推荐使用,官方解释Date类不利于国际化,推荐使用Calendar类)
- Calendar类
- DateFormat类 使用此类来时间初始化
我们发现,时间toLocalString 会有横线:
vo.setSubmitDate(new Date().toLocaleString());
可以改为:
vo.setSubmitDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance(TimeZone.getTimeZone("GMT+08:00")).getTime()));
在最近的项目中,用SimpleDateFormat出现一些奇奇怪怪的BUG:
- 1.结果值不对:转换的结果值经常和预期不同。
- 2.内存泄漏: 由于转换的结果值不对,后续的一些操作会导致系统内存泄漏,频繁触发GC(垃圾回收),造成系统不可用。
1 什么是SimpleDateFormat
在java doc对SimpleDateFormat的解释如下:
SimpleDateFormatis a concrete class for formatting and parsing dates in a locale-sensitive manner. It allows for formatting(date → text), parsing (text → date), and normalization.
SimpleDateFormat是一个用来对位置敏感的格式化和解析日期的实体类。他允许把日期格式化成text,把text解析成日期和规范化。
1.1 使用SimpleDateFormat
simpleDateFormat的使用方法比较简单:
public static void main(String[] args) throws Exception { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss"); System.out.println(simpleDateFormat.format(new Date())); System.out.println(simpleDateFormat.parse("2018-10-24 12:10:24")); }
- 1.定义一个日期"yyyy-mm-dd HH:mm:ss"的pattern, 也就是我们这个simpleDateFormat不管是格式化还是解析都需要按照这个pattern。
- 2.对于format需要传递Date的对象,会返回一个String类型,这个String会按照我们上面的格式生成。
- 3.对于parse需要传递一个按照上面pattern的字符串,如果传递错误的pattern会抛出java.text.ParseException异常,如果传递正确的会生成一个Date对象。
附:格式占位符 G 年代标志符 y 年 M 月 d 日 h 时 在上午或下午 (1~12) H 时 在一天中 (0~23) m 分 s 秒 S 毫秒 E 星期 D 一年中的第几天 F 一月中第几个星期几 w 一年中第几个星期 W 一月中第几个星期 a 上午 / 下午 标记符 k 时 在一天中 (1~24) K 时 在上午或下午 (0~11) z 时区复制代码
回到我们遇到的坑:
2 为什么SimpleDateFormat会线程不安全呢?
在SimpleDateFormat源码中,所有的格式化和解析都需要通过一个中间对象Calendar进行转换,而这将会出现线程不安全的操作
比如当多个线程操作同一个Calendar的时候后来的线程会覆盖先来线程的数据,那最后其实返回的是后来线程的数据,这样就导致我们上面所述的BUG的产生
为什么会出现这么多问题呢?
因为SimpleDateFormat线程不安全,很多人都会写个Util类,然后把SimpleDateFormat定义成全局的一个常量,所有线程都共享这个常量:
protected static final SimpleDateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd"); public static Date formatDate(String date) throws ParseException { returndayFormat.parse(date); }
3.如何避坑
对于SimpleDateFormat的解决方法有下面几种:
3.1 新建SimpleDateFormat
原因:所有线程都共用一个SimpleDateFormat,
解决办法:每次使用的时候都创建一个新的SimpleDateFormat,我们可以在DateUtils中将创建SimpleDateFormat放在方法内部:
public static Date formatDate(String date) throws ParseException { SimpleDateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd"); return dayFormat.parse(date); }
上面这个方法虽然能解决我们的问题但是引入了另外一个问题就是,如果这个方法使用量比较大,有可能会频繁造成Young gc,整个系统还是会受一定的影响。
3.2 使用ThreadLocal
使用ThreadLocal能避免造成Young gc,我们对每个线程都使用ThreadLocal进行保存
由于ThreadLocal是线程之间隔离开的,所以不会出现线程安全问题:
private static ThreadLocal simpleDateFormatThreadLocal = new ThreadLocal<>(); public static Date formatDate(String date) throws ParseException { SimpleDateFormat dayFormat = getSimpleDateFormat(); returndayFormat.parse(date); } private static SimpleDateFormatgetSimpleDateFormat() { SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get(); if(simpleDateFormat == null){ simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss") simpleDateFormatThreadLocal.set(simpleDateFormat); } returnsimpleDateFormat; }
3.3使用第三方工具包
虽然上面的ThreadLocal能解决我们出现的问题,但是第三方工具包提供的功能更加强大,在java中有两个类库比较出名一个是Joda-Time,一个是Apache common包
3.3.1 Joda-Time(推荐)
Joda-Time 令时间和日期值变得易于管理、操作和理解。对于我们复杂的操作都可以使用Joda-Time操作,下面我列举两个例子,对于把日期加上90天,如果使用原生的Jdk我们需要这样写:
Calendar calendar = Calendar.getInstance();calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);SimpleDateFormat sdf = new SimpleDateFormat("E MM/dd/yyyy HH:mm:ss.SSS"); calendar.add(Calendar.DAY\\_OF\\_MONTH, 90); System.out.println(sdf.format(calendar.getTime()));
但是在我们的joda-time中只需要两句话,并且api也比较通俗易懂,所以你为什么不用Joda-Time呢?
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0); System.out.println(dateTime.plusDays(90).toString("E MM/dd/yyyy HH:mm:ss.SSS");
3.3.2 common-lang包
在common-lang包中有个类叫FastDateFormat,由于common-lang这个包基本被很多Java项目都会引用,所以你可以不用专门去引用处理时间包,即可处理时间,在FastDateFormat中每次处理时间的时候会创建一个calendar,使用方法比较简单代码如下所示:
FastDateFormat.getInstance().format(new Date());
3.4升级jdk8(推荐)
在java8中Date这个类中的很多方法包括构造方法都被打上了@Deprecated废弃的注解,取而代之的是LocalDateTime,
LocalDate LocalTime这三个类:
- LocalDate无法包含时间;
- LocalTime无法包含日期;
- LocalDateTime才能同时包含日期和时间。
知识点:Java8 在日期的格式化和解析方面不用考虑线程安全性
代码如下:
public static String formatTime(LocalDateTime time,String pattern) { returntime.format(DateTimeFormatter.ofPattern(pattern)); }
localDateTime不仅解决了线程安全的问题,同样也提供了一些其他的运算比如加减天数:
//日期加上一个数,根据field不同加不同值,field为ChronoUnit.* public static LocalDateTime plus(LocalDateTime time, long number, TemporalUnit field) { return time.plus(number, field); } //日期减去一个数,根据field不同减不同值,field参数为ChronoUnit.* public static LocalDateTime minu(LocalDateTime time, long number, TemporalUnit field){ return time.minus(number,field); }
延伸
使用LocalDateTime 会改变现有的代码,我们可以将他们两进行互转:
//Date转换为LocalDateTime public static LocalDateTime convertDateToLDT(Date date) { return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); } //LocalDateTime转换为Date public static Date convertLDTToDate(LocalDateTime time) { return Date.from(time.atZone(ZoneId.systemDefault()).toInstant()); }
【公众号】:一只阿木木
以上是关于不推荐使用Date日期和Calendar日期的主要内容,如果未能解决你的问题,请参考以下文章