java8.秒转换成date_JAVA8时间API 最佳实践

JAVA8时间API 最佳实践

实践一:初始化时间日期

方式一:JDK1.0 java.util.Date#Date(int, int, int, int, int, int)

// jdk 1.0 写法,yinw date的日期是从1900开始,此方法已过时
        Date date1 = new Date(2020-1900, 10, 10, 10, 10, 10);       
        // Tue Nov 10 10:10:10 CST 2020
        System.out.println(date1);

需要注意的是这里的月份是从0开始的。即月份10 代表11月

方式二:JDK1.1 开始使用Calendar类

使用Calendar之后,初始化年份是直接使用当前年份即可,不需要转换。

// jdk1.1 开始使用Calendar类
        Calendar calendar = Calendar.getInstance();
        calendar.set(2020,Calendar.NOVEMBER,10,10,10,10);
        Date date2 = calendar.getTime();
        System.out.println(date2);// Tue Nov 10 10:10:10 CST 2020

在使用了Calendar类之后,就引入了时区的概念了。 查看Calendar类的方法。

3448994c0fbc64508f4880d45086680d.png

可以看到多个一个参数 TimeZone。

测试下不同时区下设置相同时间输出结果会是怎么样?

// jdk1.1 开始使用Calendar类
        System.out.println("起始时间:" + new Date(0));
        Calendar calendar = Calendar.getInstance();
        calendar.set(2020,Calendar.NOVEMBER,10,10,10,10);
        Date date2 = calendar.getTime();
        System.out.println("北京时间:"+date2);// Tue Nov 10 10:10:10 CST 2020

        // 设置成美国的时区
        Calendar calendar2 = Calendar.getInstance(TimeZone.getTimeZone("America/Honolulu"));
        calendar2.set(2020,Calendar.NOVEMBER,10,10,10,10);
        Date date3 = calendar2.getTime();
        System.out.println("美国时间:"+date3);// Tue Nov 10 18:10:10 CST 2020

可以看到不同时区相同时间,时间戳是不一样的,UTC的时间戳相差了8小时,因此使用Calendar实例化日期是,需要意识到有时区的概念。

而Date类是没有时区的概念的。

对于Date类,保存的是UTC时间,UTC是是以原子时秒长为基础,不以太阳作为参照,没有时区的概念。 因此,date保存的是一个时间戳,代表是从1970年1月1日0点到现在的毫秒数(纪元时间)。
代码 System._out_.println("起始时间:" + new Date(0)); 的输出为 输出:起始时间:Thu Jan 01 08:00:00 CST 1970 因为是纪元时间,在北京时间(CST)就是早上8点。

对于国际化的团队来说,日期的时期就很重要了。一般有两种处理方式:

  • 使用UTC保存,就不涉及时区了。即使用Date的时间戳保存。
  • 使用文本保存,但是需要记录时区的信息。而那到时间时,需要设置好时区。

例如,在北京时间发了一条消息,在美国的服务器获取发送时间的时候就需要进行转换了。 如果是使用UTC保存,直接将时间戳读取转换即可,如果使用文本记录,那么就需要根据文本记录的日期,转换成当地的时间了。

这里涉及到国际的业务推荐的最佳实践是使用UTC保存。

方式三:JDK1.8 使用java.time.* 下的时间工具类 (推荐)

java.time.LocalDateTime

Java 8 推出了新的时间日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime、 DateTimeFormatter,处理时区问题更简单清晰。

使用 java.time.LocalDateTime 创建时间

LocalDateTime localDateTime = LocalDateTime.of(2020,10,10,10,10,10);
        System.out.println(localDateTime); // 2020-10-10T10:10:10
UTC转本地时间时间带T Z
时间中包含T Z (如:2018-01-31T14:32:19Z) UTC统一时间 T代表后面跟着是时间,Z代表0时区(相差北京时间8小时)

需要注意的是java.time.LocalDateTime 是不带时区属性的,设计本类的目的是便于人阅读使用,可以理解为时间的字符串,和现实中挂着的时钟是一样的含义。所有名称中有 loacl 。他仅仅是一个本地时间的表示。

A date-time without a time-zone in the ISO-8601 calendar system, such as { @code 2007-12-03T10:15:30}.

其次,这个是个不可变的时间对象(immutable

那么这个类可以做什么呢?

  • 可以获取某一个日期,例如10月的第一个周
  • 可以计算两个时间点之间的差值

在java8中如果需要代表适合全球的时间,那么就需要使用带时区的对象ZonedDateTime。

java.time.ZonedDateTime

使用ZonedDateTime 设置时间,需要使用 java.time.ZoneId 类设置时区

// 这里的月份10 代表的就是十月
        LocalDateTime localDateTime = LocalDateTime.of(2020,10,10,10,10,10);
        System.out.println(localDateTime.toString()); // 2020-10-10T10:10:10

        ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Shanghai"));
        System.out.println(zonedDateTime);  // 2020-10-10T10:10:10+08:00[Asia/Shanghai]

可以看到输出的时间带有时区信息。

这里要正确处理国际化的实际问题,这里推荐使用ZonedDateTime。因为java8的日期工具提供了根据丰富的方法,本文就不详细展开了。 建议查阅 《JAVA8实战》 第12章

实践2:解析和格式化日期

SimpleDateFormat 会遇到的“坑”

情况1:日期格式化跨年bug

BUG还原:

@Test
    public void test03(){
        Calendar calendar = Calendar.getInstance();
        calendar.set(2019,Calendar.DECEMBER,29,0,0,0);
        SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
        System.out.println("格式化:"+sdf.format(calendar.getTime()));
    }

输出: 格式化:2020-12-29 原因分析:出现这个问题的原因在于,小写 y 是年,而大写 Y 是 week year,也就是所在的周属于哪一年。这个案例告诉我们,针对年份的日期格式化,应该一律使用 “y” 。阿里巴巴的JAVA开发手册中也有规定:

8cfe811071fa751357b5c6ed1667ab87.png

** 《Java 开发手册》

**

情况2:static的SimpleDataFormat 有线程安全问题

bug演示:

private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * 演示SimpleDataFormat 线程安全问题
     */
    @Test
    public void test04(){

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++){
            executorService.execute(() -> {
                for (int j = 0; j < 100; j++){
                    try {
                        System.out.println(sdf.parse("2020-10-10 10:10:10"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        while (true){

        }
    }

打印结果

4e40df74c8d170522ac388746c94ad9c.png

e3eb00c973c250a455871d498451ea54.png

打印中可以看到 出现不同日期和异常信息。

原因分析: 查看_sdf_.parse(), 方法调用的是父类方法。 java.text.DateFormat#parse(java.lang.String)。 之后调用 java.text.DateFormat#parse(java.lang.String, java.text.ParsePosition)方法,simpleDateFormat 对其进行了重写。

在parse重写方法中 使用CalendarBuilder创建Calendar,

parsedDate = calb.establish(calendar).getTime();

而传入 java.text.CalendarBuilder#establish的参数是DateFormat的一个字段,

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.
     *
     * <p>Subclasses should initialize this field to a {@link Calendar}
     * appropriate for the {@link Locale} associated with this
     * <code>DateFormat</code>.
     * @serial
     */
    protected Calendar calendar;

    // ...
}

establish内部先对Calendar进行clear 之后,再构建。但是对这个操作没有加锁。因此在对于calendar存在线程安全问题

解决办法,使用ThreadLocal进行处理。 改造后的代码如下:

private static ThreadLocal<SimpleDateFormat> threadLocalSdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    /**
     * 演示SimpleDataFormat 线程安全问题 解决方案
     */
    @Test
    public void test05(){

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++){
            executorService.execute(() -> {
                for (int j = 0; j < 100; j++){
                    try {
                        System.out.println(threadLocalSdf.get().parse("2020-10-10 10:10:10"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        while (true){

        }
    }

再次运行,输出的日期就正常了。

同样的原因,format方法也存在线程安全问题。

情况3:错误的格式进行错误的匹配

重现bug

/**
     * SimpleDateFormat 错误的格式进行错误的匹配
     */
    @Test
    public void test06() throws ParseException {
        String str = "20201010";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
        System.out.println(sdf.parse(str)); //Fri Feb 01 00:00:00 CST 2104
    }

输出

Fri Feb 01 00:00:00 CST 2104

预期的结果应该是 2020年,错输出了2104年2月。

原因在于 程序将1010 当成了月份。1010/12 = 84年余2个月。输出年份就变成了 2020+84=2104年

使用 java8提供的 java.time.format.DateTimeFormatter

处理日期和时间对象时,格式化以及解析日期-时间对象是另一个非常重要的功能。新的java.time.format包就是特别为这个目的而设计的.
这个包中,最重要的类是DateTimeFormatter。创建格式器最简单的方法是通过它的静态工厂方法以及常量。 像BASIC_ISO_DATE和 ISO_LOCAL_DATE 这 样 的 常 量 是 DateTimeFormatter 类 的 预 定 义 实 例 。 所 有 的DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。
/**
     * 最佳实践 DateTimeFormatter 
     */
    @Test
    public void test07() throws ParseException {
        LocalDate date = LocalDate.of(2014, 3, 18);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);

        System.out.println(date.format(DateTimeFormatter.ISO_LOCAL_DATE)); // 2014-03-18
        System.out.println(date.format(formatter)); // 18/03/2014
        System.out.println(date.format(italianFormatter)); //18. marzo 2014

        DateTimeFormatter complexFormatter = new DateTimeFormatterBuilder()
                .appendText(ChronoField.DAY_OF_MONTH)
                .appendLiteral(". ")
                .appendText(ChronoField.MONTH_OF_YEAR)
                .appendLiteral(" ")
                .appendText(ChronoField.YEAR)
                .parseCaseInsensitive()
                .toFormatter(Locale.getDefault());

        System.out.println(date.format(complexFormatter)); // 18. 三月 2014

    }

使用 java.time.format.DateTimeFormatter 有以下的优点:

  • 所有的DateTimeFormatter实例都是线程安全的。所以,你能够以单例模式创建格式器实例,就像DateTimeFormatter所定义的那些常量,并能在多个线程间共享这些实例。
  • 解析格式严格,不会出现类似情况3中的解析格式错误

实践3:操作日期时间

方式一:使用Date类

想要获取30天后的时间。

/**
     * 操作日期的方式
     */
    @Test
    public void test09(){
        // 获取30天后的时间
        Date date = new Date(System.currentTimeMillis() + 30 * 24 * 60 * 60 * 1000L);
        System.out.println(date); 
    }

这种方式需要注意的是用使用long类型,否则数值太大会造成int溢出。可以看出这种方式不够灵活,语义不明显

方式二: 使用Calendar

/**
     * 操作日期的方式
     */
    @Test
    public void test10(){
        // 获取30天后的时间
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        calendar.add(Calendar.DAY_OF_MONTH,30);
        System.out.println(calendar.getTime());
    }

这种方式相比与第一种方式语义明确多了。但是当我们需要更为复杂的时间计算时,例如 获取下个月的最后一天、同一个月中,最后一个符合星期几的要求的值,这个时候使用Calendar就太简便了。

方式三:Java 8 日期时间 API (最佳实践)

/**
     * JAVA8 操作日期的方式
     */
    @Test
    public void test11(){
        LocalDateTime localDateTime = LocalDateTime.now();
        // 获取年份
        int year = localDateTime.getYear();

        /**
         * 以相对方式修改时间
         *
         */
        localDateTime.plusWeeks(3); //3周后
        localDateTime.minusHours(9); // 9小时前
        localDateTime.plus(1, ChronoUnit.MONTHS); //1个月之后


        // with 方法 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本
        LocalDateTime newDateTime = localDateTime.with(ChronoField.DAY_OF_MONTH, 4);

        // 生成本月最后一天
        LocalDateTime dateTime2 = newDateTime.with(TemporalAdjusters.lastDayOfMonth());


    }
  • plus和minus方法都声明于Temporal接口中。通过这些方法,对TemporalUnit对象加上或者减去一个数字,我们能非常方便地将Temporal对象前溯或者回滚至某个时间段,通过ChronoUnit枚举我们可以非常方便地实现TemporalUnit接口
  • TemporalAdjusters类提供了默认中的工厂方法 ,也可以自定义自己的TemporalAdjuster方法。下面是java8中默认提供的方法。

43987443136071fe825d4bfadec705f0.png

参考引用
  • 《JAVA8实战》第12章
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值