学习一种新的编程语言比学习新的口头语言要容易得多。然而,在这两种学习过程中,都要付出额外的努力去学习不带口音地说新语言。如果您熟悉 C 或 C++,那么学习 Java 语言并不困难,这就像是会说瑞典语的人去学丹麦语一样。语言虽有不同,但又彼此互通。但若不够谨慎,您的口音每次都会暴露出您并非原生语言使用者这个秘密。
C++ 程序员往往会对 Java 代码做出一些变形,而这样的举动将他们与原生 Java 语言用户清晰地区分开来。他们的代码可以无错运行,但对于原生语言用户来说,就是有一些地方不对劲。因而原生语言用户可能会轻视非原生用户。从 C 或 C++(或者 Basic、Fortran、Scheme 等)转到 Java 语言时,您需要根除一些习惯用语,并纠正某些发音,以便流畅地使用新语言。
在本文中,我探讨了一些往往被忽视的 Java 编程细节,因为从语义上来说,它们并不重要,甚至是无关紧要的。它们纯粹是风格和惯例问题。其中有些细节有着似是而非的理由,其他一些甚至连似是而非的理由也没有。但所有这些细节都是当今编写的 Java 代码中真实存在的现象。
这是什么语言?
让我们首先来看一段代码,其作用是将华氏温度转换为摄氏度,如清单 1 所示:
float F, C; float min_tmp, max_tmp, x; min_tmp = 0; max_tmp = 300; x = 20; F = min_tmp; while (F <= max_tmp) { C = 5 * (F-32) / 9; printf("%f\t%f\n", F, C); F = F + x; } |
清单 1 中使用的是什么语言?很显然是 C 语言 —请等一下,让我们来看看完整的程序,如清单 2 所示:
class Test { public static void main(String argv[]) { float F, C; float min_tmp, max_tmp, x; min_tmp = 0; max_tmp = 300; x = 20; F = min_tmp; while (F <= max_tmp) { C = 5 * (F-32) / 9; printf("%f\t%f\n", F, C); F = F + x; } } private static void printf(String format, Object... args) { System.out.printf(format, args); } } |
无论您是否相信,清单 1 和清单 2 都是使用 Java 语言编写的。它们只是以 C 语言方言(老实说,清单 1 也确实可以是 C 代码)编写的 Java 代码。这里的几个习语标志着:编写这段代码的人是以 C 语言思考的,只是单纯地将其翻译为 Java 语言:
如果仅仅考虑所编写的这些代码是否能够编译或者是否会得到正确的结果,那么这些方言都不是错误的。如果分开来看,这几点都并不明显。但将它们结合在一起,就构成了一段非常古怪的代码,Java 程序员难以读懂,就像美国人难以听懂北英格兰人的方言一样。您使用的此类 C 语言方言越少,您的代码就会越清晰。请牢记这一点,下面我们将继续分析 C 语言程序员暴露自己身份的一些常见方式,并说明如何才能使他们的代码更符合 Java 程序员的眼光。
命名规范
根据您原本使用的是 C、C++ 还是 C#,您可能有一些较为主观的类命名规范。举例来说,在 C# 中,类名都是以小写字母开头的,方法名和字段名以大写字母开头。Java 风格则恰好相反。我没有任何合理的原因能评判一种规范是否比另一种更好,但我了解,混用命名规范会使代码看起来存在严重错误。这种做法也会导致 bug。如果您知道,每一个全部由大写字母组成的名称都是常量,则会以不同的方式进行处理。在寻找命名规范与声明类型不匹配之处时,我发现了程序中的许多 bug。
args而非 argv这一点是最微不足道的,但也正是这场风格之争所关注的细节。在 Java 的惯例中 main()方法的参数名为 args,而不是 argv:
public static void main(String[] args) |
这至多只是对 argv 这个名称进行了一点细微的改进。作为参数的缩写,它或多或少地比 argv 更易懂一些。 当然,在合乎惯例的 Java 代码中,通常是禁止使用缩写的(参见 请勿缩写)。我们使用 args 作为 main() 方法的参数名的惟一原因与 C 程序员使用 argv 的原因是相同的 — 第一本关于 C 语言的图书的作者 Kernighan 和 Ritchie 使用了这个名称。而 Gosling 和 Arnold 使用了 args。除此之外,再无其他原因。同样,所有原生 Java 程序员都倾向于使用 args,如果您希望保持原汁原味,那么也应该这样做。
Java 编程中的基本命名规则非常简单,也值得牢记:
请勿缩写
像 sprintf 和 nmtkns 这样的名称是超级计算机只有 32 KB 内存时代的遗物。编译器将标识符限制为 8 个字符或更少,以此来节约内存。近 30 年来,这已经不再是需要担心的问题。如今,再没有任何理由不使用完整拼写的变量和方法名称。难以解读、无元音字母的变量名清楚地表明这个程序出自一名皈依 Java 的 C 程序员之手,请参见清单 3:
for (int i = 0; i < nr; i++) { for (int j = 0; j < nc; j++) { t[j] = s[j]; } } |
不缩写、采用驼峰式大小写风格的名称更易读易懂,如清单 4 所示:
for (int row = 0; i < numRows; row++) { for (int column = 0; column < numColumns; column++) { target[row][column] = source[row][column]; } } |
一段代码被阅读的次数要远远超过编写的次数,Java 语言为易读性而进行了优化。C 程序员近乎沉迷于难解的代码,而 Java 程序员则不然。Java 语言将易读性置于简洁性之前。
有一些极为常用的缩写形式,您仍然可以放心使用:
除此之外(或许还有少数一些例外),您应完整拼写出名称中使用的所有词。
将变量置入循环
常见的一种特殊情况就是在循环外部声明变量。例如,考虑清单 6 中简单的 for 循环,其作用是计算斐波那契数列的前 20 项:
int high = 1; int low = 1; int tmp; int i; for (i = 1; i < 20; i++) { System.out.println(high); tmp = high; high = high+ low; low = tmp; } |
所有这四个变量都是在循环外声明的,尽管它们仅在循环内部使用,但作用域不止于此。这容易导致 bug,变量可能会在其目标作用域之外被重用。对于使用常用名的变量来说更是这样,例如 i 和 tmp。某次使用的值可能会残留下来,并以无法预计的方式干扰后续的代码。
第一项改进(C 语言的现代版本也支持这项改进)是将 i 循环变量的声明移到循环之内,如清单 7 所示:
int high = 1; int low = 1; int tmp; for (int i = 1; i < 20; i++) { System.out.println(high); tmp = high; high = high+ low; low = tmp; } |
到这里还没有结束,经验丰富的 Java 程序员还会将 tmp 变量移入循环,如清单 8 所示:
int high = 1; int low = 1; for (int i = 1; i < 20; i++) { System.out.println(high); int tmp = high; high = high+ low; low = tmp; } |
某些极度追求速度而又不够老练的开发人员有时会提出反对意见,认为这种做法导致循环内执行过多操作,而不只是必要的操作,从而降低代码运行速度。实际上,在运行时,声明根本不会执行。将声明移动到循环内绝不会给 Java 平台造成负面的性能影响。
许多程序员,包括许多经验丰富的 Java 程序员都可能在这里止步。然而,还有一种不太常见的技巧,将所有变量都移入循环。您可以在 for 循环的初始化阶段声明多个变量,只需使用逗号分隔即可,如清单 9 所示:
for (int i = 1, high = 1, low = 1; i < 20; i++) { System.out.println(high); int tmp = high; high = high + low; low = tmp; } |
这已经不仅仅是惯用的流畅代码,而是真正的专业代码。与 C 代码相比,Java 代码中的 for循环更多、while循环更少,原因就在于这种严格限制本地变量作用域的能力。
文章评论(0条评论)
登录后参与讨论