this 與 static

May 29, 2022

除了被宣告為 static 的地方外,this 關鍵字可以出現在類別中任何地方,在物件建立後為「這個物件」的參考名稱。

使用 this

目前你看到的應用,就是在建構式參數與物件資料成員同名時,可用 this 加以區別。

public class CashCard {
    private String number;
    private int balance;
    private int bonus;

    public CashCard(String number, int balance, int bonus) {
        this.number = number;       // 參數 number 指定給這個物件的 number
        this.balance = balance;     // 參數 balance 指定給這個物件的 balance
        this.bonus = bonus;         // 參數 bonus 指定給這個物件的 bonus
    }
    ...
}

來看這個程式片段:

public class Some {
    private int a = 10;
    private String text = "n.a.";

    public Some(int a) {
        if(a > 0) {
            this.a = a;
        }
    }

    public Some(int a, String text) {
        if(a > 0) {
            this.a = a;
        }
        if(text != null) {
            this.text = text;
        }
    }
    ...
}

建構式的部份你發現了什麼?粗體字部份流程是重複的,重複在程式設計中是個不好的味道(Bad smell),你可以在建構式中呼叫另一個已定義的建構式。例如:

public class Some {
    private int a = 10;
    private String text = "n.a.";

    public Some(int a) {
        if(a > 0) {
            this.a = a;
        }
    }

    public Some(int a, String text) {
       this(a);
        if(text != null) {
            this.text = text;
        }
    }
    ...
}

在 Java 中,this() 代表了呼叫另一個建構式,至於呼叫哪個建構式,看呼叫 this() 時給的引數型態與個數而定,在上例中,this(a) 會呼叫 Some(int a) 版本的建構式,再執行 if(text != null) 之後的程式碼。this() 呼叫只能出現在建構式的第一行。

在建構物件之後、呼叫建構式之前,若有想執行的流程,可以使用 {} 定義,直接來看個範例比較清楚:

package cc.openhome;

class Other {
    { 
        System.out.println("物件初始區塊"); 
    }
    
    Other() {
        System.out.println("Other() 建構式");
    }
    
    Other(int o) {
        this();
        System.out.println("Other(int o) 建構式");
    }
}

public class ObjectInitialBlock {
    public static void main(String[] args) {
        new Other(1);
    }
}

在這個範例中,呼叫了 Other(int o) 版本的建構式,而其中使用 this() 呼叫了 Other() 版本的建構式,如果有撰寫物件初始區塊,物件建立之後會先執行物件初始區塊,接著才呼叫你指定的建構式,所以結果就是:

物件初始區塊
Other() 建構式
Other(int o) 建構式

如果區域變數宣告了 final,表示設值後就不能再變動,物件資料成員上也可以宣告 final,如果是以下程式片段:

class Something {
    final int x = 10;
    ...
}

同樣地,程式中其它地方不能再有對 x 設值的動作,否則會編譯錯誤。那以下的程式片段呢?

public class Something {
    final int x;
    ...
}

x 設為預設初始值 0,而其它地方不能對再 x 設值?不對!如果物件資料成員被宣告為 final,但沒有明確使用 = 指定值,那表示延遲物件成員值的指定,在建構式執行流程中,一定要有對該資料成員指定值的動作,否則編譯錯誤:

class Something {
    final int x;

    Something() { // 編譯錯誤
        
    }

    Something(int x) {
        this.x = x;
    }
}

在上例中,雖然 Something(int x) 版本的建構式有對 final 物件成員x設值,但如果使用者呼叫了 Something() 版本的建構式,那x就不會被設值,因而編譯錯誤。如果你改為以下就可以通過編譯:

class Something {
    final int x;

    Something() {
        this(10);
    }

    Something(int x) {
        this.x = x;
    }
}

static 類別成員

來看看一個程式片段:

class Ball {
    double radius;
    final double PI = 3.14159;
    ...
}

如果你建立了多個 Ball 物件,每個 Ball 物件都會有自己的 radiusPI 成員,不過我們都知道,圓周率其實是個固定的常數,不用每個物件各自擁有,你可以在 PI 上宣告 static,表示它屬於類別:

class Ball {
    double radius;
    static final double PI = 3.141596;
    ...
}

被宣告為 static 的成員,不會讓個別物件擁有,而是屬於類別,如上定義後,如果建立多個 Ball 物件,每個 Ball 物件只會各自擁有 radius

被宣告為 static 的成員,是將類別名稱作為名稱空間,也就是說,你可以如下取得圓周率:

System.out.println(Ball.PI);

也就是透過類別名稱與 . 運算子,就可以取得 static 成員。你也可以將宣告方法為 static 成員。例如:

class Ball {
    double radius;
    static final double PI = 3.141596;

    static double toRadians(double angdeg) { // 角度轉徑度
        return angdeg * (Ball.PI / 180);
    }
}

被宣告為 static 的方法,也是將類別名稱作為名稱空間,可以透過類別名稱與.運算子來呼叫 static 方法:

System.out.println(Ball.toRadians(100));

雖然語法上,也是可以透過參考名稱存取 static 成員,但非常不建議如此撰寫:

Ball ball = new Ball();
System.out.println(ball.PI);                // 極度不建議
System.out.println(ball.toRadians(100));    // 極度不建議

Java 程式設計領域,早就有許多良好命名慣例,沒有遵守慣例並不是錯,但會造成溝通與維護的麻煩。以類別命名實例來說,首字是大寫,以 static 使用慣例來說,是透過類別名稱與 . 運算子來存取。在大家都遵守命名慣例的情況下,看到首字大寫就知道它是類別,透過類別名稱與 . 運算子來存取,就會知道它是 static 成員。

你一直在用的 System.outSystem.in 呢?沒錯!out 就是 System 擁有的 static 成員,in 也是 System 擁有的 static 成員。

先前遇過的例子還有 Integer.parseIntLong.parseLong 等剖析方法,根據命名慣例,首字大寫就是類別,類別名稱加上.運算子直接呼叫的,就是 static 成員,你可以自行查詢 API 文件來確認這件事。

正如先前 Ball 類別所示範,static 成員屬於類別所擁有,將類別名稱當作是名稱空間是其最常使用之方式。例如在 Java SE API 中,只要想到與數學相關的功能,就會想到 java.lang.Math,因為有許多以 Math 類別為名稱空間的常數與公用方法。因為都是 static 成員,就可以這麼使用:

System.out.println(Math.PI);
System.out.println(Math.toRadians(100));

由於 static 成員是屬於類別,而非個別物件,在 static 成員中使用 this,會是一種語意上的錯誤,具體來說,就是在 static 方法或區塊中不能出現 this 關鍵字,否則編譯錯誤。

如果你在程式碼中撰寫了某個物件資料成員,雖然沒有撰寫 this,但也隱含了這個物件某成員的意思,也就是 static 方法或區塊中,不能使用值域成員,也不能呼叫非 static 方法或區塊。

static 方法或區塊中,可以使用 static 資料成員或方法成員。例如:

class Ball {
    static final double PI = 3.141596;

    static void doOther() {
        double o = 2 * PI;
    }
   
    static void doSome() {
        doOther();
    }
    ...
}    

必要時,static 區塊也可以使用 publicprivate 等來修飾權限。

如果你有些動作,想在位元碼載入後執行,則可以定義 static 區塊。例如:

class Ball {
    static {
        System.out.println("位元碼載入後就會被執行");
    }
}

在這個例子中,Ball.class 載入 JVM 後,預設就會執行 static 區塊。實際上,載入 JDBC 驅動程式的方式之一是運用 Class.forName 動態載入 Driver 實作類別的位元碼:

Class.forName("com.mysql.jdbc.Driver");

這個程式碼片段,會將 Driver.class 載入 JVM,而 com.mysql.jdbc.Driver 的原始碼中,就是在 static 區塊中進行驅動程式實例註冊的動作:

public class Driver extends NonRegisteringDriver
                        implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    ...
}

import static

import static 語法可以在使用靜態成員時少打幾個字。例如 Systemoutstatic 成員,為了要在文字模式下顯示訊息,本來都要這麼撰寫:

System.out.println("好麻煩");

有了 import static,就可以簡化:

package cc.openhome;

import java.util.Scanner;
import static java.lang.System.in;
import static java.lang.System.out;

public class ImportStatic {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(in);
        out.print("請輸入姓名:");
        out.printf("%s 你好!%n", scanner.nextLine());
    }
}

在不影響可讀性的情況下,適時使用 import static 可以簡化程式碼,讓程式碼讀來更流暢。

如果一個類別中有多個 static 成員想偷懶,也可以使用 *。例如將上例中 import static 的兩行改為如下一行,也可以編譯成功:

import static java.lang.System.*;

import 一樣,import static 語法是為了偷懶,但別偷懶過頭,以免發生名稱衝突問題。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter