死磕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,胡適先生說的話。 怕什麼真理無窮,進一寸有一寸的歡喜 共勉!

相關文章
相關標籤/搜索