同学们,这一次讲座,我们讲一下Inner Class内部类。
我们平时写的程序是由一个个类构成的,这些类之间是相互独立的关系。我们说过,这种思路源自对现实世界的模拟,拉近了“问题空间”和“解决空间”。因此简化了系统的设计。
而Inner class 内部类是指一个类是另一个类的内部成员,定义在某个类的内部的,对外可能可见也可能不可见。
基本形式还是蛮简单的,我们看一个例子:
public class OuterClass {
private String outerName;
public void display(){
System.out.println("OuterClass display...");
System.out.println(outerName);
}
public class InnerClass{
private String innerName;
InnerClass(){
innerName = "inner class";
}
public void display(){
System.out.println("InnerClass display...");
System.out.println(innerName);
}
}
}
如上面的代码所示,这样在OuterClass里面就定义了一个InnerClass类。使用的时候可以这么用:
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.display();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
innerClass.display();
}
从代码可以看出,内部类跟一个普通属性一样使用,只要在外部类创建好的情况下,就可以去使用,可以明显看出这种被包含的关系。
而且,外部的程序能这么用的一个前提是内部类声明为public,这样才会为外部程序所见,这个跟普通属性也是一样的。当然,这个public不是约束包含的那个外部类的,无论把内部类声明为public还是private,对包含它的外部类都是可见的。
外部类和内部类的这种包含关系,决定了互相可以调用。代码如下(OuterClass.java):
public class OuterClass {
private String outerName;
public OuterClass(){
outerName="OurClass Default Name";
}
public void display(){
System.out.println("OuterClass display...");
System.out.println(outerName);
}
public void displayInner(){
InnerClass innerClass=new InnerClass();
innerClass.display();
}
private class InnerClass{
private String innerName;
InnerClass(){
outerName="outer class new name";
innerName = "inner class default name";
}
public void displayOuter(){
System.out.println(outerName);
}
public void display(){
System.out.println("InnerClass display...");
System.out.println(innerName);
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.display();
outerClass.displayInner();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
innerClass.display();
innerClass.displayOuter();
}
}
我们在OuterClass里面用下面两行创建了内部类并调用方法:
InnerClass innerClass=new InnerClass();
innerClass.display();
而在内部类里面,我们直接使用了外部内的属性。这一段演示了外部类内部类的互操作。编译器会做一个处理,对上面的内部类方法:
public void displayOuter(){
System.out.println(outerName);
}
编译之后的代码会变成:
OuterClass.this.outerName。
好奇的人一般会去bin目录下看一眼编译之后的结果,生成了两个class文件,OuterClass.class和OuterClass
除了上述的基本型内部类,内部类还可以定义成静态的,或者在一个方法体内进行定义,如:
void method1() {
class InnerClass {
public void print() {
System.out.println("method inner class.");
}
}
InnerClass inner = new InnerClass();
inner.print();
}
这个简单,这里不再完整举例。下面举一个静态内部类的例子,代码如下(Employee.java):
public class Employee{
public String empName;
public Company company;
public Employee(String empName){
this.empName = empName;
}
public static class Company{
public String compName;
public String compRegion;
public Company(String compName,String compRegion){
this.compName = compName;
this.compRegion = compRegion;
}
}
public static void main(String[] args) {
Company company = new Employee.Company("Sun/Oracle", "China");
Employee alice = new Employee("Alice");
Employee bob = new Employee("Bob");
alice.company = company;
bob.company = company;
}
}
上面的Employee里面包含一个静态内部类Company,这样跟一个普通类类似了,外部程序可以直接创建这个内部类。而且多个外部类employee可以共享同一个内部类company。要注意的是,这个时候,外部类可以访问内部类,而内部类访问不了外部类。你们可以自己动手试一下,在Employee中增加一个test(),然后试着在Company中调用,会有编译错误:
Cannot make a static reference to the non-static method test() from the type Employee。
有了内部类的这些基础知识,下面我们要讨论进阶一点的内容了。
有一种很有用的场合时匿名内部类。它看起来就是一个常规的内部类,但是不显式起名,因此叫匿名类。这个场景,定义和实例化是写在一起同时的。当我们需要实现一个接口或者抽象类的时候,我们经常这么用。
我们看一个监听器响应事件触发的例子。代码如下:
先定义一个Listener接口:
public interface ClickListener {
void onClick();
}
再定义一个Button类,里面包含一个Listener匿名内部类,代码如下(Button.java):
public class Button {
public void click(){
new ClickListener(){
public void onClick(){
System.out.println("click ...");
}
}.onClick();
}
public static void main(String[] args) {
Button button=new Button();
button.click();
}
}
仔细看看上面的代码,按照常规,我们应该在click()方法中定义这个内部类,然后new一个实例,再调用方法。而使用匿名内部类,我们用一句话一气呵成:
new ClickListener(){
public void onClick(){
System.out.println("click ...");
}
}.onClick();
之所以这么用,是因为其实我们只是想实现onClick()方法,至于这个类叫什么名字,我们并不关心,所以就匿名了。这种需求在些事件响应程序式会经常用到。这个匿名可以看成一种简写,对编译器来讲,他还是规规矩矩地生成了一个内部类的class文件ButtonInnerClass.class。为什么需要内部类?用两个外部类不是一样的吗?从程序功能上来说,确实是一样的。我个人理解的是,采用内部类技术,隐藏细节和内部结构,封装性更好,让程序结构更加合理优雅。在现实世界里,一个事物内部都由很多部件组成,每个部件也还可能包含子部件,这些子部件不需要暴露出来。内部类的思想就是借鉴了这个现实世界,概念的同一性让它很好理解。这种思想更贴近现实世界,“问题空间”与“解决空间”更接近了。
除了上述的基本型内部类,内部类还可以定义成静态的,或者在一个方法体内进行定义,如:¨G6G这个简单,这里不再完整举例。下面举一个静态内部类的例子,代码如下(Employee.java):¨G7G上面的Employee里面包含一个静态内部类Company,这样跟一个普通类类似了,外部程序可以直接创建这个内部类。而且多个外部类employee可以共享同一个内部类company。要注意的是,这个时候,外部类可以访问内部类,而内部类访问不了外部类。你们可以自己动手试一下,在Employee中增加一个test(),然后试着在Company中调用,会有编译错误:¨G8G有了内部类的这些基础知识,下面我们要讨论进阶一点的内容了。有一种很有用的场合时匿名内部类。它看起来就是一个常规的内部类,但是不显式起名,因此叫匿名类。这个场景,定义和实例化是写在一起同时的。当我们需要实现一个接口或者抽象类的时候,我们经常这么用。
我们看一个监听器响应事件触发的例子。代码如下:先定义一个Listener接口:¨G9G再定义一个Button类,里面包含一个Listener匿名内部类,代码如下(Button.java):¨G10G仔细看看上面的代码,按照常规,我们应该在click()方法中定义这个内部类,然后new一个实例,再调用方法。而使用匿名内部类,我们用一句话一气呵成:¨G11G之所以这么用,是因为其实我们只是想实现onClick()方法,至于这个类叫什么名字,我们并不关心,所以就匿名了。这种需求在些事件响应程序式会经常用到。这个匿名可以看成一种简写,对编译器来讲,他还是规规矩矩地生成了一个内部类的class文件Button1.class。
我们要的是这个方法,而接口里面也只有着一个方法,这种场景叫函数式接口,Java8之后可以用函数式编程进一步简化上面的代码:
public void click(){
ClickListener listener = ()->{System.out.println("click ...");};
listener.onClick();
}
函数式接口与匿名类是不同的实现方式。编译器并不会生成一个内部类class文件。
把匿名类当成方法的参数是常见的使用方式。
我们写一个程序,一个事件处理器响应事件的发生,响应完之后发一个消息通知。
先定义一个发消息的接口:
public interface IMessenger {
void sendMessage(String string);
}
再定义事件处理器,代码如下(Handler.java):
public class Handler {
public void handleEvent(IMessenger messenger) {
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
messenger.sendMessage("SUCCESS.");
}
}).start();
}
}
这个处理器的主体方法是handleEvent(IMessenger),异步处理事件花费两秒钟,之后发消息。
下面我们使用这个事件处理器,代码如下(HandlerTest.java):
public class HandlerTest {
public static void main(String[] args) {
Handler handler = new Handler();
handler.handleEvent(new IMessenger() {
public void sendMessage(String msg) {
System.out.println("event handled. " + msg);
}
});
System.out.println("------event handling test--------");
}
}
注意上面代码的主体部分:
handler.handleEvent(new IMessenger() {
public void sendMessage(String msg) {
System.out.println("event handled. " + msg);
}
});
这里就是生成了一个实现IMessenger接口的匿名类,然后传给handleEvent()方法作为参数使用。自然,编译器会自动生成一个匿名类class文件。
再一次提及函数式编程,因为这个IMessenger是一个只有唯一一个方法的接口,所以此处我们可以再次用函数式编程简化代码:
public static void main(String[] args) {
Handler handler = new Handler();
handler.handleEvent((msg) ->{System.out.println("event handled. " + msg);});
System.out.println("------event handling test--------");
}
匿名内部类来自外部闭包环境的*变量必须是final的。这一点让人费解,很多人在编译器提示错误的时候就自动修改一下,并不深究。这个与Java对Closure闭包的实现有关,我们来初步探究一下。这是比进阶更加高级的课题了。
闭包是包含*变量的函数,这些变量是在定义函数的环境中定义的,而函数本身也当成一个参数进行传递和返回,返回后,这个*变量还同函数一同存在。在JavaScript和别的语言中流行。
大家也一直希望Java实现闭包。Java是通过内部类实现的,但是实现的不完整。
先看现象。我们在上面的HandlerTest程序的主体部分增加一个*变量,代码如下:
int i = 0;
test.handleEvent(new IMessenger() {
public void sendMessage(String msg) {
System.out.println("event handled. " + msg);
int j=0;
j = i+j;
}
});
在JDK7之前,会有编译错误:Local variable i defined in an enclosing scope must be final。这个错误出现在j=i+j;这一行。必须让外面的这个*变量i定义成final。(JDK8之后不会出错。但是实际上还是有这个限制,你试着把上面的代码修改一下,给i进行一个赋值就会看出来了。)
把i定义成final之后,意味着我们在匿名类中不能给*变量i赋值了。
这儿在类里面定义了内部类,而内部类又引用了外部的*变量,这就构成了典型的闭包。Java并没有完全实现闭包,在生成匿名类的时候,它把外部*变量i的value传入,实际上是一个拷贝,因此内部其实不能修改外部的*变量了。Java团队此处又偷了一个懒,干脆就规定外部的这个*变量为final。
我们可以反编译这个类,看看编译器处理之后的代码,为:
public void sendMessage(String msg) {
System.out.println("event handled. " + msg);
int j = 0;
j = this.val$i + j;
}
注意编译器把j=i+j;变成了j = this.val$i + j;,只是一个外部i的拷贝。
对于普通的内部类,为什么又不用呢?因为普通的内部类有构造函数,在构造函数过程中把外部类传进来。但是匿名内部类是没有构造函数的。
Java这样处理,引起了多年的争论。我个人还是比较赞同Java团队的做法的,这样虽然限制很多,但是代码安全,并且预留了以后完全支持闭包的可能性。
以后我会有专题讲解闭包,这次讲解只是就着匿名内部类带一下。你们看到了,Inner Classs起步于一个简单的模型,基本内容很简单,但是一步步深入,就会牵扯出高级的话题。这就有点像推倒多米诺骨牌,第一下只是很小的一个动作,后面却又大动作等着。很多技术都是这样的,越研讨越深,不断发现新挑战,这也是一种乐趣。
正如我们探索未知的山川,层峦叠嶂,接应不暇,这山望见那山高,前路永远有精彩的风景。也正如我们进入桃园洞口,刚开头武陵人只是缘溪捕鱼,到了半道,忽逢桃花林,复前行,林尽水源却得一山,从口入,行数十步,豁然开朗,得此桃源仙境。
学问上这种探索的乐趣,王国维先生曾言:众里寻他千百度,慕然回首,那人却在,灯火阑珊处。
咦!微斯人,吾谁与归