起源

新葡京娱乐场:然而,到21世纪30年代初,石油的非燃料用途,特别是用于石化产品的生产,将成为石油需求增长的主要来源。

前几天,赵明威在图灵社区发表了“算法导论学习之补漏:斐波那契数列”,该文中最后的 Java 程序中有一个 fibonacci 函数,如左栏所示 。我在评论中指出,这个函数应该如右栏这样写:

static BigInteger fibonacci(int num) {
  BigInteger x = BigInteger.ZERO;
  BigInteger y = BigInteger.ONE;
  BigInteger z;
  for(int i = 0; i < num; i++) {
    z = y;
    y = x.add(y);
    x = z;
  }
  return x;
}
static BigInteger fibonacci(int num) {
  BigInteger x = BigInteger.ZERO;
  BigInteger y = BigInteger.ONE;

  for(int i = 0; i < num; i++) {
    BigInteger z = y;
    y = x.add(y);
    x = z;
  }
  return x;
}

在左栏的程序中,变量 z 的作用域有 8 行,而右栏只有 4 行。

比较这两个程序

可能有人会认为,修改后的程序每次循环都要重新定义变量 z,意味着每次循环都要重新在栈中分配一个局部变量,导致性能没有修改前的程序好。

我们使用 javac 分别编译这两个程序,然后使用 javap -c 分别把编译后的 .class 文件反汇编为 Java bytecode,如下所示:

bytecode

从上图中可以看出,除了因为声明变量的顺序不同,导致变量 z 在这两个程序中分别是第 3 号和第 4 号变量,而变量 i 在这两个程序中分别是第 4 号和第 3 号变量之外,这个两个程序是相同的。也就是,它们的运行速度完全一样。

fibonacci 函数中,各个量如下所示:

  • 量 0: num
  • 量 1: x
  • 量 2: y
  • 量 3: z 或 i
  • 量 4: i 或 z
  • 量 5: BigInteger.ZERO
  • 量 6: BigInteger.ONE
  • 量 7: BigInteger.add

实际上,局部变量表在使用 javac 编译这两个程序时就决定了,在 fibonacci 函数被调用之前就分配好所有的局部变量(包括函数的参数),在函数结束时随退栈操作一起释放。

变量的作用域最小化原则

根据《代码大全》第 10 章“使用变量的一般事项”第 4 节“作用域”:

  • 作用域或者可见性指的是变量在程序内的可见和可引用的范围。
  • 减少变量作用域的方法之一就是尽量使变量局部化。
  • 当对变量的作用域犹豫不决的时候,你应该倾向于选择该变量所能具有的最小的作用域:首先将变量局限于某个特定的循环,然后是局限于某个子程序,其次成为类的 private 变量、protected 变量,再其次对包可见(如果你的编程语言支持包),最后在不得已的情况下再把它作为全局变量。
  • 你应该把每个变量定义成只对需要看到它的、最小范围的代码段可见。如果你能把变量的作用域限定到一个单独的的循环或者子程序,那是再好不过的了。
  • 一般而言,应使变量的作用域最小化,把变量引用点尽可能集中在一起,从而能够对变量施加控制。将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。

Code

参考资料

  1. 豆瓣:代码大全
  2. Wikipedia: Java bytecode