JAVA内部类

 

内部类是JAVA语言的主要附加部分。内部类几乎可以处于一个类内部任何位置,可以与实例变量处于同一级,或处于方法之内,甚至是一个表达式的一部分。因为很少使用,一直对内部类的理解不够深入(暴露了基础不扎实)。最近在阅读AQS源码,发现了大量内部类的使用,so,为了通顺的阅读,决定把内部类搞清楚。

 

第一节 什么是内部类

第二节 内部类的分类

       1. 成员内部类

       2. 局部内部类

                     为何局部变量传参必须是final的

                     绕开final传递局部变量

       3. 静态内部类

     4. 匿名内部类

 

第一节  什么是内部类

内部类是JAVA语言的主要附加部分。嵌套类从JDK1.1开始引入。把类定义在另一个类的内部,该类就被称为内部类。
如:
public class OuterClassDemo0 {
    class SimpleInnerClass0{
            
    }
}

SimpleInnerClass0 定义在 OuterClassDemo0 中,SimpleInnerClass0 便是一个内部类。

对于JAVA来说,每个类在被编译时都会生成一份 .class 文件,在加载入虚拟机后,都会生成一个 instanceKlass 对象作为虚拟机内类的数据结构。在这点上,内部类与外部类没有什么不同。就比如上述的  OuterClassDemo0 类,我们可以用 javac 编译一下:

 可以看到,生成了两份字节码文件 OuterClassDemo0  一份,SimpleInnerClass0 一份,但不同的是 SimpleInnerClass0 编译后的文件名为OuterClassDemo0$SimpleInnerClass0 ,代表了其余 OuterClassDemo0  的从属关系。

我们打开编译后的文件看一下:

 可以看到,jvm自动为其生成了无参的构造方法。

 内部类编译后生成了有参的构造方法,而传入的参数是外部类对象的引用。

也就是说,内部类在创建实例时必须传入一个外部类的实例,内部类对象与外部类的对象是紧紧绑定的。
内部类可以借此直接访问外部类对象的所有成员(包括private)。
而外部类想要访问内部类则必须创建内部类的对象(创建后也可以访问内部类的private成员)。

第二节 内部类的分类

内部类分为一下几类:

成员内部类、局部内部类、静态内部类、匿名内部类。

1.成员内部类

成员内部类即位于外部类成员位置的内部类。内部类可以访问外部类所有成员(包括private),外部类也可以访问内部类实例的所有成员(包括private)。具体用法如下:

public class OuterClassDemo0 {
    //外部类私有属性
    private String outerString = "i am outerString";

    public void printInnerStr0(){
        SimpleInnerClass0 inner=new SimpleInnerClass0();
        System.out.println("外部类访问内部类私有成员  :   "+inner.innerString);
    }

    //位于成员位置的内部类
    class SimpleInnerClass0 {
        //内部类私有属性
        private String innerString = "i am innerString0";

        public void printOuterStr() {
            //内部类访问外部类私有属性
            System.out.println("成员内部类访问外部类私有成员  :   "+outerString);
        }
    }

    public static void main(String[] args){
        //创建外部类对象
        OuterClassDemo0 outer=new OuterClassDemo0();

        //创建成员内部类对象
        OuterClassDemo0.SimpleInnerClass0 simpleInner=outer.new SimpleInnerClass0();
        outer.printInnerStr0();
        simpleInner.printOuterStr();
    }
}

成员内部类可以由 private、protected、static 修饰,这对于外部类来说是不被允许的。

如果我们的内部类不想轻易的被他人访问,可以将内部类定义为 private ,这样外部就不能沟通过创建对象的方式来访问内部类。我们可以仅将内部类提供给外部类使用,并在必要的时候由外部类提供获取内部类实例的方法。

举一个实际使用时的例子:我们定义一个 个人PC的 类,实现了  Computer 接口。对于计算机,肯定有自己的 CPU ,但 CPU 也有许多的分类,属于另外一条继承链,我们也有一个 CPU 接口。

我们希望,个人PC 在创建时便已经有了自己的CPU,所以CPU 不能由别人随意创建,只能打开电脑的后壳拿本电脑的出来操作。

给 PC 内置一个 CPU 对象显然比 PC 同时实现Computer与CPU接口更加富有语义,而如果直接在 PC 内部定制CPU(内部类)会使我们的PC更加实用。

我们则可以使用内部类来解决上述的多继承以及对CPU创建和使用权限控制的问题:

//Computer接口
public interface Computer {
    public void getCPU();
}
//CPU接口
public interface CPU {
    public void getVersion();
}
public class PC implements Computer {
    //本电脑拥有的cpu
    private CPU cpu;

    //构造方法,创建一台电脑时,该电脑便有了自己的cpu
    PC() {
        cpu = new IntelCPU();
    }

    public void getVersion(){
        System.out.println("PC's name is :  "+this.toString()+" , and CPU is  :   "+cpu.getVersion());
    }

    @Override
    public CPU getCPU() {
        return cpu;
    }

    //IntelCPU内部类,实现cpu接口,定制CPU;声明为private,除所属外部类外,其它类不能通过创建对象访问该类
    private class IntelCPU implements CPU {
@Override
public String getVersion() {
return "this is intel "+this.toString()+" cpu of v 1.0.0";
}
}
public static void main(String[] args){
        PC pc=new PC();
        pc.getVersion();
    }
}

还有一点需要注意的是,非static的内部类中不能有static块、方法、变量

静态变量是要占用内存的,在编译时只要是定义为静态变量了,系统就会自动分配内存给他,而内部类是在宿主类编译完编译的。
也就是说,必须有宿主类存在后才能有内部类,这也就和编译时就为静态变量分配内存产生了冲突。
因为系统执行:运行宿主类--->静态变量内存分配--->内部类,而此时内部类的静态变量先于内部类生成,这显然是不可能的,所以不能定义静态变量

 2.局部内部类

局部内部类是定义在一个方法或一个作用域里的内部类。

由于定义在方法或者作用域内,所以可以访问同作用域的局部变量,但需要注意的是,访问的局部变量必须用final修饰。同时我们也只能在这个作用域内访问这个内部类,即只能在这个作用域内创建该类的对象

public class OuterClassDemo1 {
    public void test() {
        Object o = new Object();
        int i = 1111;
        /**
         *   @Author Nxy
         *   @Date 2019/12/9 22:52
         *   @Description 局部内部类
         */
        class InnerClass {
            public void say() {
                System.out.println(o);
                System.out.println(i);
            }
        }
    }
}

上面是一个局部内部类的例子,但是 o 与 i 均没有被 final 修饰但却可以被局部内部类访问。

这是因为编译器自动为我们增加了 final 修饰符(1.8之后的功能),我们不必再显式的声明为 final。

所以将上面的代码复制下来的话,会发现用1.7及之前版本编译时会报错,而用1.8环境则不会。

以下是1.8环境,我们可以发现并没有报错:

 但是如果我们尝试修改 o 或者 i ,则会报错,因为它们被隐式修饰为了 final :

 为什么局部变量要用 final 修饰:

因为局部变量会随着退出作用域而失效,比如方法体中的局部变量,一旦方法执行完毕,栈帧释放,那么局部变量就被清除了。

但此时,堆中的数据(内部类对象)却不会立即消失。所以此时堆中还在使用着该变量,该变量却已经不存在了。

从程序设计语言的理论上:局部内部类(即:定义在方法中的内部类),由于本身就是在方法内部(可出现在形式参数定义处或者方法体处),因而访问方法中的局部变量(形式参数或局部变量)是天经地义的.是很自然的,但是实际上却很难实现,原因是编译技术是无法实现的或代价极高(局部变量的生命周期与局部内部类的对象的生命周期的不一致性)。为了使变量的生命周期迎合堆中引用它的对象的生命周期,我们需要将局部变量声明为final。当我们用final修饰变量时,堆中存储的是变量值而不是变量的引用(标灰存疑)

这点一直存疑,JAVA内是没有引用传递的,所有的传递都是值传递。即使我们传递的是对象的引用,也只是将引用的值传给了另一个引用而已。即:我们在内部类访问的,其实是局部变量的复制品而不是局部变量本身。final 关键字是在编译期产生作用的,不会影响运行期

而对于堆中存储的是变量值而不是引用这一点:static与final的区别在于static会改变变量的存储位置,声明为static的变量会存储在堆中。但是局部变量是不允许声明为static的,否则在编译期就会报错。也就是说对变量来说,static只能修饰类的成员变量,而成员变量的值本身就是存储在堆中的。static真正影响存储的地方是:非static成员变量存储在堆中,而static成员变量存储在堆的方法区中。

final在编译期确保了一个传入内部类的局部变量,无论是在内部类实例中还是在内部类外,其值都不会发生改变!

代码举证:

即使在内部类中尝试改变传入的局部变量值,也会在编译期报错。

 按值传递的代码举证,如果传递的是引用,判断结果应该为true:

    private static Object o0=new Object();

    private static void test(Object o){
        o=new Object();
        System.out.println(o==o0);
    }

    public static void main(String[] args){
        test(o0);
    }

如何绕开final传递局部变量

有的时候,我们可能需要向内部类传参,但又不想参数是final的(程序逻辑上不需要内部类对象与作用域中变量值的一致),我们可以通过一些别的方法来进行传参。比如下例,定义一个 init 方法接收参数:

    private  void test() {
        Object o = new Object();
        class OuterClassDemo2 extends OuterClassDemo1{
            Object oi;
            private OuterClassDemo2 init(Object object){
                this.oi=object;
                return this;
            }
        }
        OuterClassDemo2 demo2=new OuterClassDemo2().init(o);
    }

除上述方法外也可以将要传递的参数放入一个 final 数组,数组引用不可变但内容是可变的。也就是需要的参数除数组外都不必声明为final。

3.静态内部类

我们所知道static是不能用来修饰类的,但是成员内部类可以看做外部类中的一个成员,所以可以用static修饰,这种用static修饰的内部类我们称作静态内部类,也称作嵌套内部类。

声明为static的内部类,不需要内部类对象和外部类对象之间的联系,就是说我们可以直接引用outer.inner,即不需要创建外部类对象,也不需要创建内部类对象。 非静态内部类编译后会默认的保存一个指向外部类的引用,而静态类却没有。嵌套类和普通的内部类还有一个区别:普通内部类不能有static数据和static属性,也不能包含嵌套类,但嵌套类可以。而嵌套类不能声明为private,一般声明为public,方便调用。

单例模式就有通过静态内部类实现单例的方法,因为外部类的加载是不会引起内部类的加载的,只有在使用时内部类才可以被加载。借助这点可以实现懒人模式。

/**
*   @Author Nyr
*   @Date 2019/11/19 20:48
*   @Description 单例模式-静态内部类方式
*/
public class Car2 {
    private Car2(){}

    private  static class InnerCar2{
        private static Car2 car2=new Car2();
    }

    public static Car2 getCar2(){
        return InnerCar2.car2;
    }
}

4. 匿名内部类

 匿名内部类是一个没有名字的内部类,是内部类的简化写法。如果一个类在定义后只会被使用一次,那么就可以使用匿名内部类。

平时我们是无法new一个接口或者抽象类的,但是我们可以通过匿名内部类的方式new一个接口或抽象类,而实际是new了一个集成了接口或抽象类的匿名子类的对象

     public interface Computer {
        public CPU getCPU();
     } 
        new Computer(){
            @Override
            public CPU getCPU(){
                return null;
            }
        }.getCPU();

我们也可以通过多态为这个匿名对象加一个引用,方便调用:

        Computer u=new Computer(){
            @Override
            public CPU getCPU(){
                return null;
            }
        };

我们在开发的时候,会看到抽象类,或者接口作为参数。而这个时候,实际需要的是一个子类对象。如果该方法仅仅调用一次,我们就可以使用匿名内部类的格式简化。比如我们常用的Thread对象(这种写法不被推荐,应使用线程池,否则难以控制创建线程的数量):

        new Thread(){
            @Override
            public void run(){
                
            }
        }.start();

 

posted @ 2019-12-11 10:24  牛有肉  阅读(790)  评论(0编辑  收藏  举报