合成的语法,继承的语法,合成与继承的结合,合成与继承的结合,双重检查锁的问题

一、类再生

在这一章里,我们将介绍两个达到这一目标的方法。

第一个最简单:在新类里简单地创建原有类的对象。

我们把这种方法叫作“合成”,因为新类由现有类的对象合并而成。我们只是简单地重复利用代码的功能,而不是采用它的形式。

第二种方法则显得稍微有些技巧:它创建一个新类,将其作为现有类的一个“类型”。

我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”(Inheritance),涉及的大多数工作都是由编译器完成的。

对于面向对象的程序设计,“继承”是最重要的基础概念之一。它对我们下一章要讲述的内容会产生一些额外的影响。

我的理解:

类再生可以理解为如何安全有效地使用原有类,从而减轻我们的代码量。

类再生分为两种形式:一种是合成,即在新类中创建原有类的对象,实现对原有类的操作。第二种是继承,通过新类对原有类的继承,不但不影响原有类,还可以使用原有类的字段和方法。

因为有继承往往设计多个类,所以会导致jvm中的一些流程变得比较复杂,所以需要掌握良好才知道在jvm中发生了什么,为什么。

(1)合成的语法

为进行合成,我们只需在新类里简单地置入对象句柄即可。

举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。

编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望句柄得到初始化,可在下面这些地方进行:

  1. 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。
  2. 在那个类的构建器中。
  3. 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。

我的理解:

合成其实就是在新类中进行原有类的初始化,从而使用原有类。

比较有意思的是这句话:紧靠在要求实际使用那个对象之前进行创建,假如对象并不需要创建的话,这样做可减少不必要的开销。

假如说程序中是有逻辑分支的,那么对象有可能并不会被使用。所以这时候就不需要过早进行进行初始化,避免资源浪费,只在使用前进行初始化即可。

还有一种需要“延迟初始化”的情况:

  1. 有一个对象的创建开销很大时,应用程序可能不会使用它,所以希望能够在使用之前才进行初始化。
  2. 有一个对象的创建开销很大,您想要将创建它的时间延迟到完成其他开销大的操作之后,才进行初始化。

这时候可能需要用到延迟初始化(我觉得这是初始化对象的时机问题)。

在早期的jvm中,延迟初始化可能会导致一些问题。在多线程的环境下,如果进行延迟初始化,对象可能会被多个线程同时进行操作。如果使用synchronized等线程安全的关键字,有可能导致程序性能的降低。

还有一个经典的双重检查锁的问题,详细可以看问题分析的原文:

http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization

在类中初始化对象(合成)看起来是很简单,如果要保证安全并且高效,则比较有难度。

(2)继承的语法

创建一个类时肯定会进行继承,若非如此,会从Java的标准根类Object中继承。

用于合成的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。

需要继承的时候,我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。

若采取这种做法,就可自动获得基础类的所有数据成员以及方法。

由于这里涉及到基础类及衍生类两个类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。

从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。

当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java会自动插入对基础类构建器的调用。

举个例子:

结果为:

我的理解:

可以看见,虽然我们从外部看上去只是实例化了一个Cartoon对象,实际上内部对所有父类都做了实例化,所以我们才能调用父类的字段和方法。也就是说,继承实际上是把父类也实现了,然后封装到我们实例化的对象中。

(3)合成与继承的结合

许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构建器初始化工作。

尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。

1.确保正确的清除

Java不具备象C++的“破坏器”那样的概念。在C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行破坏它们。垃圾收集器会在必要的时候自动回收内存。

垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。

正如第4章已经指出的那样,我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。

而在所有这一切的后面,就如第9章(违例控制)要详细解释的那样,必须将这样的清除代码置于一个finally从句中,从而防范任何可能出现的违例事件。

我的理解:

垃圾回收的概念和之前写的差不多。但是这里特别提到,因为不知道什么时候才会GC,如果想要专门为一个对象进行清理,就要写一个特殊的清除方法,放在finally块中,最后进行回收。

举个例子:我们可以在finally块中调用方法释放数据库链接、socket链接,书里说的大概就是这个意思。

那么问题来了:加入存在一些我们自定义的资源,如何写方法进行回收?这个问题就比较复杂了,这里不展开讨论。

2.名字的隐藏

如果Java基础类有一个方法名被“过载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。所以无论方法在这一级还是在一个基础类中定义,过载都会生效。

很少会用与基础类里完全一致的签名和返回类型来覆盖同名的方法,否则会使人感到迷惑。

我的理解:

其实就是简单的方法重载。

(4)选择合成还是继承

无论合成还是继承,都允许我们将子对象置于自己的新类中。大家或许会奇怪两者间的差异,以及到底该如何选择。

1.如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择合成。

也就是说,我们可嵌入一个对象,使自己能用它实现新类的特性。但新类的用户会看到我们已定义的接口,而不是来自嵌入对象的接口。考虑到这种效果,我们需在新类里嵌入现有类的private对象。

2.有些时候,我们想让类用户直接访问新类的合成,也可以说是使用接口,那么就可以使用继承。

也就是说,需要将成员对象的属性变为public。成员对象会将自身隐藏起来,所以这是一种安全的做法。而且在用户知道我们准备合成一系列组件时,接口就更容易理解。

我的理解:

这个还算是好理解。虽然这个概念看上去比较复杂,但是实际工作中应该都能做出恰当的选择。

二、总结

主要讲述了合成和继承的概念。通过这次学习,感觉我对继承的实现又了解深了一些。

从外部看上去,我们只是实现了子类,创建了子类对象。然而在子类内部中,已经实现了父类,并且把父类中的字段和方法封装在子类对象中了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注