Java Package為何被設計?如果你沒想過,我這裡或許可以提供一種視角。
想象一下,作為一個語言的設計者,你一定會考慮一個問題:變數名的衝突。為了解決這個問題,C++引入了名稱空間(namespace),而Java引入了package。
1.變數名衝突的情況
我們平常接觸的所有軟體編寫,基本都是以檔案為基本單位儲存的,所以下面以檔案為維度進行討論。
- 同一檔案內:在同一個檔案中的變數名衝突,是完全可以通過編譯去控制的,如果編譯階段檢測到兩個變數重複宣告,可以報出錯誤給開發者。
- 不同檔案間:當小A和小B分別編輯兩個檔案A.cpp和B.cpp時,就無法保證這兩個檔案中的變數名無重複,比如這兩個檔案中都存在
int b
變數,則小A和小B都可以正常的分別編譯A.cpp和B.cpp,所以在這裡小A和小B都不會即時發現問題;但當小C引用了A.cpp和B.cpp,這兩個檔案合併編譯的時候就會出錯,可能這時小A和小B已經出去玩了。。。而這和單檔案編譯報錯不同,這是兩個不同的開發者開發的原始碼,小C最好不要亂改。
2.提出解決衝突的方案
現在擺在我們面前的,就是要解決小C的困擾,我們有以下兩種方案:
- 把小A和小B叫回來,告訴他們他們再次宣告變數的時候,需要互相通知,並且現在馬上改一下他們造成的問題,這樣用人工的方式避免掉變數名的衝突。
- 引入一種編譯機制,編譯檔案的時候,給編譯的檔案內的變數名加上"檔名.",例如A.cpp裡面的int b編譯時標識成
int A.b
,這樣,只要保證檔案不重名,就不用擔心這兩個檔案中的所有變數會有重複了,至於檔名就交給OS的檔案系統去判斷重複不重複了。
這個方案的選擇不是很難,正常人都會選第2種,但其實第2種方案還是有待完善的,不過我們的大方向走對了。
3.解決方案的優化
在變數名前面加上一個標識字首(檔名.)確實是一種辦法,我們暫時稱為字首法。但上面的方案只是會在編譯階段自動追加字首,這樣會引出一個問題:我如果在c.cpp中想引用A.cpp的int b
變數,我又該如何?
所以,我們可以將字首法運用在程式碼編寫階段,而不是編譯階段,什麼意思呢?舉個例子:
A.cpp編寫的時候所有的變數都要加一個字首(暫時約定為檔名),所以之前編寫的int b應該改為
int A.b
,注意,這裡是在編輯時改為了int A.b
,而不是編譯時,要區分出這個時機。
這樣,我便可以在C.cpp檔案中,直接用A.b這個變數了。通過將字首法轉移到了編輯階段,實現了多檔案之間可以互相引用變數和方法而且不會引發變數名或方法名衝突了。
4.解決方案的例項
字首法現在稍有成就了,解決了多檔案之間的命名衝突,但還是有一些問題的。
字首約定為"檔名."其實並不安全,因為我們知道同一目錄下檔案系統會要求不能存在重名的檔案,但是不同目錄下就可以,所以可能存在/Usr/A.cpp和/Dev/A.cpp兩個檔案合併編譯的時候會發生錯誤。
同樣,顯而易見我們可以給出N種方案,這裡給出三種:
我們不要把字首和檔名劃等號,我們可以給每一個檔案的字首指定不同的值,怎麼做呢?在每份程式碼檔案中用一個關鍵字(例如namespace)來標識這個檔案的字首,寫法是這樣的namespace devA或者namespace usrA,這樣就給兩個相同檔名的檔案賦予了不同的字首,而他們之間也可以互相引用。
我們還是把字首和檔名綁在一起,但是這次狠一點,把資料夾也綁進來,什麼意思呢?就是/Usr/A.cpp的變數都寫成
int usr.A.b
,這樣其實字首就和檔案層次結合起來了,這樣也可以完美解決問題。我們不要考慮字首,檔案內的字首也不要,只要在跨檔案引用的地方動態指定字首就可以了,什麼意思呢?就是/usr/A.cpp和/usr/A.cpp檔案裡變數
int b
還是寫int b
,但是當C.cpp引用他們兩個的時候,再他們指定字首,看下面:imort /usr/A.cpp userA;
imort /dev/A.cpp decA;
print userA.b -- 此處引用變數
print devA.b -- 此處引用變數
其實這三種也是分別對應的C++名稱空間、Java Packge機制、Nodejs名稱空間的解決方案的例項。
注意:C++ 名稱空間和Java Package的區別在這裡也可以看出來,在名稱空間裡只是每個檔案中的namespace不同,和物理磁碟的檔名、路徑無關;而Java Pakage是和檔案層次綁在一起的,所以是和物理儲存層次有關的。
4.package機制總結
既然我們的題目是Java Package,那麼繼續在第二種方案上繼續往前。
對於Java的某個類,它唯一的識別符號是package+類名,比方說com.test.Test,而我們編寫的時候是通過package關鍵字,指明瞭Test類的的Package字首為com.test。編譯的時候,我們通過下面命令進行編譯:
/ > javac Test.java
通過這行命令,在根目錄下會生成一個Test.class檔案,這時候大家注意到了,Test的Package編譯完了並沒有和檔案系統的層次有對應關係,是的,確實沒有,package的層次關係會指示出Test類應該所在的路徑,以便可以讓jvm找到。這裡,你完全可以自己新建目錄 /com/test/ 並將Test.class 放到這個目錄裡面,在回到根目錄,執行 java com.test.Test
,你會發現正常執行。
另外,在Test 裡面的package已經指明層次關係了,其實是可以讓javac自動生成對應的檔案層次,並把Test.class放進去,免去手動移動的麻煩,就是javac後面加一個-d 目錄名。
javac -d ./ com.test.Test
5.結尾
之前是對javac和package結合的地方很不明白,通過這麼從零推導,現在明白多了,順便分享給大家,希望對大家有幫助。