高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210 后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢? 高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记上标注着:5343,因此可算出那天是:1791年12月15日。高斯获得博士学位的那天日记上标着:8113 。现在请算出高斯获得博士学位的年月日?
天才少年
可能大部分同学都听说过一个知名的故事。一位小学老师,为了让同学们停止吵闹,给出了一道数据题 1+2+3+…+100 = ? 原本以为可以让他们安静二三十分钟,结果1分钟不到,就有一个小朋友举手回答了出来,老师漫不经心的看了一眼答案,万万没想到竟然是正确的。
这个小朋友只有9岁,他就是高斯。
日记里的秘密
高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210
后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?
高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记上标注着:5343,因此可算出那天是:1791年12月15日。高斯获得博士学位的那天日记上标着:8113 。现在请算出高斯获得博士学位的年月日?
解题思路
我们来抽象一下问题,本质上是要求取某年某月某日后的n天,是哪年哪月哪日?
方案一:循环遍历法
比较容易想到的就是遍历的方式,循环n次,就可以推导出n天后的日期了,我们来尝试一下。
public static void calc(int year, int month, int day, int n) {
for (int i = 0; i < n; i++) {
// 日子一天天过
day++;
// 如果过到月底,需要把日期重置,月份+1
if(day > getMonthLastDay(year,month)){
day = 1;
month++;
// 如果过到年底,需要把月份和日期同时重置,年份+1
if(month >12){
month = 1;
day = 1;
year++;
}
}
}
System.out.println( n + "天后的日期为:"+year + "-" + month + "-" +day);
}
先把已知条件带入,在main函数中执行 calc(1777,4,30,5343);
可以得到结果为1791年12月16日,和题目上的1791年12月15日相差一天,这是什么原因呢?
细想可以察觉,高斯生日的这一天其实被算作了第一天的,也就是我们带入方法时,需要传入1777年4月29日
calc(1777,4,29,5343); 的运行结果是 1791年12月15日
而 calc(1777,4,29,8113); 的运行结果是 1799年7月16日,可以想见高斯在22岁时就已获得博士学位了,真正的年少得志。
方案二:善用工具法
jdk8给我们提供了一系列很友好的日期api,让我们求解此类问题时,可以快速拿到答案。
// 创建一个高斯生日前一天的日期
LocalDate date = LocalDate.of(1777,4,29);
// 调用增加天数的方法,能够直接获得n天后的日期
System.out.println(date.plusDays(8113));
方案比较
既然有我们自己来书写的方案,还有jdk提供的方案,那么问题来了,哪种方案执行更快、效率更好呢?
long start = System.currentTimeMillis();
calc(1777, 4, 29, 8113);
long cost = System.currentTimeMillis() - start;
System.out.println("耗时" + cost);
System.out.println("===========");
start = System.currentTimeMillis();
//日期工具 jdk8 joda time
LocalDate date = LocalDate.of(1777, 4, 29);
System.out.println(date.plusDays(8113));
cost = System.currentTimeMillis() - start;
System.out.println("耗时" + cost);
我们通过毫秒计算工具来测试一下,得到结果如下
注意,这里的时间单位是ms,两种方案实际相差为0.1s左右。
大跌你的眼镜吧,我们自己写的实现竟然比jdk提供的实现方式快,这是为什么呢?
源码分析
我们深入LocalDate类的实现看一下,和我们自己实现的有何区别?
public LocalDate plusDays(long daysToAdd) {
if (daysToAdd == 0) {
return this;
}
// addExact就是一个简单的加法运算
// toEpochDay() 其中epoch代表的是元年,计算机元年是1970年1月1日,这里计算的是当前日期距离元年的天数
// 当前日期距离元年的天数 + 当前日期过后的n天 = n天后距离元年的天数
long mjDay = Math.addExact(toEpochDay(), daysToAdd);
// ofEpochDay(int n) 是计算距离元年n天的日期
return LocalDate.ofEpochDay(mjDay);
}
再点击进 toEpochDay() 看一下
public long toEpochDay() {
long y = year;
long m = month;
long total = 0;
total += 365 * y;
if (y >= 0) {
total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400;
} else {
total -= y / -4 - y / -100 + y / -400;
}
total += ((367 * m - 362) / 12);
total += day - 1;
if (m > 2) {
total--;
if (isLeapYear() == false) {
total--;
}
}
return total - DAYS_0000_TO_1970;
}
可以看到,这个方法都是通过公式来计算的,而 ofEpochDay() 也同样如此,所以我们可以简单把工具法等价为公式法。
总结
为什么循环法比公式法还快呢,我们拉长一下这个问题。当要计算更多天数之后的日期时,两者的表现如何呢?
我们计算 8113333 天后的日期,可以发现的循环法耗时和公式法耗时基本一致,都在0.1s左右。再增加到81133333 天后呢,可以明显的看到公式法仍然保持在0.1s,而循环法的耗时变为1s以上,大幅提高。
这说明在计算次数增加之后,循环法的耗时会成斜线增长,而公式法的耗时基本保持在水平直线上,这也是jdk的设计者们所做的权衡。对我们自身来讲,不同的应用场景下,可以选择不同的实现方式,没有最好的,只有最适合的,你get到了吗?