又踩坑了,java日期閏年處理,算少一天!
前言
今年是2024年,剛好是閏年。大家都知道,閏年是有366天的,其中二月份有29天。最近作者有個項目組出了一個生產(chǎn)問題,跟閏年相關(guān)的。所以寫篇文章跟大家講講這個bug,順便講講Java日期處理的一些坑,讓大家避坑~
1. 閏年處理的坑
這個bug是如何產(chǎn)生的呢?我給大家舉個例子:
比如產(chǎn)品要求支持查詢一年之間內(nèi)的交易流水,并且對這個時間區(qū)間的檢驗。用戶選了開始時間和結(jié)束時間后,前端先檢驗,檢驗通過后,傳到后端。后端也檢驗,然后開始查詢。
假設(shè)當天時間 2024.02.29日,客戶選的開始時間是2023.02.28,結(jié)束時間是2024.02.29.前端檢驗的時候,是用終止時間 2024.02.29減12個月,得到開始時間2023.02.28,因此認為區(qū)間是合法的后端檢驗的時候,用開始時間 2023.02.28去加上12個月,得到2024.02.28,即得到2023.02.28 ~ 2024.02.28,因此認為這個時間范圍是不合法的。這就前后端不一致了,產(chǎn)生bug了。
后端偽代碼是這樣實現(xiàn)的:
public class Test {
public static void main(String[] args) {
// 從前端獲取的日期字符串
String startDateString = "20230228"; // 替換為從前端獲取的startDate
String endDateString = "20240229"; // 替換為從前端獲取的endDate
// 將字符串轉(zhuǎn)換為LocalDate對象
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate startDate = LocalDate.parse(startDateString, formatter);
LocalDate endDate = LocalDate.parse(endDateString, formatter);
// 將startDate增加12個月
LocalDate startDatePlus12Months = startDate.plusMonths(12);
// 檢查是否在時間區(qū)間范圍內(nèi)
if (startDatePlus12Months.isBefore(endDate)) {
System.out.println("時間區(qū)間超過一年");
throw new RuntimeException();
} else {
System.out.println("時間區(qū)間小于一年");
}
}
}
運行結(jié)果拋出了一場,輸出時間區(qū)間超過一年
其實這個例子,就是我們是否把閏年的02.29日這一天算進去,怎么理解都算合理,但是就是要避免前后端不一致的坑。
關(guān)于閏年處理的坑,我總結(jié)為主要是這幾個方面:
-
在一個日期值上加或減時間的代碼,比如像加減1年或1個月的代碼 -
數(shù)據(jù)庫查詢,報表這些,月度和年度統(tǒng)計可能會少算 1 天 -
證書/密碼/密鑰/緩存 等的過期時間,可能會比預(yù)期的早了一天 固定長度數(shù)組,長度365時不夠的 -
判斷是否是閏年的方法, 4年一潤,百年不潤,四百年又潤
小伙伴們在開發(fā)代碼的時候,注意就好啦~
2. 日期格式y(tǒng)yyy-MM-dd HH:mm:ss 大小寫含義區(qū)別
YYYY 和 yyyy 區(qū)別
-
年份大寫YYYY: 當天所在的周屬于的年份,一周從周日開始,周六結(jié)束,只要本周跨年,那么這周就算進入下一年。 -
yyyy: 表示四位年份。
比如看這個例子:
從輸出結(jié)果,大家可以發(fā)現(xiàn),同一個日期,只是轉(zhuǎn)化了一下格式,日期就變了。就是因為年份YYYY大寫的問題。
HH 和 hh 區(qū)別
-
小時大寫 HH:代表24小時制的小時。 -
hh:是12制的日期格式,當時間為12點,會處理為0點。
3.SimleDateFormat的format初始化問題
4. DateTimeFormatter 日期本地化問題
反例:
String dateStr = "Wed Mar 18 10:00:00 2020";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy");
LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter);
System.out.println(dateTime);
運行結(jié)果:
Exception in thread "main" java.time.format.DateTimeParseException: Text 'Wed Mar 18 10:00:00 2020' could not be parsed at index 0
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
at java.time.LocalDateTime.parse(LocalDateTime.java:492)
at com.example.demo.SynchronizedTest.main(SynchronizedTest.java:19)
解析:
DateTimeFormatter 這個類默認進行本地化設(shè)置,如果默認是中文,解析英文字符串就會報異常??梢詡魅胍粋€本地化參數(shù)(Locale.US)解決這個問題
正例:
String dateStr = "Wed Mar 18 10:00:00 2020";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy",Locale.US);
LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter);
System.out.println(dateTime);
5. Calendar設(shè)置時間的坑
反例:
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR, 10);
System.out.println(c.getTime());
運行結(jié)果:
Thu Mar 26 22:28:05 GMT+08:00 2020
解析:
我們設(shè)置了10小時,但運行結(jié)果是22點,而不是10點。因為Calendar.HOUR默認是按12小時制處理的,需要使用Calendar.HOUR_OF_DAY,因為它才是按24小時處理的。
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 10);
6.日期計算的坑
日期計算,比如new Date()加一個30天,有些小伙伴可能會這么寫:
得到的日期居然比當前日期還要早,根本不是晚 30 天的時間。這是因為發(fā)生了int發(fā)生了溢出。
可以把30修改為 30L,或者直接用Java 8的加減API,如下:
7. SimpleDateFormat 線性安全問題
比如這個例子:
public class SimpleDateFormatTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000));
while (true) {
threadPoolExecutor.execute(() -> {
String dateString = sdf.format(new Date());
try {
Date parseDate = sdf.parse(dateString);
String dateString2 = sdf.format(parseDate);
System.out.println(dateString.equals(dateString2));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
}
SimpleDateFormat 是個共享變量,多線程跑的時候,就會有問題:
Exception in thread "pool-1-thread-49" java.lang.NumberFormatException: For input string: "5151."
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.example.demo.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:19)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-47" java.lang.NumberFormatException: For input string: "5151."
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.example.demo.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:19)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
或者直接使用DateTimeFromatter
8. Java日期的夏令時問題
反例:
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.parse("1986-05-04 00:30:00"));
運行結(jié)果:
Sun May 04 01:30:00 CDT 1986
解析:
先了解一下夏令時
-
夏令時,表示為了節(jié)約能源,人為規(guī)定時間的意思。 -
一般在天亮早的夏季人為將時間調(diào)快一小時,可以使人早起早睡,減少照明量,以充分利用光照資源,從而節(jié)約照明用電。 -
各個采納夏時制的國家具體規(guī)定不同。目前全世界有近 110個國家每年要實行夏令時。 -
1986年4月,中國中央有關(guān)部門發(fā)出“在全國范圍內(nèi)實行夏時制的通知”,具體作法是:每年從四月中旬第一個星期日的凌晨2時整(北京時間),將時鐘撥快一小時。(1992年起,夏令時暫停實行。) -
夏時令這幾個時間可以注意一下哈, 1986-05-04, 1987-04-12, 1988-04-10, 1989-04-16, 1990-04-15, 1991-04-14。
結(jié)合demo代碼,中國在1986-05-04當天還在使用夏令時,時間被撥快了1個小時。所以0點30分打印成了1點30分。如果要打印正確的時間,可以考慮修改時區(qū)為東8區(qū)。
正例:
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.parse("1986-05-04 00:30:00"));
???? 點擊下方閱讀原文,獲取魚皮往期編程干貨。
往期推薦
