原生态Java 程序员容易忽视的编程细节有哪些
今天就跟大家聊聊有关原生态Java 程序员容易忽视的编程细节有哪些,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。
Java是Java程序设计语言和Java平台的总称,要想学好一门语言,打好基础最关键的,学习一种新的编程语言比学习新的口头语言要容易得多。然而,在这两种学习过程中,都要付出额外的努力去学习不带口音地说新语言。
如果您熟悉C或C++,那么学习Java语言并不困难,这就像是会说瑞典语的人去学丹麦语一样。语言虽有不同,但又彼此互通。但若不够谨慎,您的口音每次都会暴露出您并非原生语言使用者这个秘密。
C++ 程序员往往会对Java代码做出一些变形,而这样的举动将他们与原生Java语言用户清晰地区分开来。他们的代码可以无错运行,但对于原生语言用户来说,就是有一些地方不对劲。因而原生语言用户可能会轻视非原生用户。从 C 或 C++(或者 Basic、Fortran、Scheme 等)转到Java语言时,您需要根除一些习惯用语,并纠正某些发音,以便流畅地使用新语言。在本文中,我探讨了一些往往被忽视的Java编程细节,因为从语义上来说,它们并不重要,甚至是无关紧要的。它们纯粹是风格和惯例问题。其中有些细节有着似是而非的理由,其他一些甚至连似是而非的理由也没有。但所有这些细节都是当今编写的Java代码中真实存在的现象。
这是什么语言?
让我们首先来看一段代码,其作用是将华氏温度转换为摄氏度,如清单 1 所示:
清单 1. 一段C代码?
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); FF = F + x; }
清单 1 中使用的是什么语言?很显然是 C 语言 —请等一下,让我们来看看完整的程序,如清单 2 所示:
清单 2. Java 程序
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); FF = F + x; } } private static void printf(String format, Object... args) { System.out.printf(format, args); } }
无论您是否相信,清单 1 和清单 2 都是使用 Java 语言编写的。它们只是以 C 语言方言(老实说,清单 1 也确实可以是 C 代码)编写的 Java 代码。这里的几个习语标志着:编写这段代码的人是以 C 语言思考的,只是单纯地将其翻译为 Java 语言:
◆变量是 float 而非 double。
◆所有变量都是在方法上方声明的。
◆初始化紧接声明之后。
◆使用了 while 循环而非 for 循环。
◆使用了 printf 而非 println。
◆main() 方法的参数名为 argv。
◆数组括号紧接参数名之后,而非类型之后。
如果仅仅考虑所编写的这些代码是否能够编译或者是否会得到正确的结果,那么这些方言都不是错误的。如果分开来看,这几点都并不明显。但将它们结合在一起,就构成了一段非常古怪的代码,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 编程中的基本命名规则非常简单,也值得牢记:
◆类和接口名以大写字母开头,如 Frame。
◆方法、字段和本地变量名以小写字母开头,如 read()。
◆类、方法和字段名均使用驼峰式大小写风格,如 InputStream 和 readFully()。
◆常量 — 终态静态字段和临时终态本地变量 — 全部适用大写字母,并以下划线分隔各词,如 MAX_CONNECTIONS。
像 sprintf 和 nmtkns 这样的名称是超级计算机只有 32 KB 内存时代的遗物。编译器将标识符限制为 8 个字符或更少,以此来节约内存。近 30 年来,这已经不再是需要担心的问题。如今,再没有任何理由不使用完整拼写的变量和方法名称。难以解读、无元音字母的变量名清楚地表明这个程序出自一名皈依 Java 的 C 程序员之手,请参见清单 3:
清单 3. Abbrvtd nms r hrd 2 rd
for (int i = 0; i < nr; i++) { for (int j = 0; j < nc; j++) { t[i][j] = s[i][j]; } }
不缩写、采用驼峰式大小写风格的名称更易读易懂,如清单 4 所示:
清单 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 语言将易读性置于简洁性之前,有一些极为常用的缩写形式,您仍然可以放心使用:
◆max 表示***(maximum)
◆min 表示最小(minimum)
◆in 表示 InputStream
◆out 表示 OutputStream
◆e 或 ex 表示 catch 子句中的异常(不用于其他位置)
◆num 表示数字(number),仅用作前缀,如 numTokens 或 numHits
◆tmp 表示主要在本地使用的临时变量 — 针对实例,在交换两个值的时候,除此之外(或许还有少数一些例外),您应完整拼写出名称中使用的所有词。
变量声明、初始化和使用(重用)
早期版本的 C 需要在方法开始处声明所有变量。这样是为了在编译器中实现一定的优化,允许它在 RAM 极为有限的环境中运行。因而,C 语言中的方法大多以几行变量声明开头:
int i, j, k; double x, y, z; float cf[], gh[], jk[];
然而,这种风格也有一些缺陷。它将变量的声明与其使用分离开来,使代码的易读性降低。此外,它会为多种不同的用途重用一个本地变量,有可能并非刻意而为。但若变量持有代码的某个片段无法接受的残值,这可能会带来无法预料的 bug。这一点与 C 语言中简短而难解的变量名结合在一起,将会后患无穷。
在 Java 语言(和较新版本的 C 语言)中,变量可在初次使用或接近初次使用时声明。在编写 Java 代码时,请采取这种做法。这将使您的代码更加安全、更不易出现 bug,也更易于阅读。此外,Java 代码通常在声明变量时初始化各变量,而 C 程序员有时会写出下面这样的代码:
int i; i = 7;
尽管这在语法上是正确的,但 Java 程序员永远不会写出这样的代码。他们会这样写这段代码:
int i = 7;
这有助于避免因意外使用了未经初始化的变量而导致的 bug。惟一的常见例外是一个变量的作用域需要同时包含 try 块和 catch 或 finally 块。这往往是由于代码涉及需要在 finally 块中关闭的输入流和输出流而导致的,如清单 5 所示:
清单 5. 异常处理可能会使变量的作用域难以合理设定
InputStream in; try { in = new FileInputStream("data.txt"); // read from InputStream } finally { if (in != null) { in.close(); } }
但这几乎是惟一的异常,这种风格的***一种连锁反应就是 Java 程序员通常每行仅声明一个变量。例如,他们初始化变量的方法如下:
int i = 3; int j = 8; int k = 9;
通常不会写出下面这样的代码:
int i=3, j=8, k=9;
这条语句在语法上是正确的,但除非在一种特殊的例外情况下,专业 Java 程序员是不会这样做的,后文将介绍这种特殊情况。老式的 C 程序员甚至可能编写一个四行的代码:
int i, j, k; i = 3; j = 8; k = 9;
Java 风格将声明与初始化结合在一起,因而实际上要更简练一些,只需要三行代码。
将变量置入循环
常见的一种特殊情况就是在循环外部声明变量。例如,考虑清单 6 中简单的 for 循环,其作用是计算斐波那契数列的前 20 项:
清单 6. C 程序员喜欢在循环外部声明变量
int high = 1; int low = 1; int tmp; int i; for (i = 1; i < 20; i++) { System.out.println(high); tmp = high; highhigh = high+ low; low = tmp; }
所有这四个变量都是在循环外声明的,尽管它们仅在循环内部使用,但作用域不止于此。这容易导致 bug,变量可能会在其目标作用域之外被重用。对于使用常用名的变量来说更是这样,例如 i 和 tmp。某次使用的值可能会残留下来,并以无法预计的方式干扰后续的代码。***项改进(C 语言的现代版本也支持这项改进)是将 i 循环变量的声明移到循环之内,如清单 7 所示:
清单 7. 将循环变量移入循环
int high = 1; int low = 1; int tmp; for (int i = 1; i < 20; i++) { System.out.println(high); tmp = high; highhigh = high+ low; low = tmp; }
到这里还没有结束,经验丰富的 Java 程序员还会将 tmp 变量移入循环,如清单 8 所示:
清单 8. 在循环内声明临时变量
int high = 1; int low = 1; for (int i = 1; i < 20; i++) { System.out.println(high); int tmp = high; highhigh = high+ low; low = tmp; }
某些极度追求速度而又不够老练的开发人员有时会提出反对意见,认为这种做法导致循环内执行过多操作,而不只是必要的操作,从而降低代码运行速度。实际上,在运行时,声明根本不会执行。将声明移动到循环内绝不会给 Java 平台造成负面的性能影响。许多程序员,包括许多经验丰富的 Java 程序员都可能在这里止步。然而,还有一种不太常见的技巧,将所有变量都移入循环。您可以在 for 循环的初始化阶段声明多个变量,只需使用逗号分隔即可,如清单 9 所示:
清单 9. 在循环内声明所有变量
for (int i = 1, high = 1, low = 1; i < 20; i++) { System.out.println(high); int tmp = high; highhigh = high + low; low = tmp; }
这已经不仅仅是惯用的流畅代码,而是真正的专业代码。与 C 代码相比,Java 代码中的 for循环更多、while循环更少,原因就在于这种严格限制本地变量作用域的能力。
不要回收变量
上述讨论得出这样一个结论,Java 程序员几乎不会为不同的值和对象重用本地变量。例如,清单 10 建立了一些按钮及其关联的动作侦听器:
清单 10. 回收本地变量
Button b = new Button("Play"); b.addActionListener(new PlayAction()); b = new Button("Pause"); b.addActionListener(new PauseAction()); b = new Button("Rewind"); b.addActionListener(new RewindAction()); b = new Button("FastForward"); b.addActionListener(new FastForwardAction()); b = new Button("Stop"); b.addActionListener(new StopAction());
经验丰富的 Java 程序员会用 5 个不同的本地变量重写这段代码,如清单 11 所示:
清单 11. 未回收的变量
Button play = new Button("Play"); play.addActionListener(new PlayAction()); Button pause = new Button("Pause"); pause.addActionListener(new PauseAction()); Button rewind = new Button("Rewind"); rewind.addActionListener(new RewindAction()); Button fastForward = new Button("FastForward"); fastForward.addActionListener(new FastForwardAction()); Button stop = new Button("Stop"); stop.addActionListener(new StopAction());
为多个逻辑上不同的值或对象重用一个本地变量容易导致 bug。实际上,本地变量(并非始终是它们指向的对象)并不影响内存和时间问题。所以不必为此担忧,可以根据您的需要使用多个不同的本地变量。
信任垃圾收集器的内存管理能力出身 C++ 世界的程序员往往过度担心内存消耗和内存泄漏问题。此类程序员有两种表现。一种是在使用过变量后将变量设置为 null。另一种是调用 finalize()或将其用作一种伪析构函数。这是完全没有必要的。尽管有些时候确实需要在 Java 代码中手动释放内存,但这种情况十分罕见。大多数时候,只需依靠垃圾收集器即可合理快速地完成内存管理。与大多数优化一样,***实践准则就是:除非能够证明是有必要的,否则不要去干涉。
使用***原语数据类型
Java 语言有八种原语数据类型,但仅使用了其中的六种。在 Java 代码中,float 比 C 代码中少得多。float 变量或文字在 Java 代码中极为罕见,更常用的是 double。使用 float 的惟一时机就是操纵精度有限的大型多维浮点数字数组,此时存储空间较为重要。否则使用 double 即可。
比 float 更不常见的是 short。我在 Java 代码中几乎没有见过 short 变量。只有惟一的一次(我要警告您,这是极其罕见的情况),读入的外部定义数据格式碰巧包含 16 位有符号整型类型。在这种情况下,大多数程序员都会将其作为 int 读入。
确定私有属性的范围
您是否见过清单 22 中这种 equals() 方法?
清单 12. C++ 程序员编写的 equals()方法
public class Foo { private double x; public double getX() { return this.x; } public boolean equals(Object o) { if (o instanceof Foo) { Foo f = (Foo) o; return this.x == f.getX(); } return false; } }
这个方法在技术上是正确的,但我确信,这个类是由一名保守的 C++ 程序员编写的。他在一个方法中使用了私有字段 x 和公共 getter 方法 getX(),实际上是在一行代码之中,这泄漏了他的身份。在 C++ 中,这种做法是必要的,因为私有属性的范围是对象而不是类。也就是说,在 C++ 中,同一个类的对象无法看到彼此的私有成员变量。他们必须使用 accessor 方法。在 Java 语言中,私有属性的范围是类而非对象。类型同为 Foo 的两个对象可直接访问对方的私有字段。
某些微妙 — 往往又不相关 — 的考虑思路认为,您应该在 Java 代码中***直接字段访问而非 getter 访问,或者反之。字段访问相对速度较快,但在少数时候,getter 访问可能会提供与直接字段访问略有不同的值,特别是在涉及子类的时候。在 Java 语言中,没有任何理由在同一行代码中为同一个类的同一个字段同时使用直接字段访问和 getter 访问。
标点和语法方言
下面是一些与 C 语言对应部分不同的 Java 方言,在某些情况下,这样的差异是为了利用某些 Java 语言特性。将数组括号紧接于类型之后,Java 语言声明数组的方式与 C 语言中大致相同:
int k[]; double temperature[]; String names[];
但Java语言也提供了一种替代性的语法,将数组复括号紧接于类型之后,而不是变量名之后:
int[] k; double[] temperatures; String[] names;
大多数 Java 程序员都采用了第二种风格。上面的代码表示 k 的类型是 int 数组,temperatures 的类型是 double 数组,names 的类型是 String 数组。同样,与其他本地变量一样,Java 程序员习惯在声明时初始化这些变量:
int[] k = new int[10]; double[] temperatures = new double[75]; String[] names = new String[32];
使用 s == null 而不是 null == s,谨慎的 C 程序员已经学会了将文字置于比较运算符的左侧。例如:
if (7 == x) doSomething();
目标在于避免意外地使用单等号赋值运算符而非双等号比较运算符:
if (7 = x) doSomething();
若将文字置于左侧,这样的错误就会成为编译时错误。这项技巧是 C 语言中一项著名的编程实践。它能帮助避免出现真正的 bug,因为若将文字置于右端,将始终返回 true。然而,不同于 C 语言,Java 语言具有独立的 int 和 boolean 类型,赋值运算符返回 int,而比较运算符返回 boolean。因而,if (x = 7) 已经成为编译时错误,就没有必要为比较语句使用不自然的形式 if (7 == x),流畅的 Java 程序员不会这样做。
连接字符串而非格式化字符串
多年以来,Java 语言一直没有 printf() 函数。最终,Java 5 中增加了这个函数,有些时候能够发挥作用。具体来说,在您希望将数字格式化为特定宽度或小数点后带有特定位数的形式时,在这种不常见的情况下,格式字符串是一种便捷的字段特定语言。而 C 程序员往往在 Java 代码中过多地使用 printf()。不应使用它取代简单的字符串连接。例如:
System.out.println("There were " + numErrors + " errors reported.");
优于:
System.out.printf("There were %d errors reported.\n", numErrors);
变体使用了字符串连接,更易于阅读,在简单的情况下更是如此,此外,由于不存在格式字符串中的占位符和数字或变量参数的类型匹配不当的情况,出现 bug 的机会也更少。
***后增量而非前增量
在某些位置,i++ 和 ++i 之间的差别十分显著。Java 程序员为这些位置定义了一个具体的名称,那就是“bug”。不应该编写依赖于前增量和后增量之间差异的代码(对于 C 语言来说也是如此)。原因在于难以理解、易于出错。如果您发现,在您编写的代码中两者的差别有重大影响,那么就应该重新将代码组织为独立的语句,使之不再能够影响大局。
如果前增量和后增量之间的差别不显著 — 例如,for 循环的增量步数 — 80% 的 Java 程序员更倾向于使用后增量,只有 20% 的 Java 程序员会选择前增量。i++ 比 ++i 更为常用。我无法评判孰是孰非,但事实就是这样。如果您编写的代码中包含 ++i,那么任何阅读您的代码的人都要浪费时间去思考您为什么要这样写。因而,除非有特殊的原因必须使用前增量(应该不存在必须使用前增量的情况),否则请使用后增量。
错误处理
错误处理是 Java 编程中最令人困惑的问题之一,也是真正地将语言风格大师与平凡开发者区分开来的一道门槛。实际上,仅仅错误处理就可以自成一篇文章。简而言之,合理使用异常,切勿返回错误代码。非原生语言使用者的***类错误是返回一个表示错误的值,而不是抛出异常。如果回溯到 Java 1.0 的年代,在 Sun 的所有程序员都充分理解了这种新语言之前,在某些 Java 语言自己的 API 中也会看到这样的情况。例如,考虑 java.io.File 中的 delete()方法:
public boolean delete()
若文件或目录被成功删除,此方法将返回 true,否则返回 false。但最合理的做法 应该是,在成功完成时不返回任何内容,若存在出于某些原因未能删除的文件,则抛出异常:
public void delete() throws IOException
在方法返回错误值时,每一个方法调用都要包含错误处理代码。在大多数正常情况下,这使得跟踪和理解方法的正常执行流变得困难。同时,如果由异常指出错误条件,错误处理即可单独作为文件末尾处的一个代码块。如果存在更适合处理问题的位置,甚至可将其移动到其他方法和其他类中。这就带来了错误处理中的第二种反模式。具有 C++ 背景的程序员有时会竭力在异常抛出后尽快处理异常。如果达到极限,可能会得到如清单 13 所示的代码:
清单 13. 过早的异常处理
public void readNumberFromFile(String name) { FileInputStream in; try { in = new FileInputStream(name); } catch (FileNotFoundException e) { System.err.println(e.getMessage()); return; } InputStreamReader reader; try { reader = new InputStreamReader(in, "UTF-8"); } catch (UnsupportedEncodingException e) { System.err.println("This can't happen!"); return; } BufferedReader buffer = new BufferedReader(reader); String line; try { line = buffer.readLine(); } catch (IOException e) { System.err.println(e.getMessage()); return; } double x; try { x = Double.parseDouble(line); } catch (NumberFormatException e) { System.err.println(e.getMessage()); return; } System.out.println("Read: " + x); }
这段代码非常难以阅读,甚至比异常处理取代的 if (errorCondition) 测试更为难解。流畅的 Java 代码将错误处理与故障点分离开来,不会将错误处理代码与正常执行流混合在一起。清单 14 中的版本更易于阅读和理解:
清单 14. 保持代码的主执行路线完好
public void readNumberFromFile(String name) { try { FileInputStream in = new FileInputStream(name); InputStreamReader reader = new InputStreamReader(in, "UTF-8"); BufferedReader buffer = new BufferedReader(reader); String line = buffer.readLine(); double x = Double.parseDouble(line); System.out.println("Read: " + x); } catch (NumberFormatException e) { System.err.println("Data format error"); } catch (IOException e) { System.err.println("Error reading from file: " + name); } }
某些时候,您可能需要使用嵌套的 try-catch 块来分离造成相同异常的不同故障模式,但这种情况并不常见。主要的实践经验是:如果一个方法中存在多个 try 块,那么就表明方法过于庞大,应拆分为多个较小的方法。***,具有各种语言背景、刚刚接触 Java 编程的程序员往往会错误地假设他们必须在抛出检查异常(checked exception)的方法中捕捉到这些异常。而抛出异常的方法通常并不是应该负责捕捉异常的方法。例如,考虑如清单 15 所示的方法:
清单 15. 过早的异常处理
public static void copy(InputStream in, OutputStream out) { try { while (true) { int datum = in.read(); if (datum == -1) break; out.write(datum); } out.flush(); } catch (IOException ex) { System.err.println(ex.getMessage()); } }
此方法没有足够的信息来处理很有可能发生的 IOException。它并不了解谁调用了它,也不了解故障的后果。对于此方法来说,惟一合理的举措就是允许 IOException 上行至调用方。编写此方法的正确方式如清单 16 所示:
清单 16. 并非所有异常都需要在***时间捕捉
public static void copy(InputStream in, OutputStream out) throws IOException { while (true) { int datum = in.read(); if (datum == -1) break; out.write(datum); } out.flush(); }
简而言之,这更为简单、更容易理解,将错误信息传递给代码中最适合处理这些信息的部分。这些问题是否真的那么重要?这些问题都不是关键问题。某些是惯例:在初次使用时声明;在不知道如何处理错误时抛出异常。其他则是纯粹的风格惯例(args 而非 argv;i++ 而非 ++i)。我并不认为这些规则能使您的代码运行速度更快,但其中一些确实能帮助您避免 bug。如果您要成为一名流畅的 Java 语言使用者,所有这些规则都是重要的。
无论如何,以纯正的口音讲话(或编写代码)都能使其他人更加尊重您、更加关注您所表达的内容,甚至会为您表达的内容付给您更多的钱。此外,以纯正的口音使用 Java 语言要比说无口音的法语、汉语或英语要简单得多。一旦您学会了一门语言,就值得付出努力来使您的表达变得更加原汁原味。
看完上述内容,你们对原生态Java 程序员容易忽视的编程细节有哪些有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注编程网行业资讯频道,感谢大家的支持。
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341