Java各種日期類(lèi)間的互轉(zhuǎn)
日期的大小比較及差值計(jì)算
Date有的LocalDateTime都有,Date沒(méi)有的LocalDateTime也有。
一、Date的弊端以及SimpleDateFormat的安全問(wèn)題
1??Date 如果不格式化,打印出的日期可讀性差
Thu Mar 19 10:25:36 CST 2020
2??使用 SimpleDateFormat 對(duì)時(shí)間進(jìn)行格式化,但并不是線(xiàn)程安全的。該類(lèi)的 format() 代碼:
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
calendar 是共享變量,并且這個(gè)共享變量沒(méi)有做線(xiàn)程安全控制。當(dāng)多個(gè)線(xiàn)程同時(shí)使用同一個(gè)【如用 static 修飾的】SimpleDateFormat 對(duì)象調(diào)用 format 方法時(shí),多個(gè)線(xiàn)程會(huì)同時(shí)調(diào)用 calendar.setTime 方法,一個(gè)線(xiàn)程剛設(shè)置好的 time 值有可能被另一個(gè)線(xiàn)程設(shè)置的 time 值覆蓋,導(dǎo)致返回的格式化時(shí)間錯(cuò)誤。SimpleDateFormat 除了 format 是線(xiàn)程不安全以外,parse 方法也是線(xiàn)程不安全的。parse 方法實(shí)際調(diào)用 alb.establish(calendar).getTime() 來(lái)解析,alb.establish(calendar) 里主要完成了以下三步(這三步不是原子操作):
- 重置日期對(duì)象 cal 的屬性值
- 使用 calb 中中屬性設(shè)置 cal
- 返回設(shè)置好的 cal 對(duì)象
多線(xiàn)程并發(fā)如何保證線(xiàn)程安全:
- 避免線(xiàn)程之間共享一個(gè) SimpleDateFormat 對(duì)象,每個(gè)線(xiàn)程使用時(shí)都創(chuàng)建一次SimpleDateFormat 對(duì)象 =>
創(chuàng)建和銷(xiāo)毀對(duì)象的開(kāi)銷(xiāo)大
- 對(duì)使用 format 和 parse 方法的地方進(jìn)行加鎖 =>
線(xiàn)程阻塞性能差
- 使用 ThreadLocal 保證每個(gè)線(xiàn)程最多只創(chuàng)建一次 SimpleDateFormat 對(duì)象 =>
較好的方法
tip:
Date 對(duì)時(shí)間處理比較麻煩,比如想獲取某年、某月、某星期,以及 n 天以后的時(shí)間。Date 類(lèi)的 getYear、getMonth 這些方法,雖然獲取年月日很容易,但都被棄用了。因?yàn)?Date 類(lèi)在設(shè)計(jì)中有很多問(wèn)題,如 getYear 指的是 1900 年以來(lái)的年數(shù),getMonth 是從 0 開(kāi)始的。事實(shí)上,不止 Date 類(lèi),Java 的其余時(shí)間相關(guān)類(lèi)都存在設(shè)計(jì)問(wèn)題,以下舉些例子,并提供解決方案。
- Date 的缺陷。Date 的 setYear 和 getYear 等函數(shù)是刪除線(xiàn)顯示的。原因在:比如今天是 2009-01-04,那么獲取的年竟然是 109,是有問(wèn)題的。
- Calender 常常用于時(shí)間的回卷,經(jīng)常使用的就是 roll(Day_of_Year,-7) 就是七天前。但是如果是 2009-01-04,那么七天前是 2009-12-28,而非 2008 年,這是因?yàn)樗粚?duì)天回卷了,年沒(méi)有回卷。
二、Java8 全新的日期和時(shí)間API
1??LocalDate 只獲取年月日
創(chuàng)建 LocalDate
//獲取當(dāng)前年月日
LocalDate localDate = LocalDate.now();
System.out.println(localDate);//2008-08-08
//構(gòu)造指定的年月日
LocalDate localDate1 = LocalDate.of(2008, 8, 8);
System.out.println(localDate1);//2008-08-08
獲取年、月、日、星期幾
int year = localDate.getYear();//2008
int year1 = localDate.get(ChronoField.YEAR);//2008
Month month = localDate.getMonth();//August
int month1 = localDate.get(ChronoField.MONTH_OF_YEAR);//8
int day = localDate.getDayOfMonth();//8
int day1 = localDate.get(ChronoField.DAY_OF_MONTH);//8
DayOfWeek dayOfWeek = localDate.getDayOfWeek();//Friday
int dayOfWeek1 = localDate.get(ChronoField.DAY_OF_WEEK);//5
第一個(gè)例子通過(guò)靜態(tài)工廠(chǎng)方法now()創(chuàng)建了當(dāng)天日期。 另一個(gè)工廠(chǎng)方法 LocalDate.of() 創(chuàng)建任意日期, 該方法需要傳入年、月、日做參數(shù),返回對(duì)應(yīng)的 LocalDate 實(shí)例。這個(gè)方法的好處是沒(méi)再犯老 API 的設(shè)計(jì)錯(cuò)誤,比如年度起始于 1900,月份是從 0 開(kāi)始等等。日期所見(jiàn)即所得。
2??LocalTime 只獲取幾點(diǎn)幾分幾秒
創(chuàng)建 LocalTime
LocalTime localTime = LocalTime.of(8, 8, 8);
System.out.println(localTime);//08:08:08
LocalTime localTime1 = LocalTime.now();
System.out.println(localTime1);//08:08:08.008
獲取時(shí)分秒
//獲取小時(shí)
int hour = localTime.getHour();
int hour1 = localTime.get(ChronoField.HOUR_OF_DAY);
//獲取分
int minute = localTime.getMinute();
int minute1 = localTime.get(ChronoField.MINUTE_OF_HOUR);
//獲取秒
int second = localTime.getSecond();
int second1 = localTime.get(ChronoField.SECOND_OF_MINUTE);
3??LocalDateTime 獲取年月日時(shí)分秒,等于 LocalDate+LocalTime
創(chuàng)建 LocalDateTime
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime localDateTime1 = LocalDateTime.of(2008, Month.August, 8, 8, 8, 8);
LocalDateTime localDateTime2 = LocalDateTime.of(localDate, localTime);
LocalDateTime localDateTime3 = localDate.atTime(localTime);
LocalDateTime localDateTime4 = localTime.atDate(localDate);
獲取 LocalDate
LocalDate localDate2 = localDateTime.toLocalDate();
獲取 LocalTime
LocalTime localTime2 = localDateTime.toLocalTime();
4??Instant 獲取秒數(shù)
//創(chuàng)建Instant對(duì)象:通過(guò)靜態(tài)工廠(chǎng)方法now()返回當(dāng)前的時(shí)間戳
Instant instant = Instant.now();//2008-08-08T08:08:08.888Z
//獲取秒數(shù)---1970-01-01T00:00:00Z時(shí)代的秒數(shù)
long currentSecond = instant.getEpochSecond();//1590200686
//獲取毫秒數(shù)---1970-01-01T00:00:00Z時(shí)代的毫秒數(shù)
long currentMilli = instant.toEpochMilli();//1590200686487
如果只是為了獲取秒數(shù)或者毫秒數(shù),使用System.currentTimeMillis()來(lái)得更為方便。
5??Clock 時(shí)鐘類(lèi)
Java8 增加了一個(gè) Clock 時(shí)鐘類(lèi)用于獲取當(dāng)時(shí)的時(shí)間戳,或當(dāng)前時(shí)區(qū)下的日期時(shí)間信息。以前用到 System.currentTimeInMillis() 和 TimeZone.getDefault() 的地方都可用 Clock 替換。
public static void main(String[] args) {
//獲取最佳可用系統(tǒng)時(shí)鐘,使用UTC時(shí)區(qū)轉(zhuǎn)換為日期和時(shí)間。
Clock clock = Clock.systemUTC();
System.out.println(clock);//SystemClock[Z]
// 根據(jù)系統(tǒng)時(shí)鐘區(qū)域返回時(shí)間
Clock defaultClock = Clock.systemDefaultZone();
System.out.println(defaultClock);//SystemClock[Asia/Shanghai]
}
6??LocalDate 類(lèi)的 isLeapYear() 校驗(yàn)閏年
public static void main(String[] args) {
LocalDate ld = LocalDate.now();
System.out.println(ld.isLeapYear());
}
7??計(jì)算兩個(gè)日期之間的天數(shù)和月數(shù)
有一個(gè)常見(jiàn)日期操作是計(jì)算兩個(gè)日期之間的天數(shù)、周數(shù)或月數(shù)。在 Java8 中可以用java.time.Period類(lèi)來(lái)做計(jì)算。
public static void main(String[] args) {
LocalDate start = LocalDate.now();
LocalDate end = LocalDate.of(2020, 8, 23);
Period per = Period.between(start, end);
System.out.println(per);//P3M
}
三、修改LocalDate、LocalTime、LocalDateTime、Instant
LocalDate、LocalTime、LocalDateTime、Instant為不可變對(duì)象,修改這些對(duì)象會(huì)返回一個(gè)副本。
1??增加、減少年數(shù)、月數(shù)、天數(shù)等,以 LocalDateTime 為例。它的 plus() 用來(lái)增加月、周、天、時(shí)、秒,甚至一個(gè)世紀(jì),ChronoUnit 類(lèi)聲明了這些時(shí)間單位。由于不可變,返回后一定要用變量賦值。
LocalDateTime localDateTime = LocalDateTime.of(2008, Month.August, 8,
8, 8, 8);
//增加一年
localDateTime = localDateTime.plusYears(1);
localDateTime = localDateTime.plus(1, ChronoUnit.YEARS);
//減少一個(gè)月
localDateTime = localDateTime.minusMonths(1);
localDateTime = localDateTime.minus(1, ChronoUnit.MONTHS);
2??通過(guò) with 修改某些值//修改年為 2008
localDateTime = localDateTime.withYear(2009);
//修改為2008
localDateTime = localDateTime.with(ChronoField.YEAR, 2008);
3??還可以修改月、日
時(shí)間計(jì)算
想知道這個(gè)月的最后一天是幾號(hào)、下個(gè)周末是幾號(hào),通過(guò)提供的時(shí)間和日期 API 可以很快得到答案
LocalDate localDate = LocalDate.now();
LocalDate localDate1 = localDate.with(firstDayOfYear());
比如通過(guò) firstDayOfYear() 返回了當(dāng)前日期的第一天日期,還有很多方法這里不在舉例說(shuō)明。
格式化時(shí)間
LocalDate localDate = LocalDate.of(2008, 8, 8);
String s1 = localDate.format(DateTimeFormatter.BASIC_ISO_DATE);
String s2 = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
//自定義格式化
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String s3 = localDate.format(dateTimeFormatter);
DateTimeFormatter 默認(rèn)提供了多種格式化方式,如果默認(rèn)提供的不能滿(mǎn)足要求,可以通過(guò) DateTimeFormatter 的 ofPattern 方法創(chuàng)建自定義格式化方式
解析時(shí)間
LocalDate localDate1 = LocalDate.parse("20190910", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate localDate2 = LocalDate.parse("2019-09-10", DateTimeFormatter.ISO_LOCAL_DATE);
和SimpleDateFormat相比,DateTimeFormatter是線(xiàn)程安全的
四、SpringBoot中應(yīng)用LocalDateTime
1??將 LocalDateTime 字段以時(shí)間戳的方式返回給前端。
添加日期轉(zhuǎn)化類(lèi)
public class LocalDateTimeConverter extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeNumber(value.toInstant(ZoneOffset.of("+8")).toEpochMilli());
}
}
并在 LocalDateTime 字段上添加如下注解:
@JsonSerialize(using = LocalDateTimeConverter.class)
protected LocalDateTime gmtModified;
2??將 LocalDateTime 字段以指定格式化日期的方式返回給前端。添加如下注解即可:
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
protected LocalDateTime gmtModified;
3??對(duì)前端傳入的日期進(jìn)行格式化。
在 LocalDateTime 字段上添加如下注解即可:
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
protected LocalDateTime gmtModified;
五、總結(jié):Java8 日期時(shí)間 API 的重點(diǎn)
1??提供了javax.time.ZoneId獲取時(shí)區(qū)。
2??提供了 LocalDate 和 LocalTime 類(lèi)。
3??Java8 的所有日期和時(shí)間API都是不可變類(lèi)并且線(xiàn)程安全
,而現(xiàn)有的Date和Calendar API中的java.util.Date和SimpleDateFormat是非線(xiàn)程安全的。
4??主包是 java.time。包含了表示日期、時(shí)間、時(shí)間間隔的一些類(lèi)。里面有兩個(gè)子包java.time.format用于格式化, java.time.temporal用于更底層的操作。
5??時(shí)區(qū)代表了地球上某個(gè)區(qū)域內(nèi)普遍使用的標(biāo)準(zhǔn)時(shí)間。每個(gè)時(shí)區(qū)都有一個(gè)代號(hào),格式通常由區(qū)域/城市構(gòu)成(Asia/Tokyo),再加上與格林威治或 UTC 的時(shí)差。例如:東京的時(shí)差是+09:00。
Instant:瞬時(shí)實(shí)例。
6??關(guān)鍵類(lèi)
- LocalDate:本地日期,不包含具體時(shí)間。例如:2008-08-08 可以用來(lái)記錄生日、紀(jì)念日、加盟日等。
- LocalTime:本地時(shí)間,不包含日期。
- LocalDateTime:組合了日期和時(shí)間,但不包含時(shí)差和時(shí)區(qū)信息。
- ZonedDateTime:最完整的日期時(shí)間,包含時(shí)區(qū)和相對(duì) UTC 或格林威治的時(shí)差。
- 新 API 還引入了 ZoneOffSet 和 ZoneId 類(lèi),使得解決時(shí)區(qū)問(wèn)題更為簡(jiǎn)便。解析、格式化時(shí)間的 DateTimeFormatter 類(lèi)也全部重新設(shè)計(jì)。