dream

一个菜鸟程序员的成长历程

0%

JAVA-LocalDateTime时间格式化,转换时间戳和源码分析

JAVA-LocalDateTime时间格式化,转换时间戳和源码分析

LocalDateTime

LocalDateTime作为java8新加的时间类型,也是后面开发中常用的时间类型。

作为时间类型,最关注的点无非是这几个

  • 获取当前时间
  • 获取指定时间
  • 时间格式化
  • 时间转时间戳
  • 时间戳转时间
  • 时间比较
  • 时间加减

这些点通过LocalDateTime来操作,都会比较简单

获取当前时间

只需要now一下就可以获取到当前的时间,还是很方便的。

1
LocalDateTime time = LocalDateTime.now();

再看一下之前的Date

1
Date date = new Date();

获取指定时间

这个有比较多的方式

  • 通过原来的datedateTime类型来生成
  • 通过传年月日时分秒生成
1
LocalDateTime time = LocalDateTime.of(2022,11,30,6,6,6);

原来Date类的方式。比较奇怪,他的年份会+1900,所以2022年就得是122,月份也会+1,所以11月就是10.但是这个方法呢后面会被删除,已经被标记为弃用了,使用Calendar代替。

1
Date date = new Date(122,10,30,6,6,6);

看一下Calendar的使用。这个年份就正常了,是2022,但是月份还是会+1.

1
2
Calendar calendar = Calendar.getInstance();
calendar.set(2022,10,30,6,6,6);

时间格式化

时间格式化都是通过format函数,需要传一个DateTimeFormatter对象进去,我们可以通过DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")来生成自己想要的格式。

DateTimeFormatter类里面也有一些定义好的格式可以直接用,除了下面列出的还有一些其他的,感兴趣可以看一下,不过我觉得都没啥用。

  • ISO_DATE_TIME 2011-12-03T10:15:30
  • ISO_OFFSET_DATE_TIME 2011-12-03T10:15:30+01:00
  • ISO_LOCAL_DATE_TIME 2011-12-03T10:15:30
1
time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

看一下Date的格式化。这个需要借用SimpleDateFormat类来完成格式化。

1
2
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
format.format(date);

时间转时间戳

时间转时间戳分为两种,一种是当你已经有一个LocalDateTime类型的时间了,需要转换成秒或者毫秒的时间戳。

时间转换秒级时间戳

只需要直接用toEpochSecond方法就可以了。

1
2
LocalDateTime time = LocalDateTime.now();
time.toEpochSecond(ZoneOffset.ofHours(8));

Date类型没有办法直接获取秒级时间戳,只能获取毫秒级再转秒。

时间转换毫秒级时间戳

转换毫秒需要先转换成instant对象,然后才能转换成毫秒级时间戳。

1
2
LocalDateTime time = LocalDateTime.now();
time.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();

Date获取毫秒就很简单了。

1
2
Date date = new Date();
date.getTime();

字符串转换成时间戳

时间转时间戳分为两种,除了上面的,还有一种是有一个格式化好的字符串,比如2022-12-18 10:00:00这种,但是这个是字符串并不是时间类型。所以要先转换成LocalDateTime类型,然后就可以转换成时间戳了。

其实就是上面格式化的一种反向操作。使用parse方法就可以了。

1
2
LocalDateTime.parse("2022-12-18 10:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
LocalDateTime.parse("2022-12-18", DateTimeFormatter.ofPattern("yyyy-MM-dd"));

Date类型的字符串转时间戳也是通过SimpleDateFormat类来完成。

1
2
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = format.parse("2022-12-18 10:00:00")

时间戳转时间

那如果我们现在转换成时间戳以后又想转换成时间呢?也可以通过instant对象来做到。

毫秒时间戳转时间

Instant.ofEpochSecond(1671365543834)是将一个毫秒时间戳转换成一个instant对象。在通过ofInstant方法就可以将instant对象转换成LocalDateTime对象了。

1
LocalDateTime.ofInstant(Instant.ofEpochSecond(1671365543834), ZoneOffset.ofHours(8));

Date

1
Date date = new Date(1669759566000L);

秒时间戳转时间

Instant.ofEpochMilli(1671365543)是将一个秒时间戳转换成instant对象。和上面的区别就是使用的是ofEpochMilli方法。

1
LocalDateTime.ofInstant(Instant.ofEpochMilli(1671365543), ZoneOffset.ofHours(8));

Date类不支持秒,只能把秒转成毫秒在转时间戳。

时间比较

通过compareTo方法可以进行时间的一个比较大小。如果大于会返回1,小于返回-1.

1
2
LocalDateTime time = LocalDateTime.now();
time.compareTo(LocalDateTime.now());

Date也是通过compareTo方法进行比较

1
2
Date date = new Date(1669759566000L);
date.compareTo(new Date());

时间加减

如果加上几天,就是plusDays。加几个小时就是plusHours。当然也可以使用plus

1
2
3
4
LocalDateTime time = LocalDateTime.now();
time.plusDays(1);
time.plusHours(1);
time.plus(Period.ofDays(1));

如果减去几天就是minusDays.减去几个小时就是minusHours。当然也可以使用minus

1
2
3
4
LocalDateTime time = LocalDateTime.now();
time.minusDays(1);
time.minusHours(1);
time.minus(Period.ofDays(1));

Date类不支持时间加减,只能通过Calendar类实现。

1
2
3
4
5
6
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, 1);
//减去
calendar.add(Calendar.DAY_OF_MONTH, -1);

时间格式在入参出参中的使用

入参的时候需要通过JsonFormat注解来指定需要的是字符串类型和对应的时间格式。

1
2
3
4
5
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")
private LocalDate date;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
private LocalDateTime time;

出参的时候就很简单了,因为只是返回了一个字符串。

1
private String time;

格式化时间源码分析

格式化的时候这两个年是不一样的,具体的可以看一下源码。我们来找一下。

首先点进去是LocalDateTime这个类里面

1
2
3
4
5
6
7
@Override  // override for Javadoc and performance
public String format(DateTimeFormatter formatter) {
//判断参数是否空
Objects.requireNonNull(formatter, "formatter");
//真正的执行格式化
return formatter.format(this);
}

接下来点进去,看一下怎么执行的,可以看到又调用了formatTo这个函数,说明主要的格式化代码都在这里面。

1
2
3
4
5
6
7
8
public String format(TemporalAccessor temporal) {
//创建了一个32长度的字符串构建器
StringBuilder buf = new StringBuilder(32);
//格式化主要代码
formatTo(temporal, buf);
//转成字符串
return buf.toString();
}

看一下formatTo函数,可以发现主要是调用printerParser这个对象的format方法,那我们这个对象是哪来的呢,是在一开始指定格式化类型的时候来的。不同的格式化类型对应不同的解析器,也就会执行不同的format方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void formatTo(TemporalAccessor temporal, Appendable appendable) {
//判断参数,这里不用管
Objects.requireNonNull(temporal, "temporal");
Objects.requireNonNull(appendable, "appendable");
try {
//创建一个DateTimePrintContext对象
DateTimePrintContext context = new DateTimePrintContext(temporal, this);
//判断,显然我们之前传过来的就是一个StringBuilder
if (appendable instanceof StringBuilder) {
//主要看这个怎么处理 这里有个 printerParser 对象,这个对象是怎么来的呢,其实是上面DateTimeFormatter.ofPattern的时候给创建的。
printerParser.format(context, (StringBuilder) appendable);
} else {
//这里其实就是如果传的不是个StringBuilder,就在创建一个然后执行
// buffer output to avoid writing to appendable in case of error
StringBuilder buf = new StringBuilder(32);
printerParser.format(context, buf);
appendable.append(buf);
}
} catch (IOException ex) {
throw new DateTimeException(ex.getMessage(), ex);
}
}

接下来我们看一下ofPattern这个方法里面是怎样的吧。这里是创建了一个 时间格式化的建造者,然后把我们这个字符串添加进去了。

1
2
3
4
//这里的字符串就是我们传的 yyyy-MM-dd HH:mm:ss
public static DateTimeFormatter ofPattern(String pattern) {
return new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter();
}

看一下appendPattern里面是怎么把字符串加进去的。

1
2
3
4
5
6
7
public DateTimeFormatterBuilder appendPattern(String pattern) {
//忽略
Objects.requireNonNull(pattern, "pattern");
//主要的解析逻辑
parsePattern(pattern);
return this;
}

继续追踪到parsePattern方法里面。这个方法代码比较多,这里只关注我们想知道的。其余的有兴趣的可以看一下。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
private void parsePattern(String pattern) {
//这里给字符串做循环,注意 pattern = yyyy-MM-dd HH:mm:ss 这个字符串。
for (int pos = 0; pos < pattern.length(); pos++) {
//取出字符 比如第一个就是 y 对应的ASCII码就是121
char cur = pattern.charAt(pos);
//这里就是判断是否是大小写字母了,也就是A-Z或者a-z
if ((cur >= 'A' && cur <= 'Z') || (cur >= 'a' && cur <= 'z')) {
//初始化变量 start = 0 pos = 1
int start = pos++;
//这里做一个循环,目的其实就是找出相同的字符有几个,比如y有4个,pos就会变成4
for ( ; pos < pattern.length() && pattern.charAt(pos) == cur; pos++); // short loop
//这里就是算出具体的数量 4 - 0 = 4
int count = pos - start;
// padding 这里忽略 我们这里面没有这个字符
if (cur == 'p') {
int pad = 0;
if (pos < pattern.length()) {
cur = pattern.charAt(pos);
if ((cur >= 'A' && cur <= 'Z') || (cur >= 'a' && cur <= 'z')) {
pad = count;
start = pos++;
for ( ; pos < pattern.length() && pattern.charAt(pos) == cur; pos++); // short loop
count = pos - start;
}
}
if (pad == 0) {
throw new IllegalArgumentException(
"Pad letter 'p' must be followed by valid pad pattern: " + pattern);
}
padNext(pad); // pad and continue parsing
}
//接下来是主要逻辑。
// main rules
//从hashMap里面取出对应的值,这个map放在下面了。y取出来就是 YEAR_OF_ERA
TemporalField field = FIELD_MAP.get(cur);
//判断map里面取出来的是否为空,如果不为空就直接解析,如果为空就接着往下走,看是不是 zvZOXxWwY 这几个,如果都不是就会报错了
if (field != null) {
//我们y是能取出来的,直接解析这里 cur = y, count = 4, field = YEAR_OF_ERA
parseField(cur, count, field);
} else if (cur == 'z') {
if (count > 4) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
} else if (count == 4) {
appendZoneText(TextStyle.FULL);
} else {
appendZoneText(TextStyle.SHORT);
}
} else if (cur == 'V') {
if (count != 2) {
throw new IllegalArgumentException("Pattern letter count must be 2: " + cur);
}
appendZoneId();
} else if (cur == 'Z') {
if (count < 4) {
appendOffset("+HHMM", "+0000");
} else if (count == 4) {
appendLocalizedOffset(TextStyle.FULL);
} else if (count == 5) {
appendOffset("+HH:MM:ss","Z");
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
} else if (cur == 'O') {
if (count == 1) {
appendLocalizedOffset(TextStyle.SHORT);
} else if (count == 4) {
appendLocalizedOffset(TextStyle.FULL);
} else {
throw new IllegalArgumentException("Pattern letter count must be 1 or 4: " + cur);
}
} else if (cur == 'X') {
if (count > 5) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
appendOffset(OffsetIdPrinterParser.PATTERNS[count + (count == 1 ? 0 : 1)], "Z");
} else if (cur == 'x') {
if (count > 5) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
String zero = (count == 1 ? "+00" : (count % 2 == 0 ? "+0000" : "+00:00"));
appendOffset(OffsetIdPrinterParser.PATTERNS[count + (count == 1 ? 0 : 1)], zero);
} else if (cur == 'W') {
// Fields defined by Locale
if (count > 1) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
appendInternal(new WeekBasedFieldPrinterParser(cur, count));
} else if (cur == 'w') {
// Fields defined by Locale
if (count > 2) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
appendInternal(new WeekBasedFieldPrinterParser(cur, count));
} else if (cur == 'Y') {
// Fields defined by Locale
appendInternal(new WeekBasedFieldPrinterParser(cur, count));
} else {
throw new IllegalArgumentException("Unknown pattern letter: " + cur);
}
pos--;

} else if (cur == '\'') {
// parse literals
int start = pos++;
for ( ; pos < pattern.length(); pos++) {
if (pattern.charAt(pos) == '\'') {
if (pos + 1 < pattern.length() && pattern.charAt(pos + 1) == '\'') {
pos++;
} else {
break; // end of literal
}
}
}
if (pos >= pattern.length()) {
throw new IllegalArgumentException("Pattern ends with an incomplete string literal: " + pattern);
}
String str = pattern.substring(start + 1, pos);
if (str.length() == 0) {
appendLiteral('\'');
} else {
appendLiteral(str.replace("''", "'"));
}

} else if (cur == '[') {
optionalStart();

} else if (cur == ']') {
if (active.parent == null) {
throw new IllegalArgumentException("Pattern invalid as it contains ] without previous [");
}
optionalEnd();

} else if (cur == '{' || cur == '}' || cur == '#') {
throw new IllegalArgumentException("Pattern includes reserved character: '" + cur + "'");
} else {
// - : 这两个符号就会走到这里了
appendLiteral(cur);
}
}
}

看一下通过不同的key取值的map

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
//时代
FIELD_MAP.put('G', ChronoField.ERA); // SDF, LDML (different to both for 1/2 chars)
//这个时代的年份,也就是我们常用的年份yyyy
FIELD_MAP.put('y', ChronoField.YEAR_OF_ERA); // SDF, LDML
//单纯的年份
FIELD_MAP.put('u', ChronoField.YEAR); // LDML (different in SDF)
FIELD_MAP.put('Q', IsoFields.QUARTER_OF_YEAR); // LDML (removed quarter from 310)
FIELD_MAP.put('q', IsoFields.QUARTER_OF_YEAR); // LDML (stand-alone)
//一年里面的月份,也是我们常用的月份 MM
FIELD_MAP.put('M', ChronoField.MONTH_OF_YEAR); // SDF, LDML
FIELD_MAP.put('L', ChronoField.MONTH_OF_YEAR); // SDF, LDML (stand-alone)
//一年里面的天,我们基本不用这个作为日子
FIELD_MAP.put('D', ChronoField.DAY_OF_YEAR); // SDF, LDML
//一个月里面的天,我们常用这个获取多少号
FIELD_MAP.put('d', ChronoField.DAY_OF_MONTH); // SDF, LDML
FIELD_MAP.put('F', ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH); // SDF, LDML
FIELD_MAP.put('E', ChronoField.DAY_OF_WEEK); // SDF, LDML (different to both for 1/2 chars)
FIELD_MAP.put('c', ChronoField.DAY_OF_WEEK); // LDML (stand-alone)
FIELD_MAP.put('e', ChronoField.DAY_OF_WEEK); // LDML (needs localized week number)
FIELD_MAP.put('a', ChronoField.AMPM_OF_DAY); // SDF, LDML
//一天里面的小时,常用的小时 HH
FIELD_MAP.put('H', ChronoField.HOUR_OF_DAY); // SDF, LDML
FIELD_MAP.put('k', ChronoField.CLOCK_HOUR_OF_DAY); // SDF, LDML
FIELD_MAP.put('K', ChronoField.HOUR_OF_AMPM); // SDF, LDML
FIELD_MAP.put('h', ChronoField.CLOCK_HOUR_OF_AMPM); // SDF, LDML
//一个小时里面的分钟,常用的分钟 mm
FIELD_MAP.put('m', ChronoField.MINUTE_OF_HOUR); // SDF, LDML
//一分钟里面的秒数,常用的秒数 ss
FIELD_MAP.put('s', ChronoField.SECOND_OF_MINUTE); // SDF, LDML
//这个大S基本不用,这是秒里面的纳秒
FIELD_MAP.put('S', ChronoField.NANO_OF_SECOND); // LDML (SDF uses milli-of-second number)
FIELD_MAP.put('A', ChronoField.MILLI_OF_DAY); // LDML
FIELD_MAP.put('n', ChronoField.NANO_OF_SECOND); // 310 (proposed for LDML)
FIELD_MAP.put('N', ChronoField.NANO_OF_DAY); // 310 (proposed for LDML)

继续深入,直接解析y的方法parseField。可以看到这个是根据我们格式化的字母执行不同的代码,比如u,y都执行到一个代码块。4个y走到了appendValue方法里面。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
private void parseField(char cur, int count, TemporalField field) {
boolean standalone = false;
switch (cur) {
case 'u':
case 'y':
//判断数量
if (count == 2) {
//yy走这里
appendValueReduced(field, 2, 2, ReducedPrinterParser.BASE_DATE);
} else if (count < 4) {
//y or yyy走这里
appendValue(field, count, 19, SignStyle.NORMAL);
} else {
// yyyy走这里 field = YEAR_OF_ERA count = 4
appendValue(field, count, 19, SignStyle.EXCEEDS_PAD);
}
break;
case 'c':
if (count == 2) {
throw new IllegalArgumentException("Invalid pattern \"cc\"");
}
/*fallthrough*/
case 'L':
case 'q':
standalone = true;
/*fallthrough*/
case 'M':
case 'Q':
case 'E':
case 'e':
switch (count) {
case 1:
case 2:
//两个MM输出月份走到这里
if (cur == 'c' || cur == 'e') {
appendInternal(new WeekBasedFieldPrinterParser(cur, count));
} else if (cur == 'E') {
appendText(field, TextStyle.SHORT);
} else {
if (count == 1) {
appendValue(field);
} else {
//经过判断走到这里
appendValue(field, 2);
}
}
break;
case 3:
appendText(field, standalone ? TextStyle.SHORT_STANDALONE : TextStyle.SHORT);
break;
case 4:
appendText(field, standalone ? TextStyle.FULL_STANDALONE : TextStyle.FULL);
break;
case 5:
appendText(field, standalone ? TextStyle.NARROW_STANDALONE : TextStyle.NARROW);
break;
default:
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'a':
if (count == 1) {
appendText(field, TextStyle.SHORT);
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'G':
switch (count) {
case 1:
case 2:
case 3:
appendText(field, TextStyle.SHORT);
break;
case 4:
appendText(field, TextStyle.FULL);
break;
case 5:
appendText(field, TextStyle.NARROW);
break;
default:
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'S':
appendFraction(NANO_OF_SECOND, count, count, false);
break;
case 'F':
if (count == 1) {
appendValue(field);
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'd':
case 'h':
case 'H':
case 'k':
case 'K':
case 'm':
case 's':
if (count == 1) {
appendValue(field);
} else if (count == 2) {
//可以看到dd HH mm ss也是走到这里,最终也是通过NumberPrinterParser这个对象来格式化
appendValue(field, count);
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'D':
if (count == 1) {
appendValue(field);
} else if (count <= 3) {
appendValue(field, count);
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
default:
if (count == 1) {
appendValue(field);
} else {
appendValue(field, count);
}
break;
}
}

看一下appendValue方法。field = YEAR_OF_ERA,minWidth = 4, maxWidth = 19, signStyle = SignStyle.EXCEEDS_PAD。前面是一些判断,重点是创建了一个NumberPrinterParser的对象。最后转换的时候其实就是通过这个对象来转换的。

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
public DateTimeFormatterBuilder appendValue(
TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) {
//这里不执行 忽略
if (minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE) {
return appendValue(field, maxWidth);
}
//参数校验
Objects.requireNonNull(field, "field");
Objects.requireNonNull(signStyle, "signStyle");
//一些校验规则
if (minWidth < 1 || minWidth > 19) {
throw new IllegalArgumentException("The minimum width must be from 1 to 19 inclusive but was " + minWidth);
}
if (maxWidth < 1 || maxWidth > 19) {
throw new IllegalArgumentException("The maximum width must be from 1 to 19 inclusive but was " + maxWidth);
}
if (maxWidth < minWidth) {
throw new IllegalArgumentException("The maximum width must exceed or equal the minimum width but " +
maxWidth + " < " + minWidth);
}
//重点是这里,创建了一个 NumberPrinterParser的对象,把参数传进去了。
NumberPrinterParser pp = new NumberPrinterParser(field, minWidth, maxWidth, signStyle);
appendValue(pp);
return this;
}

看一下NumberPrinterParser类。还记得最开始格式化的时候那一段代码printerParser.format(context, (StringBuilder) appendable);吗,实际调用的就是这里。?

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

//构造方法赋值
NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) {
// validated by caller
this.field = field;
this.minWidth = minWidth;
this.maxWidth = maxWidth;
this.signStyle = signStyle;
this.subsequentWidth = 0;
}

//格式化方法
@Override
public boolean format(DateTimePrintContext context, StringBuilder buf) {
//从context上下文中获取字段 field = YEAR_OF_ERA context实际包含了真正的时间 2022-12-01T00:00:00
Long valueLong = context.getValue(field);
if (valueLong == null) {
return false;
}
//获取到以后 value = 2022
long value = getValue(context, valueLong);
DecimalStyle decimalStyle = context.getDecimalStyle();
String str = (value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value)));
if (str.length() > maxWidth) {
throw new DateTimeException("Field " + field +
" cannot be printed as the value " + value +
" exceeds the maximum print width of " + maxWidth);
}
//转换一个格式类型
str = decimalStyle.convertNumberToI18N(str);

//这些条件都不满足,忽略
if (value >= 0) {
switch (signStyle) {
case EXCEEDS_PAD:
if (minWidth < 19 && value >= EXCEED_POINTS[minWidth]) {
buf.append(decimalStyle.getPositiveSign());
}
break;
case ALWAYS:
buf.append(decimalStyle.getPositiveSign());
break;
}
} else {
switch (signStyle) {
case NORMAL:
case EXCEEDS_PAD:
case ALWAYS:
buf.append(decimalStyle.getNegativeSign());
break;
case NOT_NEGATIVE:
throw new DateTimeException("Field " + field +
" cannot be printed as the value " + value +
" cannot be negative according to the SignStyle");
}
}
//填充0 也就是yyyy minWidth = 4就会填充0 MM minWidth = 2如果 1月就会填充01,一个M就不会走到循环填充0
for (int i = 0; i < minWidth - str.length(); i++) {
buf.append(decimalStyle.getZeroDigit());
}
//输出到buf中
buf.append(str);
return true;
}

可以看到上面的代码,但是NumberPrinterParser其实只是解析了yMdHms这些格式的。也可以再看一下M的确认一下。

首先是appendValue这个方法。大差不差,除了传到解析器的参数不一样,没啥区别,其实dd这些也都一样。

1
2
3
4
5
6
7
8
9
10
11
public DateTimeFormatterBuilder appendValue(TemporalField field, int width) {
//参数校验
Objects.requireNonNull(field, "field");
if (width < 1 || width > 19) {
throw new IllegalArgumentException("The width must be from 1 to 19 inclusive but was " + width);
}
//可以发现MM也是用的yyyy这个解析器格式化的,但是后面三个参数不一样
NumberPrinterParser pp = new NumberPrinterParser(field, width, width, SignStyle.NOT_NEGATIVE);
appendValue(pp);
return this;
}

那我们-,:这些格式化符号的输出呢?是通过另外一个解析器,它先是取到char类型的一个字符来判断的时候会走到else里面然后走appendLiteral(cur);这个方法。看一下这个方法里面。这里可以看到主要使用的是 CharLiteralPrinterParser 这个解析器。

1
2
3
4
5
public DateTimeFormatterBuilder appendLiteral(char literal) {
//这里可以看到主要使用的是 CharLiteralPrinterParser 这个解析器
appendInternal(new CharLiteralPrinterParser(literal));
return this;
}

看一下 CharLiteralPrinterParser 这个解析器

1
2
3
4
5
6
7
8
9
10
11
//构造方法
CharLiteralPrinterParser(char literal) {
this.literal = literal;
}

@Override
public boolean format(DateTimePrintContext context, StringBuilder buf) {
//简单粗暴 直接把 - : 这种符号添加到字符串里面
buf.append(literal);
return true;
}

接下来看一下为啥我们刚才上面说的,y代表 YEAR_OF_ERA,为啥就能从2022-12-01里面取到2022呢?这个可以看到我们NumberPrinterParser这个解析器里面主要调用了一个context.getValue(field)方法。

主要是temporal.getLong(field)方法,其实temporal就是我们的日期时间,在我们一开始创建上下文的时候过来的。回忆一下上面的创建。这里的temporal可以再往上一层传过来的,传的其实就是LocalDateTime的对象

new DateTimePrintContext(temporal, this)

1
2
3
4
5
6
7
8
9
10
11
Long getValue(TemporalField field) {
try {
//主要是这里,其实temporal就是我们的日期时间,在我们一开始创建上下文的时候过来的。
return temporal.getLong(field);
} catch (DateTimeException ex) {
if (optional > 0) {
return null;
}
throw ex;
}
}

所以我们再看一下getLong方法。可以看到有一个类型判断,yMdHms这几个类型就会走到if里面,如果是时间的 Hms这几个调用time.getLong方法,yMd日期的调用日期的getLong方法。Y的话就会走到 getFrom 这个方法。而且是通过field调用。

1
2
3
4
5
6
7
8
9
10
11
@Override
public long getLong(TemporalField field) {
//类型判断 yMdHms这几个走的这里面
if (field instanceof ChronoField) {
ChronoField f = (ChronoField) field;
//如果是时间的 Hms这几个调用time.getLong方法,yMd日期的调用日期的getLong方法
return (f.isTimeBased() ? time.getLong(field) : date.getLong(field));
}
//Y走这个方法
return field.getFrom(this);
}

看一下getFrom方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public long getFrom(TemporalAccessor temporal) {
if (rangeUnit == WEEKS) { // day-of-week
return localizedDayOfWeek(temporal);
} else if (rangeUnit == MONTHS) { // week-of-month
return localizedWeekOfMonth(temporal);
} else if (rangeUnit == YEARS) { // week-of-year
return localizedWeekOfYear(temporal);
} else if (rangeUnit == WEEK_BASED_YEARS) {
return localizedWeekOfWeekBasedYear(temporal);
} else if (rangeUnit == FOREVER) {
// YYYY 大写的Y走的是这里
return localizedWeekBasedYear(temporal);
} else {
throw new IllegalStateException("unreachable, rangeUnit: " + rangeUnit + ", this: " + this);
}
}

如果大写的Y格式化就会走下面的函数,主要就是取出年份以后计算周数,如果周数=0就认为是上一年的,年份-1,如果周数大于等于下一年的周数就年份+1

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
private int localizedWeekBasedYear(TemporalAccessor temporal) {
//获取到这周的第几天 第5天
int dow = localizedDayOfWeek(temporal);
//获取日期中的年份 2021
int year = temporal.get(YEAR);
//获取今年的第几天 2021-12-30 是 364天
int doy = temporal.get(DAY_OF_YEAR);
//这周开始的偏移量 5
int offset = startOfWeekOffset(doy, dow);
//今年的第几周 53周
int week = computeWeek(offset, doy);
//如果这周是0周,就是上一年的,年份就-1
if (week == 0) {
// Day is in end of week of previous year; return the previous year
return year - 1;
} else {
//如果接近年底,使用更高精度的逻辑
//检查 如果年份的日期包含在下一年的部分的星期里面了
// If getting close to end of year, use higher precision logic
// Check if date of year is in partial week associated with next year
//获取一年里面的天数 对象里面包含 最小1天 - 最大365天
ValueRange dayRange = temporal.range(DAY_OF_YEAR);
//获取到年份的长度,也就是365
int yearLen = (int)dayRange.getMaximum();
//下一年的周数 根据下面的计算公式得出 (7 + 5 + 366 - 1) / 7 = 53
//这里为啥是366呢,因为yearLen是今年的天数也就是365 + 1,其实也就是到下一年去了。为的是计算下一年的第一周
int newYearWeek = computeWeek(offset, yearLen + weekDef.getMinimalDaysInFirstWeek());
//比较如果今年的这周大于等于下一年的周 就年份 +1 所以这里格式化就会出错了。
if (week >= newYearWeek) {
return year + 1;
}
}
return year;
}

那么是怎么计算今年的第几周的呢,看一下computeWeek方法。其实就是一个计算公式。

1
2
3
4
5
//offset = 5 , day = 今年的第几天 364 天
private int computeWeek(int offset, int day) {
//计算公式 ( 7 + 5 + (364 - 1)) / 7
return ((7 + offset + (day - 1)) / 7);
}

还有一个问题,就是我们用到了一个周的偏移量,这个偏移量怎么计算的呢,看一下这个方法startOfWeekOffset。以2021-12-30为例,day = 364,dow = 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int startOfWeekOffset(int day, int dow) {
// offset of first day corresponding to the day of week in first 7 days (zero origin)
//算出上一周 (364 - 5) % 7 = 2
int weekStart = Math.floorMod(day - dow, 7);
// offset = -2
int offset = -weekStart;
//这里 2 + 1 > 1会走进去
if (weekStart + 1 > weekDef.getMinimalDaysInFirstWeek()) {
// The previous week has the minimum days in the current month to be a 'week'
//这里 7 - 2 = 5 返回的就是5
offset = 7 - weekStart;
}
return offset;
}

上面看完了大写的Y,再来看一下小写的y。走的getLong方法。

日期的getLong方法。经过判断后主要看get0这个方法。可以看到这个命名就很随意了。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public long getLong(TemporalField field) {
//这个判断也会走进来
if (field instanceof ChronoField) {
//这两个判断忽略
if (field == EPOCH_DAY) {
return toEpochDay();
}
if (field == PROLEPTIC_MONTH) {
return getProlepticMonth();
}
//走到这里
return get0(field);
}
return field.getFrom(this);
}

看一下日期的get0方法。可以发现了,这里主要处理了这几种类型。我们常用的

  • y也就是YEAR_OF_ERA 处理很简单,判断了一下year >= 1就返回 year。
  • M也就是MONTH_OF_YEAR 处理很简单,返回日期的month.
  • d也就是DAY_OF_MONTH 返回日期的day.

从这里也可以看出我们格式化成YEARERA作为年其实也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int get0(TemporalField field) {
switch ((ChronoField) field) {
case DAY_OF_WEEK: return getDayOfWeek().getValue();
case ALIGNED_DAY_OF_WEEK_IN_MONTH: return ((day - 1) % 7) + 1;
case ALIGNED_DAY_OF_WEEK_IN_YEAR: return ((getDayOfYear() - 1) % 7) + 1;
case DAY_OF_MONTH: return day;
case DAY_OF_YEAR: return getDayOfYear();
case EPOCH_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'EpochDay' for get() method, use getLong() instead");
case ALIGNED_WEEK_OF_MONTH: return ((day - 1) / 7) + 1;
case ALIGNED_WEEK_OF_YEAR: return ((getDayOfYear() - 1) / 7) + 1;
case MONTH_OF_YEAR: return month;
case PROLEPTIC_MONTH: throw new UnsupportedTemporalTypeException("Invalid field 'ProlepticMonth' for get() method, use getLong() instead");
case YEAR_OF_ERA: return (year >= 1 ? year : 1 - year);
case YEAR: return year;
case ERA: return (year >= 1 ? 1 : 0);
}
throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
}

看完了日期的处理再看一下时间的吧,其实大同小异了。

时间的getLong方法。同样的经过判断走到get0里面,注意这是时间的getLongget0

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public long getLong(TemporalField field) {
if (field instanceof ChronoField) {
if (field == NANO_OF_DAY) {
return toNanoOfDay();
}
if (field == MICRO_OF_DAY) {
return toNanoOfDay() / 1000;
}
return get0(field);
}
return field.getFrom(this);
}

时间的get0方法。处理的就是这些类型了。主要看我们关注的几个

  • H 也就是 HOUR_OF_DAY, 直接返回时间的 hour
  • m 也就是MINUTE_OF_HOUR,直接返回时间的 minute
  • s 也就是 SECOND_OF_MINUTE, 直接返回时间的 second
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private int get0(TemporalField field) {
switch ((ChronoField) field) {
case NANO_OF_SECOND: return nano;
case NANO_OF_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'NanoOfDay' for get() method, use getLong() instead");
case MICRO_OF_SECOND: return nano / 1000;
case MICRO_OF_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'MicroOfDay' for get() method, use getLong() instead");
case MILLI_OF_SECOND: return nano / 1000_000;
case MILLI_OF_DAY: return (int) (toNanoOfDay() / 1000_000);
case SECOND_OF_MINUTE: return second;
case SECOND_OF_DAY: return toSecondOfDay();
case MINUTE_OF_HOUR: return minute;
case MINUTE_OF_DAY: return hour * 60 + minute;
case HOUR_OF_AMPM: return hour % 12;
case CLOCK_HOUR_OF_AMPM: int ham = hour % 12; return (ham % 12 == 0 ? 12 : ham);
case HOUR_OF_DAY: return hour;
case CLOCK_HOUR_OF_DAY: return (hour == 0 ? 24 : hour);
case AMPM_OF_DAY: return hour / 12;
}
throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
}

总结

好了,到这里我们知道了时间格式的各种使用方法和格式化的源码。

对于不同格式化的区别。总结一下。

  • y 处理简单,只是判断了year > 1 就返回了year。
  • Y 处理较复杂,还判断了周,根据情况对年份+1和-1。某些年份的某些日期会有坑。一定要注意!!!
  • Md Hms处理非常简单,直接返回了日期时间上面对应的数。
  • -: 一些特殊字符,格式化的时候是直接增加到字符串里面的。

下面总结一下源码对应文件和方法的追踪链。感兴趣的可以自己在多翻翻源码。

ofPattern指定格式的调用链

  • DateTimeFormatter.java -> public static DateTimeFormatter ofPattern(String pattern)
    • DateTimeFormatterBuilder.java -> public DateTimeFormatterBuilder appendPattern(String pattern)
    • DateTimeFormatterBuilder.java -> private void parsePattern(String pattern)
    • DateTimeFormatterBuilder.java -> private void parseField(char cur, int count, TemporalField field)
    • DateTimeFormatterBuilder.java -> public DateTimeFormatterBuilder appendValue(TemporalField field, int width)
    • 在这里创建的解析器
    • DateTimeFormatterBuilder.java -> static class NumberPrinterParser implements DateTimePrinterParser
    • DateTimeFormatterBuilder.java -> static final class CharLiteralPrinterParser implements DateTimePrinterParser

format方法调用链

  • LocalDateTime.java -> public String format(DateTimeFormatter formatter)
    • DateTimeFormatter.java -> public String format(TemporalAccessor temporal)
    • DateTimeFormatter.java -> public void formatTo(TemporalAccessor temporal, Appendable appendable)
      • 接下来根据不同的处理解析器进行处理,主要有两个解析器
      • DateTimeFormatterBuilder.java -> static class NumberPrinterParser implements DateTimePrinterParser
      • DateTimeFormatterBuilder.java -> static final class CharLiteralPrinterParser implements DateTimePrinterParser
        • DateTimePrintContext.java -> Long getValue(TemporalField field)
          • LocalDateTime.java -> public long getLong(TemporalField field)
            • 这里日期调日期的 LocalDate.java -> public long getLong(TemporalField field)
            • LocalDate.java -> private int get0(TemporalField field)
            • 时间调时间的 LocalTime.java -> public long getLong(TemporalField field)
            • LocalTime.java -> private int get0(TemporalField field)