近期,阿里巴巴发布了《Java开发手册(泰山版)》。
摘录部分官网的说明
《Java开发手册》始于阿里内部规约,在全球Java开发者共同努力下,已成为业界普遍遵循的开发规范。
手册涵盖编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程规约、设计规约七大维度。
此次泰山版发布,将带来三大亮点:新增5条日期时间规约;新增2条表别名sql规约;新增统一错误码规约。
阿里在2016年12月7日首次向业界开放《Java开发手册》以来,获得了绝大部分开发者的认可,目前已经成为行业的事实标准。很多公司直接引用作为内部的编码规范标准,或者在此基础上再针对自己行业的情况来扩展一些内部规约来形成自己的编码规范标准。在开发手册的指导下,限制了过度的个性化,以一个相对统一的标准,提升了代码的可读性,降低了沟通成本。
现在一些公司在面试Java开发的时候,会要求看候选人的github上的代码,来考察候选人在编码规范、风格方面是否能符合业界统一的规范,因为规范的代码会给团队带来很多好处,逻辑清晰,阅读友好,别人接手起来也易于维护和升级。所以,作为开发者来说,学习最新版《Java开发手册》,不仅是对自身编码能力的提升,对团队协作、适用这个不断变化的开发生态也很重要。
因为之前学习过之前的版本,所以本次主要针对新增的内容进行一下学习,也说说自己的理解,今天先来说的是,日期和时间相关的内容。
先来看一下《Java开发手册(泰山版)》关于这块的内容
1. 【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。
说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY就是下一年。
正例:
表示日期和时间的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
2. 【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。
说明:日期格式中的这两对字母表意如下:1) 表示月份是大写的 M; 2) 表示分钟则是小写的 m; 3) 24 小时制的是大写的 H; 4) 12 小时制的则是小写的 h。
3. 【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime()。
说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。
4. 【强制】不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。
说明:第 1 个不记录时间,getHours()抛出异常;第 2 个不记录日期,getYear()抛出异常;第 3 个在构造方法 super((time/1000)*1000),fastTime 和 nanos 分开存储秒和纳秒信息。反例: java.util.Date.after(Date)进行时间比较时,当入参是 java.sql.Timestamp 时,会触发 JDK BUG(JDK9 已修复),可能导致比较时的意外结果。
5. 【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误。
正例:
// 获取今年的天数
int daysOfThisYear = LocalDate.now().lengthOfYear();
// 获取指定某年的天数
LocalDate.of(2011, 1, 1).lengthOfYear();
反例:
// 第一种情况:在闰年 366 天时,出现数组越界异常
int[] dayArray = new int[365];
// 第二种情况:一年有效期的会员制,今年 1 月 26 日注册,硬编码 365 返回的却是 1 月 25 日
Calendar calendar = Calendar.getInstance();
calendar.set(2020, 1, 26);
calendar.add(Calendar.DATE, 365);
6. 【推荐】避免公历闰年 2 月问题。闰年的 2 月份有 29 天,一年后的那一天不可能是 2 月 29日。
7. 【推荐】使用枚举值来指代月份。如果使用数字,注意 Date,Calendar 等日期相关类的月份month 取值在 0-11 之间。
说明:参考 JDK 原生注释,Month value is 0-based. e.g., 0 for January.正例: Calendar.JANUARY,Calendar.FEBRUARY,Calendar.MARCH 等来指代相应月份来进行传参或比较。
我们一条一条来拆解。
1. 【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。
平时不注意小细节的同学,可能已经在2019-12-31遇到了这个问题了,先来看一下下面代码:
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, 2019);
calendar.set(Calendar.MONTH, Calendar.DECEMBER);
calendar.set(Calendar.DAY_OF_MONTH, 31);
SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat format2 = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
System.out.println("2019-12-31 用yyyy格式化的结果:" + format1.format(calendar.getTime()));
System.out.println("2019-12-31 用YYYY格式化的结果:" + format2.format(calendar.getTime()));
运行结果:
2019-12-31 用yyyy格式化的结果:2019-12-31 19:24:55
2019-12-31 用YYYY格式化的结果:2020-12-31 19:24:55
可以看到,2019-12-31用YYYY格式化的结果是2020年,不太符合我们的预期。原因是,YYYY格式化年份,是按照这一周(按照国际惯例,一周从周日开始,到周六结束)所属的年份确定的,而如果这一周如果刚好跨年了,那么这一周所有的日期的年份格式化都会是新的一年。查询万年历,看到下图:
可以看到,这一周是从2019-12-29至2020-01-04,所以29、30、31这几天用YYYY去格式化的时候,都会显示成2020。
说到这里,其实不止yyyy和YYYY,mm和MM,dd和DD,大小写所代表的含义都是不同的,整理了一份资料,可以参考
y 年 Year 1996; 96
M 年中的月份 Month July; Jul; 07
w 年中的周数 Number 27
W 月份中的周数 Number 2
D 年中的天数 Number 189
d 月份中的天数 Number 10
F 月份中的星期 Number 2
E 星期中的天数 Text Tuesday; Tue
a Am/pm 标记 Text PM
H 一天中的小时数e799bee5baa6e997aee7ad94e4b893e5b19e31333363383438(0-23) Number 0
k 一天中的小时数(1-24) Number 24
K am/pm 中的小时数(0-11) Number 0
h am/pm 中的小时数(1-12) Number 12
m 小时中的分钟数 Number 30
s 分钟中的秒数 Number 55
S 毫秒数 Number 978
z 时区 General time zone Pacific Standard Time; PST; GMT-08:00
Z 时区 RFC 822 time zone -0800
顺便说下,上面有1行代码大家可能没特别留意
calendar.set(Calendar.MONTH, Calendar.DECEMBER);
这里设置月份的时候,值的范围是0-11,代表我们认知中的1月-12月,最好直接使用Calendar.DECEMBER这样的常量,点进去也可以清晰看到常量的值和注释,这样代码可读性更高,也更加健壮
/**
* Value of the {@link #MONTH} field indicating the
* twelfth month of the year in the Gregorian and Julian calendars.
*/
public static final int DECEMBER = 11;
2. 【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。
这条在上一条里面讲过了,不重复,可以参考上面的含义表。
3. 【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime()。
这条我理解一方面是效率上的考虑,一方面是不用new这个Date对象出来。有人会问,为什么System.currentTimeMillis()效率更高呢?其实我们看一下Date的getTime方法,一看便知
public long getTime() {
return getTimeImpl();
}
private final long getTimeImpl() {
if (cdate != null && !cdate.isNormalized()) {
normalize();
}
return fastTime;
}
上面2个方法都是Date类里面的,可以看到getTime方法其实是返回了fastTime属性的值,那么这个属性的值是怎么来的呢?我们看下构造方法
/**
* Allocates a {@code Date} object and initializes it so that
* it represents the time at which it was allocated, measured to the
* nearest millisecond.
*
* @see java.lang.System#currentTimeMillis()
*/
public Date() {
this(System.currentTimeMillis());
}
/**
* Allocates a {@code Date} object and initializes it to
* represent the specified number of milliseconds since the
* standard base time known as "the epoch", namely January 1,
* 1970, 00:00:00 GMT.
*
* @param date the milliseconds since January 1, 1970, 00:00:00 GMT.
* @see java.lang.System#currentTimeMillis()
*/
public Date(long date) {
fastTime = date;
}
这回就非常清晰了,这个fastTime的值其实是构造Date对象的时候初始化的,当我们调用无参的构造方法来构造Date对象的时候,其实是使用了System.currentTimeMillis()来作为入参来调用了有参的构造方法,所以如果我们要获取当前时间戳,System.currentTimeMillis()的效率肯定会比new Date().getTime()更高。
当面说道这里,我们也注意到了说明里面的另外一段话
如果想获取更加精确的纳秒级时间值,使用 System.nanoTime 的方式。
那么,这个System.nanoTime是什么呢?
/**
* Returns the current value of the running Java Virtual Machine's
* high-resolution time source, in nanoseconds.
*
* This method can only be used to measure elapsed time and is
* not related to any other notion of system or wall-clock time.
* The value returned represents nanoseconds since some fixed but
* arbitrary origin time (perhaps in the future, so values
* may be negative). The same origin is used by all invocations of
* this method in an instance of a Java virtual machine; other
* virtual machine instances are likely to use a different origin.
*
* This method provides nanosecond precision, but not necessarily
* nanosecond resolution (that is, how frequently the value changes)
* - no guarantees are made except that the resolution is at least as
* good as that of {@link #currentTimeMillis()}.
*
*
Differences in successive calls that span greater than
* approximately 292 years (263 nanoseconds) will not
* correctly compute elapsed time due to numerical overflow.
*
*
The values returned by this method become meaningful only when
* the difference between two such values, obtained within the same
* instance of a Java virtual machine, is computed.
*
*
For example, to measure how long some code takes to execute:
*
{@code
* long startTime = System.nanoTime();
* // ... the code being measured ...
* long elapsedNanos = System.nanoTime() - startTime;}
*
* To compare elapsed time against a timeout, use
{@code
* if (System.nanoTime() - startTime >= timeoutNanos) ...}
* instead of {@code
* if (System.nanoTime() >= startTime + timeoutNanos) ...}
* because of the possibility of numerical overflow.
*
* @return the current value of the running Java Virtual Machine's
* high-resolution time source, in nanoseconds
* @since 1.5
*/
@HotSpotIntrinsicCandidate
public static native long nanoTime();,
官方的注释也比较长,我们挑重点的内容翻译一下:
这个方法会返回一个当前运行java虚拟机的高精度的时间戳,精度到了纳秒。可能对纳秒还没什么概念,我们来比较一下
1s = 1000ms
1s = 1000000000ns
毫秒其实已经是一个挺小的单位了,看到1秒等于那么多纳秒,可以看到所谓的高精度也是名副其实。
但是我们知道currentTimeMillis()返回的是1970年1月1日0点到现在的毫秒数,那么nanoTime()的返回值的含义呢?继续看注释,其实这个时间就是java虚拟机给定的一个时间作为起始,并不具备实际的含义,所以它的作用并不像毫秒时间戳一样,单独就可以表示一个时刻,它常用的地方是做2个时间点的比较,看中间经过了多少时间。例如,我们要知道一个方法运行耗费了多少时间,那么我们就可以在方法的起止获取2个nanoTime(),相减得到的值就是这个方法以纳秒为单位的运行时间。
4. 【强制】不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。
5. 【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误。
6. 【推荐】避免公历闰年 2 月问题。闰年的 2 月份有 29 天,一年后的那一天不可能是 2 月 29日。
这3条就不多做解读了,就是字面含义,大家在代码中如果用到了时间相关的函数的话,上面3条应该本来就有注意吧。
7. 【推荐】使用枚举值来指代月份。如果使用数字,注意 Date,Calendar 等日期相关类的月份month 取值在 0-11 之间。
这条在第1条的解读里面也有提到,就是这个月份在系统中是0-11,跟生活中的1-12这个差异而已。
上面就是我对《Java开发手册(泰山版)》的中新增的日期时间类规范的一些理解,如果有不对的地方,欢迎拍砖,大家一起学习讨论。