静态内部类
除了静态内部类之外,其它的都默认持有外部类的引用,构造函数中传进去,所以都可以通过Outer.this(默认的不需要显示)访问外部类的属性和方法,包括private的,另一方面这有可能造成内存泄漏,比如Android中handler的定义
非静态内部类(也叫成员内部类)
public class Outter {
private int age = 10;
class Inner {
public int getAge() {
return age;
//return Outter.this.age;
}
public Outter getOutter() {
return Outter.this;
}
}
}
对Outter$Inner.class反编译一下
class Outter$Inner
{
Outter$Inner(Outter paramOutter) {} //默认持有外部类引用
public int getAge()
{
return Outter.access$0(this.this$0);
}
public Outter getOutter()
{
return this.this$0;
}
}
age等价于Outter.this.age; 默认持有外部类的引用
局部内部类/匿名内部类
局部内部类/匿名内部类 不能有访问符,访问的局部变量必须final修饰 jdk8以前的话,必须强制是final,否则会编译不通过。但是jdk8,新特性只有检查到了基本类型改变了值/对象指向了新引用才会报编译错误,否则如果是只是访问基本类型/String或者修改对象内部是不会报错的
举例 局部变量是基本类型/String s = “binjing”; 此时在常量区,局部内部类默认构造函数会复制一份基本类型
public class Outter {
private String s = "Outter";
private void test() {
String s = "Local";
String name="binjing";
class LocalInner {
String s = "Inner";
public String getStr() {
return s; //此时返回的是"Inner",内部类的属性,局部变量“Local"被无视了
}
public String getOutterStr() {
return Outter.this.s; //此时返回的是外部类的属性"Outter"
}
public String getName(){
return name; //返回局部变量name
}
}
// s = "hahja"; jdk8报错,改变了s指向
}
可以看到局部内部类可以直接访问外部类属性,没有什么限制。同时jdk8中这样是不会报错的,但是jdk8以下版本就会强制要求s修饰为final
对Outter$1LocalInner.class反编译一下
class Outter$1LocalInner
{
String s = "Inner";
Outter$1LocalInner(Outter paramOutter, String paramString) {} //持有外部类的引用,同时只有一个String(name)参数,因为局部s被内部类的s覆盖掉了
public String getStr()
{
return this.s; //表明是内部类的属性
}
public String getOutterStr()
{
return Outter.access$1(this.this$0); //表明是外部类的属性
}
public String getName()
{
return this.val$name;
}
}
上述例子说明了内部类怎么去访问外部属性,内部属性,以及局部变量
private void test() {
String s = "binjing";
class LocalInner {
public String getStr() {
return s;
}
}
s = "LiaBin";
}
但是此时就算是在jdk8还是会报编译错误的,因为此时检测到局部变量被修改了,还是提示要final修饰。 jdk8上去掉s = "LiaBin";
即可不需要final修饰 Jdk8的特性
另外的例子,局部变量是对象类型,复制一个对象的引用,局部内部类默认构造函数会复制对象的引用,但是还是执行同一个对象的
private void test(StringBuilder name) {
new Thread() { //此时匿名内部类会复制name引用,但是指向的却是堆中的同一个对象
public void run() {
try {
Thread.sleep(1000);
System.out.println("test: " + name.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
name.append(" + haha");
//name = new StringBuilder("hahaaaa"); 报错,引用指向了别的对象
}
所以jdk8中这样是不会报错的,除非name = new StringBuilder("hahaaaa");
引用指向了别的对象会报错。当然在jdk8以下都报错,提示需要final
局部内部类为什么只能访问final局部变量?
通过Outter$1LocalInner.class反编译的结果我们可以看到,局部变量都会通过局部内部类的构造函数传递进去,为什么要这么处理?
局部变量的生命周期与局部内部类的对象的生命周期的不一致。局部变量当所处的函数执行结束后就已经死亡了,不存在了,但是局部内部类对象还可能一直存在(只要有人还引用该对象),这是就会出现了一个悲剧的结果,局部内部类对象访问一个已不存在的局部变量。Java为了避免上述情况,才发明了上述机制,偷偷地将局部变量的引用放在内部类对象的成员变量中。上述的例子不能说明这个问题
private void anoTest() {
String name = "BinJing";
new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(2000);
System.out.println("name: " + name);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}.start();
}
调用anoTest()方法,一调用那么name引用因为是局部变量在栈中(binjing还是在字符串常量池的),所以该变量立刻无效了,但是匿名对象Thread这个对象的生命周期需要等待2s才结束,休眠了2s,所以生命周期不一致了吧,如果不做局部变量都会通过局部内部类的构造函数传递进去处理,那么肯定有问题的,局部变量的name引用在栈中作用域一过就消失,Thread的name引用是在堆中的,new出来的对象都是在堆中创建,类的属性也在堆中,需要等到GC才能回收该对象
Outter$1.class反编译
class Outter$1
extends Thread
{
Outter$1(Outter paramOutter, String paramString) {} //生成的构造函数,默认持有外部类引用,同时会把访问的局部变量参数传递进来,一份拷贝
public void run()
{
try
{
Thread.sleep(2000L);
System.out.println("name: " + this.val$name);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
但是解决了生命周期的问题之后,又引出了另一个问题,这样的话,如果不用final修饰,那么内部类就可以肆无忌惮的修改局部变量name属性了,所以虚拟机为了约束这两个变量的一致性,所以虚拟机强制要求使用final,否则编译不过.final保证了内部类和局部变量是同一个对象或者基本类型,对于对象,修改对象的内容是没问题的