Java语言十五讲(第七讲 InnerClass)

 

同学们,这一次讲座,我们讲一下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起步于一个简单的模型,基本内容很简单,但是一步步深入,就会牵扯出高级的话题。这就有点像推倒多米诺骨牌,第一下只是很小的一个动作,后面却又大动作等着。很多技术都是这样的,越研讨越深,不断发现新挑战,这也是一种乐趣。
正如我们探索未知的山川,层峦叠嶂,接应不暇,这山望见那山高,前路永远有精彩的风景。也正如我们进入桃园洞口,刚开头武陵人只是缘溪捕鱼,到了半道,忽逢桃花林,复前行,林尽水源却得一山,从口入,行数十步,豁然开朗,得此桃源仙境。
学问上这种探索的乐趣,王国维先生曾言:众里寻他千百度,慕然回首,那人却在,灯火阑珊处。
咦!微斯人,吾谁与归

 

 

上一篇:Java类访问权限


下一篇:用Jquery去写树结构