Skip to content

Commit 01a0973

Browse files
author
Justin Lin
committed
completed ch7
1 parent 7171f43 commit 01a0973

File tree

2 files changed

+295
-1
lines changed

2 files changed

+295
-1
lines changed

docs/CH07.md

Lines changed: 295 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,4 +508,298 @@ public class StaticBlockDemo {
508508
7.2 關於方法
509509
------------
510510

511-
待續 ...
511+
在對定義類別有了瞭解之後,接下來再深入討論類別中的方法成員,在 Java 中,您可以「重載」(Overload)同名方法,而在 J2SE 5.0 之後,您還可以提供方法不定長度引數(Variable-length Argument),當然,最基本的您要知道遞迴(Recursive)方法的使用,最後還要討論一下 finalize() 方法,並從中瞭解一些 Java「垃圾收集」(Garbage collection)的機制。
512+
513+
## 7.2.1 重載(Overload)方法
514+
515+
Java 支援方法「重載」(Overload),又有人譯作「超載」、「過載」,這種機制為類似功能的方法提供了統一的名稱,但可根據參數列的不同而自動呼叫對應的方法。
516+
517+
一個例子可以從 String 類別上提供的一些方法看到,像是 String 的 valueOf() 方法就提供了多個版本:
518+
519+
static String valueOf(boolean b)
520+
static String valueOf(char c)
521+
static String valueOf(char[] data)
522+
static String valueOf(char[] data, int offset, int count)
523+
static String valueOf(double d)
524+
static String valueOf(float f)
525+
static String valueOf(int i)
526+
static String valueOf(long l)
527+
static String valueOf(Object obj)
528+
529+
雖然您呼叫的方法名稱都是 valueOf(),但是根據所傳遞的引數資料型態不同,您會呼叫對應版本的方法來進行對應的動作,例如若是 String.valueOf(10),因為 10 是 int 型態,所以會執行的方法是 valueOf(int i) 的版本,而若是 String.valueOf(10.12),因為 10.12 是 double 型態,則會執行的方法是 valueOf(double d) 的版本。
530+
531+
方法重載的功能使得程式設計人員能較少苦惱於方法名稱的設計,以統一的名稱來呼叫相同功能的方法,方法重載不僅可根據傳遞引數的資料型態不同來呼叫對應的方法,參數列的參數個數也可以用來設計方法重載,例如您可以這麼重載 someMethod() 方法:
532+
533+
public class SomeClass {
534+
// 以下重載了someMethod()方法
535+
public void someMethod() {
536+
// ...
537+
}
538+
public void someMethod(int i) {
539+
// ...
540+
}
541+
public void someMethod(float f) {
542+
// ...
543+
}
544+
public void someMethod(int i, float f) {
545+
// ...
546+
}
547+
}
548+
549+
要注意的是返回值型態不可用作為方法重載的區別根據,例如以下的方法重載是不正確的,編譯器仍會將兩個 someMethod() 視為重複的定義:
550+
551+
public class SomeClass {
552+
public int someMethod(int i) {
553+
// ...
554+
return 0;
555+
}
556+
public double someMethod(int i) {
557+
// ...
558+
return 0.0;
559+
}
560+
}
561+
562+
在 J2SE 5.0 後當您使用方法重載時,要注意到 autoboxing、unboxing 的問題,來看看範例 7.9,您認為結果會是什麼?
563+
564+
#### **範例 7.9 OverloadTest.java**
565+
```java
566+
public class OverloadTest {
567+
public static void main(String[] args) {
568+
someMethod(1);
569+
}
570+
571+
public static void someMethod(int i) {
572+
System.out.println("int 版本被呼叫");
573+
}
574+
575+
public static void someMethod(Integer integer) {
576+
System.out.println("Integer 版本被呼叫");
577+
}
578+
}
579+
```
580+
581+
結果會顯示 "int 版本被呼叫",您不能期待裝箱(boxing)的動作會發生,如果您想要呼叫參數列為 Integer 版本的方法,您要明確指定,例如:
582+
583+
someMethod(new Integer(1));
584+
585+
編譯器在處理重載方法、裝箱問題及「不定長度引數」時,會依下面的順序來尋找符合的方法:
586+
587+
- 找尋在還沒有裝箱動作前可以符合引數個數與型態的方法
588+
- 嘗試裝箱動作後可以符合引數個數與型態的方法
589+
- 嘗試設有「不定長度引數」並可以符合的方法
590+
- 編譯器找不到合適的方法,回報編譯錯誤
591+
592+
## 7.2.2 不定長度引數
593+
594+
在呼叫某個方法時,要給方法的引數個數事先無法決定的話該如何處理?例如 System.out.printf() 方法中並沒有辦法事先決定要給的引數個數,像是:
595+
596+
System.out.printf("%d", 10);
597+
System.out.printf("%d %d", 10, 20);
598+
System.out.printf("%d %d %d", 10, 20, 30);
599+
600+
在 J2SE 5.0 之後開始支援「不定長度引數」(Variable-length Argument),這可以讓您輕鬆的解決這個問題,直接來看範例 7.10 的示範。
601+
602+
#### **範例 7.10 MathTool.java**
603+
```java
604+
public class MathTool {
605+
public static int sum(int... nums) { // 使用...宣告參數
606+
int sum = 0;
607+
for(int num : nums) {
608+
sum += num;
609+
}
610+
return sum;
611+
}
612+
}
613+
```
614+
615+
要使用不定長度引數,在宣告參數列時要於型態關鍵字後加上 "...",而在 sum() 方法的區塊中您可以看到,實際上 nums 是一個陣列,編譯器會將參數列的 (int... nums) 解釋為 (int[] nums),您可以如範例 7.11 的方式指定各種長度的引數給方法來使用。
616+
617+
#### **範例 7.11 TestVarargs.java**
618+
```java
619+
public class TestVarargs {
620+
public static void main(String[] args) {
621+
int sum = 0;
622+
623+
sum = MathTool.sum(1, 2);
624+
System.out.println("1 + 2 = " + sum);
625+
626+
sum = MathTool.sum(1, 2, 3);
627+
System.out.println("1 + 2 + 3 = " + sum);
628+
629+
sum = MathTool.sum(1, 2, 3, 4, 5);
630+
System.out.println("1 + 2 + 3+ 4+ 5 = " + sum);
631+
}
632+
}
633+
```
634+
635+
執行結果:
636+
637+
1 + 2 = 3
638+
1 + 2 + 3 = 6
639+
1 + 2 + 3+ 4+ 5 = 15
640+
641+
編譯器會將傳遞給方法的引數解釋為 int 陣列傳入至 sum() 中,所以實際上不定長度引數的功能也是J2SE 5.0所提供的「編譯蜜糖」(Compiler Sugar)。
642+
643+
在方法上使用不定長度引數時,記得必須宣告的參數必須設定在參數列的最後一個,例如下面的方式是合法的:
644+
645+
public void someMethod(int arg1, int arg2, int... varargs) {
646+
// ....
647+
}
648+
649+
但下面的方式是不合法的:
650+
651+
public void someMethod(int... varargs, int arg1, int arg2) {
652+
// ....
653+
}
654+
655+
您也沒辦法使用兩個以上的不定長度引數,例如下面的方式是不合法的:
656+
657+
public void someMethod(int... varargs1, int... varargs2) {
658+
// ....
659+
}
660+
661+
如果使用物件的不定長度引數,宣告的方法相同,例如:
662+
663+
public void someMethod(SomeClass... somes) {
664+
// ....
665+
}
666+
667+
## 7.2.3 遞迴方法
668+
669+
「遞迴」(Recursion)是在方法中呼叫自身同名方法,而呼叫者本身會先被置入記憶體「堆疊」(Stack)中,等到被呼叫者執行完畢之後,再從堆疊中取出之前被置入的方法繼續執行。堆疊是一種「先進後出」(First in, last out)的資料結構,就好比您將書本置入箱中,最先放入的書會最後才取出。
670+
Java 支援遞迴,遞迴的實際應用很多,舉個例子來說,求最大公因數就可以使用遞迴來求解,範例 7.12 是使用遞迴來求解最大公因數的一個實例。
671+
672+
#### **範例 7.12 UseRecursion.java**
673+
```java
674+
import java.util.Scanner;
675+
676+
public class UseRecursion {
677+
public static void main(String[] args) {
678+
Scanner scanner = new Scanner(System.in);
679+
680+
System.out.println("輸入兩數:");
681+
System.out.print("m = ");
682+
int m = scanner.nextInt();
683+
684+
System.out.print("n = ");
685+
int n = scanner.nextInt();
686+
687+
System.out.println("GCD: " + gcd(m, n));
688+
}
689+
690+
private static int gcd(int m, int n) {
691+
if(n == 0)
692+
return m;
693+
else
694+
return gcd(n, m % n);
695+
}
696+
}
697+
```
698+
699+
執行結果:
700+
701+
輸入兩數:
702+
m = 10
703+
n = 20
704+
GCD: 10
705+
706+
範例 7.12 是使用輾轉相除法來求最大公因數;遞迴具有重複執行的特性,而可以使用遞迴求解的程式,實際上也可以使用迴圈來求解,例如下面的程式片段就是最大公因數使用迴圈求解的方式。
707+
708+
private static int gcd(int m, int n) {
709+
int r;
710+
while(n != 0) {
711+
r = m % n;
712+
m = n;
713+
n = r;
714+
}
715+
return m;
716+
}
717+
718+
使用遞迴好還是使用迴圈求解好?這並沒有一定的答案。由於遞迴本身有重複執行與記憶體堆疊的特性,所以若在求解時需要使用到堆疊特性的資料結構時,使用遞迴在設計時的邏輯會比較容易理解,程式碼設計出來也會比較簡潔,然而遞迴會有方法呼叫的負擔,因而有時會比使用迴圈求解時來得沒有效率,但迴圈求解時若使用到堆疊時,通常在程式碼上會比較複雜。
719+
720+
> **良葛格的話匣子** 在我的網站上有很多題目可以作練習,也不乏有遞迴求解的例子:
721+
>
722+
> - http://openhome.cc/Gossip/AlgorithmGossip/
723+
724+
## 7.2.4 垃圾收集
725+
726+
在解釋「垃圾收集」(Garbage collection)之前,要先稍微提一下 C++ 中對物件資源的管理,以利待會瞭解 Java 的物件資源管理機制。
727+
728+
在 C++ 中,使用 "new" 配置的物件,必須使用 "delete" 來清除物件,以釋放物件所佔據的記憶體空間,如果沒有進行這個動作,若物件不斷的產生,記憶體就會不斷的被物件耗用,最後使得記憶體空間用盡,在 C++ 中有所謂的「解構方法」(Destructor),它會在物件被清除前執行,然而使用 "delete" 並不是那麼的簡單,如果不小心清除了尚在使用中的物件,則程式就會發生錯誤甚至整個崩潰(Crash),如何小心的使用 "new" 與 " delete",一直是 C++ 中一個重要的課題。
729+
730+
在 Java 中,使用 "new" 配置的物件,基本上也必須清除以回收物件所佔據的記憶體空間,但是您並不用特別關心這個問題,因為 Java 提供垃圾收集機制,在適當的時候,Java 執行環境會自動檢查物件,看看是否有未被參考的物件,如果有的話就清除物件、回收物件所佔據的記憶體空間。
731+
732+
在 Java 中垃圾收集的時機何時開始您並無法得知,可能會在記憶體資源不足的時候,或是在程式執行的空閒時候,您可以建議執行環境進行垃圾收集,但也僅止於建議,如果程式當時有優先權更高的執行緒(Thread)正在進行,則垃圾收集並不一定會馬上進行。
733+
734+
在 Java 中並沒有解構方法,在 Java 中有 finalize() 這個方法,它被宣告為 "protected",finalize() 會在物件被回收時執行,但您不可以將它當作解構方法來使用,因為不知道物件資源何時被回收,所以也就不知道 finalize() 真正被執行的時間,所以無法立即執行您所指定的資源回收動作,但您可以使用 finalize() 來進行一些相關資源的清除動作,如果這些動作與立即性的收尾動作沒有關係的話。
735+
736+
如果您確定不再使用某個物件,您可以在參考至該物件的名稱上指定 "null",表示這個名稱不再參考至任何物件,不被任何名稱參考的物件將會被回收資源,您可以使用 System.gc() 建議程式進行垃圾收集,如果建議被採納,則物件資源會被回收,回收前會執行 finalize() 方法。
737+
738+
![沒有被名稱參考到的物件資源將會被回收](../images/img07-06.png)
739+
740+
圖 7.5 沒有被名稱參考到的物件資源將會被回收
741+
742+
範例7.13簡單的示範了finalize()方法的使用。
743+
 範例7.13 GcTest.java
744+
public class GcTest {
745+
private String name;
746+
747+
public GcTest(String name) {
748+
this.name = name;
749+
System.out.println(name + "建立");
750+
}
751+
752+
// 物件回收前執行
753+
protected void finalize() {
754+
System.out.println(name + "被回收");
755+
}
756+
}.
757+
758+
使用範例 7.14來作個簡單的執行測試。
759+
760+
#### **範例 7.14 UseGC.java**
761+
```java
762+
public class UseGC {
763+
public static void main(String[] args) {
764+
System.out.println("請按Ctrl + C終止程式........");
765+
766+
GcTest obj1 = new GcTest("object1");
767+
GcTest obj2 = new GcTest("object2");
768+
GcTest obj3 = new GcTest("object3");
769+
770+
// 令名稱不參考至物件
771+
obj1 = null;
772+
obj2 = null;
773+
obj3 = null;
774+
775+
// 建議回收物件
776+
System.gc();
777+
778+
while(true); // 不斷執行程式
779+
}
780+
}
781+
```
782+
783+
在程式中您故意加上無窮迴圈,以讓垃圾收集在程式結束前有機會執行,藉以瞭解垃圾收集確實會運作,程式執行結果如下所示:
784+
785+
請按Ctrl + C終止程式........
786+
bject1建立
787+
bject2建立
788+
bject3建立
789+
bject3被回收
790+
bject2被回收
791+
bject1被回收
792+
793+
7.3 接下來的主題
794+
----------------
795+
796+
每一個章節的內容由淺至深,初學者該掌握的深度要到哪呢?在這個章節中,對於初學者我建議至少掌握以下幾點內容:
797+
798+
- 如何使用 class 定義類別
799+
- 如何定義資料成員
800+
- 如何定義方法成員與建構方法
801+
- 瞭解 this 的作用
802+
- 瞭解靜態(static)成員的作用
803+
- 瞭解如何重載方法
804+
805+
在物件導向程式設計中,只是單純的封裝物件特性只能解決一部份的問題,有時候您必須提取出物件的共同抽象特性並加以定義,然後再「繼承」(Inherit)抽象的定義對個別的物件加以實作,有時您必須繼承某個類別並重新改寫類別中的某些定義,這在下一個章節中都會加以說明,並且您也將瞭解「抽象類別」(Abstract class)與「介面」(Interface)的不同。

images/img07-06.png

25.4 KB
Loading

0 commit comments

Comments
 (0)