Thinking In Java -- Chapter 7 -- 复用类

本文深入探讨了Java中的继承概念,包括如何使用`super`关键字调用超类方法,构造器的调用顺序,以及如何处理没有默认构造器的情况。此外,还解释了名称屏蔽现象,`final`关键字在对象、方法和类级别的应用,以及在继承和组合之间的选择。最后,文章讨论了初始化过程,包括静态域的初始化和对象创建时的步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承语法:


  1. 为了继承,一般的规则是将所有的数据成员定义成private,将所有的方法指定成public成员。
  2. 继承类如果对超类中的方法进行了修改,而又想使用超类中未被修改的方法,可以通过”super.”后面添加超类的方法名来调用。
  3. 当创建一个导出类的对象时,该对象包含一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类的对象内部。Java会自动在导出类的构造器中插入对基类构造器的调用。即使你在导出类中不为基类创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。如下例:
    //: reusing/Cartoon.java
    // Constructor calls during inheritance.
    import static net.mindview.util.Print.*;
    
    class Art {
      Art() { print("Art constructor"); }
    }
    
    class Drawing extends Art {
      Drawing() { print("Drawing constructor"); }
    }
    
    public class Cartoon extends Drawing {
      public Cartoon() { print("Cartoon constructor"); }
      public static void main(String[] args) {
        Cartoon x = new Cartoon();
      }
    } /* Output:
    Art constructor
    Drawing constructor
    Cartoon constructor
    *///:~
    

    当然,上例中的各个类均有默认的不带参数的构造器。编译器可以轻松的调用它们是因为不需要考虑要传递什么样的参数的问题。但是,如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super显式的编写调用基类构造器的语句,并且配以适当的参数列表,如下例:

    //: reusing/Chess.java
    // Inheritance, constructors and arguments.
    import static net.mindview.util.Print.*;
    
    class Game {
      Game(int i) {
        print("Game constructor");
      }
    }
    
    class BoardGame extends Game {
      BoardGame(int i) {
        super(i);
        print("BoardGame constructor");
      }
    }	
    
    public class Chess extends BoardGame {
      Chess() {
        super(11);
        print("Chess constructor");
      }
      public static void main(String[] args) {
        Chess x = new Chess();
      }
    } /* Output:
    Game constructor
    BoardGame constructor
    Chess constructor
    *///:~
    

    上例中,如果不在BoardGame中调用基类的构造器,编译器就会抱怨午饭找到附和Game()形式的构造器。而且,调用基类构造器必须是你在导出类构造器中要做的第一件事

名称屏蔽:


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

//: reusing/Hide.java
// Overloading a base-class method name in a derived
// class does not hide the base-class versions.
import static net.mindview.util.Print.*;

class Homer {
  char doh(char c) {
    print("doh(char)");
    return 'd';
  }
  float doh(float f) {
    print("doh(float)");
    return 1.0f;
  }
}

class Milhouse {}

class Bart extends Homer {
  void doh(Milhouse m) {
    print("doh(Milhouse)");
  }
}

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(new Milhouse());
  }
} /* Output:
doh(float)
doh(char)
doh(float)
doh(Milhouse)
*///:~

Java SE5新增了@Override注解,它并不是关键字,但是可以把它当做关键字使用。当你想覆写某个方法时,可以选择添加这个注解,在你不留心重载而非覆写了该方法时,编译器就会报错,这样就可以防止你在不想重载时意外的进行了重载。如下:

//: reusing/Lisa.java
// {CompileTimeError} (Won't compile)

class Lisa extends Homer {
  @Override void doh(Milhouse m) {
    System.out.println("doh(Milhouse)");
  }
} ///:~

在组合和继承之间选择:


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

向上转型:


  1. 由导出类转型成基类,在继承图上是向上移动的,因此一般称为向上转型。由于向上转型是从一个较专用类型向较通用类型转移,所以总是安全的。也就是说,导出类是基类的一个超集。它可能比基类含有更多的方法,但它必须至少具备积累中所含有的方法。
  2. 继承技术其实是不多用的。什么时候需要使用呢,一个最清晰的判断方法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的;但如果不需要,则应当好好考虑自己是否需要继承。

final关键字:


  1. 当final修饰的是对象时,final是使得引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象本身却是可以被修改的。
  2. Java允许生成空白final,所谓空白final就是指被申明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化。空白final在关键词final的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保持其恒定不变的特性。如下例:
    //: reusing/BlankFinal.java
    // "Blank" final fields.
    
    class Poppet {
      private int i;
      Poppet(int ii) { i = ii; }
    }
    
    public class BlankFinal {
      private final int i = 0; // Initialized final
      private final int j; // Blank final
      private final Poppet p; // Blank final reference
      // Blank finals MUST be initialized in the constructor:
      public BlankFinal() {
        j = 1; // Initialize blank final
        p = new Poppet(1); // Initialize blank final reference
      }
      public BlankFinal(int x) {
        j = x; // Initialize blank final
        p = new Poppet(x); // Initialize blank final reference
      }
      public static void main(String[] args) {
        new BlankFinal();
        new BlankFinal(47);
      }
    } ///:~
    

    必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用中总是被初始化的原因所在。

  3. Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法再方法中更改参数引用所指向的对象,如下例:

    //: reusing/FinalArguments.java
    // Using "final" with method arguments.
    
    class Gizmo {
      public void spin() {}
    }
    
    public class FinalArguments {
      void with(final Gizmo g) {
        //! g = new Gizmo(); // Illegal -- g is final
      }
      void without(Gizmo g) {
        g = new Gizmo(); // OK -- g not final
        g.spin();
      }
      // void f(final int i) { i++; } // Can't change
      // You can only read from a final primitive:
      int g(final int i) { return i + 1; }
      public static void main(String[] args) {
        FinalArguments bf = new FinalArguments();
        bf.without(null);
        bf.with(null);
      }
    } ///:~
    

    上例中,还可以看到一个f()方法和g()方法,这两个方法展示了当基本类型的参数被指明为final时所出现的结果:你可以读参数,但却无法修改参数。这一特性主要用匿名内部类传递数据。

final方法:


  1. 使用final方法的原因有两个,一个就是把方法锁定,以防任何继承类修改它的定义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。第二个原因是早期Java版本中的效率问题。但在新的java版本中,已经不需要使用final来处理效率问题了,应该让编译器和JVM去处理效率问题。
  2. 类中所有的private方法都是隐式地指定为final的。由于无法取用private方法,所以也就无法覆盖它。

final类:


当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这么做。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或者出于安全的考虑,你不希望它有子类。而且,由于final类禁止继承,所以final类中所有的方法都隐式的指定为是final的,因为无法覆盖它们。

继承和初始化:


我们通过下面这个例子来了解包括继承在内的初始化全过程,以对所发生的一切有一个全局性的把握:

//: reusing/Beetle.java
// The full process of initialization.
import static net.mindview.util.Print.*;

class Insect {
  private int i = 9;
  protected int j;
  Insect() {
    print("i = " + i + ", j = " + j);
    j = 39;
  }
  private static int x1 =
    printInit("static Insect.x1 initialized");
  static int printInit(String s) {
    print(s);
    return 47;
  }
}

public class Beetle extends Insect {
  private int k = printInit("Beetle.k initialized");
  public Beetle() {
    print("k = " + k);
    print("j = " + j);
  }
  private static int x2 =
    printInit("static Beetle.x2 initialized");
  public static void main(String[] args) {
    print("Beetle constructor");
    Beetle b = new Beetle();
  }
} /* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*///:~

如上例,在Beetle上运行java时,首先试图访问Beetle.main(),于是加载器开始启动并找出Beetle类的编译代码(在名为Beetle.class的文件之中),在对它进行加载的过程中,编译器注意到了它有一个基类,然后就继续加载它的基类,如果基类还有基类那就以此类推。加载的过程中,就需要先对static域进行初始化,上例中就是先初始化基类中x1,然后初始化导出类中的x2。这时,两个类就都已经加载完成了,对象就可以被创建了。首先将对象中所有的基本类型设置为默认值,对象引用被设为null。然后先是基类的构造器被调用,接着是导出类的构造器调用。然后结果就如最后得output所示。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值