[关闭]
@lemonguge 2015-06-23T02:27:07.000000Z 字数 6958 阅读 453

继承与实现

JAVA


继承

继承是所有OOP语言和Java语言不可缺少的组成部分。

当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从Java的标志根类Object进行继承。

根据面向对象的思想,当我们了解所要创建的新类与旧类相似时,这才建议使用继承。通过在新类的右边,左边花括号之前,书写后面紧跟基类名称的关键字extends来声明继承。

子类会自动得到基类中的所有域和方法,有些人可能会想,可我在子类中调用private的成员时编译器会报错呀。切记private是权限修饰词,我们的确是得到了基类中的所有域和方法,只是没有权限访问而已。

Java中支持单继承。不直接支持多继承,但对C++中的多继承机制进行改良。

因为多个父类中有相同成员,会产生调用不确定性(由于方法体,多继承时如果在不同父类中出现名称和方法参数类型列表相同时,调用子类中的该方法时会不明确),Java中是通过"多实现"的方式来体现,因为具体的实现是由自己明确的。

Java支持多层(多重)继承,就会出现继承体系。

当要使用一个继承体系
1. 查看该体系中的顶层类,了解该体系的基本功能。
2. 创建体系中的最子类对象,完成功能的使用。

组合

我们对继承的使用一定要小心,只有当你确定了想要创建的类与一个已存在的类之间产生关系,才可以使用继承。组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形,一般而言,我们使用组合来完成我们所需要的功能更为常见。

组合只是在新的类中产生现有类的对象。

is-a”(是一个)的关系是用继承来表达,而“has-a”(有一个)的关系则是用组合来表达。

  1. //窗户
  2. class Window {
  3. // 窗户摇上
  4. public void rollup() {
  5. System.out.println("Window rollup");
  6. }
  7. // 窗户摇下
  8. public void rolldown() {
  9. System.out.println("Window rolldown");
  10. }
  11. }
  12. // 车
  13. public class Car {
  14. private Window window = new Window();
  15. public void run() {
  16. System.out.println("car run");
  17. }
  18. public void stop() {
  19. System.out.println("car stop");
  20. }
  21. // 开窗
  22. public void windowUp() {
  23. window.rollup();
  24. }
  25. // 关窗
  26. public void windowDown() {
  27. window.rolldown();
  28. }
  29. }//车子并不是窗户,而是车子有窗户!用组合来实现
  1. //乐器
  2. class Instrument{
  3. public void play(){
  4. System.out.println("play Instrument..");
  5. }
  6. }
  7. //风琴
  8. public class Wind {
  9. //具有自己的演奏方式
  10. public void play(){
  11. System.out.println("play Wind..");
  12. }
  13. }//风琴是一种乐器,用继承来体现两者的关系

“为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。

由导出类转型成基类,在继承图上是向上移动的,因此一般称为向上转型。由于向上转型是从一个较专用类型向较通用类型转换,所以总是安全的。也就是说导出类是基类的一个超集,它可能比基类含有更多的方法,但它至少具备基类中所含的方法。简单的一句话讲,一切都是对象!

到底是该用继承还是用组合,我们可以问问自己是否需要向上转型,如果必须向上转型的话,那么继承就是必须的;但是如果不需要,则应当好好考虑一下是否需要继承,在这种情况下,更好的方式是选择“组合”。组合不会强制我们的程序设计进入继承的层次结构中,所以组合更加灵活,继承在编译时就需要知道确切类型。

也许有些程序员在写代码的时候并不会去思考到底该用继承还是用组合,在这里,笔者想对大家说一句“我们写的不仅是代码,更是思想的体现”。

final关键字

从它的词意可以感受到这是无法改变的,最终的。

final数据

我们在开发中可能会碰到两个方面会需要使用到final关键字。

  1. 一个永不改变的编译时常量。
  2. 一个在运行时被初始化的值,而你不喜欢它改变。

一个既是static又是final的域只占据一段不能改变的存储空间。

当对对象引用而不是基本类型运用final时,其含义会有一点令人迷惑。对于基本数据类型(直接存储值),final使数值恒定不变;而对对象引用(持有对象在堆内存中的首地址),final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身却是可以被修改的,java并未提供使任何对象恒定不变的途径(但可以自己编写类以取得使对象恒定不变的效果)。这一限制同样适用数组,它也是对象。

Java允许生成“空白final”,所谓空白final是指被声明为fianl但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化

  1. class Poppet {
  2. private int i;
  3. // ! final int j; // 会在构造器那一行报错!
  4. Poppet(int ii) {//The blank final field j may not have been initialized
  5. i = ii;
  6. }
  7. }

final修饰成员变量时,必须要对该成员变量进行初始化(不包括默认初始化),否则编译器会报错!

  1. class Poppet {
  2. private int i;
  3. Poppet(int ii) {
  4. i = ii;
  5. }
  6. public void setI(int i) {
  7. this.i = i;
  8. }
  9. }
  10. public class BlankFinal {
  11. private final int i = 0; // 显式初始化
  12. private final int j; // Blank final
  13. private final Poppet p; // Blank final reference
  14. public BlankFinal() {
  15. // ! System.out.println(j); //j未被初始化,编译器报错!
  16. j = 1; // 构造器初始化
  17. p = new Poppet(1); // 构造器初始化
  18. }
  19. public BlankFinal(int x) {
  20. j = x; // Initialize blank final
  21. p = new Poppet(x); // Initialize blank final reference
  22. }
  23. public static void main(String[] args) {
  24. new BlankFinal();
  25. BlankFinal bf = new BlankFinal(47);
  26. // ! bf.p = new Poppet(20); // `final`使引用数据类型恒定不变
  27. bf.p.setI(20); // 可以改变引用指向对象自身的内容
  28. final int k;
  29. k = 10;
  30. // ! k = 11; // final的基本数据类型的数值恒定不变
  31. }
  32. }

可以在BlankFinal类的无参构造器中发现,编译器不允许我们使用未经过初始化的final的成员变量。

Java允许在参数列表中以声明的方式将参数指明为final,这意味着你无法在方法中更改参数引用所指向的对象!

  1. class Gizmo {
  2. public void spin() {
  3. }
  4. }
  5. public class FinalArguments {
  6. void with(final Gizmo g) {
  7. // ! g = new Gizmo(); // Illegal -- g is final
  8. }
  9. void without(Gizmo g) {
  10. g = new Gizmo(); // OK -- g not final
  11. g.spin();
  12. }
  13. // ! void f(final int i) { i++; } // 自增,i=i+1,Can't change
  14. // You can only read from a final primitive:
  15. int g(final int i) {
  16. return i + 1;
  17. }
  18. }

这一特性主要用来向匿名内部类传递数据,到时候会给出详细的解释。

final方法

使用final方法的原因有两个。

  1. 把方法锁定,以防任何继承类修改它的含义,不会被覆盖。
  2. 效率。这是早期的做法,现在的JVM已经不再需要使用final方法进行优化。

在使用Java SE5/6时,应该让编译器和JVM去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为final的。

类中所有的private方法都隐式地指定为是final的,所以给private方法添加final关键字,不能给该方法增加任何额外的意义。

final类

当将某个类的整体定义为final时(通过将关键字final置于它的定义之前),就表明了你不打算继承该类,而且也不允许别人这么做。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或者出于安全的考虑,你不希望它有子类。

由于final类禁止继承,所以final类中所有的方法都隐式指定为是final的,因为无法覆盖它们,所以在final类中给方法添加final修饰词不会增添任何意义。

协办返回类型

在Java SE5中添加了协办返回类型,在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型。

简单的说,协办返回类型允许返回更具体的类型。

  1. class Grain {
  2. public String toString() { return "Grain"; }
  3. }
  4. class Wheat extends Grain {
  5. public String toString() { return "Wheat"; }
  6. }
  7. class Mill {
  8. Grain process() { return new Grain(); }
  9. }
  10. class WheatMill extends Mill {
  11. // 覆盖了Mill类中的process()方法,返回类型Wheat为基类方法返回类型的导出类型。
  12. @Override
  13. Wheat process() { return new Wheat(); }
  14. }
  15. public class CovariantReturn {
  16. public static void main(String[] args) {
  17. Mill m = new Mill();
  18. Grain g = m.process();
  19. System.out.println(g);
  20. m = new WheatMill();
  21. g = m.process(); // 后期绑定
  22. System.out.println(g);
  23. }
  24. } /* Output:
  25. Grain
  26. Wheat
  27. *///:~

Java SE5与Java较早版本之间的主要差异就是较早的版本,导出类中被覆盖的方法的返回类型必须与基类中的返回类型一致!

向下转型与运行时类型识别

我们知道向上转型是安全的,因为基类不会具有大于导出类的接口。但是对于向下转型,例如,我们无法知道一个“几何形状”它确实就是一个“圆”,它可以是一个三角形、正方形或其他一些类型。

在Java中,所有转型都会得到检查!

在编译时期,我们通过加括弧形式进行类型转换。在进入运行时期仍然会对其进行检查,以便保证它的确是我们希望得到的类型。如果不是,就会返回一个ClassCastException(类转换异常)。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)。

instanceof通常在向下转型前用于健壮性的判断,只能用于引用数据类型判断。


实现

接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。

抽象类和抽象方法

抽象类是普通类和接口之间的中庸之道。

由于为抽象类创建对象是不安全的(调用抽象方法没意义),所以我们会从编译器那里得到一条出错信息。这样,编译器会确保抽象类的纯粹性,我们不必担心会误用它。

虽然抽象类不可以被实例化,但是抽象类是具有构造函数,用于给子类对象进行初始化,所以抽象类一定是个父类。

如果从抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供方法定义。如果不这么做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。

我们也有可能创建一个没有任何抽象方法的抽象类。大家也许会想,为什么要这样做,考虑这么一种情况:如果有一个类,让其包含任何abstract方法都显得没有意义,而且我们想要阻止产生这个类的任何对象,那么这是这样做就很有用(在java.awt包中的Component类,虽然是abstract的,但是没有abstract方法,一般称这样的类为适配器,这种类的方法虽然有方法体,但是除了隐式语句renturn;外没有其他内容)。

abstract关键字不可以与staticprivatefinal关键字共存。

抽象类与普通类的异同点

接口

interface关键字使抽象的概念更向前迈进了一步。

接口的特点:

interface这个关键字产生一个完全抽象的类,它根本就没有提供任何具体实现,只是提供了形式。它允许创建者确定方法名、参数列表和返回类型,但是没有任何方法体。

如同enum关键字一样,创建接口时,需要用interface关键字来代替class关键字。

即使不显式的声明接口中的方法为public abstract和域为public static final,它们也将被隐式指明。接口中的域不能为“空白final”,在接口中也不能定义代码块或静态代码块来进行初始化,所以在接口中的域只能被显式初始化

要让一个类遵循某个特定的接口(或者是一组接口),需要使用implements关键字。

类与接口之间是实现关系,而类可以继承一个类的同时实现多个接口。

  1. 一个实现了接口的子类要被实例化,必须覆盖了接口中所有的抽象方法。否则,这个子类就是一个抽象类。
  2. 一个类可以继承一个类的同时实现多个接口,前提是基类和多个接口中不能有方法名和参数类型列表相同但是返回类型不同的方法。(在子类中会因为出现不是重载的方法而报错,不兼容"incompatible")
  3. 接口的出现避免了单继承的局限性。
  1. interface A{ public void show(); }
  2. interface B{ public void show(); }
  3. interface C{ public int show(); }
  4. class Z{
  5. public void show(){
  6. System.out.println("Z show..");
  7. }
  8. }
  9. class ImplA implements A{
  10. public void show(){
  11. System.out.println("ImplA show");
  12. }
  13. }
  14. class ImplAB implements A, B {
  15. // 同时实现了A, B接口
  16. public void show() {
  17. System.out.println("ImplAB show");
  18. }
  19. }
  20. class ExtZImplAB extends Z implements A, B { } // 从Z类继承过来的方法直接实现了A, B接口
  21. // ! class ImplAC implements A, C { } // incompatible
  22. // ! class ExtZImplC extends Z implements C { } // incompatible
  23. public class Impl{
  24. public static void run(A a){
  25. a.show();
  26. }
  27. public static void main(String[] args){
  28. run(new ImplA());
  29. run(new ImplAB());
  30. run(new ExtZImplAB());
  31. }
  32. } /* Output:
  33. ImplA show
  34. ImplAB show
  35. Z show..
  36. *///:~

接口与接口之间可以有继承关系,而且接口可以多继承。

因为接口中的方法没有方法体,具体的实现是由自己明确的,也就不会出现调用的不确定性。

抽象类和接口的异同点

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注