SimpleDateFormat

9/25/2022 Java

心灵鸡汤

人是从挫折当中去奋进,从怀念中向往未来,从疾病当中恢复健康,从无知当中变得文明,从极度苦恼当中勇敢救赎,不停的自我救赎,并尽可能的帮助他人。人之优势所在,是必须充满精力自我悔改自我反省自我成长;并非一味的向人抱怨

# 一:基本用法

SimpleDateFormat是Java提供的一个格式化和解析日期的工具类。它允许进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。

在Java中,可以使用SimpleDateFormat的format方法,将一个Date类型转化成String类型,并且可以指定输出格式。

// Date转String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);

// String转Data
System.out.println(sdf.parse(dataStr));
1
2
3
4
5
6
7
8

日期和时间模式表达方法

字母 日期或时间元素 表示 示例
G Era 标志符 Text AD
y Year 1996; 96
M 年中的月份 Month July; Jul; 07
w 年中的周数 Number 27
W 月份中的周数 Number 2
D 年中的天数 Number 189
d 月份中的天数 Number 10
F 月份中的星期 Number 2
E 星期中的天数 Text Tuesday; Tue
a Am/pm 标记 Text PM
H 一天中的小时数(0-23) Number 0
k 一天中的小时数(1-24) Number 24
K am/pm 中的小时数(0-11) Number 0
h am/pm 中的小时数(1-12) Number 12
m 小时中的分钟数 Number 30
s 分钟中的秒数 Number 55
S 毫秒数 Number 978
z 时区 General time zone Pacific Standard Time; PST; GMT-08:00
Z 时区 RFC 822 time zone -0800

时区

默认情况下,如果不指明,在创建日期的时候,会使用当前计算机所在的时区作为默认时区,这也是为什么我们通过只要使用 new Date() 就可以获取中国的当前时间的原因。

那么,如何在Java代码中获取不同时区的时间呢?SimpleDateFormat可以实现这个功能。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));
1
2
3

# 二:非线程安全

下面例子将演示使用SimpleDateFormat类在多线程环境下处理日期得出的结果却是错误的情况,这也是在多线程环境开发中容易遇到的问题。

public class SimpleDateFormatTest1 extends Thread {

    private SimpleDateFormat sdf;
    private String dateString;

    public SimpleDateFormatTest1(SimpleDateFormat sdf, String dateString) {
        super();
        this.sdf = sdf;
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date dateRef = sdf.parse(dateString);
            String newDateString = sdf.format(dateRef);
            if (!newDateString.equals(dateString)) {
                System.out.println("ThreadName=" + this.getName() + "报错了 日期字符串:" + dateString + " 转换成的日期为:" + newDateString);
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

        String[] dateStringArray = new String[]{"2000-01-01", "2000-01-02", "2000-01-03", "2000-01-04", "2000-01-05", "2000-01-06", "2000-01-07", "2000-01-08", "2000-01-09", "2000-01-10"};

        for (int i = 0; i < 10; i++) {
            new SimpleDateFormatTest1(sdf, dateStringArray[i]).start();
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

从控制台输出的结果来看,使用单例的SimpleDateFormat类在多线程环境中处理日期极易出现日期转换错误的情况。

# 2.1 原因

查看SimpleDateFormat类中format方法的实现其实就能发现端倪。

SimpleDateFormat中的format方法在执行过程中,会使用一个成员变量calendar来保存时间。这其实就是问题的关键。

由于我们在声明SimpleDateFormat临时变量的时候,多个线程共享同一个SimpleDateFormat实例,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。

假设线程1刚刚执行完 calendar.setTime 把时间设置成 2000-01-01,还没等执行完,线程2又执行了 calendar.setTime 把时间改成了2000-01-02。这时候线程1继续往下执行,拿到的 calendar.getTime 得到的时间就是线程2改过之后的。

除了format方法以外,SimpleDateFormat的parse方法也有同样的问题。所以,不要把SimpleDateFormat作为一个共享变量使用。

# 2.2 解决方式一

public class DateTools1 {

    public static Date parse(String formatPattern, String dateString) throws ParseException {
        return new SimpleDateFormat(formatPattern).parse(dateString);
    }

    public static String format(String formatPattern, Date date) {
        return new SimpleDateFormat(formatPattern).format(date).toString();
    }

}


public class SimpleDateFormatTest2 extends Thread {

    private String dateString;

    public SimpleDateFormatTest2(String dateString) {
        super();
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date dateRef = DateTools1.parse("yyyy-MM-dd", dateString);
            String newDateString = DateTools1.format("yyyy-MM-dd", dateRef);
            if (!newDateString.equals(dateString)) {
                System.out.println("ThreadName=" + this.getName() + "报错了 日期字符串:" + dateString + " 转换成的日期为:" + newDateString);
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        String[] dateStringArray = new String[]{"2000-01-01", "2000-01-02", "2000-01-03", "2000-01-04", "2000-01-05", "2000-01-06", "2000-01-07", "2000-01-08", "2000-01-09", "2000-01-10"};

        for (int i = 0; i < 10; i++) {
            new SimpleDateFormatTest2(dateStringArray[i]).start();
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

处理错误的原理其实就是创建多个SimpleDateFormat类的实例。

# 2.3 解决方式二

可以对需要使用到 SimpleDateFormat 格式化和解析的地方使用 synchronized 代码块

# 2.4 解决方式三

ThreadLocal类能使线程绑定到指定对象,使用该类也可以解决多线程环境下SimpleDateFormat类处理错误的情况。在阿里巴巴Java开发手册的第一章第六节——并发处理中关于这一点也有明确说明:

public class DateTools2 {

    private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();

    public static SimpleDateFormat getSimpleDateFormat(String datePattern) {
        SimpleDateFormat sdf = null;
        sdf = tl.get();
        if (sdf == null) {
            sdf = new SimpleDateFormat(datePattern);
            tl.set(sdf);
        }
        return sdf;
    }
    
}


public class SimpleDateFormatTest3 extends Thread {

    private String dateString;

    public SimpleDateFormatTest3(String dateString) {
        super();
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date dateRef = DateTools2.getSimpleDateFormat("yyyy-MM-dd").parse(dateString);
            String newDateString = DateTools2.getSimpleDateFormat("yyyy-MM-dd").format(dateRef);
            if (!newDateString.equals(dateString)) {
                System.out.println("ThreadName=" + this.getName() + "报错了 日期字符串:" + dateString + " 转换成的日期为:" + newDateString);
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        String[] dateStringArray = new String[]{"2000-01-01", "2000-01-02", "2000-01-03", "2000-01-04", "2000-01-05", "2000-01-06", "2000-01-07", "2000-01-08", "2000-01-09", "2000-01-10"};

        for (int i = 0; i < 10; i++) {
            new SimpleDateFormatTest3(dateStringArray[i]).start();
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

强烈推荐使用方式三

# 三:DateTimeFormatter

除了上述两种解决方式,如果使用JDK1.8应用,可以使用DateTimeFormatter代替SimpleDateFormat,这是一个线程安全的格式化工具类。可以只创建一个实例,到处引用。就像官方文档中说的,这个类 simple beautiful strong immutable thread-safe

# 3.1 常用 API

  • public static DateTimeFormatter ofPattern(String pattern):创建一个格式化程序使用指定的模式;
  • public static DateTimeFormatter ofPattern(String pattern, Locale locale):创建一个格式化程序使用指定的模式和现场;
  • public String format(TemporalAccessor temporal):使用此格式化程序格式的日期时间对象,TemporalAccessor 是一个接口,其实现类有 LocalDate、LocalTime、LocalDateTime、ZonedDateTime 等。
public class DateTimeFormatterTest {

    public static void main(String[] args) {
        //自定义输出格式
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        System.out.println(dtf.format(LocalDateTime.now()));    // 2022/09/25 21:35:56
        System.out.println("===================================");

        //自定义格式解析
        LocalDateTime localDateTime = LocalDateTime.parse("2001/07/27 22:22:22", dtf);
        System.out.println(localDateTime);  // 2001-07-27T22:22:22
        System.out.println("===================================");

        // 按照Locale默认习惯格式化
        ZonedDateTime zonedDateTime = ZonedDateTime.now();
        System.out.println(zonedDateTime);  // 2022-09-25T21:35:57.006+08:00[Asia/Shanghai]
        System.out.println("===================================");

        DateTimeFormatter formatter01 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ZZZZ");
        System.out.println(formatter01.format(zonedDateTime));  // 2022-09-25T21:35:GMT+08:00
        System.out.println("===================================");

        DateTimeFormatter formatter02 = DateTimeFormatter.ofPattern("yyyy MMM dd EE:HH:mm", Locale.CHINA);
        System.out.println(formatter02.format(zonedDateTime));  // 2022 九月 25 星期日:21:35
        System.out.println("===================================");

        DateTimeFormatter formatter03 = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
        System.out.println(formatter03.format(zonedDateTime));  // Sun, September/25/2022 21:35
        System.out.println("===================================");

        // 过DateTimeFormatter预定义静态变量
        LocalDateTime l2 = LocalDateTime.now();
        System.out.println(l2); // 2022-09-25T21:35:57.047
        System.out.println(DateTimeFormatter.ISO_DATE.format(l2));  // 2022-09-25
        System.out.println(DateTimeFormatter.ISO_DATE_TIME.format(l2)); // 2022-09-25T21:35:57.047
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

上面例子,有Java8新增的日期时间类,可以查看 时间日期

# 四:第三方类库

  1. 使用Apache commons 里的FastDateFormat,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行format, 不能对日期串进行解析;
  2. 使用Joda-Time类库来处理时间相关问题。

# 五:参考文献

最后更新: 9/26/2022, 2:17:23 PM