复用代码是Java众多引人注目的功能之一,但要想成为极具革命性的语言,仅仅能够复制并加以改变是不够的。它还必须能做更多的事情。在Java中问题的解决都是围绕着类展开的,可以通过创建新类来复用代码,而不必从头开始编写。可以使用别人已开发调试好的类。
通过创建新类来复用代码的窍门在于使用类而不破坏现有程序代码:
方法一:只需在新的类中产生现有类的对象。新的类由现有类的对象组成,这叫组合,该方法只是复用了现有代码的功能,而非它的形式。
方法二:按照现有类的类型来创建新类,无需改变现有类的形式,采用现有类的形式并在其中添加新代码,这叫继承。
组合语法
1.1 组合语法的使用
对于组合语法的使用,只需将对象引用置于新类中即可:
class Test{
private String s;
Test(){
System.out.println("Test()");
s="Constructed";
}
public String toString(){
return s;
}
}
public class Thinking_test {
private String value1,value2,value3,value4;
private Test t=new Test();
private int i;
private float f;
@Override
public String toString() {
return "Thinking_test{" +
"value1='" + value1 + '\'' +
", value2='" + value2 + '\'' +
", value3='" + value3 + '\'' +
", value4='" + value4 + '\'' +
", t=" + t +
", i=" + i +
", f=" + f +
'}';
}
public static void main(String []args){
Thinking_test test=new Thinking_test();
System.out.println(test);
}
}
Output:
Test()
Thinking_test{value1='null', value2='null', value3='null',
value4='null', t=Constructed, i=0, f=0.0}
这里有一个特殊的函数:toString()。每一个非基本类型的对象都有一个toString()方法,而且当编译器需要一个String而你却只有一个对象时,该方法便会被调用。
1.2 引用的初始化
编译器并不是简单地为每一个引用创建默认对象,同时在初始化引用时可以在代码中的以下位置进行:
1.在定义对象的地方
,这意味着它们总是能够在构造器被调用之前被初始化。
2.在类的构造器中
3.就在正要使用这些对象之前
,这种方式称为:惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。
4.使用实例初始化
如果没有在定义处初始化,那么除非发生了不可避免的运行期异常,否则将不能保证信息在发送给对象引用之前已经被初始化。
继承语法:extends
2.1 继承语法
当创建一个类时总是在继承,因此,除非已明确指出要从其他类中继承,否则就是隐式地从Java的标准根类Object进行继承。组合的语法比较平实,但是继承使用的是一种特殊的语法。在继承过程中,需要先声明“新类与旧类相似”。这种声明通过在类的左边花括号之前,书写后面紧跟基类名称的关键字extends而实现。这样做,会自动得到基类种所有的域和方法
。
2.2 初始化基类:
继承类从外部看,就像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者的区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部。
仅有一种方法来保证基类子对象的正确初始化:在导出类构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。
Java会自动在导出类的构造器中插入对基类构造器的调用:
class Art{
Art(){
System.out.println("Art");
}
}
class Drawing extends Art{
Drawing(){
System.out.println("Drawing");
}
}
public class Cartoon extends Drawing{
public Cartoon(){
System.out.println("Cartoon");
}
public static void main(String[] args) {
Cartoon cartoon=new Cartoon();
}
}
Output:
Art
Drawing
Cartoon
构建过程是从基类“向外”扩散的,所以基类在导出类构造器中可以访问它之前,就已经完成了初始化。即使没有给Cartoon()创建构造器,编译器也会自动合成一个默认的构造器,该构造器将调用基类的构造器。
使用组合和继承
使用组合和继承是很常见的事,通过使用组合和继承可以创建比较复杂的类。
3.1 名称屏蔽
若Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类的任何版本。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作
。
class Homer{
char doh(char c){
System.out.println("doh(char)");
return 'c';
}
float doh(float f){
System.out.println("doh(float)");
return 1f;
}
}
class MilHouse{};
class Bart extends Homer{
void doh(MilHouse m){
System.out.println("doh(MilHouse)");
}
}
public class Hide {
public static void main(String[] args) {
Bart b=new Bart();
b.doh('x');
b.doh(1f);
b.doh(new MilHouse());
}
}
Output:
doh(char)
doh(float)
doh(MilHouse)
从上方代码可以看出,我们进行了重载,而不是覆写,覆写的话应该是函数名和参数都相同。
3.2 组合和继承之间的选择
组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承是隐式地做。
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所有嵌入对象的接口。为取此效果,需要在新类中嵌入一个现有类的private对象。而在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。
用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅是一种交通工具(is-a关系)。“is-a”(是一个)的关系是用继承来表达的,而“has-a”(有一个)的关系则是用组合来表达的。
(还有一个关系是is-like-a关系,指的是继承之后又添加了一些方法)
3.3 向上转型
向上转型:子类引用转换为父类引用,参数接受的是父类,但给的引用是子类。
“为新的类提供方法”,并不是继承技术中最重要的方面,其最重要的方面是用来表现类和基类之间的关系,这种关系可以用 “新类是现有类的一种类型” 概括。
例如:假设有一个称为Instrument的代表乐器的基类和一个称为Wind的导出类。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的消息同样也可以向导出类发送。如果Instrument类具有一个play()方法,那么Wind乐器也将同样具备。这意味着我们可以准确地说Wind对象也是一种类型的Instrument。
例:
class Instrument{
public void play(){
System.out.println(this.getClass().getName());
}
static void tune(Instrument instrument){
instrument.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args) {
Wind wind=new Wind();
Instrument.tune(wind);
}
}
Output:
com.test.Wind
在此例中我们可以看出:tune()方法可以接受Instrument引用。但在Wind.main()中,传递给tune()方法的是一个Wind引用。但是在对类型检查十分严格个Java语言中确正常运行了,这显然很奇怪。之所以可以正常运行,是因为Wind对象同样也是一种Instrument对象,程序可以对Instrument以及其所有导出类起作用,这种将Wind引用转换为Instrument引用的动作称之为想上转型。
final关键字
根据上下文环境,Java的final关键字存在着细微的区别,但通常指的是 “这是无法改变的”。不想改变可能处自两种理由:设计或效率
4.1 final数据:
每种编程语言都存在某种方法告知编译器,一块数据是恒定不变的。数据恒定不变有其作用:一个永不改变的编译时常量、或者一个在运行时被初始化的值,而不希望它被改变。
对于编译时常量,编译器可以将该常量代入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这减轻了一些运行时的负担。在Java中,这类常量必须是基本数据类型
,并且以关键字final表示。在对这个常量进行定义时,必须对其进行赋值。
当对对象引用而不是基本类型运用final时,其含义有点令人迷惑。对于基本类型,final使数据恒定不变;而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其本身是可以被修改的,Java并未提供任何对象恒定不变的途径。这一限制同样适用数组,它也是对象。
4.2 final参数:
Java允许在参数列表中以声明的方式将参数指明为final。这意味着,你无法在方法中更改参数引用所指向的对象。
4.3 final方法:
使用final方法的原因有两个:
1.锁定方法,以防任何类修改他的含义;确保在继承中使方法行为保持不变,并且不会被覆盖。
2.效率,在Java早期实现中,将方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法,它会根据自己的判断,跳过插入程序代码这种正常方式而执行方法调用机制,并且以方法体中的实际代码的副本来代替方法调用。这将消除方法调用的开销。
4.4 final和private关键字:
当方法为private时,已经隐式的将其指定为final,private方法无法被复写;如果导出类出现和private方法名相同,参数也相同的情况只是为该类添加了一个新的方法。
4.5 final类:
当将某个类整体定义为final时,表明该类无法被继承。该类永远不可做任何变动,且没有子类。
在设计类时,将方法指明是final的,应该说是明智的。但是,要预见类是如何被复用的一般是困难的,特别是对于一个通用类更是如此。如果将方法指定为final,可能会妨碍其他程序员在项目中通过继承来复用你的类,而这只是因为你没有想到它会以哪种方式被运用。
初始化及类的加载
在许多传统语言中,程序是作为启动过程的一部分立刻被加载的。然后是初始化,紧接着程序开始运行。这些语言的初始化过程必须小心控制,以确保定义为static的东西,其初始化顺序不会造成麻烦。但是在Java中不会出现这种问题,Java采用了一种不同的加载方式。
加载是众多变得更加容易的动作之一,因为Java中的所有事物都是对象。每个类的编译都存在于它自己的独立的文件中。该文件只在需要使用程序代码时才会被加载。一般来说,类的代码在初次使用时才加载。这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载。
初次使用之处也是static初始化发生之处。所有的static对象和static代码段都会在加载时依程序中的顺序(即,定义类时的书写顺序)而一次初始化。当然,定义为static的东西只会被初始化一次。