10 内部类
可以将一个类的定义放在另一个类的定义内部,这就是内部类。内部类是一种非常有用的特性,它允许你把一些逻辑相关的类组织在一起,并控制位于内部类的可视性。内部类与组合是两种完全不同的概念。内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是内部类远不止于此,它了解外围类,并能与之通信;而且你用内部类写出的代码更加清晰优雅。
10.1 创建内部类
创建内部类的方式就是把类的定义置于外围类里面:
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; }
}
// Using inner classes looks just like
// using any other class, within Parcel1:
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");
}
} /* Output:
Tasmania
*///:~
ship()方法在使用内部类的时候,和使用普通类没有什么区别。更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用。
//: innerclasses/Parcel2.java
// Returning a reference to an inner class.
public class Parcel2 {
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 Destination to(String s) {
return new Destination(s);
}
public Contents contents() {
return new Contents();
}
public void ship(String dest) {
Contents c = contents();
Destination d = to(dest);
System.out.println(d.readLabel());
}
public static void main(String[] args) {
Parcel2 p = new Parcel2();
p.ship("Tasmania");
Parcel2 q = new Parcel2();
// Defining references to inner classes:
Parcel2.Contents c = q.contents();
Parcel2.Destination d = q.to("Borneo");
}
} /* Output:
Tasmania
*///:~
10.2 链接到外部类
生成一个内部类对象时,此对象与制造它的外围对象之间就有了一种联系,它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。
内部类自动拥有对其外围类所有成员的访问权。这是当外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在访问此外围类的成员时,就是用那个引用来选择外围类成员。编译器会帮助处理所有的细节。内部类对象只能在与其外围类的对象相关联的情况下才能被创建(内部类是非static类时)。
10.3 使用.this与.new
如果需要生成对外部类对象的引用,可以使用外部类名字后面紧跟圆点和this。例如
//: innerclasses/DotThis.java
// Qualifying access to the outer-class object.
public class DotThis {
void f() { System.out.println("DotThis.f()"); }
public class Inner {
public DotThis outer() {
return DotThis.this;
// A plain "this" would be Inner's "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();
}
} /* Output:
DotThis.f()
*///:~
如果需要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,必须在new表达式中提供对其他外部类对象的引用,这需要使用.new语法。例如:
//: innerclasses/DotNew.java
// Creating an inner class directly using the .new syntax.
public class DotNew {
public class Inner {}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
} ///:~
在拥有外部类对象之前是不可能创建内部类对象的。这是应为内部类对象会暗暗地链接到创建它的外部类对象上。但是如果创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用。
10.4 内部类与向上转型
当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。这是因为此内部类——某个接口的实现——能够完全不可见,并且不可用。所以得到的只是指向基类或接口的引用,所以能够很方便的隐藏实现细节。
例如
public interface Destination{
String readLabel();
}
public interface Contents{
int value();
}
当取得了一个指向基类或接口的引用时,甚至可能无法找到确切的类型
//: innerclasses/TestParcel.java
class Parcel4 {
private class PContents implements Contents {
private int i = 11;
public int value() { return i; }
}
protected class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public Destination destination(String s) {
return new PDestination(s);
}
public Contents contents() {
return new PContents();
}
}
public class TestParcel {
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination("Tasmania");
// Illegal -- can't access private class:
//! Parcel4.PContents pc = p.new PContents();
}
} ///:~
10.5 在方法和作用域内的内部类
前述的内部类是内部类的典型用法,然而内部类还覆盖了其他大量难以理解的技术,例如在一个方法里或者作用域内定义内部类。这被称之为局部内部类。这种做法的缘由是:实现了某类型的接口,可以创建并返回对其的引用或者想要解决一个复杂问题,创建一个类来辅助但是又不希望这个类是公共可用的时候。
例如在方法内定义类:
//: innerclasses/Parcel5.java
// Nesting a class within a method.
public class Parcel5 {
public Destination destination(String s) {
class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
Destination d = p.destination("Tasmania");
}
} ///:~
或者在任意作用域内嵌入一个内部类
//: innerclasses/Parcel6.java
// Nesting a class within a scope.
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();
}
// Can't use it here! Out of scope:
//! TrackingSlip ts = new TrackingSlip("x");
}
public void track() { internalTracking(true); }
public static void main(String[] args) {
Parcel6 p = new Parcel6();
p.track();
}
} ///:~
内部类被嵌入在if语句的作用域内,这个类与其他类都一起被编译了,但是在作用域之外,这个类是不可用的,除此之外与其他类一样。
10.6 匿名内部类
匿名内部类示例:
//: innerclasses/Parcel7.java
// Returning an instance of an anonymous inner class.
public class Parcel7 {
public Contents contents() {
return new Contents() { // Insert a class definition
private int i = 11;
public int value() { return i; }
}; // Semicolon required in this case
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Contents c = p.contents();
}
} ///:~
contests()方法将返回值的生成与表示这个返回值的类定义结合在一起,这种语法是指:“创建一个继承自Contents的匿名内部类的对象”,通过new表达式返回的引用被自动向上转型为对Contents的引用,前述的简化形式如下:
//: innerclasses/Parcel7b.java
// Expanded version of Parcel7.java
public class Parcel7b {
class MyContents implements Contents {
private int i = 11;
public int value() { return i; }
}
public Contents contents() { return new MyContents(); }
public static void main(String[] args) {
Parcel7b p = new Parcel7b();
Contents c = p.contents();
}
} ///:~
这个匿名内部类是使用了默认的构造器来生成Contents.如果基类需要一个有参数的构造器呢,示例如下:
//: innerclasses/Parcel8.java
// Calling the base-class constructor.
public class Parcel8 {
public Wrapping wrapping(int x) {
// Base constructor call:
return new Wrapping(x) { // Pass constructor argument.
public int value() {
return super.value() * 47;
}
}; // Semicolon required
}
public static void main(String[] args) {
Parcel8 p = new Parcel8();
Wrapping w = p.wrapping(10);
}
} ///:~
在匿名类定义字段时,还可以对其进行初始化操作,例如:
//: innerclasses/Parcel9.java
// An anonymous inner class that performs
// initialization. A briefer version of Parcel5.java.
public class Parcel9 {
// Argument must be final to use inside
// anonymous inner class:
public Destination destination(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.destination("Tasmania");
}
} ///:~
如果定义一个匿名内部类,希望它使用一个其在外部定义的对象,那么编译器会要求其参数引用是final的,否则会得到编译错误信息。例如上述的示例,参数String dest必须有final修饰。
如果想要在匿名内部类中做一些构造器的行为,但是匿名内部类不可能有命名构造器,可以通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果。例如:
//: innerclasses/AnonymousConstructor.java
// Creating a constructor for an anonymous inner class.
import static net.mindview.util.Print.*;
abstract class Base {
public Base(int i) {
print("Base constructor, i = " + i);
}
public abstract void f();
}
public class AnonymousConstructor {
public static Base getBase(int i) {
return new Base(i) {
{ print("Inside instance initializer"); }
public void f() {
print("In anonymous f()");
}
};
}
public static void main(String[] args) {
Base base = getBase(47);
base.f();
}
} /* Output:
Base constructor, i = 47
Inside instance initializer
In anonymous f()
*///:~
上述例子中变量i不一定是final,因为这个i被传递给匿名类的基类的构造器,不会被匿名类内部直接使用。如果是匿名类内部直接使用,必须用final修饰。
10.7 嵌套类(静态内部类)
不需要内部类与外围类对象之间有联系,那么可以将内部类声明为static,这通常称之为嵌套类。普通的内部类隐式的保存了一个引用,指向创建它的外围类对象,然而内部类是static的时候,就不是这样了。意味着:
- 创建嵌套类对象,不需要外围类的对象
- 不能从嵌套类的对象中访问非静态的外围类对象
普通内部类不能有static数据和static字段,也不能包含嵌套类,但是嵌套类可以包含这些东西。
示例:
//: innerclasses/Parcel11.java
// Nested classes (static inner classes).
public class Parcel11 {
private static class ParcelContents implements Contents {
private int i = 11;
public int value() { return i; }
}
protected static class ParcelDestination
implements Destination {
private String label;
private ParcelDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
// Nested classes can contain other static elements:
public static void f() {}
static int x = 10;
static class AnotherLevel {
public static void f() {}
static int x = 10;
}
}
public static Destination destination(String s) {
return new ParcelDestination(s);
}
public static Contents contents() {
return new ParcelContents();
}
public static void main(String[] args) {
Contents c = contents();
Destination d = destination("Tasmania");
}
} ///:~
10.7.1 接口内部类
正常情况下,接口内部是不能放置任何代码,但是嵌套类可以作为接口的一部分,放到接口中的任何类都自动是public和static(自己在eclipse中则显示没有默认添加,在编程时最好手动加上)。类是static,只是将嵌套类置于接口命名空间内,不违反接口的规则。甚至可以在内部类实现外围接口。
示例:
//: innerclasses/ClassInInterface.java
// {main: ClassInInterface$Test}
public interface ClassInInterface {
void howdy();
static class Test implements ClassInInterface {
public void howdy() {
System.out.println("Howdy!");
}
public static void main(String[] args) {
new Test().howdy();
}
}
} /* Output:
Howdy!
*///:~
如果想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,就可以使用接口内部的嵌套类就会显得很方便。
关于嵌套类的另一种用法是用来放置测试代码。
示例:
public clss TestBed{
public void f(){System.out.println("f()")}
public static class Tester{
public static void main(String[]args){
TestBed t = new TestBed();
t.f();
}
}
}
10.7.2 多层嵌套类中访问外部类成员
一个内部类不管被嵌套多少层,都可以透明地访问它所嵌入的外围类的所有成员
示例:
//: innerclasses/MultiNestingAccess.java
// Nested classes can access all members of all
// levels of the classes they are nested within.
class MNA {
private void f() {}
class A {
private void g() {}
public class B {
void h() {
g();
f();
}
}
}
}
public class MultiNestingAccess {
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
} ///:~
10.8 为什么需要内部类
一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。
内部类实现一个接口与外围类实现这个接口的区别是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以使用内部类最主要的原因是:每个内部类都能独立地继承自一个(接口)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类是没有影响的。(接口解决了部分问题,而内部类有效地实现了多重继承)。
例如:一个类中以某种方式实现两个接口,有两种选择,一种是使用单一类,另一种是使用内部类:
//: innerclasses/MultiInterfaces.java
// Two ways that a class can implement multiple interfaces.
package innerclasses;
interface A {}
interface B {}
class X implements A, B {}
class Y implements A {
B makeB() {
// Anonymous inner class:
return new B() {};
}
}
public class MultiInterfaces {
static void takesA(A a) {}
static void takesB(B b) {}
public static void main(String[] args) {
X x = new X();
Y y = new Y();
takesA(x);
takesA(y);
takesB(x);
takesB(y.makeB());
}
} ///:~
如果没有其他限制条件,二者并没有什么特殊区别,都能够正常运作。如果拥有的是抽象的类或具体的类,不是接口,就只能使用内部类才能够实现多重继承。
//: innerclasses/MultiImplementation.java
// With concrete or abstract classes, inner
// classes are the only way to produce the effect
// of "multiple implementation inheritance."
package innerclasses;
class D {}
abstract class E {}
class Z extends D {
E makeE() { return new E() {}; }
}
public class MultiImplementation {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args) {
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
} ///:~
10.8.1 闭包与回调
闭包(closure)就是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。
java中最引人争议的问题之一是人们认为java应该包含某种类似指针机制,以允许回调。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。
例如:
//: innerclasses/Callbacks.java
// Using inner classes for callbacks
package innerclasses;
import static net.mindview.util.Print.*;
interface Incrementable {
void increment();
}
// Very simple to just implement the interface:
class Callee1 implements Incrementable {
private int i = 0;
public void increment() {
i++;
print(i);
}
}
class MyIncrement {
public void increment() { print("Other operation"); }
static void f(MyIncrement mi) { mi.increment(); }
}
// If your class must implement increment() in
// some other way, you must use an inner class:
class Callee2 extends MyIncrement {
private int i = 0;
public void increment() {
super.increment();
i++;
print(i);
}
private class Closure implements Incrementable {
public void increment() {
// Specify outer-class method, otherwise
// you'd get an infinite recursion:
Callee2.this.increment();
}
}
Incrementable getCallbackReference() {
return new Closure();
}
}
class Caller {
private Incrementable callbackReference;
Caller(Incrementable cbh) { callbackReference = cbh; }
void go() { callbackReference.increment(); }
}
public class Callbacks {
public static void main(String[] args) {
Callee1 c1 = new Callee1();
Callee2 c2 = new Callee2();
MyIncrement.f(c2);
Caller caller1 = new Caller(c1);
Caller caller2 = new Caller(c2.getCallbackReference());
caller1.go();
caller1.go();
caller2.go();
caller2.go();
}
} /* Output:
Other operation
1
1
2
Other operation
2
Other operation
3
*///:~
上述例子展示了外围类实现一个接口与内部类实现该接口之间的差别。Callee1是简单的实现方式,Callee2继承了MyIncrement,后者已经有了一个不同的increment()方法,如果Callee2继承了MyIncrement,就不能为了Incrementable的用途而覆盖increment()方法,于是就只能使用内部类,独立地实现Incrementable。
10.8.2 内部类与控制框架
应用控制框架就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的提供的通用解决方案,以解决特定的问题。
控制框架是一类特殊的应用程序框架,用来解决响应事件的需求。主要用来响应事件的系统被称作为事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),几乎完全是事件驱动的系统。Java Swing库就是一个控制框架,优雅地解决了GUI为题,并使用了大量的内部类。关于这部分在22章一并详细讨论。
10.9 内部类的继承
内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得复杂。指向外围类对象的秘密的引用必须被初始化,而在导出类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联。
例如:
//: innerclasses/InheritInner.java
// Inheriting an inner class.
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // Won't compile
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
} ///:~
InheritInner只继承自内部类,不是外围类,但是当要生成一个构造器时,必须在构造器内使用如下语法:enclosingClassReference.super()
这样才提供了必要的引用,编译才会通过。
10.10 内部类可以被覆盖吗
如果创建了一个内部类,然后继承其外围类并重新定义了此内部类时,内部类可以被覆盖吗?
例如:
//: innerclasses/BigEgg.java
// An inner class cannot be overriden like a method.
import static net.mindview.util.Print.*;
class Egg {
private Yolk y;
protected class Yolk {
public Yolk() { print("Egg.Yolk()"); }
}
public Egg() {
print("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg {
public class Yolk {
public Yolk() { print("BigEgg.Yolk()"); }
}
public static void main(String[] args) {
new BigEgg();
}
} /* Output:
New Egg()
Egg.Yolk()
*///:~
覆盖内部类就好像他的外围类的一个方法,其实并不起什么作用。这个例子说明,当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在各自的命名空间内。当然,明确地继承某个内部类也是可以的:
//: innerclasses/BigEgg2.java
// Proper inheritance of an inner class.
import static net.mindview.util.Print.*;
class Egg2 {
protected class Yolk {
public Yolk() { print("Egg2.Yolk()"); }
public void f() { print("Egg2.Yolk.f()");}
}
private Yolk y = new Yolk();
public Egg2() { print("New Egg2()"); }
public void insertYolk(Yolk yy) { y = yy; }
public void g() { y.f(); }
}
public class BigEgg2 extends Egg2 {
public class Yolk extends Egg2.Yolk {
public Yolk() { print("BigEgg2.Yolk()"); }
public void f() { print("BigEgg2.Yolk.f()"); }
}
public BigEgg2() { insertYolk(new Yolk()); }
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g();
}
} /* Output:
Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()
*///:~
10.11 局部内部类
在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分,但是他可以访问当前代码块内的常量,以及此外围类的所有成员。
例如:
//: innerclasses/LocalInnerClass.java
// Holds a sequence of Objects.
import static net.mindview.util.Print.*;
interface Counter {
int next();
}
public class LocalInnerClass {
private int count = 0;
Counter getCounter(final String name) {
// A local inner class:
class LocalCounter implements Counter {
public LocalCounter() {
// Local inner class can have a constructor
print("LocalCounter()");
}
public int next() {
printnb(name); // Access local final
return count++;
}
}
return new LocalCounter();
}
// The same thing with an anonymous inner class:
Counter getCounter2(final String name) {
return new Counter() {
// Anonymous inner class cannot have a named
// constructor, only an instance initializer:
{
print("Counter()");
}
public int next() {
printnb(name); // Access local final
return count++;
}
};
}
public static void main(String[] args) {
LocalInnerClass lic = new LocalInnerClass();
Counter
c1 = lic.getCounter("Local inner "),
c2 = lic.getCounter2("Anonymous inner ");
for(int i = 0; i < 5; i++)
print(c1.next());
for(int i = 0; i < 5; i++)
print(c2.next());
}
} /* Output:
LocalCounter()
Counter()
Local inner 0
Local inner 1
Local inner 2
Local inner 3
Local inner 4
Anonymous inner 5
Anonymous inner 6
Anonymous inner 7
Anonymous inner 8
Anonymous inner 9
*///:~
10.12 内部类标识符
每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息,内部类也必须生成一个.class文件,以包含它们的class对象信息,这些类文件的命名有严格的规则:外围类的名字,加上$,再加上内部类的名字。例如前述例子生成的.class文件有:
Counter.class
LocalInnerClass$1.class
LocalInnerClass$1LocalCounter.class
LocalInnerClass.class
如果内部类是匿名的,编译器会简单产生一个数字作为其标识符。