Java程式設計師必備基礎:內部類解析
前言
整理了一下內部類的相關知識,算是比較全,比較基礎的,希望大家一起學習進步。
一、什麼是內部類?
在Java中,可以將一個類的定義放在另外一個類的定義內部,這就是內部類。內部類本身就是類的一個屬性,與其他屬性 定義方式一致。
一個內部類的例子:
public class Outer { private int radius = 1; public static int count = 2; public Outer() { } class inner{ public void visitOuter() { System.out.println("visit outer private member variable:" + radius); System.out.println("visit outer static variable:" + count); } } }
二、內部類的種類
內部類可以分為四種:成員內部類、區域性內部類、匿名內部類和靜態內部類。
靜態內部類
定義在類內部的靜態類,就是靜態內部類。
public class Outer {
private static int radius = 1;
static class StaticInner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
}
}
}
靜態內部類可以訪問外部類所有的靜態變數,而不可訪問外部類的非靜態變數;靜態內部類的建立方式,new 外部類.靜態內部類()
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();
成員內部類
定義在類內部,成員位置上的非靜態類,就是成員內部類。
public class Outer { private static int radius = 1; private int count =2; class Inner { public void visit() { System.out.println("visit outer static variable:" + radius); System.out.println("visit outer variable:" + count); } } }
成員內部類可以訪問外部類所有的變數和方法,包括靜態和非靜態,私有和公有。成員內部類依賴於外部類的例項,它的建立方式外部類例項.new 內部類()
,如下:
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();
區域性內部類
定義在方法中的內部類,就是區域性內部類。
public class Outer {
private int out_a = 1;
private static int STATIC_b = 2;
public void testFunctionClass(){
int inner_c =3;
class Inner {
private void fun(){
System.out.println(out_a);
System.out.println(STATIC_b);
System.out.println(inner_c);
}
}
Inner inner = new Inner();
inner.fun();
}
public static void testStaticFunctionClass(){
int d =3;
class Inner {
private void fun(){
// System.out.println(out_a); 編譯錯誤,定義在靜態方法中的區域性類不可以訪問外部類的例項變數
System.out.println(STATIC_b);
System.out.println(d);
}
}
Inner inner = new Inner();
inner.fun();
}
}
定義在例項方法中的區域性類可以訪問外部類的所有變數和方法,定義在靜態方法中的區域性類只能訪問外部類的靜態變數和方法。區域性內部類的建立方式,在對應方法內,new 內部類()
,如下:
public static void testStaticFunctionClass(){
class Inner {
}
Inner inner = new Inner();
}
匿名內部類
匿名內部類就是沒有名字的內部類,日常開發中使用的比較多。
public class Outer {
private void test(final int i) {
new Service() {
public void method() {
for (int j = 0; j < i; j++) {
System.out.println("匿名內部類" );
}
}
}.method();
}
}
//匿名內部類必須繼承或實現一個已有的介面
interface Service{
void method();
}
除了沒有名字,匿名內部類還有以下特點:
- 匿名內部類必須繼承一個抽象類或者實現一個介面。
- 匿名內部類不能定義任何靜態成員和靜態方法。
- 當所在的方法的形參需要被匿名內部類使用時,必須宣告為 final。
- 匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的介面的所有抽象方法。
匿名內部類建立方式:
new 類/介面{
//匿名內部類實現部分
}
三、內部類的優點
我們為什麼要使用內部類呢?因為它有以下優點:
- 一個內部類物件可以訪問建立它的外部類物件的內容,包括私有資料!
- 內部類不為同一包的其他類所見,具有很好的封裝性;
- 內部類有效實現了“多重繼承”,優化 java 單繼承的缺陷。
- 匿名內部類可以很方便的定義回撥。
一個內部類物件可以訪問建立它的外部類物件的內容,包括私有資料!
public class Outer {
private int radius = 1;
protected void test(){
System.out.println("我是外部類方法");
}
class Inner {
public void visit() {
System.out.println("訪問外部類變數" + radius);
test();
}
}
}
我們可以看到,內部類Inner是可以訪問外部類Outer的私有變數radius或者方法test的。
內部類不為同一包的其他類所見,具有很好的封裝性
當內部類使用 private修飾時,這個類就對外隱藏了。當內部類實現某個介面,並且進行向上轉型,對外部來說,介面的實現已經隱藏起來了,很好體現了封裝性。
//提供的介面
interface IContent{
String getContents();
}
public class Outer {
//私有內部類遮蔽實現細節
private class PContents implements IContent{
@Override
public String getContents() {
System.out.println("獲取內部類內容");
return "內部類內容";
}
}
//對外提供方法
public IContent getIContent() {
return new PContents();
}
public static void main(String[] args) {
Outer outer=new Outer();
IContent a1=outer.getIContent();
a1.getContents();
}
}
我們可以發現,Outer外部類對外提供方法getIContent,用內部類實現細節,再用private修飾內部類,遮蔽起來,把Java的封裝性表現的淋漓盡致。
內部類有效實現了“多重繼承”,優化 java 單繼承的缺陷。
我們知道Java世界中,一個類只能有一個直接父類,即以單繼承方式存在。但是內部類讓“多繼承”成為可能:
- 一般來說,內部類繼承某個類或者實現某個介面,內部類的程式碼操作建立它的外圍類的物件。內部類提供了某種進入其外圍類的視窗。
- 每個內部類都可以隊裡的繼承自一個(介面的)實現,所以無論外圍類是否已經繼承了某個(介面的)實現,對於內部類沒有影響
- 介面解決了部分問題,一個類可以實現多個介面,內部類允許繼承多個非介面型別(類或抽象類)。
一份來自Java程式設計思想,內部類實現“多繼承”的溫暖如下:
class D {}
abstract class E{}
class Z extends D {
E makeE(){ return new E() {}; }
}
public class MultiImplementation {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args){
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
}
程式碼中出現了一個類D,一個抽象類E。然後,用類Z繼承D,內部類構造返回E。因此,當你不管要的是D還是E,Z都可以應付,“多繼承”的特點完美表現出來。
匿名內部類可以很方便的定義回撥。
什麼是回撥?假設有兩個類A和B,在A中呼叫B的一個方法b,而b在執行又呼叫了A的方法c,則c就稱為回撥函式。
當然,回撥函式也可以是a函式,這就是同步回撥,最簡單的回撥方式。
回撥應用場景挺多的,如android中的事件監聽器。匿名內部類可以很方便的定義回撥,看個例子
//定義一個CallBack介面
public interface CallBack {
void execute();
}
public class TimeTools {
/**
* 測試函式呼叫時長,通過定義CallBack介面的execute方法
* @param callBack
*/
public void testTime(CallBack callBack) {
long beginTime = System.currentTimeMillis(); //記錄起始時間
callBack.execute(); ///進行回撥操作
long endTime = System.currentTimeMillis(); //記錄結束時間
System.out.println("[use time]:" + (endTime - beginTime)); //列印使用時間
}
public static void main(String[] args) {
TimeTools tool = new TimeTools();
tool.testTime(new CallBack(){
//匿名內部類,定義execute方法
public void execute(){
TestTimeObject testTimeObject = new TestTimeObject();
testTimeObject.testMethod();
}
});
}
}
在呼叫testTime()測時間的時候,用匿名內部類實現一個方法execute(),在該方法內搞事情(執行目標函式),執行完後,又回到testTime方法,很好了實現測試函式呼叫時長的功能。顯然,匿名內部類讓回撥實現變得簡單。
四、內部類的底層
內部類標誌符
每個內部類都會產生一個.class檔案,其中包含了如何建立該型別的物件的全部資訊。內部類也必須生成一個.class檔案以包含它們的Class物件資訊。內部類檔案的命名有嚴格規則:外圍類的名字+$+內部類的名字。
一個簡單例子:
public class Outer {
class Inner{
}
}
javac Outer.java編譯完成後, 生成的class檔案如下:
如果內部類是匿名的,編譯器會簡單地產生一個數字作為其識別符號。如果內部類是巢狀在別的內部類之中(靜態內部類),只需直接將它們的名字加在其外圍類標誌符與“$”的後面。
為什麼內部類可以訪問外部類的成員,包括私有資料?
由上一小節,我們知道內部類可以訪問外部類的成員,包括私有資料。那麼它是怎麼做到的呢?接下來揭曉答案。
先看這個簡單地例子:
public class Outer {
private int i = 0;
class Inner{
void method(){
System.out.println(i);
}
}
}
一個外部類Outer,一個外部類私有屬性i,一個內部類Inner,一個內部類方法method。內部類方法訪問了外部類屬性i。
先編譯,javac Outer.java,生成.class檔案,如下:
javap -classpath . -v Outer$Inner
,反編譯Outter$Inner.class檔案得到以下資訊:
我們可以看到這一行,它是一個指向外部類物件的指標:
final innerclass.Outer this$0;
雖然編譯器在建立內部類時為它加上了一個指向外部類的引用, 但是這個引用是怎樣賦值的呢?編譯器會為內部類的構造方法新增一個引數,進行初始化, 引數的型別就是外部類的型別,如下:
innerclass.Outer$Inner(innerclass.Outer);
成員內部類中的Outter this&0 指標便指向了外部類物件,因此可以在成員內部類中隨意訪問外部類的成員。
區域性內部類和匿名內部類訪問區域性變數的時候,為什麼變數必須要加上final?
區域性內部類和匿名內部類訪問區域性變數的時候,為什麼變數必須要加上final呢?它內部原理是什麼呢?
先看這段程式碼:
public class Outer {
void outMethod(){
final int a =10;
class Inner {
void innerMethod(){
System.out.println(a);
}
}
}
}
反編譯(Outer$1Inner)得到以下資訊
我們在內部類innerMethod方法中,可以看到以下這條指令:
3: bipush 10
- 它表示將常量10壓入棧中,表示使用的是一個本地區域性變數。
- 其實,如果一個變數的值在編譯期間可以確定(demo中確定是10了),則編譯器會預設在匿名內部類(區域性內部類)的常量池中新增一個內容相等的字面量或直接將相應的位元組碼嵌入到執行位元組碼中。
- 醬紫可以確保區域性內部類使用的變數與外層的區域性變數區分開,它們只是值相等而已。
以上例子,為什麼要加final呢?是因為生命週期不一致, 區域性變數直接儲存在棧中,當方法執行結束後,非final的區域性變數就被銷燬。而區域性內部類對區域性變數的引用依然存在,如果區域性內部類要呼叫區域性變數時,就會出錯。加了final,可以確保區域性內部類使用的變數與外層的區域性變數區分開,解決了這個問題。
我們再來看一段程式碼,其實就是把變數a挪到傳參方式進來
public class Outer {
void outMethod(final int a){
class Inner {
void innerMethod(){
System.out.println(a);
}
}
}
}
反編譯可得
我們看到匿名內部類Outer$1Inner的構造器含有兩個引數,一個是指向外部類物件的引用,一個是int型變數,很顯然,這裡是將變數innerMethod方法中的形參a以引數的形式傳進來對匿名內部類中的拷貝(變數a的拷貝)進行賦值初始化。
那麼,新的問題又來了,既然在innerMethod方法中訪問的變數a和outMethod方法中的變數a不是同一個變數,當在innerMethod方法中修改a會怎樣?那就會造成資料不一致的問題了。
怎麼解決呢?使用final修飾符,final修飾的引用型別變數,不允許指向新的物件,這就解決資料不一致問題。注意: 在Java8 中,被區域性內部類引用的區域性變數,預設新增final,所以不需要新增final關鍵詞。
五、內部類的應用場景。
一般我們在哪些場景下使用內部類呢?
場景之一:一些多演算法場合
一些演算法多的場合,也可以藉助內部類,如:
Arrays.sort(emps,new Comparator(){
Public int compare(Object o1,Object o2)
{
return ((Employee)o1).getServedYears()-((Employee)o2).getServedYears();
}
});
場景二:解決一些非面向物件的語句塊。
如果一些語句塊,包括if…else語句,case語句等等比較多,不好維護擴充套件,那麼就可以藉助內部類+設計模式解決。
場景之三:適當使用內部類,使得程式碼更加靈活和富有擴充套件性。
適當的使用內部類,可以使得你的程式碼更加靈活和富有擴充套件性。如JDK的lamda表示式,用內部類非常多,程式碼優雅很多。如下
// JDK8 Lambda表示式寫法
new Thread(() -> System.out.println("Thread run()")).start();
場景四:當某個類除了它的外部類,不再被其他的類使用時。
如果一個類,不能為其他的類使用;或者出於某種原因,不能被其他類引用。那我們就可以考慮把它實現為內部類。資料庫連線池就是這樣一個典型例子。
六、內部類常見面試題
最後,我們來看一道經典內部類面試題吧。
public class Outer {
private int age = 12;
class Inner {
private int age = 13;
public void print() {
int age = 14;
System.out.println("區域性變數:" + age);
System.out.println("內部類變數:" + this.age);
System.out.println("外部類變數:" + Outer.this.age);
}
}
public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.print();
}
}
執行結果:
參考與感謝
- 《Java程式設計思想》
- Java中的內部類(回撥)
- Java進階 ——— 區域性內部類訪問區域性變數為什麼必須加final關鍵字
- Java內部類詳解
- 幕後英雄的用武之地——淺談Java內部類的四個應用場景
個人公眾號
- 如果你是個愛學習的好孩子,可以關注我公眾號,一起學習討論。
- 如果你覺得本文有哪些不正確的地方,可以評論,也可以關注我公眾號,私聊我,大家一起學習進步哈。