美文网首页
java内部类

java内部类

作者: 奔向学霸的路上 | 来源:发表于2020-04-12 10:58 被阅读0次

    开篇问题

    • 为什么需要内部类?
    • 为什么内部类,会持有外部类的引用?
    • 为什么匿名内部类使用到外部类方法中的局部变量时需要是final类型的?
    • 如何创建内部类实例,如何继承内部类?
    • Lambda表达式是如何实现的?

    为什么需要内部类?

    首先需要弄明白什么是内部类,java有三种类型的内部类

    普通的内部类

    public class Test {
        int i = 1;
        class Inner{
            int j = 2;
        }
    
        public static void main(String[] args) {
            System.out.println(new Test().new Inner().j);
        }
    }
    

    静态内部类
    静态内部类使用场景一般是当外部类需要使用内部类,而内部类无需外部类资源,并且内部类可以单独创建的时候会考虑采用静态内部类的设计;

    也就是说:静态内部类我们不需要创建外部类来访问,可以直接访问它。

    public class Test {
        int i = 1;
        static class Inner{
            int j = 2;
        }
    
        public static void main(String[] args) {
            System.out.println(new Inner().j);
        }
    }
    

    匿名内部类
    在使用匿名内部类的过程中,我们需要注意如下几点:

    1. 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。
    2. 匿名内部类中是不能定义构造函数的。
    3. 匿名内部类中不能存在任何的静态成员变量和静态方法。
    4. 匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
    5. 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
    6. 匿名内部类使用的形参为final,因为匿名内部类并不是直接调用外部类传递的参数,而是利用自身的构造函数对外部参数进行备份,相当于拷贝引用,为了避免引用值发生改变,形参必须为final。

    语法格式为:

    new 父类构造器(参数列表)|实现接口()  
        {  
         //匿名内部类的类体部分  
        }
    
    public class Test {
        void show() {
            System.out.println("外部类");
        }
    }
    
    class Inner {
        void show() {
            Test inner = new Test() {
                @Override
                void show() {
                    System.out.println("匿名内部类");
                }
            };
            inner.show();
        }
    
        public static void main(String[] args) {
            Inner inner = new Inner();
            inner.show();
        }
    }
    ------------------
    Output:
    匿名内部类
    

    方法内局部内部类

    public class Test {
        public void work() {
            class InnerRunnable implements Runnable {
                @Override
                public void run() {
    
                }
            }
            InnerRunnable runnable = new InnerRunnable();
            runnable.run();
        }
    
        public static void main(String[] args) {
            Test test = new Test();
            test.work();
        }
    }
    

    这三种形式的内部类,大家肯定都用过,但是技术在设计之初肯定也是要用来解决某个问题或者某个痛点,那可以想想内部类相对比外部定义类有什么优势呢? 我们通过一个小例子来做说明

    public class Worker {
        private List<Job> mJobList = new ArrayList<>();
    
        public void addJob(Runnable task) {
            mJobList.add(new Job(task));
        }
    
        private class Job implements Runnable {
            Runnable task;
            public  Job(Runnable task) {
                this.task = task;
            }
    
            @Override
            public void run() {
                runnable.run();
                System.out.println("left job size : " + mJobList.size());
            }
        }
    }
    

    定义了一个Worker类,暴露了一个addJob方法,一个参数task,类型是Runnable,然后定义 了一个内部类Job类对task进行了一层封装,这里Job是私有的,所以外界是感知不到Job的存在的,所以有了内部类第一个优势。

    • 内部类能够更好的封装,内聚,屏蔽细节
      我们在Job的run方法中,打印了外部Worker的mJobList列表中剩余Job数量,代码这样写没问题,但是细想,内部类是如何拿到外部类的成员变量的呢?这里先卖个关子,但是已经可以先得出内部类的第二个优势了。
    • 内部类天然有访问外部类成员变量的能力
      内部类主要就是上面的二个优势。当然还有一些其他的小优点,比如可以用来实现多重继承,可以将逻辑内聚在一个类方便维护等,这些见仁见智,先不去说它们。

    为什么内部类,会持有外部类的引用?

    普通内部类的实现

    public class Demo {
        // 普通内部类
        public class DemoRunnable implements Runnable {
            @Override
            public void run() {
            }
        }
    }
    

    切到Demo.java所在文件夹,命令行执行 javac Demo.java,在Demo类同目录下可以看到生成了二个class文件

    image.png

    Demo.class很好理解,另一个 类Demo$DemoRunnable.class
    就是我们的内部类编译出来的,它的命名也是有规律的,外部类名Demo+$+内部类名DemoRunnable。 查看反编译后的代码(IntelliJ IDEA本身就支持,直接查看class文件即可)

    package inner;
    
    public class Demo$DemoRunnable implements Runnable {
        public Demo$DemoRunnable(Demo var1) {
            this.this$0 = var1;
        }
    
        public void run() {
        }
    }
    

    生成的类只有一个构造器,参数是Demo类型,而且保存到内部类本身的this$0字段中。到这里我们其实已经可以想到,内部类持有的外部类引用就是通过这个构造器传递进来的,它是一个强引用。

    验证我们的想法

    我们需要在Demo.class类中加入一个方法,来实例化DoemRunable内部类对象

       // Demo.java
        public void run() {
            DemoRunnable demoRunnable = new DemoRunnable();
            demoRunnable.run();
        }
    

    再次执行 javac Demo.java,再执行javap -verbose Demo.class,查看Demo类的字节码,这里我们重点关注run方法

      public void run();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=2, args_size=1
             0: new           #2                  // class inner/Demo$DemoRunnable
             3: dup
             4: aload_0
             5: invokespecial #3                  // Method inner/Demo$DemoRunnable."<init>":(Linner/Demo;)V
             8: astore_1
             9: aload_1
            10: invokevirtual #4                  // Method inner/Demo$DemoRunnable.run:()V
            13: return
    
    • 调用new指令,新建一个Demo$DemoRunnable
    • aload_0指令将外部类Demo对象自身加载到栈帧中
    • 调用Demo$DemoRunnable类的init方法,注意这里将Demo对象作为了参数传递进来了

    到这一步其实已经很清楚了,就是将外部类对象自身作为参数传递给了内部类构造器,与我们上面的猜想一致。

    匿名内部类的实现

    public class Demo {
        // 匿名内部类
        private Runnable runnable = new Runnable() {
            @Override
            public void run() {
    
            }
        };
    }
    

    同样执行javac Demo.java,这次多生成了一个Demo$1.class,反编译查看代码

    class Demo$1 implements Runnable {
        Demo$1(Demo var1) {
            this.this$0 = var1;
        }
    
        public void run() {
        }
    }
    

    方法内局部内部类的实现

    public class Demo {
    
        public void work() {
            class InnerRunnable implements Runnable {
                @Override
                public void run() {
    
                }
            }
            InnerRunnable runnable = new InnerRunnable();
        }
    
    }
    

    同样执行javac Demo.java,这次多生成了一个Demo$DemoRunnable.class,反编译查看代码

    class Demo$1InnerRunnable implements Runnable {
        Demo$1InnerRunnable(Demo var1) {
            this.this$0 = var1;
        }
    
        public void run() {
        }
    }
    

    可以看到三者内部类实现基本一致,只是匿名内部类编译器自动给它拼了个名字,所以匿名内部类不能自定义构造器,因为名字编译完成后才能确定。

    为什么匿名内部类使用到外部类方法中的局部变量时需要时final类型的?

    这里先申明一下,这个问题本身是有问题的,问题在哪呢?因为java8中并不一定需要声明为final。我们来看个例子

    // Demo.java
        public void run() {
            int age = 10;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    int myAge = age + 1;
                    System.out.println(myAge);
                }
            };
        }
    

    匿名内部类对象runnable,使用了外部类方法中的age局部变量。编译运行完全没问题,而age并没有final修饰啊! 那我们再在run方法中,尝试修改age试试

     public void run() {
            int age = 10;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    int myAge = age + 1;
                    System.out.println(myAge);
                    age = 20;   // error
                }
            };
        }
    

    编译器报错了,提示信息是”age is access from inner class, need to be final or effectively final“。很显然编译器很智能,由于我们第一个例子并没有修改age的值,所以编译器认为这是effectively final,是安全的,可以编译通过,而第二个例子尝试修改age的值,编译器立马就报错了。

    外部类变量是怎么传递给内部类的?

    这里对于变量的类型分三种情况来说明

    非final局部变量

    我们去掉尝试修改age的代码,然后执行javac Demo.java,查看Demo$1.class的实现代码

    class Demo$1 implements Runnable {
        Demo$1(Demo var1, int var2) {
            this.this$0 = var1;
            this.val$age = var2;
        }
    
        public void run() {
            int var1 = this.val$age + 1;
            System.out.println(var1);
        }
    }
    

    可以看到对于非final局部变量,是通过构造器的方式传递进来的。

    final局部变量

    age修改为final

     public void run() {
            final int age = 10;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    int myAge = age + 1;
                    System.out.println(myAge);
                }
            };
        }
    

    同样执行javac Demo.java,查看Demo$1.class的实现代码

    class Demo$1 implements Runnable {
        Demo$1(Demo var1) {
            this.this$0 = var1;
        }
    
        public void run() {
            byte var1 = 11;
            System.out.println(var1);
        }
    }
    

    可以看到编译器很聪明的做了优化,age是final的,所以在编译期间是确定的,直接将+1优化为11。 为了测试编译器的智商,我们把age的赋值修改一下,改为运行时才能确定的,看编译器如何应对。

    public void run() {
            final int age = (int) System.currentTimeMillis();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    int myAge = age + 1;
                    System.out.println(myAge);
                }
            };
        }
    

    再看Demo$1 字节码实现

    class Demo$1 implements Runnable {
        Demo$1(Demo var1, int var2) {
            this.this$0 = var1;
            this.val$age = var2;
        }
    
        public void run() {
            int var1 = this.val$age + 1;
            System.out.println(var1);
        }
    }
    

    编译器意识到编译期age的值不能确定,所以还是采用构造器传参的形式实现。现代编译器还是很机智的。

    外部类成员变量
    将age改为Demo的成员变量,注意没有加任何修饰符,是包级别访问。

    public class Demo {
        int age = 10;
        public void run() {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    int myAge = age + 1;
                    System.out.println(myAge);
                    age = 20;
                }
            };
        }
    }
    

    javac Demo.java,查看匿名内部内的实现

    class Demo$1 implements Runnable {
        Demo$1(Demo var1) {
            this.this$0 = var1;
        }
    
        public void run() {
            int var1 = this.this$0.age + 1;
            System.out.println(var1);
            this.this$0.age = 20;
        }
    }
    

    这一次编译器直接通过外部类的引用操作age,没毛病,由于age是包访问级别,所以这样是最高效的。

    如果将age改为private,编译器会在Demo类中生成二个方法,分别用于读取age和设置age。

    class Demo$1 implements Runnable {
        Demo$1(Demo var1) {
            this.this$0 = var1;
        }
    
        public void run() {
            int var1 = Demo.access$000(this.this$0) + 1;
            System.out.println(var1);
            Demo.access$002(this.this$0, 20);
        }
    }
    

    解答为什么局部变量传递给匿名内部类需要时final?

    通过上面的例子可以看到,不是一定需要局部变量是final的,但是你不能在匿名内部类中修改外部局部变量,因为Java对于匿名内部类传递变量的实现是基于构造器传参的,也就是说如果允许你在匿名内部类中修改值,你修改的是匿名内部类中的外部局部变量副本,最终并不会对外部类产生效果,因为已经是二个变量了。
    这样就会让程序员产生困扰,原以为修改会生效,事实上却并不会,所以Java就禁止在匿名内部类中修改外部局部变量。

    如何创建内部类实例,如何继承内部类?

    Demo.DemoRunnable demoRunnable = new Demo().new DemoRunnable();
    

    那如何继承一个内部类呢,先给出示例

     public class Demo2 extends Demo.DemoRunnable {
            public Demo2(Demo demo) {
                demo.super();
            }
    
            @Override
            public void run() {
                super.run();
            }
        }
    
    

    必须在构造器中传入一个Demo对象,并且还需要调用demo.super(); 看个例子

    public class DemoKata {
        public static void main(String[] args) {
            Demo2 demo2 = new DemoKata().new Demo2(new Demo());
        }
    
        public class Demo2 extends Demo.DemoRunnable {
            public Demo2(Demo demo) {
                demo.super();
            }
    
            @Override
            public void run() {
                super.run();
            }
        }
    }
    

    由于Demo2也是一个内部类,所以需要先new一个DemoKata对象。 这一个问题描述的场景可能用的并不多,一般也不这么去用,这里提一下,大家知道有这么回事就行。

    Lambda表达式是如何实现的?

    Java8引入了Lambda表达式,一定程度上可以简化我们的代码,但Lambda表达式并不能取代所有的匿名内部类,只能用来取代函数接口的简写。

    做技术的还是要有刨根问底的那股劲,问问自己有没有想过Java中Lambda到底是如何实现的呢?

    来看一个最简单的例子

    public class Animal {
        public void run(Runnable runnable) {
        }
    }
    

    Animal类中定义了一个run方法,参数是一个Runnable对象,Java8以前,我们可以传入一个匿名内部类对象

    run(new Runnable() {
                @Override
                public void run() {
                }
    });
    

    Java 8 之后编译器已经很智能的提示我们可以用Lambda表达式来替换。既然可以替换,那匿名内部类和Lambda表达式是不是底层实现是一样的呢,或者说Lambda表达式只是匿名内部类的语法糖呢?
    要解答这个问题,我们还是要去字节码中找线索。通过前面的知识,我们知道javac Animal.java命令将类编译成class,匿名内部类的方式会产生一个额外的类。那用Lambda表达式会不会也会编译新类呢?我们试一下便知。

    public void run(Runnable runnable) {
        }
    
        public void test() {
            run(() -> {});
        }
    

    javac Animal.java,发现并没有生成额外的类!!! 我们继续使用javap -verbose Animal.class来查看Animal.class的字节码实现,重点关注test方法

    public void test();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
             6: invokevirtual #3                  // Method run:(Ljava/lang/Runnable;)V
             9: return
    
    SourceFile: "Demo.java"
    InnerClasses:
         public static final #34= #33 of #37; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
    BootstrapMethods:
      0: #18 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
        Method arguments:
          #19 ()V
          #20 invokestatic com/company/inner/Demo.lambda$test$0:()V
          #19 ()V
    

    发现test方法字节码中多了一个invokedynamic #2 0指令,这是java7引入的新指令,其中#2 指向

    #2 = InvokeDynamic      #0:#21         // #0:run:()Ljava/lang/Runnable;
    

    而0代表BootstrapMethods方法表中的第一个,java/lang/invoke/LambdaMetafactory.metafactory方法被调用。

    BootstrapMethods:
      0: #18 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
        Method arguments:
          #19 ()V
          #20 invokestatic com/company/inner/Demo.lambda$test$0:()V
          #19 ()V
    
    

    这里面我们看到了com/company/inner/Demo.lambdatest0这么个东西,看起来跟我们的匿名内部类的名称有些类似,而且中间还有lambda,有可能就是我们要找的生成的类。
    我们不妨验证下我们的想法,可以通过下面的代码打印出Lambda对象的真实类名。

     public void run(Runnable runnable) {
            System.out.println(runnable.getClass().getCanonicalName());
        }
    
        public void test() {
            run(() -> {});
        }
    
    

    打印出runnable的类名,结果如下

    com.company.inner.Demo$$Lambda$1/764977973
    

    跟我们上面的猜测并不完全一致,我们继续找别的线索,既然我们有看到LambdaMetafactory.metafactory这个类被调用,不妨继续跟进看下它的实现

    public static CallSite metafactory(MethodHandles.Lookup caller,
                                           String invokedName,
                                           MethodType invokedType,
                                           MethodType samMethodType,
                                           MethodHandle implMethod,
                                           MethodType instantiatedMethodType)
                throws LambdaConversionException {
            AbstractValidatingLambdaMetafactory mf;
            mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                                 invokedName, samMethodType,
                                                 implMethod, instantiatedMethodType,
                                                 false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
            mf.validateMetafactoryArgs();
            return mf.buildCallSite();
        }
    
    

    内部new了一个InnerClassLambdaMetafactory对象。看名字很可疑,继续跟进

    public InnerClassLambdaMetafactory(...)
                throws LambdaConversionException {
            //....
            lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
            cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
           //....
        }
    

    省略了很多代码,我们重点看lambdaClassName这个字符串(通过名字就知道是干啥的),可以看到它的拼接结果跟我们上面打印的Lambda类名基本一致。而下面的ClassWriter也暴露了,其实Lambda运用的是Asm字节码技术,在运行时生成类文件。我感觉到这里就差不多了,再往下可能就有点太过细节了。-。-

    Lambda实现总结

    所以Lambda表达式并不是匿名内部类的语法糖,它是基于invokedynamic指令,在运行时使用ASM生成类文件来实现的。

    原文链接:[https://juejin.im/post/5d0821315188254c434686c8]

    相关文章

      网友评论

          本文标题:java内部类

          本文链接:https://www.haomeiwen.com/subject/cwxdmhtx.html