一个小问题,成为了一个坑。

相信大家对这个题目——Java获取每个月的最后一天——都不陌生吧。其实,不纠结于最后一天啦,也可以是上个月的最后一天,下个月的第一天,等等之类的。我发现网上都是写好的一些例子,提供给大家解决那些固定要获取的一个月的最后一天或者第一天,但是代码注释却又惜字如金,导致使用者在完全不理解的情况下,Ctrl+C和Ctrl+V,一个坑就暗含在了这里。

先上一组“凌乱”的代码,根据方法名,这个函数是要求得每月的最后一天:

public static String getDateLastDay(String year, String month) {//year="2018" month="2" Calendar calendar = Calendar.getInstance();// 设置时间,当前时间不用设置calendar.set(Calendar.YEAR, Integer.parseInt(year));calendar.set(Calendar.MONTH, Integer.parseInt(month));// System.out.println(calendar.getTime());calendar.set(Calendar.DAY_OF_MONTH, 1); calendar.set(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));DateFormat format = new SimpleDateFormat("yyyy-MM-dd ");return format.format(calendar.getTime());
}

解析:
1. 对于这两行代码:

calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));

产生了一条冗余代码,首先Calendar.DAY_OF_MONTH和Calendar.DATE,本来就是一回事,如果你进入该行的JDK API你就会发现如下的解释:

 /*** Field number for <code>get</code> and <code>set</code> indicating the* day of the month. This is a synonym for <code>DATE</code>.* The first day of the month has value 1.** @see #DATE*/public final static int DAY_OF_MONTH = 5;

这里明确阐明了DAY_OF_MONTH是DATE的同义词,可以理解成别名。
因此,JDK对于 Calendar.DAY_OF_MONTH和Calendar.DATE的取值都是“5”,所以看到有人一会用Calendar.DAY_OF_MONTH进行calendar实例对象的set,一会又用Calendar.DATE进行calendar实例对象的set,可见没有理解这两行代码的意思。

  1. 那个老生常谈的问题。Calendar的体系中,month是从“0”开始数,Day却是从“1”开始数。
    对于month来讲,这句话calendar.set(Calendar.MONTH, Integer.parseInt(month)); 如果设置的是1,那么下面的这句System.out.println(calendar.getTime());打印的将是3月的当前日,可见month是从“0”开始计数;同样的,对于Day,上面的那段JDK代码中也说了“The first day of the month has value 1.” 所以,在这里java8之前的JDK版本对于Calendar的设计确实是存在着凌乱的,也是容易让我们程序员感到困惑的点。

  2. calendar.set和 calendar.add。曾经我也犯过这样的错误,把add和set混用,其实对于set来说,你按照month从0开始,day从1开始这条规则设置的某个月的第几天就是第几天,对比add,也确实是在“0和1”的基线上加天数和月数。
    如果改成这样:

Calendar calendar = Calendar.getInstance();calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 2);
System.out.println(calendar.getTime());calendar.add(Calendar.MONTH, 3);
System.out.println(calendar.getTime());

先后打印的结果就是:

Tue Mar 06 10:50:38 CST 2018
Wed Jun 06 10:50:38 CST 2018

没有问题。
我们可以把Calendar的设置简单想成一个指针,set和add都是以你设置的日期为基准加上之前提到的“0和1”规则,进行指针移动,如果想要后退指针,那么很简单,set或者add负数就可以了。

  1. calendar.set(Calendar.DAY_OF_MONTH, 0)的作用。我们之前说了,day是从1开始的,那么这句话又是什么鬼呢。其实很容易理解,如果按照第三条贴出的代码,此时指针指向了2018-06-06,那么再执行这句话就相当于将指针的天数归零,然后后退一个月,也就是会输出2018-05-31 。记住,如果你这样执行:
Calendar calendar = Calendar.getInstance();calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 2);
System.out.println(calendar.getTime());calendar.add(Calendar.MONTH, 3);
System.out.println(calendar.getTime());calendar.set(Calendar.DAY_OF_MONTH, 12);
calendar.set(Calendar.DAY_OF_MONTH, 0);  

先设置了日子12,这时变成2018-06-12,再执行归零操作,那么还是变为2018-05-31,所以这句话就是将当前指针指向的日期回退到上个月的最后一天。

  1. calendar.getActualMaximum(Calendar.DATE))。在最初的凌乱代码中贴出的calendar.getActualMaximum(Calendar.DATE))这句话需要跟“归零”操作区分开,这句话是真真正正的求当前指针指向的月份的最后一天。强烈不建议把calendar.getActualMaximum(Calendar.DATE))放在calendar.set中,因为会让你对指针走到哪里了产生困惑,尤其在加入了“归零”操作的代码中,比如:
Calendar calendar = Calendar.getInstance();calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 2);
System.out.println(calendar.getTime()); //此处是三月calendar.add(Calendar.MONTH, 3);
System.out.println(calendar.getTime());  //此处是六月calendar.set(Calendar.DAY_OF_MONTH, 12);
System.out.println(calendar.getTime());  //此处是6.12
calendar.set(Calendar.DAY_OF_MONTH, 0);  //此处是5.31System.out.println(calendar.getActualMaximum(Calendar.DATE));
//此处是5月份的31天
System.out.println(calendar.getTime()); //此处是5.31calendar.set(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
System.out.print(format.format(calendar.getTime()));

我想这个例子看了我如下的操作以后,大家都会有困惑的:以上代码最后输出的是2018-05-31,如果你理解了之前讲的,这个结果是没有问题的。但是,如果我把这行:System.out.println(calendar.getTime()); //此处是5.31 注释掉,大家可以试一下,最后的输出结果是2018-07-01。
为什么?这个留给大家作为思考题,因为需要自己跟代码才有印象,总之这里的结果如同执行了——
calendar.add(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));—— “add”方法,而不是“set”方法,因为结果就是在5.31的基础上加上了31天,正好是7.1。

困惑吧,实话说我都没有信息继续深究了,因为太凌乱了,所以,最好就是不要这样写。即不夹杂“归零”操作,也不在set方法中使用这句代码calendar.getActualMaximum(Calendar.DATE),calendar.getActualMaximum()方法就是获得当前月的最大天数,就只是这么用就好了。

  1. 这里还要提到的一个Tip就是DateFormat是线程不安全的,也就是说如果两个线程同时操作最初代码片段中的最后两句话,结果会出现意想不到的事情。

那么,既然Calendar有这样那样的问题,而且时不时会出现凌乱的代码——也可以看出这是JDK最初设计上的一些瑕疵——那么JDK对这部分有没有升级呢?答案当然是肯定的。下面我就介绍一下Java8针对Calendar实现的一个完全的修改。

Java8中引入了LocalDate、LocalTime、Instant、Duration和Period,这也是Oracle在原生的Java API中为引入对日期和时间的高质量的支持整合第三方日期和时间库Joda-Time的基础上提出的。它们都存在于java.time包下:

LocalDate localDate = LocalDate.of(2018, 2, 6);
int year = localDate.getYear();
Month month = localDate.getMonth();
int day = localDate.getDayOfMonth();
DayOfWeek dow = localDate.getDayOfWeek();
int len = localDate.lengthOfMonth();
boolean bool = localDate.isLeapYear();
LocalDate today = LocalDate.now();System.out.println(year);    // 2018
System.out.println(month);   // FEBRUARY
System.out.println(day);     // 6
System.out.println(dow);     // TUESDAY
System.out.println(len);     // 28
System.out.println(bool);    // false
System.out.println(today);   // 2018-02-06

是不是一目了然呢,或许都不用做过多的解释,你就能获取你想要的日期的某一个部分了。而且,最后一个用LocalDate.now()直接获取的是格式化好的日期格式,而不是看起来有点凌乱的这种格式“Sat Feb 10 06:38:55 CST 2018”,是不是很优雅呢。(最后一个是判断闰年,提醒大家,不要自己判断闰年,但是测试的时候一定要考虑到这个规则:能被4和400整除的是闰年,能被100整除的不是闰年)。

同样的新的日期时间JDK也提供了可读性强的枚举类型来实现如上的代码,比如:

int year_value = localDate.get(ChronoField.YEAR);
int month_value = localDate.get(ChronoField.MONTH_OF_YEAR);
int day_value = localDate.get(ChronoField.DAY_OF_MONTH);System.out.println(year_value);    // 2018
System.out.println(month_value);    // 2
System.out.println(day_value);    // 6/** YEAR(* "Year",*  ChronoUnit.YEARS,*  ChronoUnit.FOREVER,*  ValueRange*          .of(-999999999L,*                  999999999L),*  "year"*/

ChronoField实现了TemporalField接口,如果深入到底层代码你会发现我在最后贴出的注释样的源代码——列出了取值范围——等等,大家有兴趣可以参考。

LocalTime也是一样,可以通过getHour()、getMinute()和getSecond()方法获取小时、分钟和秒数。

上面提到了DateFormat在多线程的环境下会出现意想不到的结果,新的JDK也给我们提供了新的选择——DateTimeFormatter,所有的DateTimeFormatter实例都是线程安全的,因此我们可以用单例模式创建格式器实例,然后共享它。

LocalDate localDate = LocalDate.of(2018, 2, 6);
String str1 = localDate.format(DateTimeFormatter.BASIC_ISO_DATE);
String str2 = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);LocalDate date1 = LocalDate.parse("20180206", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2018-02-06", DateTimeFormatter.ISO_LOCAL_DATE);System.out.println(str1);   //20180206
System.out.println(str2);   //2018-02-06System.out.println(date1);
//2018-02-06 ?不知道为什么这句话打印的不是期望的20180206,大家可以帮我看看,我也会在后续的博客中更新调查结果。
System.out.println(date2);  //2018-02-06

正如代码显示的可以使用format方法按照DateTimeFormatter提供的静态成员变量取值进行想要的日期格式化,使用parse获取期望格式的LocalDate对象。

最后,介绍一下对日期和时间的操纵,也是对开篇提到的那段凌乱代码的一个交代。新的JDK提供了对日子和月份加减的优化处理,可读性和简洁性非常棒。

LocalDate localDate = LocalDate.of(2018, 2, 6);
LocalDate localDate1 = localDate.withYear(2019);                        //年份修改为2019
LocalDate localDate2 = localDate1.withDayOfMonth(25);                   //日改为25
LocalDate localDate3 = localDate2.with(ChronoField.MONTH_OF_YEAR, 9);   //月份改为9LocalDate localDate4 = localDate3.plusWeeks(1);                         //此时的日期是2019-09-25,在此基础上增加一周是2019-10-02
LocalDate localDate5 = localDate4.minusYears(3);                        //减去三年 2016-10-02
LocalDate localDate6 = localDate5.plus(6, ChronoUnit.MONTHS);           //加上六个月2017-04-02/* 使用TemporalAdjuster进行更复杂的日期调整 */
//获取以2017-04-02为基准,第一个符合指定星期几要求的日期,2017-04-02就是星期日,程序会直接返回该对象
LocalDate localDate7 = localDate6.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
//获取2017-04-02所处月份的最后一天,同步取值还有lastDayOfNextMonth/firstDayOfMonth/firstDayOfNextMonth,等等
LocalDate localDate8 = localDate7.with(TemporalAdjusters.lastDayOfMonth()); System.out.println(localDate);  //2018-02-06
System.out.println(localDate1); //2019-02-06
System.out.println(localDate2); //2019-02-25
System.out.println(localDate3); //2019-09-25
System.out.println(localDate4); //2019-10-02
System.out.println(localDate5); //2019-10-02
System.out.println(localDate6); //2017-04-02
System.out.println(localDate7); //2017-04-02
System.out.println(localDate8); //2017-04-30

最后的最后,再唠叨两句:
首先,新的JDK处理日期和时间的方式是在JDK8的前提下支持的,如果你的代码还是JDK7或者以下,那么你只能小心的使用Calendar了;
其次,这里针对新的日期和时间处理方式介绍的还不够详细,比如JDK8对日期和时间的处理同时还支持处理不同的时区和历法、利用和UTC/格林尼治时间的固定偏差计算时区以及使用别的日历系统,类似伊斯兰教日历这种复杂的日历。我准备再单独开一篇博客专门介绍,并且提供几个成熟且常用的工具类方法,这次就写到这里了,还要去加班 :)
最后,真的希望光大的程序员小伙伴看看英文,因为再读源码的时候你真的可以省去很多时间,同时也会增加你的耐心,因为对英文头大的同学会看不下去。同时,对于输出结果,比如很简单的Mar和May,英文的熟悉能够帮你迅速的解除类似的混淆,尤其是在焦急的解决bug的时候,我就曾经遇到过把Mar当成May对着输出结果发呆的小伙伴。

真心谢谢你能看到这里,其中的错漏之处还望海涵,我会继续努力的!

java获取每月最后一天相关推荐

  1. java获取每月最后一天_java获取每月的最后一天实现方法

    实例如下: public static void main(String[] args) throws ParseException { // 获取当月的天数(需完善) SimpleDateForma ...

  2. java获取下月末,java获取每月月末日期

    java获取每月月末日期 比如要获取20140831 20140731 20140228 等等,并将结果放入到SELECT表单中. 项目中只需要获取两年即可,所以参数可自行修改. Calendar c ...

  3. java 获取每月每一天_java 求取某一段时间内的每一天、每一月、每一年

    1.求取某一段时间内的每一天 Date date0 = new SimpleDateFormat("yyyy-MM-dd").parse("2014-01-01" ...

  4. java获取月底最后一天 获取当月的上一个月

    //获取当月的上一个月public static String getLastMonth1(Date date) {SimpleDateFormat dft = new SimpleDateForma ...

  5. Oracle 获取每月最后一天的函数

    Oracle中 通过函数LAST_DAY 可以返回某个时间所在月份的最后一天 例如: e.g. SQL> select last_day(sysdate) from dual ; LAST_DA ...

  6. Java 获取指定日期

    Java 获取指定日期每周的第一天和最后一天 private String firstWeekDay(String days)throws Exception{ String first_day=&q ...

  7. Java时间工具类(获取上个月、获取月份1月1号时间戳、获取月份最后一天时间戳...)

    在开发中我们经常还会用到日期时间,以下是一个日期时间工具类,包含: 获取每月月初时间戳 获取每月15号时间戳 获取每月月末时间戳 获取当前被格式化后的日期 从这些方法中你可以改写出你需要的方法 PS: ...

  8. php每个月头一天与最后一天,PHP获取每月第一天与最后一天

    function getthemonth($date) { $firstday = date('Y-m-01', strtotime($date)); $lastday = date('Y-m-d', ...

  9. Java获取指定月份的最后一天

    https://blog.csdn.net/itmyhome1990/article/details/85619804 Java获取指定月份的最后一天 麦田 2019-01-02 17:43:38   ...

  10. java当月最后一天_Java日期循环,Java获取当月的第一天和最后一天 | 学步园

    1.Java日期循环 SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd"); String dateStr1 ...

最新文章

  1. JAVA07 接口与继承
  2. 浅析正则表达式模式匹配的 String 方法
  3. 《自然》:欧洲根据已知基因序列合成新冠病毒,助力疫苗开发
  4. 少儿编程语言python-Python少儿编程系列-前言-为什么学Python?
  5. 【bzoj2300】【Luogu P2521】 [HAOI2011]防线修建 动态凸包,平衡树,Set
  6. php cdi_本机CDI限定词:@Any和@Default
  7. js 获得明天0点时间戳_js实现一个简单钟表动画(javascript+html5 canvas)
  8. android+模拟器上gdb,使用gdb在Android Emulator中进行调试c程序
  9. python 等差素数列
  10. JAVA求解一元二次方程
  11. 六键无冲和全键无冲哪个好_键盘冷知识:全键无冲到底是什么?
  12. 【BSP视频教程】STM32H7视频教程第4期:从启动到运行过程全解析,电源域,复位,时钟,软硬件启动流程到堆栈,map和htm文件分析(2022-01-26)
  13. 在 JDK 1.4 中打印
  14. Java接口的定义、使用及成员特点
  15. android m1卡读写
  16. win10u盘被写保护怎么解除_win10系统所有usb都被写保护怎么取消_win10系统usb被写保护怎么办...
  17. 自动驾驶汽车的库尔雪维尔测试是不可能的挑战
  18. 全球与中国切板刀市场深度研究分析报告
  19. MVC与三层架构之间的关系
  20. Parboiled学习(一)

热门文章

  1. 计算机主机怎么关,教你电脑关不了机怎么办
  2. java执行cls命令_java开发系统内核:cls 屏幕清空命令的实现
  3. windows10系统插耳机有回声解决办法?
  4. 对话Huobi Prime首发项目TOP:李林80%的时间都在问项目能否落地
  5. weka,FP,priori
  6. 就算是一件小事也要做好——买土豆的故事
  7. 2022年「医疗行业」市场研报合集(共228份)
  8. 当你对未来迷茫的时候,请打开这个锦囊
  9. VMware Workstation 与 Device/Credential Guard 不兼容。在禁用 Device/Credential Guard
  10. git 报错:unable to access “http://gitlab.tydic.com:7666/tx-preformance/tx……“解决方法