Java学习笔记——复用类

本文为Java编程思想第四版的学习笔记,在此感谢作者Bruce Eckel给我们带来这样一本经典著作,也感谢该书的翻译及出版人员。本文为原创笔记,转载请注明出处,谢谢。


复用代码是Java众多引人注目的功能之一。但要想成为极具革命性的语言,仅仅能够赋值代码并对之加以改变还是不够的,它还必须能够做更多的事情。此方法的窍门在于使用类而不破坏现有程序代码。读者将会在本章中看到两种打到这一目的的方法。第一种方法非常直观:只需在新的类中产生现有类的对象。由于新的类是由现有的类的对象所组成,所以这种方法称为组合。该方法只是复用了现有程序代码的功能,而非它的形式。第二种方法则更细致一些,他按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。这种神奇的方式称为继承,而且编译器可以完成其中大部分工作。


1.组合语法

组合只需将对象引用置于新类中即可。类中域为基本类型时能够自动被初始化为零。但是对象引用会被初始化为null,而且如果你试图为他们调用任何方法,都会得到一个异常。编译器并不是简单地为每一个引用都创建默认对象,这一点是很有意义的,因为若真要那样做的话,就会在许多情况下增加不必要的负担。如果想初始化这些引用,可以在代码中的下列位置进行:

1)在定义对象的地方。这意味着他们总是能够在构造器被调用之前被初始化好。

2)在类的工造其中

3)就在正要使用这些对象之前,这种方式称为惰性初始化。在省城对象不值得及不必每次都声称对象的情况下,这种方式可以减少额外的负担。

4)使用实力初始化


2.继承语法

继承是所有OOP语言和Java语言不可缺少的组成部分。当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从Java的标准根类Object进行继承。组合语法比较平实,但是继承使用的是一种特殊的语法。在继承过程中,需要先声明“新类与旧类相似”。这种生命是通过在类主题的左边花括号之前,书写后面紧随基类名称的关键字extends而实现的。当这么做时,会自动得到基类中所有的域和方法。请记住,如果没有加任何访问权限修饰词,那么成员默认的访问权限是包访问权限,它仅允许包内的成员访问。因此,在此包中,如果没有访问权限修饰词,任何人都可以使用这些方法。所以,为了继承,一般的规则是将所有的数据成员指定为private,将所有的方法指定为public(稍后将会学到,protected成员也可以借助导出类来访问)。当然,在特殊情况下,必须作出调整,但上述方法的确是一个很有用的规则。

2.1 初始化基类

由于现在涉及基类和导出类这两个类,而不是只有一个类,所以要试着想象导出类所产生的结果对象,会有点困惑。从外部来看,它就像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用积累直接创建的对象是一样的。二者区别在于,后者来自与外部,而基类的子对象被包装在导出类对象内部。

当然,对基类子对象的正确初始化也是至关重要的,而且也仅有一种方法来保证这一点:在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需的所有知识和能力。Java会自动在导出类的构造其中出啊如对基类构造器的调用。读者会发现,构建过程是从基类“向外”扩散的,所以积累的导出类构造器可以访问它之前,就已经完成了初始化。即使你不为它创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super显式地编写调用基类构造器的语句,并且配以适当的参数列表。


3.代理

第三种关系称为代理,Java并没有提供对他的直接支持。这是继承与组合之间的中庸之道,因为我们讲一个成员对象至于所要够早的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法。


4.结合使用组合和继承

同时使用组合和继承是很常见的事。虽然编译器强制你去初始化基类,并且要求你要在构造器其实处就要这么做,但是它并不监督你必须将成员对象也初始化,因此在这一点上你自己必须时刻注意。


4.1 确保正确清理

在Java中,我们的习惯只是忘掉而不是销毁对象,并且让垃圾回收器在必要时释放其内存。通常这样做是好事,但有时类可能要在其生命周期内执行一些必须的清理活动。你并不知道垃圾回收器何时将会被调用,或者它是否被调用。因此,如果你想要某个类清理一些东西,就必须显式地编写一个特殊方法来做这件事,并要确保客户端程序员直销他们必须调用这一方法。就像在第12章所描述的那样,其首要任务就是,必须将这一清理动作置于finally子句中,以预防异常的出现。在清理方法中,还必须注意对基类清理方法和成员对象清理方法的调用顺序,以防某个子对象依赖于另一个子对象的情形发生。一般而言你,所采用的心事应该与C++编译器在其析构函数上所施加的形式相同:首先,执行类的所有特定的清理动作,其顺序同生成顺序相反(通常这就要求基类元素仍旧存活);然后调用积累的清理方法。许多情况下,清理并不是问题,仅需让垃圾回收器完成该动作就行。但当必须亲自处理清理时,就得多做努力并多加小心,因为,一旦涉及垃圾回收,能够新来的事就不会很多了。垃圾回收器可能永远也无法被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。最好的办法就是除了内存以外,不能依赖垃圾回收器去做任何事。如果需要进行清理,最好是编写你自己的清理方法,但不要使用finalize()。


4.2 名称屏蔽

如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽器在基类中的任何版本(这一点与C++不同)。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作。


5.在组合与集成之间选择

组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承则是隐式地做。读者或许想知道二者之间的区别何在,以及怎样在二者之间做出选择。

组合技术通常用于想在新类中使用现有类的功能而非他的接口这种情形。即,在新类中嵌入某个对象,让其实现所需的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。为取得此效果,需要在新类中嵌入一个现有类的private对象。有时,允许类的用户直接访问新类中的组合成分是极具意义的;也就是说,将成员对象声明为public。如果成员对象自身都隐藏了具体实现,这样做是安全的。当用户能够了解到你正在组装一组部件时,就会使得端口更加易于理解。

在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。


6.protected关键字

现在,我们已介绍完了继承,关键字protected最终具有了意义。在理想世界中,仅靠关键字private就已经足够了。但实际项目中,经常会想要某些事物尽可能对这个世界隐藏起来,但仍然允许到处类的成员访问他们。关键字protected就是起这个最用。它指明“就类用户而言,这是private的,但对于任何继承与此类的导出类或其他任何位于同一个包内的类来说,它确是可以访问的。”(protected也提供了包访问权限)尽管可以创建protected域,但是最好的方式还是将域保持为private;你应当一直保留“更改底层实现”的权利。然后通过protected方法来控制类的继承者的方访问权限。


7.向上转型

“为新的类提供方法”并不是集成技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的所有信息同样也可以向导出类发送。这种将子类引用转换为基类引用的动作,我们称之为向上转型。


7.1为什么成为向上转型

该术语的使用有其历史原因,并且是以传统的类继承图绘制方法为基础的:将根置于页面的顶端,然后逐渐向下。由导出类转换成基类,在继承图上是向上移动的,因此一般称为向上转型。由于向上转型是从一个较专用类型向较通用类型转换,所以总是很安全的。也就是私活,导出类是类是基类的一个超集。它可能比基类含有更多的方法,但它必须至少具备基类中所罕有的方法。在向上转型的过程中,类接口中唯一可能发生的事情就是丢失方法,而不是获取他们。这就是为什么编译器在“未明确表示转型”或“为曾指定特殊标记”的情况下,仍然允许向上转型的原因。


7.2 再论组合与继承

在面向对象编程中,生成和使用程序代码最有可能使用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象。也可以运用组合技术使用现有类来开发新的类;而继承技术其实是不太常用的。因此尽管在教授OOP的过程中我们多次强调继承,但这并不意味着要尽可能使用它。相反,应当慎用这一技术,其使用场合仅限于你确信使用该技术确实有效的情况。到底是该使用组合还是使用继承,一个最清晰的判断方法就是问一问自己是否需要从新类想基本类进行向上转型。如果必须向上转型,则继承是必要的;如果不需要,则应当好好考虑自己是否需要继承。


8.final关键字

根据上下文环境,Java的关键字final的含义存在着细微的区别,但通常它指的是“这是无法改变的”。不想做改变可能出于两种理由:设计或效率。犹豫着两个原因相差很远,所以关键字final有可能被误用。一下谈论了可能使用到final的三种情况:数据、方法和类。


8.1 final数据

与多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的。有时数据的恒定不变是很有用的,比如:

1)一个永远不该变得编译时常量

2)一个在运行时被初始化的值,而你不希望它被改变。

对于编译期常量这种情况,编译器可以将该常量值带入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时的负担。但在Java中,这类常量必须是基本数据类型,并且以关键字final表示。在对这个常量进行定义的时候,必须对其进行赋值。一个即是static又是final的域只占据一段不能改变的存储空间。当对对象引用而不是基本类型运用final时,其含义会有一点令人迷惑。对于基本类型,final使数值恒定不变;而对于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身确实可以被修改的,Java并未提供使任何对象恒定不变的途径(但可以自己编写类已取得是对象恒定不变的效果)。这一限制同样适用数组,它也是对象。我们不能因为某数据时final的就认为在编译时可以知道它的值。final数值定义为静态和非晶态的区别也只有当数值在运行时内被初始化时才会看到。


Java允许生成"空白final“,所谓”空白final“是指被生命为final但又为给定初值的域。无论什么情况下,编译器都确保空白final在使用前必须被初始化。但是,空白final在关键字finali的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却保持其恒定不变的特性。注意:必须在类的定义处或者每个构造其中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因。


Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象。


8.2 final方法

使用final方法的原因有两个。第一个原因就是把方法锁定,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中是方法行为保持不变,并且不会被覆盖。过去建议使用final方法的第二个原因是效率。在Java的早起实现中,如果讲一个方法指明为final,就是用以编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会根据自己的谨慎判断,跳过插入程序代码这种正常方式而执行方法调用机制(将参数压入栈,跳至方法代码处并执行,然后跳回并清理栈中参数,处理返回值),并且以方法体中的实际代码的副本来替代方法调用。这将消除方法调用的开销。当然,如果一个方法很大,你的程序代码就会膨胀,因而可能看不到内嵌带来的任何性能提高,因为,所带来的性能提高会因为花费于方法内的时间量而被减缩。在最近的Java版本中,虚拟机(特别是hotspot技术)可以探测到这些情况,并优化去掉这些效率反而减低的额外的内嵌调用,因此不再需要使用final方法来进行优化了。事实上,这种做法正在逐渐地收到劝阻。在使用Java SE5/6时,应该让编译器和JVM去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为final的。


类中所有的private方法都隐式地指定为是final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义。


8.3 final类

当将某个类的整体定义为final时(通过将关键字final置于它的定义前),就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计永远不需要做任何变动,或者出于安全考虑,你不希望它有子类。

请注意,final类的域可以根据个人的一元选择为是或不是final。不论类是否被定义为final,相同的规则都是用于定义为final的域。然而,由于final类禁止继承,所以final类中所有的方法痘印是指定是final的,因为无法覆盖它们。在final类中可以给方法添加final修饰词,但这不会增添任何意义。


8.4 有关final的忠告

在设计类时,将方法指明是final的,应该说是明智的。你可能会觉得,没人会想要覆盖你的方法,有时,这是对的。但请留意你的假设,要遇见类是如何被复用的一般是很困难的,特别是对于一个通用类而言更是如此。如果讲一个方法指定为final,可能会妨碍其他程序员在项目中通过继承来复用你的类,而这只是因为你没有想到它会以那种方式被运用。


9.初始化及类的加载

Java中的所有事物都是对象,每个类的编译代码都存在与他自己的独立的文件中。该文件只在需要使用程序代码时才会被加载。一般来说,可以说“了的代码在初次使用时才加载”。这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载。初次使用之处也是static初始化发生之初。所有的static对象和static代码段都会在加载时依程序中的顺序(即,定义类时的书写顺序)而一次初始化。当然,定义为static的东西只会被初始化一次。


总结:继承和组合都能从现有类型生成新类型。组合一般是将现有类型作为新类型底层实现的一部分来加以复用,而继承复用的是接口。在使用集成式,由于导出类具有基类接口,因此它可以向上转型至基类,这对多态来讲至关重要。尽管面向对象编程对继承极力强调,但在开始一个设计时,一般应该优先选择实用组合(或者可能是代理),只在确实必要时才使用继承。因为组合更具灵活性。此外,通过对成员类型使用继承技术的添加技巧,可以再运行时改变那些成员对象的类型和行为。因此,可以在运行时改变组合而成的对象的行为。



















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值