Java 基础夯实3:通过字节码了解内部类

文章出自:安卓进阶学习指南主要贡献者:

  • Cloud9527

  • Alex_赵

  • Struggle

  • shixinzhang

读完本文你将了解:

  • 通过反编译介绍四种内部类

  • 结合实战介绍内部类的使用场景

背景介绍

大家好,这篇文章是 《安卓进阶技能树计划》 的第一部分 《Java 基础系列》 的第三篇。

我们做这个活动,除了要保证知识点的全面、完整,还想要让每一篇文章都有自己的思考,尽可能的将知识点与实践结合,努力让读者读了有所收获。每位小伙伴都有工作在身,每个知识点都需要经过思考、学习、写作、提交、审核、修改、编辑、发布等多个过程,所以整体下来时间就会慢一些,这里先向各位道歉。

《Java 基础系列》初步整理大概有 12 篇,主要内容为:

  1. 抽象类和接口 (完成)

  2. 内部类

  3. 修饰符

  4. 装箱拆箱

  5. 注解

  6. 反射

  7. 泛型

  8. 异常(完成)

  9. 集合

  10. IO

  11. 字符串

  12. 其他

这一篇我们来聊聊内部类

“内部类”听起来是非常普遍的东西,有些朋友会觉得:这个太基础了吧,有啥好说的,你又来糊弄我。

既然你这么自信,那就来试两道笔试题吧!

第一道:要求使用已知的变量,在三个输出方法中填入合适的代码,在控制台输出30,20,10。

      class Outer {
            public int num = 10;
            class Inner {
                public int num = 20;
                public void show() {
                    int num = 30;
                    System.out.println(?);    //填入合适的代码
                    System.out.println(??);
                    System.out.println(???);
                }
            }
        }

        class InnerClassTest {
            public static void main(String[] args) {
                Outer.Inner oi = new Outer().new Inner();
                oi.show();
            }    
        }

接招,第二题:补齐代码 ,要求在控制台输出”HelloWorld

        interface Inter { 
            void show(); 
        }
        class Outer { 
            //补齐代码 
        }
        class OuterDemo {
            public static void main(String[] args) {
                  Outer.method().show();
              }
        }

题目来自:https://www.cnblogs.com/zhangyinhua/p/7260651.html

先思考几秒,看看这些题你能否应付得来。

在面试中常常遇到这样的笔试题,咋一看这题很简单,还是会有很多人答不好。根本原因是很多人对“内部类”的理解仅限于名称。

“内部类、静态内部类、匿名内部类”是什么大家都清楚。但是当转换一下思维,不仅仅为了完成功能,而是要保证整个项目架构的稳定灵活可扩展性,你会如何选择呢?

这篇文章我们努力回答这些问题,也希望你可以说出你的答案。

四种内部类介绍

定义在一个类中或者方法中的类称作为内部类。

内部类又可以细分为这 4 种:

  1. 成员内部类

  2. 局部内部类

  3. 匿名内部类

  4. 静态内部类

1.成员内部类

成员内部类就是最普通的内部类,它定义在一个类的内部中,就如同一个成员变量一样。如下面的形式:

public class OutClass2 {
    private int i = 1;
    public static String str = "outclass";

    class InnerClass { // 成员内部类
        private int i = 2;

        public void innerMethod() {
            int i = 3;
            System.out.println("i=" + i);
            System.out.println("i=" + this.i);
            System.out.println("i=" + OutClass2.this.i);
            System.out.println("str=" + str);
        }
    }}public class TestClass {

    public static void main(String[] args) {
        //先创建外部类对象
        OutClass2 outClass = new OutClass2(); 
        //创建内部类对象
        OutClass2.InnerClass in = outClass.new InnerClass();
        //内部类对象调用自己的方法
        in.innerMethod();
    }} 

因为内部类依附于外部类存在,所以需要外部类的实例来创建内部类:

outClass.new InnerClass()

注意不是直接 new outClass.InnerClass() 。

成员内部类可以无条件的访问外部类的成员属性和成员方法(包括 private 和 static 类型的成员),这是因为在内部类中,隐式地持有了外部类的引用。

我们编译上述的代码,可以看到,会生成两个 class 文件:

这个 OutClass2$InnerClass.class 就是内部类对应的字节码文件,我们使用 AS 打开,会自动进行反编译:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.example.simon.androidlife.innerclass;

import com.example.simon.androidlife.innerclass.OutClass2;

class OutClass2$InnerClass {
    private int i;

    OutClass2$InnerClass(OutClass2 var1) {
        this.this$0 = var1;
        this.i = 2;
    }

    public void innerMethod() {
        byte var1 = 3;
        System.out.println("i=" + var1);
        System.out.println("i=" + this.i);
        System.out.println("i=" + OutClass2.access$000(this.this$0));
        System.out.println("str=" + OutClass2.str);
    }
}

可以看到,在内部类 OutClass2$InnerClass 的字节码中,编译器为我们生成了一个参数为外部类对象的构造方法,这也解释了内部类为什么可以直接访问外部类的内容,因为持有外部类的引用

在这个不完整的反编译字节码中,我们可以看到,编译器会为内部类创建一个叫做 this$0 的对象,它是外部类的引用。

innerMethod() 中的 OutClass2.access$000(this.this$0)) 是什么意思呢?

为了帮助内部类访问外部类的数据,编译器会生成这个 access 方法, 参 数 是 外 部 类 的 引 用 , 如 果 外 部 类 有 N 个 成 员 , 编 译 器 会 生 成 多 个 a c c e s s 方 法 ,  符号后面的数字会会随着不同的声明顺序而改变,可以理解为一种桥接方法

对比内部类的 innerMethod() 的 java 代码和字节码我们可以得出这些结论:

  • 在内部类中,直接使用变量名,会按照从方法中的局部变量、到内部类的变量、到外部类的变量的顺序访问

  • 也就是说,如果在外部类、内部类、方法中有重名的变量/方法,编译器会把方法中直接访问变量的名称修改为方法的名称

  • 如果想在方法中强制访问内部类的成员变量/方法,可以使用 this.i,这里的 this 表示当前的内部类对象

  • 如果想在方法中强制访问外部类的成员变量/方法,可以使用 OutClass.this.i,这里的 OutClass.this 表示当前外部类对象

成员内部类就如同外部类的成员一样,同样可以被public、protected、private、缺省(default)这些修饰符来修饰。

但是有一个限制是:成员内部类不能创建静态变量/方法。如果我们尝试创建,编译器会直接 say no。

为什么会这样呢?

Stackoverflow 有一个回答很好:

“if you’re going to have a static method, the whole inner class has to be static. Without doing that, you couldn’t guarantee that the inner class existed when you attempted to call the static method. ”

我们知道要使用一个类的静态成员,需要先把这个类加载到虚拟机中,而成员内部类是需要由外部类对象 new 一个实例才可以使用,这就无法做到静态成员的要求。

2.静态内部类

说完成员内部类我们来看看静态内部类。

使用 static 关键字修饰的内部类就是静态内部类,静态内部类和外部类没有任何关系,可以看作是和外部类平级的类。

我们来反编译个静态内部类看看。

java 代码:

public class Outclass3 {

    private String name;
    private int age;

    public static class InnerStaticClass {

        private String name;

        public String getName() {
            return name;
        }

        public int getAge() {
            return new Outclass3().age;
        }
    }
}

编译后的静态内部类:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.example.simon.androidlife.innerclass;

import com.example.simon.androidlife.innerclass.Outclass3;

public class Outclass3$InnerStaticClass {
    private String name;

    public Outclass3$InnerStaticClass() {
    }

    public String getName() {
        return this.name;
    }

    public int getAge() {
        return Outclass3.access$000(new Outclass3());
    }
}

可以看到,静态内部类很干净,没有持有外部类的引用,我们要访问外部类的成员只能 new 一个外部类的对象。

否则只能访问外部类的静态属性和静态方法,同理外部类只能访问内部类的静态属性和静态方法

3.局部内部类

局部内部类是指在代码块或者方法中创建的类。

它和成员内部类的区别就是:局部内部类的作用域只能在其所在的代码块或者方法内,在其它地方是无法创建该类的对象。

public class OutClass4 {
    private String className = "OutClass";

    {
        class PartClassOne { // 局部内部类
            private void method() {
                System.out.println("PartClassOne " + className);
            }
        }
        new PartClassOne().method();
    }

    public void testMethod() {
        class PartClassTwo { // 局部类内部类
            private void method() {
                System.out.println("PartClassTwo " + className);
            }
        }
        new PartClassTwo().method();
    }}

上面的代码中我们分别在代码块和方法中创建了两个局部内部类,来看看编译后的它是怎么样的:

首先可以看到会创建两个 class 类,打开看下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.example.simon.androidlife.innerclass;

import com.example.simon.androidlife.innerclass.OutClass4;

class OutClass4$1PartClassOne {
    OutClass4$1PartClassOne(OutClass4 var1) {
        this.this$0 = var1;
    }

    private void method() {
        System.out.println("PartClassOne " + OutClass4.access$000(this.this$0));
    }
}

package com.example.simon.androidlife.innerclass;

import com.example.simon.androidlife.innerclass.OutClass4;

class OutClass4$1PartClassTwo {
    OutClass4$1PartClassTwo(OutClass4 var1) {
        this.this$0 = var1;
    }

    private void method() {
        System.out.println("PartClassTwo " + OutClass4.access$000(this.this$0));
    }
}

可以看到生成的这两个字节码和成员内部类生成的很相似,都持有了外部类的引用。

不过可惜的是出了它们声明的作用域,就再也无法访问它们,可以把局部内部类理解为作用域很小的成员内部类。

4.匿名内部类

先让我们来看一段最常见的代码

Car jeep=new Car();

在Java中操纵的标识符实际是指向一个对象的引用,也就是说 jeep 是一个指向 Car 类对象的引用,而右面的 new Car() 才是真正创建对象的语句。

这可以将 jeep 抽象的理解为 Car 类对象的“名字”,而匿名内部类顾名思义可以抽象的理解为没有“名字”的内部类:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
    }});

上面代码是 Android 中最常见的设置 button 的点击事件,其中 new OnClickListener() {…} 就是一个匿名内部类,在这里没有创建类对象的引用,而是直接创建的类对象。大部分匿名类用于接口回调。

由于 javac 无法编译 android 代码,我们写个这样的匿名内部类代码来尝试看看编译后的结果。

public class OutClass5 {
    private OnClickListener mClickListener;
    private OutClass5 mOutClass5;

    interface OnClickListener {
        void onClick();
    }

    public OutClass5 setClickListener(final OnClickListener clickListener) {
        mClickListener = clickListener;
        return this;
    }

    public OutClass5 setOutClass5(final OutClass5 outClass5) {
        mOutClass5 = outClass5;
        return this;
    }

    public void setClickInfo(final String info, int type) {
        setClickListener(new OnClickListener() {
            @Override
            public void onClick() {
                System.out.println("click " + info);
            }
        });

        setClickListener(new OnClickListener() {
            @Override
            public void onClick() {
                System.out.println("click2 " + info);
            }
        });
    }
}

上面的代码中,我们创建了一个内部接口,然后在 setDefaultClicker() 中创建了两个匿名内部类,编译后的结果:

可以看到生成了三个额外的类,OutClass5$OnClickListener 是生成的成员内部类字节码,而 OutClass5$1 和 OutClass5$2 则是两个实现 OnClickListener 的子类:

class OutClass5$1 implements OnClickListener {
    OutClass5$1(OutClass5 var1, String var2) {
        this.this$0 = var1;
        this.val$info = var2;
    }

    public void onClick() {
        System.out.println("click " + this.val$info);
    }
}
class OutClass5$2 implements OnClickListener {
    OutClass5$2(OutClass5 var1, String var2) {
        this.this$0 = var1;
        this.val$info = var2;
    }

    public void onClick() {
        System.out.println("click2 " + this.val$info);
    }
}

从反编译的代码可以看出:创建的每个匿名内部类编译器都对应生成一个实现接口的子类,同时创建一个构造函数,构造函数的参数是外部类的引用,以及匿名函数中访问的参数

现在我们知道了:匿名内部类也持有外部类的引用。

同时也理解了为什么匿名内部类不能有构造方法,只能有初始化代码块。 因为编译器会帮我们生成一个构造方法然后调用。

此外还可以看出,匿名内部类中使用到的参数是需要声明为 final 的,否则编译器会报错。

可能有朋友会提问了:参数为什么需要是 final 的?

我们知道在 Java 中实际只有一种传递方式:即引用传递。一个对象引用被传递给方法时,方法中会创建一份本地临时引用,它和参数指向同一个对象,但却是不同的,所以你在方法内部修改参数的内容,在方法外部是不会感知到的。

而匿名内部类是创建一个对象并返回,这个对象的方法被调用的时机不确定,方法中有修改参数的可能,如果在匿名内部类中修改了参数,外部类中的参数是否需要同步修改呢?

因此,Java 为了避免这种问题,限制匿名内部类访问的变量需要使用 final 修饰,这样可以保证访问的变量不可变

总结

本篇文章介绍了 Java 开发中四种内部类的概念、反编译后的格式。相信看完这篇文章,你对开头的两道题已经有了答案。

为了避免文章太长,我们把使用场景放到下一篇。

这个系列的目的是帮助大家系统、完整的打好基础、逐渐深入学习,如果你对这些已经很熟了,请不要吝啬你的评价,多多指出问题,我们一起做的更好!


点击“原文链接”去我们的 github 项目,欢迎关注!


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值