這篇文章闡明了Java中繼承和組合的概念。首先展示了繼承的例子,接著顯示如何通過(guò)組合來(lái)改進(jìn)繼承。最后總結(jié)如何在它們之間做選擇。
1. 繼承
想下,我們有個(gè)叫 insert
的類(lèi),這個(gè)類(lèi)包含兩個(gè)方法:1) move()
和2)attack()
.
class Insect {
private int size;
private String color;
public Insect(int size, String color) {
this.size = size;
this.color = color;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public void move() {
System.out.println("Move");
}
public void attack() {
move(); //assuming an insect needs to move before attacking
System.out.println("Attack");
}
}
現(xiàn)在你需要定義一個(gè)Bee
類(lèi),它是Insert
類(lèi)子類(lèi),但是有attack()
和move()
不同實(shí)現(xiàn)。它可以按照繼承如下設(shè)計(jì):
class Bee extends Insect {
public Bee(int size, String color) {
super(size, color);
}
public void move() {
System.out.println("Fly");
}
public void attack() {
move();
super.attack();
}
}
public class InheritanceVSComposition {
public static void main(String[] args) {
Insect i = new Bee(1, "red");
i.attack();
}
}
類(lèi)型繼承關(guān)系圖簡(jiǎn)單如下:
輸出:
Fly
Fly
Attack
"Fly" 被打印兩次,它顯示了move()
被調(diào)用了兩次。但是它應(yīng)該只被調(diào)用一次。
問(wèn)題是由super.attack()方法導(dǎo)致。Insert
的attack()
方法調(diào)用了move()
方法。當(dāng)子類(lèi)調(diào)用super.attack(),它也調(diào)用了被覆蓋的move()
方法。
為了解決這個(gè)問(wèn)題,我們可以:
1、除去子類(lèi)的attack
方法。它將會(huì)使子類(lèi)完全依賴(lài)父類(lèi)實(shí)現(xiàn)的attack()
方法。如果父類(lèi)的attack
方法在后面被改變了(這不是你所控制的),例如,父類(lèi)的attack()
方法調(diào)用另外的方法去移動(dòng),子類(lèi)也需要改變。這是個(gè)失敗的封裝。
2.如下重寫(xiě)attack()
方法。
public void attack() {
move();
System.out.println("Attack");
}
因?yàn)樽宇?lèi)不再依賴(lài)父類(lèi)信息,所以可以保證結(jié)果的正確性。但是,父類(lèi)的代碼是重復(fù)的。(想下 attack()
方法調(diào)用了更復(fù)雜的方法二不是僅僅打印一個(gè)字符串)它對(duì)于軟件工程師重用規(guī)則來(lái)說(shuō)的時(shí)候是不被準(zhǔn)許的。
繼承設(shè)計(jì)是不好的,因?yàn)樽宇?lèi)依賴(lài)了父類(lèi)的實(shí)現(xiàn)細(xì)節(jié)。如果父類(lèi)改變了,子類(lèi)也需要改變。
2.組合
在這個(gè)例子中,可以用組合而不是繼承。讓我們先看看組合的解決方案。
attack函數(shù)被抽象作為一個(gè)接口。
interface Attack {
public void move();
public void attack();
}
Attack
接口可以被定義為多個(gè)不同種類(lèi)的attack
實(shí)現(xiàn)。
class AttackImpl implements Attack {
private String move;
private String attack;
public AttackImpl(String move, String attack) {
this.move = move;
this.attack = attack;
}
@Override
public void move() {
System.out.println(move);
}
@Override
public void attack() {
move();
System.out.println(attack);
}
}
因?yàn)閍ttack函數(shù)被抽象了,Insert
類(lèi)將不再與attack有任何關(guān)系。
class Insect {
private int size;
private String color;
public Insect(int size, String color) {
this.size = size;
this.color = color;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
Bee是一個(gè)Insert
類(lèi)型,它可以attack。
// This wrapper class wrap an Attack object
class Bee extends Insect implements Attack {
private Attack attack;
public Bee(int size, String color, Attack attack) {
super(size, color);
this.attack = attack;
}
public void move() {
attack.move();
}
public void attack() {
attack.attack();
}
}
類(lèi)結(jié)構(gòu)圖如下:
public class InheritanceVSComposition2 {
public static void main(String[] args) {
Bee a = new Bee(1, "black", new AttackImpl("fly", "move"));
a.attack();
// if you need another implementation of move()
// there is no need to change Insect, we can quickly use new method to attack
Bee b = new Bee(1, "black", new AttackImpl("fly", "sting"));
b.attack();
}
}
輸出:
fly
move
fly
sting
3. 什么使用用它們
下面兩點(diǎn)可以指導(dǎo)繼承和組合之間的選擇:
- 如果是IS-A關(guān)系,一個(gè)類(lèi)想要對(duì)另外一個(gè)類(lèi)暴露所有接口,繼承看起來(lái)是更好的選擇。
2.如果是HAS-A關(guān)系,組合是更好的選擇。
總之,繼承和組合都有它們的應(yīng)用場(chǎng)景,理解它們的關(guān)系是值得的。