static 類別成員


來看看一個程式片段:

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

如果你建立了多個Ball物件,那每個Ball物件都會有自己的radiusPI成員:

每個Ball擁有自己的radius與PI資料成員


不過我們都知道,圓周率其實是個固定的常數,不用每個物件各自擁有,你可以在PI上宣告static,表示它屬於類別:

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

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

PI屬於Ball類別擁有


被宣告為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成員,這可以查看API文件得知:

System.err、System.in、System.out都是static


進一步按下out鏈結就會看到完整宣告(有興趣也可以看src.zip中的System.java):

out的完整宣告


所以out實際上是java.io.PrintStream型態,被宣告為static,屬於System類別擁有。先前遇過的例子還有Integer.parseInt()Long.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關鍵字。例如:

static方法中不能使用this


如果你在程式碼中撰寫了某個物件資料成員,雖然沒有撰寫this,但也隱含了這個物件某成員的意思,也就是:

static方法中不能用非static資料成員


在上圖中,雖然撰寫radius,但隱含了this.radius的意義,因此會編譯錯誤。static方法或區塊中,也不能呼叫非static方法或區塊。例如:

static方法中不能用非static方法成員


在上圖中,雖然撰寫doOther(),但實際隱含了this.doOther(),因此會編譯錯誤。static方法或區塊中,可以使用static資料成員或方法成員。例如:

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

如果你有些動作,想在位元碼載入後執行,則可以定義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!");
        }
    }
    ...
}

在JDK5之後,新增了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());
    }
}

原本編譯器看到in時,並不知道in是什麼,但想起你用import static告訴過它,想針對java.lang.System.in這個static成員偷懶,所以就試著用java.lang.System.in編譯看看,結果就成功了,out也是同樣的道理,在不影響可讀性的情況下,適時使用import static可以簡化程式碼,讓程式碼讀來更流暢。

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

import static java.lang.System.*;

import一樣,import static語法是為了偷懶,但別偷懶過頭,要注意名稱衝突問題,有些名稱衝突編譯器可透過以下順序來解析:

  • 區域變數覆蓋:選用方法中的同名變數、參數、方法名稱
  • 成員覆蓋:選用類別中定義的同名資料成員、方法名稱
  • 重載方法比對:使用import static的各個靜態成員,若有同名衝突,嘗試透用重載判斷

如果編譯器無法判斷,則會回報錯誤,例如若cc.openhome.Util定義有staticsort()方法,而java.util.Arrays也定義有staticsort()方法,以下情況編譯就會出錯:

到底是要哪個sort()?