繼續探討執行 Java 程式所需各種檢查發生的時機,這次關注介面呼叫問題。
當前 JVM 規範中,與方法呼叫相關的指令有四個:invokevirtual、invokeinterface、invokestatic、invokespecial。呼叫介面方法時使用 invokeinterface,它與其他三指令有顯著差異:不要求 JVM 驗證器檢查被呼叫物件的型別;其他三指令都要求驗證被呼叫物件。意即,使用 invokeinterface 時若被呼叫物件未實作指定介面,應在執行期而非鏈結期拋出異常;其他三指令要求在鏈結期拋出異常。
Java Virtual Machine Specification, 2nd Edition 寫道:
invokeinterface...
Runtime Exceptions...
if the class of objectref does not implement the resolved interface, invokeinterface throws an IncompatibleClassChangeError.
留意其他三指令中的 IncompatibleClassChangeError 皆為 Linking Exception 而非 Runtime Exception。
若方法通不過驗證,整個方法不會被執行;若通過驗證但拋出執行期異常,則拋出異常前的程式碼都會被執行。
直接用 Java 語言寫出的程式很難引發此錯誤,因 Java 編譯器會檢查保證型別安全。但 Java class 檔案或位元組碼可由 Java 編譯器外的其他方式產生,此時無編譯器型別安全保證,需依賴 JVM 對位元組碼驗證及執行期檢查。
先建立介面 IFoo、實作類別 FooImpl、未實作介面的類別 Bar:
public interface IFoo { void method(); }
public class FooImpl implements IFoo { public void method() { System.out.println("FooImpl.method()"); } }
public class Bar { public void anotherMethod() { System.out.println("Bar.anotherMethod()"); } }
構造引發執行期異常程式:
public class TestInterfaceCall {
public static void main(String[] args) {
IFoo f = new FooImpl();
f.method();
Bar b = new Bar();
((IFoo)b).method(); // << watch this
}
}
若直接編譯,位元組碼會有 checkcast 指令將 Bar 轉為 IFoo,執行期在該指令報錯。但我們要引發的是介面呼叫相關錯誤:以 b 為被呼叫物件,呼叫 IFoo.method() 而非 Bar 既有方法。需自行產生位元組碼避開 checkcast。
使用 Charles Nutter 的 bitescript 函式庫(需 JRuby 1.2.0+,此處用 1.3.0RC2):
gem install bitescript
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, '', [void]
astore 1
# f.method();
aload 1
invokeinterface IFoo, 'method', [void]
# Bar b = new Bar();
new Bar
dup
invokespecial Bar, '', [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
執行腳本,將產生的 TestInterfaceCall.class 與 IFoo.class、FooImpl.class、Bar.class 放同目錄。執行 java TestInterfaceCall:
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 位元組碼改寫/動態產生工具已普遍,使用時若不夠小心,此處提到的執行期異常也有機會遇到。
執行環境:java version "1.6.0_11"
