可以将一个类的定义放在另一个类的定义内部,这就是内部类。
创建内部类
创建内部类的方式就如同你想的一样——把类的定义置于外围类的里面。
public class A{
class B{
private int i = 1;
public int getI(){
return i;
}
}
public void f(){
B b = new B();
System.out.println(b.getI());
}
public static void main(String[] args) {
A a = new A();
a.f();
}
//输出:1
}
当我们在f()方法里面使用内部类的时候,这与使用普通类没什么不同。内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用,但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外内部类还拥有其外围类的所有元素的访问权。下面的例子说明了这点:
public class A{
int t = 2;
class B{
public int getAValue(){
return t;
}
}
public B getB(){
return new B();
}
public static void main(String[] args) {
A a = new A();
B b = a.getB();
System.out.println(b.getAValue());
}
//输出:2
}
所以内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。幸运的是编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外为类的对象相关联的情况下才能被创建(在内部类是非static类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都不需要程序员操心。
使用.this与.new
如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并接受到检査,因此没有任何运行时开销。下面的示例展示了如何使用.this:
public class DotThis{
public class Inner{
public DotThis getDotThis(){
return DotThis.this;
}
}
public Inner getInner(){
return new Inner();
}
public static void main(String[] args) {
DotThis d = new DotThis();
Inner i = d.getInner();
System.out.println(d == i.getDotThis());
}
//输出:true
}
有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new表达式中提供对其他外部类对象的引用,这是需要使用.new语法,就像下面这样:
public class Outer{
public class Inner{}
public static void main(String[] args) {
Outer o = new Outer();
Inner i = o.new Inner();
}
}
在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到创建它的外部类对象上。但是如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用 。
public class Outer {
public static class Inner{
public Inner() {
System.out.println("Inner init..");
}
}
public static void main(String[] args) {
Inner i = new Inner();
}
//输出:Inner init..
}
内部类与向上转型
当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类——某个接口的实现——能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,看下面的例子:
public class Parcel4 {
public interface Contents{}
public interface Destination{}
private class PContents implements Contents{}
protected class PDestination implements Destination{
private PDestination(){}
}
public PContents contents(){
return new PContents();
}
public PDestination destination(){
return new PDestination();
}
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination();
}
}
Parcel4中增加了一些新东西:内部类PContents 是private,所以除了Parcel4,没有人能访问它。 PDestination是protected,所以只有Parcel4及其子类、还有与Parcel4同一个包中的类(因为protected也给予了包访问权)能访问PDestination,其他类都不能访问PDestination。这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上甚至不能向下转型成Private内部类(或protected内部类,除非是继承自它的子类),因为不能访问其名字。于是,private内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给Java编译器提供了生成更高效代码的机会。
方法和作用域内的内部类
到目前为止,所看到的只是内部类的典型用造。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,筒单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。这么做有两个理由:
- 你实现了某类型的接口,于是可以创建并返回对其的引用。
- 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。
第一个例子展示了在方法的作用域内,创建一个完整的类。这被称作局部内部类,我们继续沿用上面的代码:
public class Parcel5 {
public Destination destination(){
class innerDestination implements Destination{
//此处的实现可以有效的隐藏
}
return new innerDestination();
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
p.destination();
}
}
下面的例子展示了如何在任意的作用域内嵌入一个内部类:
public class Parcel6 {
private Destination f(boolean b){
if(b){
class innerDestination implements Destination{
//此处的实现可以有效的隐藏
}
return new innerDestination();
}else{
class innerDestination implements Destination{
//此处另一种实现也可以有效的隐藏
}
return new innerDestination();
}
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
p.f(false);
}
}
局部内部类不能有访问说明符,因为它们不是外围类的一部分。但是它可以访问当前代码块内的常量,以及此外围类的所有成员。
匿名内部类
我们继续沿用上面的代码,看下面这个例子:
public class Parcel7 {
public Contents contents(){
return new Contents() {};
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Contents c = p.contents();
}
}
这个类是匿名的,他没有名字。更糟的是,看起来似乎是你正要创建一个Contents 对象。但是然后(到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。”这种奇径的语法指的是:“创建一个继承自Contents的匿名类的对象。通过new表达式返回的引用被自动向上转型为对Contents的引用。
在匿名内部类末尾的分号、并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此这与别的地方使用的分号是一致的。
匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现了接口,也只能实现一个接口。
嵌套类
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static。这通常称为嵌套类。想要理解static应用内部类时的含义,就必须记佳,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而当内部类是static的时候就不是这样了。嵌套类意味着:
- 要创建嵌套类的对象,并不需要其外围类的对象。
- 不能从嵌套类的对象中访问非静态的外围类对象。
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部装不能有static数据和static字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西。
public class Parcel8 {
public class Parcelson{
static int i = 1;//编译报错
}
static class Parcelson2{
static int i= 1;
};
}
接口内部的类
正常情况下,不能在接口内部放置任何代码,但嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是Public和static的。因为类是static的,只是将嵌套类置于接口的命名空问内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:
public interface Parcel9 {
void f();
class Test implements Parcel9{
@Override
public void f() {
System.out.println("Test.f()");
}
public static void main(String[] args) {
new Test().f();
}
}
}
为什么需要内部类
至此,我们已经看到了许多描述内部类的语法和语义,但是这并不能回答“为什么需要内部类”这个问题。那么,Sun公司为什么会如此费心地增加这项基本的悟言特性呢?
一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口 。
内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。”那么内部类实现一个接口与外围类实现这个接口有什么区别呢?答案是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以使用内部类最吸引人的原因是:
每个内部类都能独立地继承地继承一个(接口的)实观,所以无论外围类是否已经继承了某个(接口的)实观,对于内部类都没有影响。
如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问題就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说内部类允许继承多个非接口类型(类或抽象类)。
内部类的继承
因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外围类对象的“秘密的”引用必须被初始化,而在导出类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:
class WithInner{
class Inner{}
}
public class InherInner extends WithInner.Inner{
public InherInner(WithInner w){
w.super();
}
public static void main(String[] args) {
WithInner w = new WithInner();
InherInner i = new InherInner(w);
}
}
可以看到InherInner只继承自内部类而不是外围类,但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外必须在构造器内使用如下语法:
enclosingClassReference.super();
这样才提供了必要的引用,然后程序才能编译通过。
内部类标识符
由于每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个“meta-class”,叫做Class对象),你可能猜到了,内部类也必须生成一个.class文件以 包含它们的Class对象信息。这些类文件的命名有严格的规则:外围类的名字加上“$”,再加上内部类的名字。例如上例InherInner.java生成的.class文件包括:
InherInner.class
WithInner$Inner.class
WithInner.class
如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在別的内部类之中,只需直接将它们的名字加在其外围类标识符与“$”的后面。
虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是Java的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java编译器会尽可能地转换它们。)
- 本文来源《Java编程思想(第四版)》