@@ -508,4 +508,298 @@ public class StaticBlockDemo {
508
508
7.2 關於方法
509
509
------------
510
510
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)的不同。
0 commit comments