Java语法糖(二)
语法糖之四:内部类
内部类:顾名思义,在类的内部在定义一个类。内部类仅仅是编译时的概念,编译成字节码后,内部类会生成单独的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
Java语法糖(二)的更多相关文章
- Java语法糖设计
语法糖 Java语法糖系列,所以首先讲讲什么是语法糖.语法糖是一种几乎每种语言或多或少都提供过的一些方便程序员开发代码的语法,它只是编译器实现的一些小把戏罢了,编译期间以特定的字节码或者特定的方式对这 ...
- Java语法糖(一)
概述 语法糖(Syntactic Sugar):主要作用是提高编码效率,减少编码出错的机会. 解语法糖发生在Java源码被编译成Class字节码的过程中,还原回简单的基础语法结构. 语法糖之一:泛型( ...
- Java语法糖1:可变长度参数以及foreach循环原理
语法糖 接下来几篇文章要开启一个Java语法糖系列,所以首先讲讲什么是语法糖.语法糖是一种几乎每种语言或多或少都提供过的一些方便程序员开发代码的语法,它只是编译器实现的一些小把戏罢了,编译期间以特定的 ...
- java语法糖---枚举
java语法糖---枚举 在JDK5.0中提供了大量的语法糖,例如:自动装箱拆箱.增强for循环.枚举.泛型等.所谓“语法糖”就是指提供更便利的语法供程序员使用,只是在编译器上做了手脚,却没有提供 ...
- 深入理解java虚拟机(十二) Java 语法糖背后的真相
语法糖(Syntactic Sugar),也叫糖衣语法,是英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语.指的是,在计算机语言中添加某种语法,这些语法糖虽然不会对语言 ...
- Java 语法糖详解
语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法. 这种语法对语言的功能并没有影响,但是 ...
- Java语法糖 : try-with-resources
先了解几个背景知识 什么是语法糖 语法糖是在语言中增加的某种语法,在不影响功能的情况下为程序员提供更方便的使用方式. 什么是资源 使用之后需要释放或者回收的都可以称为资源,比如JDBC的connect ...
- Java语法糖4:内部类
内部类 最后一个语法糖,讲讲内部类,内部类指的就是在一个类的内部再定义一个类. 内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,outer.java里面定义了一个内部类inner,一旦编译成功 ...
- 转:【深入Java虚拟机】之六:Java语法糖
转载请注明出处:http://blog.csdn.net/ns_code/article/details/18011009 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家P ...
随机推荐
- SQL Server数据库日志清除
第一步 将数据库转换成 simple 模式 USE master GO ALTER DATABASE 所要删除日志的数据库名 SET RECOVERY SIMPLE WITH NO_WAIT GO 第 ...
- Vue 父组件传值到子组件
vue 父组件给子组件传值中 这里的AccessList就是子组件 如果 是静态传值的话直接 msg="xxx"就好 这里动态取值的话就 :msg=xxxxx ________ ...
- 【转】【win网络编程】socket中的recv阻塞和select的用法
在编写ftp客户端程序时,在联通后使用recv函数进行接收欢迎信息时,需要申请内存进行接收数据保存,一次读取成功,但是由于一个随机的ftp服务端在说,欢迎信息的大小是不知道的,所以在尝试使用死循环,在 ...
- linux下安装redis和使用
http://www.linuxidc.com/Linux/2014-05/101979.htm
- CentOS---JDK安装与配置
1.先查看一下CentOS中存在的jdk安装包信息 # rpm -qa | grep java 查看CentOS安装的jdk版本 #java -version 2.分别执行以下命令将所有相关包都删除 ...
- 十、Shell 函数
Shell 函数 linux shell 可以用户定义函数,然后在shell脚本中可以随便调用. shell中函数的定义格式如下: [ function ] funname [()] { action ...
- Linux-WebServer安装和配置
Apache 基本操作 解释 命令 安装 yum install httpd 启动 service httpd start 停止 service httpd stop 启动完成后 查看进程是否存在:p ...
- SpringBoot日志输出至Logstash
1.springboot项目pom.xml文件下添加如下配置 2.resources目录下创建logback-spring.xml文件 <?xml version="1.0" ...
- atm-interface-shopping
from db import db_handlerfrom interface import bank def shopping_interface(name, cost, shoppingcart) ...
- Python中的tuple
tuple_lst = [ ('元祖容器可哈希',), ('元祖中的元素不可直接修改',), ('元祖可迭代',), ('查',), ('练习',), ] 元祖容器可哈希 >>>ha ...