死磕Java內部類(一篇就夠)

Java內部類,相信大家都用過,但是多數同學可能對它了解的并不深入,只是靠記憶來完成日常工作,卻不能融會貫通,遇到奇葩問題更是難以有思路去解決。這篇文章帶大家一起死磕Java內部類的方方面面。 友情提示:這篇文章的討論基于JDK版本 1.8.0_191

開篇問題

我一直覺得技術是工具,是一定要落地的,要切實解決某些問題的,所以我們通過先拋出問題,然后解決這些問題,在這個過程中來加深理解,最容易有收獲。 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();
    }

}
復制代碼

這三種形式的內部類,大家肯定都用過,但是技術在設計之初肯定也是要用來解決某個問題或者某個痛點,那可以想想內部類相對比外部定義類有什么優勢呢? 我們通過一個小例子來做說明

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數量,代碼這樣寫沒問題,但是細想,內部類是如何拿到外部類的成員變量的呢?這里先賣個關子,但是已經可以先得出內部類的第二個優勢了。

  • 內部類天然有訪問外部類成員變量的能力

內部類主要就是上面的二個優勢。當然還有一些其他的小優點,比如可以用來實現多重繼承,可以將邏輯內聚在一個類方便維護等,這些見仁見智,先不去說它們。

我們接著看第二個問題!!!

為什么內部類(包括匿名內部類、局部內部類),會持有外部類的引用?

問這個問題,顯得我是個杠精,您先別著急,其實我想問的是,內部類Java是怎么實現的。 我們還是舉例說明,先以普通的內部類為例

普通內部類的實現

public class Demo {
    // 普通內部類
    public class DemoRunnable implements Runnable {
        @Override
        public void run() {
        }
    }
}
復制代碼

切到Demo.java所在文件夾,命令行執行 javac Demo.java,在Demo類同目錄下可以看到生成了二個class文件

死磕Java內部類(一篇就夠)

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

原文 

https://juejin.im/post/5d0821315188254c434686c8

本站部分文章源于互聯網,本著傳播知識、有益學習和研究的目的進行的轉載,為網友免費提供。如有著作權人或出版方提出異議,本站將立即刪除。如果您對文章轉載有任何疑問請告之我們,以便我們及時糾正。

PS:推薦一個微信公眾號: askHarries 或者qq群:474807195,里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

轉載請注明原文出處:Harries Blog? » 死磕Java內部類(一篇就夠)

贊 (0)
分享到:更多 ()

評論 0

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
2013平特肖公式