您好,登錄后才能下訂單哦!
前言
說(shuō)起內(nèi)部類(lèi),大家并不陌生,并且會(huì)經(jīng)常在實(shí)例化容器的時(shí)候使用到它。但是內(nèi)部類(lèi)的具體細(xì)節(jié)語(yǔ)法,原理以及實(shí)現(xiàn)是什么樣的可以不少人都還挺陌生,這里作一篇總結(jié),希望通過(guò)這篇總結(jié)提高對(duì)內(nèi)部類(lèi)的認(rèn)識(shí)。
內(nèi)部類(lèi)是什么?
由文章開(kāi)頭可知,內(nèi)部類(lèi)的定義為:定義在另一個(gè)類(lèi)或方法中的類(lèi)。而根據(jù)使用場(chǎng)景的不同,內(nèi)部類(lèi)還可以分為四種:成員內(nèi)部類(lèi),局部?jī)?nèi)部類(lèi),匿名內(nèi)部類(lèi)和靜態(tài)內(nèi)部類(lèi)。每一種的特性和注意事項(xiàng)都不同,下面我們一一說(shuō)明。
成員內(nèi)部類(lèi)
顧名思義,成員內(nèi)部類(lèi)是定義在類(lèi)內(nèi)部,作為類(lèi)的成員的類(lèi)。如下:
public class Outer { public class Inner{ } }
特點(diǎn)如下:
Outer outer=new Outer(); Outer.Inner inner=outer.new Inner();
局部?jī)?nèi)部類(lèi)
局部?jī)?nèi)部類(lèi)是定義在方法或者作用域中類(lèi),它和成員內(nèi)部類(lèi)的區(qū)別僅在于訪問(wèn)權(quán)限的不同。
public class Outer{ public void test(){ class Inner{ } } }
特點(diǎn)如下:
在JDK1.8 以后,沒(méi)有final修飾,effectively final的即可。什么意思呢?就是沒(méi)有final修飾,但是如果加上final編譯器也不會(huì)報(bào)錯(cuò)即可。
匿名內(nèi)部類(lèi)
匿名內(nèi)部類(lèi)是與繼承合并在一起的沒(méi)有名字的內(nèi)部類(lèi)
public class Outer{ public List<String> list=new ArrayList<String>(){ { add("test"); } }; }
這是我們平時(shí)最常用的語(yǔ)法。
匿名內(nèi)部類(lèi)的特點(diǎn)如下:
嵌套類(lèi)
嵌套類(lèi)是用static修飾的成員內(nèi)部類(lèi)
public class Outer { public static class Inner{ } }
特點(diǎn)如下:
構(gòu)造函數(shù)可以看作靜態(tài)方法,因此可以訪問(wèn)。
為什么要有內(nèi)部類(lèi)?
從上面可以看出,內(nèi)部類(lèi)的特性和類(lèi)方差不多,但是內(nèi)部類(lèi)有許多繁瑣的細(xì)節(jié)語(yǔ)法。既然內(nèi)部類(lèi)有這么多的細(xì)節(jié)要注意,那為什么Java還要支持內(nèi)部類(lèi)呢?
1. 完善多重繼承
1.在早期C++作為面向?qū)ο缶幊陶Z(yǔ)言的時(shí)候,最難處理的也就是多重繼承,多重繼承對(duì)于代碼耦合度,代碼使用人員的理解來(lái)說(shuō),并不怎么友好,并且還要比較出名的死亡菱形的多重繼承問(wèn)題。因此Java并不支持多繼承。
2.后來(lái),Java設(shè)計(jì)者發(fā)現(xiàn),沒(méi)有多繼承,一些代碼友好的設(shè)計(jì)與編程問(wèn)題變得十分難以解決。于是便產(chǎn)生了內(nèi)部類(lèi)。內(nèi)部類(lèi)具有:隱式包含外部類(lèi)對(duì)象并且能夠與之通信的特點(diǎn),完美的解決了多重繼承的問(wèn)題。
2. 解決多次實(shí)現(xiàn)/繼承問(wèn)題
1.有時(shí)候在一個(gè)類(lèi)中,需要多次通過(guò)不同的方式實(shí)現(xiàn)同一個(gè)接口,如果沒(méi)有內(nèi)部類(lèi),必須多次定義不同數(shù)量的類(lèi),但是使用內(nèi)部類(lèi)可以很好的解決這個(gè)問(wèn)題,每個(gè)內(nèi)部類(lèi)都可以實(shí)現(xiàn)同一個(gè)接口,即實(shí)現(xiàn)了代碼的封裝,又實(shí)現(xiàn)了同一接口不同的實(shí)現(xiàn)。
2.內(nèi)部類(lèi)可以將組合的實(shí)現(xiàn)封裝在內(nèi)部中。
為什么內(nèi)部類(lèi)的語(yǔ)法這么繁雜
這一點(diǎn)是本文的重點(diǎn)。內(nèi)部類(lèi)語(yǔ)法之所以這么繁雜,是因?yàn)樗切聰?shù)據(jù)類(lèi)型加語(yǔ)法糖的結(jié)合。想要理解內(nèi)部類(lèi),還得從本質(zhì)上出發(fā).
內(nèi)部類(lèi)根據(jù)應(yīng)用場(chǎng)景的不同分為4種。其應(yīng)用場(chǎng)景完全可以和類(lèi)方法對(duì)比起來(lái)。
下面我們通過(guò)類(lèi)方法對(duì)比的模式一一解答為什么內(nèi)部類(lèi)會(huì)有這樣的特點(diǎn)
成員內(nèi)部類(lèi)——>成員方法
成員內(nèi)部類(lèi)的設(shè)計(jì)完全和成員方法一樣。
調(diào)用成員方法:outer.getName()
新建內(nèi)部類(lèi)對(duì)象:outer.new Inner()
它們都是要依賴(lài)對(duì)象而被調(diào)用。
正如《Thinking in Java》所說(shuō),outer.getName()正真的形似是Outer.getName(outer),也就是將調(diào)用對(duì)象作為參數(shù)傳遞給方法。
新建一個(gè)內(nèi)部類(lèi)也是這樣:Outer.new Inner(outer)
下面,我們用實(shí)際情況證明:
新建一個(gè)包含內(nèi)部類(lèi)的類(lèi):
public class Outer { private int m = 1; public class Inner { private void test() { //訪問(wèn)外部類(lèi)private成員 System.out.println(m); } } }
編譯,會(huì)發(fā)現(xiàn)會(huì)在編譯目標(biāo)目錄生成兩個(gè).class文件:Outer.class和Outer$Inner.class。
PS:不知道為什么Java總是和過(guò)不去,就連變量命名規(guī)則都要比C++多一個(gè)能由組成 :)
將Outer$Inner.class放入IDEA中打開(kāi),會(huì)自動(dòng)反編譯,查看結(jié)果:
public class Outer$Inner { public Outer$Inner(Outer this$0) { this.this$0 = this$0; } private void test() { System.out.println(Outer.access$000(this.this$0)); } }
可以看見(jiàn),編譯器已經(jīng)自動(dòng)生成了一個(gè)默認(rèn)構(gòu)造器,這個(gè)默認(rèn)構(gòu)造器是一個(gè)帶有外部類(lèi)型引用的參數(shù)構(gòu)造器。
可以看到外部類(lèi)成員對(duì)象的引用:Outer是由final修飾的。
因此:
但是可以定義static final變量,這并不沖突,因?yàn)樗x的final字段必須是編譯時(shí)確定的,而且在編譯類(lèi)時(shí)會(huì)將對(duì)應(yīng)的變量替換為具體的值,所以在JVM看來(lái),并沒(méi)有訪問(wèn)內(nèi)部類(lèi)。
局部?jī)?nèi)部類(lèi)——> 局部代碼塊
局部?jī)?nèi)部類(lèi)可以和局部代碼塊相理解。它最大的特點(diǎn)就是只能訪問(wèn)外部的final變量。
先別著急問(wèn)為什么。
定義一個(gè)局部?jī)?nèi)部類(lèi):
public class Outer { private void test() { int m= 3; class Inner { private void print() { System.out.println(m); } } } }
編譯,發(fā)現(xiàn)生成兩個(gè).class文件Outer.class和Outer$1Inner.class
將Outer$1Inner.class放入IDEA中反編譯:
class Outer$1Inner { Outer$1Inner(Outer this$0, int var2) { this.this$0 = this$0; this.val$m = var2; } private void print() { System.out.println(this.val$m); } }
可以看見(jiàn),編譯器自動(dòng)生成了帶有兩個(gè)參數(shù)的默認(rèn)構(gòu)造器。
看到這里,也許應(yīng)該能明了:我們將代碼轉(zhuǎn)換下:
public class Outer { private void test() { int m= 3; Inner inner=new Outer$1Inner(this,m); inner.print(); } } }
也就是在Inner中,其實(shí)是將m的值,拷貝到內(nèi)部類(lèi)中的。print()方法只是輸出了m,如果我們寫(xiě)出了這樣的代碼:
private void test() { int m= 3; class Inner { private void print() { m=4; } } System.out.println(m); }
在我們看來(lái),m的值應(yīng)該被修改為4,但是它真正的效果是:
private void test(){ int m = 3; print(m); System.out.println(m); } private void print(int m){ m=4; }
m被作為參數(shù)拷貝進(jìn)了方法中。因此修改它的值其實(shí)沒(méi)有任何效果,所以為了不讓程序員隨意修改m而卻沒(méi)達(dá)到任何效果而迷惑,m必須被final修飾。
繞了這么大一圈,為什么編譯器要生成這樣的效果呢?
其實(shí),了解閉包的概念的人應(yīng)該都知道原因。而Java中各種詭異的語(yǔ)法一般都是由生命周期帶來(lái)的影響。上面的程序中,m是一個(gè)局部變量,它被定義在棧上,而new Outer$1Inner(this,m);所生成的對(duì)象,是定義在堆上的。如果不將m作為成員變量拷貝進(jìn)對(duì)象中,那么離開(kāi)m的作用域,Inner對(duì)象所指向的便是一個(gè)無(wú)效的地址。因此,編譯器會(huì)自動(dòng)將局部類(lèi)所使用的所有參數(shù)自動(dòng)生成成員。
為什么其他語(yǔ)言沒(méi)有這種現(xiàn)象呢?
這又回到了一個(gè)經(jīng)典的問(wèn)題上:Java是值傳遞還是引用傳遞。由于Java always pass-by-value,對(duì)于真正的引用,Java是無(wú)法傳遞過(guò)去的。而上面的問(wèn)題核心就在與m如果被改變了,那么其它的m的副本是無(wú)法感知到的。而其他語(yǔ)言都通過(guò)其他的途徑解決了這個(gè)問(wèn)題。
對(duì)于C++就是一個(gè)指針問(wèn)題。
理解了真正的原因,便也能知道什么時(shí)候需要final,什么時(shí)候不需要final了。
public class Outer { private void test() { class Inner { int m=3; private void print() { System.out.println(m);//作為參數(shù)傳遞,本身都已經(jīng) pass-by-value。不用final int c=m+1; //直接使用m,需要加final } } } }
而在Java 8 中,已經(jīng)放寬政策,允許是effectively final的變量,實(shí)際上,就是編譯器在編譯的過(guò)程中,幫你加上final而已。而你應(yīng)該保證允許編譯器加上final后,程序不報(bào)錯(cuò)。
局部?jī)?nèi)部類(lèi)還有個(gè)特點(diǎn)就是不能有權(quán)限修飾符。就好像局部變量不能有訪問(wèn)修飾符一樣
由上面可以看到,外部對(duì)象同樣是被傳入局部類(lèi)中,因此局部類(lèi)可以訪問(wèn)外部對(duì)象
嵌套類(lèi)——>靜態(tài)方法
嵌套類(lèi)沒(méi)什么好說(shuō)的,就好像靜態(tài)方法一樣,他可以被直接訪問(wèn),他也能定義靜態(tài)變量。同時(shí)不能訪問(wèn)非靜態(tài)成員。
值得注意的是《Think in Java》中說(shuō)過(guò),可以將構(gòu)造函數(shù)看作為靜態(tài)方法,因此嵌套類(lèi)可以訪問(wèn)外部類(lèi)的構(gòu)造方法。
匿名類(lèi)——>局部方法+繼承的語(yǔ)法糖
匿名類(lèi)可以看作是對(duì)前3種類(lèi)的再次擴(kuò)展。具體來(lái)說(shuō)匿名類(lèi)根據(jù)應(yīng)用場(chǎng)景可以看作:
匿名類(lèi)語(yǔ)法為:
new 繼承類(lèi)名(){ //Override 重載的方法 }
返回的結(jié)果會(huì)向上轉(zhuǎn)型為繼承類(lèi)。
聲明一個(gè)匿名類(lèi):
public class Outer { private List<String> list=new ArrayList<String>(){ { add("test"); } }; }
這便是一個(gè)經(jīng)典的匿名類(lèi)用法。
同樣編譯上面代碼會(huì)看到生成了兩個(gè).class文件Outer.class,Outer$1.class
將Outer$1.class放入IDEA中反編譯:
class Outer$1 extends ArrayList<String> { Outer$1(Outer this$0) { this.this$0 = this$0; this.add("1"); } }
可以看到匿名類(lèi)的完整語(yǔ)法便是繼承+內(nèi)部類(lèi)。
由于匿名類(lèi)可以申明為成員變量,局部變量,靜態(tài)成員變量,因此它的組合便是幾種內(nèi)部類(lèi)加繼承的語(yǔ)法糖,這里不一一證明。
在這里值得注意的是匿名類(lèi)由于沒(méi)有類(lèi)名,因此不能通過(guò)語(yǔ)法糖像正常的類(lèi)一樣聲明構(gòu)造函數(shù),但是編譯器可以識(shí)別{},并在編譯的時(shí)候?qū)⒋a放入構(gòu)造函數(shù)中。
{}可以有多個(gè),會(huì)在生成的構(gòu)造函數(shù)中按順序執(zhí)行。
怎么正確的使用內(nèi)部類(lèi)
在第二小節(jié)中,我們已經(jīng)討論過(guò)內(nèi)部類(lèi)的應(yīng)用場(chǎng)景,但是如何優(yōu)雅,并在正確的應(yīng)用場(chǎng)景使用它呢?本小節(jié)將會(huì)詳細(xì)討論。
1.注意內(nèi)存泄露
《Effective Java》第二十四小節(jié)明確提出過(guò)。優(yōu)先使用靜態(tài)內(nèi)部類(lèi)。這是為什么呢?
由上面的分析我們可以知道,除了嵌套類(lèi),其他的內(nèi)部類(lèi)都隱式包含了外部類(lèi)對(duì)象。這便是Java內(nèi)存泄露的源頭。看代碼:
定義Outer:
public class Outer{ public List<String> getList(String item) { return new ArrayList<String>() { { add(item); } }; } }
使用Outer:
public class Test{ public static List<String> getOutersList(){ Outer outer=new Outer(); //do something List<String> list=outer.getList("test"); return list; } public static void main(String[] args){ List<String> list=getOutersList(); //do something with list } }
相信這樣的代碼一定有同學(xué)寫(xiě)出來(lái),這涉及到一個(gè)習(xí)慣的問(wèn)題:
不涉及到類(lèi)成員方法和成員變量的方法,最好定義為static
我們先研究上面的代碼,最大的問(wèn)題便是帶來(lái)的內(nèi)存泄露:
在使用過(guò)程中,我們定義Outer對(duì)象完成一系列的動(dòng)作
正常來(lái)說(shuō),在getOutersList方法中,我們new出來(lái)了兩個(gè)對(duì)象:outer和list,而在離開(kāi)此方法時(shí),我們只將list對(duì)象的引用傳遞出去,outer的引用隨著方法棧的退出而被銷(xiāo)毀。按道理來(lái)說(shuō),outer對(duì)象此時(shí)應(yīng)該沒(méi)有作用了,也應(yīng)該在下一次內(nèi)存回收中被銷(xiāo)毀。
然而,事實(shí)并不是這樣。按上面所說(shuō)的,新建的list對(duì)象是默認(rèn)包含對(duì)outer對(duì)象的引用的,因此只要list不被銷(xiāo)毀,outer對(duì)象將會(huì)一直存在,然而我們并不需要outer對(duì)象,這便是內(nèi)存泄露。
怎么避免這種情況呢?
很簡(jiǎn)單:不涉及到類(lèi)成員方法和成員變量的方法,最好定義為static
public class Outer{ public static List<String> getList(String item) { return new ArrayList<String>() { { add(item); } }; } }
這樣定義出來(lái)的類(lèi)便是嵌套類(lèi)+繼承,并不包含對(duì)外部類(lèi)的引用。
2.應(yīng)用于只實(shí)現(xiàn)一個(gè)接口的實(shí)現(xiàn)類(lèi)
優(yōu)雅工廠方法模式
我們可以看到,在工廠方法模式中,每個(gè)實(shí)現(xiàn)都會(huì)需要實(shí)現(xiàn)一個(gè)Fractory來(lái)實(shí)現(xiàn)產(chǎn)生對(duì)象的接口,而這樣接口其實(shí)和原本的類(lèi)關(guān)聯(lián)性很大的,因此我們可以將Fractory定義在具體的類(lèi)中,作為內(nèi)部類(lèi)存在
簡(jiǎn)單的實(shí)現(xiàn)接口
new Thread(new Runnable() { @Override public void run() { System.out.println("test"); } } ).start(); }
盡量不要直接使用Thread,這里只做演示使用Java 8 的話建議使用lambda代替此類(lèi)應(yīng)用
同時(shí)實(shí)現(xiàn)多個(gè)接口
public class imple{ public static Eat getDogEat(){ return new EatDog(); } public static Eat getCatEat(){ return new EatCat(); } private static class EatDog implements Eat { @Override public void eat() { System.out.println("dog eat"); } } private static class EatCat implements Eat{ @Override public void eat() { System.out.println("cat eat"); } } }
3.優(yōu)雅的單例類(lèi)
public class Imple { public static Imple getInstance(){ return ImpleHolder.INSTANCE; } private static class ImpleHolder{ private static final Imple INSTANCE=new Imple(); } }
4.反序列化JSON接受的JavaBean
有時(shí)候需要反序列化嵌套JSON
{ "student":{ "name":"", "age":"" } }
類(lèi)似這種。我們可以直接定義嵌套類(lèi)進(jìn)行反序列化
public JsonStr{ private Student student; public static Student{ private String name; private String age; //getter & setter } //getter & setter }
但是注意,這里應(yīng)該使用嵌套類(lèi),因?yàn)槲覀儾恍枰屯獠款?lèi)進(jìn)行數(shù)據(jù)交換。
核心思想:
內(nèi)部類(lèi)還有很多用法,這里不一一列舉。
總結(jié)
內(nèi)部類(lèi)的理解可以按照方法來(lái)理解,但是內(nèi)部類(lèi)很多特性都必須剝開(kāi)語(yǔ)法糖和明白為什么需要這么做才能完全理解,明白內(nèi)部類(lèi)的所有特性才能更好使用內(nèi)部類(lèi),在內(nèi)部類(lèi)的使用過(guò)程中,一定記住:能使用嵌套類(lèi)就使用嵌套類(lèi),如果內(nèi)部類(lèi)需要和外部類(lèi)聯(lián)系,才使用內(nèi)部類(lèi)。最后不涉及到類(lèi)成員方法和成員變量的方法,最好定義為static可以防止內(nèi)部類(lèi)內(nèi)存泄露。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。