分享
 
 
 

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

王朝java/jsp·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

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

作者:Vladimir Roubtsov

Q:在Java中使用“循环定义”(cyclic definitions)会产生什么负面作用?

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

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

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

内嵌常量(Inlined static final constants)

考虑以下两个例子

public class 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. 重新安排求和的次序后结果似乎被改变了. 你想要看到这样的结果么?让我们分析一下为什么会这样。

虽然,A、B、C表面看想来好像在编译时可以由表达式值初始化,其实并非如此,因为这是个“循环定义”,其中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公司的高级工程师,主要工作是开发企业软件。

资源:

Note 1: The .class format is extendible, and it is possible to preserve all compilation dependencies between classes via a new .class attribute, including dependencies through static final constants. Rumor has it this is precisely what Sun is considering for future Java versions, although I have no data about whether it will happen in Java 1.5.

Note 2: Yes, Java interfaces can contain static initializers. Although the language syntax prohibits this, it is allowed at the byte code level.

Java Language Specification's Section 13.4.8 discusses the effects of declaring a field static final and the justification for the current behavior:

http://java.sun.com/docs/books/jls/second_edition/html/binaryComp.doc.html#45139

Want more? See the Java Q&A index page for the full Q&A catalog:

http://www.javaworld.com/columns/jw-qna-index.shtml

For more than 100 insightful Java tips, visit JavaWorld's Java Tips index page:

http://www.javaworld.com/columns/jw-tips-index.shtml

Browse the Core Java section of JavaWorld's Topical Index:

http://www.javaworld.com/channel_content/jw-core-index.shtml

Get more of your questions answered in our Java Beginner discussion:

http://forums.devworld.com/webx?50@@.ee6b804

Sign up for JavaWorld's free weekly email newsletters:

http://www.javaworld.com/subscribe

You'll find a wealth of IT-related articles from our sister publications at IDG.net

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有