Java 时间处理函数总结
一. Epoch Time
- 时间的格式也分为存储格式和展示格式。根据不同的时区,全世界同一时刻的展示格式不同。
- 时刻本质上是一个整数,我们称它为Epoch Time。Epoch Time是计算从1970年1月1日零点(格林威治时区/GMT+00:00)到现在所经历的秒数。
- 因此只需要存储一个整数比如 1574208900 则可表示某一时刻。当需要显示为某一地区的当地时间时,我们就把它格式化为一个字符串展示。
Java有两套日期和时间的API:
- 新版的 LocalDateTime、ZonedDateTime、DateTimeFormatter 等:位于 java.time 包
- 旧版的 Date、Calendar 和 TimeZone:位于 java.util 包
二. 新版时间API
从Java 8开始,java.time 包提供了新的日期和时间 API,主要涉及的类型有:
- 本地日期和时间:LocalDateTime,LocalDate,LocalTime;
- 带时区的日期和时间:ZonedDateTime;
- 时刻:Instant;
- 时区:ZoneId,ZoneOffset;
- 时间间隔:Duration
- 时间格式化:DateTimeFormatter
2.1 获取本地时间
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class Main {
public static void main(String[] args) {
LocalDate d = LocalDate.now(); // 当前日期
LocalTime t = LocalTime.now(); // 当前时间
LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
System.out.println(d); // 严格按照ISO 8601格式打印
System.out.println(t); // 严格按照ISO 8601格式打印
System.out.println(dt); // 严格按照ISO 8601格式打印
}
}
// 输出结果
2020-12-16
19:08:03.075111
2020-12-16T19:08:03.075138
2.2 截取日期和时间
LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
LocalDate d = dt.toLocalDate(); // 转换到当前日期
LocalTime t = dt.toLocalTime(); // 转换到当前时间
System.out.println(dt);
System.out.println(d);
System.out.println(t);
// 输出结果
2020-12-16T19:14:05.476327
2020-12-16
19:14:05.476327
2.3 数字转换为时间
LocalDate d = LocalDate.of(2019, 11, 30); // 2019-11-30
LocalTime t = LocalTime.of(15, 16, 17); // 15:16:17
LocalDateTime dt1 = LocalDateTime.of(2019, 11, 30, 15, 16, 17);
LocalDateTime dt2 = LocalDateTime.of(d, t);
2.4 字符串转换为时间
LocalDate d = LocalDate.parse("2019-11-19");
LocalTime t = LocalTime.parse("15:16:17");
LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
2.5 自定义格式
ISO 8601标准格式
- 日期: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 dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dt1 = dtf.format(LocalDateTime.now());
LocalDateTime dt2 = LocalDateTime.parse("2019-11-30 15:16:17", dtf);
System.out.println(dt1);
System.out.println(dt2);
// 输出结果
2021-05-30 11:29:49
2019-11-30T15:16:17
2.6 时刻增加减少
- 在原来的基础上操作
LocalDateTime dtNow = LocalDateTime.now();
LocalDateTime dt2 = dtNow.plusDays(1);
LocalDateTime dt3 = dtNow.minusHours(1);
LocalDateTime dt4 = dtNow.plusDays(1).minusHours(1);
System.out.println("dtNow = " + dtNow);
System.out.println("dt2 = " + dt2);
System.out.println("dt3 = " + dt3);
System.out.println("dt4 = " + dt4);
// 输出结果
dtNow = 2021-05-30T11:34:31.236
dt2 = 2021-05-31T11:34:31.236
dt3 = 2021-05-30T10:34:31.236
dt4 = 2021-05-31T10:34:31.236
- 直接指定具体的数字
LocalDateTime dt = LocalDateTime.now();
dt = dt.withYear(2030);
dt = dt.withHour(1).withMinute(0);
System.out.println("dt = " + dt);
// 输出结果
dt = 2030-05-30T01:00:26.089
- 常见时间节点
LocalDateTime dt = LocalDate.now().withDayOfMonth(1).atStartOfDay(); // 本月第一天0:00时刻
LocalDate dt1 = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()); // 本月最后1天
LocalDate dt2 = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()); // 下月第1天
LocalDate dt3 = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); // 本月第1个周一
System.out.println("dt = " + dt);
System.out.println("dt1 = " + dt1);
System.out.println("dt2 = " + dt2);
System.out.println("dt3 = " + dt3);
// 输出结果
dt = 2021-05-01T00:00
dt1 = 2021-05-31
dt2 = 2021-06-01
dt3 = 2021-05-03
2.7 时刻的比较
LocalDateTime dt1 = LocalDateTime.of(2019, 11, 19, 8, 15, 0);
LocalDateTime dt2 = LocalDateTime.of(2019, 11, 19, 8, 16, 0);
System.out.println(dt1.isAfter(dt2)); // 大于
System.out.println(dt1.isBefore(dt2)); // 小于
System.out.println(dt1.equals(dt2)); // 等于
2.8 时刻的间隔
- Duration时刻间隔:Duration表示两个时刻之间的时间间隔
- Period相隔天数:Period表示两个日期之间的天数,只能精确到天
LocalDateTime start = LocalDateTime.of(2019, 11, 19, 8, 15, 0);
LocalDateTime end = LocalDateTime.of(2020, 1, 9, 19, 25, 30);
Duration d = Duration.between(start, end);
System.out.println(d); // PT1235H10M30S
Period p = start.toLocalDate().until(end.toLocalDate());
System.out.println(p); // P1M21D
Duration d1 = Duration.ofHours(10); // 创建10小时的Duration
Duration d2 = Duration.parse("P1DT2H3M"); // 使用parse创建Duration
System.out.println(d1);
System.out.println(d2);
// 输出结果
PT1235H10M30S
P1M21D
PT10H
PT26H3M
2.9 带时区的时间
- 可以简单地把 ZonedDateTime 理解成 LocalDateTime 加 ZoneId。ZoneId 是java.time 引入的新的时区类。
// 使用 ZoneDateTime 创建带时区的时间
ZonedDateTime dt = ZonedDateTime.now(); // 默认时区
ZonedDateTime dt1 = ZonedDateTime.now(ZoneId.systemDefault()); // 默认时区
ZonedDateTime dt2 = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间
System.out.println(dt);
System.out.println(dt1);
System.out.println(dt2);
// 输出结果
2021-05-30T11:44:10.491+08:00[Asia/Shanghai]
2021-05-30T11:44:10.491+08:00[Asia/Shanghai]
2021-05-29T23:44:10.493-04:00[America/New_York]
// 使用 LocalDateTime ZoneId 创建带时区的时间
LocalDateTime dt3 = LocalDateTime.of(2019, 9, 15, 15, 16, 17);
ZonedDateTime dt4 = dt3.atZone(ZoneId.systemDefault());
ZonedDateTime dt5 = dt3.atZone(ZoneId.of("America/New_York"));
System.out.println(dt3);
System.out.println(dt4);
System.out.println(dt5);
// 输出结果
2019-09-15T15:16:17
2019-09-15T15:16:17+08:00[Asia/Shanghai]
2019-09-15T15:16:17-04:00[America/New_York]
// 查看所有的 ZoneId
Set<String> zones = ZoneId.getAvailableZoneIds();
System.out.println(zones);
// 输出结果
[Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8, Africa/Nairobi, America/Marigot,......]
// 将北京时间转换为纽约时间(带时区转换)
ZonedDateTime dt6 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime dt7 = dt6.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println(dt6);
System.out.println(dt7);
// 输出结果
2021-05-30T11:47:32.667+08:00[Asia/Shanghai]
2021-05-29T23:47:32.667-04:00[America/New_York]
// 将纽约时间转换为纽约本地时间(丢掉时区)
ZonedDateTime dt8 = ZonedDateTime.now(ZoneId.of("America/New_York"));
LocalDateTime dt9 = dt8.toLocalDateTime();
System.out.println(dt8);
System.out.println(dt9);
// 输出结果
2021-05-29T23:48:18.909-04:00[America/New_York]
2021-05-29T23:48:18.909
2.10 获取当前时间戳
// Instant 获取当前时间戳
Instant now = Instant.now();
System.out.println(now.getEpochSecond()); // 秒
System.out.println(now.toEpochMilli()); // 毫秒
// System 获取当前时间戳
System.out.println(System.currentTimeMillis()/1000); // 秒
System.out.println(System.currentTimeMillis()); // 毫秒
2.11 时间对象转换时间戳
- 注意:转换为时间戳时,必须要转换成 ZoneDateTime 以后再转换
// 方法1:直接获取 ZoneDateTIme
ZonedDateTime zdt = ZonedDateTime.now();
Instant instant = zdt.toInstant();
System.out.println(instant.getEpochSecond());
// 方法2:需要转换成 ZoneDateTIme
ZonedDateTime zdt = LocalDateTime.now().atZone(ZoneId.systemDefault());
Instant instant = zdt.toInstant();
System.out.println(instant.getEpochSecond());
2.12 时间戳转换时间对象
Instant instant = Instant.ofEpochSecond(1608133910);
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
System.out.println(zdt);
// 输出结果
2020-12-16T23:51:50+08:00[Asia/Shanghai]
三. 旧版时间API
- 旧版的 Date、Calendar 和 TimeZone:位于 java.util 包。
3.1 java.util.Date
Date now = new Date(); // 获取当前时间
int year = now.getYear() + 1900; // 必须加上 1900
int month = now.getMonth() + 1; // 0~11,必须加上1
int day = now.getDate();
System.out.println("year = " + year);
System.out.println("month = " + month);
System.out.println("day = " + day);
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String nowString = df.format(now);
System.out.println("formatted = " + nowString);
// 输出结果
year = 2021
month = 5
day = 30
formatted = 2021-05-30 12:23:22
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date dateObject= new Date(df.parse("2019-11-29 00:29:00").getTime());
System.out.println(df.format(dateObject));
// 时间字符串格式化
2019-11-29 00:29:00
3.2 java.util.Calendar
- 获取时间的年月日时分秒
Calendar calendar = Calendar.getInstance(); // 获取当前时间
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 月份需要加 1
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int millisecond = calendar.get(Calendar.MILLISECOND);
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS");
List<Integer> nowList = Arrays.asList(year, month, day, hour, minute, second, millisecond);
String datetimeStr = nowList.stream().map(String::valueOf).collect(Collectors.joining("-"));
System.out.println("one = " + df.format(calendar.getTime()));
System.out.println("two = " + datetimeStr);
// 输出结果
one = 2021-05-30 14:07:09.938
two = 2021-5-30-14-7-9-938
- 设置时间的年月日时分秒
Calendar calendar = Calendar.getInstance(); // 当前时间
calendar.clear(); // 清除所有
calendar.set(Calendar.YEAR, 2019); // 设置2019年
calendar.set(Calendar.MONTH, 8); // 设置9月:注意8表示9月
calendar.set(Calendar.DATE, 2); // 设置2日
calendar.set(Calendar.HOUR_OF_DAY, 21); // 设置小时
calendar.set(Calendar.MINUTE, 22); // 设置分钟
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime()));
- 时间的加减
Calendar calendar = Calendar.getInstance(); // 当前时间:
calendar.clear(); // 清除所有:
calendar.set(2019, Calendar.NOVEMBER, 20, 8, 15, 0); // 设置: 2019年11月20号8时15分0秒
calendar.add(Calendar.DAY_OF_MONTH, 5); // 加上5天
calendar.add(Calendar.HOUR_OF_DAY, -2); // 减2小时
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(calendar.getTime()));
3.3 java.util.TimeZone
// 查看所有的时区id
// System.out.println(Arrays.toString(TimeZone.getAvailableIDs()));
TimeZone tzDefault = TimeZone.getDefault(); // 当前时区
TimeZone tzGMT9 = TimeZone.getTimeZone("GMT+09:00"); // GMT+9:00时区
TimeZone tzNY = TimeZone.getTimeZone("America/New_York"); // 纽约时区
System.out.println(tzDefault.getID()); // Asia/Shanghai
System.out.println(tzGMT9.getID()); // GMT+09:00
System.out.println(tzNY.getID()); // America/New_York
Calendar calendar = Calendar.getInstance(); // 当前时间:
calendar.clear(); // 清除所有:
calendar.set(2019, Calendar.NOVEMBER, 20, 9, 0, 0); // 设置: 2019年11月20号9时0分0秒
calendar.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); // 设置时区,不设置时是本地默认时区
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("Europe/Landon")); // 设置打印的时区
System.out.println(sdf.format(calendar.getTime()));
// 输出结果
Asia/Shanghai
GMT+09:00
America/New_York
2019-11-20 01:00:00
四. 最佳实践总结
4.1 旧API转新API
- 如果要把旧式的Date或Calendar转换为新API对象,可以通过toInstant()方法转换为Instant对象,再继续转换为ZonedDateTime
// Date -> Instant -> ZonedDateTime
Instant ins1 = new Date().toInstant();
ZonedDateTime zdt1 = ins1.atZone(ZoneId.systemDefault());
// Calendar -> Instant -> ZonedDateTime
Calendar calendar = Calendar.getInstance();
Instant ins2 = calendar.toInstant();
ZonedDateTime zdt2 = ins2.atZone(calendar.getTimeZone().toZoneId());
4.2 新API转旧API
- 如果要把新的ZonedDateTime转换为旧的API对象,只能借助 long 型时间戳做一个中转
// ZonedDateTime -> long:
ZonedDateTime zdt = ZonedDateTime.now();
long ts = zdt.toEpochSecond() * 1000;
// long -> Date:
Date date = new Date(ts);
4.3 数据库中存储日期和时间
- 在使用Java程序操作数据库时,我们需要把数据库类型与Java类型映射起来。下表是数据库类型与Java新旧API的映射关系
数据库 | 对应Java类(旧) | 对应Java类(新) |
---|---|---|
DATETIME | java.util.Date | LocalDateTime |
DATE | java.sql.Date | LocalDate |
TIME | java.sql.Time | LocalTime |
TIMESTAMP | java.sql.Timestamp | LocalDateTime |
- 实际上只需要存储时刻(Instant)即可,因为时刻可以根据不同的时区,显示出本地时间。所以最好直接用长整数long表示,在数据库中存储为BIGINT类型。
- 通过存储一个long型时间戳,我们可以编写一个 timestampToString() 的方法,非常简单地为不同地区的用户来显示不同的本地时间。
public class Main {
public static void main(String[] args) {
long ts = 1622346756000L;
// System.out.println(ZoneId.getAvailableZoneIds());
System.out.println(timestampToString(ts, "Asia/Shanghai"));
System.out.println(timestampToString(ts, "America/New_York"));
System.out.println(timestampToString(ts, "Europe/London"));
System.out.println(timestampToString(ts, "Asia/Singapore"));
}
static String timestampToString(long epochMilli, String zoneId) {
Instant instant = Instant.ofEpochMilli(epochMilli);
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return df.format(ZonedDateTime.ofInstant(instant, ZoneId.of(zoneId)));
}
}
// 输出格式
2021-05-30 11:52:36
2021-05-29 23:52:36
2021-05-30 04:52:36
2021-05-30 11:52:36