日期与时间

计算机通过Locale来针对当地用户习惯格式化日期、时间、数字、货币等。

Locale语言_国家的字母缩写构成,例如,zh_CN表示中文+中国,en_US表示英文+美国。语言使用小写,国家使用大写。

对于日期来说,不同的Locale,例如,中国和美国的表示方式如下:

  • zh_CN:2016-11-30
  • en_US:11/30/2016

Java有两套日期和时间的API:

  • 旧的Date、Calendar和TimeZone;
  • 新的LocalDateTime、ZonedDateTime、ZoneId等。

分别位于java.utiljava.time包中。

⭐ 从Java 8开始,java.time包提供了新的日期和时间API,主要涉及的类型有:

  • 本地日期和时间:LocalDateTimeLocalDateLocalTime
  • 带时区的日期和时间:ZonedDateTime
  • 时刻:Instant
  • 时区:ZoneIdZoneOffset
  • 时间间隔:Duration

以及一套新的用于取代SimpleDateFormat的格式化类型DateTimeFormatter

Date和Calendar(旧API)

计算机表示的时间是以整数表示的时间戳存储的,即Epoch Time,Java使用long型来表示以毫秒为单位的时间戳,通过System.currentTimeMillis()获取当前时间戳。

Epoch Time是计算从1970年1月1日零点(格林威治时区/GMT+00:00)到现在所经历的秒数

Date

1
2
3
4
Date date = new Date();
System.out.println(date.getYear() + 1900); // 必须加上1900
System.out.println(date.getMonth() + 1); // 0~11,必须加上1
System.out.println(date.getDate()); // 1~31,不能加1

自定义格式输出:

1
2
3
Date date = new Date();
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(date));

假设当前月份是9月:

  • M:输出9
  • MM:输出09
  • MMM:输出Sep
  • MMMM:输出September

Calendar

Date比,主要多了一个可以做简单的日期和时间运算的功能。

Calendar的基本用法:

1
2
3
4
5
6
// 获取当前时间:
Calendar c = Calendar.getInstance();
int y = c.get(Calendar.YEAR);
int m = 1 + c.get(Calendar.MONTH);
int d = c.get(Calendar.DAY_OF_MONTH);
int w = c.get(Calendar.DAY_OF_WEEK);

TimeZone

CalendarDate相比,它提供了时区转换的功能。时区用TimeZone对象表示

利用Calendar进行时区转换的步骤是:

  1. 清除所有字段;
  2. 设定指定时区;
  3. 设定日期和时间;
  4. 创建SimpleDateFormat并设定目标时区;
  5. 格式化获取的Date对象(注意Date对象无时区信息,时区信息存储在SimpleDateFormat中)。

LocalDateTime

import java.time.*;

获取日期和时间

  • 当前日期:LocalDate d = LocalDate.now(); //2022-02-15
  • 当前时间:LocalTime t = LocalTime.now(); //08:51:53.807637953
  • 当前日期和时间:LocalDateTime dt = LocalDateTime.now(); //2022-02-15T08:51:53.807670493
  • 用标准输出会严格按照ISO 8601格式打印

如果要在同一个时刻分别输出当前日期和时间,可以采用以下方法:(如果按上述分开写,每一行程序的执行时间可能会带来影响)

1
2
3
LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
LocalDate d = dt.toLocalDate(); // 转换到当前日期
LocalTime t = dt.toLocalTime(); // 转换到当前时间

创建指定日期和时间

1
2
3
4
5
6
7
8
9
// 指定日期和时间:
LocalDate d2 = LocalDate.of(2019, 11, 30); // 2019-11-30, 注意11=11月
LocalTime t2 = LocalTime.of(15, 16, 17); // 15:16:17
LocalDateTime dt2 = LocalDateTime.of(2019, 11, 30, 15, 16, 17);
LocalDateTime dt3 = LocalDateTime.of(d2, t2);
//按照ISO 8601格式直接传入字符串
LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
LocalDate d = LocalDate.parse("2019-11-19");
LocalTime t = LocalTime.parse("15:16:17");

ISO 8601规定的日期和时间分隔符是T。标准格式如下:

  • 日期:yyyy-MM-dd
  • 时间:HH:mm:ss
  • 带毫秒的时间:HH:mm:ss.SSS
  • 日期和时间:yyyy-MM-dd’T’HH:mm:ss
  • 带毫秒的日期和时间:yyyy-MM-dd’T’HH:mm:ss.SSS

DateTimeFormatter

import java.time.format.*;

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// 自定义格式化:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dtf.format(LocalDateTime.now()));

// 用自定义格式解析:
LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf);
System.out.println(dt2);
}

时间加减

加5天减3小时:

1
LocalDateTime dt2 = dt.plusDays(5).minusHours(3);

减一个月

1
LocalDateTime dt3 = dt2.minusMonths(1);

时间调整

对日期和时间进行调整则使用withXxx()方法,例如:withHour(15)会把10:11:12改为15:11:12

  • 调整年:withYear()
  • 调整月:withMonth()
  • 调整日:withDayOfMonth()
  • 调整时:withHour()
  • 调整分:withMinute()
  • 调整秒:withSecond()

其他,例如

1
2
3
4
import java.time.*;
import java.time.temporal.*;
// 本月最后1天:
LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());

时间比较

可以使用isBefore()isAfter()方法

1
LocalDate.now().isBefore(LocalDate.of(2019, 11, 19))

Duration和Period

  • Duration表示两个时刻之间的时间间隔

  • Period表示两个日期之间的天数

    1
    2
    Duration d = Duration.between(start_dt, end_dt);// PT1235H10M30S
    Period p = LocalDate.of(2019, 11, 19).until(LocalDate.of(2020, 1, 9)); //P1M21D
  • 利用ofXxx()或者parse()方法也可以直接创建Duration

    1
    2
    Duration d1 = Duration.ofHours(10); // 10 hours
    Duration d2 = Duration.parse("P1DT2H3M"); // 1 day, 2 hours, 3 minutes

ZonedDateTime

获得不同时区的当前时间

1
2
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间

表示同一时间

给当前时间附加不同时区

1
2
3
LocalDateTime ldt = LocalDateTime.of(2019, 9, 15, 15, 16, 17);
ZonedDateTime zbj = ldt.atZone(ZoneId.systemDefault());
ZonedDateTime zny = ldt.atZone(ZoneId.of("America/New_York"));

表示不同时间

时区转换

将一个ZonedDateTime对象,通过withZoneSameInstant()将关联时区转换到另一个时区

涉及到时区时,千万不要自己计算时差,否则难以正确处理夏令时。

1
2
3
4
// 以中国时区获取当前时间:
ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 转换为纽约时间:
ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));

ZonedDateTime转换为本地时间:

1
2
ZonedDateTime zdt = ...
LocalDateTime ldt = zdt.toLocalDateTime(); //无时区信息

DateTimeFormatter

SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。

1
2
3
4
5
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E, yyyy-MMMM-dd HH:mm", Locale.US); //按照Locale默认习惯格式化(E是星期几)

System.out.println(formatter.format(zdt));

Instant

Instant.now()获取当前时间戳,效果和System.currentTimeMillis()类似,精度更高

可以和ZonedDateTime以及long互相转换。

1
2
3
4
// 以指定时间戳创建Instant:
Instant ins = Instant.ofEpochSecond(1568568760);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println(zdt); // 2019-09-16T01:32:40+08:00[Asia/Shanghai]

最佳实践——新旧API处理

处理日期和时间时,尽量使用新的java.time包;

在数据库中存储时间戳时,尽量使用long型时间戳,它具有省空间,效率高,不依赖数据库的优点。

数据库 对应Java类(旧) 对应Java类(新)
DATETIME java.util.Date LocalDateTime
DATE java.sql.Date LocalDate
TIME java.sql.Time LocalTime
TIMESTAMP java.sql.Timestamp LocalDateTime

正则表达式

匹配规则

单个字符的匹配规则如下:

正则表达式 规则 可以匹配
A 指定字符 A
\u548c 指定Unicode字符
. 任意字符 ab&0
\d 数字0~9 0~9
\w 大小写字母,数字和下划线 a~zA~Z0~9_
\s 空格、Tab键 空格,Tab
\D 非数字 aA&_,……
\W 非\w &@,……
\S 非\s aA&_,……

多个字符的匹配规则如下:

正则表达式 规则 可以匹配
A* 任意个数字符 空,AAAAAA,……
A+ 至少1个字符 AAAAAA,……
A? 0个或1个字符 空,A
A{3} 指定个数字符 AAA
A{2,3} 指定范围个数字符 AAAAA
A{2,} 至少n个字符 AAAAAAAAA,……
A{0,3} 最多n个字符 空,AAAAAA

复杂匹配规则

正则表达式 规则 可以匹配
^ 开头 字符串开头
$ 结尾 字符串结束
[ABC] […]内任意字符 A,B,C
[A-F0-9xy] 指定范围的字符 A,……,F0,……,9xy
[^A-F] 指定范围外的任意字符 A~F
AB|CD|EF AB或CD或EF ABCDEF

分组匹配

引入java.util.regex,用Pattern对象匹配,匹配后获得一个Matcher对象,如果匹配成功,就可以直接从Matcher.group(index)返回子串

正则表达式用(...)分组可以通过Matcher对象快速提取子串:

  • group(0)表示匹配的整个字符串;
  • group(1)表示第1个子串,group(2)表示第2个子串,以此类推
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.regex.*;
public class Main {
public static void main(String[] args) {
Pattern p = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})");
Matcher m = p.matcher("010-12345678");
if (m.matches()) {
String g1 = m.group(1);
String g2 = m.group(2);
System.out.println(g1);
System.out.println(g2);
} else {
System.out.println("匹配失败!");
}
}
}

非贪婪匹配

正则表达式匹配默认使用贪婪匹配,可以使用?表示对某一规则进行非贪婪匹配。

例:给定一个字符串表示的数字,判断该数字末尾0的个数。例如:

  • "123000":3个0
  • "10100":2个0
  • "1001":0个0

正则表达式(\d+?)(0*),后面?表示非贪婪匹配,让\d+尽量少匹配,让0*尽量多匹配

搜索和替换

使用正则表达式可以:

  • 分割字符串:String.split()

  • 搜索子串:Matcher.find()

  • 替换字符串:String.replaceAll()

    • 反向引用 可以使用$1$2来反向引用匹配到的子串

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
          String s = "the quick brown fox jumps over the lazy dog.";

      String r = s.replaceAll("\\s([a-z]{4})\\s", " <b>$1</b> ");
      //the quick brown fox jumps <b>over</b> the <b>lazy</b> dog.

      [Matcher (Java SE 11 & JDK 11 ) (runoob.com)](https://www.runoob.com/manual/jdk11api/java.base/java/util/regex/Matcher.html)

      > #### 模板引擎
      >
      > 模板引擎是指,定义一个字符串作为模板:
      >

Hello, ${name}! You are learning ${lang}!

1
2
3
4
5
6
7
8
9
10
11
12

其中,以`${key}`表示的是变量,也就是将要被替换的内容

当传入一个`Map<String, String>`给模板后,需要把对应的key替换为Map的value。

例如,传入`Map`为:

```json
{
"name": "Bob",
"lang": "Java"
}

然后,${name}被替换为Map对应的值"Bob”,${lang}被替换为Map对应的值"Java",最终输出的结果为:

1
Hello, Bob! You are learning Java!

用正则表达式实现:

Matcher.appendReplacement (Java SE 11 & JDK 11 ) (runoob.com)

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
package com.itranswarp.learnjava;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Learn Java from https://www.liaoxuefeng.com/
*
* @author liaoxuefeng
*/
public class Template {

final String template;
final Pattern pattern = Pattern.compile("\\$\\{(\\w+)\\}");

public Template(String template) {
this.template = template;
}

public String render(Map<String, Object> data) {
Matcher m = pattern.matcher(template);
// TODO:
StringBuilder sb = new StringBuilder();
while (m.find()) {
m.appendReplacement(sb,data.get(m.group(1)).toString());
}
m.appendTail(sb);
return sb.toString();
}

}