java中的复用类

Think  in  java (chapter 6) 复用类

代码复用能够大大简化我们的工作。面向对象的语言中一般是通过对类的重复使用来达到代码复用的目的的,Java也不例外。在Java中,复用类有两种方式,合成(has-a)与继承(is-a)。

两种代码复用的方法:

1: 在新类中产生现有类的对象,也称作组合.

2:按照现有类的类型来创建新类,不需要改变现有类的形式,采用在现有类中再添加一些字段,或者方法,又称为继承.

(1) 合成所使用的语法

合成的语法很简单,只要把要复用的类的对象的引用直接放到新类里就可以了。当然仅仅这样还是不够的,我们还要创建这个类的对象让那个引用来指向它。因为Java不会帮我们自动创建一个缺省的对象,它只会自动替我们把字段中的引用初始化为null。为引用赋值可以在三个地方,一个就是在定义这个引用的时候,另一个是在构造函数中,第三个地方就是在即将要使用这个对象之前。为了防止忘记在使用前为引用赋值,我们一般应该在前两种场合来创建对象。如果我们要创建的这个对象会花费很大开销,而且又可能不是每次都需要创建它的话,我们可以考虑第三种方式来创建这个对象。

class WaterSource{
    private String s;
    WaterSource(){
        System.out.println("WaterSource");
        s = new String("Constructor");
    }
    public String toString(){
        return s;
    }
}
public class compos_inher {
    private String value1,value2,value3,value4;
    private WaterSource source;
    private int i;
    private float f;
    public String toString(){
        return
        "value1 = " + value1 + "/n" +
        "value2 = " + value2 + "/n" +
        "value3 = " + value3 + "/n" +
        "value4 = " + value4 + "/n" +
        "i = " + i + "/n" +
        "f = " + f + "/n"+
        "source = " + source;
    }
    public static void main(String[] args){
        compos_inher x = new compos_inher();
        System.out.println(x);
    }
}

打印结果是:

value1 = null
value2 = null
value3 = null
value4 = null
i = 0
f = 0.0
source = null

本实例中又一个很特殊的地方,就是又一个toString()的函数中又这么一代码:

"source = " + source;因为source 是一个对象了.在java中为每个基本类型的对象都定义了一个toString()的方法,而且当编译器需要一个String而你只有对象的时候,该方法就会被调用.

其二:对于类中的每个基本类型的字段都会被初始化,而对于对象引用则会被初始化为null;

如果你尝试着去调用他们的任何方法都会得到一个异常.

如果想初始化这些引用可以有一下三个地方.

1:在定义对象的地方.

2:在类的构造器中

3:就在要使用这些对象之前.又称为惰性初始化.

见下面的例子详细讲了三种情况.

class Soap{
    private String s;
    Soap(){
        System.out.println("Soap()");
        s = new String("Constructor");
    }
    public String toString(){
        return s;
    }
}
public class compos_inher{
    private String s1 = new String("Happy");   // 1:符合第一种初始化情况
    private String s2 = "Happy",s3,s4;         //1;在定义对象的时候初始化
    private Soap casttilie;
    private int i;
    private float toy;
    public compos_inher(){
        System.out.println("Inside compos_inher");
        s3 = new String("joy");             //符合第二种初始化情况
        i = 34;                             //即在构造器种初始化
        toy = 4.5f;
        casttilie = new Soap();
    }
    public String toString(){
        if(s4 == null){                        //第三种初始化方法
            s4 = new String("^_^");         //就需要使用对象钱初始化
        }
        return
        "s1 = " + s1 + "/n" +
        "s2 = " + s2 + "/n" +
        "s3 = " + s3 + "/n" +
        "s4 = " + s4 + "/n" +
        "i  = " + i  + "/n" +
        "toy= " + toy+ "/n" +
        "casttilie" + casttilie;
    }
    public static void main(String[] args){
        compos_inher x = new compos_inher();
        System.out.println(x);
    }
}

打印结果:

Inside compos_inher
Soap()
s1 = Happy
s2 = Happy
s3 = joy
s4 = ^_^
i  = 34
toy= 4.5
casttilieConstructor

二、继承所使用的语法

继承是Java中的重要部分,因为Java是使用单根体系的(C++不是这样,因为它要保持向C的兼容),所以我们定义的每一个类都是继承自Java中的根类Object类。在定义一个继承自已有的类的类时,要使用extends关键字,其后跟上基类的名字,这样表示新定义的这个类是继承自那个基类。在Java中不允许多重继承(C++中允许),也就是说它不允许一个类拥有多于一个的基类,这点劣势可以用接口来弥补,因为Java允许一个类实现任意多个接口。

子类继承了一个基类后便拥有了基类中的成员,也就可以通过创建的子类对象来访问基类中可见的成员。Java是怎样做到这一点的呢?在我们创建一个子类对象的时候,这里创建的已经不是一个类的对象了,它还会创建这个类的基类的对象,这个基类的对象创建后被包括在子类的对象中。也就是说创建的子类的对象拥有其基类全部的成员(从这就可以知道为什么可以上传),但是子类对象只能访问基类中它可见的成员。那么在创建一个这样的对象时,子类和基类对象创建的顺序是怎么样的呢?为了能够正确的初始化基类,一般会调用基类的构造函数来进行初始化。Java中在调用子类的构造函数时首先会自动的调用基类的构造函数,并且这样的过程是层层传递的。比如C继承了B,而B又继承了A,在创建C的对象时,C的构造函数会首先调用B的构造函数,这时B的构造函数又会首先调用A的构造函数。(如果基类中没有默认构造函数,编译时就会报错。)但是这里自动调用的都是基类的默认构造函数(无参的),如果我们想调用基类的某个带参数的构造函数又该怎么办呢?上面提到可以用super来代替基类的引用,与在构造函数中通过this调用本类其它构造函数的形式一样,我们可以通过super来调用基类带参数的构造函数,比如“super(i, j)”。与调用本类的其它构造函数一样,对基类构造函数的显示调用也需要放在子类构造函数的最前面,在它之前不能有任何东西,如果基类的构造函数会抛出异常需要捕获的话,就会比较麻烦。

class A{
    private String str = new String("haha");
    public void append(String s){
        str += s;
    }
    public void apply(){
        append(" apply()");
    }
    public void scrup(){
        append(" scrup");
    }
    public String toString(){
        return str;
    }
    public static void main(String[] args){
        A a = new A();
        a.apply();
        a.scrup();
        System.out.println(a);
    }
}
public class compos_inher extends A{
    public void scrup(){               //3:覆写基类中的方法
        append(" compos_inher.scrup()");
        super.scrup();             //调用基类的方法
    }
    public void add(){             //2:新增加的方法
        append(" add()");
    }
    public static void main(String[] args){
        compos_inher x = new compos_inher();
        x.apply();                      //1:基类中的方法
        x.add();
        x.scrup();
        System.out.println(x);
    }
}
//打印结果:
haha apply() add() compos_inher.scrup() scrup

本实例子说明了一下几个问题:

1: 一个子类会自动获得基类中的全部字段与方法(那些由访问控制符控制的对子类而言不可见的成员也会获得,只是不可见,用不了),这也就是对基类中代码的复用。

2: 除了自动获得自基类的代码外,子类中还可定义新的成员,

3:也可以覆写基类中的方法(所谓覆写指的是方法的声明部分一样但实现不一样),这样可以让相同签名的方法拥有不一样的形为。因为子类自动拥有了基类的成员,因此在子类中自然就可以调用基类的方法。如果这个方法在子类中被覆写过,那编译器知道你是要调用哪个方法呢?Java提供了super关键字在类中表示该类的基类的引用,我们可以通过这个关键字来明确表示我们要用到的是基类中的成员。如果不写super的话,那编译器将会理解为嵌套调用。

4: 在Java程序中常常是用public类中的main()方法做为整个程序的入口。这样的静态main()方法并不是非得要在public类中才能出现的,静态的main()方法可以做所有类的入口(但只能是main(),而不能是其它名字的什么静态方法)。比如一个程序有多个class组成,我们要对其中的某个class进行单元测试时,我们就可以在这个class文件中加入main(),编译后生成这个类的.class文件,在控制台通过java来运行它就是了。

    备注:一般为了更好的继承,通常情况下将基类的数据成员指定为private,而将方法指定为

public.

三:初始化基类

1:缺省的构造器

对基类的初始化是非常重要的事情,而只有一种方法才能够保证正确的初始化,即在构造器中调用

基类的构造器执行初始化.

class Art{
    Art(){
        System.out.println("Art cons.");
    }
}
class Drawing extends Art{
    Drawing(){
        System.out.println("Drawing cons");
    }
}
public class compos_inher extends Drawing{
    public compos_inher(){
        System.out.println("compos_inher cons");
    }
    public static void main(String[] args){
        compos_inher x = new compos_inher();
    }
}///:~
打印结果:
Art cons.
Drawing cons
compos_inher cons
2:带参数的构造器
class Art{
    Art( int i){
        System.out.println("Art cons." + i);
    }
}
class Drawing extends Art{
    Drawing( int i){
        super(i);      //必须显式的调用基类的构造器.默认的只会调用默认的构造器
        System.out.println("Drawing cons" + i);
    }
}
public class compos_inher extends Drawing{
    public compos_inher( ){
        super(2);
        System.out.println("compos_inher cons");
    }
    public static void main(String[] args){
        compos_inher x = new compos_inher();
    }
}///:~
打印结果:
Art cons.2
Drawing cons2
compos_inher cons

实例子说明了如果不在带又参数的派生类构造器中显式的调用基类的构造器,编译器就会抱怨

找不到构造器,调用基类的构造器是导出派生类构造器的第一件事情.可以用super(参数);

四:确保正确清理

Java 中没有c++所拥有的析构函数(在对象被销毁的时候可以自动调用的函数),而java提供了垃圾回收机制,但是这样的回收机制不确定,当又时候想在某个生命周期内就要执行一些必要的清理工作,就必须显式的编写一个方法来处理这件事情.

如下例子:

import java.util.*;
class Shape{
    Shape(int i){
        System.out.println("Shape Cons");
    }
    void dispose(){
        System.out.println("shape dispose");
    }
}
class Triangle extends Shape{
    Triangle( int i){
        super(i);
        System.out.println("Triangle Cons");
    }
    void dispose(){
        System.out.println("Triangle dispose");
    }
}
class Line extends Shape{
    private int start,end;
    Line( int start, int end ){
        super(start);
        this.start = start;
        this.end = end;
        System.out.println("Draw Line" + start + ", " + end);
    }
    void dispose(){
        System.out.println("Erasing Line " + start + " ," + end);
    }
}
public class compos_inher extends Shape{
    private Triangle t;
    private Line[] lines = new Line[4];
    public compos_inher( int i){
        super( i + 1 );
        t = new Triangle(1);
        for( int j = 0; j < 4; j++)
            lines[i] = new Line(i,i*i);
        System.out.println("Combined Cons");
    }
    public void dispose(){
        System.out.println("CADsystem dispose");
        t.dispose();
        for( int j = lines.length; j > 0; j--){
            lines[j].dispose();
            super.dispose();
        }
    }
    public static void main(String[] args){
        compos_inher x = new compos_inher(45);
        try{
        }finally{
        x.dispose();
        }
    }
}

打印:

小结:对于垃圾回收的问题,最好的办法就是除了内存之外,其他不要依赖回收器去做,最好自己去写一个清理方法,也不要去使用finalize();

五:名字屏蔽

如果在java的基类已经拥有了没个已经被多次重载的方法名称,那么在导出类中重新定义该方法名称的时候并不会屏蔽其在基类中的任何版本(这一点非常不同于c++),因此无论是在该层还是对他的基类进行方法调用,重载机制都可以运行..

class Home{
    char doh(char ch){
        System.out.println("doh(char)");
        return 'd';
    }
    float doh(float f){
        System.out.println("doh(float)");
        return 1.2f;
    }
}
public class compos_inher extends Home{
    void doh(compos_inher b){
        System.out.println("doh(compos_inher)");
    }
    public static void main(String[] args){
        compos_inher b = new compos_inher();
        b.doh(1);
        b.doh('d');
        b.doh(3.3f);
    }
}
//打印
doh(float)
doh(char)
doh(float)

关键字protected,即:对于继承于此类的导出类或其他任何位于同一个包中的类来说,他是可以访问的,对于其他就是private.

增量开发

继承的优点就是支持增量开发,所谓增量开发就是你可以引入新的代码而不会在源代码中引入BUG,事实上就是将BUG隔离在新的代码之中.

向上转型

为导出类提供方法或者字段绝对不是继承的最终目的,最重要的是用来表现新类于基类的关系.这种关系可以用”新类是现有类的一种类型”来加以概括..

class A{
    public void play(){}
    static void tune( A a ){
        //....
        a.play();
    }
}
public class compos_inher extends A{
    public static void main(String[] args){
        compos_inher b = new compos_inher();
        A.tune(b);          //upcast
    }
}

本来在tune()的参数应该是A的对象,而且JAVA对于类型检查是非常严格的,接受某种类型的方法在接受另外一种类型的时候就觉得非常奇怪,,除非认为B也是一种特殊的A.即向上转型.

final关键字

final在Java中并不常用,然而它却为我们提供了定义常量的功能,而且,final还可以控制你的成员、方法或者是一个类是否可被覆写或继承等功能.

final成员

当你在类中定义变量时,在其前面加上final关键字,那便是说,这个变量一旦被初始化便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说其引用不可再变。其初始化可以在两个地方,一是其定义处,也就是说在final变量定义时直接给其赋值,二是在构造函数中。这两个地方只能选其一,要么在定义时给值,要么在构造函数中给值,不能同时既在定义时给了值,又在构造函数中给另外的值。下面这段代码

演示了这一点:

class compos_inher{
    final int i = 3;//定义的时候初始化
    final float f;
    compos_inher(){
        f = 0.3f;//构造函数中初始化
        System.out.println("Cons");
    }
    public static void main(String[] args){
        compos_inher a  = new compos_inher();
    }
}

此程序很简单的演示了final的常规用法。值得注意的是final字段一定要被初始化,而且有且只有两种方法初始化final字段.

还有一种用法是定义方法中的参数为final,对于基本类型的变量,这样做并没有什么实际意义,因为基本类型的变量在调用方法时是传值的,也就是说你可以在方法中更改这个参数变量而不会影响到调用语句。 然而对于对象变量,却显得很实用,因为对象变量在传递时是传递其引用,这样你在方法中对对象变量的修改也会影响到调用语句中的对象变量,当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法。

另外方法中的内部类在用到方法中的参变量时,此参变也必须声明为final才可使用;

final方法

将方法声明为final,可能有两种原因,一:把方法锁定,不需要进行扩展,并且也不允许任何从此类继承的类来覆写这个方法,但是继承仍然可以继承这个方法,也就是说可以直接使用。 另外有一种被称为inline的机制(提高效率),它会使你在调用final方法时,直接将方法主体插入到调用处,而不是进行例行的方法调用,例如保存断点,压栈等,这样可能会使你的程序效率有所提高,然而当你的方法主体非常庞大时,或你在多处调用此方法,那么你的调用主体代码便会迅速膨胀,可能反而会影响效率,所以你要慎用final进行方法定义。

final

当你将final用于类身上时,你就需要仔细考虑,因为一个final类是无法被任何人继承的,那也就意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美而不需要进行修改或扩展。 对于final类中的成员,你可以定义其为final,也可以不是final。而对于方法,由于所属类为final的关系,自然也就成了final型的。你也可以明确的给final类中的方法加上一个final,但这显然没有意义。

final class final
{
    final String str="final Data";
    public String str1="non final data";
    final public void print()
    {
      System.out.println("final method.");
  }
    public void what()
    {
        System.out.println(str+"/n"+str1);
    }
}
  public class FinalDemo
{ 
//extends final 无法继承
    public static void  main(String[] args)
    {
        final f=new final();
        f.what();
        f.print();
    }
}

从程序中可以看出,final类与普通类的使用几乎没有差别,只是它失去了被继承的特性。final方法与非final方法的区别也很难从程序行看出,只是记住慎用。

继承与初始化::

class Insect{
    private int i = 9;
    protected int j;
    Insect(){
        System.out.println("i = " + i + "j = " + j);
        //j = 39;
    }
    private static int x1 = print("static Insect.x1 initilized");
    static int print(String s){
        System.out.println(s);
        return 34;
    }
}
public class compos_inher extends Insect{
    private int x = print("compos_inher.x initilized");
    public compos_inher(){
        System.out.println("x = " + x);
        System.out.println("j = " + j);
        System.out.println("compos_inher Cons");
    }
    private static int x2 = print("static compos_inher.x2 initilized");
    public static void main(String[] args){
        compos_inher b = new compos_inher();
       
    }
}
//打印
static Insect.x1 initilized
static compos_inher.x2 initilized
i = 9j = 0
compos_inher.x initilized
x = 34
j = 0
compos_inher Cons

初始化顺序,先基类的static变量,然后导出类中的static变量,再基类中的其他变量,再基类的构造器,再导出类中的其他变量,以及导出类的构造器.