《Java從入門到失業》第四章:類和物件(4.5):包

Java大失叔發表於2020-09-23

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類來列印,但是好像也沒有匯入包啊?算你厲害,這裡就牽涉到編譯器在編譯的時候,是如何定位類的:

  1. 如果是完整類名,則直接定位到該類
  2. 如果是簡單類名,則按下面順序:
    • 從當前包下查詢是否存在該類
    • 從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檔案中都是採用完整類名的方式來引用其他類。最後用一張圖來總結一下:

相關文章