对象与内存控制
要点:
实例变量属于java对象
类变量属于类本身
实例变量的初始化细节
类变量的初始化细节
子类构造器调用父类构造器
避免在构造器钟访问子类的实例变量
避免在构造器钟调用被子类重写的方法
java继承对成员变量和方法的区别
父、子实例的实例变量的内存分配机制
父、子类的类变量的内存分配
final修饰符的作用
系统对哪些final变量执行“宏替换”
final方法注意点
使用final修饰被匿名、局部内部类访问的局部变量
java的内存管理分为两个方面:
内存分配和内存回收。
这里的内存分配特指创建java对象时JVM为该对象在堆内存中所分配的内存空间。
内存回收指的是当java对象失去引用,变成垃圾时,JVM的垃圾回收机制自动清理该对象,并回收该对象所占用的内存。
2.1实例变量和类变量:
java程序的变量大体可分为成员变量和局部变量。其中局部变量可以分为如下3类:
形参:在方法签名中定义的局部变量,由方法调用者负责为其复制,随方法的结束而消亡
方法内的局部变量:在方法内定义的局部变量,必须在方法内对齐进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随方法的结束而消亡
代码块内的局部变量:在代码块内定义的局部变量,必须在代码块内对其进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随着代码块的结束而消亡。
局部变量的作用时间很短,它们都被存储在方法的栈内存中
类体内定义的变量被称为成员变量(Field)。如果定义该成员变量时没有使用static修饰,该成员变量又被称为非静态变量或者实例变量,如果使用了static修饰,则该成员变量又可被称为静态变量或类变量
对于static关键字而言,从词义上来看,它是“静态”的意思。但是从java程序的角度来看,static的作用就是将势力成员变成类成员。static只能修饰在类里定义的成员部分,包括成员变量、方法、内部类、初始化块、内部枚举类。如果没有使用static,这里成员属于该类的实例;如果使用了static修饰,这些成员就属于类本身。从这个意义看来,static只能修饰类里的成员,不能修饰外部类、不能修饰局部变量、局部内部类
表面上看,java类里定义成员变量时没有先后顺序,但实际上java要求定义成员变量时必须采用合法的前向引用。
public class ErrorDef{
//下面代码将提示:非法向前引用
int num1 = num2 +2;
int num2 = 20;
}
类似的,两个类变量也不允许采用这种“非法前向引用”
public class ErrorEef2{
//下面代码将提示:非法前向引用
static int num1 = num2 +2;
static int num2 = 20;
}
但是如果一个实施例变量,一个是类变量,则实例变量总是可以引用类变量
public class RightDef{
//下面的代码将完全正常
int num1 = num2 +2;
static int num2 = 20;
}
前面情况的原因是实例变量的初始化时机与顺序有关,类变量和成员变量的初始化时机也不太用。
2.1.1实例变量和类变量的属性
static修饰的成员变量时类变量,属于类本身;没有static修饰的成员变量时实例变量,属于该类的实例。在同一个JVM内,每个类只对应一个Class对象,但是每个类可以创建多个java对象。
一个类的类变量只需要一块内存空间,对于实例变量,该类每创建一次实例,就需要为实例变量分配一块内存
class Person{ String name; int age; static int eyeNum; public void info(){ System.out.println("我的名字是: " + name + ",我的年龄是:" + age ); } } public class FieldTest { public static void main(String[] args){ //类变量属于该类本身,只要该类初始化完成,程序即可使用此变量 Person.eyeNum = 2; System.out.println("Person的eyeNum属性:" + Person.eyeNum); Person p = new Person(); p.name = "猪八戒"; p.age = 300; System.out.println("通过p变量访问eyeNum类变量:" + p.eyeNum); p.info(); p.eyeNum = 3; System.out.println("Person的eyeNum属性:" + Person.eyeNum); System.out.println("通过p变量访问eyeNum类变量:" + p.eyeNum); } }
大部分时候会把类和对象严格地区分开来,但是从另一个角度看,类也是对象,虽有的类都是Class的实例。每个类初始化完成之后,系统都会为该类创建一个对应的Class实例,程序可以通过反射获取某个类所对应的Class实例。
一旦Person类初始化完成,程序即可通过Person类访问eyeNum类变量,除此之外java还允许通过Person类的任意实例来访问eyeNum类变量。虽然java允许通过Person对象来访问Person类的eyeNum类变量,但由于Person对象本身并没有eyeNum类变量,因此程序通过Person对象来访问eyeNum类变量时,底层依然会转换为通过Person访问eyeNum的类变量。
2.1.2实例变量的初始化时机
对于实例变量而言,它属于java对象本身,每次程序创建java对象时都需要为实例变量分配内存空间,并执行初始化。
从程序运行的角度来看,每次创建java对象都会为实例变量分配内存空间,并对实例变量执行初始化。
从语法角度来看,程序可以在3个地方对实例变量执行初始化:
1、定义实例变量时指定初始值
2、非静态初始化代码块中对实例变量指定初始值
3、构造器中对实例变量指定初始值
其中第1、2种方式比第三种方式更早执行,但1、2种方式的执行顺序与他们在源程序中的排列顺序相同
class Cat{ String name; int age; public Cat(String name, int age){ System.out.println("执行构造器"); this.name = name; this.age = age; } { System.out.println("执行非静态初始化块"); weight = 2.0; } double weight = 2.3; public String toString(){ return "Cat[name=" + name + ",age=" + age + ",weight=" + weight + "]"; } } public class InitTest { public static void main(String[] args){ Cat cat = new Cat("kitty", 2); System.out.println(cat); Cat c2 = new Cat("Jerfield", 3); System.out.println(c2); } }
初始化块中指定初始值与定义weight时指定初始值,都属于对该实例变量执行的初始化操作,它们的执行顺序与他们在源程序中的排列顺序相同。
定义实例变量时指定的初始值、初始化块中为实例变量指定初始值的语句的地位是平等的,当经过编译器处理后,它们都将会被提取到构造器中。
double weight = 2.3;
实际上会被分为两步执行:
1、double weight; 创建java对象时系统根据该语句为该对象分配内存
2、weight = 2.3; 这条语句将会被提取到java类的构造器中执行
这个的话可以将生成的Class文件反编译看下。或者javap看编译器的处理。
2.1.3 类变量的初始化时机
实例变量属于java类本身,只有当程序初始化该Java类时才会为该类的类变量分配内存空间,并执行初始化。
从程序运行角度看,每个JVM对一个java类只初始化一次,因此Java程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化。
从语法角度来看,程序可以在2个地方对类变量执行初始化:
1、定义类变量时指定初始值
2、静态初始化块中对类变量指定初始值
这两种方式的执行顺序和它们在源代码中的排列顺序相同。我感觉Class类应该也有类似隐式构造这种概念
class Price{ final static Price INSTANCE = new Price(2.8); static double initPrice = 20; double currentPrice; public Price(double discount){ currentPrice = initPrice - discount; } } public class PriceTest { public static void main(String[] args){ System.out.println(Price.INSTANCE.currentPrice);//首先时机,声明Price中的变量,默认值与类型有关,然后按顺序初始化类变量,先INSTANCE后initPrice,所以new Price时initPrice为0,返回的是-2.8 Price p = new Price(2.8); System.out.println(p.currentPrice);//这个时候new 了一个,走构造20 -2.8=17.2 } }
2.2父类构造器
当创建任何java对象时,程序总是会依次调用每个父类静态代码块、父类构造器执行初始化,最后才调用本类的非静态初始化块、构造器执行初始化
2.2.1隐式调用和显式调用
当调用某个类的构造器来创建java对象时,系统总会先调用父类的非晶态初始化块进行初始化。这个调用时隐式执行的,而且父类的静态初始化块总是会被执行。接着调用父类的一个或多个构造器执行初始化,这个调用既可以是通过super进行显式调用,也可以是隐式调用。
class Creature{ { System.out.println("Creature的非静态初始化块"); } public Creature(){ System.out.println("Creature无参数的构造器"); } public Creature(String name){ this(); System.out.println("Creature带有name参数的构造器,name参数:" +name); } } class Animal extends Creature{ { System.out.println("Animal的非静态初始化块"); } public Animal(){} public Animal(String name){ super(name); System.out.println("Animal带一个参数的构造器,name参数:" + name); } public Animal(String name, int age){ this(name); System.out.println("Animal带两个参数的构造器,其中age:" + age); } } class Wolf extends Animal{ { System.out.println("Wolf的非静态初始化块"); } public Wolf(){ super("灰太狼",3); System.out.println("Wolf无参数的构造器"); } public Wolf(double weight){ this(); System.out.println("Wolf的带weight参数的构造器,weight参数:" + weight); } } public class InitTest02 { public static void main(String[] args){ new Wolf(5.6); } }
在程序创建java对象,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的实例。至于调用父类那个构造器执行初始化,则分为如下几种情况:
1、子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表来确定调用父类的哪个构造器
2、子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统根据this调用里传入的实参列表来确定本类另一个构造器
3、子类构造器执行中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器
注意:
super调用用于显式调用父类的构造器,this调用用于显式调用另一个重载的构造器,super和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代码。
2.2.2访问子类对象的实例变量
当创建任何java对象时,程序总是会依次调用每个父类静态代码块、父类构造器执行初始化,最后才调用本类的非静态初始化块、构造器执行初始化
2.2.1隐式调用和显式调用
当调用某个类的构造器来创建java对象时,系统总会先调用父类的非晶态初始化块进行初始化。这个调用时隐式执行的,而且父类的静态初始化块总是会被执行。接着调用父类的一个或多个构造器执行初始化,这个调用既可以是通过super进行显式调用,也可以是隐式调用。
class Creature{ { System.out.println("Creature的非静态初始化块"); } public Creature(){ System.out.println("Creature无参数的构造器"); } public Creature(String name){ this(); System.out.println("Creature带有name参数的构造器,name参数:" +name); } } class Animal extends Creature{ { System.out.println("Animal的非静态初始化块"); } public Animal(){} public Animal(String name){ super(name); System.out.println("Animal带一个参数的构造器,name参数:" + name); } public Animal(String name, int age){ this(name); System.out.println("Animal带两个参数的构造器,其中age:" + age); } } class Wolf extends Animal{ { System.out.println("Wolf的非静态初始化块"); } public Wolf(){ super("灰太狼",3); System.out.println("Wolf无参数的构造器"); } public Wolf(double weight){ this(); System.out.println("Wolf的带weight参数的构造器,weight参数:" + weight); } } public class InitTest02 { public static void main(String[] args){ new Wolf(5.6); } }
在程序创建java对象,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的实例。至于调用父类那个构造器执行初始化,则分为如下几种情况:
1、子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表来确定调用父类的哪个构造器
2、子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统根据this调用里传入的实参列表来确定本类另一个构造器
3、子类构造器执行中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器
注意:
super调用用于显式调用父类的构造器,this调用用于显式调用另一个重载的构造器,super和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代码。
子类的方法可以访问父类的实例变量,这是因为子类继承父类就会获得父类的成员变量和方法,但父类的方法不能访问子类的实例变量,因为父类根本无从得知它将被哪个子类继承,它的子类将会增加怎样的成员变量。
但是在极端的情况下,可能出现父类访问子类变量的情况。
class Base{ private int i = 2; public Base(){ this.display(); } public void display(){ System.out.println(i); } } class Derived extends Base{ private int i = 22; public Derived(){ i = 222; } public void display(){ System.out.println(i); } } public class Test01 { public static void main(String[] args){ new Derived(); //1 /** * 得到的输出结果是0 * 匪夷所思吗? * 当程序在1的位置的时候,这个Derived对象其实是有两个i实例变量的。 * * java对象是由构造器构建的吗?实际情况是构造器只对java对象实例变量进行初始化,也就是赋初始值。在执行构造器代码之前,该变量所占的内存已被分配下来,这些内存里面默认是空值。 * 当new操作的时候,系统先为Derived对象分配内存空间,这时需要为Derived的实例变量分配两块内存,分别用于存放Derived的两个i实例变量,一个是它定义的,一个是父类定义的。此时的i的值都是0 * 在程序执行Derived的构造之前会先执行父类构造,编译后,父类的构造如下格式: * i = 2; //将赋值提取到构造 * this.display(); * */ } }
上面情况的原因我还是有点混,记死几点吧:
当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它所引用的对象的实例方法时,该方法的行为将由它实际所引用的对象来决定。
this的编译类型是它所在的类Base,运行时类型是实际调用的类也就是Derived
2.2.3调用被子类重写的方法
在访问权限允许的情况下,子类可以调用父类方法,这是因为子类继承父类会获得父类定义的成员变量和方法;但是父类不能调用子类的方法,因为父类根本无从得知它将被哪个子类继承,它的子类将会增加怎么样的方法。
但是,有特殊情况:
class Animal{ private String desc; public Animal(){ this.desc = getDesc();//这里隐式为 this.desc = this.getDesc();所以getDesc代表的是实际对象的getDesc方法所以是被重写了的那个getDesc方法,因为子类还没走构造,所以都是初始值。 } public String getDesc(){ return "Animal"; } public String toString(){ return desc; } } public class Wolf extends Animal{ private String name; private double weight; public Wolf(String name, double weight){ this.name = name; this.weight = weight; } public String getDesc(){ return "Wolf[name=]" + name +" ,weight=" + weight + "]"; } public static void main(String[] args){ System.out.println(new Wolf("灰太狼", 32.3)); } }
父子类型的内存控制
2.3.1继承成员变量和继承方法的区别
好多书籍都会介绍:当子类继承父类时,子类会获得父类中定义的成员变量和方法。当访问权限允许的情况下,子类可以直接访问父类中定义的成员变量和方法。
其实,java继承中对成员变量和方法的处理是不同的。
class Base{ int count = 2; public void display(){ System.out.println(this.count); } } class Derived extends Base{ int count = 20; public void display(){ System.out.println(this.count); } } public class FieldAndMethodTest { public static void main(String[] args){ Base b = new Base(); System.out.println(b.count);//2 b.display();//2 Derived d = new Derived(); System.out.println(d.count);//20 Base bd = new Derived(); System.out.println(bd.count);//2 bd.display();//20 Base d2b = d; System.out.println(d2b.count);//2 } }
通过对象.属性访问成员变量,得到的时对象声明类型的值
对象.方法的话,调用的是实际对象的方法
可见,java继承在处理成员变量和方法时是有区别的
class Animal{ public String name; public void info(){ System.out.println(name); }; } public class Wolf extends Animal{ private double weight; }
使用javap分析可以看到:
Wolf继承Animal的时候,编译器会直接将Animal里的void info()方法转移到Wolf钟,如果Wolf也包含了void info()方法,编译器无法将Animal里的void info()方法转移到Wolf类中。这意味着编译器无法将Animal的void info()方法转移到Wolf类中
对于实例变量则不存在这样的现象,即使子类中定义了与父类完全同名的实例变量,这个变量依然不可能覆盖父类中定义的实例变量。
因为继承成员变量和继承方法之间存在这样的区别,所以对于一个引用类型的变量而言,当通过该变量来访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型;当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型
2.3.2内存中子类实例
前面我们发现,两个引用变量引用同一个对象,但是程序通过这两个引用变量访问同名实例变量时,输出不同的结果
Derived d = new Derived();
System.out.println(d.count);
Base d2b = d;
System.out.println(d2b.count);
可以发现,对象引用同一个堆地址,输出的一个是20一个是2,千米只能说过,当通过引用变量访问它所引用的实例变量时,该实例变量的值取决于声明该变量时所用的类型。
Derived对象在内存中到底如何存储的呢?很显然它有两个不同的count实例变量,这意味着必须用两块内存保存它们。
class Base{ int count = 2; } class Mid extends Base{ int count = 20; } public class Sub extends Mid{ int count = 200; public static void main(String[] args){ Sub s = new Sub(); Mid s2m = s; Base s2b = s; System.out.println(s.count); System.out.println(s2m.count); System.out.println(s2b.count); s.accessMid(); } public void accessMid(){ System.out.println(super.count); } }
此时的super代表什么?super代表父类的默认实例?这个说法含糊而笼统,如果super代表父类的默认实例,那么这个默认实例在哪里?
两个内存图:

如果图右时对的,也就是内存中保存3个java对象,三个对象各具有一个count实例变量,super正是引用sub对象关联的Mid对象,实际上图左是对的,只有一个sub对象,且sub对象持有3个count实例变量。
注意:系统内存中并不存在Mid和Base两个对象,程序内存中只有一个Sub对象,只是这个Sub对象中不仅仅保存了SUb中定义的所有实例变量,还保存了它的所有父类所定义的全部实例变量。
super的作用到底时什么?
class Fruit{ String color = "未确定颜色"; public Fruit getThis(){ return this; } public void info(){ System.out.println("Fruit方法"); } } public class Apple extends Fruit{ String color = "红色"; public void info(){ System.out.println("Apple方法"); } public void AccessSuperInfo(){ super.info(); } public Fruit getSuper(){ return super.getThis(); } public static void main(String[] args){ Apple a = new Apple(); Fruit f = a.getSuper(); System.out.println("a和f所引用的对象是否相同:" + (a == f)); System.out.println("访问a所引用对象的color实例变量:" + a.color); System.out.println("访问f所引用对象的color实例变量:" + f.color); a.info(); f.info(); a.AccessSuperInfo(); } }
程序中,Fruit类的粗体代码定义了一个getThis()方法,该方法直接返回调用该方法对象;接着Apple类的粗体子代码又定义了一个getSuper()方法,该方法返回super.getThis()。程序试图通过这种方式达到一个效果:当一个Apple对象调用getSuper()方法时,该方法返回该Apple对象所关联的父类对象。
结果:
getSuper()返回的时Apple对象本身,只是声明类型时Fruit。所以f.count得到的时Apple的count,f.info是Fruit的方法调用。
通过上面分析看出:super关键字本身并没有引用任何对象,它甚至不能被当成一个真正的引用变量使用,主要有如下两个原因:
1、子类方法不能直接使用return super;但使用return this;返回调用该方法的对象是允许的
2、程序不允许直接把super当成变量使用,例如试图判断super和a变量是否引用同一个java对象-- super == a; 但这条语句将引起编译错误
对于父、子对象在内存中存储有了准确的结论:当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即子类定义了与父类中同名实例变量。
如果在子类中定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。注意是隐藏而不是覆盖。
为了在子类方法中访问父类定义的、被隐藏的实例变量,或者为了在子类方法中调用父类中定义的、被覆盖的方法,可以通过super.作为限定六修饰这些实例变量和实例方法
2.3.3 父、子类的类变量
父、子类的类变量基本与此类似
class StaticBase{ static int count = 20; } public class StaticSub extends StaticBase{ static int count = 200; public void info(){ System.out.println("访问本类的count类变量:" + count); System.out.println("访问父类的count类变量:" + StaticBase.count); System.out.println("访问父类的count类变量:" + super.count); } public static void main(String[] args){ StaticSub sb = new StaticSub(); sb.info(); } }
如果需要访问父类中定义的count类变量,程序有两种方式:
直接使用父类的类名作为主调来访问count类变量
使用super.作为限定来访问count类变量
2.4 final修饰符
简单的几个口诀:
final可以修饰变量,被final修饰的变量被赋初始值之后,不能对它重新赋值
final可以修饰方法,被final修饰的方法不能被重写
final可以修饰类,被final修饰的类不能派生类
口诀是不够的。
2.4.1 final修饰的变量
被final修饰的实例变量必须显式指定初始值,而且只能在如下3个位置指定初始值
定义final实例变量时指定初始值
非静态初始化块中为final实例变量指定初始值
构造器中为final实例变量指定初始值
public class FinalInstanceVariableTest { public int var1 = "疯狂java讲义".length(); final int var2; final int var3; { var2 = "轻量级Jave EE企业应用实战".length(); } public FinalInstanceVariableTest(){ var3 = "疯狂XML讲义".length(); } public static void main(String[] args){ FinalInstanceVariableTest fiv = new FinalInstanceVariableTest(); System.out.println(fiv.var1); System.out.println(fiv.var2); System.out.println(fiv.var3); } }
上面例子可以知道:final实例变量必须显式被赋初始值,而且本质上final实例变量只能在构造器中被赋初始值。除此之外final实例变量将不能被再次赋值
对于final类变量而言,同样必须显式指定初始值,而且final类变量只能在2个地方指定初始值:
定义final类变量时指定初始值
在静态初始化块中为final类变量指定初始值
public class FinalClassVariableTest { final static int var1 = "疯狂java讲义".length(); final static int var2; static { var2 = "轻量级Java EE企业应用实战".length(); } public static void main(String[] args){ System.out.println(FinalClassVariableTest.var1); System.out.println(FinalClassVariableTest.var2); } }
final修饰局部变量的情形则比较简单--java本来就要求局部变量必须被显式的赋初始值,final修饰的局部变量一样需要被显式地赋初始值。与普通初始变量不同的是:final修饰的局部变量一旦被赋初始值后,再也不能重新赋值。
上面介绍,不难发现,final修饰符一个简单功能:被final修饰的变量一旦被赋予初始值,final变量的值以后将不能被改变。
除此之外,final修饰符还有一个功能。
class Price{ final static Price INSTANCE = new Price(2.8); final static double initPrice = 20; double currentPrice; public Price(double discount){ currentPrice = initPrice - discount; } } public class PriceTest { public static void main(String[] args){ System.out.println(Price.INSTANCE.currentPrice); Price p = new Price(2.8); System.out.println(p.currentPrice); } }
发现输出的都是17.2.进行javap观察,发现当使用final修饰类变量时,如果定义该final类变量时指定了初始值,而且该初始值在编译时就能被确定下来(简单的基本数据类型和string),系统将不会在静态初始化代码块中对该类变量赋初始值,而是在类定义中直接使用初始值代替final变量。final会将将其当成“宏变量”处理。也就是说所有出现该变量的地方,系统直接把它当成对应的值处理。对于上面的程序,由于使用final修饰了initPrice类变量,因此Price类的构造器中执行currentPrice = initPrice - discount;代码时,程序将会直接将initPrice替换成20.因此,执行该代码的效果相当于currentPrice = 20 -discount;
2.4.2 执行"宏替换"的变量
对于一个final变量,不管它是类变量、实例变量还是局部变量,只要定义该变量时使用了final修饰符修饰,并且在定义该final类变量时指定了初始值(在代码块,构造指定等不算),而且该初始值可以在编译时就被确定下来,那么这个final变量本质已经不再是变量,而是相当于一个直接量
2.4.3 final方法不能被重写
当final修饰某个方法时,用于限制该方法不可被它的子类重写。
学会使用@Override注解
2.4.4内部类中的局部变量
如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰符修饰。
interface IntArrayProductor{ int product(); } public class CommandTest{ public int[] process(IntArrayProductor cmd, int length){ int[] result = new int[length]; for(int i=0; i<length; i++){ result[i] = cmd.product(); } return result; } public static void main(String[] args){ CommandTest ct = new CommandTest(); final int seed = 5; int[] result = ct.process(new IntArrayProductor() { @Override public int product() { return (int)Math.round(Math.random() * seed); } },6); } }
为什么java要求内部类访问的局部变量必须使用final修饰?
对于普通局部变量而言,它的作用域就是停留在该方法内,当方法执行结束,该局部变量也随之消失;但内部类则可能产生隐式的“闭包”,闭包将使得局部变量脱离它所在的方法继续存在。
public class ClosureTest { public static void main(String[] args){ final String str = "Java"; new Thread(new Runnable(){ public void run(){ for(int i=0; i<100; i++){ System.out.println(str + "" + i); try{ Thread.sleep(100); }catch(Exception ex){ ex.printStackTrace(); } } } }).start();//1 } }
上面的第一条代码定义了一个局部变量。正常情况下,当程序执行完1代码之后,main方法的声明周期就结束了,局部变量str的用户域也会随之结束。但是只要新县城里的run方法没有执行完,匿名内部类的实例的生命周期就没有结束,将一直可以访问str局部变量的值,这就是内部类会扩大局部变量作用域的实例。
由于内部类可能扩大局部变量的作用域,如果再加上这个被内部类访问的局部变量没有使用final修饰,也就是变量的值可以随意改变,将引起极大的混乱,所以。

浙公网安备 33010602011771号