1.問題: 有如下代碼: public class Test { static { i = 0;// 給變數賦值可以正常編譯通過 System.out.print(i);// 編譯器會提示“非法向前引用”(illegal forward reference) } static int i = 1; ...
1.問題:
有如下代碼:
public class Test {
static {
i = 0;// 給變數賦值可以正常編譯通過
System.out.print(i);// 編譯器會提示“非法向前引用”(illegal forward reference)
}
static int i = 1;
}
這段代碼來自於《深入理解Java虛擬機:JVM高級特性與最佳實踐(第三版)》的第7章。
書里沒有對前向引用的進一步說明,我們自己探究一下。
把這段代碼放到IDEA中,System.out.print(i)
直接提示有錯誤。
編譯一下看看
編譯失敗,輸出的信息是
java:非法前向引用
2.什麼是forward reference?
forward reference可以翻譯成向前引用或者前向引用。百度百科沒有收錄該詞條,在維基百科中有該詞條,但是描述很簡單。
既然是Java編譯器報錯,那就去查詢Java官方資料,在JLS(Java語言規範)中找到了該詞的說明:
References to a field are sometimes restricted, even through the field is in scope. The following rules constrain forward references to a field (where the use textually precedes the field declaration) as well as self-reference (where the field is used in its own initializer).
即使該欄位在範圍內,對欄位的引用有時也會受到限制。以下規則限制對欄位的前向引用(其中使用文本在欄位聲明之前)以及自引用(其中欄位在其自己的初始值設定項中使用)。
這一句提到了兩個概念,前向引用和自引用。在JLS中說前向引用就是在欄位聲明之前使用它,再回頭看前言的例子中的代碼
public class Test {
static {
i = 0;
System.out.print(i);
}
static int i = 1;
}
i在未聲明時就在static塊中使用了,說明i = 0;
屬於前向引用。
如果註釋掉System.out.print(i);
這一行,程式可以正常編譯通過。
將上面的代碼稍微改造一下,列印i的值,看看是0還是1。
public class Test {
static {
i = 0;// 給變數賦值可以正常編譯通過
}
static int i = 1;
public static void main(String[] args) {
System.out.println(i);// 輸出1
}
}
i的值是1,符合預期。
複習一下類初始化的步驟,靜態變數(類變數)和靜態代碼塊(static{}塊)按照從上到下的順序執行。static int i = 1;
在i = 0;
後面,所以i的值是1。
再來看看Test這個類的位元組碼情況,使用jclasslib插件查看很方便。
Test類初始化方法<clinit>的位元組碼
iconst_0 // 把常量0壓入操作數棧
putstatic #3 <com/shion/init_code/Test.i : I> // 把棧頂的值0賦值給類變數i i->0
iconst_1 // 把常量1壓入操作數棧
putstatic #3 <com/shion/init_code/Test.i : I> // 把棧頂的值1賦值給類變數i i->1
return // 返回void
從位元組碼看到,類變數i確實被賦值了兩次,第一次是0,第二次是1。難道類變數沒聲明也可以賦值嗎?當然不是,答案已經呼之欲出了,我們來看看Test這個類的class文件,用IDEA查看反編譯後的代碼。
好家伙,原來是Java編譯器的功勞。
補充:
Java允許前向引用,從JLS的說明上看,不管是類變數還是實例變數皆可,Java編譯器編譯時會自動處理。
3.什麼情況屬於非法的前向引用?
既然知道了前向引用的概念,那什麼情況屬於非法的前向引用呢?
還是看JLS的說明:
解釋下什麼是簡單名稱,就是一個單詞或一個字母這種形式的名稱,和它相對的就是限定名稱(以.分隔的單詞序列,例如java.lang.Object或者System.out)。
JLS給出了一個詳細的例子來說明哪些情況屬於非法的前向引用:
點擊查看代碼
class UseBeforeDeclaration {
static {
x = 100;
// ok - assignment
int y = x + 1;
// error - read before declaration
int v = x = 3;
// ok - x at left hand side of assignment
int z = UseBeforeDeclaration.x * 2;
// ok - not accessed via simple name
Object o = new Object() {
void foo() { x++; }
// ok - occurs in a different class
{ x++; }
// ok - occurs in a different class
};
}
{
j = 200;
// ok - assignment
j = j + 1;
// error - right hand side reads before declaration
int k = j = j + 1;
// error - illegal forward reference to j
int n = j = 300;
// ok - j at left hand side of assignment
int h = j++;
// error - read before declaration
int l = this.j * 3;
// ok - not accessed via simple name
Object o = new Object() {
void foo(){ j++; }
// ok - occurs in a different class
{ j = j + 1; }
// ok - occurs in a different class
};
}
int w = x = 3;
// ok - x at left hand side of assignment
int p = x;
// ok - instance initializers may access static fields
static int u =
(new Object() { int bar() { return x; } }).bar();
// ok - occurs in a different class
static int x;
int m = j = 4;
// ok - j at left hand side of assignment
int o =
(new Object() { int bar() { return j; } }).bar();
// ok - occurs in a different class
int j;
}
通過查詢其他資料,大家總結了一句話:
通過簡單名稱引用的變數可以出現在左值位置,但不能出現在右值的位置
根據這條規則,再看上面的例子,int y = x + 1;
這行代碼中,x出現在了右值的位置。
再回頭看問題裡面的例子,System.out.print(i);
這行代碼,符合JLS里提到的
The reference appears either in a class variable initializer of C or in a static initializer of C (§8.7);
該引用出現在 C 的類變數初始值設定項(static欄位)中或 C 的靜態初始值設定項(static代碼塊)中(第 8.7 節);
4.前向引用的好處?
前向引用在語法上很容易造成誤解,特別是剛接觸Java編程的新人,那為什麼Java還要允許它的存在呢?
以下說明來自:
前向引用 - 為什麼這段代碼會編譯?
前向引用是一種編譯技術,允許在當前編譯單元中引用其他編譯單元中的類型。這種技術可以提高編譯速度,並允許在不同的編譯單元之間進行更靈活的組織和模塊化。
前向引用的優勢:
- 提高編譯速度:通過將類型聲明和定義分離,可以減少編譯器需要處理的代碼量,從而提高編譯速度。
- 更靈活的組織:前向引用允許在不同的編譯單元之間進行更靈活的組織和模塊化,這有助於提高代碼的可維護性和可讀性。
- 更好的性能:前向引用可以減少不必要的記憶體分配和釋放,從而提高程式的性能。
應用場景:- 大型項目:在大型項目中,前向引用可以幫助開發人員更好地組織代碼,提高代碼的可讀性和可維護性。
- 模塊化開發:在模塊化開發中,前向引用可以幫助開發人員將不同的模塊分離,從而提高代碼的可讀性和可維護性。
- 多編譯單元項目:在多編譯單元項目中,前向引用可以幫助開發人員更好地組織代碼,提高代碼的可讀性和可維護性。
5.總結
前向引用是Java語言層面允許的,Java編譯器進行編譯時會檢查非法的前向引用,其目的是避免迴圈初始化和其他非正常的初始化行為。
最後再簡單提一下什麼是迴圈引用,看一下下麵這個例子:
private int i = j;
private int j = i;
如果沒有前面說的強制檢查,那麼這兩句代碼就會通過編譯,但是很容易就能看得出來,i和j並沒有被真正賦值,因為兩個變數都是未初始化的(Java規定所有變數在使用之前必須被初始化),而這個就是最簡單的迴圈引用的例子。
理解前向引用等概念,可能對提高寫CRUD代碼的水平沒有什麼幫助,但是能幫助我們更好的理解這門編程語言。
參考鏈接:
https://stackoverflow.com/questions/14624919/illegal-forward-reference-java-issue
https://www.imooc.com/wenda/detail/557184
https://cloud.tencent.com/developer/information/前向引用 - 為什麼這段代碼會編譯?
https://docs.oracle.com/javase/specs/jls/se14/html/jls-8.html#jls-8.3.3
本文來自博客園,作者:三線程式猿,轉載請註明原文鏈接:https://www.cnblogs.com/shionsun/p/18040918