計算機程式的思維邏輯 (22) - 程式碼的組織機制

swiftma發表於2016-09-14

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (22) - 程式碼的組織機制

使用任何語言進行程式設計都有一個類似的問題,那就是如何組織程式碼,具體來說,如何避免命名衝突?如何合理組織各種原始檔?如何使用第三方庫?各種程式碼和依賴庫如何編譯連線為一個完整的程式?

本節就來討論Java中的解決機制,具體包括包、jar包、程式的編譯與連線,從包開始。

包的概念

使用任何語言進行程式設計都有一個相同的問題,就是命名衝突,程式一般不全是一個人寫的,會呼叫系統提供的程式碼、第三方庫中的程式碼、專案中其他人寫的程式碼等,不同的人就不同的目的可能定義同樣的類名/介面名,Java中解決這個問題的方法就是包。

即使程式碼都是一個人寫的,將很多個關係不太大的類和介面都放在一起,也不便於理解和維護,Java中組織類和介面的方式也是包。

包是一個比較容易理解的概念,類似於電腦中的資料夾,正如我們在電腦中管理檔案,檔案放在資料夾中一樣,類和介面放在包中,為便於組織,資料夾一般是一個層次結構,包也類似。

包有包名,這個名稱以頓號(.)分隔表示層次結構。比如說,我們之前常用的String類,就位於包java.lang下,其中java是上層包名, lang是下層包名,帶完整包名的類名稱為其完全限定名,比如String類的完全限定名為java.lang.String。Java API中所有的類和介面都位於包java或javax下,java是標準包,javax是擴充套件包。

接下來,我們討論包的細節,從宣告類所在的包開始。

宣告類所在的包

語法

我們之前定義類的時候沒有定義其所在的包,預設情況下,類位於預設包下,使用預設包是不建議的,文章中使用預設包只是簡單起見。

定義類的時候,應該先使用關鍵字package,宣告其包名,如下所示:

package shuo.laoma;

public class Hello {
    //類的定義
}
複製程式碼

以上宣告類Hello的包名為shuo.laoma,包宣告語句應該位於原始碼的最前面,前面不能有註釋外的其他語句。

包名和檔案目錄結構必須匹配,如果原始檔的根目錄為 E:\src\,則上面的Hello類對應的檔案Hello.java,其全路徑就應該是E:\src\shuo\laoma\Hello.java。如果不匹配,Java會提示編譯錯誤。

命名衝突

為避免命名衝突,Java中命名包名的一個慣例是使用域名作為字首,因為域名是唯一的,一般按照域名的反序來定義包名,比如,域名是:apache.org,包名就以org.apache開頭。

沒有域名的,也沒關係,使用一個其他程式碼不太會用的包名即可,比如本文使用的"shuo.laoma",表示"老馬說程式設計"中的例子。

如果程式碼需要公開給其他人用,最好有一個域名以確保唯一性,如果只是內部使用,則確保內部沒有其他程式碼使用該包名即可。

組織程式碼

除了避免命名衝突,包也是一種方便組織程式碼的機制,一般而言,同一個專案下的所有程式碼,都有一個相同的包字首,這個字首是唯一的,不會與其他程式碼重名,在專案內部,根據不同目的再細分為子包,子包可能又會分為子包,形成層次結構,內部實現一般位於比較底層的包。

包可以方便模組化開發,不同功能可以位於不同包內,不同開發人員負責不同的包。包也可以方便封裝,供外部使用的類可以放在包的上層,而內部的實現細節則可以放在比較底層的子包內。

通過包使用類

同一個包下的類之間互相引用是不需要包名的,可以直接使用。但如果類不在同一個包內,則必須要知道其所在的包,使用有兩種方式,一種是通過類的完全限定名,另外一種是將用到的類引入到當前類。

只有一個例外,java.lang包下的類可以直接使用,不需要引入,也不需要使用完全限定名,比如String類,System類,其他包內的類則不行。

比如說,使用Arrays類中的sort方法,通過完全限定名,可以這樣使用:

int[] arr = new int[]{1,4,2,3};
java.util.Arrays.sort(arr);
System.out.println(java.util.Arrays.toString(arr));
複製程式碼

顯然,這樣比較囉嗦,另外一種就是將該類引入到當前類,引入的關鍵字是import,import需要放在package定義之後,類定義之前,如下所示:

package shuo.laoma;
import java.util.Arrays;

public class Hello {
    public static void main(String[] args) {
        int[] arr = new int[]{1,4,2,3};
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}
複製程式碼

import時,可以一次將某個包下的所有類引入,語法是使用.*,比如,將java.util包下的所有類引入,語法是:import java.util.*,需要注意的是,這個引入不能遞迴,它只會引入java.util包下的直接類,而不會引入java.util下巢狀包內的類,比如,不會引入包java.util.zip下面的類。試圖巢狀引入的形式也是無效的,如import java.util.*.*。

在一個類內,對其他類的引用必須是唯一確定的,不能有重名的類,如果有,則通過import只能引入其中的一個類,其他同名的類則必須要使用完全限定名。

引入類是一個比較繁瑣的工作,不過,大多數Java開發環境都提供工具自動做這件事,比如,在Eclipse中,通過選單"Source->Organize Imports"或對應的快捷鍵ctrl+shift+O就可以自動管理引入類。

包範圍可見性

前面幾節我們介紹過,對於類、變數和方法,都可以有一個可見性修飾符,public/private/protected,而上節,我們提到可以不寫修飾符。如果什麼修飾符都不寫,它的可見性範圍就是同一個包內,同一個包內的其他類可以訪問,而其他包內的類則不可以訪問。

需要說明的是,同一個包指的是同一個直接包,子包下的類並不能訪問,比如說,類shuo.laoma.Hello和shuo.laoma.inner.Test,其所在的包shuo.laoma和shuo.laoma.inner是兩個完全獨立的包,並沒有邏輯上的聯絡,Hello類和Test類不能互相訪問對方的包可見性方法和屬性。

另外,需要說明的是protected修飾符,protected可見性包括包可見性,也就是說,宣告為protected,不僅表明子類可以訪問,還表明同一個包內的其他類可以訪問,即使這些類不是子類也可以。

總結來說,可見性範圍從小到大是:

private < 預設(包) < protected < public

jar包

為方便使用第三方程式碼,也為了方便我們寫的程式碼給其他人使用,各種程式語言大多有打包的概念,打包的一般不是原始碼,而是編譯後的程式碼,打包將多個編譯後的檔案打包為一個檔案,方便其他程式呼叫。

在Java中,編譯後的一個或多個包的Java class檔案可以打包為一個檔案,Java中打包命令為jar,打包後的檔案字尾為.jar,一般稱之為jar包。

可以使用如下方式打包,首先到編譯後的java class檔案根目錄,然後執行如下命令打包:

jar -cvf <包名>.jar <最上層包名>

比如,對前面介紹的類打包,如果Hello.class位於E:\bin\shuo\laoma\Hello.class,則可以到目錄 E:\bin下,然後執行:

jar -cvf hello.jar shuo

hello.jar就是jar包,jar包其實就是一個壓縮檔案,可以使用解壓縮工具開啟。

Java類庫、第三方類庫都是以jar包形式提供的。如何使用jar包呢?將其加入到類路徑(classpath)中即可。類路徑是什麼呢?

程式的編譯與連線

從Java原始碼到執行的程式,有編譯和連線兩個步驟。編譯是將原始碼檔案變成一種位元組碼,字尾是.class的檔案,這個工作一般是由javac這個命令完成的。連線是在執行時動態執行的,.class檔案不能直接執行,執行的是Java虛擬機器,虛擬機器聽起來比較抽象,執行的就是java這個命令,這個命令解析.class檔案,轉換為機器能識別的二進位制程式碼,然後執行,所謂連線就是根據引用到的類載入相應的位元組碼並執行。

Java編譯和執行時,都需要以引數指定一個classpath,即類路徑。類路徑可以有多個,對於直接的class檔案,路徑是class檔案的根目錄,對於jar包,路徑是jar包的完整名稱(包括路徑和jar包名),在Windows系統中,多個路徑用分號;分隔,在其他系統中,以冒號:分隔。

在Java原始碼編譯時,Java編譯器會確定引用的每個類的完全限定名,確定的方式是根據import語句和classpath。如果import的是完全限定類名,則可以直接比較並確定。如果是模糊匯入(import帶.*),則根據classpath找對應父包,再在父包下尋找是否有對應的類。如果多個模糊匯入的包下都有同樣的類名,則Java會提示編譯錯誤,此時應該明確指定import哪個類。

Java執行時,會根據類的完全限定名尋找並載入類,尋找的方式就是在類路徑中尋找,如果是class檔案的根目錄,則直接檢視是否有對應的子目錄及檔案,如果是jar檔案,則首先在記憶體中解壓檔案,然後再檢視是否有對應的類。

總結來說,import是編譯時概念,用於確定完全限定名,在執行時,只根據完全限定名尋找並載入類,編譯和執行時都依賴類路徑,類路徑中的jar檔案會被解壓縮用於尋找和載入類。

小結

本節介紹了Java中程式碼組織的機制,包和jar包,以及程式的編譯和連線。將類和介面放在合適的具有層次結構的包內,避免命名衝突,程式碼可以更為清晰,便於實現封裝和模組化開發,通過jar包使用第三方程式碼,將自身程式碼打包為jar包供其他程式使用,這些都是解決複雜問題所必需的。

我們一直在說,程式主要就是對資料的操作,為表示和運算元據,我們介紹了基本型別,類以及介面,下節,我們介紹Java中表示和操作一種特殊資料的機制 - 列舉


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (22) - 程式碼的組織機制

相關文章