继承和组合的区别

249

组合和继承是一样的吗? 如果我想实现组合模式,我应该如何在Java中实现?


2
其他相关问题:组合有没有什么继承可以实现不了的功能?https://dev59.com/t3E95IYBdhLWcg3wmvGh - ewernli
1
请参阅继承VS组合,哪个更好 - nawfal
1
这篇文章对我很有用,帮了我很多:https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose - QMaster
请查看 https://themightyprogrammer.dev/article/inheritance-composition。 - The Mighty Programmer
17个回答

377
它们是完全不同的。继承是一种"is-a"关系,而组合是一种"has-a"关系。
通过将另一个类C的实例作为您的类的字段,而不是扩展C来执行组合。一个很好的例子是java.util.Stack,其中组合比继承好得多,因为它目前扩展了java.util.Vector。这现在被认为是一个大错误。堆栈"不是一个"矢量; 您不应该允许任意插入和删除元素。它应该是组合而不是继承。
不幸的是,由于更改继承层次结构现在会破坏与现有代码的兼容性,因此已经太晚纠正这个设计错误。如果Stack使用组合而不是继承,则始终可以修改其使用其他数据结构而不违反API。
我强烈推荐Josh Bloch的书Effective Java第二版
项目16:优先使用组合而不是继承 项目17:设计和文档化继承,否则禁止继承
良好的面向对象设计不是自由地扩展现有类。您的第一直觉应该是进行组合。

另请参阅:


5
有趣。为什么不创建一个使用组合的新类 java.util.Stack2 呢? - qed
7
我感谢这个答案;然而,我觉得答案跑题了,更多地深入探讨了关于语言设计(和特定软件包)的问题,而不是回答有关组合与继承的问题。我非常支持在SO上回答问题,并引用资源-而不是仅提供一行总结的外部资源链接。请注意不要改变原来的意思。 - Thomas
6
糟糕的例子,我不得不进行额外的搜索才能理解什么是向量和堆栈。 - Sam Ramezanli
1
不错,但是你关于Java堆栈的例子并不是一个好的选择,因为这种继承是在选择继承而不是组合时做出的错误决定的一个例子,正如这篇文章所说:https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose#eight - QMaster
但是GoF设计模式经常使用继承。为什么呢?使用继承的代码比组合更易读和理解。无论我走到哪里,我都看到有利于组合而不是继承的文章,然而,我还没有看到实际应该使用组合的情况。 - codingbruh

251

组合意味着HAS A
继承意味着IS A

例子: 汽车有一个引擎并且汽车是一种汽车类

在编程中,这表示为:

class Engine {} // The Engine class.

class Automobile {} // Automobile class which is parent to Car class.

class Car extends Automobile { // Car is an Automobile, so Car class extends Automobile class.
  private Engine engine; // Car has an Engine so, Car class has an instance of Engine class as its member.
}

9
我会把“汽车”改成“动力车辆”,因为在很多解释中,汽车和动力车辆是等效的。 - nanofarad
7
我同意"Vehicle"可能是更好的选择,但同时-codaddict-已经很好地阐述了被问到的问题。 - nckbrz
6
一台引擎 - Omar Tariq
讲解得非常清楚易懂。点赞!更多阅读请参考http://javarevisited.blogspot.in/2015/06/difference-between-inheritance-and-Composition-in-Java-OOP.html - roottraveller
@AndreyAkhmetov 汽车可以有一个类型为“枚举”类型的“type”字段。 - Ojonugwa Jude Ochalifu
@ojonugwaochalifu 我不明白它与我的陈述有什么关联(那是五年前的事情,也许我已经忘记了这里的上下文)。我指的是类型名称的描述性,而不是任何枚举的添加(这不适用于此答案)。 - nanofarad

58

继承如何存在风险?

我们来举个例子

public class X{    
   public void do(){    
   }    
}    
Public Class Y extends X{
   public void work(){    
       do();    
   }
}

1)如上所示,类Y与类X之间的耦合度非常强。如果超类X中有任何变化,Y可能会出现严重故障。假设未来类X实现了一个带有以下签名的方法:

public int work(){
}

在X类中进行更改,但它将使Y类无法编译。因此,这种依赖关系可以一直延伸到任何级别,并且可能非常危险。每个超类可能没有完全可见其所有子类内部代码,并且子类可能会一直注意超类中发生的情况。因此,我们需要避免这种强大且不必要的耦合。

组合如何解决此问题?

让我们通过修改相同的示例来看看。

public class X{
    public void do(){
    }
}

Public Class Y{
    X x = new X();    
    public void work(){    
        x.do();
    }
}

在这里,我们在Y类中创建X类的引用并通过创建X类的实例调用X类的方法。 现在所有强耦合关系都消失了。超类和子类现在高度独立于彼此。在继承情况下危险的更改现在可以自由地进行。

2) 组合的第二个非常好的优点是它提供了方法调用的灵活性,例如:

class X implements R
{}
class Y implements R
{}

public class Test{    
    R r;    
}

在Test类中使用r引用,我可以调用X类和Y类的方法。这种灵活性在继承中从未存在过。

3)另一个巨大的优势:单元测试。

public class X {
    public void do(){
    }
}

Public Class Y {
    X x = new X();    
    public void work(){    
        x.do();    
    }    
}

在上面的例子中,如果 x 实例的状态未知,可以使用一些测试数据轻松地进行模拟,所有方法都可以轻松地进行测试。而在继承中完全不可能,因为你严重依赖于超类来获取实例的状态并执行任何方法。

4) 我们应该避免使用继承的另一个好理由是Java不支持多重继承。

让我们通过一个例子来理解:

Public class Transaction {
    Banking b;
    public static void main(String a[])    
    {    
        b = new Deposit();    
        if(b.deposit()){    
            b = new Credit();
            c.credit();    
        }
    }
}

需要了解的内容:

  1. 组合可以在运行时轻松实现,而继承在编译时提供其功能

  2. 组合也称为HAS-A关系,继承也称为IS-A关系

因此,出于上述各种原因,请始终优先考虑组合而不是继承。


7
同意你的解决方案,但是考虑到你使用的组合方式...我们仍然需要进行一些维护工作。例如,如果超类 X 将方法名从“do”更改为“doing”,那么子类 Y 也需要进行维护(同样需要进行更改),这仍然存在紧密耦合的问题。那么我们如何消除它呢? - stuckedoverflow
1
@stuckedoverflow 改变合同的名称或签名是反模式。然而,新的扩展是可以接受的。(SOLID中的O) - Hrishabh Gupta

22

@Michael Rodrigues所给出的答案不正确(我很抱歉;我无法直接评论),可能会引起一些混淆。

接口实现是继承的一种形式...当您实现一个接口时,您不仅继承了所有常量,而且还使您的对象成为接口指定类型的对象;这仍然是一个"is-a"关系。如果汽车实现Fillable,那么汽车"是一个"Fillable,可以在您需要使用Fillable的任何地方使用它。

组合与继承根本不同。当您使用组合时,您(如其他答案中所述)在两个对象之间建立了一个"has-a"关系,与使用继承时建立的"is-a"关系截然不同

因此,在其他问题的汽车示例中,如果我想说汽车""油箱,我将使用组合,如下所示:

public class Car {

private GasTank myCarsGasTank;

}

希望这能消除任何误解。


21

继承 显现出 IS-A 关系。 组合 显现出 HAS-A 关系。 策略模式说明在定义特定行为的算法族存在的情况下应该使用组合。
一个经典的例子是一个鸭子类,它实现了一种飞行行为。

public interface Flyable{
 public void fly();
}

public class Duck {
 Flyable fly;

 public Duck(){
  fly = new BackwardFlying();
 }
}

因此,我们可以有多个实现飞行的类,例如:

public class BackwardFlying implements Flyable{
  public void fly(){
    Systemout.println("Flies backward ");
  }
}
public class FastFlying implements Flyable{
  public void fly(){
    Systemout.println("Flies 100 miles/sec");
  }
}

如果只是使用继承,我们将不得不拥有实现飞行功能的两个不同鸟类。因此,继承和组合是完全不同的。


鸭子实现 BackwardFlying 或 BackwardFlyer 的缺点是什么? - Hrishabh Gupta

8
组合就像它听起来的那样-通过插入部件来创建对象。
编辑:以下答案的其余部分基于错误的前提条件。 这是通过接口实现的。 例如,使用上面的“汽车”示例,
Car implements iDrivable, iUsesFuel, iProtectsOccupants
Motorbike implements iDrivable, iUsesFuel, iShortcutThroughTraffic
House implements iProtectsOccupants
Generator implements iUsesFuel

所以,只需使用几个标准的理论组件,您就可以构建对象。然后,您需要填写一个“房子”如何保护其居民以及“汽车”如何保护其乘客的内容。
继承正好相反。您从一个完整(或半完整)的对象开始,然后替换或覆盖您想要更改的各个部分。
例如,“MotorVehicle”可能带有一个“Fuelable”方法和一个“Drive”方法。您可以将Fuel方法保留为原样,因为加油对于摩托车和汽车来说是相同的,但您可能会覆盖“Drive”方法,因为摩托车驾驶与“汽车”驾驶非常不同。
使用继承时,有些类已经完全实现,而其他类具有您被迫覆盖的方法。使用组合时,没有任何东西提供给您。(但是,如果您恰好拥有某些东西,则可以通过调用其他类中的方法来实现接口)。
组合被视为更灵活的方式,因为如果您有一个iUsesFuel方法,则可以在其他位置(另一个类、另一个项目)中拥有一个仅关注可加油对象的方法,而不管它是汽车、船、炉子、烧烤等。接口要求声称实现该接口的类实际上具有该接口所涉及的方法。例如,
iFuelable Interface:
   void AddSomeFuel()
   void UseSomeFuel()
   int  percentageFull()

那么你可以在其他地方编写一个方法
private void FillHerUp(iFuelable : objectToFill) {

   Do while (objectToFill.percentageFull() <= 100)  {

        objectToFill.AddSomeFuel();
   }

这是一个奇怪的例子,但它展示了这种方法不关心它正在填充什么,因为对象实现了iUsesFuel接口,所以可以进行填充。故事结束。

如果您使用继承,您将需要不同的FillHerUp方法来处理MotorVehiclesBarbecues,除非您有一些相当奇怪的“ObjectThatUsesFuel”基对象可以继承。


Java规范规定类和接口名称应该使用ThisCase写法,而不是camelCase。因此最好将您的接口命名为IDrivable等。如果您正确地将所有接口分组到一个包中,则可能不需要"I"。 - ThePyroEagle

7

组合和继承是一样的吗?

它们不一样。

组合(来自维基百科):它使得一组对象可以被视为单个对象的方式进行处理。组合的目的是将对象组成树形结构,以表示部分-整体层次结构。

继承(来自Oracle文档):一个类从其所有超类(直接或间接)继承字段和方法。子类可以覆盖它继承的方法,或者隐藏它继承的字段或方法。

如果我想实现组合模式,我该如何在Java中实现?

关键参与者:(根据维基百科链接中显示的UML图)

Component

  1. 是所有组件(包括复合组件)的抽象
  2. 声明组合中对象的接口

Leaf:

  1. 组合模式中表示叶子对象
  2. 实现了所有组件方法

Composite:

  1. 表示具有子元素的组合组件
  2. 实现操作子元素的方法
  3. 通常通过将它们委托给其子元素来实现所有组件方法

代码示例以理解组合模式

import java.util.List;
import java.util.ArrayList;

interface Part{
    public double getPrice();
    public String getName();
}
class Engine implements Part{
    String name;
    double price;
    public Engine(String name,double price){
        this.name = name;
        this.price = price;
    }
    public double getPrice(){
        return price;
    }
    public String getName(){
        return name;
    }
}
class Trunk implements Part{
    String name;
    double price;
    public Trunk(String name,double price){
        this.name = name;
        this.price = price;
    }
    public double getPrice(){
        return price;
    }
    public String getName(){
        return name;
    }
}
class Body implements Part{
    String name;
    double price;
    public Body(String name,double price){
        this.name = name;
        this.price = price;
    }
    public double getPrice(){
        return price;
    }
    public String getName(){
        return name;
    }
}
class Car implements Part{
    List<Part> parts;
    String name;
    
    public Car(String name){
        this.name = name;
        parts = new ArrayList<Part>();
    }
    public void addPart(Part part){
        parts.add(part);
    }
    public String getName(){
        return name;
    }
    public String getPartNames(){
        StringBuilder sb = new StringBuilder();
        for ( Part part: parts){
            sb.append(part.getName()).append(" ");
        }
        return sb.toString();
    }
    public double getPrice(){
        double price = 0;
        for ( Part part: parts){
            price += part.getPrice();
        }
        return price;
    }   
}

public class CompositeDemo{
    public static void main(String args[]){
        Part engine = new Engine("DiselEngine",15000);
        Part trunk = new Trunk("Trunk",10000);
        Part body = new Body("Body",12000);

        Car car = new Car("Innova");
        car.addPart(engine);
        car.addPart(trunk);
        car.addPart(body);
        
        double price = car.getPrice();
        
        System.out.println("Car name:"+car.getName());
        System.out.println("Car parts:"+car.getPartNames());
        System.out.println("Car price:"+car.getPrice());
    }
    
}

输出:

Car name:Innova
Car parts:DiselEngine Trunk Body
Car price:37000.0

说明:

  1. 零件是一片叶子
  2. 汽车包含多个部分
  3. 不同的汽车零件已经添加到汽车上
  4. 汽车的价格=每个零件的价格之和

请参考下面的问题,了解组合和继承的利弊。

更喜欢组合而不是继承吗?


将汽车类的部分实现为组合,这样做有意义吗?如果使用组合是否更好? - bhalkian

4

简单来说,聚合意味着有一种关系..

组合是聚合的一种特殊情况。更具体地说,受限制的聚合被称为组合。当一个对象包含另一个对象时,如果所包含的对象不能存在于没有容器对象的情况下,则称其为组合。 例如:一个班级包含学生。学生没有班级就无法存在。班级和学生之间存在组合关系。

为什么使用聚合

代码可重用性

何时使用聚合

当不存在“is a”关系时,通过聚合实现最佳代码重用。

继承

继承是父子关系,继承意味着“is a”关系

在Java中,继承是一种机制,其中一个对象获取父对象的所有属性和行为。

在Java中使用继承: 1. 代码可重用性。 2. 在子类中添加额外功能以及方法覆盖(因此可以实现运行时多态性)。


4

组合是指某物由不同的部分组成,并且与这些部分之间有着紧密的关系。如果主要部分死亡,其他部分也会随之死亡,它们不能独立存在。一个简单的例子就是人体。取出心脏,所有其他部分都会消失。

继承是指你只需要使用已经存在的东西,而没有强烈的关系。一个人可以继承他父亲的财产,但他也可以没有它。

我不懂Java,所以无法提供示例,但我可以解释这些概念。


4
作为另一个例子,考虑汽车类,这将是组合的一个很好的用途,汽车将“拥有”发动机、传动系统、轮胎、座位等。它不会扩展任何这些类。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接