Java:内部类如何被继承与覆盖

1 内部类的继承

此处介绍的是外围类继承内部类
继承内部类时有件事是我们必须要做的,那就是秘密地连接其外围类的对象,和普通内部类一样,如果外围类对象不存在,内部类的对象也就无法被创建,这在逻辑上是合理的,因为如果外围类对象不存在,我们又如何利用内部类去访问外围类的成员呢?
正是因为有上述需要完成的任务,我们在继承内部类时就会复杂些,不同于以往,示例如下:

class Outer {
	public class Inner {}
}

public class Test extends Outer.Inner { // 需要用外围类名称来访问内部类名称
	private int label = 810;
	Test(Outer o) { // 构造器接受外围类的对象
		o.super(); // 借用外围类对象来初始化内部类
	}
	public void readLabel() {
		System.out.println(label);
	}
	public static void main(String[] args) {
		Outer o = new Outer();
		Test t = new Test(o); // 传递外围类对象给 Test 构造器
		t.readLabel();
	}
}
/* Output
810
*/ 

解析:首先我们要为继承了内部类的类定义一个新的构造器:这个构造器接受外围类的对象,然后写上 enclosingClassReference.super(),经过这几步操作,才能确保被继承的内部类也被初始化进而编译通过,也就使内部类对象连接了外围类对象,从而确保了 Test 类的上层类都被初始化完毕,最终初始化 Test 类对象,从而创建成功。

注意:如果 A 类继承 B 类,在我们创建了 A 类对象时实际上也创建了 B 类的对象,大家可以试一下:当我们创建继承了 B 类的 A 类对象时,上层的类是否被初始化。比如在本例中,我们初始化 Test 类,就必须初始化它继承的内部类,而初始化内部类就必须先初始化外围类,因此才有了继承内部类的类初始化的这种特殊格式。

注意:上述中这种对继承了内部类的具体类初始化的形式是必须的,如果我们不在构造器中使用外围类对象来初始化内部类,那编译器就会报错,错误信息如下:
No enclosing instance of type Outer is available due to some intermediate constructor invocation
所以当我们要创建继承了内部类的类对象时,必须按照上述的方式来改写这个类的构造器,使得上层的类都被初始化,如果只使用默认构造器而不定义特殊的构造器,编译器就会报错。

2 内部类可以被覆盖吗

如果我们继承一个包含内部类的外围类,并且在新的类中修改这个内部类会发生什么呢?内部类又是否会被覆盖呢?
答案是不会被覆盖,而是在新类的命名空间内新定义了一个内部类,即使这两个内部类同名,示例如下:

class Outer {
	Outer() { // 外围类构造器
		System.out.println("new Outer");
		new Inner();
	}
	public class Inner { 
		Inner() { System.out.println("Outer.Inner"); } // 内部类构造器
	}
}

public class Test extends Outer {
	public class Inner { 
		Inner() { System.out.println("Test.Inner"); } // 新类中内部类的构造器
	}
	public static void main(String[] args) {
		new Test(); // 创建新类的对象
	}
}
/* Output:
new Outer
Outer.Inner
*/

解析:上述代码中我们首先创建了一个包含内部类的外围类,它们都有自己的构造器,并且在创建时会显示相关信息以表示操作类型与归属。然后我们定义一个继承上面的外围类的新类,再“修改”它的内部类。可是最终结果显示,我们调用的并不是新类的内部类,而是基类的内部类。
常见的覆盖我们都知道,上层引用最终会按照下层的相应方法实现来执行程序,这也正是多重继承的魅力,但是在这个程序中,它调用的却是上层的内部类,这里要先阐述下关于覆盖时对象创建的过程,如下:

  1. 创建类对象并调用这个类的构造器
  2. 构造器逐层调用基类的构造器
  3. 一直执行步骤 2 直至到达最高层类的构造器
  4. 到达最高层构造器后再逐层向下依次执行构造器代码(在步骤 4 之前所有的构造器没有执行自己代码,仅仅只是不断往上层调用构造器)

在我们了解构造过程后就会发现,当我们创建 Test 对象时会调用 Test 类的默认构造器,这个默认构造器会调用 Outer 类的命名构造器,在其中我们先打印 “new Outer”,然后新建一个内部类对象,然而新建的对象的并非是 “覆盖” 后的内部类,而是 Test “覆盖”后的内部类,因为它显示的构造器信息是 “Outer.Inner”。

结论:“覆盖” 内部类并不会按照我们所想的去实现,它并不是真的“覆盖,而是在新类中创建了一个新的内部类,它与被 “覆盖” 的内部类仅仅是名字相同,但他们处于不同的命名空间,是完全独立的两个实体。

3 用内部类继承内部类

在上面的标题 1 中我们使用了外围类来继承内部类,那内部类又该如何继承内部类呢?
这里继承的类名可以与被继承的内部类类名相同,因为它们属于不同的命名空间。示例如下:

class Egg2 {
	protected class Yolk {
		public Yolk() { System.out.println("Egg2.Yolk()"); }
		public void f() { System.out.println("Egg2.Yolk.f()"); }
	}
	private Yolk y = new Yolk(); 
	public Egg2() { System.out.println("New Egg2()"); }
	public void insertYolk(Yolk yy) { y = yy; }
	public void f() { System.out.println("Egg2.Yolk.f()"); }
	public void g() { y.f(); }
}

public class BigEgg2 extends Egg2 {
	public class Yolk extends Egg2.Yolk { // BigEgg2.Yolk 类继承了 Egg2.Yolk 类
		public Yolk() { System.out.println("BigEgg2.Yolk()"); }
		public void f() { System.out.println("BigEgg2.Yolk.f()"); }
	}
	public BigEgg2() { insertYolk(new Yolk()); }
	// 测试
	public static void main(String[] args) {
		Egg2 e2 = new BigEgg2();
		e2.g();
	}
}
/* output:
Egg2.Yolk() // 第 1 行信息
New Egg2() // 第 2 行信息
Egg2.Yolk() // 第 3 行信息
BigEgg2.Yolk() // 第 4 行信息
BigEgg2.Yolk.f() // 第 5 行信息
*/

解析:从上述代码的运行结果中我们可以看到,当我们创建 BigEgg2 类对象时,过程如下:
(1)先调用它的构造器,然后这个构造器会调用基类(即 Egg2类)的构造器
(2)在基类对象的初始化中,我们首先初始化普通的成员 y(产生第 1 行信息),然后执行构造器 Egg2() 的代码(产生第 2 行信息
(3)再回到 BigEgg2 的构造器部分去执行 insertYolk() 方法的参数 new Yolk(),它会先调用 BigEgg2 的 Yolk 类的构造器,这个构造器会调用基类构造器(即 Egg2 的 Yolk 类的构造器)的代码(产生第 3 行信息
(4)再回到并执行 BigEgg2 的 Yolk 构造器的代码(产生第 4 行信息),至此 new Yolk() 创建结束
(5)最终执行方法 insertYolk()(产生第 5 行信息)(注意此处在方法中调用的是继承类 BigEgg2 的 f() 方法)。

这段代码的重要之处在于要能够对初始化在继承中的顺序有深刻的理解,如果你对这个顺序还有模糊之处,那这种代码会成为使用者的噩梦,因为你无法预知创建类时的构造顺序及信息显示。

备注

更多细节可以看 Bruce Eckel 所著的《Java 编程思想》,欢迎大家一起学习探讨。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值