从内存逻辑模型分析Java中的内部类

时间:2019-04-18
本文章向大家介绍从内存逻辑模型分析Java中的内部类,主要包括从内存逻辑模型分析Java中的内部类使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Copyright©Stonee

还记得那是一个晴空万里的上午,一个举世瞩目的语言诞生在一栋不大不小的房子里。(上述扯淡),在Java诞生之初,是不存在内部类的。但是由于编程语言之间激烈的竞争,内部类在java1.1呼之欲出,所以才登上了时代的舞台。但是需要注意的是,内部类是编译器的语法,这在jvm中是不承认的,jvm依旧只认识普通的外部类,所以编译器就需要把内部类换成jvm可以识别的外部类。从上面我们可以得知, 内部类只是通过编译器方便了程序员 。事实上,我们完全可以不通过内部类去实现内部类的功能,不过那要复杂的多。
内部类中不能有static方法

1. 成员内部类

成员内部类是指不能带static的类,可以访问外部所有的字段(包括private),外部类同样也可以访问内部类的private
过程分为两步:

  • 第一步,先让内部类(在jvm看来是一个普通的外部类)指向外部类用来获取外部类的权限
  • 第二步,让内部类通过外部类的一个特殊方法去调用外部类的private字段

  • 我们先通过一个栗子来观察成员内部类是如何工作的:
//源程序
public class Cow {    
    private double weight;    
    public Cow(double weight) {        
        this.weight = weight;    
    }
    public void test() {      
    //CowLeg类的实例对象是在Cow类的实例方法中创建的。        
    //所以,在创建CowLeg内部类的实例对象之前,必先创建Cow外部类的实例对象。        
        CowLeg cl = new CowLeg(1.12, "黑白相间");       
        cl.info();   
    }
    
    //内部类
    private class CowLeg {     
        private double length;        
        private String color;        
        public CowLeg(double length, String color) {            
            this.length = length;            
            this.color = color;       
        }        
        public void info() {           
            System.out.println("当前牛腿颜色是:" + color + ", 高:" + length);           
            System.out.println("本牛腿所在奶牛重:" + weight);            
            }
        }    
        

很显然这是一个极为简单的成员内部类栗子,那么我们将通过字节码来观察到Java编译器是如何让内部类可以访问外部的private字段的。

//字节码反编译
public class Cow {    
    private double weight;    
    public Cow(double weight) {        
        this.weight = weight;    
    }
    
    //java编译器自主加入的静态方法,为了让子类可以通过cow类直接返回weight(如果不是调用private字段可以不要这个方法)
    static double access$0(Cow arg0){
        return arg0.weight;
    }
    
    public void test() {      

        CowLeg cl = new CowLeg(this,1.12, "黑白相间");       //cow类会在调用内部类时加入this,这个this是cow类型的,此时内部类可以通过this指向外部类
        
        cl.info();   
    }
}
    
    //内部类
    private class CowLeg {      //会先改个名字:Cow$Cowleg,下面的构造方法也需要改,不再赘述
        private double length;        
        private String color;        
        
        //java编译器自主加入实例域,为了获得外部类的this
        final Cow this$0;
        
        //java编译器会在内部构造方法中加入形参
        public CowLeg(Cow arg0, double length, String color) {      //此处会加入arg0
            this.this$0 = arg0;     //this$0指向cow类中的this
            this.length = length;            
            this.color = color;       
        }        
        public void info() {           
            System.out.println("当前牛腿颜色是:" + color + ", 高:" + length);           
            System.out.println("本牛腿所在奶牛重:" + Cow.access$0(this.this$0));    //此处会调用外部类的access代替weight        
            }
        }  
        

要注意,当java编译器编译之后,cow和cowleg是两个独立的类,不存在嵌套关系

  • 看下成员内部类的内存模型吧:
  • 刚开始学习内部类的时候,我有这么一个疑问,不就是访问另外一个类的private字段吗,用得着这么麻烦吗?譬如下例子:
class a{    
    private int b =1;    
    public int getB(){
        return b;    
    }
class Cow{    
    public static void main(String [] args){        
        System.out.println(new b().getB());
    }
}

我们完全可以通过getB来访问a中的b,为什么在内部类中费那么大力气访问外部类的private呢?

 在我个人看来,当我们用getB来实现访问外部类的private的时候,需要new一个a的实例才可以,这样无疑消耗的空间,所以java编译器通过了另外一种方法,即用static的静态方法来直接调用类中的private字段,但是又因为b不是一个静态字段,所以必须要让Cow指向a类才可以,所以语法才会变得复杂化。
 另外我们需要知道,内部类的作用不仅仅是可以访问外部类的private字段,内部类还可以对同一个包中的其他类隐藏起来。如果想要定义一个回调函数的话,使用匿名内部类或者lambda表达式(现在已经代替了匿名内部类)会更加方便。

class a{    
    private int b =1;    
    Cow stonee = new Cow(this);    
    static int accessB(a outer){        
        return outer.b;    
    }
}

class Cow{     
    a thiss;    
    public Cow(){    
    }       
    public Cow(a thiss){        
        this.thiss = thiss;    
    }    
    public void ooo(){       
        System.out.println(a.accessB(thiss));   
    }    
    public static void main(String [] args){       
        new Cow().ooo();    
    }
}

这个栗子可以和上面使用getB方法的作比较,同样都是调用其他类的私有字段

成员内部类不能定义静态属性或者静态方法

2. 静态内部类

  将成员内部类的使用再深入限制一步,假如内部类的实例对象不需要引用外部类的实例对象,只是将一个类隐藏在另外一个类的内部,可将该内部类静态化。在外部类的内部,定义的静态的内部类,叫静态内部类。(或叫嵌套类)

  • 静态内部类可以访问外部类包括private的所有静态成员

  • 静态内部类不能访问外部类实例成员

  • 内部类中可以new生成外部类的实例

  • 静态内部类可以定义static方法

  • 通过栗子:

//源程序
public class StaticInnerClassDemo {

    private int prop1 = 5;
    private static int prop2 = 9;
    
    
    static class StaticInnerClass {
    
        // 静态内部类里可以包含静态成员
        // private static int age;
        // private int number = 28;
        public void accessOuterProp() {

            // 下面代码出现错误:
            // 静态内部类无法直接访问外部类的实例变量
            //System.out.println(prop1);
            // 下面代码正常
            System.out.println(prop2);
        }
    }
    public static void main(String[] args) {

        StaticInnerClass staticInnerClass = new StaticInnerClass();
        staticInnerClass.accessOuterProp();
    }

}
  • 字节码反编译程序
//字节码反编译程序
public class StaticInnerClassDemo {

    private int prop1 = 5;
    private static int prop2 = 9;
    
    //如果内部类调用外部类的private会添加下面的程序,否则不用
    static int access&0(){
        return StaicInnerClassDemo.prop2;
    }
    
    public static void main(String[] args) {

        StaticInnerClass staticInnerClass = new StaticInnerClass();
        staticInnerClass.accessOuterProp();
    }
}

    static class StaticInnerClass { //换名字StaticInnerClassDeom$StaticInnerClass
    
        public void accessOuterProp() {

            System.out.println(StaticInnerClassDemo.access$0());    //换掉prop2
        }
    }
  • 内存逻辑模型

从上面也可以验证我在成员内部类中说的为什么 在我个人看来,当我们用getB来实现访问外部类的private的时候,需要new一个a的实例才可以,这样无疑消耗的空间,所以java编译器通过了另外一种方法,即用static的静态方法来直接调用类中的private字段,但是又因为b不是一个静态字段,所以必须要让Cow指向a类才可以,所以语法才会变得复杂化。 这段话
在静态内部类中,以为引用外部类的字段是静态的,所以就直接通过一个静态方法调用就好了,和getB的作用是一样的

java 内部类和静态内部类的区别

  • 静态内部类可以有静态成员(方法,属性),而非静态内部类则不能有静态成员(方法,属性)。

  • 静态内部类只能够访问外部类的静态成员,而非静态内部类则可以访问外部类的所有成员(方法,属性)。

  • 实例化一个非静态的内部类的方法:

a.先生成一个外部类对象实例
OutClassTest oc1 = new OutClassTest();
b.通过外部类的对象实例生成内部类对象
OutClassTest.InnerClass no_static_inner = oc1.new InnerClass();

  • 实例化一个静态内部类的方法:

a.不依赖于外部类的实例,直接实例化内部类对象
OutClassTest.InnerStaticClass inner = new OutClassTest.InnerStaticClass();
b.调用内部静态类的方法或静态变量,通过类名直接调用
OutClassTest.InnerStaticClass.static_value OutClassTest.InnerStaticClass.getMessage()

3. 局部内部类

在外部类的方法中,定义的非静态的命名的内部类,叫局部内部类。(因为内部类可以访问外部类方法的形参和局部变量而得此名)可以分为:

  • 外部类的实例方法内部 的局部内部类
  • 外部类的静态方法内部 的局部内部类
1. 实例方法的局部内部类
  • 内部类可以访问实例方法的形参,局部变量和外部类的所有成员
  • 在jdk8之前,实例方法中的形参必须要写上final,现在写不写均可,编译器会自动加上

看一个源程序

class InstanceLocalOut {
    private int age = 12;
    
    public void Print(final int x) {
        final int m = 8;
    
        // 在实例方法中定义一个局部内部类
        class InstanceLocalIn {
        // 局部内部类的实例方法
            public void inPrint() {
           
                System.out.println(age); // 直接访问外部类的private修饰的成员变量age
                System.out.println(x); // 直接访问外部类实例方法的形参x
                System.out.println(m);// 直接访问外部类实例方法的局部变量m
       }
    }
    // InstanceLocalIn类的实例对象是在InstanceLocalOut类的实例方法中创建的。
    //所以,在创建InstanceLocalIn局部内部类的实例对象之前,必先创建InstanceLocalOut外部类的实   例对象(外部类Print方法的隐藏形参this)。           
    InstanceLocalIn instanceLocalIn = new InstanceLocalIn();
    instanceLocalIn.inPrint();

}

看一下字节码的反编译文件

class InstanceLocalOut {
    private int age = 12;
    
    //编译器添加的静态方法,为了访问外部类的私有字段
    static int access$0(InstanceLocalOut arg0){
        return arg0.age;
    }
    
    public void Print(final int x) {
        final int m = 8;
     //原来内部类的位置 
    }
    InstanceLocalIn instanceLocalIn = new InstanceLocalIn(this,x,m);    //内部类构造方法参数改变,参数传入内部类
    instanceLocalIn.inPrint();

}
 class InstanceLocalIn {    //名字变为InstanceLocalOut$1InstanceLocalIn
 
        final InstanceLocalOut this$0//增加一个final字段用以链接到外部类
        private final int val$x;
        private final int val$m;        //因为内部类调用来了外部类方法的参数,故还需要两个来链接
        
        InstanceLocalIn(InstanceLocalOut this$0, int val$x, int val$m){
            this.this$0 = this$0;
            this.val$x = val$x;
            this.val$m = val$m;
        }   //对应外部类调用的构造方法
        
        public void inPrint() {
          
            System.out.println(InstanceLocalOut.access$0(this.this$0)); 
            System.out.println(this.val$x);
            System.out.println(this.val$m);     //调用外部类成员
       }
    } 
   

我们可以发现,局部内部类的原理和成员内部类原理相似,所以不再画出内存逻辑模型

2. 静态方法中的局部内部类
  • 特点和静态内部类相似
  • 内部类可以访问静态方法的形参,局部变量和外部类的静态成员

不再赘述

4. 匿名内部类

将局部内部类的使用再深入一步,假如只创建这个类的一个对象,就不必命名了。从使用上讲,匿名内部类和局部内部类的区别是一个是匿名的另一个是命名的,其它均相同。(匿名的含义是由编译器自动给内部类起一个内部名称)
在外部类的方法中,定义的非静态的没有类名的内部类,叫匿名内部类。匿名内部类适合只需要使用一次的类,当创建一个匿名内部类时会立即创建该类的一个实例对象,匿名类不能重复使用。可以分为:

  • 在外部类的实例方法内部的匿名内部类
  • 在外部类的静态方法内部的匿名内部类。
    但是尽量用lambda表达式代替匿名内部类
  • 匿名内部类的语法格式为 new classtype(parm){ ...}

比较一下普通写法,匿名内部类和lambda表达式:

//普通写法
public void start(){
    class sonClass implements superInterface{
        public void fxxk(){
            System.out.println("stonee is so cool");
        }
    }
    superInterface a = new sonClass();
    otherClass b = new otherClass( a );
}
//匿名内部类写法
public void start(){
    superInterface a = new superInterface(){
            public void fxxk(){
            System.out.println("stonee is so cool");
        }
    }
    otherClass b = new otherClass( a );
}
//lambda表达式写法
public void start(){
    otherClass b = new otherClass( a -> 
        {
            public void fxxk(){
            System.out.println("stonee is so cool");
        } );
}

在实际工程中,只有成员内部类和匿名内部类用的较多,其余的很少用。