zoukankan      html  css  js  c++  java
  • 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();
  • 相关阅读:
    Web安全测试
    性能测试---并发用户计算
    浅谈软件性能测试中关键指标的监控与分析
    fiddler抓包——IOS
    Linux下查看CPU型号,内存大小,硬盘空间,进程等的命令(详解)
    windows 升级pip
    java 使用Iterator 迭代器遍历AList、Set、Map
    springboot 封装redis工具类
    idea 取消@Autowired 不建议字段注入的警告
    查看服务器相关
  • 原文地址:https://www.cnblogs.com/niuyourou/p/12013905.html
Copyright © 2011-2022 走看看