死磕Java内部类(一篇就够)

Java内部类,相信你们都用过,可是多数同窗可能对它了解的并不深刻,只是靠记忆来完成平常工做,却不能融会贯通,遇到奇葩问题更是难以有思路去解决。这篇文章带你们一块儿死磕Java内部类的方方面面。 友情提示:这篇文章的讨论基于JDK版本 1.8.0_191java

开篇问题

我一直以为技术是工具,是必定要落地的,要切实解决某些问题的,因此咱们经过先抛出问题,而后解决这些问题,在这个过程当中来加深理解,最容易有收获。 so,先抛出几个问题。(若是这些问题你早已思考过,答案也了然于胸,那恭喜你,这篇文章能够关掉了)。程序员

  • 为何须要内部类?
  • 为何内部类(包括匿名内部类、局部内部类),会持有外部类的引用?
  • 为何匿名内部类使用到外部类方法中的局部变量时须要是final类型的?
  • 如何建立内部类实例,如何继承内部类?
  • Lambda表达式是如何实现的?

为何须要内部类?

要回答这个问题,先要弄明白什么是内部类?咱们知道Java有三种类型的内部类安全

普通的内部类

public class Demo {

    // 普通内部类
    public class DemoRunnable implements Runnable {
        @Override
        public void run() {
        }
    }
}
复制代码

匿名内部类

public class Demo {

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

        }
    };
}
复制代码

方法内局部内部类

public class Demo {

    // 局部内部类
    public void work() {
        class InnerRunnable implements Runnable {
            @Override
            public void run() {

            }
        }
        InnerRunnable runnable = new InnerRunnable();
    }

}
复制代码

这三种形式的内部类,你们确定都用过,可是技术在设计之初确定也是要用来解决某个问题或者某个痛点,那能够想一想内部类相对比外部定义类有什么优点呢? 咱们经过一个小例子来作说明bash

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的存在的,因此有了内部类第一个优点。ide

  • 内部类可以更好的封装,内聚,屏蔽细节

咱们在Job的run方法中,打印了外部Worker的mJobList列表中剩余Job数量,代码这样写没问题,可是细想,内部类是如何拿到外部类的成员变量的呢?这里先卖个关子,可是已经能够先得出内部类的第二个优点了。工具

  • 内部类自然有访问外部类成员变量的能力

内部类主要就是上面的二个优点。固然还有一些其余的小优势,好比能够用来实现多重继承,能够将逻辑内聚在一个类方便维护等,这些见仁见智,先不去说它们。测试

咱们接着看第二个问题!!!优化

为何内部类(包括匿名内部类、局部内部类),会持有外部类的引用?

问这个问题,显得我是个杠精,您先别着急,其实我想问的是,内部类Java是怎么实现的。 咱们仍是举例说明,先以普通的内部类为例ui

普通内部类的实现

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

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

普通内部类生成class.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类中加一个方法,来实例化这个DemoRunnable内部类对象

// 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,反编译查看代码

package inner;

class Demo$1 implements Runnable {
    Demo$1(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的实现代码

package inner;

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,篇幅关系,这种状况留给你们自行测试。

解答为什么局部变量传递给匿名内部类须要是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表达式,必定程度上能够简化咱们的代码,使代码结构看起来更优雅。作技术的仍是要有刨根问底的那股劲,问问本身有没有想过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.lambda$test$0这么个东西,看起来跟咱们的匿名内部类的名称有些相似,并且中间还有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生成类文件来实现的。

写在最后

这多是我迄今写的最长的一篇技术文章了,写的过程当中也在不断的加深本身对知识点的理解,颠覆了不少以往的错误认知。写技术文章这条路我会一直坚持下去。 很是喜欢获得里面的一句slogan,胡适先生说的话。 怕什么真理无穷,进一寸有一寸的欢喜 共勉!

相关文章
相关标签/搜索