繼續看到底要運行一個Java程序需要做的各種檢查是在什麼時候發生的。這次我們來看看接口調用的問題。
當前的JVM規范中,與方法調用相關的指令有4個:invokevirtual、invokeinterface、invokestatic與invokespecial。其中調用接口方法時使用的JVM指令是invokeinterface。這個指令與另外3個方法調用指令有一個顯著的差異:它不要求JVM的校驗器(verifier)檢查被調用對象(receiver)的類型;另外3個方法調用指令都要求校驗被調用對象。也就是說,使用invokeinterface時如果被調用對象沒有實現指定的接口,則應該在運行時而不是鏈接時拋出異常;而另外3個方法調用指令都要求在鏈接時拋出異常。
看看JVM規范是怎麼說的:
Java Virtual Machine Specification, 2nd Edition 寫道
invokeinterface
...
Runtime Exceptions
...
if the class of objectref does not implement the resolved interface, invokeinterface throws an IncompatibleClassChangeError.
可以留意一下另外3個方法調用指令中“IncompatibleClassChangeError”都是Linking Exception而不是Runtime Exception。
這種規定對Java程序來說可見的行為就是:如果一個方法通不過校驗,則整個方法都不會被執行;如果能通過校驗而拋出運行時異常,則方法當中拋出異常之前的部分都會被執行。
當然,我們直接用Java語言寫出來的程序很難引發這樣的錯誤,因為Java編譯器會做檢查來保證一定程度的類型安全。但是Java的class文件,或者說Java字節碼可以由Java編譯器以外的別的方式生成,此時就得不到Java編譯器對類型安全的保證,而要依賴於JVM對字節碼的校驗以及運行時的檢查了。
我是之前在讀John Rose對JSR 292的invokedynamic的講解時留意到invokeinterface的這個特點的。John特別提到invokedynamic就像invokeinterface一樣,都不在校驗時對被調用對象的類型做檢查。不過之前一直沒見過調用對一個沒實現接口的對象調用接口方法實際是個什麼樣子。
好吧,這次就來看個例子。首先創建一個接口IFoo,一個實現了該接口的類FooImpl,和一個未實現該接口的類Bar:
IFoo.java:
Java代碼
public interface IFoo { void method(); }
FooImpl.java:
Java代碼
public class FooImpl implements IFoo { public void method() { System.out.println("FooImpl.method()"); } }
Bar.java:
Java代碼
public class Bar { public void anotherMethod() { System.out.println("Bar.anotherMethod()"); } }
接下來構造出一個能引發運行時異常的程序。大致的意思是這樣的:
Java代碼
public class TestInterfaceCall { public static void main(String[] args) { IFoo f = new FooImpl(); f.method(); Bar b = new Bar(); ((IFoo)b).method(); // << watch this } }
注意第7行代碼。如果就這麼寫然後編譯的話,生成的字節碼裡會有一個checkcast指令將Bar類型的引用轉換為IFoo類型的引用。如果有checkcast的話,運行時就會在該指令上報錯,因為Bar沒有實現IFoo。但這次我想引發的錯誤不是強制轉換相關,而是接口調用相關:想達到的效果是以b為被調用對象,但調用IFoo.method()而不是Bar上已有的方法。所以要靠自己來生成字節碼,避免checkcast指令。
上個月的兩個相關帖裡我使用了ObjectWeb的ASM庫來生成Java字節碼。這個庫很實用,但寫起來還是繁瑣了些。這次我決定用Charles Nutter寫的bitescript。使用該庫需要JRuby 1.2.0或更高的版本,我這次用的是JRuby 1.3.0RC2。
安裝bitescript只要用JRuby的gem就行:
Command prompt代碼
gem install bitescript
然後編寫生成字節碼用的腳本:
test.rb:
Ruby代碼
require 'rubygems' require 'bitescript' include BiteScript IFoo = Java::IFoo FooImpl = Java::FooImpl Bar = Java::Bar fb = FileBuilder.build(__FILE__) do public_class 'TestInterfaceCall' do public_static_method 'main', void, string[] do # IFoo f = new FooImpl(); new FooImpl dup invokespecial FooImpl, '<init>', [void] astore 1 # f.method(); aload 1 invokeinterface IFoo, 'method', [void] # Bar b = new Bar(); new Bar dup invokespecial Bar, '<init>', [void] astore 2 # ((IFoo)b).method(); aload 2 ## checkcast IFoo # skip the cast to trigger IncompatibleClassChangeError invokeinterface IFoo, 'method', [void] returnvoid end end end fb.generate do |filename, class_builder| File.open(filename, 'w') do |file| file.write(class_builder.generate) end end
可以對比一下直接用ASM時的代碼,顯然用bitescript要簡潔易懂得多。Good job, Charles!
執行這個腳本,把生成出來的TestInterfaceCall.class與前面的IFoo.class、FooImpl.class和Bar.class放在同一個目錄下。然後運行java TestInterfaceCall,
Command prompt代碼
D:\sdk\jruby-1.3.0RC2\test_bitescript>java TestInterfaceCall FooImpl.method() Exception in thread "main" java.lang.IncompatibleClassChangeError at TestInterfaceCall.main(test.rb)
可以看到程序打印出了"FooImpl.method()"這句話,也就是說異常是在運行時而不是鏈接時拋出的。
如今用到Java的字節碼改寫/動態生成的工具已經很普遍了,如果在使用它們的時候不夠小心,相信這裡所提到的運行時異常也會有機會見到的 =v=
P.S. 我這次運行的環境是:
D:\sdk\jruby-1.3.0RC2\test_bitescript>java -version java version "1.6.0_11" Java(TM) SE Runtime Environment (build 1.6.0_11-b03) Java HotSpot(TM) Client VM (build 11.0-b16, mixed mode, sharing)