语法糖之四:内部类
内部类:顾名思义,在类的内部在定义一个类。内部类仅仅是编译时的概念,编译成字节码后,内部类会生成单独的Class文件。
四种:成员内部类、局部内部类、匿名内部类、静态内部类。
1、成员内部类(member inner class)
常见用法:1、List、Set集合中的迭代器类;
栗子:
public class OuterClass { private String str; public OuterClass(String str) { this.str = str; } private void print() { System.out.println("OuterClass print : " + str); } class InnerClass { //成员内部类不能含有static变量和方法 //成员内部类依附于外部类,只有先创建外部类才能创建内部类 private String strInner; public void printInner() { //内部类可以访问外部类的属性,即使是私有变量, //如果内部类的成员变量与外部类的成员变量重名,内部类的str会覆盖外部类的str System.out.println("InnerClass print : " + str); //内部类可以访问外部类的方法,即使是私有方法 //如果内部类的方法与外部类的方法重名,内部类的print会覆盖外部类的print,会出现无限递归的情况. print(); } } //获取内部类 public InnerClass getInnerClass() { return new InnerClass(); } public static void main(String[] args) { OuterClass outerClass = new OuterClass("hello world"); OuterClass.InnerClass innerClass = outerClass.getInnerClass(); //外部类访问内部类的成员变量和方法,需要通过内部类实例实现. innerClass.printInner(); } }
编译后生成OuterClass.class和OuterClass$InnerClass.class两个Class文件。
对进行反编译得到:
class OuterClass$InnerClass { // Field descriptor #6 Ljava/lang/String; private java.lang.String strInner; // Field descriptor #8 LOuterClass; final synthetic OuterClass this$0; //指向外部类对象的指针,编译器会默认为成员内部类创建指向外部类的引用 // Method descriptor #10 (LOuterClass;)V // Stack: 2, Locals: 2 //OuterClass$InnerClass构造器,虽然Java源码中内部类的构造器是一个无参构造器,但编译器会默认添加一个指向外部类引用的参数,赋值给this$0变量. //这就是为什么前文提到的只有先创建外部类实例才能创建成员内部类实例、成员内部类可以随意访问外部类的成员变量和方法。 OuterClass$InnerClass(OuterClass arg0); 0 aload_0 [this] 1 aload_1 [arg0] 2 putfield OuterClass$InnerClass.this$0 : OuterClass [12] 5 aload_0 [this] 6 invokespecial java.lang.Object() [14] 9 return Line numbers: [pc: 0, line: 15] Local variable table: [pc: 0, pc: 10] local: this index: 0 type: OuterClass.InnerClass // Method descriptor #16 ()V // Stack: 4, Locals: 1 public void printInner(); 0 getstatic java.lang.System.out : java.io.PrintStream [22] 3 new java.lang.StringBuilder [28] 6 dup 7 ldc <String "InnerClass print : "> [30] 9 invokespecial java.lang.StringBuilder(java.lang.String) [32] 12 aload_0 [this] 13 getfield OuterClass$InnerClass.this$0 : OuterClass [12] 16 invokestatic OuterClass.access$0(OuterClass) : java.lang.String [35] 19 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [41] 22 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [45] 25 invokevirtual java.io.PrintStream.println(java.lang.String) : void [49] 28 aload_0 [this] 29 getfield OuterClass$InnerClass.this$0 : OuterClass [12] 32 invokestatic OuterClass.access$1(OuterClass) : void [54] 35 return Line numbers: [pc: 0, line: 23] [pc: 28, line: 26] [pc: 35, line: 27] Local variable table: [pc: 0, pc: 36] local: this index: 0 type: OuterClass.InnerClass Inner classes: [inner class info: #1 OuterClass$InnerClass, outer class info: #36 OuterClass inner name: #60 InnerClass, accessflags: 0 default] }
由于成员内部类实例含有对外部类实例的引用,所以即便在外部类实例没有被引用但成员内部类实例有人引用的情况下,外部类实例不会被回收。
2、局部内部类(local inner Class)
局部内部类是嵌套在方法或作用域中的内部类。与成员内部类不同的是,当且仅当局部内部类出现在非静态的环境(如非静态方法)中时,才会拥有对外部类实例的引用。当出现在静态环境中,内部类实例没有对外部类实例的引用,也不拥有外围类任何静态成员。
栗子:
public class OuterClass { //局部内部类要访问外部的变量或对象必须用final修饰,非静态方法中局部内部类Inner实例含有对外围类OuterClass实例的引用 public void print(final String paramStr) { final String str = "world"; //局部内部类没有访问修饰符 class InnerClass { public void printInner() { System.out.println("InnerClass print paramStr : " + paramStr); System.out.println("InnerClass print str : " + str); } } InnerClass innerClass = new InnerClass(); innerClass.printInner(); } public static void main(String[] args) { OuterClass outerClass = new OuterClass(); outerClass.print("hello"); } }
编译后产生的Class文件为OuterClass.class和OuterClass$1InnerClass.class,局部内部类的Class文件名比成员内部类的Class文件名多了数字1。
3、匿名内部类(Anonymous Inner Class)
顾名思义,匿名类就是没有名字的类,它是一种特殊的局部内部类,匿名内部类没有构造方法,在使用的同时被声明和实例化。
常见用法:1、动态的创建函数对象。例如比较器(Comparator),策略模式。
函数对象是:如果一个对象仅仅导出执行其他对象(对象被显示传递给方法)上的操作的方法,这样的实例被称为函数对象。
下面的实现Comparator接口的匿名类实例就是一个函数对象。以这种方式使用匿名类时,每次执行方法都会新建一个实例,如果被频繁的调用,效率会很低。
Arrays.sort(stringArray, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.length() - o2.length(); } });
考虑到实现Comparator接口的匿名类没有成员变量(即它是无状态的),把它作为一个单例(Singleton)是非常合适的。
class Host { //公共静态常量 public static final Comparator<String> cmp = new MyCmp(); //私有静态内部类 private static class MyCmp implements Comparator<String> { @Override public int compare(String o1, String o2) { return o1.length() - o2.length(); } } }
2、创建过程对象,例如创建Runnable,Thread或者TimerTask实例。
3、用在静态工厂方法的内部:
栗子:
public class OuterClass { //匿名内部类要访问外部的变量或对象必须用final修饰 public void startThread(final String paramStr) { final String str = "world"; //匿名内部类没有访问修饰符 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Thread started paramStr : " + paramStr); System.out.println("Thread started : " + str); } }; Thread t = new Thread(runnable); t.start(); } public static void main(String[] args) { OuterClass outerClass = new OuterClass(); outerClass.startThread("hello"); } }
编译后产生的Class文件为OuterClass.class和OuterClass$1.class,匿名内部类的Class文件名只有数字1。
前文提到,匿名内部类和局部内部类的访问外部的成员变量必须用final修饰,下面以匿名内部类为例解释一下原因:
生命周期不一致问题:paramStr和str两个变量的生命周期仅限于startThread方法内,当startThread方法执行结束后,这两个变量的生命周期就结束了,但另外一个线程中的run方法很可能还没有结束,再去访问paramStr和str变量是不可能的。
那Java怎么解决的这个问题呢?复制
反编译Class文件得到:
// Compiled from OuterClass.java (version 1.7 : 51.0, super bit) class OuterClass$1 implements java.lang.Runnable { // Field descriptor #8 LOuterClass; final synthetic OuterClass this$0; //指向外部类实例的引用 // Field descriptor #10 Ljava/lang/String; private final synthetic java.lang.String val$paramStr; //编译器默认生成val$paramStr成员变量,即paramStr参数变量的拷贝 // Method descriptor #12 (LOuterClass;Ljava/lang/String;)V // Stack: 2, Locals: 3 OuterClass$1(OuterClass arg0, java.lang.String arg1); 0 aload_0 [this] 1 aload_1 [arg0] 2 putfield OuterClass$1.this$0 : OuterClass [14] 5 aload_0 [this] 6 aload_2 [arg1] 7 putfield OuterClass$1.val$paramStr : java.lang.String [16] 10 aload_0 [this] 11 invokespecial java.lang.Object() [18] 14 return Line numbers: [pc: 0, line: 1] [pc: 10, line: 7] Local variable table: [pc: 0, pc: 15] local: this index: 0 type: new OuterClass(){} // Method descriptor #20 ()V // Stack: 4, Locals: 1 public void run(); 0 getstatic java.lang.System.out : java.io.PrintStream [26] 3 new java.lang.StringBuilder [32] 6 dup 7 ldc <String "Thread started paramStr : "> [34] 9 invokespecial java.lang.StringBuilder(java.lang.String) [36] 12 aload_0 [this] 13 getfield OuterClass$1.val$paramStr : java.lang.String [16] 16 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [39] 19 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [43] 22 invokevirtual java.io.PrintStream.println(java.lang.String) : void [47] 25 getstatic java.lang.System.out : java.io.PrintStream [26] 28 ldc <String "Thread started : world"> [52] 30 invokevirtual java.io.PrintStream.println(java.lang.String) : void [47] 33 return Line numbers: [pc: 0, line: 10] [pc: 25, line: 11] [pc: 33, line: 12] Local variable table: [pc: 0, pc: 34] local: this index: 0 type: new OuterClass(){} Inner classes: [inner class info: #1 OuterClass$1, outer class info: #0 inner name: #0, accessflags: 0 default] Enclosing Method: #57 #59 OuterClass.startThread(Ljava/lang/String;)V }
可以看到,编译器默认为匿名内部类创建了两个成员变量:this$0指向外部类的引用;val$paramStr为String变量。构造函数中用startThread方法的形参paramStr初始化val$paramStr变量,即val$paramStr是方法形参paramStr的一个拷贝。也就是说,run方法访问的是paramStr的拷贝,所以即便paramStr生命周期结束也不会影响run方法的执行,解决了生命周期不一致问题。那paramStr为什么要用final修饰呢?假如paramStr是一个非final普通变量,那就可以在内部类中修改val$paramStr变量的值,但paramStr的值不会受影响,造成数据不一致问题,所以把paramStr声明为final变量,不允许修改。
对于局部变量str,被final修饰意味着str是一个常量,在编译期间就可以确定并放入常量池,编译器默认为内部类创建一个局部变量的拷贝,通过拷贝去常量池访问就可以了,看这条语句ldc <String "Thread started : world"> [52] 表示将字符串变量压入栈顶。(对常量池的概念理解不够深入)。
总的来说,如果局部变量的值在编译期间就可以确定(str),则直接在匿名内部类(局部内部类)中创建一份拷贝;如果局部变量的值无法在编译期间确定(paramStr),则通过构造器传参的方式对拷贝进行初始化。由于被final修饰的变量不能被修改,保证拷贝和原始变量的一致,给人的感觉好像是变量的生命周期延长了,引出了Java中的闭包。
闭包是什么东东?
4、静态内部类(static inner class)
静态内部类是最简单的一种内部类,可以把静态内部类看成是普通的类,恰好被定义在另一个类内部。它不依赖于外围类实例,可以在外围类实例之外独立存在。
常见用法:作为公有的辅助类,仅当它与外围类一起使用时才有意义。
Map中Entry为私有静态内部类,Entry是外部类的一个组件。虽然每个Entry都与一个Map相关联,但entry上的方法(getValue和getKey)不需要访问Map,所以没必要使用非静态成员内部类。因为非静态成员内部类的实例都包含指向外围类实例的引用,即每个Entry都含有一个指向Map的引用,造成空间和时间的浪费。
栗子:
public class OuterClass { private String str = "hello"; private static String staticStr = "static_hello"; private static void print() { System.out.println("OuterClass print : " + staticStr); } /*静态内部类*/ static class InnerClass { public static String staticStrInner = "static_hello_Inner"; public void printInner() { //静态内部类只能访问外部类的静态成员和静态方法,不能访问外部类的非静态成员和方法。 System.out.println("InnerClass print : " + staticStr); print(); } } public static void main(String[] args) { //静态内部类实例的创建不依赖外部类 InnerClass innerClass = new InnerClass(); innerClass.printInner(); } }
为什么要使用内部类?
1、解决多继承问题:Java不支持多继承,不管外部类有没有继承类,成员内部类都可以独立的继承某个类,而成员内部类又可以访问外部类,相当于实现多继承了。
2、对于只使用一次的类,在其他地方不会使用这个类,那么声明一个外部类就没有必要了,使用局部内部类和成员内部类就可以。
3、内部类可以实现更好的封装,使类与类之间的关系更加紧密。
如何选择使用哪种内部类?
1、如果成员内部类的每个实例都需要一个指向其外围类的引用,选择非静态成员内部类,否则选择静态成员内部类。
2、假设内部类在一个方法的内部,在方法之外不需要使用,如果只需要在一个地方创建实例且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名内部类,否则选择局部内部类。
参考资料:
1、(Java语法糖4:内部类)http://www.cnblogs.com/xrq730/p/4875907.html
2、(从反编译认识内部类)http://blog.csdn.net/le_le_name/article/details/52338096
3、(为什么必须是final的呢?)http://cuipengfei.me/blog/2013/06/22/why-does-it-have-to-be-final/
4、(Java语法糖系列五:内部类和闭包)http://www.jianshu.com/p/f55b11a4cec2
5、http://www.cnblogs.com/chenssy/p/3388487.html(java提高篇(八)----详解内部类)
6、( java提高篇(九)-----详解匿名内部类)http://blog.csdn.net/chenssy/article/details/13170015