2013年9月10日 星期二

Polymorphism and Serializable


作者: 許裕永

1. 物件多型
物件多型(Polymorphism)是物件導向的核心觀念,在Java中,物件多型的運用無所不在。物件多型是一個觀念,不是程式語法,但是這個觀念會 影響很多程式敍述的撰寫。接下來大家會看到一堆「類別」及「型別」,在這裡先複習一下:Java的型別有兩種:基本資料型別(int、long……),參 考資料型別(類別、界面)。
例:
int a;
float b;
String s;
Integer i;
WindowListener wl;
a的型別是int,b的型別是float,s的型別是String這個類別,i的型別是Integer這個類別,wl的型別是WindowListener這個界面。
在自然界的物件是多型的。以小弟本人我為例:我是人,我是動物,我是兒子,我是父親,我是老師,我是路人甲………。在不同的環璄中,或是說在不同的需求下,我可以用不同的身份出現,也就是說我這個物件,擁有多種型別(身份)。
支援物件導向的語言,也模擬了這種功能。尤其是在Java中,每個物件至少可以擁有兩種型別。
介紹物件多型之前,就必須再複習一下物件及類別的關係。假設在自然界中有一個類別叫做“人”,我是“人”這個類別建構的實體物件,所以我這個物件擁有“人”這個型別。也可以說:我是“人”這個類別的物件。
在程式碼中,當我們寫了:“String s=new String(“Java”);“。代表我們用類別String建構了一個實體物件,那個物件的名字叫s,那個物件的型別是String。所以我們可以說:類別是物件的型別。
假設在自然界中,“人”這個類別,有一個父類別叫“動物”,那麼只要是擁有“人”這個型別的物件,也就擁有“動物”這個型別。所以我是“人”這個類別的物件,也是“動物”這個類別的物件;你可以說:「我是人」,也可以說「我是動物」。
在程式碼中,Object是String的父類別,所以s是String類別的物件,也是Object類別的物件。你可以說:「s是String物件」,也可以說:「s是Object物件」。
結論:任何類別建構的物件,除了擁有建構類別的型別外,也擁有建構類別的父類別的型別。
Java中的每一個類別,都會擁有一個方法叫equals(Object o)。這個方法的參數列宣告的型別是Object,那代表什麼?代表只要是 Object這個類別的任意子類別建構的物件,都可以做為本方法的參數。因為Object是Java中所有類別的父類別,所以任何類別建構的物件,都可以 視為Object型別的物件。也就是說:Java中的所有物件,都可以做為本方法的參數。因為,Java中的所有物件,至少都擁有兩種型別:建構該物件的 類別型別及Object型別。
物件的多型,不僅止於類別,也適用於界面。在自然界中,你也可以叫我戴眼鏡的,因為我有戴眼鏡。但是並不是只有人會戴眼鏡,狗也會啊,也就是眼鏡和類別無關。如果我們把眼鏡想像成一個界面,也就是只要實作了一個界面,那麼此物件也擁有那個界面的型別了。
addWindowListener(WindowListener wl)這是視窗程式中用來新增視窗監聽器的方法,這個方法需要的參數是一個 WindowListener型別的物件,但是WindowListener是界面,它不可能建構物件。所以我們會開發一個類別實作此界面後,再建構成物 件,則此物件便擁有實作界面的型別,就可以做為這個方法的參數了。
最後請注意:子類別物件一定擁有父類別型別,但是父類別物件不可能擁有子類別型別。
 
1-1 Is A and Has A
Is a和Has a不是運算符號,也不是語法。只是描述物件與類別的關係。
Is a指的是物件與類別的型別關係。
例:
class A{}
interface B{}
class AA extends A{}
class AAB extends A implements B{}
AA is an A。
AA is an Object。
AAB is an A。
AAB is a B。
AAB is an Object。
Has a指的是類別和類別的隸屬關係。
例:
class A{
B b;
}
A Has a B。
 
1-2 向上轉型
將子類別建構的物件,指派給父類別宣告的參告變數,則該物件將會自動轉換為父類別的型別。此為向上轉型。
例:
Object o=new String(“java”);

String s=new String(“Java”);
Object o=s;
上述寫法,String類別的物件,都會自動轉型為Object的型別,而被指派給參考變數o。
子類別物件也可以指派給以父類別宣告的參數,來呼叫方法。
例:
String s=new String(“Java”);
Integer i=new Integer(8);
System.out.println(s.equals(i));
equals()宣告的參數型別是Object,但因為i是Integer物件,也是Object物件,所以i可以自動轉換為Object型別,來指派給equals()當參數。此例列印結果為false。
 
1-3 向下轉型
將向上轉型為父類別型別的物件,再轉回原來的型別,稱之為向下轉型(Casting)。再強調一次,是子類別物件,被向上轉型為父類別物件,才可以向下轉型。如果原先就是父類別物件,是不可以向下轉型的。
向下轉型除了可以轉回物件的建構類別型別,也可以轉成建構類別的父類別型別,但不可以轉成建構類別的子類別型別。
例一:
Object o=new String(“Java”);
String s=(String)o;
把參考變數代表的物件,轉型為String物件後,指派給參考變數s。
例二:
Object o=new Object();
String s=(String)o;
此例編譯時是正確的。因為語法正確,而物件的型別是在執行時期才確認。所以如果執行的話會出現錯誤訊息:
java.lang.ClassCastException
例三:
Strnig s;
if(o instanceof String){
s=(String)o;
}
為了避免執行時期發生錯誤,在向下轉型之前,我們會用instanceof這個運算符號來確認物件與類別的關係。此例執行時期便不會出現錯誤訊息,因為if不成立,指派並不會執行。
例四:
class A{
 int x;
 public boolean equals(Object o){
  if(o instanceof A && ((A)o).x=this.x){
   return true;
  }else{
   return false;
  }
 }
}
本例中的overriding equals中,if判斷句的意思是:先檢查參數物件的型別,是否可以轉型為本類別物件。若可以再以轉型後的物件來比較物件值的內容。
第一個判斷的,便是參數物件與本類別的關係,因為Object型別物件,不見得是本類別物件。若不是本類別物件,自然傳回false。而且用的是短程的 and(&&),可以避免若參數型別與本類別無關時,and後面的敍述句“((A)o)”,向下轉型時所出現的執行時期錯誤。
最後請注意:1、轉型的是物件,不是參考變數。參考變數的型別就是宣告的型別,不可以改變。2、參考變數只是代表物件,不是物件。但是我們可以隨時指派任何型別符合的物件給它代表。
 
1-4 成員呼叫的要點
物件只能執行合乎當時型別的方法。而呼叫方法的是參考變數,所以:參考變數只能呼叫其宣告類別中的方法。
只要呼叫的方法,是符合參考變數型別的方法,該方法便可以執行,但執行的是:「實際建構物件的類別中,定義的內容」。
也就是說:
  • 财 參考變數能夠呼叫的方法,限定於宣告這個參考變數的類別中,有定義的方法。
  • 财 參考變數呼叫方法後,是由物件來執行。而物件執行的,是實際建構物件的類別中,定義的內容。
總之,認證時遇到這種題目,先判斷方法的呼叫是否合法,再判斷該方法的實際執行內容。
以上講的是關於方法成員的存取,至於資料成員就沒有那麼多區分,只要記得是以執行當時型別的類別中指派的值為主,就可以了。但是因為Java並不希望資料成員可以直接用參考變數存取(應設為private),所以就不深入討論了。
 
2. 序列化
本節要討論的,是物件的輸入及輸出。物件要輸入或輸出都需要依照特定的格式,而讓物件可以依照特定格式輸入或輸出的功能,就是序列化(Serialization)。
物件想要擁有序列化的功能,建構此物件的類別就必須實作java‧io中的界面:Serializable。這個界面中並沒有任何方法,也就是說:任何類別只要用implements宣告此界面,不必overriding任何方法,此類別建構的物件便擁有序列化的功能。
例:
class A implements java.io.Serializable{}
什麼樣的物件須要序列化的功能?當你希望物件可以在程式之間流通時。日後你的電腦中可能有多支Java的程式同時執行,或是一支程式中有多個執行緒同時執行。這個時候,你希望某一個物件能夠在各個程式或執行緒中流通時,此物件便須要序列化的功能。
比如說:在一個多伺服器的網路應用程式中,我們會把每一個客戶端的購物資訊,建立成一個一個的購物車物件。因為是多伺服器,所以每個客戶端的每一次連線 (按下超連結),都不一定是同一台伺服器回應。此時,各個購物車物件就必須在各伺服器之間流通,才能隨時記錄每個客戶端的購物資訊。也就是這些購物車物件 必須擁有序列化的功能,才能在各伺服器之間流通。所以開發購物車類別時,就必須implements serialiable。
因為大家還沒有學會複雜程式的寫法,自然沒有辦法做深入的講解,大家只要知道意思就可以了,本節後續的範例將示範輸出至檔案及從檔案輸入。
 
2-1 物件輸出
執行物件輸出這個功能的物件,是ObjectOutputStream類別的物件。
例一:
import java.io.*;
public class SerTestOut{
 public static void main(String[] args)throws IOException{
  String s="Java SCJP";
  FileOutputStream fos=new FileOutputStream("SerTest.txt");
  ObjectOutputStream ops=new ObjectOutputStream(fos);
  ops.writeObject(s);
 }
}
執行結果:產生一個SerTest‧txt檔,裏面記錄著s物件的資訊。
示範主題:1、建構ObjectOutputStream物件時,需要OutputStream物件為參數。因為我們要輸出至檔案,所我們建構 FileOutputStream物件,並在建構FileOutputStream物件時指定輸出的檔案名稱。2、FileOutputStream建構 的物件,也擁有其父類別OutputStream之型別,所以可以做為建構ObjectOutputStream物件時的參數(物件多型)。3、要被 writeObject()輸出的參數物件須要有序列化的功能。4、有很多Java提供的類別,均實作序列化界面(如:String)。
 
例二:
import java.io.*;
class BuyCar implements Serializable{
 String number;
 Integer price;
}
public class SerTestOut{
 public static void main(String[] args)throws IOException{
  BuyCar bc=new BuyCar();
  bc.number="A0001";
  bc.price=80;
  ObjectOutputStream ops=new ObjectOutputStream(new FileOutputStream("SerTest2.ser"));
  ops.writeObject(bc);
 }
}
執行結果:產生SerTest2‧ser檔案,用記事本開啟後,可以看到儲存的BuyCar物件資訊。
示範主題:1、開發實作序列化界面的類別。2、記錄物件資訊的檔案名稱的副檔名可以自訂。
 
2-2 物件輸入
執行物件輸入這個功能的物件,是ObjectInputStream類別的物件。
例一:
import java.io.*;
class BuyCar implements Serializable{
 String number;
 Integer price;
}
public class SerTestIn{

 public static void main(String[] args)throws IOException, ClassNotFoundException{
  ObjectInputStream ois=new ObjectInputStream(new FileInputStream(“SerTest2.ser”));
  BuyCar bc=(BuyCar)ois.readObject();
  System.out.println(bc.number);
  System.out.println(bc.price);
 }
}
列印結果:A0001 80。
示範主題:1、用FileInputStream物件當參數,建構ObjectInputStream物件。2、readObject()的運算結果型別 是Object。也就是這個方法取得的是Object型別的物件,必須向下轉型後才可以指派給BuyCar宣告的參考變數(物件多型)。
 
2-3 內含非序列化物件
開發類別實作serializable後,要注意資料成員的宣告。若此類別擁有無序列化功能的資料成員,將會造成執行時期錯誤。
例一:
import java.io.*;
class BuyCar implements Serializable{
 Object o;
 String number;
 Integer price;
}
public class SerTestOut{
 public static void main(String[] args)throws IOException{
  BuyCar bc=new BuyCar();
  bc.o=new Object();
  bc.number="A0001";
  bc.price=80;
  ObjectOutputStream ops=new ObjectOutputStream(new FileOutputStream("SerTest2.ser"));
  ops.writeObject(bc);
 }
}
執行結果:編譯正確,執行時出現錯誤訊息:
java.io.NotSerializableException: java.lang.Object
示範主題:1、應注意資料成員是否具有序列化功能。2、不是每個Java提供的類別都有實作序列化界面。
 
例二:
import java.io.*;
class Guest{
 String name;
}
class BuyCar implements Serializable{
 Guest g;
 String number;
 Integer price;
}
public class SerTestOut{
 public static void main(String[] args)throws IOException{
  BuyCar bc=new BuyCar();
  bc.g=new Guest();
  bc.g.name="Yung";
  bc.number="A0001";
  bc.price=80;
  ObjectOutputStream ops=new ObjectOutputStream(new FileOutputStream("SerTest2.ser"));
  ops.writeObject(bc);
 }
}
執行結果:錯誤訊息:
java.io.NotSerializableException: Guest
示範主題:BuyCar中擁有自訂的,沒有實作列序化界面的類別(Gueat)所宣告的資料成員。
例二若要可以正常執行只有兩種方式:
1、類別Guest實作Serializable界面。
2、資料成員前宣告transient。
第一種方式只要把類別實作序列化界面即可,不必做範例。第二種方式其實是比較常用的,因為有些類別並不適合實作序列化界面時,均可以使用(例一亦適用)。
transient譯為暫時性的。意謂著,用此修飾詞宣告的資料成員不必持續記錄,於是物件輸出時便不會輸出此資料成員的值。
 
例三:
將例二,BuyCar中的“Guest g;”,改為
“transient Guest g;”
執行結果:編譯及執行均正確。
 
例四:
import java.io.*;
class Guest{
 String name;
}
class BuyCar implements Serializable{
 transient Guest g;
 String number;
 Integer price;
}
public class SerTestIn{
 public static void main(String[] args)throws IOException , ClassNotFoundException{
  ObjectInputStream ois=new ObjectInputStream(new FileInputStream("SerTest2.ser"));
  BuyCar bc=(BuyCar)ois.readObject();
  System.out.println(bc.g);
  System.out.println(bc.number);
  System.out.println(bc.price);
 }
}
列印結果:null A0001 80。
示範主題:宣告為transient的資料成員,輸入時便會指派預設值給該成員,任何物件的預設值都是null。所以bc‧g的值是null。
 
2-4 建構方法之執行
在序列化輸入時,只要有實作序列化界面的類別,它們的建構方法都不會執行。Jav會依照類別的宣告,配置資料成員的記憶體空間後,把序列化輸入的值指派給各資料成員,對於transient的資料成員則指派予預設值,所以沒有執行建構方法的必要。
但是,如果實作序列化界面的類別的父類別,沒有實作序列化界面,則父類別的建構方法將會執行。
例一:
檔案一:
import java.io.*;
class Guest {
 String name;
 Guest(){
  name="Guest";
 }
}
class BuyCar extends Guest implements Serializable{
 String number;
 Integer price;
}
public class SerTestOut{
 public static void main(String[] args)throws IOException{
  BuyCar bc=new BuyCar();
  bc.name="Yung";
  bc.number="A0001";
  bc.price=80;
  ObjectOutputStream ops=new ObjectOutputStream(new FileOutputStream("SerTest2.ser"));
  ops.writeObject(bc);
 }
}
檔案重點:1、BuyCar繼承Guest。2、Guest中撰寫建構方法,指派name的值。3、BuyCar中取消Guest資料成員。4、編譯後執行。
檔案二:
import java.io.*;
class Guest {
 String name;
 Guest(){
  name="Guest";
 }
}
class BuyCar extends Guest implements Serializable{
 String number;
 Integer price;
}
public class SerTestIn{
 public static void main(String[] args)throws IOException,ClassNotFoundException{
  ObjectInputStream ois=new ObjectInputStream(new FileInputStream("SerTest2.ser"));
  BuyCar bc=(BuyCar)ois.readObject();
  System.out.println(bc.name);
  System.out.println(bc.number);
  System.out.println(bc.price);
 }
}
列印結果:Guest A0001 80。
示範主題:1、資料成員name繼承自父類別,而且沒有宣告為transient,所以輸入時,並不會給予預設值null。2、因為父類別沒有實作序列化界面,所以它的建構方法會執行,而它的建構方法執行時,所指派的值會複蓋序列化輸入的值。
PS‧ 即使父類別中沒有撰寫建構方法,但是它還是有預設的建構方法,而該預設的建構方式執行時,會指派給所有資料成員預設值,所以name會變成null。
例二:例一兩個檔案的Guest後方都加上implements Serializable後,分別編譯及執行。
列印結果:Yung A0001 80。
 
2-5 static
序列化輸入及輸出,是用來流通物件的,也就是說只會輸入及輸出屬於物件的成員,也就是實體資料成員,也就當然不包括類別資料成員。
所以:宣告為static的資料成員,並不在序列化輸入及輸出的範圍,因為它們不屬於物件。
 
3. 認證重點
3-1 物件多型
  • 财 子類別建構的物件,除了擁有建構類別的型別外,也擁有建構類別的父類別的型別。
  • 财 Java中的所有物件,至少都擁有兩種型別:建構該物件的類別型別及Object型別。
  • 财 開發一個類別實作界面後,再建構成物件,則此物件便擁有實作界面的型別。
  • 财 子類別物件一定擁有父類別型別,但是父類別物件不可能擁有子類別型別。
  • 财 Is a指的是類別與類別的繼承關係。
  • 财 Has a指的是類別和類別的隸屬關係。
  • 财 將子類別建構的物件,指派給父類別宣告的參告變數,則該物件將會自動轉換為父類別的型別。
  • 财 子類別物件也可以指派給以父類別宣告的參數,來呼叫方法。
  • 财 將向上轉型為父類別型別的物件,再轉回原來的型別,稱之為向下轉型。
  • 财 向下轉型除了可以轉回物件的建構類別型別,也可以轉成建構類別的父類別型別,但不可以轉成建構類別的子類別型別。
  • 财 為了避免執行時期發生錯誤,在向下轉型之前,我們會用instanceof這個運算符號來確認物件與類別的關係。
  • 财 轉型的是物件,不是參考變數。參考變數的型別就是宣告的型別,不可以改變。
  • 财 參考變數只是代表物件,不是物件。但是我們可以隨時指派任何型別符合的物件給它代表。
  • 财 物件只能執行合乎當時型別的方法。而呼叫方法的是參考變數,所以:參考變數只能呼叫其宣告類別中的方法。
  • 财 只要呼叫的方法,是符合參考變數型別的方法,該方法便可以執行,但執行的是,實際建構物件的類別中,定義的內容。

3-2 序列化
  • 财 物件想要擁有序列化的功能,建構此物件的類別就必須實作java‧io中的界面:Serializable。
  • 财 任何類別只要用implements宣告此界面,不必overriding任何方法,此類別建構的物件便擁有序列化的功能。
  • 财 執行物件輸出這個功能的物件,是ObjectOutputStream類別的物件。
  • 财 要被writeObject()輸出的參數物件須要有序列化的功能。
  • 财 執行物件輸入這個功能的物件,是ObjectInputStream類別的物件。
  • 财 readObject()的運算結果型別是Object。也就是這個方法取得的是Object型別的物件,必須向下轉型後才可以指派給子類別宣告的參考變數(物件多型)。
  • 财 實作serializable的類別,要注意資料成員的宣告。若類別擁有無序列化功能的資料成員,將會造成執行時期錯誤。
  • 财 transient譯為暫時性的。意謂著,用此修飾詞宣告的資料成員不必持續記錄,於是物件輸出時便不會輸出此資料成員的值。
  • 财 宣告為transient的資料成員,輸入時便會指派預設值給該成員。
  • 财 在序列化輸入時,只要有實作序列化界面的類別,它們的建構方法都不會執行。
  • 财 如果實作序列化界面的類別的父類別,沒有實作序列化界面,則父類別的建構方法將會執行。
  • 财 宣告為static的資料成員,並不在序列化輸入及輸出的範圍,因為它們不屬於物件。
 

沒有留言:

張貼留言