7.7 向上转型
“为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。
这种描述并非只是一种解释继承的华丽方式,这直接是由语言所支撑的。例如,假设有一个成为Instrument的代表乐器的基类和一个称为Wind的导出类。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的所有信息同样也可以向导出类发送。如果Instrument类具有一个play()方法,那么wind乐器也将同样具备。这意味着我们可以准确地说Wind对象也是一种类型的Instrument。
//: reusing/Wind.java // Inheritance & upcasting. classInstrument { publicvoidplay() {} staticvoidtune(Instrumenti) { // ... i.play(); } } // Wind objects are instruments // because they have the same interface: publicclassWindextendsInstrument { publicstaticvoidmain(String[] args) { Windflute=newWind(); Instrument.tune(flute); // Upcasting } }
///:~
在此例中,tune()方法可以接受Instrument引用,这实在太有趣了。但是Wind.main()中,传递给tune()方法的是一个wind引用。鉴于Java对类型检查十分妍哥,接受某种类型的方法同样可以接受另外一种类型就会显得很奇怪,除非你认识到Wind对象同样也是一种Instrument对象,而且也不存在任何tune()方法是可以通过Instrument来调用,同时又不存在于Wind中。在tune()中,程序代码可以对Instrument和它所有的导出类起作用。这种将Wind引用转换为Instrument引用的动作,我们称之为向上转型。
7.7.1 为什么称为向上转型
该术语的使用有其历史原因,并且是以传统的继承图的绘制方法为基础的:将根置于页面的顶端,然后逐渐向下。于是,Wind的集成图就如下:
由导出类转型成基类,在继承图上是向上移动的,因此,一般称为向上转型。由于向上转型是从一个比较专用类型向比较通用类型转型,所以总是很安全的。也就是说,导出类是基类的一个超集。它可能比基类含有更多的方法,但是它必须至少具备基类中所含有的方法。在向上转型的过程中,类接口中唯一可能发生的事情就丢失方法,而不是获取它们。这就是为什么编译器在“未曾明确表示转型”或“未曾指定特殊标记”的情况下,仍然允许向上转型的原因。
7.7.2 再论组合与继承
在面向对象编程中,生成和使用程序代码最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象。也可以运用组合技术使用现有类来开发新的类;而继承技术其实是不太常用的。因此尽管在教授OOP的过程中我们多次强调继承,但是这并不意味着要尽可能使用它。相反,应当慎用这一技术,其使用场景仅限于你确信使用该技术确实有效的情况。到底是该使用组合还是使用继承,一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的;但是如果不需要,则应好好考虑自己是否需要继承。
7.8 final关键字
根据上下文环境,Java的关键字final的含义存在着细微的区别,但是通常它指的是“这是无法改变的。”不想做改变可能出于两种理由:设计或效率。由于这两个原因相差很远,所以关键字final有可能被误用。以下几节谈论了可能使用到final的三种情况:数据、方法和类。
7.8.1 final数据
许多编程语言都有某种方法,来向编译器告知一块数据是恒大不变的。有时数据的恒定不变是很有用的,比如:
1、一个永不改变的编译时常量
2、一个在运行时被初始化的值,而你不希望它被改变。
对于编译期常量这种情况,编译器可以将该常量值代入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时的负担。在Java中,这类常量必须是基本数据类型,并且以关键字final表示。在对这个常量进行定义的时候,必须对其进行赋值。
一个既是static又是final的域只占据一段不能改变的存储空间。
当对对象引用而不是基本类型运用final时,其含义会有一点令人迷惑。对于基本类型,final使数值恒定不变;而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改变指向另一个对象。然后,对象其自身确是可以被修改的,Java并未提供使任何对象恒定不变的途径。这一限制同样适用数组,它也是对象。
importjava.util.*; importstaticnet.mindview.util.Print.*; classValue { inti; // Package access publicValue(inti) { this.i=i; } } publicclassFinalData { privatestaticRandomrand=newRandom(47); privateStringid; publicFinalData(Stringid) { this.id=id; } // Can be compile-time constants: privatefinalintvalueOne=9; privatestaticfinalintVALUE_TWO=99; // Typical public constant: publicstaticfinalintVALUE_THREE=39; // Cannot be compile-time constants: privatefinalinti4=rand.nextInt(20); staticfinalintINT_5=rand.nextInt(20); privateValuev1=newValue(11); privatefinalValuev2=newValue(22); privatestaticfinalValueVAL_3=newValue(33); // Arrays: privatefinalint[] a= { 1, 2, 3, 4, 5, 6 }; publicStringtoString() { returnid+": "+"i4 = "+i4+", INT_5 = "+INT_5; } publicstaticvoidmain(String[] args) { FinalDatafd1=newFinalData("fd1"); //! fd1.valueOne++; // Error: can’t change value fd1.v2.i++; // Object isn’t constant! fd1.v1=newValue(9); // OK -- not final for(inti=0; i<fd1.a.length; i++) fd1.a[i]++; // Object isn’t constant! //! fd1.v2 = new Value(0); // Error: Can’t //! fd1.VAL_3 = new Value(1); // change reference //! fd1.a = new int[3]; print(fd1); print("Creating new FinalData"); FinalDatafd2=newFinalData("fd2"); print(fd1); print(fd2); } }
/* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*///:~
按照惯例,既是static又是final的域将用大写表示,并使用下划线分割各个单词。
Java允许生成”空白final”,所谓空白final是指被声明为final但又未给定初始值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化。但是,空白final在关键字final的使用上提供更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保持其恒定不变的特性。
Java允许参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数所指向的对象。