Java技巧:何时使用合成与继承,送给正在成为程序员的你

建立类之间关系的方法不止一种
组成与继承
有-有-是-有关系

在组合中,一个类具有一个类型与另一个类相同的字段。例如,Vehicle有一个String名为的字段make。它还可能有一个Engine名为的字段engine和一个Transmission名为的字段transmission:
class Vehicle{
private String make;
private Engine engine;
private Transmission transmission;
// …}
class Transmission{
// …}
class Engine{
// …}
在此示例中,我们可以说车辆由品牌,发动机和变速器组成,因为它具有一个make场,一个engine场和一个transmission场。
除了组成其他类的类之外,还可以通过将对象引用存储在另一个对象的字段中,使用此技术来组成其他对象的对象。
继承破坏封装
继承是有问题的,因为它破坏了封装。你可以从召回的Java 101:Java类和对象是封装是指构造函数,字段和方法组合成一类的机构。在继承中,子类依赖于其超类中的实现细节。如果超类的实现细节发生更改,则子类可能会中断。当开发人员无法完全控制超类时,或者在设计和记录超类时并没有考虑扩展性时,此问题尤其严重。
如何破坏子类:一个例子
为了理解这个问题,假设您已经购买了实现联系人管理器的Java类库。尽管您无权访问其源代码,但假设清单1描述了主CM类。

清单1. 实现联系人管理器的一部分

public class CM{
private final static int MAX_CONTACTS = 1000;
private Contact[] contacts;
private int size;

public CM()
{
contacts = new Contact[MAX_CONTACTS];
size = 0; // redundant because size is automatically initialized to 0
// adds clarity, however
}

public void addContact(Contact contact)
{
if (size == contacts.length)
return; // array is full
contacts[size++] = contact;
}

public void addContacts(Contact[] contacts)
{
for (int i = 0; i < contacts.length; i++)
addContact(contacts[i]);
}}
在CM类存储触头阵列,与由描述的每个接触Contact的实例。在此讨论中,的详细信息Contact并不重要。它可能和public class Contact {}。
现在,假设您要将每个联系人记录在文件中。因为没有提供日志记录功能,所以您扩展CM了清单2的LoggingCM类,该类在覆盖addContact()和addContacts()方法中添加了日志记录行为。

清单2. 扩展联系人管理器以支持日志记录 p

ublic class LoggingCM extends CM{
// A constructor is not necessary because the Java compiler will add a
// no-argument constructor that calls the superclass’s no-argument
// constructor by default.

@Override
public void addContact(Contact contact)
{
Logger.log(contact.toString());
super.addContact(contact);
}

@Override
public void addContacts(Contact[] contacts)
{
for (int i = 0; i < contacts.length; i++)
Logger.log(contacts[i].toString());
super.addContacts(contacts);
}}
本LoggingCM类依赖于Logger类(参见清单3),它的void log(String msg)类中的方法记录一个字符串的文件。甲Contact对象转换为经由一个字符串toString(),它被传递给log()。

清单3.将 log()其参数输出到标准输出流

class Logger{
static void log(String msg)
{
System.out.println(msg);
}}
尽管LoggingCM看起来还可以,但它并没有像您预期的那样起作用。假设您实例化了此类,并Contact通过addContacts()以下方式向该对象添加了一些对象:

清单4. 继承问题

class CMDemo{
public static void main(String[] args)
{
Contact[] contacts = { new Contact(), new Contact(), new Contact() };
LoggingCM lcm = new LoggingCM();
lcm.addContacts(contacts);
}}
如果运行此代码,您将发现log()输出总共六条消息。问题是预期的三个消息(每个Contact对象一个)中的每一个都重复。

发生了什么?

当LoggingCM的addContacts()方法被调用时,它首先调用Logger.log()的每个Contact中实例contacts传递到阵列addContacts()。然后,这个方法调用CM的addContacts()通过方法super.addContacts(contacts);。
CM的addContacts()方法会调用LoggingCM的重写addContact()方法,Contact它的contacts数组参数中的每个实例都使用一个。addContact()然后Logger.log(contact.toString());,该方法执行,以记录其contact参数的字符串表示形式,最后得到三个附加的记录消息。
方法重写和脆弱的基类问题
如果您不重写此addContacts()方法,此问题将消失。但是,在这种情况下,子类仍然被捆绑到一个实现细节:CM的addContacts()方法调用addContact()。
如果未记录实现细节,则不要依赖该实现细节。(请记住,您无权访问CM的源代码。)如果未记录详细信息,则可以在该类的新版本中对其进行更改。
因为基类的更改可能会破坏子类,所以此问题被称为脆弱的基类问题。当在后续发行版中将新方法添加到超类中时,就会出现相关的脆弱性原因(这也与覆盖方法有关)。
例如,假设该库的新版本将public void addContact(Contact contact, boolean unique)方法引入CM类。此方法contact在uniqueis 时将实例添加到联系人管理器false。如果unique是true,它增加了只有在先前未添加它接触的实例。
由于此方法是在LoggingCM创建类后添加的,因此LoggingCM不会addContact()通过调用覆盖新方法Logger.log()。结果,不会记录Contact传递给新addContact()方法的实例。
这是另一个问题:您在子类中引入了不在父类中的方法。新版本的超类提供了与子类方法签名和返回类型相匹配的新方法。您的子类方法现在将覆盖超类方法,并且可能无法满足超类方法的约定。
撰写(并转发)以进行救援
幸运的是,您可以使所有这些问题都消失。不用扩展超类,而是private在新类中创建一个字段,并使该字段引用超类的实例。此解决方法需要在新类和超类之间形成一个has-a关系,因此您使用的技术是组合。
另外,您可以使每个新类的实例方法调用相应的超类方法,并返回被调用方法的返回值。您可以通过保存在private字段中的超类实例来执行此操作。此任务称为转发,新方法称为转发方法。
清单5给出了一个改进的LoggingCM类,该类使用组合和转发来永远消除脆弱的基类问题和无法预料的方法重写的附加问题。

清单5. 组成和方法转发演示

public class LoggingCM{
private CM cm;

public LoggingCM(CM cm)
{
this.cm = cm;
}

public void addContact(Contact contact)
{
Logger.log(contact.toString());
cm.addContact(contact);
}

public void addContacts(Contact[] contacts)
{
for (int i = 0; i < contacts.length; i++)
Logger.log(contacts[i].toString());
cm.addContacts(contacts);
}}
请注意,在此示例中,LoggingCM该类不依赖于CM该类的实现细节。您可以在CM不中断的情况下添加新方法LoggingCM。
包装器类和装饰器设计模式
清单5的LoggingCM类是包装类的示例,包装类是其实例包装其他实例的类。每个LoggingCM对象都包装一个CM对象。LoggingCM也是装饰设计模式的一个例子。
要使用新LoggingCM类,您必须首先实例化CM并将生成的对象作为参数传递给LoggingCM的构造函数。的LoggingCM对象包装的CM物体,如下所示:
LoggingCM lcm = new LoggingCM(new CM());

结论

在这个Java技巧中,您学习了组合和继承之间的区别,以及如何使用组合来组合其他类中的类。您还了解到,组合解决了与继承相关的主要编程挑战之一,那就是它破坏了封装。
对于将来的开发人员不太可能访问或控制超类的情况,组合是一种重要的编程技术。对于未考虑扩展设计和记录类包或库的情况,这是一项特别关键的技术。
设计和记录类扩展意味着什么?设计意味着提供protected与类的内部工作挂钩的方法(以支持编写有效的子类),并确保构造函数和clone()方法从不调用可重写的方法。文件意味着清楚地描述覆盖方法的影响。
您可能还想知道什么时候应该扩展类或使用包装器。当超类和子类之间存在is -a关系并且您可以控制超类或为类扩展设计和记录超类时,扩展类。否则,请使用包装器类。
您可能听说过,不应该在回调框架中使用包装器类,该框架是一个对象框架,在该框架中,对象将自己的引用传递给另一个对象(通过this),以便后者对象可以在以后调用前者的方法。 – 回调。在这种情况下,您不应使用包装器类,因为被包装的对象不知道其包装器类(它仅通过传递其引用this),并且产生的回调方法不会调用包装器类的方法。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。

发布了38 篇原创文章 · 获赞 8 · 访问量 2715

猜你喜欢

转载自blog.csdn.net/zhaozihao594/article/details/103908224