day11_物件導向(封裝丶static丶this)

java_pedestrian發表於2020-12-17

物件導向特徵之一:封裝和隱藏

封裝從字面上來理解就是包裝的意思,專業點就是資訊隱藏,是指利用抽象資料型別將資料和基於資料的操作封裝在一起,使其構成一個不可分割的獨立實體,資料被保護在抽象資料型別的內部,儘可能地隱藏內部的細節,只保留一些對外介面使之與外部發生聯絡。系統的其他物件只能通過包裹在資料外面的已經授權的操作來與這個封裝的物件進行交流和互動。也就是說使用者是無需知道物件內部的細節,但可以通過該物件對外提供的介面來訪問該物件。

我們程式設計追求“高內聚,低耦合”。

  • 高內聚 :類的內部資料操作細節自己完成,不允許外部干涉;
  • 低耦合 :僅對外暴露少量的方法用於使用。

隱藏物件內部的複雜性,只對外公開簡單的介面。便於外界呼叫,從而提高系統的可擴充套件性、可維護性。通俗的說,把該隱藏的隱藏起來,該暴露的暴露出來。這就是封裝性的設計思想。

為什麼要封裝?

使用者對類內部定義的屬性(物件的成員變數)的直接操作會導致資料的錯誤、混亂或安全性問題。在程式碼級別上,封裝有什麼用?一個類體當中的資料,假設封裝之後,對於程式碼的呼叫人員來說,不需要關心程式碼的複雜實現,只需要通過一個簡單的入口就可以訪問了。另外,類體中安全級別較高的資料封裝起來,外部人員不能隨意訪問,來保證資料的安全性。

不封裝存在的問題

我們來看一段程式碼,在不進行封裝的前提下,存在什麼問題:

package demo01;

/*
Person表示人類:
每一個人都有年齡這樣的屬性。
年齡age,int型別。
 */
public class Person {
    //例項變數
    int age;
}

 在外部程式中訪問Person這個型別中的資料

package demo01;

// 在外部程式中訪問Person這個型別中的資料。
public class PersonTest {
    public static void main(String[] args) {
        // 建立Person物件
        Person p1 = new Person();
        // 訪問人的年齡
        // 訪問一個物件的屬性,通常包括兩種操作,一種是讀資料,一種是改資料。
        // 讀資料
        System.out.println(p1.age); //0

        // 修改資料(set表示修改/設定)
        p1.age = 50;

        //再次讀取
        System.out.println(p1.age);//50

        // 在PersonTest這個外部程式中目前是可以隨意對age屬性進行操作的。
        // 一個人的年齡值不應該為負數。
        // 程式中給年齡賦值了一個負數,按說是不符合業務要求的,但是程式目前還是讓它通過了。
        // 其實這就是一個程式的bug。
        p1.age = -100; //改(隨意在這裡對Person內部的資料進行訪問,導致了不安全。)
        System.out.println("您的年齡值是:" + p1.age); //您的年齡值是:-100
    }
}

結論:類的屬性對外暴露,可以在外部程式中隨意訪問,導致了不安全。

怎麼封裝

  • 封裝的第一步:就是將應該隱藏的資料隱藏起來,起碼在外部是無法隨意訪問這些資料的,怎麼隱藏呢?我們可以使用 java 語言中的 private 修飾符,private 修飾的資料表示私有的,私有的資料只能在本類當中訪問。
  • 封裝的第二步:對外提供公開的訪問入口,讓外部程式統一通過這個入口去訪問資料,我們可以在這個入口處設立關卡,進行安全控制,這樣物件內部的資料就安全了。

對於“一個”屬性來說,我們對外應該提供幾個訪問入口呢?通常情況下我們訪問物件的某個屬性,不外乎讀取(get)和修改(set),所以對外提供的訪問入口應該有兩個,這兩個方法通常被稱為 set 方法和 get 方法(請注意:set 和 get 方法訪問的都是某個具體物件的屬性,不同的物件呼叫 get 方法獲取的屬性值不同,所以 set 和 get 方法必須有物件的存在才能呼叫,這樣的方法定義的時候不能使用 static 關鍵字修飾,被稱為例項方法。例項方法必須使用“引用”的方式呼叫。還記得之前我們接觸的方法都是被 static 修飾的,這些方法直接採用“類名”的方式呼叫,而不需要建立物件,在這裡顯然是不行的)。

帶有static的方法/屬性(靜態方法/變數)怎麼呼叫?

  • 通過“類名.”的方式訪問。

沒有帶有static的方法/屬性(例項方法/屬性)怎麼呼叫?

  • 物件被稱為例項。例項相關的有:例項變數、例項方法。例項變數是物件變數。例項方法是物件方法。例項相關的都需要先new物件,通過“引用.”的方式去訪問。

空指標異常導致的最本質的原因

  • 空引用訪問“例項相關的資料”,會出現空指標異常。例項相關的包括:例項變數 + 例項方法。

下面我們對上面的Person類進行封裝

package demo01;

/*
Person表示人類:
每一個人都有年齡這樣的屬性。
年齡age,int型別。
 */
public class Person {
    //例項變數,每一個人年齡值不同,物件級別的屬性。
    int age;
    // get方法,通過這個方法獲取age的屬性值
    public int getAge() {
        return age;
    }

    //set方法獲取age的值
    public void setAge(int age) {
        // 能不能在這個位置上設定關卡!!!!
        if(age < 0 || age > 150){
            System.out.println("對不起,年齡值不合法,請重新賦值!");
            return; //直接終止程式的執行。
        }
        //程式能夠執行到這裡,說明年齡一定是合法的。
        this.age = age;
    }
}

我們通過方法去設定成員變數的值,就可以進行邏輯判斷了。這樣子可以保證程式的安全了。

總之,在 java 語言中封裝的步驟應該是這樣的:需要被保護的屬性使用 private 進行修飾,給這個私有的屬性對外提供公開的 set 和 get 方法,其中 set 方法用來修改屬性的值,get 方法用來讀取屬性的值。並且 set 和 get 方法在命名上也是有規範的,規範中要求 set 方法名是 set +屬性名(屬性名首字母大寫),get 方法名是 get + 屬性名(屬性名首字母大寫)。其中 set 方法有一個引數,用來給屬性賦值,set 方法沒有返回值,一般在 set 方法內部編寫安全控制程式,因為畢竟 set 方法是修改內部資料的,而 get 方法不需要引數,返回值型別是該屬性所屬型別(另外 set 方法和 get 方法都不帶 static 關鍵字,不帶 static 關鍵字的方法稱為例項方法,這些方法呼叫的時候需要先建立物件,然後通過“引用”去呼叫這些方法,例項方法不能直接採用“類名”的方式呼叫。)

四種訪問許可權修飾符

JavaBean

JavaBean是一種Java語言寫成的可重用元件。所謂javaBean,是指符合如下標準的Java類:

  • 類是公共的
  • 有一個無參的公共的構造器
  • 有屬性,且有對應的get、set方法

使用者可以使用JavaBean將功能、處理、值、資料庫訪問和其他任何可以用Java程式碼創造的物件進行打包,並且其他的開發者可以通過內部的JSP頁面、Servlet、其他JavaBean、applet程式或者應用來使用這些物件。使用者可以認為JavaBean提供了一種隨時隨地的複製和貼上的功能,而不用關心任何改變。

static關鍵字

static 是 java 語言中的關鍵字,表示“靜態的”,它可以用來修飾變數、方法、程式碼塊等,修飾的變數叫做靜態變數,修飾的方法叫做靜態方法,修飾的程式碼塊叫做靜態程式碼塊。在 java語言中凡是用 static 修飾的都是類相關的,不需要建立物件,直接通過“類名”即可訪問,即使使用“引用”去訪問,在執行的時候也和堆記憶體當中的物件無關。 

總結如下:

  • static翻譯為“靜態”
  • 所有static關鍵字修飾的都是類相關的,類級別的。
  • 所有static修飾的,都是採用“類名.”的方式訪問。
  • static修飾的變數:靜態變數
  • static修飾的方法:靜態方法

類屬性、類方法的設計思想

  • 類屬性作為該類各個物件之間共享的變數。在設計類時,分析哪些屬性不因物件的不同而改變,將這些屬性設定為類屬性。相應的方法設定為類方法。
  • 如果方法與呼叫者無關,則這樣的方法通常被宣告為類方法,由於不需要建立物件就可以呼叫類方法,從而簡化了方法的呼叫。

程式碼示例

package demo02;


class VarTest{
    // 以下例項的,都是物件相關的,訪問時採用“引用.”的方式訪問。需要先new物件。
    // 例項相關的,必須先有物件,才能訪問,可能會出現空指標異常。
    // 成員變數中的例項變數
    int i;
    
    // 例項方法
    public void m2(){
        // 區域性變數
        int x = 200;
    }
    
    // 以下靜態的,都是類相關的,訪問時採用“類名.”的方式訪問。不需要new物件。
    // 不需要物件的參與即可訪問。沒有空指標異常的發生。
    // 成員變數中的靜態變數
    static int k;

    // 靜態方法
    public static void m1(){
        // 區域性變數
        int m = 100;
    }

}

使用範圍:

  • 在Java類中,可用static修飾屬性、方法、程式碼塊、內部類

被修飾後的成員具備以下特點:

  • 隨著類的載入而載入
  • 優先於物件存在
  • 修飾的成員,被所有物件所共享
  • 訪問許可權允許時,可不建立物件,直接被類呼叫

靜態變數

java 中的變數包括:區域性變數和成員變數,在方法體中宣告的變數為區域性變數,有效範圍很小,只能在方法體中訪問,方法結束之後區域性變數記憶體就釋放了,在記憶體方面區域性變數儲存在棧當中。在類體中定義的變數為成員變數,而成員變數又包括例項變數和靜態變數,當成員變數宣告時使用了 static 關鍵字,那麼這種變數稱為靜態變數,沒有使用 static 關鍵字稱為例項變數,例項變數是物件級別的,每個物件的例項變數值可能不同,所以例項變數必須先建立物件,通過“引用”去訪問,而靜態變數訪問時不需要建立物件,直接通過“類名”訪問。例項變數儲存在堆記憶體當中,靜態變數儲存在方法區當中。例項變數在構造方法執行過程中初始化,靜態變數在類載入時初始化。

那麼變數在什麼情況下會宣告為靜態變數呢?

當一個類的所有物件的某個“屬性值”不會隨著物件的改變而變化的時候,建議將該屬性定義為靜態屬性(或者說把這個變數定義為靜態變數),靜態變數在類載入的時候初始化,儲存在方法區當中,不需要建立物件,直接通過“類名”來訪問。雖然靜態相關的成員也能使用“引用”去訪問,但這種方式並不被主張。本質上還是通過類名去訪問。

空引用訪問靜態變數和靜態方法並不會出現空指標異常,因為靜態變數不需要物件的存在。

變數在記憶體中儲存的位置

  • 靜態變數:儲存在方法區當中,在類載入的時候初始化
  • 例項變數:儲存在堆記憶體中,在建立物件的時候初始化
  • 區域性變數:儲存在棧記憶體中,在方法壓棧的時候載入到記憶體中

                

 靜態方法

在類中使用static修飾的靜態方法會隨著類的定義而被分配和裝載入記憶體中;而非靜態方法屬於物件的具體例項,只有在類的物件建立時在物件的記憶體中才有這個方法的程式碼段。靜態方法不需要建立物件,直接通過“類名”來訪問。雖然靜態相關的成員也能使用“引用”去訪問,但這種方式並不被主張。本質上還是通過類名去訪問。

注意事項:

  • 非靜態方法既可以訪問靜態資料成員 又可以訪問非靜態資料成員,而靜態方法只能訪問靜態資料成員;
  • 非靜態方法既可以訪問靜態方法又可以訪問非靜態方法,而靜態方法只能訪問靜態資料方法。
  • 因為不需要例項就可以訪問static方法,因此static方法內部不能有this。(也不能有super ? YES!)
  • static修飾的方法不能被重寫

原因: 因為靜態方法和靜態資料成員會隨著類的定義而被分配和裝載入記憶體中,而非靜態方法和非靜態資料成員只有在類的物件建立時在物件的記憶體中才有這個方法的程式碼段。

什麼時候定義為例項方法?什麼時候定義為靜態方法?

  • 此方法一般都是描述了一個行為,如果說該行為必須由物件去觸發。那麼該方法定義為例項方法。當這個方法體當中,直接訪問了例項變數,這個方法一定是例項方法。
  • 我們以後開發中,大部分情況下,如果是工具類的話,工具類當中的方法一般都是靜態的。(靜態方法有一個優點,是不需要new物件,直接採用類名呼叫,極其方便。工具類就是為了方便,所以工具類中的方法一般都是static的。)

類的成員之四:程式碼塊

程式碼塊(或初始化塊)的作用:

  • 對Java類或物件進行初始化

程式碼塊(或初始化塊)的分類:

  •  一個類中程式碼塊若有修飾符,則只能被static修飾,稱為靜態程式碼塊(static block),沒有使用static修飾的,為非靜態程式碼塊。

靜態程式碼塊

  • static程式碼塊通常用於初始化static的屬性

靜態程式碼塊的語法格式是這樣的: 

靜態程式碼塊:用static 修飾的程式碼塊

  • 可以有輸出語句。
  • 可以對類的屬性、類的宣告進行初始化操作。
  • 不可以對非靜態的屬性初始化。即:不可以呼叫非靜態的屬性和方法。
  • 若有多個靜態的程式碼塊,那麼按照從上到下的順序依次執行。
  • 靜態程式碼塊的執行要先於非靜態程式碼塊。
  • 靜態程式碼塊隨著類的載入而載入,且只執行一次。

我們要在類載入的時候解析某個檔案,並且要求該檔案只解析一次,那麼此時就可以把解析該檔案的程式碼寫到靜態程式碼塊當中了。

 程式碼示例

package demo03;

/*
    1、使用static關鍵字可以定義:靜態程式碼塊
    2、什麼是靜態程式碼塊,語法是什麼?
        static {
            java語句;
            java語句;
        }
    3、static靜態程式碼塊在什麼時候執行呢?
        類載入時執行。並且只執行一次。
        靜態程式碼塊有這樣的特徵/特點。

    4、注意:靜態程式碼塊在類載入時執行,並且在main方法執行之前執行。

    5、靜態程式碼塊一般是按照自上而下的順序執行。

    6、靜態程式碼塊有啥作用,有什麼用?
        第一:靜態程式碼塊不是那麼常用。(不是每一個類當中都要寫的東西。)
        第二:靜態程式碼塊這種語法機制實際上是SUN公司給我們java程式設計師的一個特殊的時刻/時機。
        這個時機叫做:類載入時機。

    具體的業務:
        專案經理說了:大家注意了,所有我們編寫的程式中,只要是類載入了,請記錄一下
        類載入的日誌資訊(在哪年哪月哪日幾時幾分幾秒,哪個類載入到JVM當中了)。
        思考:這些記錄日誌的程式碼寫到哪裡呢?
            寫到靜態程式碼塊當中。

*/
public class Demo01Static{

    // 靜態程式碼塊(特殊的時機:類載入時機。)
    static {
        System.out.println("A");
    }

    // 一個類當中可以編寫多個靜態程式碼塊
    static {
        System.out.println("B");
    }

    // 入口
    public static void main(String[] args){
        System.out.println("Hello World!");
    }

    // 編寫一個靜態程式碼塊
    static{
        System.out.println("C");
    }
}

輸出結果

例項程式碼塊

  • 沒有static修飾的程式碼塊,又叫例項程式碼塊

例項程式碼塊語法格式:

 特點:

  • 可以有輸出語句。
  • 可以對類的屬性、類的宣告進行初始化操作。
  • 除了呼叫非靜態的結構外,還可以呼叫靜態的變數或方法。
  • 若有多個非靜態的程式碼塊,那麼按照從上到下的順序依次執行。
  • 每次建立物件的時候,都會執行一次。且先於構造器執行。

程式碼示例

package demo03;

//判斷以下程式的執行順序
public class CodeOrder{

    // 靜態程式碼塊
    static{
        System.out.println("A");
    }

    // 入口
    public static void main(String[] args){
        System.out.println("Y");
        new CodeOrder();
        System.out.println("Z");
    }

    // 構造方法
    public CodeOrder(){
        System.out.println("B");
    }

    // 例項語句塊
    {
        System.out.println("C");
    }

    // 靜態程式碼塊
    static {
        System.out.println("X");
    }

}

執行結果

 總結執行順序

  • 類體當中定義兩個獨立的方法,這兩個方法是沒有先後順序要求的
  • 對於一個方法來說,方法體中的程式碼是有順序的,遵循自上而下的順序執行。
  • 靜態程式碼塊在類載入時執行,靜態變數在類載入時初始化,它們在同一時間發生,所以必然會有順序要求,如果在靜態程式碼塊中要訪問 i 變數,那麼 i 變數必須放到靜態程式碼塊之前。
  • 只要是構造方法執行,必然在構造方法執行之前,自動執行“例項語句塊”中的程式碼。只要是構造方法執行,必然在構造方法執行之前,自動執行“例項語句塊”中的程式碼。

總結:程式中成員變數賦值的執行順序

                               

this

this 是什麼?this 是 java 語言中的一個關鍵字,它儲存在記憶體的什麼地方呢,一起來看一段程式:

/*
    this:
        1、this是一個關鍵字,全部小寫。
        2、this是什麼,在記憶體方面是怎樣的?
            一個物件一個this。
            this是一個變數,是一個引用。this儲存當前物件的記憶體地址,指向自身。
            所以,嚴格意義上來說,this代表的就是“當前物件”
            this儲存在堆記憶體當中物件的內部。

        3、this只能使用在例項方法中。誰呼叫這個例項方法,this就是誰。
        所以this代表的是:當前物件。

        4、“this.”大部分情況下是可以省略的。

        5、為什麼this不能使用在靜態方法中??????
            this代表當前物件,靜態方法中不存在當前物件。
*/
public class ThisTest01{
    public static void main(String[] args){

        Customer c1 = new Customer("張三");
        c1.shopping();

        Customer c2 = new Customer("李四");
        c2.shopping();

        Customer.doSome();
    }
}

// 顧客類
class Customer{

    // 屬性
    // 例項變數(必須採用“引用.”的方式訪問)
    String name;   

    //構造方法
    public Customer(){
    
    }
    public Customer(String s){
        name = s;
    }

    // 顧客購物的方法
    // 例項方法
    public void shopping(){
        // 這裡的this是誰?this是當前物件。
        // c1呼叫shopping(),this是c1
        // c2呼叫shopping(),this是c2
        //System.out.println(this.name + "正在購物!");

        // this. 是可以省略的。
        // this. 省略的話,還是預設訪問“當前物件”的name。
        System.out.println(name + "正在購物!");
    }

    // 靜態方法
    public static void doSome(){
        // this代表的是當前物件,而靜態方法的呼叫不需要物件。矛盾了。
        // 錯誤: 無法從靜態上下文中引用非靜態 變數 this
        //System.out.println(this);
    }
}


class Student{

    // 例項變數,怎麼訪問?必須先new物件,通過“引用.”來訪問。
    String name = "zhangsan";

    // 靜態方法
    public static void m1(){
        //System.out.println(name);

        // this代表的是當前物件。
        //System.out.println(this.name);

        // 除非你這樣
        Student s = new Student();
        System.out.println(s.name);

    }

    //為什麼set和get方法是例項方法?
    public static void setName(String s){
        name = s;
    }
    public String getName(){
        return name;
    }

    // 又回到上午的問題了?什麼時候方法定義為例項方法,什麼時候定義為靜態方法?
    // 如果方法中直接訪問了例項變數,該方法必須是例項方法。
}

this記憶體圖如下所示

                

總結:

  • 它在方法內部使用,即這個方法所屬物件的引用; 
  • 它在構造器內部使用,表示該構造器正在初始化的物件。
  • 當前正在操作本方法的物件稱為當前物件。

什麼時候使用this關鍵字呢?

  • 當在方法內需要用到呼叫該方法的物件時,就用this。具體的:我們可以用this來區分屬性和區域性變數。比如:this.name = name;

this 可以呼叫類的屬性、方法

package demo03;

class Person { // 定義Person類
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void getInfo() {
        System.out.println("姓名:" + name);
        //this 呼叫方法
        this.speak();
    }

    public void speak() {
        //this訪問成員變數
        System.out.println("年齡:" + this.age);
    }
}

總結:

  • 在任意方法或構造器內,如果使用當前類的成員變數或成員方法可以在其前面新增this,增強程式的閱讀性。不過,通常我們都習慣省略this。
  • 當形參與成員變數同名時,如果在方法內或構造器內需要使用成員變數,必須新增this來表明該變數是類的成員變數
  • 使用this訪問屬性和方法時,如果在本類中未找到,會從父類中查詢

使用this呼叫本類的構造器

package demo03;
/*
    1、this除了可以使用在例項方法中,還可以用在構造方法中。
    2、新語法:通過當前的構造方法去呼叫另一個本類的構造方法,可以使用以下語法格式:
        this(實際引數列表);
            通過一個構造方法1去呼叫構造方法2,可以做到程式碼複用。
            但需要注意的是:“構造方法1”和“構造方法2” 都是在同一個類當中。

    3、this() 這個語法作用是什麼?
        程式碼複用。

    4、死記硬背:
        對於this()的呼叫只能出現在構造方法的第一行。
*/
class Person{ // 定義Person類
    private String name ;
    private int age ;
    public Person(){ // 無參構造器
        System.out.println("新物件例項化") ;
    }
    public Person(String name){
        this(); // 呼叫本類中的無參構造器
        this.name = name ;
    }
    public Person(String name,int age){
        this(name) ; // 呼叫有一個引數的構造器
        this.age = age;
    }
    public String getInfo(){
        return "姓名:" + name + ",年齡:" + age ;
    }
}

注意:

  • 可以在類的構造器中使用"this(形參列表)"的方式,呼叫本類中過載的其他的構造器!
  • 明確:構造器中不能通過"this(形參列表)"的方式呼叫自身構造器
  • 如果一個類中宣告瞭n個構造器,則最多有 n - 1個構造器中使用了"this(形參列表)"
  • "this(形參列表)"必須宣告在類的構造器的首行!
  • 在類的一個構造器中,最多隻能宣告一個"this(形參列表)"

相關文章