《 Thinking in Java 》第十章 内部类

本文深入探讨Java内部类的概念,包括其创建、使用及与外围类的交互方式。内部类提供了访问外部类成员的独特途径,能够独立继承接口或类,且与外围类的状态信息分离。文章还讨论了内部类的多种应用场景,如方法作用域内的内部类和匿名内部类。

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

可以将一个类的定义放在另一个类的定义内部,这就是内部类

  • 内部类与组合是完全不同的概念。
  • 内部类看起来像是一种代码隐藏机制,但是它还了解外围类,并能与之通信;
  • 更优雅!!!

创建内部类

很简单——把类的定义置于外围类的里面:

public class A {
	class InnerA {
	}
	public InnerA getInnerA() {
		return new InnerA();
	}
}

与使用普通类的方法,没什么不同,很典型的一个情况如上,外部类有一个方法,返回一个指向内部类的引用。
注意:如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须想这样来具体的指明这个对象的类型:

	//OuterClassName.InnerClassName
	public static void main(String[] args) {
		A.InnerA a  = new A().getInnerA();		
	}

链接到外部类

当生成一个内部类的对象时,次对象与制造它的外围对象之间就有了一种联系,所有它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。
这种能力是如何做到的:当某个外围类的对象创建了一个内部类对象时,次内部类对象必定会秘密的捕获一个指向那个外围类对象的引用。然后,在访问此外围类的成员时,就是用那个引用来选择外围类的成员。但是要注意,只有内部类的对象与其外围类的对象相关联的情况下才能被创建(也就是说 ,内部类是非 static 类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到就会报错。

使用 .this 与 .new

如果需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和 this 。这样产生的引用自动地具有正确的类型,这一点在编译器就被知晓并受到检查,因此没有任何运行时开销。

public  class A {
	class B {
		A getA() {
			return A.this;
		}
	}
}

如果想直接创建内部类对象,就需要使用外部类的对象来创建内部类对象。而不能声明 a.new A.b() ;

class A {
// .......
	public static void main(String[] args ) {
		A a = new A();
		A.B b = a.new B();
	}
}

在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到创建它的内部类对象上。但是,如果创建的是嵌套类(静态内部类),那么他就不需要对外部类对象的引用。

内部类与向上转型

当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此对象的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类——某个接口的实现——能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。
简单地说明这种实现

  • 创建公共接口 A 。
  • 在某一个类 B 中添加一个 private(或 protected )的 A 接口的实现类 AImpl 。
  • 在外部类提供一个 public 接口来返回一个 A 的引用。

优点:

  1. 除了 A 这个外围类,其它的类都不能访问或修改 AImpl。
  2. 通过公共接口取得的 A 的引用,由于无法访问到 B 中实现的 AImpl 名字,所以无法向下转型( 或protected 内部类,除非是继承自它的子类)。

在方法和作用域内的内部类

为什么要在一个方法里面或者在任意的作用域内定义内部类,理由有二:

  1. 实现了某类型的接口,于是可以创建并返回对其的引用。
  2. 希望创建一个不是公共可用的类来解决你的复杂的问题。

以下是一些可以放内部类的位置

  • 一个定义在方法中的类。
  • 一个定义在作用域中的类,次作用域在方法的内部。
  • 一个实现了接口的匿名类。
  • 一个匿名类,扩展了有非默认构造器的类。
  • 一个匿名类,它执行字段初始化。
  • 一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)。

一、展示在方法的作用域内创建一个完整的类。称作局部内部类

public class D {
	public A getA() {
		class AImpl implements A {
			//......
		}
		return new AImpl();
	}
	//.......
}

AImpl 类是方法的一部分,而不是 D 的一部分,,所以,在方法之外不能访问 AImpl 。虽然 AImpl 在方法中,但这并不意味方法执行完毕, AImpl 这个对象就不能用了。

在同一子类目下的任意类中对某个内部类使用类标识符 AImpl ,这并不会有命名冲突。
二、展示在任意作用域内嵌入一个内部类:

	public class A {
		private void f(boolean b) {
			if( b ) {
				class B {
					String getSlip() {
						return "1";
						} 
					//....
				}
				B b = new B();
				String str = b.getSlip();
			}
		}
	}

虽然这个类是放在 if 语句中,但是并不意味着创建需要条件,它其实与别的类一起编译过了。但是,在定义这个类的作用域之外,它是不可用的;初次之外,它与普通的类一样。

匿名内部类

public class B {
	public A newA() {
		return new A() {
			private int i = 11;
		};
	}
	//.....
}

newA() 方法将返回值的生成与表示这个返回值的类的定义结合在一起。另外这个类是匿名的。
表示的是:创建一个继承自 A 的匿名类的对象。通过 new 表达式返回的引用被自动向上转型为对 A 的引用。
在这个匿名类中,使用了默认的构造器来生成 A 。下面展示,如果基类需要带参构造器该怎么办:(注意这里区别于匿名类构造器)

public class B {
	public A newA(int x) {
		return new A(x) {
			private int i = 11;
			public int value() {
				return super.value();
			}
		};
	}
	//.....
}

只需要简单地传递合适的参数给基类的构造器即可。

如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的。

如果需要构造器的行为,因为匿名类中不可能有命名构造器(因为它根本没有名字!),但通过实例初始化,就能够达到为匿名类创建一个构造器的效果,like this:

abstract class Base {
	public Base(int i) {
		//......
	}
}
public class A {
	public static Vase getBase(int i) {
	return new Base(i) {
		{
		public void f() {};
			//.....
		}
	};
}

在这个例子中,不要求变量 i 一定是 final 的。因为 i 被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。
如果在匿名类内部使用,则要求传入的参数是 final 的。
当然它收到了限制——不能重载实例初始化方式,所以仅有一个这样的构造器。


匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备,而且如果是实现接口,也只能实现一个接口。

嵌套类

如果不需要内部类对象与其外围类之间有联系,那么可以将内部类声明为 static 。这通常成为嵌套类
普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象,然而,当内部类是 static 的时,就不是这样

  1. 要创建嵌套类的对象,并不需要其外围类的对象。
  2. 不能从嵌套类的对象中访问非静态的外围类对象。

还有一个区别就是,普通的内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西。

接口内部的类

放到接口中的任何类都自动是 public 和 static 的。因此类是 static 的,只是嵌套类置于接口的命名空间内,并不违反接口的规则。甚至可以在内部类中实现其外围类接口。

从多层嵌套类中访问外部类的成员

一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外围类的所有成员。

Class A {
	private void g(){}
	public class B {
		private void f(){}
		class C {
			void h() {
				g();
				f();
			}
		}
	}	
}
public class Test {
	public static void main(String[] args) {
		A a = new A();
		A.B b = a.new B();
		A.B.C c = b.new C();
		c.h();
	}
}

“.new” 语法能产生正确的作用域,所以不必在调用构造器时限定类名。

为什么需要内部类

一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。
使用内部类最吸引人的原因是:

  • 每个内部类都能独立地继承自一个接口的实现,所以无论外围类是否已经继承了某个接口的实现,对于内部类都没有影响。

使用内部类可以获得的一些特性:

  1. 内部类可以有多个实例,每个实例都有自己的状态信息,并且与外围类对象的信息相互独立。
  2. 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
  3. 创建内部类对象的时刻并不依赖于外围类对象的创建。
  4. 内部类没有令人迷惑的”is-a“关系,它就是一个独立的实体。

闭包与回调

请参考其他更加深入的资料。

内部类的继承

因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外围类对象的“秘密”引用必须被初始化,而在导出类中不再存在可连接的默认对象。
默认构造器不好用,而且不能只是传递一个指向外围类对象的引用,此外,必须在构造器使用如下语法

class WithInner {
	class Inner{}
}
public class InheritInner extends WithInner.Inner {
	InheritInner(WithInner wi) {
		enclosingClassReference.super();
	}
	public static void main(String[] args ) {
		WhitInner wi = new WithInner();
		InheritInner li = new InheritInner(wi);
	}

内部类可以被覆盖吗

如果创建了一个内部类,然后继承其外围类并重新定义次内部类时,其实并不会起什么作用。
因为他们各自有各自的命名空间,是两个完全独立的实体。当然,如果明确的继承某个内部类确实可以的。

class A {
	protected class B { }
}

class A2 extends A {
	public class B2 extends A.B {}
}

这样是可以发生覆盖的。

局部内部类

一如前面提到的,可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为他不是外围类的一部分;但是它可以访问当前代码块的常量,以及此外围类的所有成员。
所以使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。

内部类标识符

每个类都会产生一个 .class 文件,其中包含了如何创建该类型的对象的全部信息(信息产生一个“meta-class”,叫做 Class 对象),内部类也必须生成这两个。这些类文件名字有严格的规则:外围类的名字,加上“$” 再加上内部类的名字。如果是匿名内部类,编译器会简单地产生一个数字作为其标识符。

写在前面的话 引言 1. 前提 2. Java的学习 3. 目标 4. 联机文档 5. 章节 6. 练习 7. 多媒体 8. 源代码 9. 编码样式 10. Java版本 11. 课程和培训 12. 错误 13. 封面设计 14. 致谢 第1章 对象入门 1.1 抽象的进步 1.2 对象的接口 1.3 实现方案的隐藏 1.4 方案的重复使用 1.5 继承:重新使用接口 1.5.1 改善基础类 1.5.2 等价和类似关系 1.6 多形对象的互换使用 1.6.1 动态绑定 1.6.2 抽象的基础类和接口 1.7 对象的创建和存在时间 1.7.1 集合与继承器 1.7.2 单根结构 1.7.3 集合库与方便使用集合 1.7.4 清除时的困境:由谁负责清除? 1.8 违例控制:解决错误 1.9 多线程 1.10 永久性 1.11 Java和因特网 1.11.1 什么是Web? 1.11.2 客户端编程 1.11.3 服务器端编程 1.11.4 一个独立的领域:应用程序 1.12 分析和设计 1.12.1 不要迷失 1.12.2 阶段0:拟出一个计划 1.12.3 阶段1:要制作什么? 1.12.4 阶段2:开始构建? 1.12.5 阶段3:正式创建 1.12.6 阶段4:校订 1.12.7 计划的回报 1.13 Java还是C++? 第2章 一切都是对象 2.1 用句柄操纵对象 2.2 必须创建所有对象 2.2.1 保存在什么地方 2.2.2 特殊情况:主类型 2.2.3 Java中的数组 2.3 绝对不要清除对象 2.3.1 作用域 2.3.2 对象的作用域 2.4 新建数据类型:类 2.4.1 字段和方法 2.5 方法、自变量和返回值 2.5.1 自变量列表 2.6 构建Java程序 2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档标记 2.8.7 方法文档标记 2.8.8 文档示例 2.9 编码样式 2.10 总结 2.11 练习 第3章 控制程序流程 3.1 使用Java运算符 3.1.1 优先级 3.1.2 赋值 3.1.3 算术运算符 3.1.4 自动递增和递减 3.1.5 关系运算符 3.1.6 逻辑运算符 3.1.7 按位运算符 3.1.8 移位运算符 3.1.9 三元if-else运算符 3.1.10 逗号运算符 3.1.11 字串运算符 3.1.12 运算符常规操作规则 3.1.13 造型运算符 3.1.14 Java没有“sizeof” 3.1.15 复习计算顺序 3.1.16 运算符总结 3.2 执行控制 3.2.1 真和假 3.2.3 反复 3.2.6 中断和继续 3.2.7 切换 3.3 总结 3.4 练习 第4章 初始化和清除 4.1 由构建器保证初始化 4.2 方法过载 4.2.1 区分过载方法 4.2.2 主类型的过载 4.2.3 返回值过载 4.2.4 默认构建器 4.2.5 this关键字 4.3 清除:收尾和垃圾收集 4.3.1 finalize()用途何在 4.3.2 必须执行清除 4.4 成员初始化 4.4.1 规定初始化 4.4.2 构建器初始化 4.5 数组初始化 4.5.1 多维数组 4.6 总结 4.7 练习 第5章 隐藏实施过程 5.1 包:库单元 5.1.1 创建独一无二的包名 5.1.2 自定义工具库 5.1.3 利用导入改变行为 5.1.4 包的停用 5.2 Java访问指示符 5.2.1 “友好的” 5.2.2 public:接口访问 5.2.3 private:不能接触 5.2.4 protected:“友好的一种” 5.3 接口与实现 5.4 类访问 5.5 总结 5.6 练习 第6章 类再生 6.1 合成的语法 6.2 继承的语法 6.2.1 初始化基础类 6.3 合成与继承的结合 6.3.1 确保正确的清除 6.3.2 名字的隐藏 6.4 到底选择合成还是继承 6.6 递增开发 6.7 上溯造型 6.7.1 何谓“上溯造型”? 6.8 final关键字 6.8.1 final数据 6.8.2 final方法 6.8.3 final类 6.8.4 final的注意事项 6.9 初始化和类装载 6.9.1 继承初始化 6.10 总结 6.11 练习 第7章 多形性 7.1 上溯造型 7.1.1 为什么要上溯造型 7.2 深入理解 7.2.1 方法调用的绑定 7.2.2 产生正确的行为 7.2.3 扩展性 7.3 覆盖与过载 7.4 抽象类和
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值