標籤:
PL4 聲明模型(綁定、可見度、範圍和生命期)
1. 變數的含義
在程式設計語言的教學中,yqj2065經常困惑:是否應該使用比喻——例如將變數比喻成數學中的變數概念。不知道從哪位老先生開始,有這樣的說法:“不同於常量,變數的值是可以改變的”。如果你是我的學生,也這樣說,嗯,你過來,我保證不打死你。
在組合語言中,程式員就開始使用變數例如AX、BX以表示運算元的存放地址,而該地址中儲存實際的值。變數(variable)表示一個儲存場所,是原始碼中儲存值的符號化地址。
請記住變數和值的區別:變數是儲存場所,是一個杯子或者盒子,age、dog1這些變數(名)是杯子上面印的字,而值是杯子中裝的東西(水、點心或者遙控器)。
從其值是否可變,我寧可說:變數是(其值)可變的變數,而final variable是其值不可變的變數。
所以,懇請老師們、作者們在介紹變數時,不要將它和常量(編程導論中使用“文字”、字面值或常量值這樣的術語——見1.2.3)聯絡在一起。
參考型別的變數稱為引用變數(reference variable) ,如Dog dog1或String name中聲明的dog1和name,它們被分配同樣大小的記憶體空間,一般是32位(與JVM的實現相關)。參考型別的變數儲存的值即引用值,簡稱引用(reference)。【一些簡單的概念問題解決後,就不會出現這樣的困惑:Java中方法的參數是否能夠按引用傳遞。】
2. 變數的綁定
如:privateString name ;
publicstatic final int X = 123,Y=456;
在前面的2.2.1中,介紹了
1. 變數與類型的綁定。這裡就簡單地介紹變數的
聲明語句(declaration statements )格式。
【變數的命名,除了遵照標識符的通用文法外(如不能夠以數字、*&^標點開頭),通常採用某種命名風格。過去曾流行過匈牙利命名法,也就是在變數名前加上其資料類型的縮寫,如iAge、fNum等;也有人建議對成員變數全部加m首碼、局部變數加_首碼。】(我在書中喜歡添加這方面的內容)
3. 變數的範圍
變數的範圍(scope)指可以訪問該變數的原始碼地區,或變數在程式中起作用的範圍或區間。這種詞法範圍(lexical scoping)最早在Lisp1.5中引入,後來加入到了Algol語言中。受它們的影響,其他語言都採用了範圍技術。在原始碼中,程式員們都喜歡縮排各個範圍以增強代碼的可讀性。
使用範圍的最基本原因,是因為簡單好用的變數名沒有想象中的那麼多,
有了範圍後,一個程式中可以有很多相同的變數名,它們和平共處——前提是擁有自己的範圍。變數的範圍由變數的聲明語句在原始碼中的位置決定。(許多知識點先列出,後面再詳解)
Java中按照範圍將變數劃分為4種,它們的聲明位置和範圍如下:
3 成員變數/域(member variable):在類體中聲明,其範圍是整個類。成員變數包括靜態域和執行個體域。對於執行個體域而言,靜態方法或靜態塊是其禁區,它不能夠在靜態環境中被使用。在聲明域時,還可以界定該變數的其他特徵。如存取修飾詞界定其他對象能否訪問該變數;static標識該變數為靜態域;final表示該變數為不可變數;transient變數,在對象序列化時被排除在外;volatile用於指出變數的值可以被不同的線程所修改。執行個體域作為對象的一部分,在建立對象的時候,至少被初始化為預設值,不需要特意地初始化它也可以使用。在類載入時,靜態域被初始化。
3 形參(formal arguments):在方法或構造器的形參表中聲明,其範圍是方法或構造器的整個方法體。形參是預留位置,無法使用賦值語句對它進行初始化而是依靠實參來賦值。
3 局部變數(local variable):在方法體或者方法體的某一代碼塊中聲明,其範圍是從賦值開始到隨後遇到的第一個右花括弧即“}”。也可以簡單地說,局部變數範圍從被定義的語句到包圍其定義的最小塊(block)的結束處。程式員必須顯式地使用賦值語句將局部變數初始化,才能夠在後面的代碼中使用它。
3 異常處理參數(exception handler parameters):它在catch子句中聲明,一般形式為try{}catch(異常類型 異常處理參數){}。當try中出現異常時,JVM將捕獲的異常對象與一系列catch子句的參數類型進行匹配,以找到需要執行的catch塊中的代碼。其他方面與形參相同。
變數與它的值綁定,包括變數初始化、賦值語句和++、--操作符。賦值操作要求類型相容,相容性由編譯器的類型檢查保證。
4. 可見度與變數隱藏不同變數的範圍可以嵌套,外面的範圍包含內部的範圍。與範圍直接相關的問題有兩個:1.哪些位置的代碼可訪問該變數,2.能否用簡單地變數名訪問它。
變數的可見度即可訪問性。通用規則是:變數在其範圍內可訪問,而對於範圍外的程式,盡量使其不可訪問。類的成員變數按照其存取修飾詞的不同具有不同的可見度,但是通常將執行個體變數修飾為private,使得它們在類之外不可見。局部變數(形參、異常處理參數)僅在其範圍中可見,在範圍外不能夠被使用。
總之,變數的可訪問性很直觀。
如果存在兩個同名的變數,使得一個變數暫時不能通過變數名直接存取,稱為變數隱藏(hiding)。Java中關於變數隱藏有幾條規則:
3 父類中聲明的域,不管其存取權限如何或是否被繼承,不管是靜態還是執行個體的,子類將隱藏父類中同名的域,而且兩者的資料類型是否相同也無關緊要。例子見[6.3.2域的繼承和隱藏]。
3 一個類中的域不得同名。
3 不同範圍的局部變數互不影響。因為它們彼此沒有任何關係。
3 一個局部變數(包括形參)的範圍中不允許存在同名的變數,因為Java語言的設計者認為,那樣會使程式產生了混淆。例如整個方法體中,不允許出現與形參同名的局部變數,否則編譯器會報錯。
3 不鼓勵局部變數隱藏欄位。域的範圍是整個類,在局部變數所處的獨立王國中,局部變數能夠隱藏欄位。例如Dog有域int age,在Dog中定義一個方法foo(),其中聲明了局部變數age。
voidfoo(){
int age=80;
System.out.println(age);
System.out.println(this.age);
}
使用變數名age直接存取的是局部變數,域被隱藏。但是域仍然存在,可以用this關鍵字訪問它。局部變數隱藏欄位,通常被視為不好的編程風格。
3 鼓勵set方法中形參隱藏欄位。
set方法都帶有參數,一般用於設定域的值。set方法的形參隱藏欄位而且被視為良好的習慣,這樣省去了為等價的變數取不同名字的麻煩。
5. 生命期變數的生命期(lifetime)指在程式執行時可以訪問該變數的時間。因為原始碼決定了程式的運行,故變數的範圍決定了變數生命期。
Java中按照生命期將變數劃分為3種:
3 執行個體域:執行個體成員變數是(該類)對象的一部分,每一個對象都擁有自己的、儲存該執行個體域的記憶體空間。只要對象存在,則對象的執行個體域就存在。new運算式建立對象為對象誕生的時間,並在Java堆中分配記憶體。對象的銷毀時間由記憶體回收行程管理,程式員不需要知道其精確的銷毀時間。邏輯上,對象不被任何引用所指向,就可以認為它被銷毀。
3 靜態域:靜態域屬於類,它在載入類時被建立,只要類存在,則類的靜態域就存在。它由所有對象共用,對象不擁有自己的副本。
3 局部變數(及形參、異常處理參數):它在某一個局部(方法體或者塊)中聲明,因此每一次執行該聲明(第一次賦值)語句時,就建立局部變數,局部執行完畢則銷毀。局部變數在棧中分配記憶體。
注意,在討論變數的生命期時,不考慮變數是基本類型變數還是引用變數。有人問,基本類型變數和引用變數在哪裡分配空間?這不是一個有效問題。
對變數的記憶體配置不看類型看位置,變數的生命期不看資料類型看位置。總之,對象的空間在堆上,局部變數的空間在棧上。基本類型變數和引用變數都可能分配在stack或heap中,關鍵是看它們聲明的位置——到底是域還是局部變數。
例如class A{inti;String s ="hi";},i和s作為對象的域,在堆中擁有自己的空間。
而foo(){int i;String s="hi";}中的i和s作為局部變數,在棧上分配空間。
聲明模型界定了語言中宣告身份識別符需要遵循的準則,包括綁定、範圍/範圍、生命期、可見度。本節以變數聲明為例介紹聲明模型。包和類名的聲明參考[第6章封裝]、方法的定義參考[2.3.2 方法同名問題]。
《編程導論(Java)·2.2.3 變數的聲明模型》