Java编程思想拾遗(6) 复用类

403 阅读5分钟

复用分为组合和继承两种方式。

继承

当创建了一个导出类的对象时,该对象包含了一个基类的子对象,这个子对象与你用基类直接创建的对象是一样的,二者区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部。 对基类子对象的正确初始化至关重要,而且也仅有一种方法来保证这一点:在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力,Java会自动在导出类的构造器中插入对基类构造器的调用。

class Art {
    Art() {}
}

class Drawing extends Art {
    Drawing() {}
}

如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super显式地编写调用基类构造器的语句。

class Game {
    Game(int i) {}
}

class BoardGame extends Game {
    BoardGame(int i) {
        super(i);
    }
}

如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类的任何版本,因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作。即重载可以横跨上下父子类。

class Homer {
    char doh(char c) {
        return 'd';
    }
    float doh(float f) {
        return 1.0f;
    } 
}
    
class Bart extends Homer {
    void hod(String s) {}
}

public class Hide {
    public static void main(String[] args) {
        Bart b = new Bart();
        b.doh(1);
        b.doh('x');
        b.doh(1.0f);
        b.doh("hello"):
    }
}

为了区分重载与覆写,Java提供了@Override注解。

向上转型

神似基本类型向上提升,不过基本类型是值范围扩大,而这里是接口缩小。

由导出类转型成基类,在继承图上是向上移动的,因此一般称为向上转型,由于向上转型是从一个较专用类型向较通用类型转换,所以总是安全的。在向上转型的过程中,类接口中唯一可能发生的事情是丢失方法,而不是获取它们,这就是为什么编译器在未曾明确表示转型或未曾指定特殊标记的情况下,仍然允许向上转型的原因。

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
    }
}

在组合与继承之间选择

组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承则是隐式地做。

组合技术通常用于想在新类中使用现成类的功能而非它的接口,即在新类中嵌入某个 对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。

在继承的时候,使用某个现有类,并开发一个它的特殊版本,通常这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。

应当慎用继承,其使用场合仅限于你确信使用该技术确实有效,到底是该用组合还是用继承,一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型,如果必须向上转型,则继承是必要的。

代理

Java并没有提供对它的直接支持,这是继承和组合之间的中庸之道。本质是组合,形态像继承,但我们使用代理时可以拥有更多的控制力,因为我们可以选择只提供在成员对象中的方法的某个子集。

一些缓存和装饰器对代理模式有应用场景。

public class SpaceShipDelagation {
    private SpaceShipControls controls = new SpaceShipControls();
    
    public void back(int velocity) {
        controls.back(velocity);
    }
}

final

final指的是无法改变

  • 一个永不改变的编译时常量。因为是编译期就可以确定的,所以只有基本数据类型才算。
  • 一个在运行时被初始化的值,而你不希望它被改变。允许运行时再赋值,但只能赋值一次,对于对象和数组来说就是其引用,虽然其对象本身仍然可以改变。

Java允许在参数列表中以声明的方式将参数指明为final,这意味着你无法在方法中更改参数引用所指向的对象,这一特性主要用来向匿名内部类传递数据。

方法

使用final的方法会把方法锁住,以防任何继承类修改它的含义。

类中所有的private方法都隐式地指定为是final的,由于无法取用private方法,所以也就无法覆盖它。

“覆盖”只有在某方法是基类接口的一部分时才会出现,即必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,它就不是基类的接口的一部分,它仅是一些隐藏于类中的程序代码,只不过是具有相同的名称而已,但如果在导出类中以相同的名称生成一个public、proteced或包访问权限方法的话,此时你并没有覆盖该方法,仅是生成了一个新的方法。

class WithFinals {
    private final void f() {}
    private void g() {}
}

class OverridingPrivate extends WithFinals {
    privat final void f() {}
    private void g() {}
}

public class FinalOverridingIllusion {
    public static void main(String[] args) {
        OverridingPrivate op = new OverridingPrivate();
        op.f();
        op.g();
        WIthFinals wf = op;
        // you can upcast, but you can't call the methods
        // ! wf.f();
        // ! wf.g();
    }
}

当将某个类的整体定义为final时,就表明了不允许别人继承。

由于final类禁止继承,所以final类中所有的方法都隐式指定为是final的,因为无法覆盖它们,所以不用额外给方法添加final修饰词。