不推荐使用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日期的主要内容,如果未能解决你的问题,请参考以下文章

Java日期计算,推荐使用Calendar进行日期加减

填坑:Java 中的日期转换

6.4 Java 8的日期时间类

推荐Calendar操作日期

时间日期类

Java 通过 Calendar 获取独立的 年月日时分秒 代码封装