龙空技术网

Java数据类型系列之时间系列

Java正道的光 93

前言:

而今我们对“java毫秒数转时间”大约比较注意,同学们都想要分析一些“java毫秒数转时间”的相关资讯。那么小编同时在网摘上网罗了一些关于“java毫秒数转时间””的相关文章,希望兄弟们能喜欢,同学们快快来了解一下吧!

java 中的时间(鸡肋的时间操作)

在很长的一段时间里,Java 的日期时间解决方案一直是一个备受争议的设计,它的问题很多,有的是概念容易让人混淆(比如:Date 和 Calendar 什么情况下该用哪个),有的是接口设计的不直观(比如:Date 的 setMonth 参数是从 0 到 11),有的是实现容易造成问题(比如:前面提到的 SimpleDateFormat 需要考虑多线程并发的问题,需要每次构建一个新的对象出来)。

这种乱象存在了很长时间,有很多人都在尝试解决这个问题(比如 Joda Time)。从 Java 8 开始,Java 官方的 SDK 借鉴了各种程序库,引入了全新的日期时间解决方案。这套解决方案与原有的解决方案是完全独立的,也就是说,使用这套全新的解决方案完全可以应对我们的所有工作。

Date

java.util 包提供了 Date 类来封装当前的日期和时间。 Date 类提供两个构造函数来实例化 Date 对象。第一个构造函数使用当前日期和时间来初始化对象。第二个构造函数接收一个参数,该参数是从1970年1月1日起的毫秒数。

Date( )Date(long millisec)

Date提供以下比较常用的方法

序号

方法和描述

1

boolean after(Date date) 若当调用此方法的Date对象在指定日期之后返回true,否则返回false。

2

boolean before(Date date) 若当调用此方法的Date对象在指定日期之前返回true,否则返回false。

3

Object clone( ) 返回此对象的副本。

4

int compareTo(Date date) 比较当调用此方法的Date对象和指定日期。两者相等时候返回0。调用对象在指定日期之前则返回负数。调用对象在指定日期之后则返回正数。

5

int compareTo(Object obj) 若obj是Date类型则操作等同于compareTo(Date) 。否则它抛出ClassCastException。

6

boolean equals(Object date) 当调用此方法的Date对象和指定日期相等时候返回true,否则返回false。

7

long getTime( ) 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。

8

int hashCode( ) 返回此对象的哈希码值。

9

void setTime(long time) 用自1970年1月1日00:00:00 GMT以后time毫秒数设置时间和日期。

10

String toString( ) 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat)。

我们用的最多的其实是输出时间或者返回时间,这个时候我们就会用到toString 方法,我们看一下toString 到底输出了什么,为什么我们经常把SimpleDateFormat 和 Date 类型放在一起用

public static void main(String[] args) {    Date date = new Date();    System.out.println(date);}

输出

Thu Mar 25 17:23:23 CST 2021

我们可以看到我们之间输出date 对象的时候,可读性还是比较差的,所以我们经常将SimpleDateFormat 和 Date 类型放在一起用

SimpleDateFormat

SimpleDateFormat 主要有两方面作用

将Date类型转化成可读性比较好的字符串从字符串解析出一个Date 类型

SimpleDateFormat 的格式化编码,我们格式化Date 的时候需要根据需求选择合适的编码组合

字母

描述

示例

G

纪元标记

AD

y

四位年份

2001

M

月份

July or 07

d

一个月的日期

10

h

A.M./P.M. (1~12)格式小时

12

H

一天中的小时 (0~23)

22

m

分钟数

30

s

秒数

55

S

毫秒数

234

E

星期几

Tuesday

D

一年中的日子

360

F

一个月中第几周的周几

2 (second Wed. in July)

w

一年中第几周

40

W

一个月中第几周

1

a

A.M./P.M. 标记

PM

k

一天中的小时(1~24)

24

K

A.M./P.M. (0~11)格式小时

10

z

时区

Eastern Standard Time

'

文字定界符

Delimiter

"

单引号

`

format 友好输出

public static void main(String[] args) {    Date date = new Date();    System.out.println(date);    DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");    String formatStr = format.format(date);    System.out.println(formatStr);}

输出

Thu Mar 25 17:27:58 CST 20212021-03-25 17:27:58

可以看到可读性好了很多

parse 快速转换

我们除了将Date 类型进行输出之外,很多时候我们可能需要对时间类型进行比较(可以参考上面表格),这个时候如果我们的操作对象是字符串的时候就很不方便,我们希望将字符串的时间转化成Date 类型

    public static void parse() throws ParseException {        Date date = new Date();        System.out.println(date);        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");        String formatStr = format.format(date);        System.out.println(formatStr);        date = format.parse(formatStr);        System.out.println(date);    }
臭名昭著的 SimpleDateFormat线程不安全的原因之format 方法

当我们调用format 方法的时候其实里面的代码实现是这样的,出问题的就是在calendar.setTime(date) 上面,下面我贴出了源代码

    // 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;    }

假设在一个多线程环境下,有两个线程持有了同一个 SimpleDateFormat 的实例,分别调用 format 方法:

线程 1 调用 format 方法,改变了 calendar 这个字段。线程被中断来。线程 2 开始执行,它也改变了 calendar。线程2被中断。线程 1 回来了,此时,calendar 已然不是它所设的值,而是走上了线程 2 设置的值。结果就是线程1 输出了线程2 的时间,发生了线程安全问题线程不安全的原因之parse 方法

因为parse 方法的方法体比较长,我们先直接使用一下parse 方法,然后当抛出异常之后我们再去定位问题的原因

一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例,例如下面的代码

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void unSafeParse() throws InterruptedException {    ExecutorService service = Executors.newFixedThreadPool(100);    for (int i = 0; i < 20; i++) {        service.execute(()->{            for (int i1 = 0; i1 < 10; i1++) {                try {                    System.out.println(parse("2021-03-25 18:14:22"));                } catch (ParseException e) {                    e.printStackTrace();                }            }        });    }    // 等线程结束    service.shutdown();    service.awaitTermination(1, TimeUnit.HOURS); }public synchronized static Date parse(String strDate) throws ParseException {    return sdf.parse(strDate);}

运行结果

Exception in thread "pool-1-thread-4" java.lang.NumberFormatException: multiple points	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)	at java.lang.Double.parseDouble(Double.java:538)	at java.text.DigitList.getDouble(DigitList.java:169)	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)Thu Mar 25 18:14:22 CST 2021	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)	at java.text.DateFormat.parse(DateFormat.java:364)	at datastructure.time.DateFormatDemo.parse(DateFormatDemo.java:41)	at datastructure.time.DateFormatDemo.lambda$unSafe$0(DateFormatDemo.java:27)	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)

原因分析

我们看到代码出错了,这个代码在单线程的环境下完全没有问题,前面我们分析format 方法的时候说到了,calendar 对象在多线程的环境下被调用,导致其他线程format 的时候获取到了其他线程设置的时间。

我们看到parse 的代码实现中有这么一段,也用到了calendar 对象,并且我们parse 出来的parsedDate 就是在这里返回的,我们的parsedDate是calendar对象getTime方法的返回值

Date parsedDate;try {    parsedDate = calb.establish(calendar).getTime();    // If the year value is ambiguous,    // then the two-digit year == the default start year    if (ambiguousYear[0]) {        if (parsedDate.before(defaultCenturyStart)) {            parsedDate = calb.addYear(100).establish(calendar).getTime();        }    }}// An IllegalArgumentException will be thrown by Calendar.getTime()// if any fields are out of range, e.g., MONTH == 17.catch (IllegalArgumentException e) {    pos.errorIndex = start;    pos.index = oldStart;    return null;}return parsedDate;

我们看一下establish 方法是如何构建calendar的,我们看到这里有一个清除Calendar和对调用Calendar set 的方法

Calendar establish(Calendar cal) {    boolean weekDate = isSet(WEEK_YEAR)                        && field[WEEK_YEAR] > field[YEAR];    if (weekDate && !cal.isWeekDateSupported()) {        // Use YEAR instead        if (!isSet(YEAR)) {            set(YEAR, field[MAX_FIELD + WEEK_YEAR]);        }        weekDate = false;    }    cal.clear();    // Set the fields from the min stamp to the max stamp so that    // the field resolution works in the Calendar.    for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {        for (int index = 0; index <= maxFieldIndex; index++) {            if (field[index] == stamp) {                cal.set(index, field[MAX_FIELD + index]);                break;            }        }    }}    

其实到这里我们就大致知道了,这里的问题其实比format 的时候更严重,为什么这么说呢,因为这里这么多的线程不安全的方法的调用,只要有一个有问题,那就有问题了,例如另外一个线程刚要获取时间,然后一个线程执行了cal.clear(),那这个线程就获取不到时间了正确的时间了啊。

如何解决 SimpleDateFormat 的线程安全问题创建局部变量

这个的意思就很明显了,就是说我们不要创建这种全局变量,而是在使用的方法中去创建SimpleDateFormat的对象,因为我们的方法总是在线程中执行的,创建出来的局部变量就是线程私有的

public static void unSafeParse() throws InterruptedException {    ExecutorService service = Executors.newFixedThreadPool(100);    for (int i = 0; i < 20; i++) {        service.execute(() -> {            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");            for (int i1 = 0; i1 < 10; i1++) {                try {                                        Date date = sdf.parse("2021-03-25 18:14:22");                    System.out.println(date);                } catch (ParseException e) {                    e.printStackTrace();                }            }        });    }    // 等线程结束    service.shutdown();    service.awaitTermination(1, TimeUnit.HOURS);}
加锁

我们可以引入锁,来达到线程访问安全的目的,在这个例子中我们可以有多处可以加锁的地方,下面我们演示两种

给静态方法加锁

public static void unSafeParse() throws InterruptedException {    ExecutorService service = Executors.newFixedThreadPool(100);    for (int i = 0; i < 20; i++) {        service.execute(() -> {            for (int i1 = 0; i1 < 10; i1++) {                try {                    System.out.println(parse("2021-03-25 18:14:22"));                } catch (ParseException e) {                    e.printStackTrace();                }            }        });    }    // 等线程结束    service.shutdown();    service.awaitTermination(1, TimeUnit.HOURS);}public synchronized static Date parse(String strDate) throws ParseException {    return sdf.parse(strDate);}

代码段加锁

public static void unSafeParse() throws InterruptedException {    ExecutorService service = Executors.newFixedThreadPool(100);    for (int i = 0; i < 20; i++) {        service.execute(() -> {            for (int i1 = 0; i1 < 10; i1++) {                try {                    synchronized (sdf) {                        System.out.println(parse("2021-03-25 18:14:22"));                    }                } catch (ParseException e) {                    e.printStackTrace();                }            }        });    }    // 等线程结束    service.shutdown();    service.awaitTermination(1, TimeUnit.HOURS);}public static Date parse(String strDate) throws ParseException {    return sdf.parse(strDate);}
线程私有

上面的方案都可以解决SimpleDateFormat 线程安全的,有什么区别吗,加锁线程了多线程并发的性能,局部变量会创建大量的对象,虽然方法结束了就销毁了,不会影响GC,但是创建对象是有代价的啊,这之间有没有其他方案呢,你别说还真有一个,如果我们在方法里面的去通过局部变量的方式去规避SimpleDateFormat的线程安全问题,如果我们的方法在线程里面是多次调用,那么这个时候我们的这个线程就会多次创建这个对象。

其实我们知道线程安全问题,最终是以为临界资源引起的,所以我们只要保证每个线程都持有自己的SimpleDateFormat对象就可以保证线程安全的问题,而不是用说是非要在方法里面创建局部变量,就像下面的代码中,你可以在位置2 上创建SimpleDateFormat 对象也能保证线程安全

public static void unSafeParse() throws InterruptedException {    ExecutorService service = Executors.newFixedThreadPool(100);    for (int i = 0; i < 20; i++) {        service.execute(() -> {            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");            for (int i1 = 0; i1 < 10; i1++) {                try {                    // 位置 2                    Date date = sdf.parse("2021-03-25 18:14:22");                    System.out.println(date);                } catch (ParseException e) {                    e.printStackTrace();                }            }        });    }    // 等线程结束    service.shutdown();    service.awaitTermination(1, TimeUnit.HOURS);}

其实到这里我们就知道了,只要每个线程持有自己的SimpleDateFormat对象就可以了,为了达到这个目的,这个时候我们有两种实现方案,第一种是我们创建线程的时候,给每个线程一个SimpleDateFormat对象

线程的成员变量

public static void unSafeParse() throws InterruptedException {    ExecutorService service = Executors.newFixedThreadPool(100);    for (int i = 0; i < 20; i++) {        service.submit(new ParseTask());    }    // 等线程结束    service.shutdown();    service.awaitTermination(1, TimeUnit.HOURS);}static class ParseTask extends Thread {    public SimpleDateFormat sdf;    public ParseTask(SimpleDateFormat simpleDateFormat) {        sdf = simpleDateFormat;    }    public ParseTask() {        sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");    }    @Override    public void run() {        for (int i1 = 0; i1 < 10; i1++) {            try {                Date date = sdf.parse("2021-03-25 18:14:22");                System.out.println(date);            } catch (ParseException e) {                e.printStackTrace();            }        }    }}
ThreadLocal

当然为了实现线程私有,我们也可以使用ThreadLocal

public static void unSafeParse() throws InterruptedException {    final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();    ExecutorService service = Executors.newFixedThreadPool(100);    for (int i = 0; i < 20; i++) {        service.execute(() -> {            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");            threadLocal.set(sdf);            for (int i1 = 0; i1 < 10; i1++) {                try {                    threadLocal.get();                    Date date = sdf.parse("2021-03-25 18:14:22");                    System.out.println(date);                } catch (ParseException e) {                    e.printStackTrace();                }            }        });    }    // 等线程结束    service.shutdown();    service.awaitTermination(1, TimeUnit.HOURS);}
Calendar

为了方便时间的操作,Java 为我们提供了一个工具的类Calendar,可以方便的让我们对时间进行一些操作,例如时间的设置和获取,而且Calendar类实现了公历日历,你可以判断今年是闰年还是平年

Calendar 和 Date 的相互转换

Calendar cal = Calendar.getInstance();// Calendar转化为DateDate date = cal.getTime();// Date转化为Calendardate = new Date();cal.setTime(date);
获取特定的时间

我们经常会获取特定的时间单位下的值,例如年份,月份,一般情况下我们的做法都是将Date 转换成字符串,然后截取字符串,从而获取到我们想要的时间

int year = cal.get(Calendar.YEAR);//获取年份int month=cal.get(Calendar.MONTH);//获取月份int day=cal.get(Calendar.DATE);//获取日int hour=cal.get(Calendar.HOUR);//小时int minute=cal.get(Calendar.MINUTE);//分int second=cal.get(Calendar.SECOND);//秒int WeekOfYear = cal.get(Calendar.DAY_OF_WEEK);//一周的第几天

是不是一切看起来很美好,其实这是一种错觉,例如你将月份输出来看看,你就会发现它是从0 到 11 的,如果你想要获取正常的月份你需要加一,同理设置的时候你需要减一

设置和计算时间

cal.set(Calendar.MONDAY, 2);cal.set(Calendar.DATE, 30);// 2003-8-23  => 2004-2-23cal.add(Calendar.MONDAY, 5);
TimeZone

在地理上,地球被划分成24个时区,中国北京时间属于东八区,而程序中对时间的默认实现是以格林威治时间为标准的;这样就产生了8小时的时差。为了让程序更加通用,可以使用TimeZone设置程序中时间所属的时区,其中TimeZone就代表了时区。

TimeZone是一个抽象类,不能调用其构造器来创建实例,·但可以调用它的静态方法:getDefault()或getTimeZone()得到 Tiinezone 实例。其中 getDefault( )方法用于获得运行机器上的默认时区,默认时区可以通过修改操作系统的相关配置来进行调整:getTimeZone() 则根据时区ID来获取对应的时区。 Timezone类提供了一些有用的方法用于获取时区的相关信息

static String[] getAvailablelDs():获取Java所支持的所有时区ID。static Timezone getDefault():获取运行机器上默认的时区。String getDisplayName():获取该下TimeZone对象的时区名称。String getID():获取该时区的ID。static Timezone getTimeZone(String ID):获取指定ID对应的Timezone对象。Timezone 的基本操作

下面我们通过代码演示一下

public static void main(String[] args) {    String[] ids = TimeZone.getAvailableIDs();    System.out.println(Arrays.toString(ids));    TimeZone my = TimeZone.getDefault();    System.out.println(my.getID());    System.out.println(my.getDisplayName());    System.out.println(TimeZone.getTimeZone("Africa/Addis_Ababa").getDisplayName());}
TimeZone 和 Calendar

地球被划分成24个时区,所以每个时区的当前时间都是不一样的,所以我们一般情况下都是针对当前时区而言的。

public static void main(String[] args) {    Calendar calendar = Calendar.getInstance();    int hour=calendar.get(Calendar.HOUR_OF_DAY);    System.out.println(hour);    calendar.setTimeZone(TimeZone.getTimeZone("Africa/Asmera"));    hour=calendar.get(Calendar.HOUR_OF_DAY);    System.out.println(hour);}

输出

2116

例如对东八区而言,现在就是21点,但是对于"Africa/Asmera" 现在就是16点

而且Calendar.getInstance() 其实默认就是调用的实当前时区,当然我们可以在创建的时候传入时区信息

public static Calendar getInstance(){    return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));}public static Calendar getInstance(TimeZone zone){    return createCalendar(zone, Locale.getDefault(Locale.Category.FORMAT));}public static Calendar getInstance(Locale aLocale){    return createCalendar(TimeZone.getDefault(), aLocale);}
总结Java 中了提供的Date、Calendar、SimpleDateFormat 来供我们操作时间,Date 我们可以认识时间类,Calendar 是时间计算的util,SimpleDateFormat 是时间格式化的工具类,一起看起来那么美好,但是其实不然Calendar 里面有很多反人性的东西,SimpleDateFormat 不是线程安全的TimeZone 提供了时区相关的功能,其实这个主要是为了方便在一些对跨时区的数据进行处理的时候提供一些支持。

作者:刘不二

链接:

标签: #java毫秒数转时间 #java计算时间差返回负数 #java闰年平年