哥启传媒在日本的网络传媒公司名称称叫什么?和日本雅虎有什么关系?

深入理解为什么Java中方法内定义的内部类可以访问方法中的局部变量 - 推酷
深入理解为什么Java中方法内定义的内部类可以访问方法中的局部变量
在我的上一篇博客&
& 中, 通过使用javap工具反编译内部类的字节码, 我们知道了为什么内部类中可以访问外部类的成员, 其实是编译器在编译内部类的class文件时,偷偷做了一些工作, 使内部类持有外部类的引用, 并且通过在构造方法上添加参数注入这个引用, 在调用构造方法时默认传入了外部类的引用。 我们之所以感到疑惑, 就是因为编译器使用的障眼法。当我们把字节码反编译出来之后, 编译器的这些小伎俩就会清清楚楚的展示在我们面前。 感兴趣的朋友可以移步到上一篇博客, 博客链接:&
在本文中, 我们要对定义在方法中的内部类进行分析。 和上一篇博客一样, 我们还是使用javap工具对内部类的字节码进行解剖。 并且和上一篇文章进行对比分析, 探究定义在外部类方法中的内部类和定义在外部类中的内部类有哪些相同之处和不同之处。 这篇博客的讲解以上一篇为基础, 对这些知识点不是很熟悉的同学, 强烈建议先读上一篇博客。 博客的链接已经在上面给出。
定义在方法中的内部类
在平时写代码的过程中, 我们经常会写类似下面的代码段:
public class Test {
public static void main(String[] args) {
final int count = 0;
new Thread(){
public void run() {
}.start();
这段代码在main方法中定义了一个匿名内部类, 并且创建了匿名内部类的一个对象, 使用这个对象调用了匿名内部类中的方法。 所有这些操作都在new Thread(){}.start() 这一句代码中完成, 这不禁让人感叹java的表达能力还是很强的。 上面的代码和以下代码等价:
public class Test {
public static void main(String[] args) {
final int count = 0;
//在方法中定义一个内部类
class MyThread extends Thread{
public void run() {
new MyThread().start();
这里我们不关心方法中匿名内部类和非匿名内部类的区别, 我们只需要知道, 这两种方式都是定义在方法中的内部类, 他们的工作原理是相同的。 在本文中主要根据非匿名内部类讲解。&
让我们仔细观察上面的代码都有哪些“奇怪”的行为:
1 在外部类的main方法中有一个局部变量count, 并且在内部类的run方法中访问了这个count变量。 也就是说, 方法中定义的内部类, 可以访问方法中的局部变量(方法的参数也是局部变量);
2 count变量使用final关键字修饰, 如果去掉final, 则编译失败。 也就是说被方法中的内部类访问的局部变量必须是final的。
由于我们经常这样做, 这样写代码, 久而久之养成了习惯, 就成了司空见惯的做法了。 但是如果要问为什么Java支持这样的做法, 恐怕很少有人能说的出来。 在下面, 我们就会分析为什么Java支持这种做法, 让我们不仅知其然, 还要知其所以然。
为什么定义在方法中的内部类可以访问方法中的局部变量?
1 当被访问的局部变量是编译时可确定的字面常量时
我们首先看这样一段代码, 本文的以下部分会以这样的代码进行讲解。&
public class Outer {
void outerMethod(){
String localVar = &abc&;
/*定义在方法中的内部类*/
class Inner{
void innerMethod(){
String a = localV
在外部类的方法outerMethod中定义了成员变量&String localVar, 并且用一个编译时字面量&abc&给他赋值。在&outerMethod方法中定义了内部类Inner, 并且在内部类的方法innerMethod中访问了localVar变量。 接下来我们就根据这个例子来讲解为什么可以这样做。
首先看编译后的文件, 和普通的内部类一样, 定义在方法中的内部类在编译之后, 也有自己独立的class文件:
和普通内部类的区别是, 普通内部类的class文件名为Outer$Inner.class 。 而定义在方法中的内部类的class文件名为Outer$&N&Inner.class 。 N代表数字, 如1, 2, 3 等 。 在外部类第一个方法中定义的内部类, 编号为1, 同理在外部类第二个方法中定义的内部类编号为2, 在外部类中第N个方法中定义的内部类编号为N 。 这些都是题外话, 主要想说的是, 方法中的内部类也有自己独立的class文件。&
我们通过javap反编译工具, 把&Outer$1Inner.class 反编译成可读的形式。 关于javap工具的使用, 请参考我的上一篇博客。 反编译的输出结果如下:
Constant pool:
#1 = Class
Outer$1Inner
Outer$1Inner
#3 = Class
java/lang/Object
java/lang/Object
#10 = Fieldref
Outer$1Inner.this$0:LO
#11 = NameAndType
#12 = Methodref
java/lang/Object.&&init&&:()V
#13 = NameAndType
&&init&&:()V
#14 = Utf8
#15 = Utf8
LineNumberTable
#16 = Utf8
LocalVariableTable
#17 = Utf8
#18 = Utf8
#19 = Utf8
innerMethod
#20 = String
#21 = Utf8
#22 = Utf8
#23 = Utf8
Ljava/lang/S
#24 = Utf8
SourceFile
#25 = Utf8
Outer.java
#26 = Utf8
EnclosingMethod
#27 = Class
#28 = Utf8
#29 = NameAndType
outerMethod:()V
#30 = Utf8
outerMethod
#31 = Utf8
InnerClasses
#32 = Utf8
final Outer this$0;
flags: ACC_FINAL, ACC_SYNTHETIC
Outer$1Inner(Outer);
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield
// Field this$0:LO
5: aload_0
6: invokespecial #12
// Method java/lang/Object.&&init&&:()V
LineNumberTable:
LocalVariableTable:
void innerMethod();
stack=1, locals=2, args_size=1
// String abc
2: astore_1
LineNumberTable:
line 10: 0
line 11: 3
LocalVariableTable:
Ljava/lang/S
innerMethod方法中一共就以下有三个指令:
& & & & &0: ldc & & & & & #20 & & & & & & & & // String abc
& & & & &2: astore_1
& & & & &3: return
Idc指令的意思是将索引指向的常量池中的项压入操作数栈。 这里的索引为20 , 引用的常量池中的项为字符串“abc” 。 这句话就揭示了内部类访问方法局部变量的原理。 让我们从常量池第20项看起。
常量池中第20项确实是字符串“abc” 。 但是这个字符串“abc”明明是定义在外部类Outer中的, 因为出现在外部类的outerMethod方法中。 为了查看这个“abc”是否在外部类中, 我们继续反编译外部类Outer.class 。 为了篇幅考虑, 在这里指给出Outer.class反编译输出的常量池的一部分。
#13 = Utf8
#14 = Utf8
outerMethod
#15 = String
#16 = Utf8
我们可以看到, “abc”这个字符串确实出现在Outer.class常量池的第15项。 这就奇怪了,
明明是定义在外部类的字面量, 为什么会出现在 内部类的常量池中呢? 其实这正是编译器在编译方法中定义的内部类时, 所做的额外工作。
下面我们将这个被内部类访问的局部变量改成整形的。 看看在字节码层面上会有什么变化。 修改后的源码如下:
public class Outer {
void outerMethod(){
int localVar = 1;
/*定义在方法中的内部类*/
class Inner{
void innerMethod(){
int a = localV
内部类反编译后的class文件如下: (由于在这里常量池不是重点, 所以省略了常量池信息)
final Outer this$0;
flags: ACC_FINAL, ACC_SYNTHETIC
Outer$1Inner(Outer);
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield
// Field this$0:LO
5: aload_0
6: invokespecial #12
// Method java/lang/Object.&&init&&:()V
LineNumberTable:
LocalVariableTable:
void innerMethod();
stack=1, locals=2, args_size=1
0: iconst_1
1: istore_1
LineNumberTable:
line 10: 0
line 11: 2
LocalVariableTable:
从上面的输出可以看到,&innerMethod方法中的第一句字节码为:&
这句字节码的意义是:将int类型的常量 1 压入操作数栈。 这就是在内部类中访问外部类方法中的局部变量int localVar = 1的原理。 由此可见,
当内部类中访问的局部变量是int型的字面量时, 编译器直接将对该变量的访问嵌入到内部类的字节码中, 也就是说, 在运行时, 方法中的内部类和外部类, 和外部类方法中的局部变量就没有任何关系了。 这也是编译器所做的额外工作。
上面两种情况有一个共同点, 那就是, 被内部类访问的外部了方法中的局部变量, 都是在编译时可以确定的字面常量。 像下面这样的形式都是编译时可确定的字面常量:
String localVar = &abc&;
int localVar = 1;
他们之所以被称为字面常量, 是因为他们被final修饰, 运行时不可改变, 当编译器在编译源文件时, 可以确定他们的值, 也可以确定他们在运行时不会被修改, 所以可以实现
类似C语言宏替换的功能
也就是说虽然在编写源代码时, 在另一个类中访问的是当前类定义的这个变量, 但是在编译成字节码时, 却把这个变量的值放入了访问这个变量的另一个类的常量池中, 或直接将这个变量的值嵌入另一个类的字节码指令中。 运行时这两个类各不相干, 各自访问各自的常量池, 各自执行各自的字节码指令。在编译方法中定义的内部类时, 编译器的行为就是这样的。&
2 当被访问的局部变量的值在编译时不可确定时
那么当方法中定义的内部类访问的局部变量不是编译时可确定的字面常量, 又会怎么样呢?想要让这个局部变量变成编译时不可确定的, 只需要将源码修改如下:
public class Outer {
void outerMethod(){
String localVar = getString();
/*定义在方法中的内部类*/
class Inner{
void innerMethod(){
String a = localV
new Inner();
String getString(){
return &aa&;
由于使用getString方法的返回值为localVar赋值, 所以在编译时期, 编译器不可确定localVar的值, 必须在运行时执行了getString方法之后才能确定它的值。 既然编译时不不可确定, 那么像上面那样的处理就行不通了。 那么在这种情况下, 内部类是通过什么机制访问方法中的局部变量的呢? 让我们继续反编译内部类的字节码:
Constant pool:
#1 = Class
Outer$1Inner
Outer$1Inner
#3 = Class
java/lang/Object
java/lang/Object
val$localVar
Ljava/lang/S
#10 = Utf8
(LOLjava/lang/S)V
#11 = Utf8
#12 = Fieldref
Outer$1Inner.this$0:LO
#13 = NameAndType
#14 = Fieldref
Outer$1Inner.val$localVar:Ljava/la
#15 = NameAndType
val$localVar:Ljava/lang/S
#16 = Methodref
java/lang/Object.&&init&&:()V
#17 = NameAndType
&&init&&:()V
#18 = Utf8
#19 = Utf8
LineNumberTable
#20 = Utf8
LocalVariableTable
#21 = Utf8
#22 = Utf8
#23 = Utf8
innerMethod
#24 = Utf8
#25 = Utf8
SourceFile
#26 = Utf8
Outer.java
#27 = Utf8
EnclosingMethod
#28 = Class
#29 = Utf8
#30 = NameAndType
outerMethod:()V
#31 = Utf8
outerMethod
#32 = Utf8
InnerClasses
#33 = Utf8
final Outer this$0;
flags: ACC_FINAL, ACC_SYNTHETIC
Outer$1Inner(Outer, java.lang.String);
stack=2, locals=3, args_size=3
0: aload_0
1: aload_1
2: putfield
// Field this$0:LO
5: aload_0
6: aload_2
7: putfield
// Field val$localVar:Ljava/lang/S
10: aload_0
11: invokespecial #16
// Method java/lang/Object.&&init&&:()V
14: return
LineNumberTable:
LocalVariableTable:
void innerMethod();
stack=1, locals=2, args_size=1
0: aload_0
1: getfield
// Field val$localVar:Ljava/lang/S
4: astore_1
LineNumberTable:
line 10: 0
line 11: 5
LocalVariableTable:
Ljava/lang/S
首先来看它的构造方法。 方法的签名为:
Outer$1Inner(Outer, java.lang.String);
我们只到, 如果不定义构造方法, 那么编译器会为这个类自动生成一个无参数的构造方法。 这个说法在这里就行不通了, 因为我们看到, 这个内部类的构造方法又两个参数。 至于第一个参数, 是指向外部类对象的引用, 在前面一篇博客中已经详细的介绍过了, 不明白的可以先看上一篇博客, 这里就不再重复叙述。
这也说明了方法中的内部类和类中定义的内部类有相同的地方, 既然他们都是内部类, 就都持有指向外部类对象的引用。
&我们来分析第二个参数, 他是String类型的, 和在内部类中访问的局部变量localVar的类型相同。 再看构造方法中编号为6和7的字节码指令:
6: aload_2
7: putfield
// Field val$localVar:Ljava/lang/S
这句话的意思是, 使用构造方法的第二个参数, 为当前这个内部类对象的成员变量赋值, 这个被赋值的成员变量的名字是&val$localVar 。 由此可见,
编译器自动为内部类增加了一个成员变量, 其实这个成员变量就是被访问的外部类方法中的局部变量。 这个局部变量在创建内部类对象时, 通过构造方法注入。 在调用构造方法时, 编译器会默认为这个参数传入外部类方法中的局部变量的值。&
再看内部类中的方法innerMethod中是如何访问这个所谓的“局部变量的”。 看
innerMethod中的前两条字节码:
0: aload_0
1: getfield
// Field val$localVar:Ljava/lang/S
这两条指令的意思是, 访问成员变量val$localVar的值。 而源代码中是访问外部类方法中局部变量的值。 所以, 在这里
将编译时对外部类方法中的局部变量的访问, 转化成运行时对当前内部类对象中成员变量的访问。&
在源代码层面上, 它的工作方式有点像这样: (注意, 下面的代码不符合Java的语法, 只是模拟编译器的行为)
public class Outer {
void outerMethod(){
String localVar = getString();
/*定义在方法中的内部类*/
class Inner{
/*下面两个成员变量都是编译器自动加上的*/
final Outer this$0; //指向外部类对象的引用
final String val$localV //被访问的外部类方法中的局部变量的值
/*构造方法, 两个参数都是编译器添加的*/
public Inner(Outer outer, String outerMethodLocal){
this.this$0 =
this.val$localVar = outerMethodL
void innerMethod(){
/*将对外部类方法中的变量的访问, 转换成对当前对象的成员变量的访问*/
//String a = localV
String a = val$localV
/*在外部类方法中创建内部类对象时, 传入相应的参数,
这两个参数分别是当前外部类的引用, 和当前方法中的局部变量*/
//new Inner();
new Inner(this, localVar);
String getString(){
return &aa&;
讲到这里, 内部类的行为就比较清晰了。 总结一下就是:
当方法中定义的内部类访问的方法局部变量的值, 不是在编译时能确定的字面常量时, 编译器会为内部类增加一个成员变量, 在运行时, 将对外部类方法中局部变量的访问。 转换成对这个内部类成员变量的方法。 这就要求内部类中的这个新增的成员变量和外部类方法中的局部变量具有相同的值。 编译器通过为内部类的构造方法增加参数, 并在调用构造器初始化内部类对象时传入这个参数, 来初始化内部类中的这个成员变量的值。 所以, 虽然在源文件中看起来是访问的外部类方法的局部变量, 其实运行时访问的是内部类对象自己的成员变量。&
为什么被方法内的内部类访问的局部变量必须是final的
上面我们讲解了, 方法中的内部类访问方法局部变量是怎么实现的。 那么为什么这个局部变量必须是final的呢? 我认为有以下两个原因:
1 当局部变量的值为编译时可确定的字面常量时( 如字符串“abc”或整数1 ), 通过final修饰, 可以实现类似C语言的编译时宏替换功能。 这样的话, 外部类和内部类各自访问自己的常量池, 各自执行各自的字节码指令, 看起来就像共同访问外部类方法中的局部变量。 这样就可以达到
语义上的一致性
。 由于存在内部类和外部类中的常量值是一样的, 并且是不可改变的,这样就可以达到
数值访问的一致性
2 当局部变量的值不是可在编译时确定的字面常量时(比如通过方法调用为它赋值), 这种情况下, 编译器给内部类增加相同类型的成员变量, 并通过构造函数将外部类方法中的局部变量的值赋给这个新增的局部变量。
&如果这个局部变量是基本数据类型时, 直接拷贝数值给内部类成员变量。代码示例和运行时内存布局是这样的:
public class Outer {
void outerMethod(){
int localVar = getInt();
/*定义在方法中的内部类*/
class Inner{
void innerMethod(){
int a = localV
new Inner();
int getInt(){ return 1; }
这样的话, 内部类和外部类各自访问自己的基本数据类型的变量,
他们的变量值一样, 并且不可修改, 这样就保证了语义上和数值访问上的一致性
如果这个局部变量是引用数据类型时, 拷贝外部类方法中的引用值给内部类对象的成员变量, 这样的话, 他们就指向了同一个对象。 代码示例和运行时的内存布局如下:
public class Outer {
void outerMethod(){
Person localVar = getPerson();
/*定义在方法中的内部类*/
class Inner{
void innerMethod(){
Person a = localV
new Inner();
Person getPerson(){ return new Person(&zhangjg&, 30); }
由于这两个引用变量指向同一个对象, 所以通过引用访问的对象的数据是一样的, 由于
他们都不能再指向其他对象(被final修饰), 所以可以保证内部类和外部类数据访问的一致性
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致局部内部类为什么只能访问final局部变量,对于成员变量却可以随便访问? -
- ITeye技术网站
博客分类:
局部内部类为什么只能访问final局部变量,对于成员变量却可以随便访问?
public class OuterClass {
private int memberField = 10;
public void outerDo(){
final int localField = fromOther();
class InnerClass{
public void innerDo(){
memberField = localF
private int fromOther() {
return 20;
局部变量和成员变量对于内部类而言,具有一定的共性,都是该内部类外面的变量。如果要求内部类只能访问final的局部变量是为了确保局部变量不被修改的话,那么内部类访问成员变量应该也有类似的限制才对
我认为是由于他们的存活范围导致了这个区别:
首先内部类的实例可以在方法结束后依然存活,局部变量在方法结束后却无法存活,所以在内部类中无法访问NON-final的局部变量;
而成员变量的存活时间是取决于外部类的实例的,内部类实例中都会引用当前外部类实例,所以他们拥有一致的生命周期,于是可以访问成员变量。
剩下的问题是,为什么可以访问final的局部变量呢?
如果将一个访问了final的局部变量的内部类进行反编译,可以发现该变量是被作为构造函数的参数传入进去的,与之一起传入的参数还有外部类实例
com.study.innerclass.OuterClass$1InnerClass(com.study.innerclass.OuterClass, int);
Stack=2, Locals=3, Args_size=3
#12; //Field this$0:Lcom/study/innerclass/OuterC
#14; //Field val$localField:I
invokespecial
#16; //Method java/lang/Object."&init&":()V
LineNumberTable:
LocalVariableTable:
Lcom/study/innerclass/OuterClass$1InnerC
既然javac是这样处理内部类的,那么这与为内部类提供一个带参数的构造函数就没什么两样了!
public class OuterClass {
private int memberField = 10;
public void outerDo(){
int localField = fromOther();
class InnerClass{
public InnerClass(int local) {
this.local =
public void innerDo(){
memberField = this.
new InnerClass(localField);
private int fromOther() {
return 20;
kingquake21
浏览: 127643 次
来自: 北京
原来“PROPAGATION_REQUIRES_NEW”的说明 ...
真的非常非常非常感谢!!!终于搞明白了!!!我也遇到了嵌套事务 ...
在spring中还可以使用MappingJacksonHttp ...
我现在也碰到这个问题 请问有什么解决方法吗Java中final关键字的使用教程-Java基础-Jsp教程-壹聚教程网Java中final关键字的使用教程
final是java中的一个关键字,可以用来修饰变量、方法和类。用关键词final修饰的域成为最终域。用关键词final修饰的变量一旦赋值,就不能改变,也称为修饰的标识为常量。
final 具有“不可改变的”的含义,可以修饰 非抽象类、非抽象成员方法和变量。&&& 用 final 修饰的类不能被继承,没有子类。&&& 用 final 修饰的方法不能被子类的方法覆盖(重写)。&&& 用 final 修饰的变量表示常量,只能被赋一次值(声明变量的时候)。注:final 不能用来修饰构造方法,因为“方法覆盖”这一概念仅适用于类的成员方法,而不适用于类的构造方法,父类的构造方法和子类的构造方法之间不存在覆盖的关系,因此用final修饰构造方法没有任何意义。父类中用 private 修饰的方法不能被子类的方法所覆盖,因此可以理解为 private 类型的方法默认是 final 类型的。&final 类把类定义为 final,使这个类不能被继承。具体使用场景如下:&&& 不是专门为继承而设计的类,类本身的方法之间有复杂的调用关系。如果随意创建这些类的子类,子类有可能会错误的修改了父类的实现细节。&&& 处于安全的原因,类的实现细节不允许在被扩展。&&& 在创建对象模型时,确信这个类不会再被扩展。例如:JDK中的java.lang.String 类被定义为 final 类型。public final class String{...}final 方法在某些情况下,出于安全原因,父类不允许子类覆盖某个方法,此时可以把这个方法声明为 final 类型。例如:JDK中的java.lang.Object 类中,getClass()方法为 final 类型,而 equals() 方法不为 final 类型。所有 Object 的子类都可以覆盖 equals() 方法,但不能覆盖 getClass() 方法。final 变量用 final 修饰的变量表示取值不会改变的常量。例如:JDK中 java.lang.Integer 类中定义了两个常量。public static final int&& MIN_VALUE = 0x;public static final int&& MAX_VALUE = 0x7final 变量具有以下特征:final修饰符可以修饰静态变量、成员变量和局部变量,分别表示静态常量、实例常量和局部常量。public&class&Demo{
&&public&static&final&int&MAX_VALUE&=&23;&&&&//静态常量
&&public&static&final&int&MIN_VALUE&=&10;&&&&//静态常量
&&private&final&Date&birthday&=&new&Date();&&//成员常量
}静态常量一般以大写字母命名,单词之间以“_”符号分开。final修饰的成员变量必须初始化。public&class&FinalTest&{
&&&&final&int&a&=&1;&&&&//成员常量&默认初始化
&&&&static&final&int&b&=&2;&&&&//静态常量&默认初始化
&&&&final&int&c;&&&&//成员常量
&&&&static&final&int&d;&&&&//静态常量
&&&&public&FinalTest(){
&&&&&&&&c&=&3;&&&&//成员常量&未默认初始化,可在构造函数中初始化
&&&&static&{
&&&&&&&&d&=&4;&&&&//静态常量&未默认初始化,可在静态代码块中初始化
}final 变量只能赋一次值,如果将引用类型的变量用 final 修饰,该变量只能始终引用一个对象,但可以改变对象的内容。public&class&FinalTest&{
&&&&String&str&=&&&;
&&&&public&void&print(){
&&&&&&&&System.out.println(str);
&&&&public&static&void&main(String[]&args){
&&&&&&&&final&FinalTest&finalTest&=&new&FinalTest();
&&&&&&&&finalTest.str&=&&xixihaha&;
&&&&&&&&finalTest.print();
}&总结:实际程序中,通过 final 修饰符来定义常量,目的为:提高程序的安全性;提高代码可维护性;提供代码可读性;浅析Java中的final关键字谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字。另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法。一.final关键字的基本用法在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。下面就从这三个方面来了解一下final关键字的基本用法。1.修饰类当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。2.修饰方法下面这段话摘自《Java编程思想》第四版第143页:“使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。注:类的private方法会隐式地被指定为final方法。3.修饰变量修饰变量是final用得最多的地方,也是本文接下来要重点阐述的内容。首先了解一下final变量的基本语法:对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。举个例子:上面的一段代码中,对变量i和obj的重新赋值都报错了。二.深入理解final关键字在了解了final关键字的基本用法之后,这一节我们来看一下final关键字容易混淆的地方。1.类的final变量和普通变量有什么区别?当用final作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了。那么final变量和普通变量到底有何区别呢?下面请看一个例子:public&class&Test&{
&&&&public&static&void&main(String[]&args)&&{
&&&&&&&&String&a&=&&hello2&;&
&&&&&&&&final&String&b&=&&hello&;
&&&&&&&&String&d&=&&hello&;
&&&&&&&&String&c&=&b&+&2;&
&&&&&&&&String&e&=&d&+&2;
&&&&&&&&System.out.println((a&==&c));
&&&&&&&&System.out.println((a&==&e));
}  truefalse大家可以先想一下这道题的输出结果。为什么第一个比较结果为true,而第二个比较结果为fasle。这里面就是final变量和普通变量的区别了,当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。这种和中的宏替换有点像。因此在上面的一段代码中,由于变量b被final修饰,因此会被当做编译器常量,所以在使用到b的地方会直接将变量b 替换为它的& 值。而对于变量d的访问却需要在运行时通过链接来进行。想必其中的区别大家应该明白了,不过要注意,只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化,比如下面的这段代码就不会进行优化:public&class&Test&{
&&&&public&static&void&main(String[]&args)&&{
&&&&&&&&String&a&=&&hello2&;&
&&&&&&&&final&String&b&=&getHello();
&&&&&&&&String&c&=&b&+&2;&
&&&&&&&&System.out.println((a&==&c));
&&&&public&static&String&getHello()&{
&&&&&&&&return&&hello&;
}这段代码的输出结果为false。2.被final修饰的引用变量指向的对象内容可变吗?在上面提到被final修饰的引用变量一旦初始化赋值之后就不能再指向其他的对象,那么该引用变量指向的对象的内容可变吗?看下面这个例子:public&class&Test&{
&&&&public&static&void&main(String[]&args)&&{
&&&&&&&&final&MyClass&myClass&=&new&MyClass();
&&&&&&&&System.out.println(++myClass.i);
class&MyClass&{
&&&&public&int&i&=&0;
}这段代码可以顺利编译通过并且有输出结果,输出结果为1。这说明引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。3.final和static很多时候会容易把static和final关键字混淆,static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变。看下面这个例子:public&class&Test&{
&&&&public&static&void&main(String[]&args)&&{
&&&&&&&&MyClass&myClass1&=&new&MyClass();
&&&&&&&&MyClass&myClass2&=&new&MyClass();
&&&&&&&&System.out.println(myClass1.i);
&&&&&&&&System.out.println(myClass1.j);
&&&&&&&&System.out.println(myClass2.i);
&&&&&&&&System.out.println(myClass2.j);
class&MyClass&{
&&&&public&final&double&i&=&Math.random();
&&&&public&static&double&j&=&Math.random();
}运行这段代码就会发现,每次打印的两个j值都是一样的,而i的值却是不同的。从这里就可以知道final和static变量的区别了。4.匿名内部类中使用的外部局部变量为什么只能是final变量?这个问题请参见《Java内部类详解》中的解释,在此处不再赘述。5.关于final参数的问题关于网上流传的”当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法外的变量“这句话,我个人理解这样说是不恰当的。因为无论参数是基本数据类型的变量还是引用类型的变量,使用final声明都不会达到上面所说的效果。看这个例子就清楚了:上面这段代码好像让人觉得用final修饰之后,就不能在方法中更改变量i的值了。殊不知,方法changeValue和main方法中的变量i根本就不是一个变量,因为java参数传递采用的是值传递,对于基本类型的变量,相当于直接将变量进行了拷贝。所以即使没有final修饰的情况下,在方法内部改变了变量i的值也不会影响方法外的i。再看下面这段代码:public&class&Test&{
&&&&public&static&void&main(String[]&args)&&{
&&&&&&&&MyClass&myClass&=&new&MyClass();
&&&&&&&&StringBuffer&buffer&=&new&StringBuffer(&hello&);
&&&&&&&&myClass.changeValue(buffer);
&&&&&&&&System.out.println(buffer.toString());
class&MyClass&{
&&&&void&changeValue(final&StringBuffer&buffer)&{
&&&&&&&&buffer.append(&world&);
}运行这段代码就会发现输出结果为 helloworld。很显然,用final进行修饰并没有阻止在changeValue中改变buffer指向的对象的内容。有人说假如把final去掉了,万一在changeValue中让buffer指向了其他对象怎么办。有这种想法的朋友可以自己动手写代码试一下这样的结果是什么,如果把final去掉了,然后在changeValue中让buffer指向了其他对象,也不会影响到main方法中的buffer,原因在于java采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响。所以关于网上流传的final参数的说法,我个人不是很赞同。
上一页: &&&&&下一页:相关内容

我要回帖

更多关于 91四哥浙大传媒 的文章

 

随机推荐