深入理解 Java 方法

靜默虛空發表於2019-03-16

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

:notebook: 本文已歸檔到:「blog

:keyboard: 本文中的示例程式碼已歸檔到:「javacore

方法的使用

方法定義

方法定義語法格式:

[修飾符] 返回值型別 方法名([引數型別 引數名]){
    ...
    方法體
    ...
    return 返回值;
}
複製程式碼

示例:

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

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

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

方法的呼叫

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

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");
        }
    }
}
複製程式碼

方法引數

在 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 中的引數傳遞

方法修飾符

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

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

訪問控制修飾符

訪問許可權控制的等級,從最大許可權到最小許可權依次為:

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

static

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

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

  • 在外部呼叫靜態方法時,可以使用 類名.方法名 的方式,也可以使用 物件名.方法名 的方式。而例項方法只有後面這種方式。也就是說,呼叫靜態方法可以無需建立物件

  • 靜態方法在訪問本類的成員時,只允許訪問靜態成員(即靜態成員變數和靜態方法),而不允許訪問例項成員變數和例項方法;例項方法則無此限制。

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

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 方法,否則會編譯報錯。

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
複製程式碼

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()
複製程式碼

synchronized

synchronized 用於併發程式設計。synchronized 修飾的方法在一個時刻,只允許一個執行緒執行。

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

特殊方法

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

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]
複製程式碼

構造方法

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

每個類都有構造方法。如果沒有顯式地為類定義任何構造方法,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 修飾,這種情況下,類無法呼叫此構造方法去例項化物件,這常常用於設計模式中的單例模式。

變參方法

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]
複製程式碼

finalize() 方法

finalize 在物件被垃圾收集器析構(回收)之前呼叫,用來清除回收物件。

finalize 是在 java.lang.Object 裡定義的,也就是說每一個物件都有這麼個方法。這個方法在 GC 啟動,該物件被回收的時候被呼叫。

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

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

擴充套件閱讀:

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

覆寫和過載

覆寫(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
複製程式碼

小結


深入理解 Java 方法

參考資料

相關文章