1、空白final
Java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。无论什么 情况,编译器都确保空白final在使用前被初始化。但是,空白final在关键字final的使用上提供了更大的灵活性,为此,一个类中的final域可以做到根据对象而有所不同,却又保持其恒大不变的特性。
2、final方法
使用final方法的原因有2个。第一个原因是把方法锁定,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。
过去建议使用final方法的第二个原因是效率。在Java的早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会根据自己的谨慎判断,跳过插入程序代码这种正常方式而执行方法调用机制(将参数压入栈,跳至方法代码处并执行,然后跳回并清理栈中的参数,处理返回值),并且以方法体中的实际代码的副本来替代方法调用。这将消除方法调用的开销。当然 ,如果一个方法很大,你的程序代码会膨胀,因而可能看不到内嵌带来的任何性能提高,因为,所带来的性能提高会因为花费于方法内的时间量而被缩减。
虚拟机(特别是hotspot技术)可以探测这些情况,并优化去掉这些效率反而降低的额外的内嵌调用。因而不再需要使用final方法来进行优化。在使用Java SE 5/6时,应该让编译器和JVM去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为final的。
3、final和private关键字
类中所有的private方法都隐式指定为是final的。由于无法取用private方法,所以也就无法覆盖它,可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义。
4、覆盖
覆盖只在某方法是基类的接口的一部分时才出现。必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,他就不是基类的接口的一部分。它仅是一些隐藏于类中的程序代码,只不过是具有相同的名称而已。如果在导出类中以相同的每次生成一个public、protected或包访问权限方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况。此时你并没有覆盖该方法,仅是生成了一个新的方法。由于private无法触及而且能有效隐藏,所有除了把它看成是因为它所归属的类的组织结构的原因而存在外,其他任何事物都不需要考虑它。
5、final类 没有子类
当将某个类的整体定义为final时(通过将关键字置于它的定义之前),就表名了你不打算继承该类,而且也不允许别人这样做。换句话说,你对该类的设计永不需要做任何变动,或者出于安全的考虑,你不希望它有子类。
final类中可以给方法添加final修饰词,但这不会增添任何意义。
6、有关final的忠告
如果将一个方法指定为final,可能会妨碍其他程序员在项目中通过继承来复用你的类,而这知识因为你没有想到它会以那种方式被运用。
Java标准库中的Java 1.0/1.1中Vector被广泛运用,人们可能会想要继承并覆盖如此继承并好用的类,但是设计者认为这不太合适。这里有2个原因。第一:stack继承自Vector,就是说Stack是个Vector,这从逻辑的的观点看是不正确的。尽管如此,Java的设计者们自己仍旧继承了Vector。在以这种方式创建Stack时,他们意识到了Final方法显得过于严苛了。
第二个原因:Vector的许多最重要的方法如addElement()和elementAt()是同步的。这将导致很大的执行开销,可能会抹杀final所带来的好处。这种情况增强了人们关于程序员无法正确猜测优化应当发生于何处的观点。幸运的是我们现代Java容器库用ArrayList替代了Vector。
hashTable也是一个重要的java1.0/1.1标准类库,而且不含任何final方法,方法相对于Vector中的方法要简洁很多。对于使用者来说,这是一个本不该如此轻率的事物,这种不规则的情况只能使用户付出更多的努力。现代Java的容器用HashMap替代了HashTable
7、多态
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能创建可扩展的程序————即无论在项目最初创建时还是在需要添加新功能时都可以“生长”的程序。。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。而多态的作用则是消除类型之间的耦合关系。继承允许将对象视为它自己本身的类型或其基类型来加以处理。它允许将多种类型(从同一基类导出的)视为同一类型来处理,而同一份代码也就可以毫无差别地允许在这些不同类型之上了。多态方法调用允许一种类型表现出与其他类型之间的区别,只要它们都是从同一基类导出的。这种区别是根据方法行为的不同而表示出来的,虽然这些方法都可以通过一个基类来调用。
多态 也称作动态绑定、后期绑定或运行时绑定。
8、方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。C只有一种方法调用就是前期绑定。
为了解决具体调用哪一个子类的方法,解决的办法为后期绑定,它的含义就是 在运行时根据对象的类型进行绑定。后期绑定 也叫做动态绑定或运行时绑定。后期绑定需要在对象中安置某种“类型信息”来找到正确的方法体,并加一调用。
Java中除了static方法和final方法(private属于final方法)之外,其他都是后期绑定。通常情况下,我们不必判断释放应该进行后期绑定——它会自动发生。
为什么要将某个方法声明为final?它不仅可以防止其他人覆盖该方法。更重要的一点或许是:这样可以有效地“关闭”动态绑定,或者说告诉编译器不需要对其进行动态绑定。这样,编译器可以为final方法调用生成更有效的代码。然而,大多数情况下对程序的整体性能不会有什么改观,最好根据设计而不是实体提高性能的目的来使用final。
9、多态的缺陷:“覆盖”私有方法
由于private方法被自动认为是final方法,而且对导出类是屏蔽的。因此,在这种情况下,子类中的private方法就是一个全新的方法;既然基类的private方法在子类中不可见,因此甚至也不能被重载【1.8编译报错】
只有非private方法才可以被覆盖,但是还需要密切注意覆盖private方法的现象。在导出类中,对于基类中的private方法,最好采用不同的名字。
10、多态的缺陷:域与静态方法
只有普通的方法调用是可以多态的。如果直接访问某个域,这个访问就将在编译器进行解析。
任何域访问操作都将由编译器解析,因此不是多态的。如果同一个域变量在父分类和子类同时存在,子类在多态方式创建的会会同时拥有父类和子类的域变量。为了得到父类的域变量,必须显式指明super.域对象。
尽管看起来容易混淆,但是在实践中,我们通常会将所有的域设置成private,因此不能直接访问它们,其副作用是只能调用方法 访问它们。而且我们不会对基类中的域和导出类的域赋予相同的名字,这种做法容易令人混淆。
如果某个方法方法是静态的,它的行为就不具有多态性。静态方法是与类,而并非与单个的对象相关联的。
11、构造器和多态
通常构造器不同于其他种类的方法。构造器并不具有多态性(他们实际上是static方法,只不过static声明是隐式)。
12、构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限来当自己的元素进行初始化。因此,必须令所有构造器都得到调用,否则就不可能正确地构造完整对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果没有明确指定调用某个基类构造器,他就会默默地调用默认构造器。如果不存在默认构造器,编译器会报错(若某个类没有构造器,编译器会自动合成一个默认构造器)
调用构造器的顺序:
12.1、调用基类构造器。这个步骤会不断低反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最底层的导出类。
12.2、按声明顺序调用成员的初始化方法
12.3、调用导出类构造器的主体。
构造器的调用顺序是很重要的。当进行继承时,我们已经知道基类的一切,并且可以访问基类中任何声明为public和protected的成员。这意味着在导出类中,必须假定基类的所有成员都是有效的。一种标准方法是,构造动作一经发生,那么对象所有部分的全体成员都会得到构建。然后,在构造器内部,我们必须确保所要使用的成员都已经构建完毕。为确保这一目的,唯一的办法就是首先调用基类构造器。那么在进入导出类构造器时,在基类中可供我们访问的成员都已经得到初始化。此外,知道构造器 中所有成员都有效也是因为,当成员对象在类内进行定义的时候,只要有可能,就应该对它们进行初始化(也就是,通过组合方法将对象置于类内)。若遵循这一规则,那么就能保证所有基类成员已经当前对象的成员对象都被初始化了。但遗憾的是,这一做法不适用于所有情况。
13、构造器内部的多态方法的行为
在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法按所在的那个类,还是属于那个类的导出类。
如果要调用构造器内部的一个动态绑定方法,就要用到那个方法被覆盖后的定义。然后,这个调用的效果可能难于预料,因为被覆盖的方法的对象被完全构造之前就会被调用。这可能会造成一些难于发现的隐藏错误。
构造器的工作事件上是创建对象。在任何构造器内部,整个对象可能只是部分形成——我们只知道基类对象已经初始化。如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么导出部分在当前构造器正在被调用的时空仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么就可能会调用某个方法,而这个方法所操作的成员可能还未进行初始化。
Father.draw()方法设计为将要被覆盖,这种覆盖是在Son中发生的。但是Father的构造器会调用这个方法,结构导致了对Son.darw()的调用。但是输出结果我们会发现当Father的构造器调用draw()方法时,rad不是默认值,而是0.
初始化的实际过程:
13.1、在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
13.2、如前所属那样调用基类构造器。此时,调用被覆盖后的draw()(要在Son构造器之前调用),由于步骤1的缘故,我们此时会发现rad的值为0.
13.3、安装声明的顺序调用成员的初始化方法。
13.4、调用导出类的构造器主体。
这样做有一个优点,那就是所有东西都至少初始化成零(或者是某些特殊数据类型中与 “零”等价的值),而不是仅仅留作垃圾。其中 包括通过“组合”而嵌入一个类内部的对象引用,其值为null。如果忘记为该引用进行初始化,就会在运行时出现异常。
编写构造器有一条有效的准则:用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法。在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法,他们自动属于final方法)这些方法不能被覆盖,因此也就不会出现上述问题。
14、用继承进行设计
继承在编译时就需要知道确切类型
中间对象包含了对一个基类的引用,基类被初始化其中一个超类。超类的特有方法会产生某种特殊行为。既然引用在运行时可以与另一个超类重新绑定起来,所以基类的另一个超类可以替换初始化的超类,然后这个方法也会产生不同的行为,这样,我们在运行期间获得了动态灵活性(这也称作动态模式)。于此相反,我们不能在允许期间决定继承不同的对象,因为他要求在编译期间完全确定下来。
一条同样的准则:用继承表达行为间的差异,并用字段表达状态上的变化。通过继承得到了两个不同的类,用于表达方法的差异,而中间对象通过运用组合使自己的状态发生变化。在这种情况下,这种状态的改变也就产生了行为的改变。
构造器内部的多态方法的行为
public class Father {
Father() {
System.out.println("before draw");
draw();
System.out.println("after draw");
}
void draw() {
}
public static void main(String[] args) {
new Son(5);
}
}
class Son extends Father {
int rad = 1;
Son() {
System.out.println("init");
}
Son(Integer i) {
rad = i;
System.out.println("rad+=" + rad);
}
@Override
void draw(){
System.out.println("rad+=" + rad);
}
}