文章目录
- 前言
- 一、创建内部类
- 二、链接外部类
- 三、使用.this 和.new
- 四、内部类与向上转型
- 五、内部类方法和作用域
- 六、匿名内部类
- 七、嵌套类
- 1. 接口内部的类
- 2. 从多层嵌套类中访问外部类的成员
- 八、继承内部类
- 九、局部内部类
- 总结
前言
一个定义在另一个类中的类,叫作内部类。内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不止如此,它了解外围类,并能与之通信。
一、创建内部类
创建内部类的方式就如同你想的一样,把类的定义置于外围类的里面:
public class Parcel1 {
class Contents {
private int i = 11;
public int value() {
return i;
}
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() {
return label;
}
}
public void ship(String dest) {
Contents c = new Contents();
Destination d = new Destination(dest);
System.out.println(d.readLabel());
}
public static void main(String[] args) {
Parcel1 p = new Parcel1();
p.ship("Tasmania");
}
}
在外部类方法中可以直接指明类型 InnerClassName,在其他类中需要指明OuterClassName.InnerClassName
二、链接外部类
到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用,但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。
public class External {
private String name;
public External(String name) {
this.name = name;
}
public void printName(){
System.out.println(name);
}
public class Internal{
public void get(){
System.out.println(name);
printName();
}
}
public Internal internal(){
return new Internal();
}
public static void main(String[] args) {
External external = new External("张三");
Internal internal = external.internal();
internal.get();
}
}
所以内部类自动拥有对其外围类所有成员的访问权,当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。幸运的是,编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外围类的对象相关联的情况下才能被创建(内部类是非static 类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。
三、使用.this 和.new
如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和 this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。
public class DotThis {
void f() {
System.out.println("DotThis.f()");
}
public class Inner {
public DotThis outer() {
return DotThis.this;
}
}
public Inner inner() {
return new Inner();
}
public static void main(String[] args) {
DotThis dt = new DotThis();
DotThis.Inner dti = dt.inner();
dti.outer().f();
}
}
有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在 new 表达式中提供对其他外部类对象的引用,这是需要使用 .new 语法,就像下面这样:
public class DotNew {
public class Inner {
}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
}
四、内部类与向上转型
当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。这是因为此内部类,某个接口的实现,能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。
public interface Destination {
String readLabel();
}
public interface Contents {
int value();
}
public class Parcel4 {
private class PContents implements Contents {
private int i = 11;
@Override
public int value() {
return i;
}
}
protected final class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
@Override
public String readLabel() {
return label;
}
}
public Destination destination(String s) {
return new PDestination(s);
}
public Contents contents() {
return new PContents();
}
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination("Tasmania");
}
}
在 Parcel4 中,内部类 PContents 是 private,所以除了 Parcel4,没有人能访问它。普通(非内部)类的访问权限不能被设为 private 或者 protected;他们只能设置为 public 或 package 访问权限。
PDestination 是 protected,所以只有 Parcel4 及其子类、还有与 Parcel4 同一个包中的类(因为protected 也给予了包访问权)能访问 PDestination,其他类都不能访问 PDestination,这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上,甚至不能向下转型成 private 内部类(或 protected 内部类,除非是继承自它的子类),因为不能访问其名字,就像在 TestParcel 类中看到的那样。private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会。
五、内部类方法和作用域
通常,如果所读、写的代码包含了内部类,那么它们都是 “平凡的” 内部类,简单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作
用域内定义内部类。这么做有两个理由:
- 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
- 想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。
在后面的例子中,先前的代码将被修改,以用来实现:
- 一个定义在方法中的类。
- 一个定义在作用域内的类,此作用域在方法的内部。
- 一个实现了接口的匿名类。
- 一个匿名类,它扩展了没有默认构造器的类。
- 一个匿名类,它执行字段初始化。
- 一个匿名类,它通过实例初始化实现构造(匿名内部类不可能有构造器)。
在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。这被称作局部内部类:
public class Parcel5 {
public Destination destination(String s) {
final class PDestination implements Destination {
private String lable;
public PDestination(String lable) {
this.lable = lable;
}
@Override
public String readLabel() {
return lable;
}
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel5 parcel5 = new Parcel5();
parcel5.destination("张");
}
}
在任意的作用域内嵌入一个内部类:
*/
public class Parcel6 {
private void internalTracking(boolean b) {
if (b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() {
return id;
}
}
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
}
}
public void track() {
internalTracking(true);
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
p.track();
}
}
六、匿名内部类
public class Parcel7 {
public Contents contents() {
return new Contents() {
private int i = 11;
@Override
public int value() {
return i;
}
};
}
public static void main(String[] args) {
Parcel7 parcel7 = new Parcel7();
Contents contents = parcel7.contents();
}
}
contents() 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。
如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的(也就是说,它在初始化后不会改变,所以可以被当作final)。
public abstract class Base {
Base(int i) {
System.out.println("Base constructor, i = " + i);
}
public abstract void f();
}
public class AnonymousConstructor {
public static Base getBase(int i) {
return new Base(i) {
{
System.out.println("Inside instance initializer");
}
@Override
public void f() {
System.out.println("In anonymous f()");
}
};
}
public static void main(String[] args) {
Base base = getBase(47);
base.f();
}
}
七、嵌套类
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static,这通常称为嵌套类。想要理解 static 应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是 static 的时,就不是这样了。嵌套类意味着:
- 要创建嵌套类的对象,并不需要其外围类的对象。
- 不能从嵌套类的对象中访问非静态的外围类对象。
嵌套类与普通的内部类还有一个区别。普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西:
public class Parcel11 {
private static class ParcelContents implements Contents {
private static int i = 11;
@Override
public int value() {
return i;
}
}
public static void main(String[] args) {
ParcelContents parcelContents = new ParcelContents();
}
}
1. 接口内部的类
嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 public 和 static 的。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:
public interface ClassInInterface {
void howdy();
class Test implements ClassInInterface {
@Override
public void howdy() {
System.out.println("Howdy!");
}
public static void main(String[] args) {
new Test().howdy();
}
}
}
2. 从多层嵌套类中访问外部类的成员
一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外围类的所有成员,如下所示:
public class MNA {
private void f() {
}
class A {
private void g() {
}
public class B {
void h() {
g();
f();
}
}
}
}
八、继承内部类
因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外围类对象的 “秘密的” 引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:
public class InheritInner extends WithInner.Inner{
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
可以看到,InheritInner 只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法:enclosingClassReference.super(); 这样才提供了必要的引用,然后程序才能编译通过。
九、局部内部类
前面提到过,可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分;但是它可以访问当前代码块内的常量,以及此外围类的所有成员。
public class LocalInnerClass {
private int count = 0;
Counter getCounter(final String name) {
class LocalCounter implements Counter {
LocalCounter() {
System.out.println("LocalCounter()");
}
@Override
public int next() {
System.out.print(name); // Access local final
return count++;
}
}
return new LocalCounter();
}
}
总结
比起面向对象编程中其他的概念来,接口和内部类更深奥复杂,比如 C++ 就没有这些。将两者结合起来,同样能够解决 C++ 中的用多重继承所能解决的问题。虽然这些特性本身是相当直观的,但是就像多态机制一样,这些特性的使用应该是设计阶段考虑的问题。