基本概念

日期与时间

  • 日期是指具体某一天,如2020-10-30,是非连续的
  • 时间指具体某刻,如12:30:59 2020-1-1 20:21:59

    • 时间也分2种,一种带日期的时间,一种不带日期

      本地时间/时区

  • 时区

    • 全球一共分为24个时区,伦敦所在的时区称为标准时区,其他时区按东/西偏移的小时区分,北京所在的时区是东八区。
    • 光靠本地时间还无法唯一确定一个准确的时刻,所以我们还需要给本地时间加上一个时区,比如:东八区的2019年11月20日早上8:15,和西五区的2019年11月19日晚上19:15,他们的时刻是相同的
  • 时区有好几种表示方式:

    • 一种是以GMT或者UTC加时区偏移表示,例如:GMT+08:00或者UTC+08:00表示东八区。
    • 另一种是缩写,例如,CST表示China Standard Time,也就是中国标准时间。但是CST也可以表示美国中部时间Central Standard Time USA,因此,缩写容易产生混淆,我们尽量不要使用缩写。
    • 最后一种是以洲/城市表示,例如,Asia/Shanghai,表示上海所在地的时区。特别注意城市名称不是任意的城市,而是由国际标准组织规定的城市。

      夏令时

  • 时区还不是最复杂的,更复杂的是夏令时。

    • 所谓夏令时,就是夏天开始的时候,把时间往后拨1小时,夏天结束的时候,再把时间往前拨1小时
  • 计算夏令时请使用标准库提供的相关类,不要试图自己计算夏令时

    本地化

  • 在计算机中,通常使用Locale表示一个国家或地区的日期、时间、数字、货币等格式。

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

        Epoch Time(时间戳)

  • 任何时间其实都一样,只不过表现形式不同,如不同地区的时区时间在同一时间是一样的,时间在计算机中本质是一个整数,即Epoch Time

    • Epoch Time是计算从1970年1月1日零点(格林威治时区/GMT+00:00)到现在所经历的
  • Epoch Time又称为时间戳,在不同的编程语言中,会有几种存储方式:
    • 以秒为单位的整数: 1574208900,缺点是精度只能到秒;
    • 以毫秒为单位的整数:1574208900123,最后3位表示毫秒数;
    • 以秒为单位的浮点数:1574208900.123,小数点后面表示零点几秒。
  • java中时间戳通常是用long表示的毫秒数
  • 使用System.currentTimeMillis()获取当前时间戳

  • System.nanoTime()用于精确计算时间差,用法同currentTimeMillis差不多,但是返回的是纳秒值。且不能用于计算当前时间,仅仅用作计算差值

    • currentTimeMillis计算差值运行太快可能为0,而nanoTime可以精确到6位毫秒(即精确到纳秒)

      时间日期API

  • Java标准库有两套处理日期和时间的API:

    • 一套定义在java.util这个包里面,主要包括Date、Calendar和TimeZone这几个类;
    • 一套新的API是在Java 8引入的,定义在java.time这个包里面,主要包括LocalDateTime、ZonedDateTime、ZoneId等。
  • 因为历史遗留原因,旧的API存在很多问题,所以引入了新的API。

    • 很多遗留代码仍然使用旧的API,所以目前仍然需要对旧的API有一定了解,很多时候还需要在新旧两种对象之间进行转换。

      旧API

      Date

  • 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());

  1. <a name="YsARv"></a>
  2. ## Calendar 计算
  3. - 主要用于计算时间和日期
  4. - Calendar可以用于获取并设置年、月、日、时、分、秒,它和Date比,主要多了一个可以做简单的日期和时间运算的功能。
  5. - 返回的年份无需像date一样进行转换
  6. - 返回的月份需要加1
  7. - 星期为1—7
  8. ```java
  9. // 获取当前时间:
  10. Calendar c = Calendar.getInstance();
  11. int y = c.get(Calendar.YEAR); //年
  12. int m = 1 + c.get(Calendar.MONTH); //月
  13. int d = c.get(Calendar.DAY_OF_MONTH); //日
  14. int w = c.get(Calendar.DAY_OF_WEEK); //周,即是这个月的第几周,类似的如day_of_year就是获取某一天是该年的第几天
  15. int hh = c.get(Calendar.HOUR_OF_DAY); //时
  16. int mm = c.get(Calendar.MINUTE); //分
  17. int ss = c.get(Calendar.SECOND); //秒
  18. 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()

    • 加减会自动根据不同月的天数进行计算,我们无需担心
      LocalDateTime dt = LocalDateTime.of(2019, 10, 26, 20, 30, 59);
      // 加5天减3小时:
      LocalDateTime dt2 = dt.plusDays(5).minusHours(3);     //2019-10-31T17:30:59
      

      设置某部分值

  • 直接指定某一个单位的值:如withHour(15)会把10:11:12变为15:11:12

  • 如果把31日的月份设置为30日的月份,天会自动从31变为30

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

      开头与结尾

      // 本月第一天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,理解成地区格式即可,如LocaleLocale.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类转换关系

  • LocalDateTimeZoneIdInstantZonedDateTimelong都可以互相转换

    • 转换的时候,只需要留意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 |