iT邦幫忙

0

【左京淳的JAVA學習筆記】第六章 繼承與多型

在創造各式各樣的物件時,有很多時候會發現怎麼重複的代碼很多。
為了解決這個問題,可以採用繼承與介面的方式。

繼承的文法

class Employee {}
class sales extends Employee {}

想像一間公司裡有許多員工,有業務有後勤有管理人員。這些職位的人員雖然都有各自的特性,但也都是員工的一種,他們應該會有員工編號之類的共用屬性。
例如sales類繼承了Employee類,就可以獲得裡面所有的變數與方法,不用重新寫一次。而且還可以追加新的方法,或是覆寫掉不喜歡的方法!

介面的文法

class Employee implements MyInterface {}

介面有點類似繼承,一樣可以獲得來自其他類裡面的變數和方法。不過兩者概念上有點不一樣。
繼承只能繼承一個類,概念像是一個Employee類可以升級為業務、後勤或是管理人員,擁有進階的能力。
而介面可以引用很多個,類似裝備或工具,可以隨時裝上或拆卸。

接下來介紹一下關於繼承的細節
被繼承的類又被稱為Super類或是父類,繼承類則被稱為Sub類或子類。
請看以下關於覆寫(override)的範例:

class Super {
  public void print(String s){
    System.out.println("Super class : " + s );
  }
  public void method(){}
}

class Sub extends Super {
  public void print(String s){
    System.out.println("Sub class : " + s );
  }
  //void method(){}  
}

class Sample6_1 {
  public static void main(String[] args) {
    Super s1 = new Super();
    s1.print("text");  //會調用Super類的方法
    Sub s2 = new Sub();
    s2.print("text");  //會調用Sub類的方法
  }
}

執行結果

Super class : text
Sub class : text

可以發現Sub class裡面的print方法被複寫掉了。與Super class的執行結果不同。
要注意的是,覆寫Super類的方法時,使用一樣的名字和返回值類型,修飾詞則需一樣或者權限更開放。
(權限開放程度: public > protected > 不指定 > private)
例如本案例的method()在Super class裡面的修飾詞是public,則Sub class裡面的method()也必須是public,如果沒寫或寫其他修飾詞就會報錯。

final修飾詞

final修飾詞可以用在變數前面,讓其成為無法被修改的定數。也可以放在方法前面,讓其無法被覆寫。如果是放在類的前面,則此類無法被繼承。如下例:

class Super {
  final void method(){}
}

final class Super {}

this指令

this用來代稱本物件,用來解決變數代稱的問題。
例如有個有名的對話故事如下
「誰打我?」
誰:「我沒打人」
人:「我知道」
我:「知道什麼?」

程式範例如下

int id;
void setId(int id){
  //id = id;  //左右兩個id都是同一個變數(第二行的id)。
  this.id = id;  //this.id是指這個物件的id(第一行的id)。
}

this也可以用來減少重複寫代碼的問題,例如應用在建構式中,範例如下:

class Foo {
  String s; int i;
  public Foo(){
    this("no_data");
  }
  public Foo(String s){
    this(s,1);
  }
  public Foo(String s, int i){
    this.s = s;this.i = i;
    System.out.println("String : " + this.s);
    System.out.println("int : " + this.i);
  }
}

class Sample6_2 {
  public static void main(String[] args) {
    System.out.println("調用Foo()------");
    Foo f1 = new Foo();
    System.out.println("調用Foo(String s)------");
    Foo f2 = new Foo("Tom");
    System.out.println("調用Foo(String s, int i)------");
    Foo f3 = new Foo("Mary",30);
  }
}

執行結果

調用Foo()------
String : no_data
int : 1
調用Foo(String s)------
String : Tom
int : 1
調用Foo(String s, int i)------
String : Mary
int : 30

可以看到Foo裡面有三個建構式,分別對應無資料、只有文字、有文字及數值資料等三種狀況。
利用this指令,當無String資料時,將預設值("no_data")填入後,傳給第二個建構式。
在第二個建構式中,當無int值時,將預設值(1)填入後,然後傳給第三個建構式。
在第三個建構式中進行完整的處理。

利用這樣的結構,就不需要重複寫很多個類似的建構式了。

super指令

super表示父類,利用這個指令可以調用父類的方法或變數。
雖然子類一般都繼承了父類的方法,但是如果有覆寫的狀況,則可以利用super調用原本的父類方法。

class Super {
  int num;
  public void methodA(){num += 100;}
  public void print(){System.out.println("num = " + num);}
}

class Sub extends Super {
  public void methodA(){num += 500;}
  public void methodB(int num){
    methodA();
    print();
    super.methodA();
    print();
  }
}

class Sample6_3 {
  public static void main(String[] args) {
    Sub s = new Sub();
    s.methodB(0);
  }
}

執行結果

num = 500
num = 600

調用覆寫後的方法讓num增加了500
再調用父類原本的方法,讓num增加了100

利用super指令呼叫建構式

當子類被實例化時,會先執行父類的建構式,再執行子類的建構式。
但是有個跟直覺不太一樣的地方,需要透過super指令來修正。看看以下範例:

class Super {
  public Super(){System.out.println("Super()");}
  public Super(int a){System.out.println("Super(int a)");}
}

class Sub extends Super {
  public Sub(){System.out.println("Sub()");}
  public Sub(int a){System.out.println("Sub(int a)");}
}

class Sample6_4 {
  public static void main(String[] args) {
    Sub s1 = new Sub();
    Sub s2 = new Sub(10);
  }
}

執行結果

Super()
Sub()
Super()
Sub(int a)

由結果的第一行及第二行可以發現,實例化物件時,確實先執行了父類的建構式,然後執行子類的建構式。
但是第三行為什麼不是super(int a)呢?
這是因為當子類被實例化時,預設會執行父類的無參數建構式(因為參數並未傳遞給父類)。
如果要避免這種現象,需要使用super指令,把參數丟給父類。如下例:

class Super {
  public Super(){System.out.println("Super()");}
  public Super(int a){System.out.println("Super(int a)");}
}

class Sub extends Super {
  public Sub(){System.out.println("Sub()");}
  public Sub(int a){
    super(a);
    System.out.println("Sub(int a)");
  }
}

class Sample6_5 {
  public static void main(String[] args) {
    Sub s1 = new Sub();
    Sub s2 = new Sub(10);
  }
}

執行結果

Super()
Sub()
Super(int a)
Sub(int a)

abstract class(抽象類)

在JAVA裡面,處理內容被詳細記載,可以實例化並使用的類稱為實例類。相對的,只記載了方法名稱,卻未填寫內容的類,被稱為抽象類。
想像一下,假如我們想設計一台吸塵器,但形式和內容都還不確定,只知道它需要110V的電,能夠吸灰塵。
那麼首先我們就先設計一個具有110V插頭、具有吸灰塵方法的class,至於詳細的內容,就等物件實例化時再填寫就好。

抽象類具有以下特點:
無法被實例化。要實例化需新增一個實例類,繼承了此抽象類之後,把所有抽象方法都覆寫成實例方法(記入內容)。
抽象類裡面可以寫抽象方法以及實例方法
抽象類也可以繼承抽象類。

抽象類和抽象方法的寫法,就是在最前面加上一個abstract修飾詞即可。
abstract class 類名{}
abstract 返回值 方法名(引數);

範例

abstract class Employee{}
class abstract Employee{}  //報錯,abstract須放在最前面

abstract void funcA();
abstract void funcA(){}; //報錯,abstract方法不需要{}及其內容。
void abstract funcA(); //報錯,abstract須放在最前面

interface(介面)

介面說起來有點像抽象類,裡面也放了一些抽象方法。不過不一樣的是,介面使用implements來引用,而非extends繼承。
請看以下範例:

interface MyInter1 {
  double methodA(int num);
  default void methodB(){System.out.println("methodB()");}
}

interface MyInter2 {
  int methodC(int val1, int val2);
  static void methodD(){System.out.println("methodD()");}
}

class MyClass implements MyInter1,MyInter2 {
  public double methodA(int num){return num * 0.3;}
  public int methodC(int val1, int val2){return val1 * val2;}
}

class Sample6_6 {
  public static void main(String[] args) {
    MyClass obj = new MyClass();
    System.out.println("methodA() " + obj.methodA(10));
    System.out.println("methodC() " + obj.methodC(10,20));
    obj.methodB();
    //obj.methodD();  //error
    MyInter2.methodD();
  }
}

執行結果

methodA() 3.0
methodC() 200
methodB()
methodD()

說明
methodA和methodC是抽象方法,在MyClass裡面被覆寫為實例方法而能執行。(注意覆寫時使用了public修飾詞,這是因為覆寫時的權限必須寬於抽象方法。)
methodB前面加了default修飾詞,這是因為interface類原本是只能寫抽象方法的,但JAVA SE8之後利用default修飾詞也可以寫實例方法。
methodD是static方法,看起來沒有傳遞給實例化物件使用,只能由interface呼叫。

interface的定數和方法

在interface裡面,儲存值會自動加上public static final修飾詞。

在interface裡面,方法會自動加上public和abstract修飾詞

interface的繼承
interface簡稱為IF,也可以使用繼承,範例如下:

interface XIF {
  void methodA();
}
interface YIF {
  void methodB();
}
interface SubIF extends XIF, YIF{
  void methodC();
}

class MyClass implements XIF,YIF {
  public void methodA(){System.out.println("methodA()");}
  public void methodB(){System.out.println("methodB()");}
  public void methodC(){System.out.println("methodC()");}
}

class Sample6_7 {
  public static void main(String[] args) {
    MyClass c = new MyClass();
    c.methodA();c.methodB();c.methodC();
  }
}

以下兩點請注意:
interface可以繼承複數的interface(還記得class只能繼承一個class嗎?)
將抽象方法覆寫時,需使用pbulic修飾詞。

基本Data型的變換

為了方便,有時JAVA會自動進行資料型態的變換。
由左至右,比較小的資料儲存型態可以轉換成比較大的資料儲存型態。
byte -> short -> int -> long -> float -> double
char -> int
如果要反向轉換的話,需使用()符號進行強制轉換。

範例
變數的自動轉換

short s = 10;
int i = s;

變數s會從short型自動轉換為int型態

引數的自動轉換

int i = 100;
method(i);

void method(double b){};

i雖然是int型態的資料,但是作為引數被丟入method時,可以自動轉換為double型。

返回值的自動轉換

double d = method();
int method(){int i = 100; return i;}

返回的int型資料,可以自動被轉換為double型資料填入變數d中。

變數的強制轉換

int i = 100;
short s = (short)i;

由大至小的轉換需要使用()符號進行強制轉換。

引數的強制轉換

double d = 10.5
method((int)d);
void method(int i){}

返回值的強制轉換

int i = method();
int method(){double d = 10.5; return (int)d;}

計算時的注意點:
兩個數值進行計算時,JAVA會自動把雙方變為一樣的資料型態。
如果原本的資料型態是byte,short等較小的資料型態,會被轉換成int型態之後計算。
範例

short s1 = 10;
s1 = ++s1;  //可順利執行。因為沒有使用計算符,因此資料型態未被轉換。
s1 = s1 + 1; //NG,因為s1會被轉換為int型後+1,無法再放回short型的變數裡面。

解決例

s1 = short(s1 + 1);

物件的型態變換

除了數值外,物件也可以自動變換。
子類可以自動變換為父類。
實例類可以自動變換為抽象類。

這是什麼意思呢?
想像員工是一個父類,業務是一個子類。
由於業務保有員工的各種屬性,所以可以被當成員工來處理。
但如果是相反方向的強制轉換,則須使用()。
例:

class Super {}
class Sub extends Super {}
class Test {
  Super super = new Sub();  //子類轉父類可自動轉換
  Sub sub = (Sub)super;  //父類轉子類須強制轉換
}

物件轉換的使用時機

請看以下範例

class Super {
  void methodA(){}
}

class Sub extends Super {
  void methodA(){}
  void methodB(){}
}

class Test {
  Super super = new Sub();
  super.methodA();
  //super.methodB();
  Sub sub = (Sub)super;
  sub.methodB();
}

此範例中實例化了一個Sub物件,並轉存成Super形式。
super.methodA(); 這行執行的methodA,會是Sub class裡覆寫後的。
super.methodB(); 這行會報錯,因為super class裡面沒有methodB方法。
須將其轉回Sub形式,才能呼叫methodB方法。

舉個容易瞭解的例子,假設員工擁有「溝通(基礎)」這個技能,業務也有,但是是升級版的「溝通(進階)」。
這時你請這位員工執行此技能,就會執行出「溝通(進階)」(回不去了)。
但基於JAVA的保護機制,你無法讓一個人以員工身分執行業務專有的技能,例如「開發市場」。(即便他會)
必須要明白的告訴JAVA說,這個員工是個業務,才能夠使用業務的技能。

關於物件的轉換,有一些需要注意的點,請看下例:

class Super {}
class Sub extends Super {}
class Foo {}

Super obj1 = new Sub();
Sub sub1 = (Sub)obj1;  //可正常執行。

Foo obj2 = new Foo();
Sub sub2 = (Sub)obj2;  //NG,無繼承關係的物件無法轉換,編譯時會報錯。

Super obj3 = new Super();
Sub sub3 = (Sub)obj3;  //NG,本質是Super的物件無法轉換為Sub物件,執行時會報錯。

以上案例說明了強制轉換只是一種聲明,並不能改變物件的本質。
所以本來是Sub類的物件,即使轉為Super,也可再轉回Sub。
但原本是Super的物件,無法轉換為Sub。

概念說明如下:
Sub(擁有100%能力) ->Super(被當成Super看待,部分能力受限)->Sub(擁有100%能力)
Super(擁有100%能力) ->Sub(被當成Sub看待,超出自己的能力範圍,因此NG)

原來只有超人才能變身成超人呀..

利用instanceof演算符判斷實例化物件的類型

instance是實例的意思,可以判斷目標物件是否為特定的class(也包含父類或interface)
來看看範例:

interface A {}
interface B {}
class C {}
class D extends C implements B {}
class E {}

class Sample6_8 {
  public static void main(String[] args) {
    D d = new D();
    System.out.println(d instanceof A);
    System.out.println(d instanceof B);
    System.out.println(d instanceof C);
    System.out.println(d instanceof D);
    //System.out.println(d instanceof E);
  }
}

執行結果

false
true
true
true

以上是第六章 繼承與多型的學習心得,下一章會介紹API的使用。

參考教材: JAVAプログラマSilver SE8 - 山本 道子


尚未有邦友留言

立即登入留言