Java內部類,相信你們都用過,可是多數同窗可能對它瞭解的並不深刻,只是靠記憶來完成平常工做,卻不能融會貫通,遇到奇葩問題更是難以有思路去解決。這篇文章帶你們一塊兒死磕Java內部類的方方面面。 友情提示:這篇文章的討論基於JDK版本 1.8.0_191java
我一直以爲技術是工具,是必定要落地的,要切實解決某些問題的,因此咱們經過先拋出問題,而後解決這些問題,在這個過程當中來加深理解,最容易有收穫。 so,先拋出幾個問題。(若是這些問題你早已思考過,答案也瞭然於胸,那恭喜你,這篇文章能夠關掉了)。程序員
要回答這個問題,先要弄明白什麼是內部類?咱們知道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
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
複製代碼
到這一步其實已經很清楚了,就是將外部類對象自身做爲參數傳遞給了內部類構造器,與咱們上面的猜測一致。
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() {
}
}
複製代碼
能夠看到匿名內部類和普通內部類實現基本一致,只是編譯器自動給它拼了個名字,因此匿名內部類不能自定義構造器,由於名字編譯完成後才能肯定。 方法局部內部類,我這裏就不贅述了,原理都是同樣的,你們能夠自行試驗。 這樣咱們算是解答了第二個問題,來看第三個問題。
這裏先申明一下,這個問題自己是有問題的,問題在哪呢?由於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的值,編譯器立馬就報錯了。
這裏對於變量的類型分三種狀況分別來講明
咱們去掉嘗試修改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局部變量,是經過構造器的方式傳遞進來的。
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的,可是你不能在匿名內部類中修改外部局部變量,由於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對象。 這一個問題描述的場景可能用的並很少,通常也不這麼去用,這裏提一下,你們知道有這麼回事就行。
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表達式並非匿名內部類的語法糖,它是基於invokedynamic指令,在運行時使用ASM生成類文件來實現的。
這多是我迄今寫的最長的一篇技術文章了,寫的過程當中也在不斷的加深本身對知識點的理解,顛覆了不少以往的錯誤認知。寫技術文章這條路我會一直堅持下去。 很是喜歡獲得裏面的一句slogan,胡適先生說的話。 怕什麼真理無窮,進一寸有一寸的歡喜 共勉!