栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

匿名内部类引用外部类对象是可以的且并不一定需要final及内部类引用外部类为私有时编译器会为其开“后门”

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

匿名内部类引用外部类对象是可以的且并不一定需要final及内部类引用外部类为私有时编译器会为其开“后门”

//例1:
public class test {

    public static String userName = "欢哥";

    public static void main(String[] args){

        Map map = new HashMap() {{
            put(userName, userName);
        }};

        System.out.println();

        userName = "333";

        System.out.println(map);

        System.out.println();

    }



}

输出:{欢哥=欢哥}

//例2:
public class test {

    public String userName = "欢哥";

    public static void main(String[] args){

        new test().aa();

    }

     public void aa(){

        Map map = new HashMap() {{
            put(userName, userName);
        }};

        System.out.println();

        userName = "333";

        System.out.println(map);

        System.out.println();
    }


}

输出:{{欢哥=欢哥}

这里修改userName,map打印却没变原因就是匿名内部类引入外部类对象的原理。即HashMap里的static class Node是一个静态内部类。解释在下面看"匿名内部类引用外部类对象需要final修饰?"

javac编译例2:
会生成test.class文件和test$1.class文件。
test$1.class文件:

class test$1 extends HashMap {  //继承了HashMap,自然得到了其Node类,可以put数据了
    test$1(test var1) {  //通过构造函数注入test对象
        this.this$0 = var1;  //把这个对象当作指针
        this.put(this.this$0.userName, this.this$0.userName);
    }
}

为什么引用外部类的字段却是可以不用final修饰的呢?

因为内部类保存了外部类的引用,因而内部类中对任何字段的修改都会真实的反应到外部类实例本身上,所以不需要用final来修饰它。

为什么匿名内部类引用外部类对象需要final修饰?
public class Outter {
 
    public static void main(String[] args)  {
        Outter outter = new Outter();
        int b = 10;
        outter.test(b);
    }
 
    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

  可以看到当 test 方法执行完毕之后,变量 a 的生命周期就结束了,而此时 Thread 对象的生命周期很可能还没有结束,那么在 Thread 的 run 方法中继续访问变量 a 就变成不可能了,但是又要实现这样的效果,怎么办呢?

反编译后…(省略)
  我们看到在 run 方法中有一条指令:bipush 10

  这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。

  这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中(通过反编译可以看到其实是通过生成的xxx$1.class文件通过构造函数注入了)。

  这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

即编译器的设计如此。


章后大发现————内部类引用外部类为私有时编译器会为其开“后门”:
  如上篇例2,userName是public修饰的,所以是可以任意类访问的,我们知道当为private是只有该类下可以访问该成员变量的,那么内部类是怎么获取private成员变量的呢?~~让我们反编译下吧!

源码:

public class test {

    private  String userName1 = "a1a";
    private  String userName2 = "a2a";
    private  String userName3 = "a3a";

    public static void main(String[] args){

        new test().aa();

    }

    public void aa(){

        Map map = new HashMap() {{
            put(userName2+userName3, userName2+userName1);
        }};

        System.out.println();

        userName2 = "333";

        System.out.println(map);

        System.out.println();
    }


}

反编译test$1.class:

class test$1 extends HashMap {
    test$1(test var1) {
        this.this$0 = var1;
        //这里我们可以看到如果是public修饰应该是this.this$0.userName才对
        //这个access$000是啥?
        this.put(test.access$000(this.this$0) + test.access$100(this.this$0), test.access$000(this.this$0) + test.access$200(this.this$0));
    }
}

  外部类的access$000, access$100,access$200等方法的调用代码,但反编译外部类时,却看不到这些代码。 这涉及到JAVA编译器中对内部类的处理方式。

  为了让内部类能够访问外部类的私有数据成员 (因为有些private数据成员是不提供外界访问它的所谓的getter()的) ,JAVA编译器会为外部类自动生成 static Type access$iii(Outer); 方法,供内部类调用。 一般的数据成员都是private的,因此这些方法实际上就是访问private的数据成员的 “后门”。

  编译器对这些自动生成的方法,在编译时进行检查,是不允许程序员直接来调用的,也就是不能直接调用这些后门。

  但也有方法类绕过这些检查,我们可以利用JAVA编译器对类的编译特性来绕过这个检查:

具体技术演示如下:
  第一步:定义如下的类:

class Outer {
private final int xx = 123;  
//由于是final,故不再自动生成access$000(Outer);  如果是final会直接在编译时被解释成字面量

public Inner getInner() {
return new Inner(); 
} 
public class Inner { 
public int getDate() { //这个必须要,因为内部类本身需要引用外部类变量才会生成“后门”,即编译成Outer.access$000(this.this$0);
return xx; 
} 
} //class Inner

 static int access$000(Outer outer)//这个是自已定义的!
 {
  return  1;
 }
}

  第二步:定义你的其它类,来直接调用这个access$000()方法:

public class test {
 public static void main(String[] args)
 {

  System.out.println(Outer.access$000(new Outer()));  //这个调用是没有问题的,因为是自己定义的!
 }

}

将上述两个JAVA文件编译成class,执行test的main函数(cmd输入java test)会输出 1

第三步:这是变戏法的一步:
  将第一步的类Outer改为如下:

class Outer {
private int xx = 123;  
//由于不是final,故自动生成access$000(Outer);

public Inner getInner() {
return new Inner(); 
} 
public class Inner { 
public int getDate() { //这个必须要,因为内部类本身需要引用外部类变量才会生成“后门”,即编译成Outer.access$000(this.this$0);
return xx; 
} 
} //class Inner

 
}

再次只编译Outer文件编译成class,执行test的main函数(cmd输入java test)会输出 123

完成偷天换日~~


  同理,如果成员变量同时被private和static修饰,则会变成这样(注意,生成的access$iii是外部类的方法!):

class test$1 extends HashMap {
    test$1(test var1) {
        this.this$0 = var1;
        this.put(test.access$000() + test.access$100(), test.access$000() + test.access$200());
    }
}

  所以后面源代码应该是这样的:
private修饰:

static String access$000(this.this$0){  //入参是外部类对象
	return this.this$0.userName;  //因为非静态成员变量需要对象.成员变量引用
}

private+static修饰:

static String access$000(){  //入参是外部类对象
	return test.userName;  //通过 类名.静态变量  test是类名
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/314540.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号