基本概念
日期与时间
- 日期是指具体某一天,如
2020-10-30
,是非连续的 时间指具体某刻,如
12:30:59
2020-1-1 20:21:59
时区
- 全球一共分为24个时区,伦敦所在的时区称为标准时区,其他时区按东/西偏移的小时区分,北京所在的时区是东八区。
- 光靠本地时间还无法唯一确定一个准确的时刻,所以我们还需要给本地时间加上一个时区,比如:东八区的2019年11月20日早上8:15,和西五区的2019年11月19日晚上19:15,他们的时刻是相同的
时区有好几种表示方式:
时区还不是最复杂的,更复杂的是夏令时。
- 所谓夏令时,就是夏天开始的时候,把时间往后拨1小时,夏天结束的时候,再把时间往前拨1小时
-
本地化
在计算机中,通常使用Locale表示一个国家或地区的日期、时间、数字、货币等格式。
任何时间其实都一样,只不过表现形式不同,如不同地区的时区时间在同一时间是一样的,时间在计算机中本质是一个整数,即Epoch Time
- Epoch Time是计算从1970年1月1日零点(格林威治时区/GMT+00:00)到现在所经历的秒数
- Epoch Time又称为时间戳,在不同的编程语言中,会有几种存储方式:
- 以秒为单位的整数:
1574208900
,缺点是精度只能到秒; - 以毫秒为单位的整数:
1574208900123
,最后3位表示毫秒数; - 以秒为单位的浮点数:
1574208900.123
,小数点后面表示零点几秒。
- 以秒为单位的整数:
- java中时间戳通常是用long表示的毫秒数
- 使用
System.currentTimeMillis()
获取当前时间戳
System.nanoTime()
用于精确计算时间差,用法同currentTimeMillis
差不多,但是返回的是纳秒值。且不能用于计算当前时间,仅仅用作计算差值Java标准库有两套处理日期和时间的API:
- 一套定义在java.util这个包里面,主要包括Date、Calendar和TimeZone这几个类;
- 一套新的API是在Java 8引入的,定义在java.time这个包里面,主要包括LocalDateTime、ZonedDateTime、ZoneId等。
因为历史遗留原因,旧的API存在很多问题,所以引入了新的API。
Date对象有几个严重的问题:它不能转换时区,除了toGMTString()可以按GMT+0:00输出外,Date总是以当前计算机系统的默认时区为基础进行输出。
- 我们也很难对日期和时间进行加减 ```java Date date = new Date(); //得到当前时间:Tue Dec 14 23:39:19 CST 2021 或者Date date=new Date(时间戳);
int year=date.getYear() + 1900; //getyear得到的是自1900计算的年份数 int month=date.getMonth() + 1 //getmonth得到的是0-11 int day=date.getDate() //得到1-31
// 转换为String: System.out.println(date.toString()); // 转换为GMT时区: 28 Dec 2021 15:43:06 GMT System.out.println(date.toGMTString()); // 转换为本地时区: Dec 28, 2021, 3:43:06 PM System.out.println(date.toLocaleString());
<a name="YsARv"></a>
## Calendar 计算
- 主要用于计算时间和日期
- Calendar可以用于获取并设置年、月、日、时、分、秒,它和Date比,主要多了一个可以做简单的日期和时间运算的功能。
- 返回的年份无需像date一样进行转换
- 返回的月份需要加1
- 星期为1—7
```java
// 获取当前时间:
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); //周,即是这个月的第几周,类似的如day_of_year就是获取某一天是该年的第几天
int hh = c.get(Calendar.HOUR_OF_DAY); //时
int mm = c.get(Calendar.MINUTE); //分
int ss = c.get(Calendar.SECOND); //秒
int ms = c.get(Calendar.MILLISECOND); //毫秒
- Calendar只有一种方式获取,即
**Calendar.getInstance()**
,而且一获取到就是当前时间。如果我们想给它设置成特定的一个日期和时间,就必须先创建当前时间对象,然后再清除所有字段 getTime
可以获得计算后的Date ```java // 当前时间:直接输出对象得到的是一系列信息的集合Calendar c = Calendar.getInstance(); // 清除所有: c.clear(); // 设置2019年: c.set(Calendar.YEAR, 2019); // 设置9月:注意8表示9月: c.set(Calendar.MONTH, 8); // 设置2日: c.set(Calendar.DATE, 2); // 设置时间: c.set(Calendar.HOUR_OF_DAY, 21); c.set(Calendar.MINUTE, 22); c.set(Calendar.SECOND, 23);
//可以直接c.setTime(Date)设置指定时间 System.out.println(new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”).format(c.getTime())); // 2019-09-02 21:22:23
---
- Calendar计算,传入要修改的单位与数值即可
```javascript
c.add(Calendar.DAY_OF_MONTH, 5);
c.add(Calendar.HOUR_OF_DAY, -2);
TimeZone 时区转换
SimpleDateFormat 格式化
- 根据指定规则对一个
date
进行转换,即主要用于格式化。创建的对象就是一个规则 simpledateformat
是线程不安全的,使用的时候,只能在方法内部创建新的局部变量- 可以自由组合并添加其他字符,常用的为:
yyyy-MM-dd HH:mm:ss
- yyyy:年
- MM:月
- dd: 日
- HH: 小时
- mm: 分钟
- ss: 秒
- MMM:简写英文月份
- E:简写英文星期几
- ZZZZ:时区
- 一般来说,字母越长,输出越长。以M为例,假设当前月份是9月:
- M:输出9
- MM:输出09
- MMM:输出Sep
- MMMM:输出September
```java
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat(“月:MM 月:dd 年:yyyy 时间:HH:mm:ss”);
System.out.println(sdf.format(date));
//月:12 月:28 年:2021 时间:23:48:45
//字符串转时间 Date date=sdf.parse(string); string为sdf的格式
<a name="Esri6"></a>
# 新API
从Java 8开始,java.time包提供了新的日期和时间API,主要涉及的类型有:
- 本地日期和时间:LocalDateTime,LocalDate,LocalTime;
- 带时区的日期和时间:ZonedDateTime;
- 时间戳:Instant;
- 时区:ZoneId,ZoneOffset;
- 时间间隔:Duration。
以及一套新的用于取代SimpleDateFormat的格式化类型DateTimeFormatter
---
和旧的API相比,新API严格区分了时刻、本地日期、本地时间和带时区的日期时间,并且,对日期和时间进行运算更加方便。<br />此外,新API修正了旧API不合理的常量设计:
- **Month的范围用1~12表示1月到12月; 在date中月份是0-11**
- Week的范围用1~7表示周一到周日。
<a name="ehA7M"></a>
## LocalDateTime
- **本地日期和时间通过now()获取到的总是以当前默认时区返回的**,和旧API不同,LocalDateTime、LocalDate和LocalTime默认严格按照ISO 8601规定的日期和时间格式进行打印。
- **如果需要获取日期和时间2个值,因为程序执行需要一些时间,所以应该通过当前日期兼时间拆分2个值,旧api也一样**
- 注意到LocalDateTime无法与时间戳进行转换,因为LocalDateTime没有时区,无法确定某一时刻。后面我们要介绍的ZonedDateTime相当于LocalDateTime加时区的组合,它具有时区,可以与long表示的时间戳进行转换。
- LocalDateTime输出的`T`是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
```java
LocalDate d = LocalDate.now(); // 当前日期 2022-01-01
LocalTime t = LocalTime.now(); // 当前时间 13:47:24.822353309
LocalDateTime dt = LocalDateTime.now(); //当前时间兼日期 2022-01-01T13:47:24.822384364
//如果需要获取日期和时间2个值,因为程序执行需要一些时间,所以应该通过当前日期兼时间拆分2个值
LocalDate d = dt.toLocalDate(); // 转换到当前日期
LocalTime t = dt.toLocalTime(); // 转换到当前时间
//指定时间构建,构建的不会带有毫秒值
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);
//传入字符串以获取对象
LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
LocalDate d = LocalDate.parse("2019-11-19");
LocalTime t = LocalTime.parse("15:16:17");
LocalDateTime 计算
- LocalDateTime除了是新api里的基本时间日期类,同时相比于
date
还提供了一些方法可以进行计算
加和减
minus,plus
搭配加减的单位即是一个方法,如minusMonths()``plusMonths()
;minus/plusHours()
;...Days()
直接指定某一个单位的值:如
withHour(15)会把10:11:12变为15:11:12
如果把31日的月份设置为30日的月份,天会自动从31变为30
- 调整年:withYear()
- 调整月:withMonth()
- 调整日:withDayOfMonth()
- 调整时:withHour()
- 调整分:withMinute()
-
开头与结尾
// 本月第一天0:00时刻: LocalDateTime firstDay = LocalDate.now().withDayOfMonth(1).atStartOfDay(); System.out.println(firstDay); // 本月最后1天: LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()); System.out.println(lastDay); // 下月第1天: LocalDate nextMonthFirstDay = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()); System.out.println(nextMonthFirstDay); // 本月第1个周一: LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); System.out.println(firstWeekday);
比较先后
要判断两个LocalDateTime的先后,可以使用isBefore()、isAfter()方法,对于LocalDate和LocalTime类似
LocalDateTime now = LocalDateTime.now(); LocalDateTime target = LocalDateTime.of(2019, 11, 19, 8, 15, 0); System.out.println(now.isBefore(target)); //false System.out.println(LocalDate.now().isBefore(LocalDate.of(2019, 11, 19))); //false System.out.println(LocalTime.now().isAfter(LocalTime.parse("08:15:00"))); //true
计算时间差
Duration表示两个时刻之间的时间间隔。
- Period表示两个日期之间的天数
- 注意到两个LocalDateTime之间的差值使用Duration表示,类似PT1235H10M30S,表示1235小时10分钟30秒。而两个LocalDate之间的差值用Period表示,类似P1M21D,表示1个月21天
- Duration和Period的表示方法也符合ISO 8601的格式,它以P…T…的形式表示,P…T之间表示日期间隔,T后面表示时间间隔。如果是PT…的格式表示仅有时间间隔。
利用ofXxx()或者parse()方法也可以直接创建Duration:
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 = LocalDate.of(2019, 11, 19).until(LocalDate.of(2020, 1, 9)); System.out.println(p); // P1M21D
Duration d1 = Duration.ofHours(10); // 10 hours Duration d2 = Duration.parse("P1DT2H3M"); // 1 day, 2 hours, 3 minutes
DateTimeFormatter 格式化
用于格式化,也可以用于将非
iso 8601
格式的字符串转LocalDateTime
对象。- 同
simpledateformat
一样,创建的对象也是规则,用法也差不多;顾名思义,DateTimeFormatter
是用于操作LocalDateTime
- 新旧格式化api使用的格式化字符都是一样的,如yyyy都表示年
- 和SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。线程的概念我们会在后面涉及到。现在我们只需要记住:因为SimpleDateFormat不是线程安全的,使用的时候,只能在方法内部创建新的局部变量。而DateTimeFormatter可以只创建一个实例,到处引用。
- 新api可以指定
locale
,理解成地区格式即可,如Locale
为Locale.CHINA
时星期会被输出为中文,如周六
,为Locale.US
时会周末会输出为三位简写英文
当我们直接调用System.out.println()对一个ZonedDateTime或者LocalDateTime实例进行打印的时候,实际上,调用的是它们的toString()方法
//格式化 DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); System.out.println(dtf.format(LocalDateTime.now())); //另外一种创建方式:传入格式化字符串时,同时指定Locale: DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E, yyyy-MMMM-dd HH:mm", Locale.?); // 用自定义格式解析: LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf); System.out.println(dt2);
```java //locale示例 ZonedDateTime zdt = ZonedDateTime.now();
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ"); System.out.println(formatter.format(zdt)); var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA); System.out.println(zhFormatter.format(zdt)); var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US); System.out.println(usFormatter.format(zdt));
2022-01-08T13:27 GMT 2022 1月 08 周六 13:27 Sat, January/08/2022 13:27
<a name="P97Y1"></a>
## ZonedDateTime 时区
[链接](https://www.liaoxuefeng.com/wiki/1252599548343744/1303904694304801)
<a name="YHqxG"></a>
## Instant 时间戳
- Instant是新api中关于时间戳的类
- 实际上,Instant内部只有两个核心字 `final long seconds`,`final int nanos`前者是秒为单位的时间戳,后者是纳秒级的时间戳
- `Instant`表示高精度时间戳,它可以和`ZonedDateTime`以及`long`互相转换
- 23点13得到的Instant为`2022-01-08T15:08:42.405Z `
- T 表示这是iso 8601表示
- **Z表示世界标准时间**,所以15换成北京时间是23点,北京是东八区,+8
- .**405是毫秒,新旧api中的**`**.?**`**都表示是毫秒**
```java
Instant now = Instant.now(); //获得当前时间 2022-01-08T15:08:42.405Z
System.out.println(now.getEpochSecond()); // 秒
System.out.println(now.toEpochMilli()); // 毫秒
Instant.ofEpochSecond(...) //根据秒时间戳创建Instant
Instant.ofEpochMilli(...) //根据毫秒时间戳创建Instant
instant可以添加时区信息创建一个
ZonedDateTime
对象// 以指定时间戳创建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类转换关系
LocalDateTime
,ZoneId
,Instant
,ZonedDateTime
和long
都可以互相转换- 转换的时候,只需要留意
long
类型以毫秒还是秒为单位即可。┌─────────────┐ │LocalDateTime│────┐ └─────────────┘ │ ┌─────────────┐ ├───>│ZonedDateTime│ ┌─────────────┐ │ └─────────────┘ │ ZoneId │────┘ ▲ └─────────────┘ ┌─────────┴─────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Instant │<───>│ long │ └─────────────┘ └─────────────┘
long自动转不同地区与时区例子
- 转换的时候,只需要留意
因为long保存时间可以与地区,时区,数据库等都无关的特点,我们采用long保存,输出时再根据用户的地区转为适合其的时间格式 ```java public class Main { public static void main(String[] args) {
long ts = 1574208900000L; System.out.println(timestampToString(ts, Locale.CHINA, "Asia/Shanghai")); System.out.println(timestampToString(ts, Locale.US, "America/New_York"));
}
static String timestampToString(long epochMilli, Locale lo, String zoneId) {
Instant ins = Instant.ofEpochMilli(epochMilli); DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT); return f.withLocale(lo).format(ZonedDateTime.ofInstant(ins, ZoneId.of(zoneId)));
} }
2019年11月20日 上午8:15 Nov 19, 2019, 7:15 PM
<a name="KWtyz"></a>
# 新旧API转换
<a name="T9tK3"></a>
## 旧 ——> 新
- 如果要把旧式的Date或Calendar转换为新API对象,可以通过toInstant()方法转换为Instant对象,再继续转换为ZonedDateTime
- 旧的TimeZone提供了一个toZoneId(),可以把自己变成新的ZoneId
```java
// Date -> Instant:
Instant ins1 = new Date().toInstant();
// Calendar -> Instant -> ZonedDateTime:
Calendar calendar = Calendar.getInstance();
Instant ins2 = calendar.toInstant();
ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());
新 ——> 旧
- 如果要把新的ZonedDateTime转换为旧的API对象,只能借助long型时间戳做一个“中转”
- 新的ZoneId转换为旧的TimeZone,需要借助ZoneId.getId()返回的String完成 ```java // ZonedDateTime -> long: ZonedDateTime zdt = ZonedDateTime.now(); long ts = zdt.toEpochSecond() * 1000;
// long -> Date: Date date = new Date(ts);
// long -> Calendar: Calendar calendar = Calendar.getInstance(); calendar.clear(); calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId())); calendar.setTimeInMillis(zdt.toEpochSecond() * 1000); ```
数据库时间类型
- 在数据库中,我们需要存储的最常用的是时刻(Instant),因为有了时刻信息,就可以根据用户自己选择的时区,显示出正确的本地时间。
- 所以,最好的方法是直接用长整数long表示,在数据库中存储为BIGINT类型。
- 使用long型时间戳存时间,还具有省空间,效率高,不依赖数据库的优点
- 貌似有的数据库插入时间格式字符串也能转为数据库的时间类型。
- MVC提供了一个注解:
**@DateTimeFormat(pattern = "yyyy-MM-dd")**
可以将字符串参数映射为时间类型- 能用于参数,也能用于实体类的字段(用于实体类接收时)。没用过就是
- 在数据库中,也存在几种日期和时间类型:
- DATETIME:表示日期和时间;
- DATE:仅表示日期;
- TIME:仅表示时间;
- TIMESTAMP:和DATETIME类似,但是数据库会在创建或者更新记录的时候同时修改TIMESTAMP | 数据库 | 对应Java类(旧) | 对应Java类(新) | | —- | —- | —- | | DATETIME | java.util.Date | LocalDateTime | | DATE | java.sql.Date | LocalDate | | TIME | java.sql.Time | LocalTime | | TIMESTAMP | java.sql.Timestamp | LocalDateTime |