第七章 复用类
第一种方法非常直观:只需在新的类中产生现有类的对象(组合)。
第二种方法更细致一些:它按照现有类的类型来创建新类(继承)。
7.1 组合语法
只需将对象引用置于新类中即可。
class A{}class B{A a = new A();}
如果想初始化这些引用,可在下列位置进行:
- 在定义对象的地方。
- 在类的构造器中。
- 在真正使用这些对象之前,惰性初始化。
- 使用实例初始化。
7.2 继承语法
当创建一个类时,总是在继承。
在继承过程中,需要先声明“新类与旧类相似”。在基类名称后紧随关键词extends,当这么做时,会自动得到基类中所有的域和方法。
为了继承,一般将数据成员指定为private,将方法指定为public(protected成员也可以借助导出类访问)。
Java使用super关键字表示超类,当前类就是从超类继承来的,如super.scrub()将调用基类(父类)版本的scrub()。
7.2.1 初始化基类
当创建一个导出类对象,该对象包含了一个基类的子对象。这个字对象与你用基类直接创建的对象是一样的。
Java会自动在导出类的构造器中插入对基类构造器的调用。
7.3 代理
代理是继承和组合之家你的中庸之道。将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象所有方法(就像继承)。
class AControls{void up(){}void down(){}}class B{AControls a = new AControls();void up(int i){a.up(i);}void down(int i){a.down(i);}}
7.4 在组合和继承之间选择
is-a(是一个)的关系用继承来表达。
has-a(有一个)的关系用组合来表达。
7.5 protected关键字
就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一包内的类来说,它却可以访问。
7.6 向上转型
“为新的类提供方法”并不是继承最总要的,最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”概括。
class A{public void play(){}static void tune(A a){a.play();}}class B extends A{public static void main(String[] args){B b = new B();A.tune(b); // 向上转型}
在tune()中,程序代码可以对A(父类)和他所有的导出类(子类)起作用,这种将B(子)引用转换为A(父)引用的动作,称之为向上转型。
7.6.1 为什么称为向上转型
由导出类转型为基类,在继承图上是向上移动的,一般称向上转型。由于向上转型是从一个较专用类型向较普通类型转换,所以总是很安全的。
7.6.2 再论组合与继承
一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的,但如果不需要,则应当考虑是否继承。
7.7 final关键字
7.7.1 final数据
- 一个永不改变的编译时常量。
- 一个在运行时被初始化的值,而你不希望它被改变。
一个既是static又是final的域只占据一段不能被改变的存储空间。
当对对象引用而不是基本类型运用final时,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法把它改为指向另一个对象,然而,对象其自身却可以修改(指针不能变,对象内存域中的值可以变)。
不能因为某数据是final就认为在编译时可知的,如果使用随机数初始化在编译时不可知。
空白final
被声明为final但又未给初始值的域,提高了灵活性,必须在域的定义处或每个构造器中用表达式对final进行赋值。
final参数
Java允许在参数列表中将参数声明为final,意味着方法中无法更改引用指向。可以读参数,无法修改参数,主要用来向匿名内部类传递数据。
7.7.2 final方法
- 锁定方法,防止任何继承类修改它的含义,使方法形为不变,不会被覆盖。
- (早期)使用final方法效率高。
final和private关键字
类中所有的private方法都是隐式地指定为final的。
这会造成混淆,如果你覆盖一个private方法(隐含final),似乎是奏效的。“覆盖”只有在某方法是基类的接口的一部分时才会出现(能向上转型为基类并调用相同方法)。**如果某方法为private,它就不是基类的接口的一部分。**如果在导出类中以相同名称生成public、protected或包访问权限方法,此时没有覆盖该方法,仅是生成新方法。由于private无法触及而且有效隐藏,只看成它归属类的组织结构,其他情况不考虑。
7.7.3 final类
当将某个类定义为final,表明不打算继承该类,且不允许别人这样做。该类设计永不变动,并且不希望它有子类。final类中的方法也是隐式final的,无法覆盖。
7.8 初始化及类的加载
Java中的所有事物都是对象。每个类的编译文件都存在于它自己的独立文件中。该文件只有需要使用时才被加载。一般来说”类的代码在初次使用时才加载”。
初次使用指:
- 创建类的第一个对象(创建对象调用构造器,构造器也是隐式static,准确的说,类是在任何static成员被访问时加载)。
- 访问static域或static方法。
7.8.1 继承与初始化
class A{private int i = 9;protected int j;A(){System.out.println("i = " + i + ", j = " + j);j = 39;}private static int x1 = printInit("static A.x1 initialized");static int printInit(String s){System.out.println(s);return 47;}}class B extends A{private int k = printInit("B.k initialized");public B(){System.out.println("k = " + k);System.out.println("j = " + j);}private static int x2 = printInit("static B.x2 initialized");public static void main(String[] args){System.out.println("B constructor");B b = new B();}}
输出
static A.x1 initialized-------------父类静态成员初始化static B.x2 initialized-------------子类静态成员初始化B constructor-----------------------子类main(静态)打印语句//此时声明B b = new B();父类普通成员初始化i=9i = 9, j = 0------------------------父类构造器打印语句父类构造器中初始化j=39B.k initialized---------------------子类普通成员初始化k=47k = 47------------------------------子类构造器打印语句j = 39 子类构造器打印语句
初始化过程如下:
父类静态-子类静态-父类构造-父类变量-子类构造-子类变量
-
在B上运行Java时,发生的第一件事就是试图访问B.main()(一个static方法),于是加载器开始启动并找出B类的编译代码(在名为B.class的文件中)。
-
在对B.class加载过程中,编译器注意到B有一个基类A,于是它继续进行加载A。不管你是否打算产生一个该基类对象,这都要发生。
2.1 如果该基类还有其自身的基类,那么第二个基类被加载,以此类推。
-
根基类(A类)中的static初始化被执行。
3.1 然后是下一个导出类的static初始化,以此类推。
-
到此为止,必要的类都已加载完毕,对象可以被创建。
4.1 首先,基类(A类)对象所有的基本类型都被设为默认值,对象引用设为null。
4.2 基类(A类)构造器被调用。
4.3 基类(A类)构造器和导出类(B类)构造器一样,以相同顺序执行。
4.4 在基类构造器完成之后,实例变量按照其次续被初始化。
4.5 最后构造器的其余部分被执行。