天天看點

Java通路修飾符的正确使用姿勢

1、簡介

通路修飾符是Java文法中很基礎的一部分,但是能正确的使用Java通路修飾符的程式員隻在少數。在Java元件開發中,如果能夠恰到好處的使用通路修飾符,就能很好的隐藏元件内部資料和不必公布的實作細節,進而把元件API和實作細節隔離;正确的使用通路修飾符開發的Java元件,在元件與元件的調用和依賴過程中,也能很好的解耦程式,以至于整個元件能夠持續開發、持續測試、持續更新。

小捌溫馨總結:

  1. 通過限制通路範圍達到資訊隐藏或封裝的效果,保證程式實作細節的安全
  2. 解耦元件,使得元件之間的耦合關系降低,進而能夠低成本、低風險(不影響其他元件)的疊代

2、通路修飾符

Java文法提供了四種級别的通路修飾符,作用于域、方法、類、接口,它們的可通路性如下所示:

通路修飾符 名稱 通路性
private 私有的 聲明該成員的類才可以通路。注意:頂層類不能被private和protected修飾,内部類可以
default/package-private 包級私有的 聲明該成員的類同一包下的任何類均可以通路
protected 受保護的 聲明該成員的類同一包下、子類可以通路
public 共有的 任何地方均可通路

注意:private和default并不是絕對安全,如果類實作了Serializable,這些被private和defaulte修飾的域同一可能被導出;其次反射也是可以跨過通路修飾符的限制。

3、原則

Java通路修飾符使用的原則非常簡單:在實作Java元件的過程中,保證元件功能一緻的同時,盡可能讓類、類成員不被外界通路。

這一條規則看似非常簡單,但是往往給讓程式員産生一種誤導,他把類所有的方法和屬性都不假思索的設定為private。這會導緻一個什麼問題呢?在元件對外公布的時候或者疊代更新的時候,需要不斷的颠覆以前的設計,把更多的API對外公出來,但是總的來說這也好過把類中所有成員都用public修飾,這種方式是完全不能接收的,兄弟們。

那問題來了,具體應該怎麼搞呢?

其實小捌覺得隻需要明白三個點,因為通路修飾符作用于類、方法、屬性;是以針對如下三者分析它們應該怎麼選擇通路修飾符。

對于類來說有如下規則:

  1. 接口沒得選,預設就是public
  2. 頂層普通類,我們可以選擇public和default,此時應該着重考慮這個頂層類是否隻是在目前包中提供的抽象,如果滿足這個條件就可以好不由于的設定為default,但是如果這個頂層類需要被包外其他類直接使用,那就隻能設定為public
  3. 非頂層普通類,這種類主要是内部類,内部類有匿名内部類、非匿名内部類;匿名内部類不考慮;非匿名内部類又有靜态内部類和非靜态内部類,這兩者在選擇通路修飾符的時候小捌認為沒有差別,盡可能的選擇私有,因為你都将他設計為内部類,說明這個類抽象就是給外層類提供抽象支援的;是以處于元件設計安全性考慮,盡可能設計為私有,如果在外部需要使用,可以通過外層類提供API通路。

對于方法來說有如下規則:

  1. 接口方法沒得選,預設public,根據裡氏替換原則,任何使用超類的地方均可以使用子類執行個體,子類的通路修飾符必須大于等于超類,是以子類也隻有public一種選擇
  2. 普通類方法,設計類之前要先設計類需要對外公布的API,也就是類需要對外提供那些功能/服務,這個一定要先于寫代碼之前設計好,之後我們再考慮将這些API設計為default、protected、public,關于具體細節必須使用private修飾

對于屬性來說有如下規則:

  1. 如果類是共有的,一定不能将執行個體域公開;因為一旦公開執行個體域,等于其他類中可以修改這個執行個體域,無法保證執行個體域的安全性
  2. 如果屬性能夠定義為常量,我們一定要使用static final進行修飾,這樣對外暴露的域具有較高安全性。注意不要在常量域中定義數組等可變對象。

關于常量域中定義數組對象帶來的危險性,小捌做個Demo示範

定義Person對象:

public class Person {

    private String name;

    public Person(String name) {

        this.name = name;

    }

    public String getName() {

        return name;

    @Override

    public String toString() {

        return "Person{" +

            "name='" + name + '\'' +

            '}';

}

定義數組域所屬類:

public class PersonDemo {

    public static final Person[] PERSONS = new Person[] {new Person("李子柒"), new Person("李子捌")};

測試代碼:

class Test {

    public static void main(String[] args) {

        Arrays.stream(PersonDemo.PERSONS).forEach(System.out::println);

        for (int i = 0; i < PersonDemo.PERSONS.length; i++) {

            PersonDemo.PERSONS[i] = new Person(PersonDemo.PERSONS[i].getName() + "被修改啦!");

        }

        System.out.println();

測試結果可以看出,數組内容被修改了,這往往不是我們定義一個常量時所希望看到的。

Java通路修飾符的正确使用姿勢

關于這種方式的處理也很簡單,可以将數組域私有化,并且提供一個API來通路數組的拷貝

    private static final Person[] PERSONS = new Person[] {new Person("李子柒"), new Person("李子捌")};

    public static final Person[] getPersons() {

        return PERSONS.clone();

此時外部無法直接通路PERSONS數組,通路的隻是數組的拷貝,修改的也隻是數組的拷貝,無法修改到數組域的内容。

此外也可以使用Collections工具類将其包裝為不可變集合,包裝成UnmodifiableCollection對象之後,set、add、remove等方法調用會抛出UnsupportedOperationException:

    public static final List<Person> getPersons() {

        return Collections.unmodifiableList(Arrays.asList(PERSONS));