Java编程思想小笔记2
54.包:库单元
注意:①java包的命名规则全部使用小写字母,包括中间的字也是如此。
②package语句必须是文件中的第一行非注释程序代码。
假设你编写一个Stack类并安装到了一台机器上,而该机器上已经有了一个别人编写的 Stack类,我们该如何解决呢?由于名字之间的潜在冲突,在java中对名称空间进行完全控制并为每个类创建唯一的标识符组合就成为了非常重要的事情。
当编写一个java源代码文件时,此文件通常被称为编译单元(有时也被称为转译单元)。每个编译单元都必须有一个后缀名.java,而在编译单元内则可以有一个public类,该类的名称必须与文件的名称相同(包括大小写,但不包括文件的后缀名.java)。每个编译单元只能有一个public类,否则编译器就不会接受。如果在该编译单元之中还有额外的类的话,那么在包之外的世界是无法看见这些类的,这是因为它们不是public类,而且它们主要用来为主public类提供支持。
当编译一个.java文件时,在.java文件中的每个类都会有一个输出文件,而该输出文件的名称与.java文件中每个类的名称相同,只是多了一个后缀名.class。因此,在编译少量.java文件之后,会得到大量的.class文件。
总结:在.java文件中,不是必须含有public类的。但如果含有public类,只有有一个,必须名字和文件名一样。
java解释器的运行过程如下:首先,找出环境变量CLASSPATH(可以通过操作系统来设置,有时也可通过安装程序-用来在你的机器上安装java或者基于java的工具-来设置)。CLASSPATH包含一个或多个目录,用作查找.class文件的根目录。从根目录开始,解释器获取包的名称并将每个句点替换成反斜杠,以从CLASSPATH根中产生一个路径名称(于是,package foo.bar.baz就变成了foo\bar\baz或foo/bar/baz或其他,这一切取决于操作系统)。得到的路径会与CLASSPATH中的各个不同的项相连接,解释器就在这些目录中查找与你所要创建的类名称相关的.class文件(解释器还会去查找某些涉及java解释器所在位置的标准目录)。
55.java访问权限修饰词
默认访问权限没有任何关键字,但通常是指包访问权限(有时也表示成为friendly)
public、protected、friendly(缺省的)、private,其中protected主要用于继承,但是它也具有包访问权限。
private只能被这个类自身所访问;
default被类自身和同一个包中的类访问;
protected被这个类自身、同一个包中的类以及它的子类(与该类在同一个包中或者不在同一个包中)访问;
public没有访问限制。
56.接口和实现
访问权限的控制常被称为是具体实现的隐藏。把数据和方法包装进类中,以及具体实现的隐藏,常共同被称作是封装。
57.类的访问权限
①每个编译单元(文件)都只能有一个public类。这表示,每个编译单元都有单一的公共接口,用public类来实现。该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的public类,编译器就会给出出错信息。
②public类的名称必须完全与含有该编译单元的文件名相匹配,包括大小写。如果不匹配,同样将得到编译时错误。
③虽然不是很常用,但编译单元内完全不带public类也是可能的。在这种情况下,可以随意对文件命名。(尽管随意命名会使得人们在阅读和维护代码时产生混淆)
58.note:类既不可以是private的(这样会使得除该类之外,其他任何类都不可以访问它),也不可以是protected的。(事实上,一个内部类可以是private或是protected的,但那是一个特例,这将在第10章介绍到)。所以对于类的访问,仅有两个选择:包访问权限或public。如果不希望其他任何人对该类拥有访问权限,可以把所有的构造器都指定为private,从而阻止任何人创建该类的对象,但是有一个例外,就是你在该类的static成员内部可以创建,然后通过static方法返回该类的对象,详见6.4节。
public static Soup1 makeSoup(){
return new Soup1();//该处返回的是一个对Soup1类的对象的引用,而不是该对象本身
}
请牢记:如果没有明确地至少创建一个构造器的话,就会帮你创建一个默认构造器(不带有任何参数的构造器)。
如果把该构造器指定为private,那么就谁也无法创建该类的对象了。但是现在别人怎么使用这个类呢?上面的例子(6.4节)就给出了两种选择:在Soup1中,创建一个static方法,它创建一个新的Soup1对象并返回一个对它的引用。如果想要在返回引用之前在Soup1上做一些额外的工作,或是如果想要记录到底创建了多少个Soup1对象(可能要限制其数量),这种做法将会大有裨益。
Soup2用到了所谓的设计模式,这种特定的模式被称为singleton(单例),这是因为你始终只能创建它的一个对象。Soup2类的对象时作为Soup2的一个static private成员而创建的,所以有且仅有一个,而且除非是通过public方法access(),否则是无法访问到它的。
附:6.4节例子
class Soup1{
private Soup1(){}
public static Soup1 makeSoup(){
return new Soup1();
}
}
class Soup2{
private Soup2(){}
private static Soup2 ps1=new Soup2();
public static Soup2 access(){
return ps1;
}
public void f(){}
}
public class Lunch{
void testPrivate(){
//can't do this!private constructor;
//!Soup1 soup = new Soup1();
}
void testStatic(){
Soup1 soup = Soup1.makeSoup();
}
void testSingleton(){
Soup2.access().f();
}
}
59.复用代码
复用代码有两种方法:
第一种方法非常直观:只需在新的类中产生现有类的对象。由于新的类是有现有类的对象组成的,所以这种方法称为组合。该方法只是复用了现有程序代码的功能,而非它的形式。
第二种方法则更细致一些,它按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。这种神奇的方法称为继承,而且编译器可以完成其中大部分的工作。
就组合和继承而言,其语法和行为大多是相似的。由于它们是利用现有类型生成新类型,所以这样做极富意义。
60.继承语法
继承是所有OOP语言和java语言不可缺少的组成部分。当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从java的标准根类Object进行继承。
为了继承,一般的规则是将所有的数据成员都指定为private,将所有的方法指定为public。当然,在特殊情况下,必须做出调整,但上述方法的确是一个很有用的规则。
当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象时一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部。
对基类子对象的正确初始化也是至关重要的,而且也仅有一种方法来保证这一点:在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。java会自动在导出类的构造器中插入对基类构造器的调用。下例展示了上述机制在三层继承关系上是如何工作的:
class Art{
Art(){System.out.println("Art constructor");}
}
class Drawing extends Art{
Drawing(){System.out.println("Drawing constructor");}
}
public class Cartoon extends Drawing{
public Cartoon(){System.out.println("Cartoon constructor");}
public static void main(String[] args){
Cartoon x = new Cartoon();
}
}/* Output:
Art constructor
Drawing constructor
Cartoon constructor
*///:~
读者会发现,构建过程是从基类“向外”扩散的,所以基类在导出类构造器可以访问它之前,就已经完成了初始化。即使你不为Cartoon()创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。
如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super显式地编写调用基类构造器的语句。并且配以适当的参数列表。调用基类构造器必须是你在导出类构造器中要做的第一件事(如果你做错了,编译器会提醒你)。
61.代理
第三种关系称为代理,java并没有提供对它的直接支持。这是继承与组合之间的中庸之道,因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。详见7.3节
62.结合使用组合和继承
note:虽然编译器强制你去初始化基类,并且要求你要在构造器起始处就要这么做,但是它并不监督你必须将成员对象也初始化,因此在这一点上你必须时刻注意。
无论try块是怎样退出的,保护区后的finally字句中的代码总是要被执行的。这里finally字句表示的是“无论发生什么事,一定要执行finally中的代码”。
许多情况下,清理并不是问题,仅需让垃圾回收器完成该动作就行。但当必须亲自处理清理时,就得多做努力并多加小心。因为,一旦涉及垃圾回收,能够信赖的事就不会很多了。垃圾回收器可能永远也无法被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。最好的办法是除了内存以外,不能依赖垃圾回收器去做任何事。如果需要进行清理,最好是编写你自己的清理方法,但不要使用finalize()。
63.名称屏蔽
如果java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本(这一点与C++不同)。因此,无论是在该层或者它的基类中对该方法进行定义,重载机制都可以正常工作。但是java SE5新增了@Override注释,它并不是关键字,但是可以把它当作关键字使用。当你想要覆写某个方法时,可以选择添加这个注释,在你不留心重载而并非覆写了该方法时,编译器就会生成一条错误消息。这样,@Override注解可以防止你不在不想重载时而意外地进行了重载。
64.在组合与继承之间选择
在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。略微思考下就会发现,用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅是一种交通工具(is-a关系)。“is-a”(是一个)的关系使用继承来表达的,而“has-a”(有一个)的关系则用组合来表达的。
65.protected关键字
在实际项目中,经常会想要将某些事物尽可能对这个世界隐藏起来,但仍然允许导出类的成员访问它们。关键字protected就是起这个作用的。它指明“就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的”。(protected也提供了包内访问权限。)
66.向上转型
“为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。
例子:
class Instrument{
public void play(){}
static void tune(Instrument i){
i.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args){
Wind flute = new Wind();
Instrument.tune(flute);//向上转型-upcasting,这种将Wind引用转换为 //Instrument引用的动作称为向上转型。
}
}
67.再论组合与继承
到底是该用组合还是用继承,一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,而继承是必要的;但如果不需要,则应当好好考虑自己是否需要继承。
68.final关键字
根据上下文环境,java的关键字final的含义存在着细微的区别,但通常它指的是“这是无法改变的”。
69.final数据
数据的恒定不变是很有用的,比如:
①一个永不改变的编译时常量。
②一个在运行时被初始化的值,而你不希望它被改变。
一个既是static又是final的域只占据一段不能改变的存储空间。
当对对象引用而不是基本类型运用final时,其含义会有一点令人迷惑。对于基本类型,final使数值恒定不变;而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身却是可以被修改的,java并未提供使任何对象恒定不变的途径(但可以自己编写类以取得使对象恒定不变的效果)。这一限制同样适用数组,它也是对象。
注意:根据惯例,既是static又是final的域(即编译期常量)将用大写表示,并使用下划线分隔各个单词。
定义为static,则强调只有一份;定义为final,则表明它是一个常量。请注意,带有恒定初始值(即编译期常量)的final static基本类型全用大写字母命名,并且字与字之间用下划线隔开。
java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化。但是,空白final在关键字final的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保持其恒定不变的特性。
必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因所在。
final参数:java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象。
70.final方法
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。
过去建议使用final方法的第二个原因是效率。在java的早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。这将消除方法调用的开销。当然,如果一个方法很大,你的程序代码就会膨胀,因而可能看不到内嵌带来的任何性能提高,因为,所带来的性能提高会花费于方法内的时间量而被缩减。
在最近的java版本中,虚拟机(特别是hotspot技术)可以探测到这些情况,并优化去掉这些效率反而降低的额外的内嵌调用,因此不再需要使用final方法来进行优化了。事实上,这种做法正在逐渐地受到劝阻。在使用java SE5/6时,应该让编译器和jvm去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为final的(不要陷入对仓促优化的强烈渴望之中。如果你的系统得以运行,而其速度很慢,使用final关键字来修复该问题是难以奏效的)。
final和private关键字:
类中所有的private方法都隐式地指定为是final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义。
“覆盖”只有在某方法是基类的接口的一部分时才会出现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法(这一点在下一章阐明)。如果某方法为private,它就不是基类的接口的一部分。它仅是一些隐藏于类中的程序代码,只不过是具有相同的名称而已。但如果在导出类中以相同的名称生成一个public、protected或包访问权限的方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况。此时你并没有覆盖该方法,仅是生成了一个新的方法。由于private方法无法触及而且能有效隐藏,所以除了把它看成是因为它所属的类的组织结构的原因而存在外,其他任何事物都不需要考虑到它。
71.final类
当将某个类的整体定义为final时(通过将关键字final置于它的定义之前),就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,处于某种考虑,你对该类的设计永不需要做任何变动,或者处于安全的考虑,你不希望它有子类。
final类的域可以根据个人的意愿选择为是或不是final。不论类是否被定义为final,相同的规则都适用于定义为final的域。然而,由于final类禁止继承,所以final类中所有的方法都隐式指定为是final的,因为无法覆盖它们。在final类中可以给方法添加final修饰词,但这不会增添任何意义。
72.初始化及类的加载
在许多传统语言中,程序是作为启动过程的一部分立刻被加载。然后是初始化,紧接着程序开始运行。然而java采用一种不同的加载方式-类的代码在初次使用时才加载。这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载①。
①构造器也是static方法,尽管static关键字并没有显式地写出来。因此更准确地讲,类是在其任何static成员被访问时加载的。
初次使用之处也是static初始化发生之处。所有的static对象和static代码段都会在加载时依程序中的顺序(即,定义类时的书写顺序)而依次初始化。当然,定义为static的东西只会被初始化一次。
请证明加载类的动作只发生一次?
因为类的加载发生于创建类的第一个对象时,或者访问static域和static方法时,并且定义为static的东西只会被初始化一次,所以我们可以定义一个类,类中定义static域或者static方法,通过static东西的初始化来证明类的加载只发生一次,记住不进行创建对象。这样可以方便验证。
总结:类的加载是从导出类到基类,如果该基类还有其自身的基类,那么第二个基类就会被记载。而对象的实例化是从相反的方向进行的,即先基类,再导出类。
73.多态
在面向对象的程序设计语言中,多态是继数据抽象和继承以后的第三种基本特征。
多态也称作动态绑定、后期绑定或运行时绑定。
多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出而来的。这种区别是根据方法行为的不同而表现出来的,虽然这些方法都可以通过同一个基类来调用。
74.再论向上转型
对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。而这种把对某个对象的引用视为对其基类型的引用的做法被称作向上转型-因为在继承树的画法中,基类是放置在上方的。
75.方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。读者可能以前从来没有听过这个术语,因为它是面向过程的语言中不需要选择就默认的绑定方式。例如,C只有一种方法调用,那就是前期绑定。
上述程序之所以令人迷惑,主要是因为前期绑定。因为,当编译器只有一个Instrument引用时,它无法知道究竟调用哪个方法才对。
解决的办法就是后期绑定,它的含义就是在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定或运行时绑定。如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要想一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。
java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定-它会自动发生。
static方法和final方法都没法覆写。
为什么要将某个方法声明为final呢?正如前一章提到的那样,它可以防止其他人覆盖该方法。但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者说,告诉编译器不需要对其进行动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。然而,大多数情况下,这样做对程序的整体性能不会有什么改观。所以,最好根据设计来决定是否使用final,而不是处于试图提高性能的目的来使用final。
一旦知道java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。或者换一种说法,发送消息给某个对象,让该对象去断定应该做什么事。
76.缺陷:“覆盖”私有方法
只有非private方法才可以覆盖;但是还需要密切注意覆盖private方法的现象,这时虽然编译器不会报错,但是也不会按照我们所期望的来执行。确切地说,在导出类中,对于基类中的private方法,最好采用不同的名字。详见8.2.4节
77.缺陷:域与静态方法
静态方法不能被覆写的原因:静态方法是与类,而并非与单个的对象想关联的,而动态绑定是根据对象类型来决定调用哪个方法,所以动态绑定是与对象相关联的。->不能动态绑定,就不能被覆写。
例子:
class Super{
public int field = 0;
public int getField(){
return field;
}
}
class Sub extends Super{
public int field = 1;
public int getField(){
return field;
}
public int getSuperField(){
return super.field;
}
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub();
System.out.println("sup.field = " + sup.field +",sup.getField() = "+ sup.getField());
Sub sub = new Sub();
System.out.println("sub.field=" + sub.field + ",sub.getField()="+ sub.getField() + ",sub.getSuperField()="+ sub.getSuperField());
}
}/*Output:
sup.field=0,sup.getField()=1
sub.field=1,sub.getField()=1,sub.getSuperField()=0
*///:~
当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,因此不是多态的。
在本例中,为Super.field和Sub.field分配了不同的存储空间。这样,Sub实际上包含两个称为field的域:它自己的和它从Super处得到的。然而,在引用Sub中的field时所产生的默认域并非Super版本的field域。因此,为了得到Super.field,必须显式地指明super.field。
78.构造器和多态
通常,构造器不同于其他种类的方法。涉及到多态时仍是如此。尽管构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运作。
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此,必须令所有构造器都得到调用,否则就不可能正确构造完整对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果没有明确指定调用某个基类构造器,它就会“默默”地调用默认构造器。如果不存在默认构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成出一个默认构造器)。
详见8.3.1例子:
表明了这一复杂对象调用构造器要遵循下面的顺序:
1)调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最低层的导出类。
2)按声明顺序调用成员的初始化方法。
3)调用导出类构造器的主体。
79.继承与清理
通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常都会留给垃圾回收器进行处理。如果确实遇到清理的问题,那么必须用心为新类创建dispose()方法(在这里我选用此名称,读者可以提出更好的)。并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类的dispose()方法时,务必记住调用基类版本dispose()方法;否则,基类的清理动作就不会发生。
销毁的顺序应该和初始化顺序相反,防止某个子对象要依赖于其他对象。对于字段,而意味着与声明的顺序相反(因为字段的初始化是按照声明的顺序进行的)。对于基类(遵循C++中析构函数的形式),应该首先对其导出类进行清理,然后才是基类。
80.构造器内部的多态方法的行为
构造器调用的层次结构带来了一个有趣的问题。如果在一个构造器的内部调用正在构造的对象的某个动态绑定的方法,会发生什么情况?
例子如下:
package thinkInJava.duotai;
class Glyph{
void draw(){
System.out.println("Glyph.draw()");
}
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
radius = r;
System.out.println("RoundGlyph.RoundGlyph().radius="+radius);
}
void draw(){
System.out.println("RoundGlyph.draw().radius = "+radius);
}
}
public class PolyConstructors {
public static void main(String[] args){
new RoundGlyph(5);
}
}/*
Glyph() before draw()
RoundGlyph.draw(),radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph().radius=5
*///~
Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw()的调用,这看起来似乎是我们的目的。但是如果看到输出结果,我们会发现当Glyph的构造器调用draw()方法时,radius不是默认初始值1,而是0。
前一节讲述的初始化顺序并不完整,初始化的实际过程是:
1)在其它任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
2)如前所述那样调用基类构造器。此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们发现此时radius的值为0.
3)按照声明的顺序调用成员的初始化方法。
4)调用导出类的构造器主体。
因此,编写构造器时有一条有效的准则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其它方法”。在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法,它们自动属于final方法)。这条方法不能被覆盖,因此也就不会出现上述令人惊讶的问题。你可能无法总是能够遵循这条准则,但是应该朝着它努力。
81.协变返回类型
java se5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型:
class Grain{
public String toString(){return "Grain";}
}
class Wheat extends Grain{
public String toString(){return "Wheat";}
}
class Mill{
Grain process(){return new Grain();}
}
class WheatMill extends Mill{
Wheat process(){return new Wheat();}
}
public class CovarianReturn{
public static void main(String[] args){
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
}/*
Grain
Wheat
*/
java se5与java较早的版本将强制process()的覆盖版本必须返回Grain,而不能返回Wheat,尽管Wheat是从Grain导出的,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的Wheat类型。
注意:
1)、覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;
2)、覆盖的方法的返回值必须和被覆盖的方法的返回一致;
3)、覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
4)、被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
82.向下转型与运行时类型识别
在某些程序设计语言(如C++)中,我们必须执行一个特殊的操作来获得安全的向下转型。但是在java语言中,所有转型都会得到检查!所以即使我们只是进行一次普通的加括号形式的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就会返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)。
83.抽象类和抽象方法
抽象类是普通类与接口之间的一种中庸之道。
抽象方法声明采用的语法是:abstract void f(),抽象方法相当于C++语言的纯虚函数。
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。(否则,编译器就会报错。)
如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供方法定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。
如果想创建一个抽象类的对象,编译器就会报错。
我们也可能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要阻止产生这个类的任何对象,那么这时这样做就很有用了。
如果在一个类中含有abstract方法,那么该类也必须使用关键字abstract声明,否则编译器会报错。
84.接口
interface这个关键字产生了一个完全抽象的类,它根本就没有提供任何具体实现。它允许创建者确定方法名、参数列表和返回类型,但是没有方法体。接口只提供了形式,而未提供任何具体实现。
interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继承的特性。
接口也可以包含域,但是这些域隐式地是static和final的。
可以选择在接口中显式地将方法声明为public的,但即使你不这么做,它们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被定义为是public的;否则,它们将只能得到默认的包访问权限,这样在方法被继承的过程中,其可访问权限就被降低了,这是java编译器所不允许的。
要注意的是,在接口中的每一个方法确实都只是一个声明,这是编译器所允许的在接口中唯一能够存在的事物。此外,在Instrument中没有任何方法被声明为是public的,但是它们自动就都是public的。
85.完全解耦
1)策略设计模式
例子如下:
class Processor{
public String name(){
return getClass().getSimpleName();
}
Object process(Object input){
return input;
}
}
class Upcase extends Processor{
@Override
String process(Object input){
return ((String)input).toUpperCase();
}
}
class Downcase extends Processor{
String process(Object input){
return ((String)input).toLowerCase();
}
}
class Splitter extends Processor{
String process(Object input){
return Arrays.toString(((String)input).split(" "));
}
}
public class Apply {
public static void process(Processor p,Object s){
System.out.println("Using Processor "+p.name());
System.out.println(p.process(s));
}
public static String s = "Disagreement with beliefs is by definition incorrect";
public static void main(String[] args) {
process(new Upcase(), s);
process(new Downcase(), s);
process(new Splitter(), s);
}
}
Apply.process()方法可以接受任何类型的Processor,并将其应用到一个Object对象上,然后打印结果。像本例这样,创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要执行的算法中固定不变的部分,而“策略”包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。这里,Processor对象就是一个策略,在main()中可以看到有三种不同类型的策略应用到了String类型的s对象上。
2)适配器模式
86.java中的多重继承
接口不仅仅只是一种更纯粹形式的抽象类,它的目标比这更高。因为接口是根本没有任何具体实现的-也就是说,没有任何与接口相关的存储;在C++中,组合多个类的接口的行为被称作多重继承。它可能会使你背负很沉重的包袱,因为每个类都有一个具体实现。在java中,你可以执行相同的行为,但是只有一个类可以由具体实现;因此,通过组合多个接口,C++中的问题是不会在java中发生的。
87.通过继承来扩展接口
extends用于类的时候,只能继承一个类;如果用于接口时,可以继承多个接口,只需用逗号将接口名一一分隔开即可。
组合接口时的名字冲突:在打算组合的不同接口中使用相同的方法名通常会造成代码可读性的混乱,请尽量避免这种情况。
88.接口中的域
因为你放入接口中的任何域都自动是static和final的,所以接口就成为了一种很便捷的用来创建常量组的工具。在java se5之前,这是产生与C或C++中的enum(枚举类型)具有相同效果的类型的唯一途径。因此在java se5之前的代码中你会看到下面这样的代码:
public interface Months{
int JANUARY = 1,FEBRUARY = 2,MARCH = 3,
APRIL = 4,MAY = 5,JUNE = 6,JULY = 7,
AUGUST = 8,SEPTEMBER = 9,OCTOBER = 10,
NOVEMBER = 11,DECEMBER = 12;
}
请注意,java中标识具有常量初始化值的static final时,会使用大写字母的风格(在一个标识符中用下划线来分隔多个单词)。接口中的域自动是public的,所以没有显式地指明这一点。
有了java se5,你就可以使用更加强大而灵活的enum关键字,因此,使用接口来群组常量已经显得没什么意义了。但是,当你阅读遗留的代码时,在许多情况下你可能还是会碰到这种旧的习惯用法。
在接口中定义的域不能是“空final”,但是可以被非常量表达式初始化。例如:
public interface RandVals{
Random RAND = new Random(47);
int RANDOM_INT = RAND.nextInt(10);
long RANDOM_LONG = RAND.nextLong()*10;
float RANDOM_FLOAT = RAND.nextLong()*10;
double RANDOM_DOUBLE = RAND.nextDouble()*10;
}
既然域是static的,它们就可以在类第一次被加载时初始化,这发生在任何域首次被访问时。这里给出了一个简单的测试:
public class TestRandVals{
public static void main(String[] args){
System.out.println(RandVals.RANDOM_INT);
System.out.println(RandVals.RANDOM_LONG);
System.out.println(RandVals.RANDOM_FLOAT);
System.out.println(RandVals.RANDOM_DOUBLE);
}
}
当然,这些域不是接口的一部分,它们的值被存储在该接口的静态存储区域内。
89.嵌套接口
note:当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,private接口不能在定义它的类之外被实现。
90.接口与工厂
接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。工厂方法设计模式通过添加额外级别的间接性实现的,但为什么要用工厂方法设计模式呢?一个常见的原因是想要创建框架。
总结:恰当的原则应该是优先选择类而不是接口。从类开始,如果接口的必需性变得非常明确,那么就进行重构。接口是一种重要的工具,但是它们容易被滥用。
91.内部类
可以将一个类的定义放在另一个类的定义内部,这就是内部类。
如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在main()方法中那样,具体地指明这个对象的类型:OuterClassName.InnerClassName。详见10.1。
92.链接到外部类
到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用的,但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权(这与C++嵌套类的设计非常不同,在C++中只是单纯的名字隐藏机制,与外围对象没有联系,也没有隐含的访问权)。
内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。幸运的是,编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外围类的对象相关联的情况下才能被创建(就像你应该看到的,在内部类是非static类时)。构造内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都无需程序员操心。
93.使用.this与.new
如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。例子:
public class DotThis{
void f(){System.out.println("DotThis.f()");}
public class Inner{
public DotThis outer(){
return DotThis.this;//如果只有this表示Inner对象的引用
}
}
public static void main(String[] args){
DotThis dt = new DotThis();
DotThis.Inner dti = dt.new Inner();
dti.outer().f();
}
}/*
DotThis.f()
*///:~
有时候你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new表达式中提供对其他外部类对象的引用,这是需要使用.new语法,就像下面例子:
public class DotNew{
public class Inner{}
public static void main(String[] args){
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
}
要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字DotNew,而是必须使用外部类的对象来创建该内部类对象,就像在上面的程序中看到的那样。这也解决了内部类名字作用域的问题,因此你不必声明(实际上你不能声明)dn.new DotNew.Inner()。
在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到创建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用。
94.内部类与向上转型
当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类-某个接口的实现-能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。
95.在方法和作用域内的内部类
可以在一个方法里面或者在任意的作用域内定义内部类。这么做有两个理由:
1)如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
2)你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类时公共可用的。
在后面的例子中,先前的代码将被修改,以用来实现:
1)一个定义在方法中的类。
2)一个定义在作用域内的类,此作用域在方法的内部。-称为:“局部内部类”
3)一个实现了接口的匿名类。
4)一个匿名类,它扩展了有非默认构造器的类。
5)一个匿名类,它执行字段初始化。
6)一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)。
96.匿名内部类
如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的,就像你在destination()的参数中看到的那样。如果你忘记了,将会得到一个编译时错误信息。详见10.6.
对于参数引用是final的,该参数必须在匿名内部类内被使用,才要求参数引用为final。
如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:
abstract class Base{
public Base(){
System.out.println("Base constructor.i = "+i);
}
public abstract void f();
}
public class AnonymousConstructor{
public static Base getBase(int i){
return new Base(i){
{System.out.println(Inside instance initializer);}
public void f(){
System.out.println("In anonymous f()");
}
};
}
public static void mian(String[] args){
Base base = getBase(47);
base.f();
}
}/*Output:
Base constructor.i = 47
Inside instance initializer
In anonymous f()
*///:~
在此例中,不要求变量i一定是final的。因为i被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。
97.嵌套类
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static。这通常称为嵌套类(与C++嵌套类大致相似,只不过在C++中那些类不能访问私有成员,而在java中可以访问)。记住:普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是static的时,就不是这样了。嵌套类意味着:
1)要创建嵌套类的对象,并不需要其外围类的对象。
2)不能从嵌套类的对象中访问非静态的外围类对象。
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西。
在一个普通的(非static)内部类中,通过一个特殊的this引用可以链接到其外围类对象。嵌套类就没有这个特殊的this引用,这使得它类似于一个static方法。
98.接口内部的类
正常情况下,不能在接口内部放置任何代码,但嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是public和static的。因为类是static的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口。就像下面这样:
public interface ClassInInterface{
void howdy();
static class Test implements ClassInInterface{
public void howdy(){
System.out.println("Howdy!");
}
public static void main(String[] args){
new Test().howdy();
}
}
}/*
Howdy!
*///:~
如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。
在每个类中都写一个main()方法,用来测试这个类。这样做有一个缺点,那就是必须带着那些已编译过的额外代码。如果这对你是个麻烦,那就可以使用嵌套类来放置测试代码。
public class TestBed{
public void f(){System.out.println("f()");}
public static class Tester{
public static void mian(String[] args){
TestBed t = new TestBed();
t.f();
}
}
}
这生成了一个独立的类TestBed$Tester(要运行这个程序,执行java TestBed$Tester即可,在Unix/Linux系统中必须转义$)。可以使用这个类来做测试,但是不必在发布的产品中包含它,在将产品打包前可以简单地删除TestBed$Tester.class。
99.从多层嵌套类中访问外部类的成员
一个内部类被嵌套多少层并不重要-它能透明地访问所有它所嵌入的外围类的所有成员,如下:
class MNA{
private void f(){}
class A{
private void g(){}
public class B{
void h(){
g();
f();
}
}
}
}
public class MultiNestingAccess{
public static void main(String[] args){
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}
可以看到在MNA.A.B中,调用方法g()和f()不需要任何条件(即使它们被定义为private)。这个例子同时展示了如何从不同的类里创建多层嵌套的内部类对象的基本语法。“.new”语法能产生正确的作用域,所以不必在调用构造器时限定类名。
100.为什么需要内部类
内部类最吸引人的原因是:每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或抽象类)。
如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:
1)内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
2)在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承统一个类。稍后展示一个这样的例子。
3)创建内部类对象的时刻并不依赖于外围类对象的创建。
4)内部类并没有令人迷惑的“is-a”关系;它就是一个独立的实体。