将一个类的定义嵌套在另一个类内部就是内部类(内部接口也可以),另一个类就是外部类。形式上很简单,关键问题是内部类有什么用,它能解决什么别人解决不了的问题?
在Java编程思想第四版中文版中有如下说法“内部类对我们非常有用,因为利用它可以对那些逻辑上相互联系的类进行分组,并可控制一个类在另一个类里的可见性”。
1、利用内部类实现分组
先看“因为利用它可以对那些逻辑上相联系的类(或者接口)进行分组这句话”。假如我们想定义一个“Tree”类,它要实现“Root”接口与“Leaf”接口。因为"Root"与"Leaf"在逻辑上是相关的,它们都是树的组成部分或者是其它植物的组成部分,因此最好把它们定义在一个文件中,也就是组织在一起,创建一个Interface.java文件,内容如下:
示例一
public interface Interface {
public interface Root {
public void show();
}
public interface Leaf {
public void show();
}
}
注意接口Root与Leaf都是public的,如果不使用内部类或者内部接口,就需要把这两个接口分别定义在各自独立的文件中,显然没有上例中使用的方式好。如果想实现以上接口的话,代码如下:
示例二
public class Tree implements Interface.Root, Interface.Leaf{
@Override
public void show() {
System.out.println("Implement who?Root or Leaf?");
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Interface.Root r = new Tree();
r.show();
Interface.Leaf l = new Tree();
l.show();
Tree tree = new Tree();
tree.show();
}
}
上例中与不使用内部接口的区别只有一个地方,就是在涉及到Root、Leaf时要在前边加上外部接口的限定符,如Interface.Root与Interface.Leaf。
2、控制内部类的可见性
上述代码中同时暴露了接口的一个问题,就是接口内方法名冲突的问题。show()这个方法同时出现在interface.Root与interface.Leaf中,在Tree中,show()方法同时实现了来自两个不同接口的show()方法,这显然是不能接受的,Root的show()方法与Leaf的show()方法在实现上不可能相同。这个问题本质上是因为继承中的“多重接口”机制引起的,因此,要解决这个问题就不能使用接口的多重实现这种机制。解决的基本思路是定义两个新类,分别实现Root与Leaf,这种方法与内部类没有关系,示例代码如下:
示例三
class MyRoot implements Interface.Root {
@Override
public void show() {
System.out.println("Implement Root");
}
}
class MyLeaf implements Interface.Leaf {
@Override
public void show() {
System.out.println("Implement Leaf");
}
}
public class Tree2 {
private MyRoot r = new MyRoot();
private MyLeaf l = new MyLeaf();
void show(String type) {
if ("root".equals(type)) {
r.show();
} else {
l.show();
}
}
public static void main(String[] args) {
Tree2 t2 = new Tree2();
t2.show("tree");
t2.show("leaf");
// 错误
// Interface.Root r = new Tree2();
}
}
分别定义了MyRoot与MyLeaf实现两个不同接口中的相同方法show。然后定义Tree2将MyRoot与MyLeaf组合进去。Tree2中的show()方法,注意它是独立的方法,与Root中与Leaf中的show()没有关系。它只是根据参数分别调用Root与Leaf中的不同show()。本例中的Tree2确实实现了两个不同版本的show(),但是与示例二的Tree不同,Tree2不能向上转型了。当然可以添加两个成员函数,让它模拟向上转型,示例代码如下:
示例四
public class Tree2 {
private MyRoot r = new MyRoot();
private MyLeaf l = new MyLeaf();
void show(String type) {
if ("root".equals(type)) {
r.show();
} else {
l.show();
}
}
Interface.Root getRoot () {
return r;
}
Interface.Leaf getLeaf () {
return l;
}
public static void main(String[] args) {
Tree2 t2 = new Tree2();
t2.show("tree");
t2.show("leaf");
Interface.Root r = t2.getRoot();
r.show();
}
}
注意Interface.Root r = t2.getRoot();这句话,它是模拟向上转型,不是真正的向上转型。以上的代码只是铺垫,与内部类没有关系,接下来就有关系了。
我们定义Tree2的目的是向外暴露一棵树的实现,而在上例中MyRoot与MyLeaf也同时暴露出去了,这不符合我们的意图,只所以分别定义MyRoot与MyLeaf是为了解决接口中方法名冲突的问题,这是我们的实现细节,需要对外隐藏。我们知道Java中普通的类只有public访问控制与默认访问控制,但内部类可以有private访问控制。因此把MyRoot与MyLeaf定义在Tree2内部,并把它设置成private型,这样就实现了隐藏,使用我们代码的人看不到MyRoot与MyLeaf了,示例如下:
示例五
public class Tree2 {
private class MyRoot implements Interface.Root {
@Override
public void show() {
System.out.println("[" + NAME + "]" + "Implement Root");
}
}
private class MyLeaf implements Interface.Leaf {
@Override
public void show() {
System.out.println("[" + NAME + "]" + "Implement Leaf");
}
}
private final String NAME="Tree2";
private MyRoot r = new MyRoot();
private MyLeaf l = new MyLeaf();
void show(String type) {
if ("root".equals(type)) {
r.show();
} else {
l.show();
}
}
Interface.Root getRoot () {
return r;
}
Interface.Leaf getLeaf () {
return l;
}
public static void main(String[] args) {
Tree2 t2 = new Tree2();
t2.show("tree");
t2.show("leaf");
Interface.Root r = t2.getRoot();
r.show();
}
可以看到,MyRoot与MyLeaf成为Tree2的私有内部类,使用Tree2的人看不到MyRoot、MyLeaf,甚至不知道它们的存在,这也就是通过内部类控制MyRoot与MyLeaf的可见性,从而实现了隐藏实现细节的目的。
另一个非常重要的问题就是,注意Tree2中的NAME属性,它是Tree2中的私有final成员,但是对于内部类MyRoot与MyLeaf而言却是可见的,也就是说内部类可以共享外部类的一切,这一点也很重要。
3、通过方法内部类、语句块内部类、匿名内部类实现更深层次的隐藏
内部类不仅可以嵌套在类内部,还可以嵌套在方法内部、语句块内部,还可以匿名。示例代码如下:
示例六
public class Tree2 {
private final String NAME = "Tree2";
private Interface.Root r;
private Interface.Leaf l;
private void createRoot(int len) {
class MyLeaf implements Interface.Root {
@Override
public void show() {
System.out.println("[" + NAME + "]" + "[" + len + "]" + "Implement Leaf");
}
}
r = new MyLeaf();
}
private void createLeaf(String color) {
class MyRoot implements Interface.Root {
@Override
public void show() {
System.out.println("[" + NAME + "]" + "[" + color + "]" + "Implement Root");
}
}
r = new MyRoot();
}
Tree2 (int len, String color) {
createRoot(len);
createLeaf(color);
}
void show(String type) {
if ("root".equals(type)) {
r.show();
} else {
l.show();
}
}
Interface.Root getRoot () {
return r;
}
Interface.Leaf getLeaf () {
return l;
}
public static void main(String[] args) {
Tree2 t2 = new Tree2(10, "blue");
t2.show("tree");
t2.show("leaf");
Interface.Root r = t2.getRoot();
r.show();
}
}
将MyRoot与MyLeaf分别定义在私有方法createRoot与createLeaf中,现在MyRoot与MyLeaf不再是类级别的内部类了,变成方法级别的内部类了。它们分别只在createRoot中createLeaf中可见,对于类中的其它方法它们都不可见,隐藏的更深。另外代码看起来结构更合理,因为相关的部分都被定义在了一起。
利用匿名内部类更进一步隐藏:
示例七
public class Tree2 {
private final String NAME = "Tree2";
private Interface.Root r;
private Interface.Leaf l;
Tree2(int len, String color) {
r = new Interface.Root() {
@Override
public void show() {
System.out.println("[" + NAME + "]" + "[" + len + "]" + "Implement Leaf");
}
};
l = new Interface.Leaf() {
@Override
public void show() {
System.out.println("[" + NAME + "]" + "[" + color + "]" + "Implement Root");
}
};
}
void show(String type) {
if ("root".equals(type)) {
r.show();
} else {
l.show();
}
}
Interface.Root getRoot() {
return r;
}
Interface.Leaf getLeaf() {
return l;
}
public static void main(String[] args) {
Tree2 t2 = new Tree2(10, "blue");
t2.show("tree");
t2.show("leaf");
Interface.Root r = t2.getRoot();
r.show();
}
}
createRoot与createLeaf方法都省了,直接在构建函数中使用内部匿名类,无需指定名称。
注意一个问题,匿名类没有名字,那么自然它就不可能有构建器。我们知道,构建器的一个重要作用是接收参数并用不同的逻辑初始化实例。上边的例子中,我们给Tree2类的构造方法传递了参数,而在内部匿名类中可以直接使用这个参数,这样,内部匿名类也就不需要通过构造器传递参数了。如果实在需要用一段逻辑初始化内部类,那么也可以用构造函数的替换方法initializer,就是把构造逻辑用{}扩起来就可以了,因此内部匿名类没有构造函数完全没有问题。
4、总结
总结一下内部类的典型用法:
- 将逻辑上相关的类与接口组织在一起而不是各个定义单独的文件,如示例一。
- 更细粒度的控制类的可见性,如示例五。
从内部类的典型用法来看,内部类并非一定要用。它的一个作用是组织代码,使代码看起来更紧凑,组织的更合理。这个的话没有内部类也可以,只不过是代码的组织看起来不那么好而已。
关于细粒度控制类的可见性,没有这个也可以。我们可以把类定义成默认的访问控制,这样包以外的用户,也就是外部的用户就看不见这个类。而对于包内的开发人员,一般都是一个团队的人,自然知道怎么用,当然也就不用刻意隐藏来防止误用了。
只有在解决不同接口的方法名冲突时,内部类是必需的。实际上内部类的真正威力不是在这里,以上的代码里你可能忽略了一个事实,内部类可以访问外部类的一切,包括外部类私有的成员与方法,这个特性才是它的真正威力所在,我打算在下一篇文章中介绍这个特性。