深入理解 Java 方法

方法(有的人喜歡叫函數)是一段可重用的代碼段。

📓 本文已歸檔到:「javacore

🔁 本文中的示例代碼已歸檔到:「javacore

1. 方法的使用

1.1. 方法定義

方法定義語法格式:

[修飾符] 返回值類型 方法名([參數類型 參數名]){
    ...
    方法體
    ...
    return 返回值;
}

示例:

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

方法包含一個方法頭和一個方法體。下面是一個方法的所有部分:

  • 修飾符 - 修飾符是可選的,它告訴編譯器如何調用該方法。定義了該方法的訪問類型。
  • 返回值類型 - 返回值類型表示方法執行結束後,返回結果的數據類型。如果沒有返回值,應設爲 void。
  • 方法名 - 是方法的實際名稱。方法名和參數表共同構成方法簽名。
  • 參數類型 - 參數像是一個佔位符。當方法被調用時,傳遞值給參數。參數列表是指方法的參數類型、順序和參數的個數。參數是可選的,方法可以不包含任何參數。
  • 方法體 - 方法體包含具體的語句,定義該方法的功能。
  • return - 必須返回聲明方法時返回值類型相同的數據類型。在 void 方法中,return 語句可有可無,如果要寫 return,則只能是 return; 這種形式。

1.2. 方法的調用

當程序調用一個方法時,程序的控制權交給了被調用的方法。當被調用方法的返回語句執行或者到達方法體閉括號時候交還控制權給程序。

Java 支持兩種調用方法的方式,根據方法是否有返回值來選擇。

  • 有返回值方法 - 有返回值方法通常被用來給一個變量賦值或代入到運算表達式中進行計算。
int larger = max(30, 40);

  • 無返回值方法 - 無返回值方法只能是一條語句。
System.out.println("Hello World");

遞歸調用

Java 支持方法的遞歸調用(即方法調用自身)。

🔔 注意:

- 遞歸方法必須有明確的結束條件。

- 儘量避免使用遞歸調用。因爲遞歸調用如果處理不當,可能導致棧溢出。

斐波那契數列(一個典型的遞歸算法)示例:

public class RecursionMethodDemo {
    public static int fib(int num) {
        if (num == 1 || num == 2) {
            return 1;
        } else {
            return fib(num - 2)   fib(num - 1);
        }
    }

    public static void main(String[] args) {
        for (int i = 1; i < 10; i  ) {
            System.out.print(fib(i)   "\t");
        }
    }
}

2. 方法參數

在 C/C 等編程語言中,方法的參數傳遞一般有兩種形式:

  • 值傳遞 - 值傳遞的參數被稱爲形參。值傳遞時,傳入的參數,在方法中的修改,不會在方法外部生效。
  • 引用傳遞 - 引用傳遞的參數被稱爲實參。引用傳遞時,傳入的參數,在方法中的修改,會在方法外部生效。

那麼,Java 中是怎樣的呢?

Java 中只有值傳遞。

示例一:

public class MethodParamDemo {
    public static void method(int value) {
        value =  value   1;
    }
    public static void main(String[] args) {
        int num = 0;
        method(num);
        System.out.println("num = ["   num   "]");
        method(num);
        System.out.println("num = ["   num   "]");
    }
}
// Output:
// num = [0]
// num = [0]

示例二:

public class MethodParamDemo2 {
    public static void method(StringBuilder sb) {
        sb = new StringBuilder("B");
    }

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("A");
        System.out.println("sb = ["   sb.toString()   "]");
        method(sb);
        System.out.println("sb = ["   sb.toString()   "]");
        sb = new StringBuilder("C");
        System.out.println("sb = ["   sb.toString()   "]");
    }
}
// Output:
// sb = [A]
// sb = [A]
// sb = [C]

說明:

以上兩個示例,無論向方法中傳入的是基礎數據類型,還是引用類型,在方法中修改的值,在外部都未生效。

Java 對於基本數據類型,會直接拷貝值傳遞到方法中;對於引用數據類型,拷貝當前對象的引用地址,然後把該地址傳遞過去,所以也是值傳遞。

擴展閱讀:

圖解 Java 中的參數傳遞

3. 方法修飾符

前面提到了,Java 方法的修飾符是可選的,它告訴編譯器如何調用該方法。定義了該方法的訪問類型。

Java 方法有好幾個修飾符,讓我們一一來認識一下:

3.1. 訪問控制修飾符

訪問權限控制的等級,從最大權限到最小權限依次爲:

public > protected > 包訪問權限(沒有任何關鍵字)> private

  • public - 表示任何類都可以訪問;
  • 包訪問權限 - 包訪問權限,沒有任何關鍵字。它表示當前包中的所有其他類都可以訪問,但是其它包的類無法訪問。
  • protected - 表示子類可以訪問,此外,同一個包內的其他類也可以訪問,即使這些類不是子類。
  • private - 表示其它任何類都無法訪問。

3.2. static

static 修飾的方法被稱爲靜態方法。

靜態方法相比於普通的實例方法,主要有以下區別:

  • 在外部調用靜態方法時,可以使用 類名.方法名 的方式,也可以使用 對象名.方法名 的方式。而實例方法只有後面這種方式。也就是說,調用靜態方法可以無需創建對象
  • 靜態方法在訪問本類的成員時,只允許訪問靜態成員(即靜態成員變量和靜態方法),而不允許訪問實例成員變量和實例方法;實例方法則無此限制。

靜態方法常被用於各種工具類、工廠方法類。

3.3. final

final 修飾的方法不能被子類覆寫(Override)。

final 方法示例:

public class FinalMethodDemo {
    static class Father {
        protected final void print() {
            System.out.println("call Father print()");
        };
    }

    static class Son extends Father {
        @Override
        protected void print() {
            System.out.println("call print()");
        }
    }

    public static void main(String[] args) {
        Father demo = new Son();
        demo.print();
    }
}
// 編譯時會報錯

說明:

上面示例中,父類 Father 中定義了一個 final 方法 print(),則其子類不能 Override 這個 final 方法,否則會編譯報錯。

3.4. default

JDK8 開始,支持在接口 Interface 中定義 default 方法。default 方法只能出現在接口 Interface

接口中被 default 修飾的方法被稱爲默認方法,實現此接口的類如果沒 Override 此方法,則直接繼承這個方法,不再強制必須實現此方法。

default 方法語法的出現,是爲了既有的成千上萬的 Java 類庫的類增加新的功能, 且不必對這些類重新進行設計。 舉例來說,JDK8 中 Collection 類中有一個非常方便的 stream() 方法,就是被修飾爲 default,Collection 的一大堆 List、Set 子類就直接繼承了這個方法 I,不必再爲每個子類都注意添加這個方法。

default 方法示例:

public class DefaultMethodDemo {
    interface MyInterface {
        default void print() {
            System.out.println("Hello World");
        }
    }


    static class MyClass implements MyInterface {}

    public static void main(String[] args) {
        MyInterface obj = new MyClass();
        obj.print();
    }
}
// Output:
// Hello World

3.5. abstract

abstract 修飾的方法被稱爲抽象方法,方法不能有實體。抽象方法只能出現抽象類中。

抽象方法示例:

public class AbstractMethodDemo {
    static abstract class AbstractClass {
        abstract void print();
    }

    static class ConcreteClass extends AbstractClass {
        @Override
        void print() {
            System.out.println("call print()");
        }
    }

    public static void main(String[] args) {
        AbstractClass demo = new ConcreteClass();
        demo.print();
    }

}
// Outpu:
// call print()

3.6. synchronized

synchronized 用於併發編程。synchronized 修飾的方法在一個時刻,只允許一個線程執行。

在 Java 的同步容器(Vector、Stack、HashTable)中,你會見到大量的 synchronized 方法。不過,請記住:在 Java 併發編程中,synchronized 方法並不是一個好的選擇,大多數情況下,我們會選擇更加輕量級的鎖 。

4. 特殊方法

Java 中,有一些較爲特殊的方法,分別使用於特殊的場景。

4.1. main 方法

Java 中的 main 方法是一種特殊的靜態方法,因爲所有的 Java 程序都是由 public static void main(String[] args) 方法開始執行。

有很多新手雖然一直用 main 方法,卻不知道 main 方法中的 args 有什麼用。實際上,這是用來接收接收命令行輸入參數的。

示例:

public class MainMethodDemo {
    public static void main(String[] args) {
        for (String arg : args) {
            System.out.println("arg = ["   arg   "]");
        }
    }
}

依次執行

javac MainMethodDemo.java
java MainMethodDemo A B C

控制檯會打印輸出參數:

arg = [A]
arg = [B]
arg = [C]

4.2. 構造方法

任何類都有構造方法,構造方法的作用就是在初始化類實例時,設置實例的狀態。

每個類都有構造方法。如果沒有顯式地爲類定義任何構造方法,Java 編譯器將會爲該類提供一個默認構造方法。

在創建一個對象的時候,至少要調用一個構造方法。構造方法的名稱必須與類同名,一個類可以有多個構造方法。

public class ConstructorMethodDemo {

    static class Person {
        private String name;

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

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    public static void main(String[] args) {
        Person person = new Person("jack");
        System.out.println("person name is "   person.getName());
    }
}

注意,構造方法除了使用 public,也可以使用 private 修飾,這種情況下,類無法調用此構造方法去實例化對象,這常常用於設計模式中的單例模式。

4.3. 變參方法

JDK5 開始,Java 支持傳遞同類型的可變參數給一個方法。在方法聲明中,在指定參數類型後加一個省略號 ...。一個方法中只能指定一個可變參數,它必須是方法的最後一個參數。任何普通的參數必須在它之前聲明。

變參方法示例:

public class VarargsDemo {
    public static void method(String... params) {
        System.out.println("params.length = "   params.length);
        for (String param : params) {
            System.out.println("params = ["   param   "]");
        }
    }

    public static void main(String[] args) {
        method("red");
        method("red", "yellow");
        method("red", "yellow", "blue");
    }
}
// Output:
// params.length = 1
// params = [red]
// params.length = 2
// params = [red]
// params = [yellow]
// params.length = 3
// params = [red]
// params = [yellow]
// params = [blue]

4.4. finalize() 方法

finalize 在對象被垃圾收集器析構(回收)之前調用,用來清除回收對象。

finalize 是在 java.lang.Object 裏定義的,也就是說每一個對象都有這麼個方法。這個方法在 GC 啓動,該對象被回收的時候被調用。

finalizer() 通常是不可預測的,也是很危險的,一般情況下是不必要的。使用終結方法會導致行爲不穩定、降低性能,以及可移植性問題。

請記住:應該儘量避免使用 finalizer()。千萬不要把它當成是 C/C 中的析構函數來用。原因是:Finalizer 線程會和我們的主線程進行競爭,不過由於它的優先級較低,獲取到的 CPU 時間較少,因此它永遠也趕不上主線程的步伐。所以最後可能會發生 OutOfMemoryError 異常。

擴展閱讀:

下面兩篇文章比較詳細的講述了 finalizer() 可能會造成的問題及原因。

- Java 的 Finalizer 引發的內存溢出

- 重載 Finalize 引發的內存泄露

5. 覆寫和重載

覆寫(Override)是指子類定義了與父類中同名的方法,但是在方法覆寫時必須考慮到訪問權限,子類覆寫的方法不能擁有比父類更加嚴格的訪問權限。

子類要覆寫的方法如果要訪問父類的方法,可以使用 super 關鍵字。

覆寫示例:

public class MethodOverrideDemo {
    static class Animal {
        public void move() {
            System.out.println("會動");
        }
    }
    static class Dog extends Animal {
        @Override
        public void move() {
            super.move();
            System.out.println("會跑");
        }
    }

    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.move();
    }
}
// Output:
// 會動
// 會跑

方法的重載(Overload)是指方法名稱相同,但參數的類型或參數的個數不同。通過傳遞參數的個數及類型的不同可以完成不同功能的方法調用。

🔔 注意:

重載一定是方法的參數不完全相同。如果方法的參數完全相同,僅僅是返回值不同,Java 是無法編譯通過的。

重載示例:

public class MethodOverloadDemo {
    public static void add(int x, int y) {
        System.out.println("x   y = "   (x   y));
    }

    public static void add(double x, double y) {
        System.out.println("x   y = "   (x   y));
    }

    public static void main(String[] args) {
        add(10, 20);
        add(1.0, 2.0);
    }
}
// Output:
// x   y = 30
// x   y = 3.0

6. 小結

img

7. 參考資料

發佈了182 篇原創文章 · 獲贊 32 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章