可以将一个类的定义放在另一个类的定义内部,这就是内部类
- 内部类与组合是完全不同的概念。
- 内部类看起来像是一种代码隐藏机制,但是它还了解外围类,并能与之通信;
- 更优雅!!!
创建内部类
很简单——把类的定义置于外围类的里面:
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 的引用。
优点:
- 除了 A 这个外围类,其它的类都不能访问或修改 AImpl。
- 通过公共接口取得的 A 的引用,由于无法访问到 B 中实现的 AImpl 名字,所以无法向下转型( 或protected 内部类,除非是继承自它的子类)。
在方法和作用域内的内部类
为什么要在一个方法里面或者在任意的作用域内定义内部类,理由有二:
- 实现了某类型的接口,于是可以创建并返回对其的引用。
- 希望创建一个不是公共可用的类来解决你的复杂的问题。
以下是一些可以放内部类的位置
- 一个定义在方法中的类。
- 一个定义在作用域中的类,次作用域在方法的内部。
- 一个实现了接口的匿名类。
- 一个匿名类,扩展了有非默认构造器的类。
- 一个匿名类,它执行字段初始化。
- 一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)。
一、展示在方法的作用域内创建一个完整的类。称作局部内部类
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 的时,就不是这样
- 要创建嵌套类的对象,并不需要其外围类的对象。
- 不能从嵌套类的对象中访问非静态的外围类对象。
还有一个区别就是,普通的内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 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” 语法能产生正确的作用域,所以不必在调用构造器时限定类名。
为什么需要内部类
一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。
使用内部类最吸引人的原因是:
- 每个内部类都能独立地继承自一个接口的实现,所以无论外围类是否已经继承了某个接口的实现,对于内部类都没有影响。
使用内部类可以获得的一些特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与外围类对象的信息相互独立。
- 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
- 创建内部类对象的时刻并不依赖于外围类对象的创建。
- 内部类没有令人迷惑的”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 对象),内部类也必须生成这两个。这些类文件名字有严格的规则:外围类的名字,加上“$” 再加上内部类的名字。如果是匿名内部类,编译器会简单地产生一个数字作为其标识符。