您的位置:首页 > 编程语言 > Java开发

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

2017-11-30 00:00 561 查看
文章出自:安卓进阶学习指南主要贡献者:Cloud9527

Alex_赵

Struggle

shixinzhang

读完本文你将了解:通过反编译介绍四种内部类

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

背景介绍


大家好,这篇文章是 《安卓进阶技能树计划》 的第一部分 《Java 基础系列》 的第三篇。我们做这个活动,除了要保证知识点的全面、完整,还想要让每一篇文章都有自己的思考,尽可能的将知识点与实践结合,努力让读者读了有所收获。每位小伙伴都有工作在身,每个知识点都需要经过思考、学习、写作、提交、审核、修改、编辑、发布等多个过程,所以整体下来时间就会慢一些,这里先向各位道歉。《Java 基础系列》初步整理大概有 12 篇,主要内容为:
抽象类和接口 (完成)

内部类

修饰符

装箱拆箱

注解

反射

泛型

异常(完成)

集合

IO

字符串

其他

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

既然你这么自信,那就来试两道笔试题吧!第一道:要求使用已知的变量,在三个输出方法中填入合适的代码,在控制台输出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.成员内部类

成员内部类就是最普通的内部类,它定义在一个类的内部中,就如同一个成员变量一样。如下面的形式: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个成员,编译器会生成多个access方法, 符号后面的数字会会随着不同的声明顺序而改变,可以理解为一种桥接方法。对比内部类的 
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 项目,欢迎关注!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: