您的位置:首页 > 编程语言 > Java开发

think in java中的初始化,final,static,继承

2011-01-24 11:00 281 查看
2.6.3 static关键字
通常,我们创建类时会指出那个类的对象的外观与行为。除非用new创建那个类的一个对象,
否则实际上并未得到任何东西。只有执行了new后,才会正式生成数据存储空间,并可使用相应的方法。
但在两种特殊的情形下,上述方法并不堪用。
一种情形是只想用一个存储区域来保存一个特定的数据——无论要创建多少个对象,
甚至根本不创建对象。另一种情形是我们需要一个特殊的方法,它没有与这个类的任何对象关联。
也就是说,即使没有创建对象,也需要一个能调用的方法。为满足这两方面的要求,可使用static(静态)关键字。
一旦将什么东西设为static,数据或方法就不会同那个类的任何对象实例联系到一起。
所以尽管从未创建那个类的一个对象,仍能调用一个static方法,或访问一些static数据。
而在这之前,对于非static数据和方法,我们必须创建一个对象,并用那个对象访问数据或方法。
这是由于非static数据和方法必须知道它们操作的具体对象。当然,在正式使用前,
由于static方法不需要创建任何对象,所以它们不可简单地调用其他那些成员,同时不引用一个已命名的对象,
从而直接访问非static成员或方法(因为非static成员和方法必须同一个特定的对象关联到一起)。
有些面向对象的语言使用了“类数据”和“类方法”这两个术语。
它们意味着数据和方法只是为作为一个整体的类而存在的,并不是为那个类的任何特定对象。
有时,您会在其他一些Java书刊里发现这样的称呼。
为了将数据成员或方法设为static,只需在定义前置和这个关键字即可。
-----------------------------------------------------------------------------------------------------

1. 在构造函数中可以调用其它的构造函数,必须在开始首先调用,再去初始化其实变量。也不能调用两个以上的constructor
2. 4.4 成员初始化
4.4.1 规定初始化
4.4.2 构建器初始化
---------------------------------------------------------------------------------------------
1. 初始化顺序
在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,
那些变量仍会在调用任何方法之前得到初始化——甚至在构建器调用之前。

2. 静态数据的初始化
若数据是静态的(static),那么同样的事情就会发生;
如果它属于一个基本类型(主类型),而且未对其初始化,就会自动获得自己的标准基本类型初始值;
如果它是指向一个对象的句柄,那么除非新建一个对象,并将句柄同它连接起来,否则就会得到一个空值(NULL)。
如果想在定义的同时进行初始化,采取的方法与非静态值表面看起来是相同的。
但由于static值只有一个存储区域,所以无论创建多少个对象,
都必然会遇到何时对那个存储区域进行初始化的问题。

static初始化只有在必要的时候才会进行
初始化的顺序是首先static(如果它们尚未由前一次对象创建过程初始化),接着是非static对象。

在这里有必要总结一下对象的创建过程。请考虑一个名为Dog的类:
(1) 类型为Dog的一个对象首次创建时,或者Dog类的static方法/static字段首次访问时,Java解释器必须找到Dog.class(在事先设好的类路径里搜索)。
(2) 找到Dog.class后(它会创建一个Class对象,这将在后面学到),它的所有static初始化模块都会运行。因此,static初始化仅发生一次——在Class对象首次载入的时候。
(3) 创建一个new Dog()时,Dog对象的构建进程首先会在内存堆(Heap)里为一个Dog对象分配足够多的存储空间。
(4) 这种存储空间会清为零,将Dog中的所有基本类型设为它们的默认值(零用于数字,以及boolean和char的等价设定)。
(5) 进行字段定义时发生的所有初始化都会执行。
(6) 执行构建器。正如第6章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时候。

3. 明确进行的静态初始化
Java允许我们将其他static初始化工作划分到类内一个特殊的“static构建从句”(有时也叫作“静态块”)里。
它看起来象下面这个样子:

class Spoon {
static int i;
static {
i = 47;
}

4. 非静态实例的初始化
针对每个对象的非静态变量的初始化,Java 1.1提供了一种类似的语法格式。

---------------------------------------------------------------------------------------------

如希望句柄得到初始化,可在下面这些地方进行:
(1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。
(2) 在那个类的构建器中。
(3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。

6.9 初始化和类装载
在许多传统语言里,程序都是作为启动过程的一部分一次性载入的。随后进行的是初始化,再是正式执行程序。
在这些语言中,必须对初始化过程进行慎重的控制,保证static数据的初始化不会带来麻烦。
比如在一个static数据获得初始化之前,就有另一个static数据希望它是一个有效值,那么在C++中就会造成问题。
Java则没有这样的问题,因为它采用了不同的装载方法。由于Java中的一切东西都是对象,
所以许多活动变得更加简单,这个问题便是其中的一例。正如下一章会讲到的那样,
每个对象的代码都存在于独立的文件中。除非真的需要代码,否则那个文件是不会载入的。
通常,我们可认为除非那个类的一个对象构造完毕,否则代码不会真的载入。
由于static方法存在一些细微的歧义,所以也能认为“类代码在首次使用的时候载入”。
首次使用的地方也是static初始化发生的地方。装载的时候,
所有static对象和static代码块都会按照本来的顺序初始化(亦即它们在类定义代码里写入的顺序)。
当然,static数据只会初始化一次。

6.9.1 继承初始化
我们有必要对整个初始化过程有所认识,其中包括继承,对这个过程中发生的事情有一个整体性的概念。
请观察下述代码:

//: Beetle.java
// The full process of initialization.

class Insect {
int i = 9;
int j;
Insect() {
prt("i = " + i + ", j = " + j);
j = 39;
}
static int x1 =
prt("static Insect.x1 initialized");
static int prt(String s) {
System.out.println(s);
return 47;
}
}

public class Beetle extends Insect {
int k = prt("Beetle.k initialized");
Beetle() {
prt("k = " + k);
prt("j = " + j);
}
static int x2 =
prt("static Beetle.x2 initialized");
static int prt(String s) {
System.out.println(s);
return 63;
}
public static void main(String[] args) {
prt("Beetle constructor");
Beetle b = new Beetle();
}
} ///:~

该程序的输出如下:

static Insect.x initialized
static Beetle.x initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 63
j = 39

对Beetle运行Java时,发生的第一件事情是装载程序到外面找到那个类。在装载过程中,
装载程序注意它有一个基础类(即extends关键字要表达的意思),所以随之将其载入。
无论是否准备生成那个基础类的一个对象,这个过程都会发生(请试着将对象的创建代码当作注释标注出来,
自己去证实)。
若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,
会在根基础类(此时是Insect)执行static初始化,再在下一个衍生类执行,以此类推。
保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。
此时,必要的类已全部装载完毕,所以能够创建对象。
首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设为null。
随后会调用基础类构建器。在这种情况下,调用是自动进行的。
但也完全可以用super来自行指定构建器调用(就象在Beetle()构建器中的第一个操作一样)。
基础类的构建采用与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,
实例变量会按本来的顺序得以初始化。最后,执行构建器剩余的主体部分

--------------------------------------------------------------------------------
6.8 final关键字
由于语境(应用环境)不同,final关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明
“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇有些区别,
所以也许会造成final关键字的误用。
在接下去的小节里,我们将讨论final关键字的三种应用场合:数据、方法以及类。

6.8.1 final数据
许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:
(1) 编译期常数,它永远不会改变
(2) 在运行期初始化的一个值,我们不希望它发生变化
对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,
计算可在编译期间提前执行,从而节省运行时的一些开销。在Java中,
这些形式的常数必须属于基本数据类型(Primitives),而且要用final关键字进行表达。
在对这样的一个常数进行定义的时候,必须给出一个值。
无论static还是final字段,都只能存储一个数据,而且不得改变。
若随同对象句柄使用final,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。
对于基本数据类型,final会将值变成一个常数;但对于对象句柄,final会将句柄变成一个常数
。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。
然而,对象本身是可以修改的。Java对此未提供任何手段,可将一个对象直接变成一个常数(但是,
我们可自己编写一个类,使其中的对象具有“常数”效果)。这一限制也适用于数组,它也属于对象。
2. 空白final
Java 1.1允许我们创建“空白final”,它们属于一些特殊的字段。尽管被声明成final,
但却未得到一个初始值。无论在哪种情况下,空白final都必须在实际使用前得到正确的初始化。
而且编译器会主动保证这一规定得以贯彻。然而,对于final关键字的各种应用,空白final具有最大的灵活性。
举个例子来说,位于类内部的一个final字段现在对每个对象都可以有所不同,同时依然保持其“不变”的本质。
3. final自变量
Java 1.1允许我们将自变量设成final属性,方法是在自变量列表中对它们进行适当的声明。
这意味着在一个方法的内部,我们不能改变自变量句柄指向的东西。

注意此时仍然能为final自变量分配一个null(空)句柄,同时编译器不会捕获它。
这与我们对非final自变量采取的操作是一样的。
我们只能读取自变量,不可改变它。

6.8.2 final方法
之所以要使用final方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,
防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,
而且不可被覆盖或改写,就可以采取这种做法。
采用final方法的第二个理由是程序执行的效率。将一个方法设成final后,
编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final方法调用,
就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;
跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,
它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。
当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。
因为任何提升都被花在方法内部的时间抵消了。Java编译器能自动侦测这些情况,并颇为“明智”
地决定是否嵌入一个final方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。
通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。
类内所有private方法都自动成为final。由于我们不能访问一个private方法,
所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。
可为一个private方法添加final指示符,但却不能为那个方法提供任何额外的含义。

6.8.3 final类
如果说整个类都是final(在它的定义前冠以final关键字),就表明自己不希望从这个类继承,
或者不允许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;
或者出于安全方面的理由,我们不希望进行子类化(子类处理)。
除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。

注意数据成员既可以是final,也可以不是,取决于我们具体选择。应用于final的规则同样适用于数据成员,
无论类是否被定义成final。将类定义成final后,结果只是禁止进行继承——没有更多的限制。然而,
由于它禁止了继承,所以一个final类中的所有方法都默认为final。因为此时再也无法覆盖它们。
所以与我们将一个方法明确声明为final一样,编译器此时有相同的效率选择。
可为final类内的一个方法添加final指示符,但这样做没有任何意义。

6.8.4 final的注意事项
设计一个类时,往往需要考虑是否将一个方法设为final。可能会觉得使用自己的类时执行效率非常重要,
没有人想覆盖自己的方法。这种想法在某些时候是正确的。
但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。
常规用途的类尤其如此。若将一个方法定义成final,
就可能杜绝了在其他程序员的项目中对自己的类进行继承的途径,因为我们根本没有想到它会象那样使用。
标准Java库是阐述这一观点的最好例子。其中特别常用的一个类是Vector。如果我们考虑代码的执行效率,
就会发现只有不把任何方法设为final,才能使其发挥更大的作用。
我们很容易就会想到自己应继承和覆盖如此有用的一个类,但它的设计者却否定了我们的想法。
但我们至少可以用两个理由来反驳他们。首先,Stack(堆栈)是从Vector继承来的,
亦即Stack“是”一个Vector,这种说法是不确切的。其次,对于Vector许多重要的方法,
如addElement()以及elementAt()等,它们都变成了synchronized(同步的)。
正如在第14章要讲到的那样,这会造成显著的性能开销,可能会把final提供的性能改善抵销得一干二净。
因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,
真不敢想象会在程序员里引发什么样的情绪。
另一个值得注意的是Hashtable(散列表),它是另一个重要的标准类。该类没有采用任何final方法。
正如我们在本书其他地方提到的那样,显然一些类的设计人员与其他设计人员有着全然不同的素质
(注意比较Hashtable极短的方法名与Vecor的方法名)。对类库的用户来说,这显然是不应该如此轻易就能看出的。
一个产品的设计变得不一致后,会加大用户的工作量。
这也从另一个侧面强调了代码设计与检查时需要很强的责任心。
--------------------------------------------------------------------------------------------------

7.6 内部类
--------------------------------------------------------------------
在Java 1.1中,可将一个类定义置入另一个类定义中。这就叫作“内部类”。
内部类对我们非常有用,因为利用它可对那些逻辑上相互联系的类进行分组,
并可控制一个类在另一个类里的“可见性”。然而,
我们必须认识到内部类与以前讲述的“合成”方法存在着根本的区别。
通常,对内部类的需要并不是特别明显的,至少不会立即感觉到自己需要使用内部类。
在本章的末尾,介绍完内部类的所有语法之后,大家会发现一个特别的例子。
通过它应该可以清晰地认识到内部类的好处。
创建内部类的过程是平淡无奇的:将类定义置入一个用于封装它的类内部

若想在除外部类非static方法内部之外的任何地方生成内部类的一个对象,
必须将那个对象的类型设为“外部类名.内部类名”,

7.6.1 内部类和上溯造型
迄今为止,内部类看起来仍然没什么特别的地方。
毕竟,用它实现隐藏显得有些大题小做。
Java已经有一个非常优秀的隐藏机制——只允许类成为“友好的”(只在一个包内可见),
而不是把它创建成一个内部类。
然而,当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,
内部类就开始发挥其关键作用(从用于实现的对象生成一个接口句柄具有与上溯造型至一个基础类相同的效果)。
这是由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此。
所以我们可以非常方便地隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,
而且甚至有可能不知道准确的类型。

7.6.2 方法和作用域中的内部类
至此,我们已基本理解了内部类的典型用途。对那些涉及内部类的代码,
通常表达的都是“单纯”的内部类,非常简单,且极易理解。
然而,内部类的设计非常全面,

不可避免地会遇到它们的其他大量用法——假若我们在一个方法甚至一个任意的作用域内创建内部类。
有两方面的原因促使我们这样做:
(1) 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。
(2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。

在下面这个例子里,将修改前面的代码,以便使用:
(1) 在一个方法内定义的类
(2) 在方法的一个作用域内定义的类
(3) 一个匿名类,用于实现一个接口
(4) 一个匿名类,用于扩展拥有非默认构建器的一个类
(5) 一个匿名类,用于执行字段初始化
(6) 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构建器)

7.6.4 static内部类
为正确理解static在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对象的句柄。
然而,假如我们说一个内部类是static的,这种说法却是不成立的。static内部类意味着:
(1) 为创建一个static内部类的对象,我们不需要一个外部类对象。
(2) 不能从static内部类的一个对象中访问一个外部类对象。
但在存在一些限制:由于static成员只能位于一个类的外部级别,
所以内部类不可拥有static数据或static内部类。
倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static。
为了能正常工作,同时也必须将内部类设为static

7.7 构建器和多形性
同往常一样,构建器与其他种类的方法是有区别的。在涉及到多形性的问题后,这种方法依然成立。
尽管构建器并不具有多形性(即便可以使用一种“虚拟构建器”——将在第11章介绍),
但仍然非常有必要理解构建器如何在复杂的分级结构中以及随同多形性使用。
这一理解将有助于大家避免陷入一些令人不快的纠纷。

7.7.1 构建器的调用顺序
构建器调用的顺序已在第4章进行了简要说明,但那是在继承和多形性问题引入之前说的话。
用于基础类的构建器肯定在一个衍生类的构建器中调用,而且逐渐向上链接,
使每个基础类使用的构建器都能得到调用。之所以要这样做,是由于构建器负有一项特殊任务:
检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员
(这些成员通常都具有private属性)。
只有基础类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。
所以,必须令所有构建器都得到调用,否则整个对象的构建就可能不正确。
那正是编译器为什么要强迫对衍生类的每个部分进行构建器调用的原因。
在衍生类的构建器主体中,若我们没有明确指定对一个基础类构建器的调用,
它就会“默默”地调用默认构建器。如果不存在默认构建器,
编译器就会报告一个错误(若某个类没有构建器,编译器会自动组织一个默认构建器)。

对于一个复杂的对象,构建器的调用遵照下面的顺序:
(1) 调用基础类构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,
然后是下一个衍生类,等等。直到抵达最深一层的衍生类。
(2) 按声明顺序调用成员初始化模块。
(3) 调用衍生构建器的主体。

构建器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,
并且能访问基础类的任何public和protected成员。这意味着当我们在衍生类的时候,
必须能假定基础类的所有成员都是有效的。采用一种标准方法,构建行动已经进行,
所以对象所有部分的成员均已得到构建。但在构建器内部,必须保证使用的所有成员都已构建。
为达到这个要求,唯一的办法就是首先调用基础类构建器。然后在进入衍生类构建器以后,
我们在基础类能够访问的所有成员都已得到初始化。此外,
所有成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比如上例中的b,c和l),
由于我们应尽可能地对它们进行初始化,所以也应保证构建器内部的所有成员均为有效。
若坚持按这一规则行事,会有助于我们确定所有基础类成员以及当前对象的成员对象均已获得正确的初始化。
但不幸的是,这种做法并不适用于所有情况,这将在下一节具体说明。

前一节讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的:
(1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
(2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构建器调用之前),此时会发现radius的值为0,这是由于步骤(1)造成的。
(3) 按照原先声明的顺序调用成员初始化代码。
(4) 调用衍生类构建器的主体。

采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐