2013年2月15日 星期五

[Java]2-物件導向大補丸 static/instance/final、this/super、繼承(Overload/Override)/多型


第二節static 、instance、final
static&instance:
◎加上static的方法為類別方法:static void skillA(){}   /   反之,則為物件方法 void skillB(){}   (變數也是一樣)
建構子屬於物件成員不可加上static關鍵字。
同一個類別中分為Static和Non-Static兩種型態,且又分為四種資料成員,分別為「class field」、「class method」、「instance filed」、「instance method」
Static method (→ 不能存取) instance method,反之Static method (← 可存取) instance method 則可存取,但仍以「類別名稱.變數/method name」來存取,在同一類別中,instance method和instance method間,可以互相呼叫。
把Static看成是類別所擁有的,而且是一開始就放置於記憶體中的,而Instance則沒有建立自己的物件是伴隨著物件產生而產生的。若要呼叫instance method或是instance filed,則需先建立物件,再使用下列程式碼:
  • objectVariableName.fieldName
  • objectVariableName.methodName
靜態方法不能存取this也不能重置為非靜態方法
以下範例:
public class CD
{
public static double PI = Math.PI;
public double area = 0;
public CD()
{
.........
}}
Static的使用方式:而這段程式碼之中,因為PI這個變數是宣告為static的,所以他是屬於CD類別,因此若您要在別的類別存取static method/field,只要使用「CD.PI」即可,其中「CD為類別名稱」、「PI則為變數名稱」,若方法的話則為className.methodName。

Non-Static的使用方式:因為area為非static變數,因此只能先建立物件,再存取他,所以您如果要在別的類別存取他,只能這樣使用。
CD cd1 = new CD();
cd1.area = 20;


Static特性一:函數或者變數被宣告為static時,永遠會是一個唯一值!!永遠只佔著那一組記憶體空間。管該類別被new幾個object,該永遠都會是一樣的!

我們看到下面code的程式例子:
01class test{
02     static int iValue = 0;
03     public test() {
04     }
05}
06public class main {
07     public static void main(String[] argv)  {
08          test test1 = new test();
09          test test2 = new test();
10          System.out.println(test1.iValue);
11          test2.iValue  = 10;
12          System.out.println(test2.iValue);
13          System.out.println(test1.iValue);
14     }
15}
一開始先印出test1的iValue出來,印出0。再印出test2的iValue,印出10 ,最後再印出這時候再印出test1的iValue出來!結果印出的是10。
原因是因為我們將iValue宣告成static了。而iValue成為一個共同的變數。這就是static 的一個特性!

Static特性二:static可以透過類別直接存取使用!
不需要new 一個object出來。也可以透過類別直接去存取該變數
01class test{
02     static int iValue = 0;
03     public test() {
04     }
05}
06public class main {
07     public static void main(String[] argv)  {
08          System.out.println(test.iValue);
09     }
10}

final(迷思)
程式內的成員:類、方法(包含main() )、變數、物件宣告時若使用關鍵字final 修飾
  1. 宣告在class時不被繼承.就再也不能成為別人的父類別.所有在final類別中的成員都是final.
  2. final、static及private 宣告在方法無法被繼承的子類別override.
  3. 宣告在變數時則無法修改(覆寫).


第三節-this & super  (逃離傳統的C把函數跟變數全部宣告成全域的)
this 可以參考到目前使用該物件的屬性、方法、以及建構子,不過 this 只能用在方法或建構子中。
this( ) 和 super( ) 必須存在於建構子的第1行,且不能同時存在。
this( ) 和 super( ) 也不能存在於static成員的程式中實作。
this通常指當前對象,super則指父類的。當想用當前對象的某個東西,比如當前對象的某個方法或成員,可利用this來實現,而this的另一個用途是調用當前對象的另一個建構函數。

This觀念導讀:在一般你的方法中的某個型別參數名與当前對象的某個成員有相同的名字,為了不至于混淆,便需要明確使用this關鍵字来指明你要使用某個成员,使用方法是“this.成員名”,而不带this的那個便是型別參數。另外,還可用“this.方法名”來引用當前對象的某個方法,這時this就不是必須的了,你可以直接用方法名來訪問那個方法,compiler會知道你要用的是哪一個。

"this"在解釋它存在的必要之前,我們必須先為一個人生難題找出解答:如何在不說「我」的情況下,用合理的文法表達「教授把我當了」?如果你說「教授把XXX當了」,這樣的句子很奇怪,像是說另一個和你同名同姓還被你爆料的可憐同學。如果你說「教授當了」,請問教授到底當了誰?如果你說「被教授當了」,聽起來合理,但這其實是把「我」省略的慣用句型,文法上不合理,因為缺乏主詞。
此時,若沒有第一人稱代名詞,我們很難用語言表達自己的存在,也很難做第一人稱敘述。
在物件導向的世界裡也是一樣的。由於一個類別可以產生許多實體,就必須用一個代名詞來讓個別實體表達「我」,以區分和其他實體的不同。一個類別的實體就是用「this」表達「我」。

例如以下程式碼:
class Student{
    private int score;
    Student(int s){
        score = s; //A
    }
    public void say(){
        if( this.score > 59)  // B
              System.out.println("教授讓我過了!");
        else
               System.out.println("教授把我當了!");
    }
    public static void main(String[] arg){
        Student 好學生 = new Student(80);
        Student 混學生 = new Student(40);
        好學生.say();
        混學生.say();
    }}
在main中,產生兩個Student類別的實體,兩個實體say的結果不一樣,因為this.score的值不一樣。這樣就可以看出,每個new出來的實體都有自己的this,代表各自的實體。
※B的this是可以省略的,因為是在同一個類別,Java編譯器會在編譯時自動加入this。如A的score也可以改為this.score。
其實看下圖就一目了然:

main方法的Stack Frame中,「好學生」與「混學生」參照分別參考到不同的Student實體,兩個Student實體的this參照也分別參考到自己從屬的實體。如此一來,每個實體就可以用this表達「我」。例如,我的score就是this.score。
那麼你可能會有個疑惑:難道「this」存在的意義只是為了表達像this.score這種可以省略的語法嗎?
*「this」真正的好處在於,實體可以將自己傳遞給其他實體。*

為了解釋這點,我們定義一個Professor類別,並在Student類別裡加入getScore方法,如下:
class Professor{
    public void grade(Student student){
        student.score = (int)(Math.random()*101);
    }}
class Student{
   ...
   Professor 叫獸 = new Professor();
    public int getScore(){
        叫獸.grade(this); // C
       return score;
    }

public static void main(String[] arg){
        Student 羔羊 = new Student(0);
        羔羊.getScore();
        羔羊.say();
    }}
我們先產生一個Student的實體「羔羊」,並呼叫getScore方法。呼叫時,羔羊的實體就會傳入叫獸.grade()方法,並設定score。C的部分,就是Student實體將自身傳入叫獸.grade()方法的部分。其實這行程式碼,翻成中文就是「叫獸給我打分數。」「this」就是「我」!


Super:則是父類別的引用方法。


第四節-繼承(extends來實現Overload多載Override覆蓋) & 多型(Polymorphism,或稱多態) 
繼承基本觀念:
◎歐萊里 深入淺出中各種動物共同性找出來弄成animal 的類別,由每個動物類別繼承animal 再針對每個子類別作細部的修飾. 即override,沒讀快去讀!
subclass會繼承super所有public的variable與method,繼承後子類別即可共用父類別的成員並額外定義新的變數。
Java使用extends來表達繼承觀念,即"subclass是extend過superclass出來的",可用IS - A 單向關係方法測試是否是繼承關係。河馬是動物,但是動物不一定是河馬。
◎"extend=←"如果 class Y ←(繼承) class  X,且class Y是class Z的superclass,則Z應該能通過IS-A X的測試。即class Z←(繼承)class X。
一個子類別通常加入它自己的instance variable以及instance method.
Math類不能用於繼承,定義public final class Math
可用get與set間接存取(使用方法如下:)
要了解is a和has a這二種relationships那些地方不同.


override&overload:
override(覆蓋 = 忤逆家長 (也就是ride在家長頭上太over的意思)。
1、子類中override時方法名、方法的返回類型參數類型需要相同(必須精確匹配)。接入控制符只能更加公有化。
2、override存在於父類和子類之間且子類方法不能縮小父類別方法的訪問權限,子類別方法不能抛出比父類別方法更多的異常(但子類別方法可以不抛出異常)。
3、父類別繼承下來的method被定義為final、static、private(Java視它們爲被隱藏)及instance variable不能被override,但子類欲覆蓋父類別privae的方法時,父類別方法視為隱藏,所以子類別方法也等於是一個新的方法,編譯是沒有錯誤的)
4在JAVA中只允許單一繼承(Single Inheritance),也就是說子類別同時只能繼承一個父類別。
  • default 在同一個package都可以存取
  • private只能在同一個class裡被存取,無法繼承到子類別→所以就沒辦法override.
  • protected可以在同一個package下被存取,但不同package下就必須被繼承子類別才能被存取
  • public無條件都行。
5當某個Method在subclass中被override過,呼叫這個method時會較用到override過的版本。(JVM會從最底層的method開始找)
6、方法override時,若override不對(參數類型一致,返回類型不同,編譯錯誤 ),提示“試圖用不相容的返回類型override”。(除非參數類型也不一樣,這樣java才會認爲不是override,而是overload)。

overload(重載)= 給予太多工作 (load多到太over)。
1、 同一類別裡要用同一方法名(或多個同名函數、多重constructor也是一種overload)就要重載(只會發生在同一個class中),重載參數類型個數、順序
少有一個不相同方法的修飾符、返回值、抛出的異常、方法名不同均不能作爲區分重載方法的依據。(繼承體系中也有重載現象).
2、方法被定義為final、static、private(Java視它們爲被隱藏)子類別可以overload


Oerloading over = new Overloading():
int i;
String s1,s2
over.temp( s1);
over.temp( i )
over.temp(s1,s2);
override 則是將extend 的 method 修改成自己想要的形式,但是呼叫方法要一致

Java允許定義數個名稱相同的方法,只要這些方法擁有不一樣的參數組合即可,這就叫方法覆載(method overloading)
// Fig. 6.16: MethodOverload.java
// Using overloaded methods
import java.awt.Container;
import javax.swing.*;

public class MethodOverload extends JApplet
{
JTextArea outputArea;

public void init()
{
outputArea = new JTextArea( 2, 20 );
Container c = getContentPane();
c.add( outputArea );

outputArea.setText(
"The square of integer 7 is " + square( 7 ) +
"\nThe square of double 7.5 is " + square( 7.5 ) );
}

public int square( int x )
{
return x * x;
}

public double square( double y )
{
return y * y;
}}
      注意override和overload的不同
2.override的合法宣告如下
 public class X{         
protected void showMe(int i,char j){
  }
}
class Y extends X{
  public void showMe(int m,char n){
  }
}
//Y裡面method的return type要跟X 相同。都是void
//Y裡面method的名稱要跟X 相同。都是showMe
//Y裡面method的argument list型別順序個數要跟X相同。先是int接著是char
//Y裡面method的access modifer只能比X更開放或相等。可以是protected或public



3. overload的合法宣告如下
public class X{         
  public X(){ } 
  public void X(){  //變成method,不是constructor
  } 
  public X(char a){ 
  private X(int b){ } 
  public String showMe(){
    return "cloud";
  } 
  protected void showMe(int c,char d){ }
  private void showMe(int e){ }
 }
//只要Method的argument list不同就是overload
//在overload時access modifer不受限制
//要注意constructor不用return type,加入return type會把constructor變成method

Overload & Override 另解(以下程式碼看完不會也通一半):
給予太多工作,顧名思義就是給同一個人太大的工作量,讓他什麼事情都要包辦。在物件導向程式設計的世界裡,就是給予一個方法(method)或一個運算子(operator)多重任務的意思!最常見的例子就是Java中的加號運算子(+),可用在數字相加也可用在字串串接,相當於一個人揹了兩項任務。如下列程式碼,給予+號不同型態的運算元,就會進行不同的操作。

Code 1.
int i = 1+2+3;   // i = 6
String str = "Hello"+" World"; // str = "Hello World"
Java目前並不開放給程式員Overload運算子,只開放方法的Overloading。要在Java中Overload方法,只要使用同樣的名字與不同的參數形式宣告多個方法就行了,如下:

Code 2.
class Overloading{
    String hello(String str){
        return "Hello!";
}

    int hello(int i){
        return 100;
}

    String hello(String str1, String str2){
        return str1+str2;
}

    public static void main(String... arg){
        Overloading over = new Overloading();
     System.out.println(over.hello("Go!"));
      System.out.println(over.hello("Hello"," World"));
     System.out.println(over.hello(300));
}}
Hello!
Hello World
100
hello方法有三種宣告,分別接受不同型態與數量的參數。從呼叫方法的程式碼來看,像是我們讓hello方法處理三種不同的參數形式。然而實際上,這三個方法雖然有同樣的名稱,卻可以視為不同的方法。
原則上只要方法名稱一樣但方法簽名(Method Signature,即方法名稱加上參數形式)不同,就是合法的Overloading,回傳值型態則無所謂,呼叫方法的時候不要弄錯即可。實際上Java編譯器在編譯之後確實是產生3個不同的方法宣告。你可能會好奇這樣為什麼需要Overloading來擾亂視聽,其實沒有Overloading才真的很不方便,想像一下要替幾個功能一致只是參數形式不同的方法取名字的時候,光是要想出清楚卻又不能重複的名字,就讓人感到很挫折了。


忤逆家長,顧名思義就是不理會家長的那一套,決心貫徹自己的做法!在物件導向設計的世界中,指的就是重新定義從父類別那繼承下來的方法。Overriding的概念比較難以文字解釋,因此話不多說,直接切入程式碼。
我們現在有一個父類別如下:
Code 3.
class Parent{
    protected Money work(){
        System.out.println("家長種田");
        return new Money(300);
    }}
代表家長種田,每次賺300塊。Money類別內容如下:

Code 4.
class Money{
    private double money;
    public Money(double amount){
        this.money = amount;
}
public double getNTAmount(){
        return money;
}}
現在這個家長生了個有志氣的孩子。這個孩子不想繼承家業,憑自己努力用功讀書,最終成為一個優秀的Java程式員。這個孩子雖然也要工作(work),卻不去繼承家長的工作方式,自己定義了work的實作,如下:
Code 5.
class Child extends Parent{
    public Money work(){
        System.out.println("兒子寫Java");
        return new Money(30);
    }}
像這樣改寫從父類別繼承下來的方法,就是忤逆家長,就是Overriding! 
Overriding的限制比較多,除了方法名稱必須要與「被忤逆」的方法一樣之外,方法的回傳值型態與參數形式也必須與父類別一模一樣(否則就變成Overloading)!另外,方法的存取權限只能和父類別的方法一樣或更開放。也就是說,若父類別方法是使用protected權限,子類別想要「忤逆」該方法的話,權限就只能是protected或public。還有一點得注意:Final的方法不予許「被忤逆」。
值得一提的是,Java 5 開始有一種例外情況予許子類別「忤逆」的方法的傳回值型態可以是「被忤逆」的方法的回傳值型態的子類別。簡單的說,Child類別的work方法的回傳值型態不一定要是Money類別,也可以是Money的子類別。這就叫共變回傳(Covarient Return)。
舉例來說,假如那個孩子成為Java程式員之後,因為勤奮工作,被主管推薦到矽谷的總公司上班,薪水也從台幣換成美金,我們就定義一個美金(USDollar)類別繼承Money類別:

Code 6.
class USDollar extends Money{
    public USDollar(double amount){
        super(amount);
}
    public double getNTAmount(){
        return super.getNTAmount()*32;
}}
並且將Child中work方法的回傳值型態改為USDollar:
Code 7.
class Child extends Parent{
    public USDollar work(){
        System.out.println("兒子寫Java");
     return new USDollar(30);
}}
由於USDollar是Money的子類別,因此在Java 5之後能夠作為「忤逆」方法的合法回傳值型態。
關於共變回傳,還是不太熟悉的人可以參考這篇文章 http://tw.knowledge.yahoo.com/question/question?qid=1508092008302 
也許有人會覺得要記那麼多原則很煩,但其實萬變不離其宗,大原則就是子類別必須能被當作父類別使用(如Code 8)。這就是著名的Liskov代換原則(LSP) 。假設子類別的「忤逆」方法的存取權限、參數形式或傳回值型態與父類別「被忤逆」的方法不相容,如何確保子類別能被當作父類型型態使用呢?這也就是為何「忤逆」方法的存取權限必須與「被忤逆」的方法一樣或更開放,回傳值型態也必須要能相容的原因。

Code 8.
class Main{
    public static void main(String... arg){
        Parent p = new Child(); //子類別實體當父類別用
     Money money = p.work();
     System.out.println("薪水:"+money.getNTAmount());
}}
兒子寫Java
薪水:960.0


在像Java或C++這種靜態語言中使用Overloading與Overriding會有一定的風險在,因為兩者很容易混淆,導致執行結果可能不是程式員所預期的。例如下列程式碼:

Code 9.
import java.util.*;
class Problem{
    public static void main(String... arg){
        List ‹String›  list = new LinkedList‹ String › ();
     list.add("Hello");
     list.add("World");
     list.remove(0);
     ((Collection)list).remove(0);
     System.out.println("length"+ list.size());
}}
猜猜最後list中會有多少個元素?
答案是:1個。

你一定會疑惑增加兩個元素後又刪除兩個元素,為什麼結果不是個空串列。這就要看看List介面的remove方法是Overloading還是Overriding了。Collection介面固然提供了一個remove方法:
Collection
boolean remove(Object  o)
然而List介面不只繼承了Collection的remove,還Overload了另一個remove方法:
List
boolean remove(Object  o)
E  remove(int index)
因此當list.remove(0)被呼叫時,實際上是呼叫到List介面Overload的另一個remove方法(因為參數型態最接近),而不是定義在Collection中的remove方法。然而,當list被轉型為Collection型態並呼叫remove方法時,由於Collection介面並沒有定義接收int型態參數的remove方法,因此參數0會被Auto-boxing成Integer型態進而與Object型態相容(所有型態都間接或直接繼承Object類別),所以Collection介面的remove方法還是會被呼叫。但由於兩個remove方法行為不同(一個是移除Collection中的o元素,一個是移除List中第index個元素),導致結果不是我們所預期的空串列。這種情況下,若沒有去查JavaDoc看看Collection與List的差別,會以為List介面的remove方法是Overriding而不是Overloading,進而完全找不到問題出在哪裡,因此在使用上必須注意這點。

如果擔心在Overriding時因為參數型態打錯而變成Overloading,可以在方法前加上@Override這樣的Annotation。@Override會通知編譯器在編譯時確認該方法是否真的有Overriding。以上面的Child類別為例:
List
class Child extends Parent{
//打錯參數形式,造成Overloading
    @Override
    public USDollar work(String str){
        System.out.println("兒子寫Java");
     return new USDollar(30);
}}
method does not override or implement a method from a supertype
@Override
^
1 error
萬一打錯字變成Overloading就無法編譯,可以節省很多因為打錯字而浪費掉的debug的時間!

多型-讓系統很容易的增加能力
多型(Polymorphism) 代表能夠在執行階段,物件能夠依照不同情況變換資料型態,換句話說,多型是指一個物件參考可以在不同環境下,扮演不同角色的特性,指向不同的物件實體,可透過實作多個繼承或介面來實現父類別,並使用Override或Overload來達成。
多型延伸了繼承的使用,在繼承中我們需要宣告多個不同型態的變數來完成,但在多型只要宣告父類別,剩下的子類只要new就能使用了。
多態是出現於類的繼承層次中,通過向上轉型方法重寫override的機制來實現的。當加入新的繼承類時,大部分程式碼都不會受影響而改寫(也即是說代碼具有擴充性)。所以當調用新加入的繼承類時,都會首先向上轉型爲基類。這就是多態的向上轉型。
多態最重要的一點在於run-time binding。多態是物件導向程式設計的目標。

關於多態中覆蓋注意如下:
屬性既可以繼承,也是可以“覆蓋”的。但是對屬性而言沒有動態綁定這一特性,所以覆蓋的屬性被認爲是子類的特別屬性。從某種意義上來講,屬性決定了類(性質)。另一方面,申明的類型就決定了物件的屬性。這是因爲,任何物件或變數等號右面是用來賦值給符合等號左面所申明類型的,所以左面的類型是先決條件,賦值必須要符合申明類型。對於向上轉型而言,因爲等號右面的物件is a申明類型,所以是成立的。一定要記住,屬性永遠都是跟著申明類型走。但是,對方法而言是在運行時動態綁定的,它取決於物件自身的實際類型(實際上對方法而言,也是先檢查向上轉型後的基類該方法,若無該方法的定義,則編譯錯,然後再動態綁定到繼承類的覆蓋方法)。
繼承概念用法範例(一):
public class Animal {
    public String moveMethod() {
        return "Unspecified";
    }
}
public class Bird extends Animal {
    public String moveMethod() {
        return "Fly";
    }
}
public class Dog extends Animal {
    public String moveMethod() {
        return "run";
    }
}
public class Fish extends Animal {
    public String moveMethod() {
        return "swim";
    }
}
若class宣告時沒有指定extends,則Java會自動extends java.lang.Object。
public class A {
}
和下面的寫法相同
public class A extends java.lang.Object {
}
繼承概念用法範例(二):
class Son extends Father{ }

繼承的UML結構圖
class Father{
    public int money=100;
    public void getMoney(){
        System.out.println("我有:"+money);
    }}

public class Son extends Father{

    public static void main(String[] args) {
        Son son = new Son();
        son.getMoney();
        System.out.println("son.money="+son.money);
    }}

執行結果 :
我有:100 
son.money=100

繼承關係下的建構子:
類別 Father 和 Son 在編譯的時候會產生預設建構子,所以必須看成 :
class Father{
    Father(){  }
}

class Son{
    Son(){ }}

但是Son 類別繼承Father 類別,所以在預設javac會在Son(){ }建構子中的第一行加入無帶入參數的 super(),也就是呼叫富類別Father 的預設建構子,所以Son 類別應該看成 :
Son(){
        super(); //呼叫父類別中的建構子
    }
}


多型程式碼範例:

public class Main {
    public static void main(String[] args) {
        Animal a;
        a=new Dog("流浪犬");
        System.out.println(a.getName()+"  "+a.getEat());
        a=new Monkey("台灣獼猴");
        System.out.println(a.getName()+"  "+a.getEat());
    }}

執行結果:


1 則留言:

  1. 您寫的讓我受益良多,形容方式極為淺顯易懂,十分感謝分享!

    回覆刪除