《码处高效:Java开发手册》之代码风格
流水淡,碧天长,鸿雁成行。编码风格,简捷清爽,反引无限风光。
在美剧《硅谷》中有这样一个经典镜头,主人公 Richard 与同为开发工程师的女友闹分手,理由是两人对缩进方式有着截然不同的编程习惯,互相鄙视对方的代码风格。Richard 认为" one tab saves four spaces ”,缩进使用 Tab 键操作更快,更节省存 储空间,而女友坚持使用空格缩进,连续四次敲击空格的声音,把 Richard 折磨到几近崩溃,认为这是种精神折磨。 Richard 觉得难以相处,吵完架下楼梯时,不小心摔倒了 还淡定地说,”I just tried to go down the stairs four steps at a time ” (这只是表达我的立场而已)。Tab 键和空恪键的争议在现实编程中确实存在。除此之外,在其他代码风格上,也存在不同的处理方式,往往是谁也说服不了谁,都站在自身“完全正确”的立场上,试图说服对方。这在团队开发效率上,往往是一个巨大的内耗,无休止的争论与最后的收益是成反比的。所以我们认为一致性很重要,就像交通规则一样,我国规定靠右行驶,有些国家则规定靠左行驶,并没有绝对的优劣之分,但是在同一个国家或地区内必须要有统一的标准。代码风格也是如此,无论选择哪一种处理方式,都需要部分人牺牲小我,成就大我,切实提升团队的研发效能。
代码风格并不影响程序运行,没有潜在的故障风险,通常与数据结构、逻辑表达无关,是指不可见字符的展示方式、代码元素的命名方式和代码注释风格等。比如,大括号是否换行、缩进方式、常量与变量的命名方式、注释是否统一放置在代码上方等。代码风格的主要诉求是清爽统一、便于阅读和维护。统一的代码风格可以让开发工程师们没有严重的代码心理壁垒,每个人都可以轻松地阅读并快速理解代码逻辑,便于高效协作,逐步形成团队的代码“味道”。
命名规约
代码元素包括类、方法、参数、常量、变量等程序中的各种要素。合适的命名,可以体现出元素的特征、职责 ,以及元素之间的差异性和协同性。为了统一代码风格,元素的命名要遵守以下约定。
-
命名符合本语言特性
当前主流的编程语言有 50 种左右,分为两大阵营—面向对象与面向过程,但是按变量定义和赋值的要求,分为强类型语言和弱类型语言。每种语言都有自己的独特命名风格,有些语言在定义时提倡以前缀来区分局部变量、全局变量、控件类型。比如 Ii_count 表示 local int 局部整型变量, dw_report 表示 data window 用于展示报表数据的控件。有些语言规定以下画线为前缀来进行命名。这些语言的命名风格,自成一派,也无可厚非,但是在同种语言中,如果使用多种语言的命名风格 就会引起其他开发工程师的反感。比如,在 Java 中,所有代码元素的命名均不能以下画线或美元符号开始或结束。
-
命名体现代码元素特征
命名上可体现出代码元素的特征,仅从名字上即可知道代码元素的属性是什么,有利于快速理清代码脉络。面向对象代码元素的命名形式分为两大类,即首字母大写的 UpperCamelCase 和首字母小写的 lowerCamelCase ,前者俗称大驼峰,后者俗称小驼峰。类名采用大驼峰形式,一般为名词,例如 Object、StringBuffer、 FileInputStream 等。 方法名采用小驼峰形式,一般为动词,与参数组成动宾结构,例如Object的wait()、StringBuffer的append(String)、FileInputStream的read() 等。变量包括参数、成员变量、局部变量等,也采用小驼峰形式。常量的命名方式比较特殊,字母全部大写,单词之间用下画线连接。常量和变量是最基本的代码元素,就像血液中的红细胞一样无处不在。合理的命名有利于保障代码机体的清爽、健康。
在命名时若能体现出元素的特征,则有助于快速识别命名对象的作用,有助于快速理解程序逻辑。我们推荐在 Java 命名时,以下列方式体现元素特征:
- 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,则可以使用复数形式。
- 抽象类命名使用 Abstract或Base 开头,异常类命名使用 Exception 结尾;测试类命名以它要测试的类名开始,以Test 结尾。
- 类型与中括号紧挨相连来定义数组。
- 枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下画线隔开。
-
命名最好望文知义
望文知义是在不需要额外解释的情况下,仅从名称上就能够理解某个词旬的确切含义。在代码元素命名时做到望文知义,从而减少注释内容,达到自解释的目的。在实践中,望文知义的难度是最大的,就好像给孩子起名一样需要反复斟酌。文不对题的命名方式,肯定会加大理解成本,更大的罪过是把程序员引导到一个错误的理解方向上。某些不规范的缩写会导致理解成本增加。比如 condition 缩写成 condi 类似随意的缩写会严重降低代码的可理解性。再比如,以单个字母命名的变量,在上下文理解时 会带来很大的困扰。本书中的所有示例代码都比较精筒,没有具体业务含义。重点在于阐述示例背后的编程思维,所以采用单字母的简洁命名方式,在实际业务代码中请勿模仿。
主流的编程语言基本上以英语为基础,此处望文知义的“文”指的是英文。随着开源社区的发展与繁荣,各国程序员踊跃参与开源项目的共建,国际交流与合作越来越频繁,英语能力已经成为程序员必备的基础技能之一。虽然有人认为命名方式应该符合本国语言习惯,拼音这种命名方式,应该是被允许的,但是在国际化项目或开源项目中,对于非汉语国家的开发工程师而言,拼音这种命名方式的可读性几乎为零。即使在汉语系家,拼音也存在地区差异。中英文混合的方式,更不应该出现。比如在某业务代码中,曾经出现过DaZePromotion ,猜了很久才被命名者告知是打折促销的类。最让人无法容忍的是拼音“首字母”简写的命名方式,即使发挥极致的想象力,也很难猜出具体的含义,比如 PfmxBuilder 名称意思是评分模型的创建工厂类!这些命名方式,极大增加了程序的理解成本。所以,正确的英文拼写和语法可以让阅读者易于理解,避免歧义。 alibaba、taobao、hangzhou 等国际通用的名称,可视同英文。某些复合语义的情况下,尽量使用完整的单词组合来达到望文知义的目的 比如 KeyboardShortcutsHandler、AtomicReferenceFieldUpdater。
命名要符合语言特性、体现元素特征。命名做到望文知义、自解释是每个开发工程师的基本素质之一。我们在思量更好的代码元素命名的同时,也要敢于修改已有的、不合理的命名方式。
在所有代码元素中,常量和变量最为常见,优雅地定义与使用好它们,是开发工程师的基本功之一。
常量
什么是常量?常量是在作用域内保持不变的值, 一般用 final 关键字进行修饰,根据作用域区分,分为全局常量、类内常量、局部常量。全局常量是指类的公开静态属性 使用 public static final 修饰;类内常量是私有静态属性,使用 private static final 修饰,局部常量分为方法常量和参数常量,前者是在方法或代码块内定义的常量,后者是在定义形式参数时 增加 final 表示此参数值不能被修改。全局常量和类内常量是最主要的常量表现形式,它们的命名方式比较特殊,采用字母全部大写、单词之间加下画线的方式。而局部常量采用小驼峰形式即可。示例代码如下:
public class Constant {
public static final String GLOBAL CONSTANT = "shared in global";
private static final String CLASS CONSTANT = "shared in class";
public void f(String a) {
final String methodConstant = "shared in method";
}
public void g( final int b) {
// 编译出错,不允许对常量参数进行重新赋值
b = 3;
}
}
常量在代码中具有穿透性,使用甚广。如果没有一个恰当的命名,就会给代码阅读带来沉重的负担,甚至影响对主干逻辑的理解。首当其冲的问题就是到处使用魔法值。魔法值即“共识层面”上的常量,直接以具体的数值或者字符出现在代码中。这些不知所云的魔法值极大地影响了代码的可读性和可维护性。下面先来看一段实际业务代码。
public void getOnlinePackageCourse(Long packageId, Long userId) {
if (packageId == 3) {
logger.error("线下课程,无法在线观看");
return;
}
// 其它逻辑处理
PackageCourse online = packageService.getByTeacherId(userId);
if (online.getPackageId() == 2) {
logger.error("未审核课程");
return;
}
// 其他逻将处理
}
以上示例代码中,信手拈来的2和3分别表示未审核课程和线下课程,仅仅是两个数字,似乎很容易记忆。但事实上除2和3两种状态外,还有1、4、5分别代表新建、审核未通过、审核通过。在团队规模较小时,口口相传,倒也勉强能够记住这五个数字的含义,早期还有零星的注释,驾轻就熟的情况下,连注释也省了。现实是残酷的,团队迅速扩大后,课程状态个数也在逐步增加,新来的开发工程师在上线新功能模块时,把“审核通过”和“未审核课程”对应的数字搞反了,使得课程展示错误,导致用户大量投诉。随着应用变得越来越复杂,这些魔法值几乎成了整个后台服务代码中的梦魔。团队架构师终于下定决心进行系统重构,把这些魔法值以合适的命名方式定义成全局常量。使用 Enum 枚举类来定义课程类型,示例代码如下:
public enum CourseTypeEnurn {
/**
* 允许官方和讲师创建和运营
*/
VIDEO_COURSE(l, "录插课程"),
/**
* 只允许官方创建和运营,初始化必须设置合理的报名人数上限
*/
LIVE_COURSE(2, "直播课程"),
/**
* 只允许官方创建和运营
*/
OFFLINE_COURSE(3,"线下课程");
private int seq;
private String desc ;
CourseTypeEnurn (int seq, String desc) {
this.seq = seq;
this. desc = desc ;
}
public int getSeq() {
return seq;
}
public String getDesc() {
return desc;
}
}
上述示例代码把课程类型分成三种:录播课程、直播课程、线下课程。枚举类型几乎是固定不变的全局常量,使用频率高、范围广,所以枚举常量都需要添加清晰的注释,比如业务相关信息或注意事项等。再把课程状态分为新课程、未审核课程、审核通过、审核未通过、已删除五种状态。考虑到后续课程状态还会再追加,并且状态没有扩展信息,所以用不能实例化的抽象类的全局常量来表示课程状态,示例代码如
下:
public abstract class BaseCourseState {
public static final int NEW_COURSE = 1;
public static final int UNAUTHED_COURSE = 2;
public static final int PASSED_COURSE = 3;
public static final int NOT_PASSED_COURSE = 4;
public static final int DELETED_COURSE = 5;
}
使用重构后的常量修改原有的魔法值,对比一下代码的可读性
public void getOnlinePackageCourse(Long packageId, Long userId) {
if (packageId == CourseTypeEnum.OFFLINE_COURSE.getSeq()) {
logger.error("线下课程,无法在线观看");
return;
}
// 其它逻辑处理
VideoCourse course = packageService.getByTeacherId(userId);
if (course.getState() == BaseCourseState.UNAUTHED_COURSE) {
logger.error("未审核课程");
return;
}
// 其他逻将处理
}
我们认为,系统成长到某个阶段后,重构是种必然选择。优秀的架构设计不是去阻止未来切重构的可能性,毕竟技术枝、业务方向和规模都在不断变化,而是尽可能让重构来得晚一些,重构幅度小一些。即使类内常量和局部常量当前只使用一次,也需要赋予一个有意义的名称,目的有两个:第一、望文知义,方便理解 第二、后期多次使用时能够保证值出同源。因此,无论如何都不允许任何魔法值直接出现在代码中,避免魔法值随意使用导致取值不一致,特别是对于字符串常量来说,应避免没有预先定义,就直接使用魔法值。所谓常 在河边走,哪有不湿鞋,在反复的复制与粘贴后,难免会出现问题,警示代码如下:
String key = "Id#taobao_" + tradeId;
cache.put(key, value);
上述代码是保存信息到缓存中的方法,即使用魔法值组装 Key。这就导致各个调用方到处复制和粘贴字符串 Id#taobao_ 这样似乎很合理。但某一天,某个粗心的程序员把Id#taobao_ 复制成为Id#taobao,少了下画线。这个错误在测试过程中,并不容易被发现 因为没有命中缓存,会自动访问数据库。但在大促时,数据库压力急剧上升,进而发现缓存全部失效,导致连接占满,查询变慢。小处不小,再次说明魔法值害人害己。
某些公认的字面常量是不需要预先定义的,如 for( int i=0; … )这里的0是可以直接使用的。true和 false也可以直接使用,但是如果具备了特殊的含义,就必须定义出有意义的常量名称,比如在 TreeMap 源码中,表示红黑树节点颜色的 true 和 false 就被定义成为类内常量,以方便理解∶
private static final boolean RED = false;
private static final boolean BLACK = true;
常量命名应该全部大写,单词间用下画线隔开,力求语义表达完整清楚,不要嫌名字长,比如,把最大库存数量命名为 MAX_STOCK_COUNT,把缓存失效时间命名为 CACHE_EXPIRED TIME。
变量
什么是变量从广义来说,在程序中变量是一切通过分配内存并赋值的量,分为不可变量(常量)和可变变量。从狭义来说,变量仅指在程序运行过程中可以改变其值的量,包括成员变量和局部可变变量等。
一般情况下,变量的命名需要满足小驼峰格式,命名体现业务含义即可。存在一种特殊情况,在定义类成员变量时,特别是在 POJO类中,针对布尔类型的变量,命名不要加 is 前缀,否则部分框架解析会引起序列化错误。例如,定义标识是否删除的成员变量为 Boolean isDeleted,它的 getter 方法也是 isDeleted(),框架在反向解析的时候,”误以为”对应的属性名称是 deleted,导致获取不到属性,进而抛出异常。但是在数据库建表中,推荐表达是与否的值采用 is_xxx 的命名方式,针对此种情况,需要在<resultMap>中设置,将数据表中的 is_xxx 字段映射到 POJO类中的属性Xxx。
代码展示风格
缩进、空格与空行
缩进、空格与空行造就了代码的层次性和规律性,有助于直观、快速、准确地理解业务逻辑。没有缩进、空格和空行的代码可读性极差。如下反例所示∶
table=newTab;
if (oldTab!=null){ for(int j=0;j<oldCap;++j){if((e=oldTab[j])!=null){
oldTab[j]=null;
if (e.next==null)
newTab[e.hash&(newCap-1)]=e;else if(e instanceof TreeNode)
if(loTail==null)loHead=e;else oTail.next=e;modCount++;
if((tab=table)!=null&&size>=0){
for(int i=0;i<tab.length;++i)tab[i]=null;
// 其他代码
}
-
缩进
缩进表示层次对应关系。使用 Tab 键缩进还是空格缩进长期以来备受争议,形成两大阵营。每当在分享会现场调研缩进方式选择的时候,参与度几乎都是100%,通常支持空格的人数多于支持Tab 键的人数。这时候 Tab 键方一般都会提出∶”空格不是有2、4、8个之分吗?不如让空格方继续投票一下,我们Tab 键方还是非常团结一致的”。某报告对40万个开源代码库进行了调研,发现近75%的代码文件使用了空格进行缩进。对于团队协作来说,一致性风格很重要。我们推荐采用4个空格缩进,禁止使用Tab 键。 由于不同编辑器对 Tab 的解析不一致,因此视觉体验会有差异,而空格在编辑器之间是兼容的。2个空格缩进的层次区分度不明显,超过4个空格的缩进方式又留白过多,且大多数IDE 默认为4个空格缩进,所以我们采用4个空格的缩进方式。对习惯用 Tab 键的工程师来说,唯一的福音是很多IDE 工具提供了Tab 键与空格之间的快速转换设置。IDEA 设置 Tab 键为4个空格时,请勿勾选 Use tab character;而在Eclipse 中,必须勾选 Insert spaces for tabs。
-
空格
空格用于分隔不同的编程元素。空格可以让运算符、数值、注释、参数等各种编程元素之间错落有致,方便快速定位。空格的使用有如下约定∶
(1)任何二目、三目运算符的左右两边都必须加一个空格。(2)注释的双斜线与注释内容之间有且仅有一个空格。
(3)方法参数在定义和传入时,多个参数逗号后边必须加空格。
(4)没有必要增加若干空格使变量的赋值等号与上一行对应位置的等号对齐。
(5)如果是大括号内为空,则简洁地写成{}即可,大括号中间无须换行和空格。
(6)左右小括号与括号内部的相邻字符之间不要出现空格。
(7)左大括号前需要加空格。
例如,有些工程师习惯在多行赋值语句中对齐等号,如果增加了一条较长的赋值语句,工程师需要更新之前所有的语句对齐格式,这种做法无疑提高了开发成本。此外,虽然不推荐空大括号的代码出现,但可能会存在干某些测试代码或者流程语句中,我们推荐空大括号中间无须换行和空格。详细的示例代码如下,重点看注释内容∶
public class SpaceCodeStyle { // 没有必要增加若干空格使变量的赋值等号与上一行对应位置的等号对齐 private static Integer one = 1; private static Long two = 2L; private static Float three = 3F; private static StringBuilder sb = new StringBuilder("code style:"); //缩进 4 个空格(注意∶本代码中的任何注释在双斜线与注释内容之间有且仅有一个空格) public static void main (String[] args)( //继续缩进4个空格 try { // 任何二目运算符的左右必须有一个空格 int count = 0; // 三目运算符的左右两边都必须有一个空格 boolean condition =(count == 0)? true : false; // 关键词if与左侧小括号之间必须有一个室格 // 左括号内的字母c与左括号、字母n与右括号都不需要空格 // 右括号与左大括号前加室格且不换行,左大括号后必须换行 if (condition) { System.out.println ("world"); // else 的前后都必须加空格 // 右大括号前换行,右大括号后有 else时,不用换行 } else { System.out.println ("ok"); //在右大括号后直接结束,则必须换行 } //如果是大括号内为空,则简洁地写成{}即可,大括号中间无须换行和空格 } catch (Exception e){} // 在每个实参逗号之后必须有一个空格 String result = getString(one, two, three, sb); System.out.println (result); } //方法之间,通过空行进行隔断。在方法定义中,每个形参之后必须有一空格 private static String getString(Integer one, Long two, Float three, StringBuilder sb){ // 任何二目运算符的左右必须有一个空格,包括赋值运算符,加号运算符等 Float temp = one + two + three; sb.append (temp); return sb.toString(); } }
-
空行
空行用来分隔功能相似、逻辑内聚、意思相近的代码片段,使得程序布局更加清晰。在浏览代码时,空行可以起到自然停顿的作用,提升阅读代码的体验。哪些地方需要空行呢?在方法定义之后、属性定义与方法之间、不同逻辑、不同语义、不同业务的代码之间都需要通过空行来分隔。
换行与高度
- 换行
代码中需要限定每行的字符个数,以便适配显示器的宽度,以及方便CodeReview时进行 diff 比对。对于无节制的行数字符,需要不断地拉取左右滚动条或者键盘移动光标,那是多么差的体验。因此,约定单行字符数不超过120个,超出则需要换行,换行时遵循如下原则∶
(1)第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进,参考示例。
(2)运算符与下文一起换行。
(3)方法调用的点符号与下文一起换行。
(4)方法调用中的多个参数需要换行时,在逗号后换行。
(5)在括号前不要换行。
StringBuffer sb = new StringBuffer();
// 超过120个字符的情况下,换行缩进 4个空格,并且方法前的点号一起换行
sb.append ("ma").append("chu")...
.append ("gao")...
.append ("xiao")...
.append("yealh");
-
方法行数限制
水平方向上对字符数有限制,那么垂直方向上呢?对于类的长度,只要类功能内聚,不做强制要求。但方法是执行单位,也是阅读代码逻辑的最高粒度模块。庞大的方法容易引起阅读疲劳,让人抓不住重点。代码逻辑要分主次、个性和共性。不要把不同层次的逻辑写在一个大方法体里,应该将次要逻辑抽取为独立方法,将共性逻辑抽取成为共性方法(比如参数校验、权限判断等),便于复用和维护,使主干代码逻辑更加清晰。
高内聚、低耦合是程序员最熟悉的口号。如何内聚和解耦,其实方法的行数限制就引发了这些维度的思考。把相关的功能强内聚,把弱相关的功能拆解开来,重新抽象、重新封装。在拆分方法的过程中,通常会纠结对参数的处理,因为拆分的各个方法之间需要通过参数才能传递数据。有这种纠结的前提是方法需要传入大量的参数,事实上这是另外一个话题。限制参数列表过长的方式有很多,比如包装成类、隐式传递或放在集合中等。
综上所述,约定单个方法的总行数不超过80行。详细的判定标准如下,除注释之外,方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过80 行。为什么是80 行?心理学认为人对事物的印象通常不能超过3这个魔法数,三屏是人类短期记忆的极限,而80 行在一般显示器上是两屏半的代码量。另外,通过对阿里代码抽样调查显示,只有不到5% 的方法才会超过 80行,而这些方法通常都有明显的优化空间。
最后有人说,80行的硬性要求会让程序员在写代码时刻意将多个变量定义在一行,或者if后不写大括号,或者catch 代码后使用空语句{}结束。每个公司都有一些强制的代码风格,肯定有些是大家的代码素养决定的,少数人偏偏冒天下之大不韪,被这个群体淘汰也是迟早的事情。
-
控制语句
控制语句是底层机器码跳转指令的实现。方法内部的跳转控制主要由条件判断语句和循环语句实现。跳转能力使程序能够处理复杂逻辑,具备像人一样的判断能力和记忆回溯能力。条件判断主要由 if、switch、三目运算符组成。循环严格意义上也是一种跳转,主要由 for、while、do-while 组成。
控制语句是最容易出现 Bug 的地方,所以特别需要代码风格的约束,而不是天马行空地乱跳。控制语句必须遵循如下约定∶
(1)在if、else、for、while、do-while等语句中必须使用大括号。即使只有一行代码,也需要加上大括号.(2)在条件表达式中不允许有赋值操作,也不允许在判断表达式中出现复杂的逻辑组合。有些控制语句的表达式逻辑相当复杂,与、或、取反混合运算甚至穿插了赋值操作,理解成本非常高,甚至会产生误解。要解决这个问题,有一个非常简单的办法∶将复杂的逻辑运算赋值给一个具有业务含义的布尔变量。例如∶
// 逻辑判断中使用复杂的逻辑判断,不易于理解 if((file.open(fileName,"w")!= null) && (...) || !(...)){ } 言 //将复杂的逻辑运算赋值给一个易于理解的布尔变量,方便阅读代码 final boolean existed =(file.open(fileName,"w")!= null) && (...) || !(...); if (existed) { ... }
(3) 多层嵌套不能超过3层。多层嵌套在哪里都不受欢迎,是因为条件判断和分支逻辑数量呈指数关系。如果非得使用多层嵌套,请使用状态设计模式。对于超过3层的if-else 的逻辑判断代码,可以使用卫语句、策略模式、状态模式等来实现,其中卫语句示例如下:
public void today(){ if(isBusy()){ System.out.println("change time."); return; } if(isFree()){ System.out.println("go to travel."); return; } System.out.println("stay at home to learn Easy Coding."); return; }
(4)避免采用取反逻辑运算符。取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。比如使用if(x<628)表达x小于628,而不是使用if(!(x >=628))。
代码注释
注释三要素
注释是一个看起来简单,容易被忽视,但是作用又不容小觑的话题。好的注释能起到指路明灯、拨云见日、警示等作用,具体包括∶能够准确反映设计思想和代码逻辑;能够描述业务含义,使其他工程师能迅速了解背景知识。与代码不同,注释没有语法的限制,完全取决于编写者的能力和发挥,但这并不意味着注释可以天马行空。书写注释要满足优雅注释三要素。
-
Nothing is strange
完全没有注释的大段代码对于阅读者来说形同天书。注释是给自己看的,即使离写完代码很长时间,也能清晰地理解当时的思路;注释也是给维护者看的,使其能够快速理解代码逻辑。
相信大多数人阅读JDK 源码时都十分吃力,比如并发控制、集合算法等,这些天才级的程序基本上没有任何注释。JDK的代码稳定、高效压倒一切,不会朝编夕改。但是业务代码需要被不断地维护更新,没有注释的代码给人一种陌生感。世界上最遥远的距离是,我和要修改的代码间缺少一段注释。因此,我们提倡要写注释,然后才是把注释写得精简。
-
Less is more
从代码可读性及维护成本方面来讲,代码中的注释一定是精华中的精华。首先,真正好的代码是自解释的,准确的变量命名加上合理的代码逻辑,无须过多的文字说明就足以让其他工程师理解代码的功能。如果代码需要大量的注释来说明解释,那么工程师应该思考是否可以优化代码表现力。
其次,泛滥的注释不但不能帮助工程师理解代码,而且会影响代码的可读性,甚至会增加程序的维护成本。如下示例代码是滥用注释的样例,方法名 put,加上两个有意义的变量名elephant和fridge,已经明确表达了代码功能,完全不需要额外的注释。在遇到修改代码逻辑时,注释泛滥会带来灾难性的负担。
// put elephant into fxidge put (elephant, fridge);
-
Advance with the times
与时俱进的重要性对于开发工程师来说是不言而喻的。就像道路状况与导航软件一样,如果导航软件严重滞后,就失去了导航的意义。同样,针对一段有注释的代码,如果程序员修改了代码逻辑,但是没有修改注释,就会导致注释无法跟随代码前进的脚步,误导后续开发者。因此,任何对代码的修改,都应该同时修改注释。
注释格式
注释格式主要分为两种∶ 一种是 Javadoc 规范,另一种是简单注释。
- Javadoc规范
类、类属性和类方法的注释必须遵循Javadoc规范,使用文档注释(/***/)的格式。按 Javadoc 规范编写的注释,可以生成规范的 JavaAPI 文档,为外部用户提供非常有效的文档支持。而且在使用IDE 工具编码时,IDE 会自动提示所用到的类、方法等注释,提高了编码的效率。
这里要特别强调对枚举的注释是必需的。有人觉得枚举通常带了String name 属性,已经简要地说明了这个枚举属性值的意思,此时注释是多余的。其实不然,因为∶
(1)枚举实在太特殊了。它的代码极为稳定。如果它的定义和使用出现错误,通常影响较大。
(2)注释的内容不仅限于解释属性值的含义,还可以包括注意事项、业务逻辑。如果在原有枚举类上新增或修改一个属性值,还需要加上创建和修改时间,让使用者零成本地知道这个枚举类的所有意图。
(3)枚举类的删除或者修改都存在很大的风险。不可直接删除过时属性,需要标注为过时,同时注释说明过时的逻辑考虑和业务背景。
- 简单注释
包括单行注释和多行注释。特别强调此类注释不允许写在代码后方,必须写在代码上方,这是为了避免注释的参差不齐,导致代码版式混乱。双画线注释往往使用在方法内部,此时的注释是提供给程序开发者、维护者和关注方法细节的调用者查看的。因此,注释的作用更应该是画龙点睛的,通常添加在非常必要的地方,例如复杂算法或需要警示的特殊业务场景等。
说明:本文内容参考《码出高效:Java开发手册》第三章 代码风格,有兴趣的读者可以看书的原文。