9.3 KiB
11 | 组织好代码段,让人对它“一见钟情”
当我们看到一个事物的时候,它的轮廓首先进入视野,给了我们第一印象。如果第一印象没有吸引到我们,那我们就不会集中注意力去关注它,也不会想去认识它。
我觉得有个俗语非常好地概括了这个认知习惯。这个俗语就是“不起眼”,更通俗一点的说法是“放在人群里认不出来”。
不管我们愿不愿意,第一印象特别影响我们的判断和心情。我们看到美好的东西,自己也跟着高兴;看到乱糟糟的东西,自己也感觉乱糟糟的。
代码也是这样的。如果我们看到整齐、清爽的代码,我们就对它有好感,愿意阅读,也愿意改进。 如果代码一团糟,风格混乱,我们就特别想静静地走开,一行都不想看。
前面的几讲中,我们讨论了注释、声明、格式、注解等编码规范。现在,是时候把这些零散的知识放到一块儿了。这些规范组合在一起,就会让代码既有让人喜悦的轮廓,也有让人清爽的细节。
这一次,我们将使用大量的例子,从代码文件头部结构、对象结构、类的内部结构、方法的代码结构、限定词的使用以及空行的使用这六大维度,来说明到底该怎么组织一个源代码文件。
代码文件头部结构
一般来说,一个源代码文件的开始部分包含三项内容。 按照出现顺序,分别为:
-
版权和许可声明;
-
命名空间(package);
-
外部依赖(import)。
下面就是一个例子。
在版权部分中,“2003, 2013”中的2003表示的是代码的诞生日期,2013表示的是代码的最近更改日期。需要注意的是,每次代码变更,都要更改代码的最近更改日期。
代码文件对象结构
紧随着源代码的头部,是对目标类的定义,这一部分包含三个部分。 按照出现顺序,分别为:
-
类的规范;
-
类的声明;
-
类的属性和方法。
这一部分中,我们需要注意的是,对于公共类,需要使用since标签,标明从哪一个版本开始定义了这个类。 这样的标明,方便我们对类进行版本管理,减少我们进行代码变更时的工作量。
比如在上面的例子中,如果需要修改Readable,当看到“since 1.5”的标签时,我们就不需要检查Java 1.4的代码了。
你也许会问,为什么Java 1.4不能使用Readable呢?since标签同样可以给你答案,因为Readable是从Java 1.5开始才有的接口。
这些问题虽然简单,但是如果没有使用since标签,答案就没有这么直观明显了。
类的内部代码结构
类的属性和方法,一般是一个代码文件的主要组成部分。类的结构,推荐的编码顺序依次为:
-
类的属性;
-
构造方法;
-
工厂方法;
-
其他方法。
我摘抄了一段JDK的代码,你看这样是不是更干净整洁?
类似于构造方法,工厂方法也是用来构造一个类的实例。不同的是,工厂方法有具体的返回值。它可以是静态方法,也可以是实例方法。
如果是实例方法,工厂方法还可以被子类重写。这是工厂方法和构造方法的另外一个重要的区别。由于工厂方法也扮演着构造类实例的功能,我们一般把它放在构造方法的后面,其他方法的前面。
方法的代码结构
讲完了类的内部代码结构,我们再来讲讲方法的代码结构。一般来说,一个方法需要包含三项内容:
-
方法的规范;
-
方法的声明;
-
方法的实现。
内部类的内部方法,可以没有第一部分。但对于公开类的公开方法,方法的规范一定不能缺失。 一个典型的规范,应该包含以下十个部分:
-
方法的简短介绍;
-
方法的详细介绍(可选项);
-
规范的注意事项 (使用apiNote标签,可选项);
-
实现方法的要求 (使用implSpec标签,可选项);
-
实现的注意事项 (使用implNote标签,可选项);
-
方法参数的描述;
-
返回值的描述;
-
抛出异常的描述:需要注意的是,抛出异常的描述部分,不仅要描述检查型异常,还要描述运行时异常;
-
参考接口索引(可选项);
-
创始版本(可选项)。
下面的这个例子,也是来自JDK的源代码。你可以清晰地看到,这段代码中的规范是非常典型的。
如果方法的创始版本和它所属类的创始版本一致,方法的创始版本描述可以省略。 要不然,一定要加入方法的创始版本标签。
像下面这个例子,就添加了创始版本标签。
按顺序使用限定词
在声明一个类、方法或者方法属性时,为了更准确地描述和理解声明的适用场景,我们通常要使用修饰性的关键词。这些修饰性的关键词,我们通常称它们是修饰符或者限定词。 一个声明,可以使用多个限定词。
Java的语法中,限定词的使用顺序没有强制性规定。但是,限定词的使用顺序有一个约定俗成的规则。按照这个规则使用限定词,一般来说,我们最关切的修饰符最先进入我们的视野,和标识符最密切的位置最靠近标识符。使用一致性的顺序,我们就能更快速地理解一个声明。
限定词推荐使用顺序:
-
public/private/protected (访问控制限定词,制定访问权限)
-
abstract (抽象类或者抽象方法,具体实现由子类完成)
-
static (静态类、方法或者类属性)
-
final (定义不能被修改的类、方法或者类属性)
-
transient(定义不能被序列化的类属性)
-
volatile(定义使用主内存的变量)
-
default(声明缺省的方法)
-
synchronized(声明同步的方法)
-
native(声明本地的方法,也就是Java以外的语言实现的方法)
-
strictfp(声明使用精确浮点运算)
使用空行分割代码块
我们之前讲过怎么整理代码,一个重要的原则就是“给代码分块”,通过空格、缩进、空行实现这个目的。
再来回顾一下空行的作用,空行用于垂直分割,用于分开同级别的不同代码块。
我们可以使用空行分割如下的代码块:
-
版权和许可声明代码块;
-
命名空间代码块;
-
外部依赖代码块
-
类的代码块;
-
类的属性与方法之间;
-
类的方法之间;
-
方法实现的信息块之间。
小结
对于软件开发者来说,组织代码是一项基本技能,也是我们需要养成的好习惯。组织代码有许多不同的习惯和策略,我们要学会辨别这些策略中哪些是有效的,哪些是有害的。
怎么辨别呢?
和其他技能一样,最快的提升方法是仔细思考一下为什么我们要做出这样的选择,而不是其他的。知其然远远不够,还要知其所以然。
你可以试着看看你的项目,源代码是按照这种方式组织的吗?哪些部分采用了合理的组织方式,哪些部分还有改进的空间?哪些是值得分享的经验?欢迎你把想法分享在留言区,我们一起来学习。
一起来动手
下面的这段Java代码,我们已经很熟悉了。前面,我们对它做过很多方面的修改。这一次,我们把前面的修改集中起来。你试着去找到所有可以改进的地方,然后比较一下修改前和修改后的代码。你有什么感受?
欢迎你把优化的代码公布在讨论区,让我们一起来感受、来欣赏!
也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来体验修改代码的快感。
import java.util.HashMap;
import java.util.Map;
class Solution {
/**
* Given an array of integers, return indices of the two numbers
* such that they add up to a specific target.
*/
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
}