摘自海子:Java内部类详解

深入理解内部类

1.为什么成员内部类可以无条件访问外部类的成员?

  在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是Outter.java的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    protected class Inner {
        public Inner() {
             
        }
    }
}

  编译之后,出现了两个字节码文件:

  反编译Outter$Inner.class文件得到下面信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
  SourceFile: "Outter.java"
  InnerClass:
   #24= #1 of #22//Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
  minor version: 0
  major version: 50
  Constant pool:
const #1 class        #2;     //  com/cxh/test2/Outter$Inner
const #2 = Asciz        com/cxh/test2/Outter$Inner;
const #3 class        #4;     //  java/lang/Object
const #4 = Asciz        java/lang/Object;
const #5 = Asciz        this$0;
const #6 = Asciz        Lcom/cxh/test2/Outter;;
const #7 = Asciz        <init>;
const #8 = Asciz        (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz        Code;
const #10 = Field       #1.#11//  com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;//  this$0:Lcom/cxh/test2/Outter;
const #12 = Method      #3.#13//  java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;//  "<init>":()V
const #14 = Asciz       ()V;
const #15 = Asciz       LineNumberTable;
const #16 = Asciz       LocalVariableTable;
const #17 = Asciz       this;
const #18 = Asciz       Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz       SourceFile;
const #20 = Asciz       Outter.java;
const #21 = Asciz       InnerClasses;
const #22 class       #23;    //  com/cxh/test2/Outter
const #23 = Asciz       com/cxh/test2/Outter;
const #24 = Asciz       Inner;
 
{
final com.cxh.test2.Outter this$0;
 
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   putfield        #10//Field this$0:Lcom/cxh/test2/Outter;
   5:   aload_0
   6:   invokespecial   #12//Method java/lang/Object."<init>":()V
   9:   return
  LineNumberTable:
   line 160
   line 189
 
  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      10      0    this       Lcom/cxh/test2/Outter$Inner;
 
 
}

  第11行到35行是常量池的内容,下面逐一第38行的内容:

final com.cxh.test2.Outter this$0;

  这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:

public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);

  从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

2.为什么局部内部类和匿名内部类只能访问局部final变量?

  Java编译器实现的是capture by value,并没有实现capture by reference.  

  想必这个问题也曾经困扰过很多人,在讨论这个问题之前,先看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

  这段代码会被编译成两个class文件:Test.class和Test1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class(x为正整数)。1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outte

  

  根据上图可知,test方法中的匿名内部类的名字被起为 Test$1。

  上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题:

  当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制  的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:

  我们看到在run方法中有一条指令:

bipush 10

  这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

  下面再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int a) {
        new Thread(){
            public void run() {
                System.out.println(a);
            };
        }.start();
    }
}

  反编译得到:

  我们看到匿名内部类Test$1的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。

  也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

  从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?

  对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

  到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。

  3.静态内部类有特殊的地方吗?

  从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

内部类的使用场景和好处

  为什么在Java中需要内部类?总结一下主要有以下四点:

  1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,

  2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。

  3.方便编写事件驱动程序

  4.方便编写线程代码

  个人觉得第一点是最重要的原因之一,内部类的存在使得Java的多继承机制变得更加完善。

补充

匿名内部类是唯一一种没有构造器的类。

非静态内部类为什么不能有静态成员变量和静态方法:

  1. static类型的属性和方法,在类加载时就会存在于内存中。
  2. 要想使用某个类的static属性和方法,那么这个类必须要加载到虚拟机中。
  3. 非静态内部类并不随外部类一起加载,只有再实例化外部类之后才会加载。

Java 深入理解内部类的更多相关文章

  1. [java] 深入理解内部类: inner-classes

    [java] 深入理解内部类: inner-classes // */ // ]]>   [java] 深入理解内部类: inner-classes Table of Contents 1 简介 ...

  2. java 深入理解内部类以及之间的调用关系

    什么是内部类 内部类是指在一个外部类的内部再定义一个类.内部类作为外部类的一个成员,并且依附于外部类而存在的.内部类可为静态,可用protected和private修饰(而外部类只能使用public和 ...

  3. Java基础(53):内部类(转)

    java中的内部类总结 内部类不是很好理解,但说白了其实也就是一个类中还包含着另外一个类 如同一个人是由大脑.肢体.器官等身体结果组成,而内部类相当于其中的某个器官之一,例如心脏:它也有自己的属性和行 ...

  4. Effective Java通俗理解(持续更新)

    这篇博客是Java经典书籍<Effective Java(第二版)>的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约 ...

  5. Java 中的内部类

    前言 在第一次把Java 编程思想中的内部类这一章撸完后,有点印象.大概知道了什么时内部类,局部内部类,匿名内部类,嵌套内部类.随着时间的推移,自己慢慢的就忘记了,总感觉自己思考的东西不多,于是 看了 ...

  6. Effective Java通俗理解(上)

    这篇博客是Java经典书籍<Effective Java(第二版)>的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约 ...

  7. Java基础(五)--内部类

    内部类简单来说就是把一个类的定义放到另一个类的定义内部 内部类分为:成员内部类.局部内部类.匿名内部类.静态内部类 成员内部类:最常见的内部类 public class Outter { privat ...

  8. Java中的内部类怎么用

    一.为什么需要内部类?java内部类有什么好处?为什么需要内部类? 首先举一个简单的例子,如果你想实现一个接口,但是这个接口中的一个方法和你构想的这个类中的一个方法的名称,参数相同,你应该怎么办?这时 ...

  9. Java中的内部类(成员内部类、静态内部类、局部内部类、匿名内部类)

    Java中的内部类(成员内部类.静态内部类.局部内部类.匿名内部类) 神话丿小王子的博客主页 我们先看这样一段话:人是由大脑.肢体.器官等身体结果组成.而组成我们人体的心脏它也有自己的属性和行为(血液 ...

随机推荐

  1. 支持n多视频网站的下载,推荐 you-get

    其实刚开始是想下载youtube视频, 网上找了下 其中之一:http://jingyan.baidu.com/article/39810a23d2deb2b637fda66c.html 访问一个在线 ...

  2. VBScript开发Excel常见问题

    VBS基础 基本概念:VB & VBS & VBA VB.VBScript和VBA(Visual Basic For Application)这三种语言,既有联系又有区别.三种语言的语 ...

  3. Totem协议(SRP/RRP)讲解

    基本概念 •SRP:  The Totem Single-Ring Ordering and MembershipProtocol            –基于以太网的组通信协议,节点间组成单环结构 ...

  4. DrawerLayout使用

    1 :DrawerLayout侧边栏用于实现如图所示的效果:

  5. 软工读书笔记 week3 (《黑客与画家》上)

    一.何谓黑客? 黑客,在我们大多数普通人眼里,就是入侵计算机的人,通常还与干坏事挂钩.而书中告诉我们,这 并不是它的真正含义.而要想理解这本书,就要首先理解什么是黑客. 黑客这个词最初起源时,完全是一 ...

  6. win10系统80端口被System (PID=4)占用的解决

    今天想用wamp搭建虚拟目录.发现80端口被占用,操作挺麻烦的,所以想要更改. 具体流程如下: 1.“win+R”输入“cmd”,然后输入“netstat -ano | findstr "8 ...

  7. Bearer Token的加密解密规则(OAuth中间件)

    在OAuthBearerAuthenticationMiddleware中使用Microsoft.Owin.Security.DataHandler. SecureDataFormat<TDat ...

  8. December 10th 2016 Week 50th Saturday

    Storms make trees take deeper roots. 风暴使树木深深扎根. Sometimes, you may feel frustrated for failing to wi ...

  9. December 31st 2016 Week 53rd Saturday

    In every triumph, there's a lot of try. 每个胜利背后都有许多尝试. This Year is over, and let it be. It would be ...

  10. December 27th 2016 Week 53rd Tuesday

    A journey of one thousand miles begins with one step. 千里之行始于足下. No matter how slowly you walk, as lo ...