protected,累积开发,final,初始化和类装载

一、protected

在理想情况下,private成员随时都是“私有”的,任何人不得访问。但在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问衍生类的成员。

protected关键字可帮助我们做到这一点。它的意思是“它本身是私有的,但可由从这个类继承的任何东西或者同一个包内的其他任何东西访问”。也就是说,Java中的protected会成为进入“友好”状态。

我们采取的最好的做法是保持成员的private状态——无论如何都应保留对基 础的实施细节进行修改的权利。在这一前提下,可通过protected方法允许类的继承者进行受到控制的访问。

举个例子:

protected修饰的方法可以被子类使用,private修饰的则不能。

二、累积开发

继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。

这样可将新错误隔离到新代码里。通过从一个现成的、功能性的类继承,同时增添成员新的数据成员及方法(并重新定义现有方法),我们可保持现有代码原封不动(另外有人也许仍在使用它),不会为其引入自己的编程错误。一旦出现错误,就知道它肯定是由于自己的新代码造成的。

这样一来,与修改现有代码的主体相比,改正错误所需的时间和精力就可以少很多。

类的隔离效果非常好,这是许多程序员事先没有预料到的。甚至不需要方法的源代码来实现代码的再生。

大家要记住这样一个重点:程序开发是一个不断递增或者累积的过程,就象人们学习知识一样。当然可根据要求进行尽可能多的分析,但在一个项目的设计之初,谁都不可能提前获知所有的答案。如果能将自己的项目看作一个有机的、能不断进步的生物,从而不断地发展和改进它,就有望获得更大的成功以及更直接的反馈。

尽管继承是一种非常有用的技术,但在某些情况下,特别是在项目稳定下来以后,仍然需要从新的角度考察自己的类结构,将其收缩成一个更灵活的结构。请记住,继承是对一种特殊关系的表达,意味着“这个新类属于那个旧类的一种类型”。我们的程序不应纠缠于一些细树末节,而应着眼于创建和操作各种类型的对象,用它们表达出来自“问题空间”的一个模型。

我的理解:

如果使用继承,可以很快发现自己代码中的错误。如果出现新的问题,必然在子类中发生。但是,为了优化项目,可以从新的角度考察自己的类结构,也许可以将一些继承进行精简。

三、上溯造型

继承最值得注意的地方就是它没有为新类提供方法。继承是对新类和基础类之间的关系的一种表达。可这样总结该关系:“新类属于现有类的一种类型”。

这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。

这里的tune()方法为什么可以调用Wind对象?

我们一定要认识到一个Wind对象也是一个Instrument对象。而且对于不在Wind中的一个Instrument(乐器),没有方法可以由tune()调用。在tune()中,代码适用于Instrument以及从Instrument衍生出来的任何东西。

在这里,我们将从一个Wind句柄转换成一个Instrument句柄的行为叫作“上溯造型”。

我的理解:

这个知识点是我没有掌握的。在tune()中,代码适用于Instrument以及从Instrument衍生出来的任何东西。即是说,子类可以代替父类对象进行实现,即是上溯造型。我以前对这种特性用得很少,之后可以好好考虑一下如何去使用这种特性。

(1)何谓“上溯造型”?

1

由于造型的方向是从衍生类到基础类,箭头朝上,所以通常把它叫作“上溯造型”,即Upcasting。

上溯造型肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。换言之,衍生类是基础类的一个超集。它可以包含比基础类更多的方法,但它至少包含了基础类的方法。进行上溯造型的时候,类接口可能出现的唯一一个问题是它可能丢失方法,而不是赢得这些方法。这便是在没有任何明确的造型或者其他特殊标注的情况下,编译器为什么允许上溯造型的原因所在。

也可以执行下溯造型,但这时会面临第11章要详细讲述的一种困境。

(2)再论合成与继承

在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是:将数据和方法统一封装到一个类里,并且使用那个类的对象。

有些时候,需通过“合成”技术用现成的类来构造新类。而继承是最少见的一种做法。

因此,尽管继承在学习OOP的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。相反,使用它时要特别慎重。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。

为判断自己到底应该选用合成还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。若必须上溯,就需要继承。但如果不需要上溯造型,就应提醒自己防止继承的滥用。

在下一章里(多形性),会向大家介绍必须进行上溯造型的一种场合。但只要记住经常问自己“我真的需要上溯造型吗”,对于合成还是继承的选择就不应该是个太大的问题。

我的理解:

这里的概念,我理解得还不够透彻。对于继承,我是是这样判断是否使用的:这个子类和父类很“相似”,子类中很多方法/字段是和父类重复的,如果新建一个类似乎有一种浪费资源的嫌疑,所以干脆就继承之。关于这里由“上溯造型”而判断是否使用继承的概念,我还是不是很理解。

四、final关键字

由于语境(应用环境)不同,final关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇有些区别,所以也许会造成final关键字的误用。

常用于:数据、方法以及类。

(1)final数据

许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:

  1. 编译期常数,它永远不会改变。
  2. 在运行期初始化的一个值,我们不希望它发生变化。

对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。

也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在Java中,这些形式的常数必须属于基本数据类型(Primitives),而且要用final关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个值。

无论static还是final字段,都只能存储一个数据,而且不得改变。

若随同对象句柄使用final,而不是基本数据类型,它的含义就稍微让人有点迷糊了。对于基本数据类型,final会将值变成一个常数;但对于对象句柄,final会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有“常数”效果)。这一限制也适用于数组,它也属于对象。

final亦适用于数组,后者只不过是另一种类型的句柄而已。将句柄变成final看起来似乎不如将基本数据类型变成final那么有用。

我的理解:

有几个关键词。

1.编译期常数:

基本数据类型,如果使用final关键字进行修饰,可以在编译期间进行初始化,可以节省运行时动态创建的开销。在进行常数的定义时,必须给定一个值,否则报错。

2.static和final字段只能存储一个数据:

static声明的字段,在内存中只有一个拷贝(节省内存),jvm只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。static只能在同一内存中进行创建、读取和修改,因此只能存储一个数据。

final声明的字段,在内存中是只可读取,不可修改,因此只能存储一个数据。

关于常数必须进行初始化不可修改,这里有一个例子:

3.final句柄

对于基本数据类型,final会将值变成一个常数;但对于对象句柄,final会将句柄变成一个常数。

进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。

关于final句柄,这里有个例子:

最后文章还说:将句柄变成final看起来似乎不如将基本数据类型变成final那么有用。这个final有时候还是有用的,要看实际使用情况。

(2)空白final

Java 1.1允许我们创建“空白final”,它们属于一些特殊的字段。尽管被声明成final,但却未得到一个初始值。

无论在哪种情况下,空白final都必须在实际使用前得到正确的初始化。而且编译器会主动保证这一规定得以贯彻。

然而,对于final关键字的各种应用,空白final具有最大的灵活性。举个例子来说,位于类内部的一个final字段现在对每个对象都可以有所不同,同时依然保持其“不变”的本质。

我的理解:

不用在声明字段时就马上进行赋值

可以延迟赋值,但是在实际使用之前一定要被初始化。

反正要在类里面完成初始化工作,不然就报错。

(3)final自变量

Java 1.1允许我们将自变量设成final属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一个方法的内部,我们不能改变自变量句柄指向的东西。

我的理解:

这里的自变量就是函数中的自变量(参数)哦。

大概是这样的用法:

这就保证了使用的对象一定是传入的对象,不可进行修改。

(4)final方法

之所以要使用final方法,可能是出于对两方面理由的考虑。

第一个是为方法“上锁”,防止任何继承类改变它的本来含义。

设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。

第二个理由是程序执行的效率。

将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。

只要编译器发现一个final方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。

相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。

当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。

类内所有private方法都自动成为final。由于我们不能访问一个private方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。可为一个private方法添加final指示符,但却不能为那个方法提供任何额外的含义。

我的理解:

关于这里我只能浅略的说说我的理解。

为什么要使用final方法呢?第一种考虑是保持方法的不变,这个是上文一直在进行强调的用法。重要的是第二种:提高效率,这个是我没有掌握的。

只要编译器发现一个final方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。

这个是final方法的执行流程,我感觉这样做不是去调用方法,而是“把方法拆出来了”,把方法中的过程放到了常规代码中进行执行。但是这样做一定会节省开销吗?“把方法拆出来”这个步骤,相对于直接去调用方法,又会有多少收益呢?

为了回答这个问题,书中又说:

若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。幸好,Java编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final方法。也就是说,final方法是否会如同想象一般地执行,还是个未知数,这是由java编译器进行判断的。

之后文章给出了一个建议:

只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。这是很明智的,因为在开销很小的方法中,比起去调用方法,直接执行往往效率更高。

最后文章讲明了private和final的关系:类内所有private方法都自动成为final。

由于我们不能访问一个private方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。private意味着此方法在类外不可访问(调用),自然代表着方法不可重写,而final意味着方法不可修改,两个关键词的实现效果是类似的,所以说,如果为一个已经是private的方法加上final,是起不到什么额外的效果的。

(5)final类

如果说整个类都是final(在它的定义前冠以final关键字),就表明自己不希望从这个类继承,或者不允许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出于安全方面的理由,我们不希望进行子类化(子类处理,即有其他类去继承final类)。

除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效(这里就和final方法中的思想差不多)

举个例子:

注意数据成员既可以是final,也可以不是,取决于我们具体选择。

应用于final的规则同样适用于数据成员,无论类是否被定义成final。将类定义成final后,结果只是禁止进行继承——没有更多的限制。然而,由于它禁止了继承,所以一个final类中的所有方法都默认为final。因为此时再也无法覆盖它们。所以与我们将一个方法明确声明为final一样,编译器此时有相同的效率选择。

可为final类内的一个方法添加final指示符,但这样做没有任何意义。

我的理解:

要注意,字段在final声明的class中,并不意味着这个字段也是final的,就像上面那个例子,还是可以进行修改的。但是方法就不一样了,因为final类无法被继承,也就是说final类中的方法没有办法被覆盖了(没办法修改了)。

(6)final的注意事项

设计一个类时,往往需要考虑是否将一个方法设为final。可能会觉得使用自己的类时执行效率非常重要,没有人想覆盖自己的方法。这种想法在某些时候是正确的。

但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的类尤其如此。若将一个方法定义成final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途径,因为我们根本没有想到它会象那样使用。

我的理解:

我基本上是没有用过final类的…其实这里涉及到一个重用的问题。

五、初始化和类装载

在许多传统语言里,程序都是作为启动过程的一部分一次性载入的。随后进行的是初始化,再是正式执行程序。在这些语言中,必须对初始化过程进行慎重的控制,保证static数据的初始化不会带来麻烦。比如在一个static数据获得初始化之前,就有另一个static数据希望它是一个有效值,那么在C++中就会造成问题。

Java则没有这样的问题,因为它采用了不同的装载方法。由于Java中的一切东西都是对象,所以许多活动变得更加简单,这个问题便是其中的一例。正如下一章会讲到的那样,每个对象的代码都存在于独立的文件中。除真的需要代码,否则那个文件是不会载入的。通常,我们可认为除非那个类的一个对象构造完毕,否则代码不会真的载入。由于static方法存在一些细微的歧义,所以也能认为“类代码在首次使用的时候载入”。

首次使用的地方也是static初始化发生的地方。装载的时候,所有static对象和static代码块都会按照本来的顺序初始化(亦即它们在类定义代码里写入的顺序)。当然,static数据只会初始化一次。

我的理解:

又是一个老生常谈的对象初始化的问题。这里有个概念:每个对象的代码都存在于独立的文件中。除真的需要代码,否则那个文件是不会载入的。通常,我们可认为除非那个类的一个对象构造完毕,否则代码不会真的载入。我觉得这个和java的动态加载特性这的很想(也许就是一回事,暂时还没弄清楚,回头再解决这个概念问题)。

(1)继承初始化

我们有必要对整个初始化过程有所认识,其中包括继承,对这个过程中发生的事情有一个整体性的概念。

举个例子:

对Beetle运行Java时,发生的第一件事情是装载程序到外面找到那个类。在装载过程中,装载程序注意它有一个基础类(即extends关键字要表达的意思),所以随之将其载入。无论是否准备生成那个基础类的一个对象,这个过程都会发生(请试着将对象的创建代码当作注释标注出来,自己去证实)。

若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(此时是Insect)执行static初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。

此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设为null。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完全可以用super来自行指定构建器调用(就象在Beetle()构建器中的第一个操作一样)。基础类的构建采用与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构建器剩余的主体部分。

我的理解:

我打算新开一篇文章来详细谈谈我对这个例子的看法。

六、总结

这一章的概念也是非常重要,在实际开发中运用得特别广泛。特别是引入继承后,开发变得更加清晰便捷,但是对象的初始化过程变得更加复杂,需要详细理解。下次我想要新开一篇文章来谈谈我对最后这个例子的看法。

发表评论

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