一、内部类
一个类定义在另一个类中, 这就叫内部类
内部类的对象默认持有创建它的那个封装类的一个对象的句柄
1.内部类可以隐藏实施细节
如果普通的定义一个内部类,那么内部类没什么特别的地方。然而,当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,内部类就开始发挥其关键作用(从用于实现的对象生成一个接口句柄具有与上溯造型至一个基础类相同的效果)。这是由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此。所以我们可以非常方便地隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型。就象下面这样:
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel3 {
private class PContents extends 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 dest(String s) {
return new PDestination(s);
}
public Contents cont() {
return new PContents();
}
}
class Test {
public static void main(String[] args) {
Parcel3 p = new Parcel3();
Contents c = p.cont();
Destination d = p.dest("Tanzania");
// Illegal -- can't access private class: Parcel3.PContents c = p.new PContents();
}
} ///:~
现在,Contents和Destination代表可由客户程序员使用的接口(记住接口会将自己的所有成员都变成public属性)。为方便起见,它们置于单独一个文件里,但原始的Contents和Destination在它们自己的文件中是相互public的。
在Parcel3中,一些新东西已经加入:内部类PContents被设为private,所以除了Parcel3之外,其他任何东西都不能访问它。PDestination被设为protected,所以除了Parcel3,Parcel3包内的类(因为protected也为包赋予了访问权;也就是说,protected也是“友好的”),以及Parcel3的继承者之外,其他任何东西都不能访问PDestination。这意味着客户程序员对这些成员的认识与访问将会受到限制。事实上,我们甚至不能下溯造型到一个private内部类(或者一个protected内部类,除非自己本身便是一个继承者),因为我们不能访问名字,就象在classTest里看到的那样。所以,利用private内部类,类设计人员可完全禁止其他人依赖类型编码,并可将具体的实施细节完全隐藏起来。除此以外,从客户程序员的角度来看,一个接口的范围没有意义的,因为他们不能访问不属于公共接口类的任何额外方法。这样一来,Java编译器也有机会生成效率更高的代码。
2.内部类拥有封装类对象的一个引用:链接到外部类
迄今为止,我们见到的内部类好象仅仅是一种名字隐藏以及代码组织方案。尽管这些功能非常有用,但似乎并不特别引人注目。然而,我们还忽略了另一个重要的事实。创建自己的内部类时,那个类的对象同时拥有指向封装对象(这些对象封装或生成了内部类)的一个链接。所以它们能访问那个封装对象的成员——毋需取得任何资格。除此以外,内部类拥有对封装类所有元素的访问权限(注释②)。下面这个例子阐示了这个问题
package test;
interface Selector {
boolean end();
Object current();
void next();
}
public class Sequence {
private Object[] o;
private int next = 0;
public Sequence(int size) {
o = new Object[size];
}
public void add(Object x) {
if(next < o.length) {
o[next] = x;
next++;
}
}
private class SSelector implements Selector {
int i = 0;
public boolean end() {
return i == o.length;
}
public Object current() {
return o[i];
}
public void next() {
if(i < o.length) i++;
}
}
public Selector getSelector() {
return new SSelector();
}
public static void main(String[] args) {
Sequence s = new Sequence(10);
for(int i = 0; i < 10; i++)
s.add(Integer.toString(i));
Selector sl = s.getSelector();
while(!sl.end()) {
System.out.println((String)sl.current());
sl.next();
}
}
} ///:~
从上诉代码中我们可以看出,SSelector内部类可以引用外部类的成员变量Object[]o,从而实现了序列容器的迭代迭代功能。其实,翻看java源代码中的容器类,其中大部分的容器的迭代功能都是通过内部类实现iterator接口来实现的。比如ArrayList:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
/**
* An optimized version of AbstractList.ListItr
*/
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
然后通过公有方法上溯造型来得到iterator,这样得到的迭代器是符合容器特性的容器,而且还隐藏了具体的实施细节,让使用者可以忽略iterator在不同容器的实现区别而统一使用定义的接口方法。
public ListIterator<E> listIterator() {
return new ListItr(0);
}
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/
public Iterator<E> iterator() {
return new Itr();
}
我们现在知道一个内部类可以访问封装类的成员。这是如何实现的呢?内部类必须拥有对封装类的特定对象的一个引用,而封装类的作用就是创建这个内部类。随后,当我们引用封装类的一个成员时,就利用那个(隐藏)的引用来选择那个成员。幸运的是,编译器会帮助我们照管所有这些细节。但我们现在也可以理解内部类的一个对象只能与封装类的一个对象联合创建。在这个创建过程中,要求对封装类对象的句柄进行初始化。若不能访问那个句柄,编译器就会报错。进行所有这些操作的时候,大多数时候都不要求程序员的任何介入。
二、方法和作用域中的内部类
至此,我们已基本理解了内部类的典型用途。对那些涉及内部类的代码,通常表达的都是“单纯”的内部类,非常简单,且极易理解。然而,内部类的设计非常全面,不可避免地会遇到它们的其他大量用法——假若我们在一个方法甚至一个任意的作用域内创建内部类。有两方面的原因促使我们这样做:
1. 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。
2. 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。
在下面这个例子里,将修改前面的代码,以便使用:
1. 在一个方法内定义的类
2. 在方法的一个作用域内定义的类
3. 一个匿名类,用于实现一个接口
4. 一个匿名类,用于扩展拥有非默认构建器的一个类
5. 一个匿名类,用于执行字段初始化
6. 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构建器)
所有这些都在innerscopes包内发生。首先,来自前述代码的通用接口会在它们自己的文件里获得定义,使它们能在所有的例子里使用:
//: Destination.java
package c07.innerscopes;
interface Destination {
String readLabel();
} ///:~
由于我们已认为Contents可能是一个抽象类,所以可采取下面这种更自然的形式,就象一个接口那样:
//: Contents.java
package c07.innerscopes;
interface Contents {
int value();
} ///:~
尽管是含有具体实施细节的一个普通类,但Wrapping也作为它所有衍生类的一个通用“接口”使用:
//: Wrapping.java
package c07.innerscopes;
public class Wrapping {
private int i;
public Wrapping(int x) { i = x; }
public int value() { return i; }
} ///:~
在上面的代码中,我们注意到Wrapping有一个要求使用自变量的构建器,这就使情况变得更加有趣了。
第一个例子展示了如何在一个方法的作用域(而不是另一个类的作用域)中创建一个完整的类:
//: Parcel4.java
// Nesting a class within a method
package c07.innerscopes;
public class Parcel4 {
public Destination dest(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) {
Parcel4 p = new Parcel4();
Destination d = p.dest("Tanzania");
}
} ///:~
PDestination类属于dest()的一部分,而不是Parcel4的一部分(同时注意可为相同目录内每个类内部的一个内部类使用类标识符PDestination,这样做不会发生命名的冲突)。因此,PDestination不可从dest()的外部访问。请注意在返回语句中发生的上溯造型——除了指向基础类Destination的一个句柄之外,没有任何东西超出dest()的边界之外。当然,不能由于类PDestination的名字置于dest()内部,就认为在dest()返回之后PDestination不是一个有效的对象。
下面这个例子展示了如何在任意作用域内嵌套一个内部类:
//: Parcel5.java
// Nesting a class within a scope
package c07.innerscopes;
public class Parcel5 {
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) {
Parcel5 p = new Parcel5();
p.track();
}
} ///:~
TrackingSlip类嵌套于一个if语句的作用域内。这并不意味着类是有条件创建的——它会随同其他所有东西得到编译。然而,在定义它的那个作用域之外,它是不可使用的。除这些以外,它看起来和一个普通类并没有什么区别。
下面这个例子看起来有些奇怪:
//: Parcel6.java
// A method that returns an anonymous inner class
package c07.innerscopes;
public class Parcel6 {
public Contents cont() {
return new Contents() {
private int i = 11;
public int value() { return i; }
}; // Semicolon required in this case
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
Contents c = p.cont();
}
} ///:~
cont()方法同时合并了返回值的创建代码,以及用于表示那个返回值的类。除此以外,这个类是匿名的——它没有名字。而且看起来似乎更让人摸不着头脑的是,我们准备创建一个Contents对象:
return new Contents()
但在这之后,在遇到分号之前,我们又说:“等一等,让我先在一个类定义里再耍一下花招”:
return new Contents() {
private int i = 11;
public int value() { return i; }
};
这种奇怪的语法要表达的意思是:“创建从Contents衍生出来的匿名类的一个对象”。由new表达式返回的句柄会自动上溯造型成一个Contents句柄。匿名内部类的语法其实要表达的是:
class MyContents extends Contents {
private int i = 11;
public int value() { return i; }
}
return new MyContents();
在匿名内部类中,Contents是用一个默认构建器创建的。下面这段代码展示了基础类需要含有自变量的一个构建器时做的事情:
//: Parcel7.java
// An anonymous inner class that calls the
// base-class constructor
package c07.innerscopes;
public class Parcel7 {
public Wrapping wrap(int x) {
// Base constructor call:
return new Wrapping(x) {
public int value() {
return super.value() * 47;
}
}; // Semicolon required
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Wrapping w = p.wrap(10);
}
} ///:~
也就是说,我们将适当的自变量简单地传递给基础类构建器,在这儿表现为在“new Wrapping(x)”中传递x。匿名类不能拥有一个构建器,这和在调用super()时的常规做法不同。
在前述的两个例子中,分号并不标志着类主体的结束(和C++不同)。相反,它标志着用于包含匿名类的那个表达式的结束。因此,它完全等价于在其他任何地方使用分号。
若想对匿名内部类的一个对象进行某种形式的初始化,此时会出现什么情况呢?由于它是匿名的,没有名字赋给构建器,所以我们不能拥有一个构建器。然而,我们可在定义自己的字段时进行初始化:
//: Parcel8.java
// An anonymous inner class that performs
// initialization. A briefer version
// of Parcel5.java.
package c07.innerscopes;
public class Parcel8 {
// Argument must be final to use inside
// anonymous inner class:
public Destination dest(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel8 p = new Parcel8();
Destination d = p.dest("Tanzania");
}
} ///:~
若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。这正是我们将dest()的自变量设为final的原因。如果忘记这样做,就会得到一条编译期出错提示。
只要自己只是想分配一个字段,上述方法就肯定可行。但假如需要采取一些类似于构建器的行动,又应怎样操作呢?通过Java 1.1的实例初始化,我们可以有效地为一个匿名内部类创建一个构建器:
//: Parcel9.java
// Using "instance initialization" to perform
// construction on an anonymous inner class
package c07.innerscopes;
public class Parcel9 {
public Destination
dest(final String dest, final float price) {
return new Destination() {
private int cost;
// Instance initialization for each object:
{
cost = Math.round(price);
if(cost > 100)
System.out.println("Over budget!");
}
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.dest("Tanzania", 101.395F);
}
} ///:~
在实例初始化模块中,我们可看到代码不能作为类初始化模块(即if语句)的一部分执行。所以实际上,一个实例初始化模块就是一个匿名内部类的构建器。当然,它的功能是有限的;我们不能对实例初始化模块进行过载处理,所以只能拥有这些构建器的其中一个。
上诉就是匿名内部的使用规则及方法,实际上我们队匿名内部类的使用非常常见及广泛,比如我们经常使用的线程类0可以这样使用:
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.print("t1");
}
});
t1.start();
还有android中经常使用到的actionlistenner等等。
三、静态内部类
为正确理解static在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对象的句柄。然而,假如我们说一个内部类是static的,这种说法却是不成立的。static内部类意味着:
(1) 为创建一个static内部类的对象,我们不需要一个外部类对象。
(2) 不能从static内部类的一个对象中访问一个外部类对象。
但在存在一些限制:由于static成员只能位于一个类的外部级别,所以内部类不可拥有static数据或static内部类。
倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static。为了能正常工作,同时也必须将内部类设为static。如下所示:
//: Parcel10.java
// Static inner classes
package c07.parcel10;
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel10 {
private static class PContents
extends Contents {
private int i = 11;
public int value() { return i; }
}
protected static class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public static Destination dest(String s) {
return new PDestination(s);
}
public static Contents cont() {
return new PContents();
}
public static void main(String[] args) {
Contents c = cont();
Destination d = dest("Tanzania");
}
} ///:~
在main()中,我们不需要Parcel10的对象;相反,我们用常规的语法来选择一个static成员,以便调用将句柄返回Contents和Destination的方法。
通常,我们不在一个接口里设置任何代码,但static内部类可以成为接口的一部分。由于类是“静态”的,所以它不会违反接口的规则——static内部类只位于接口的命名空间内部:
//: IInterface.java
// Static inner classes inside interfaces
interface IInterface {
static class Inner {
int i, j, k;
public Inner() {}
void f() {}
}
} ///:~
四、内部类的覆盖
若创建一个内部类,然后从封装类继承,并重新定义内部类,那么会出现什么情况呢?也就是说,我们有可能覆盖一个内部类吗?这看起来似乎是一个非常有用的概念,但“覆盖”一个内部类——好象它是外部类的另一个方法——这一概念实际不能做任何事情:
//: BigEgg.java
// An inner class cannot be overriden
// like a method
class Egg {
protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}
private Yolk y;
public Egg() {
System.out.println("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg {
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}
public static void main(String[] args) {
new BigEgg();
}
} ///:~
默认构建器是由编译器自动合成的,而且会调用基础类的默认构建器。大家或许会认为由于准备创建一个BigEgg,所以会使用Yolk的“被覆盖”版本。但实际情况并非如此。输出如下:
New Egg()
Egg.Yolk()
这个例子简单地揭示出当我们从外部类继承的时候,没有任何额外的内部类继续下去。然而,仍然有可能“明确”地从内部类继承:
//: BigEgg2.java
// Proper inheritance of an inner class
class Egg2 {
protected class Yolk {
public Yolk() {
System.out.println("Egg2.Yolk()");
}
public void f() {
System.out.println("Egg2.Yolk.f()");
}
}
private Yolk y = new Yolk();
public Egg2() {
System.out.println("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() {
System.out.println("BigEgg2.Yolk()");
}
public void f() {
System.out.println("BigEgg2.Yolk.f()");
}
}
public BigEgg2() { insertYolk(new Yolk()); }
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g();
}
} ///:~
现在,BigEgg2.Yolk明确地扩展了Egg2.Yolk,而且覆盖了它的方法。方法insertYolk()允许BigEgg2将它自己的某个Yolk对象上溯造型至Egg2的y句柄。所以当g()调用y.f()的时候,就会使用f()被覆盖版本。输出结果如下:
Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()
对Egg2.Yolk()的第二个调用是BigEgg2.Yolk构建器的基础类构建器调用。调用
g()的时候,可发现使用的是f()的被覆盖版本。
五、内部类的标识符
由于每个类都会生成一个.class文件,用于容纳与如何创建这个类型的对象有关的所有信息(这种信息产生了一个名为Class对象的元类),所以大家或许会猜到内部类也必须生成相应的.class文件,用来容纳与它们的Class对象有关的信息。这些文件或类的名字遵守一种严格的形式:先是封装类的名字,再跟随一个$,再跟随内部类的名字。例如,由InheritInner.java创建的.class文件包括:
InheritInner.class
WithInner$Inner.class
WithInner.class
如果内部类是匿名的,那么编译器会简单地生成数字,把它们作为内部类标识符使用。若内部类嵌套于其他内部类中,则它们的名字简单地追加在一个$以及外部类标识符的后面。
这种生成内部名称的方法除了非常简单和直观以外,也非常“健壮”,可适应大多数场合的要求(注释③)。由于它是Java的标准命名机制,所以产生的文件会自动具备“与平台无关”的能力(注意Java编译器会根据情况改变内部类,使其在不同的平台中能正常工作)