4.5包
前面我們已經聽過包(package)這個概念了,比如String類在java.lang包下,Arrays類在java.util包下。那麼為什麼要引入包的概念呢?我們思考一個問題:java類庫提供了上千個類,我們很難完全記住他們,如果我們編寫了一個類,類名和類庫中的某個類名字重複了怎麼辦?
其實一個作業系統的檔案系統也會遇到類似的問題,那麼windows系統如何解決的?這個你肯定知道,就是採用目錄層次結構。我們把硬碟分成很多分割槽,例如c盤、d盤等,這個叫做根目錄。然後再一級一級的建立資料夾,看圖:
我們在workspace和workspace2下分別建立同名檔案:hello.java,那麼這2個檔案的完整路徑為:
D:\Java大失叔\workspace\hello.java
D:\Java大失叔\workspace2\hello.java
因此不會有衝突。
4.5.1包的概念
在Java中,是用包來解決這個問題的。包就類似於檔案目錄層次結構,是採用圓點(.)來分割,例如java.util。包類似於名稱空間,我們平時說的類名,其實是類名的簡寫,一個類真正的名字是包名.類名,我們稱之為完整類名。例如String類的的完整類名是java.lang.String,Arrays類的完整類名是java.util.Arrays。有了包之後,我們只需把我們自己編寫的類放到我們自己的包中,這樣即使類名和類庫中的名字重複,也不會有衝突了(當然我們不建議這麼做),例如我們也編寫一個String類,放在我們自己的包javadashishu下,則我們的String類的完整類名是javadashishu.String,和java.lang.String不一樣,就不會有衝突。
為了保證包名不衝突,針對包名我們會有一套推薦的命名方法,Sun公司的建議是:
- 包名都採用小寫英文字母或數字,不能以圓點(.)開頭或結尾
- 用倒置的域名作為包名字首,例如
org.apache
com.google
- 子包名使用專案或功能的名字,儘量使用有意義的單詞
- 儘量避免和JDK中的類同名
例如,筆者可以把《Java從入門到失業》的例子都放到包:com.javadss.javase下。
4.5.2建立包
我們已經瞭解了包的概念,那麼怎麼把一個類放到一個包下呢?下面們用Eclipse來演示建立類和包的過程,首先我們先建立本書第四章的包:com.javadss.javase.ch04。右鍵點選工程src檔案,如下圖:
在彈出的如下頁面輸入包名:
點選“Finish”按鈕,我們發現,工程目錄多了一個包,然後我們在包目錄上點選右鍵,建立一個類PackageTest,Eclipse會自動生成程式碼如下:
package com.javadss.javase.ch04; public class PackageTest { }
我們發現,該檔案的第一行多了一句程式碼:
package com.javadss.javase.ch04;
這句程式碼的含義就是表示該原始檔中的所有類都將被放到包com.javadss.javase.ch04的下面。如果我們在該原始檔中繼續寫一個類PackageTest2如下:
package com.javadss.javase.ch04; public class PackageTest { } class PackageTest2 { }
這樣原始檔PackageTest.java中的2個類PackageTest和PackageTest2 都被放到包com.javadss.javase.ch04中了。因此它們的完整類名分別為:
com.javadss.javase.ch04.PackageTest
com.javadss.javase.ch04.PackageTest2
假如我們的原始檔開頭沒有宣告包語句,那麼這個原始檔會被放置到一個預設包中,預設包是一個沒有名字的包,像我們之前的例子,都沒有宣告包,因此它們都是放置在預設包中的。不過在實際運用中,非常不推薦把類放置在預設包中。
定義好了類的包後,我們看看這個原始檔被放在什麼地方了。筆者的Eclipse的工作空間目錄為D:\Java大失叔\workspace,本書的工程為BaseJava,工程中src為原始碼目錄,bin為class檔案目錄。則最終原始檔PackageTest.java的路徑為:
D:\Java大失叔\workspace\BaseJava\src\com.javadss.javase.ch04\PackageTest.java
目錄層次結構如下:
因為我們使用了Eclipse,它會自動幫我們編譯類,還記得我們在3.1演示HelloWorld的時候教大家如何建立工程嗎?Eclipse建立工程預設會使用專案檔案目錄作為根目錄,下面會建立2個資料夾:src和bin。src用來存放原始碼檔案,bin用來存放編譯後的位元組碼class檔案。我們會發現,最終src和bin變成如下結構:
實際上,如果我們不用IDE,我們手工在src目錄下編寫上述原始檔,然後用命令列工具編譯(我們也編譯到bin目錄下),命令如下(還記得編譯命令嗎?紅色表示編譯後的class檔案的目錄,藍色表示原始檔的路徑):
javac -d D:\Java大失叔\workspace\BaseJava\bin D:\Java大失叔\workspace\BaseJava\src\com\javadss\javase\ch04\PackageTest.java
編譯成功後,也會在bin目錄下形成上述結構。
4.5.3包作用域
上面我們說過包可以用來解決同名類的衝突問題,實際上,包還有一個作用,就是可以控制許可權。
前面我們接觸過訪問修飾符public、private,其實還有一個protected,它們可以用來修飾類、屬性及方法,當類、屬性或方法不用任何訪問修飾符修飾的時候,我們可以認為有一個default修飾符在修飾它們,這樣一來,可以認為有4種訪問修飾符,這4個修飾符可以控制對同一個類、同一個包、不同包的子類、不同包非子類的訪問許可權,下面我們用表格的形式列出它們的許可權範圍:
|
同一個類 |
同一個包 |
不同包子類 |
不同包非子類 |
public |
√ |
√ |
√ |
√ |
protected |
√ |
√ |
√ |
|
default |
√ |
√ |
|
|
private |
√ |
|
|
|
我們看到,當不用任何修飾符修飾的時候,類、方法和屬性對同一個包下的其他類是開放的。我們用一個例子演示一下,看如下2個類:
package com.javadss.javase.ch04; class PackageTest { private String sPrviate = "我是私有的"; String sDefault = "我是預設的"; void testYes() { System.out.println("同一個包可以訪問我"); } private void testNo() { System.out.println("只有我自己可以訪問我"); } }
package com.javadss.javase.ch04; class PackageTest2 { public static void main(String[] args) { PackageTest test = new PackageTest(); test.testYes(); System.out.println(test.sDefault); } }
我們看到,PackageTest和PackageTest2在同一個包中,因此PackageTest2可以訪問PackageTest的testYes方法和sDefault屬性,但是不能訪問private修飾的testNo方法和sPrivate屬性。
這裡其實有一個問題,就是PackageTest2可以修改PackageTest的sDefault屬性,其實對於有些情況來說,算是破壞了封裝性。比如我編寫了一個小工具類DssUtil提供給別人使用,包名是com.javadss.util。其中有很多預設修飾的屬性,我本意是不想對外界開放。但是實際上使用者可以把他的類也放到包com.javadss.util中,這樣他的類就可以隨意訪問DssUtil中的預設修飾屬性了。不過這一點也有辦法控制,後面我們有機會可以討論包密封機制來解決這個問題(Java虛擬機器從類載入上禁止載入使用者自定義的以java.開頭的類來解決這個問題)。
另外,還有一個問題需要注意,包並沒有包含關係,例如對於包com.javadss.javase和com.javadss.javase.ch04這是2個包,因此這2個包下的類,不能互相訪問預設作用域的類、屬性和方法。
4.5.4包的匯入
一個類可以訪問同包中的所有類和其他包中的public類,如果需要訪問其他類,需要匯入以後才能訪問。匯入類有幾種方式,我們一一介紹。
4.5.4.1直接寫完整類名
第一種方式,是直接寫完整類名。例如我們要使用Arrays類對一個陣列排序,可以這樣:
class PackageTest2 { public static void main(String[] args) { int[] a = new int[] { 4, 1323, 1, 33 }; java.util.Arrays.sort(a);//直接寫完整類名 } }
但是這種方式顯然是比較痛苦和令人討厭的,一般我們採用下面這種方式。
4.5.4.2import語句
第二種方式就是用import語句匯入。import語句可以匯入某個特定的類或匯入整個包。例如:
import java.util.Arrays;//匯入特定的類
或者
import java.util.*;//匯入整個包
使用import語句匯入後,就可以不用寫完整類名了。這裡需要注意的是,匯入整個包的方式,並不能把子包匯入進來。一般情況下,我們推薦匯入特定類的方式,這樣可以直接看到某個類在哪個包下。
聰明的同學可能要問了,我記得我們之前一直使用System類來列印,但是好像也沒有匯入包啊?算你厲害,這裡就牽涉到編譯器在編譯的時候,是如何定位類的:
- 如果是完整類名,則直接定位到該類
- 如果是簡單類名,則按下面順序:
-
- 從當前包下查詢是否存在該類
- 從import語句中查詢是否存在該類
- 從java.lang包中查詢是否存在該類
看到了嗎?實際上就相當於編譯器會幫我們匯入當前包和java.lang包下的類。因為System是java.lang包下的類,因此我們可以不必顯式的匯入。
有的時候還存在一個問題,當2個包下存在同名的類時,比如在JDK中存在java.util.Date和java.sql.Date,如果我們用瞭如下語句:
import java.util.*; import java.sql.*;
那麼我們無法使用簡單的類名Date。因為無法區分是用哪個包下的Date類。假如我們只使用其中一個,比如我們用java.util.Date,可以有一個取巧的辦法:
import java.util.*; import java.sql.*; import java.util.Date;
但是這種辦法解決不了我們同時需要使用2個Date類。如果同時需要使用的時候,只能用完整類名的方式了。
4.5.4.3靜態匯入
從Java5.0開始,增加一種新的匯入方式,可以匯入靜態方法和靜態屬性。前面我們經常使用System.out.println()來列印,其實out就是System類的一個靜態屬性。如果我們在原始檔進行如下匯入:
import static java.lang.System.out;
這樣我們就可以直接使用out.println()來列印輸出了:
1 package com.javadss.javase.ch04; 2 3 import static java.lang.System.exit; 4 import static java.lang.System.out; 5 6 class PackageTest2 { 7 public static void main(String[] args) { 8 out.println("靜態匯入"); 9 exit(0); 10 } 11 }
注意看第3行,exit()是System的一個靜態方法,這裡不管這個方法有啥用,只是演示可以匯入靜態方法。當然,我們可以靜態匯入整個類的靜態屬性和方法:
import static java.lang.System.*;
不過說實話,我不太喜歡使用靜態匯入,有的時候反而引起一些閱讀障礙。
4.5.5小結
通過本小結的討論,我們知道:
- 包可以解決類名衝突,一個類的完整類名是包名.類名
- 在一個類中訪問其他類,可以寫完整的類名,也可以用import語句匯入;從Java5.0開始還可以匯入靜態方法和靜態屬性。
- 包可以隔離訪問許可權,預設修飾的類、方法、屬性可以被同包的其他類訪問。
另外,包主要是讓編譯器知道如何定位到類,當編譯成位元組碼class檔案後,class檔案中都是採用完整類名的方式來引用其他類。最後用一張圖來總結一下: