使用一种程序设计语言,就应该专业地使用它。本文是IBM developerWorks中的一篇文章,它描述的都是Java编程中的细节问题,尽管如此,还是值得大家玩味一番,至少我作为一名老鸟还是从中受益了。

  学习一种新的程序设计语言比学习一种新的口头语言要容易。但是,在这两种努力中,都需要付出额外的工夫去学着能地道地说这种新的语言。当你已会C或 C++,那么学习Java程序设计语言将不会很困难;这就类似于,当你已会说瑞典语时又去学习丹麦语。语言是不同的,但能相互理解。但如果你不注意,你的口音每次都会暴露出你不是一个本地人。

  C++程序员经常会写变了味的Java代码,他们错误地将自己当作语言的转换者,而非说该种语言的本地人。这些代码仍能工作,但对于地道的Java程序员,它们看起来有些问题。结果,地道的Java程序员可能看不起非地道的Java程序员。当从C/C++(或Basic或Fortran或Scheme) 转向Java时,你需要去除某些风格并纠正一些发音,以便你能讲得流畅。

  在本文中,我探索了一些Java编程方面的细节,这些细节经常会被忽视,因为它们不是什么大事情,如果有的话。这些都是编程风格和规范上的问题。其中较少的一些有真实可信的理由,有一些甚至还没有这样的理由。但是所有的问题在此时所写的Java程序中都是真实存在的。

  这是什么语言?

  让我们以一段将华氏温度转化为摄氏温度的程序开始,如清单1所示:

  清单1 一点儿C语言代码

Java代码
  1. float F, C;  
  2. float min_tmp, max_tmp, x;  
  3.   
  4. min_tmp = 0;    
  5. max_tmp = 300;  
  6. x  = 20;  
  7.   
  8. F = min_tmp;  
  9. while (F <= max_tmp) {  
  10.    C = 5 * (F-32) / 9;  
  11.    printf("%f"t%f"n", F, C);  
  12.    F = F + x;  
  13. }  

  清单1使用的是什么语言?很显示是C语言--但很等等。看看清单2中的完整应用程序:

  清单2 Java程序

Java代码
  1. class Test  {  
  2.   
  3.     public static void main(String argv[]) {    
  4.         float F, C;  
  5.         float min_tmp, max_tmp, x;  
  6.         
  7.         min_tmp = 0;    
  8.         max_tmp = 300;  
  9.         x  = 20;  
  10.         
  11.         F = min_tmp;  
  12.         while (F <= max_tmp) {  
  13.           C = 5 * (F-32) / 9;  
  14.           printf("%f"t%f"n", F, C);  
  15.           F = F + x;  
  16.         }  
  17.     }  
  18.   
  19.     private static void printf(String format, Object args) {  
  20.         System.out.printf(format, args);  
  21.     }  
  22.       
  23. }  

  不管是否相信,清单1和清单2都是由Java语言写成的。只不过它们是用C语言的风格写的Java代码(公平来说,清单1也能是真正的C代码)。然而这些Java代码看起来比较有趣。此处的一些编程风格在抱有C语言思维的人看来只不过是将C代码翻译成了Java代码罢了。

  变量为float型,而非double型。

  所有的变量都声明在方法的顶部。

  初始化在声明之后。

  使用while循环,而非for循环。

  使用printf,而非println。

  main方法的参数声明为argv。

  数组的中括号紧跟变量名,而不在类型之后。

  从编写的代码能够通过编译或不产生错误答案的意义来看,这些编码风格都没错。单独来看,这些风格没有一个是重要的。然而,把它们都放到一些奇怪的代码中,这就让Java程序员难以阅读,就这如同让美国人去理解基尼(Geordie)。你越少的使用C语言风格,你的代码就越清晰。基于这种思维,我将分析一些 C程序员暴露自己的最常方式,并将展示如何使他们的代码更能适应Java程序员的视角。

  命名规范

  依你是否来自于C/C++或C#,你们内部可能有不同的命名规范。例如,在C#中类名以小写字母开头,方法和字段的名称以大写字母开头。Java的风格则正好相反。我没有任何明智的理由证明哪一种规范是合理的,但我肯定知道混合使用命名规范将使代码的感觉十分糟糕。它还会导致Bug。当你看到所有的名称都是大写,那就是常量,你对待它的方式就会不同。仅通过查找不匹配被声明类型所使用命名规范的地方,我就已经发现了程序中的很多BUG。

  Java程序设计中关于名称的基本原则十分简单,值得去记忆:

  类和接口的名称以大写字母开头,如Frame。

  方法,字段和局部变量的名称以小写字母开头,如read()。

  类,方法和字段名称都要使用驼峰字(camel casing),如InputStream和readFully。

  常量--final静态字段,以及某些final局部变量--全部使用大写字母书写,并且各单词之间用下划线分隔,如MAX_CONNECTIONS。

  不要使用缩写

  像printf和nmtkns这样的名称都是超级计算机都只有32K内存的时代的遗产了。编译器通过将标识符限制为不多与8个字符来节约内存。然而,在过去30年中,这已经不是一个问题了。现在,没有任何理由不为变量和方法的名称使用全拼。非明智的,缺少元音字符的变量名,没有比这更能使你的产品被认为是由C语言程序转变过去的了,如清单3所示:

  清单3 Abbrvtd nms r hrd 2 rd

Java代码
  1. for (int i = 0; i < nr; i++) {  
  2.     for (int j = 0; j < nc; j++) {  
  3.         t[i][j] = s[i][j];  
  4.     }  
  5. }  

  基于驼峰字的非缩写名称要清晰得多,如你在清单4中所见的那样:

  清单4 不使用缩写的名称方便阅读

Java代码
  1. for (int row = 0; row < numRows; row++) {  
  2.     for (int column = 0; column < numColumns; column++) {  
  3.         target[row][column] = source[row][column];  
  4.     }  
  5. }  

  代码读得比写得多,Java语言就为阅读而被改进的。C语言程序员具有一种几乎无可抗拒的诱惑力去弄乱代码;Java程序员则不会。Java语言会把易读性放在优先于简洁性的位置。

  有一些缩写十分的通用,你使用它而无需感到愧疚:

  针对最大化的max

  针对最小化的min

  针对InputStream的in

  针对OutStream的out

  在catch语句块中(但不是在所有地方),针对一个异常的e或ex。

  针对数字的num,但只能当用于前缀时,如numTokens或numHits。

  针对用于局部的临时变更的tmp--例如,当交换两个值时。

  除了上述缩写和其它的一些可能之外,你应该完整地拼写出名称中所有的单词。

  变量的声明,初始化和(重复)使用

  C语言的早期版本要求所有的变量要声明在方法的开始之处。这使得编译器能够进行特定的优化,这些优化使程序能够运行在RAM很小的环境中。因此,C语言的方法倾向于由几行变量声明开始:

Java代码
  1. int i, j, k;  
  2. double x, y, z;  
  3. float cf[], gh[], jk[];  

  然而,这一风格有一些消极作用。它将变量的声明与其使用分隔开了,使代码有点儿难以为继。此外,这也使它看起来像是一个局部变量会被不同的程序重复使用,而这可能并非程序员的本意。当一个变量引用一个多余的值,而这段代码并非所期望的,那么就这会引起预期之外的Bug。这一风格再与C语言对简短,隐晦变量名的爱好相结合,你就会导致一场灾难了。

  在Java语言(以及最新版的C语言)中,变量可以声明在(或靠近)它第一次使用的地方。当你编写Java代码时,就这么做。这能使你的代码安全,更少地出现Bug,并易于阅读。

  与此相关的,Java代码常常在每个变量声明的时候就进行初始化。有时候,C程序员编写的代码却像这样:

Java代码
  1. int i;  
  2. i = 7;  

  Java程序员几乎不会这样写代码,尽管这些代码在语法上是正确的。Java程序员会像下面这样编写代码:

Java代码
  1. int i = 7;  

  这就帮助避免Bug,这样的Bug会导致无意地使用未被初始化的变量。一般地,唯一的例外是,当一个变量要被包含在try-catch/finally块中时。最常出现的情况就是,当代码要在finally语句块中关闭输入流和输出流时,如清单5所示:

  清单5 异常处理使得难以恰当地控制变量的作用域

Java代码
  1. InputStream in;  
  2. try {  
  3.   in = new FileInputStream("data.txt");  
  4.   // read from InputStream  
  5. }  
  6. finally {  
  7.   if (in != null) {  
  8.     in.close();  
  9.   }  
  10. }  

  然而,这几乎是这一例外唯一能发生的时刻。

  终了,该风格的最后一个连锁效应就是Java程序员常常每行只定义一个变量。例如,他们像下面那样初始化三个变量:

Java代码
  1. int i = 3;  
  2. int j = 8;  
  3. int k = 9;  

  他们不会编写这样的代码:

Java代码
  1. int i=3, j=8, k=9;  

  这条语句在语法上是正确的,但在任何时候Java程序员都不会这么做,除非出现一种特别的情况,我下面将会涉及到。

  一个老派的C程序员可能会写成四行代码:

Java代码
  1. int i, j, k;  
  2. i = 3;  
  3. j = 8;  
  4. k = 9;  

  因此,通常的Java风格会更简洁些,它只需三行代码,因为它将声明与初始化结合在了一起。

  将变量置于循环内

  经常出现的一种情景就是在循环外声明变量。例如,考虑清单6所示的简单for循环,该循环将计算Fibonacci数列的前20个值:

  清单6 C程序员喜欢在循环外声明变量

Java代码
  1. int high = 1;  
  2. int low = 1;  
  3. int tmp;  
  4. int i;  
  5. for (i = 1; i < 20; i++) {  
  6.   System.out.println(high);  
  7.   tmp = high;  
  8.   high = high+ low;  
  9.   low = tmp;  
  10. }  

  所有4个变量都声明在了循环之外,因此它们就有了过大的作用范围,尽管它们只是用在循环内。这就可能产生Bug,因为变量可用在它们期望之外的范围中。当变量使用例如i和tmp这样通用的名称时,尤其会产生Bug。由一个变量引用的对象会一直存在,并会被随后的代码以意外的方式干扰。

  第一个改进(C语言的现代版本也支持这一改进)就是把循环变量i置于循环的内部,如清单7所示:

  清单7 将循环变量移入循环内

Java代码
  1. int high = 1;  
  2. int low = 1;  
  3. int tmp;  
  4. for (int i = 1; i < 20; i++) {  
  5.   System.out.println(high);  
  6.   tmp = high;  
  7.   high = high+ low;  
  8.   low = tmp;  
  9. }  

  但不能就此止步。有经验的Java程序还会将把tmp变量置于循环的内部,如清单8所示:

  清单8 在循环内声明临时变量

Java代码
  1. int high = 1;  
  2. int low = 1;  
  3. for (int i = 1; i < 20; i++) {  
  4.   System.out.println(high);  
  5.   int tmp = high;  
  6.   high = high+ low;  
  7.   low = tmp;  
  8. }  

  对程序速度有着狂热崇拜的大学生们有时候会反对道在循环中做无谓的工作会降低代码的运行效率。然而,在运行时,变量的声明完全不做任何实际的工作。在Java平台上,无论怎样,将声明置入循环内都不会产生性能上的损失。

  许多程序员,包括许多经验丰富的Java程序员,也会止步与此。然而,还有一点儿有用的技术能将所有的变量都转移到循环中。你可在for循环的初始化语句中声明超过一个变量,只需通过逗号进行分隔,如清单9所示:

  清单9 所有的变量都置入循环内

Java代码
  1. for (int i = 1, high = 1, low = 1; i < 20; i++) {  
  2.   System.out.println(high);  
  3.   int tmp = high;  
  4.   high = high+ low;  
  5.   low = tmp;  
  6. }  

  现在才是把仅仅语法上通顺的代码转化成真正的专家级代码。这种紧紧地束缚局部变量作用域的能力就是你为什么看到Java语言代码中的for循环比C语言代码多得多而while循环少得多的一个重要原因。

  不要重用变量

  由上述可得出的推论就是Java程序员很少为不同的值和对象重用局部变量。例如,清单10为一些按钮设置与之关联的侦听器:

  清单10 重用局部变量

Java代码
  1. Button b = new Button("Play");  
  2. b.addActionListener(new PlayAction());  
  3. b = new Button("Pause");  
  4. b.addActionListener(new PauseAction());  
  5. b = new Button("Rewind");  
  6. b.addActionListener(new RewindAction());  
  7. b = new Button("FastForward");  
  8. b.addActionListener(new FastForwardAction());  
  9. b = new Button("Stop");  
  10. b.addActionListener(new StopAction());  

  有经验的Java程序员会使用5个不同的局部变量来重写这段代码,如清单11所示:

  清单11 未被重用的变量

Java代码
  1. Button play = new Button("Play");  
  2. play.addActionListener(new PlayAction());  
  3. Button pause = new Button("Pause");  
  4. pause.addActionListener(new PauseAction());  
  5. Button rewind = new Button("Rewind");  
  6. rewind.addActionListener(new RewindAction());  
  7. Button fastForward = new Button("FastForward");  
  8. fastForward.addActionListener(new FastForwardAction());  
  9. Button stop = new Button("Stop");  
  10. stop.addActionListener(new StopAction());  

  为多个逻辑上不同的值或对象重用一个局部变量可能产生Bug。实质上,局部变量(尽管它们并不总是指向对象)在对内存和时间都很敏感的环境中都是适用的。只要你需要,不要惧怕使用众多不同的局部变量。

  最好使用基本数据类型

  Java语言有8种基本数据类型,但只使用了其中的6种。在Java代码中,float远不如在C代码中用得多。在Java代码中你几乎看不到float 型变量或常量;而double型的则很多。float变量仅仅被用于处理多维浮点型数组,这能在存储空间意义重大的环境中限制数据的精度。否则,使每个变量都为double型。

  比float型还不常见的就是short型。我很少在Java代码中看到short型变量。曾经唯一出现过的情况--我要警告你,这是一种极端罕见的情况 --就是当要读取的外部定义的数据格式中包含16位符号整数类型时。在这种情况下,大部分程序员都会把这些数据当作int型数据去读取。

  控制私有作用域

  你见过像清单2中示例那样的equals方法吗?

  清单12 由C++程序员编写的一个eqauls()方法

Java代码
  1. public class Foo {  
  2.   
  3.   private double x;  
  4.   
  5.   public double getX() {  
  6.     return this.x;  
  7.   }  
  8.   
  9.   public boolean equals(Object o) {  
  10.     if (o instanceof Foo) {  
  11.       Foo f = (Foo) o;  
  12.       return this.x == f.getX();  
  13.     }  
  14.     return false;  
  15.   }  
  16.   
  17. }  

  就技术上而言,该方法是正确的,但我能向你保证这个类是由一位还没改造好的C++程序员写成的。对私有域x的应用并在同一方法甚至同一行中使用公有的 getter方法getX()泄露了这一点。在C++中,这样做是必须的,因为私有性是限定在对象而不是类中的。即,在C++中,同一个类的对象看不到其它对象的私有成员变量,它们必须使用访问器方法。在Java语言中,私有性是限定在类而不是对象中,类型Foo的两个对象中的一个能直接访问到另一个的私有域。

  有些细微的--但往往是不相关的--思考会建议你更应直接访问字段而不是使用访问器方法,或者在Java代码中使用相反的方式。访问字段可能稍快些,但很少见。有时候,通过访问器进行访问相比于直接访问字段可以提供一点儿不同的值,特别是当使用子类时。但是,在Java语言中,没有任何理由在同一个类的同一行中既使用字段访问又使用访问器访问。

  标点和语法风格

  此处有一些不同于C语言的Java语言风格,其中一些例子应用了特定的Java语言特性。

  在类型处放置数组的括号

  Java语言可以如C语言那样声明数组:

Java代码
  1. int k[];  
  2. double temperature[];  
  3. String names[];  

  然而,Java语言也提供了另一种语法,将数组括号置于类型而不是变量之后:

Java代码
  1. int[] k;  
  2. double[] temperatures;  
  3. String[] names;  

  大部分Java程序员已经采用了第二种风格。我们就可以说,k是int的数组类型,temperatures是double的数组类型,names是String类型的数组。

  与其它局部变量一样,Java程序员也倾向于在数组的声明处对其进行初始化:

Java代码
  1. int[] k = new int[10];  
  2. double[] temperatures = new double[75];  
  3. String[] names = new String[32];  

  Use s == null, not null == s

  谨慎的C程序员已经学会了将常量放置在比较符的左边。例如:

Java代码
  1. if (7 == x) doSomething();  

  在此处,其目的在于避免无意地使用单等于号的赋值操作符,而不是双等于号的比较操作符。

Java代码
  1. if (7 = x) doSomething();  

  将常量置于左边会产生一个编译时错误。这项技术是C语言提倡的编程实践。它能帮助防止实际中的Bug,因为将常量置于右边将总是会返回true。

  但不同于C语言,Java语言将int与boolean类型分隔开了。赋值操作符返回一个int值,然而比较操作符返回boolean值。结果,if (x = 7)已是一个编译时错误,所以没有任何理由在比较语句中使用不自然的格式if (7 == x),熟练的Java程序员就不会这么做。

  连接字符串而不要格式化它们