当一个常量并不是真正的常量时

原创 2003年05月14日 13:53:00

当一个常量并不是真正的常量时<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

 

 

作者:Vladimir Roubtsov

 

QJava中使用循环定义(cyclic definitions)会产生什么负面作用?

    注:循环定义(如a = b, b = c, c = a)

 

A通常,Java编译结果都是动态输出的:你可以只重新编译一个类,其余类将会自动获得这个改变。这是因为.class在进行类与类之间操作时采用动态链接的形式,并且链接只会在类加载的时候才被确定。

 

在本文中,我将为大家讲述一个众所周知又非常有意义的异常,但它能产生很细微的并难以发现的错误。循环定义操作将导致后面程序中产生一些很难确定的异常。

 

内嵌常量(Inlined static final constants)

考虑以下两个例子

 

public class <?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />Main implements InterfaceA

{

    public static void main (String [] args)

    {

        System.out.println (A);

    }

 

} // End of class

 

public interface InterfaceA

{

    public static final int A = 1;

 

} // End of interface

 

依照Java规范,所有的static final(field)在编译时不是将表达式作为其值初始化,而是先将表达式计算后使用表达式值进行初始化。换句话说,在运行时,类Main不会动态的去获取InterfaceA中A的值。而是直接使用1代替“A”编译进Main.main()中。你可以用下面的方法检查Javap dump,来证实这一点:

 

Method void main(java.lang.String[])

   0 getstatic #23 <Field java.io.PrintStream out>

   3 iconst_1

   4 invokevirtual #29 <Method void println(int)>

   7 return

 

上面的iconst_1在调用System.out.println()之前指令将整数1(Push)进JVM操作堆栈中。将这个值嵌入字节码中,而没有使用Interface.A。如果你重新编译InterfaceA.java,将A改为2,但不要重新编译Main.java,这时Main.main()输出的值仍和以前一样。

 

任何一个有经验的Java程序员都知道以上这些内容。这只是Java在调用时的一些微不足道的特点。当我们使用一个可以仅根据源文件修改时间进行增量重编译的Java编译工具时需要特别注意这个特性。(见Note1)因为,这个特点有时会导致编译器编译出一个不理想的版本。

 

它看上去像一个常量,但它不是

注意,前文所示在某种情况下可能会产生不同的效果。例如,当用于初始化域的表达式只可以在运行时被求值的话,不会出现前文所示的情况。

 

public interface InterfaceA

{

    public static final int A = new java.util.Random ().nextInt ();

 

} // End of interface

 

 

InterfaceA改变后, Main.main()的字节码将变成:

 

Method void main(java.lang.String[])

   0 getstatic #27 <Field java.io.PrintStream out>

   3 getstatic #31 <Field int A>

   6 invokevirtual #37 <Method void println(int)>

   9 return

 

注意,字节码中出现了对于A的动态引用。

 

上面的Interface.A改变是十分明显的。然而,想象一下在一个应用程序中使用下列三个接口会是什么情况:

 

public interface InterfaceA

{

    public static final int A = 2 * InterfaceB.B;

 

} // End of interface

 

public interface InterfaceB

{

    public static final int B = InterfaceC.C + 1;

 

} // End of interface

 

public interface InterfaceC extends InterfaceA

{

    public static final int C = A + 1;

 

} // End of interface

 

 

试用这个版本的main():

 

 

public class Main implements InterfaceA, InterfaceB, InterfaceC

{

    public static void main (String [] args)

    {

        System.out.println (A + B + C);

    }

 

} // End of class

 

打印结果为7。暂时忘掉这个值并改变main()中一个看起来并不重要的地方:

 

public class Main implements InterfaceA, InterfaceB, InterfaceC

{

    public static void main (String [] args)

    {

        System.out.println (C + B + A); // The sum is still the same, right?

    }

 

} // End of class

 

 

现在结果是6. 重新安排求和的次序后结果似乎被改变了. 你想要看到这样的结果么?让我们分析一下为什么会这样。

 

虽然,ABC表面看想来好像在编译时可以由表达式值初始化,其实并非如此,因为这是个循环定义,其中A依赖B,而B依赖C,最后C又依赖A

 

结果,编译器不能在加载/初始化三个域时做任何替换静态初始化代码的行为。原因是三个域中各个域都要依靠重载另一个域才能求出值。(见Note2)计算出的第一个main()结果后,我注意到InterfaceA是第一个被加载的(加法计算法则的顺序是从左向右加),于是InterfaceC是第一个被完整初始化的(依据三者间的依赖关系)。InterfaceC依赖InterfaceA,而此时A还未被初始化,(因此A的值是0)。这时C的值是1,B的值是2,A的值当然就是4了,相加得出结果7。第二个版本就作为读者的练习吧。(提示:三个值都将有所变化)

 

或许我这个例子举的不够好。然而,想像一下相同的三个接口分散在一个庞大的代码库中不同的包中会是一个什么样的情形:循环定义”可并不简单。看上去每个都好像已在内部被定义,不仔细的查看是不会看出什么问题的。但是,这些问题将在以后导致很难以排除的问题:尽管一些表达式的值在你的应用程序的版本中可被复写,但它及可能在某处导致不同的类的载入顺序和无法预见的执行顺序。例如,在并发线程队列里不可预知的改变可能会导致不同的类加载顺序。不幸地是,大多数编译器不会考虑这些错误甚至连一个对程序员的警告都没有。

 

关于作者:
Vladimir Roubtsov
他从1995年起开始接触Java,有超过13年的多语言编程经验. 现在,他是Trilogy公司的高级工程师,主要工作是开发企业软件。

资源:

 

关于三目运算符与左右位移操作符

三目运算符的表达 ----a?b:c 相当于 if(a){b}else{c} 在Java编程规范中提到:当后有两个表达式有一个是常量表达式时,另外一个类型是T时,而常量表达式可以被T表示时,输出结果...
  • szcsun5
  • szcsun5
  • 2017年02月27日 22:33
  • 166

inno setup使用1 记录一下相关参数

Inno Setup的使用。这个是来自程序自己有使用帮助。这一部分到Setup section。这个也是东西最多的section。现在都还只是翻译,以后会增加相应的效果。Inno setup用iss后...
  • powerat123
  • powerat123
  • 2018年01月11日 11:29
  • 20

编译期常量

编译期常量static final
  • u012852385
  • u012852385
  • 2015年10月11日 17:00
  • 455

Java常量定义需要注意事项及static作用(复习)

在任何开发语言中,都需要定义常量。在Java开发语言平台中也不例外。不过在Java常量定义的时候,跟其他语言有所不同。其有自己的特色。在这篇文章中,主要针对Java语言中定义常量的注意事项进行解析,帮...
  • u011131296
  • u011131296
  • 2015年01月21日 13:42
  • 1722

C++---如何在类中声明一个常量?

需求:有时候需要在类定义中声明一个常量,怎么声明呢? 常见错误用法: class Student {。。。    const int  Len=10;    char name[Len];  //错误...
  • hezikui1987
  • hezikui1987
  • 2013年07月23日 20:17
  • 944

ES6——Day2(跨模块常量+全局对象属性)

1、什么是模块? 在面向对象编程设计中,模块至少归属于一个“类”; 在javascript编程中,可以把js文件归属于一个模块。 案例1:module.js //module.js expo...
  • zxy9602
  • zxy9602
  • 2017年03月09日 19:52
  • 800

Java中定义常量几种方式

在开发中定义常量是很常见的事,但常量定义有哪几种方式可选?各种定义方式有什么优缺点?咱们就用这篇小文来梳理下^_^ 1.通过接口Interface来定义(不推荐) 定义方式...
  • hezikui1987
  • hezikui1987
  • 2017年08月20日 22:51
  • 1810

一个变量,一个常量,用equals()方法比较,让咱们,看看到底是常量放前面好啊,还是变量放前面好

转载自:http://www.cnblogs.com/baby-zhude/p/4218846.html 其实说白了,如果是两个都是变量,那就放哪都行没啥区别;(有点废话了) 现在主要...
  • github_40018627
  • github_40018627
  • 2017年09月19日 14:08
  • 304

equals方法变量和常量位置区别

我们说的左右位置是基于一个常量一个变量来说的,如果都是变量那么左右位置没有任何区别。 if (i == 2) { if (stringtokenizer.hasMoreTokens()) ...
  • woshishui6501
  • woshishui6501
  • 2015年12月31日 14:07
  • 2383

java中定义常量方法

一、常量定义的基本注意事项。    在Java语言中,主要是利用final关键字(在Java类中灵活使用Static关键字)来定义常量。当常量被设定后,一般情况下就不允许再进行更改。如可以利用如下的...
  • bugDemo
  • bugDemo
  • 2012年06月10日 20:33
  • 8933
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:当一个常量并不是真正的常量时
举报原因:
原因补充:

(最多只允许输入30个字)