電腦程式的思維邏輯 (21) - 內部類的本質

来源:http://www.cnblogs.com/swiftma/archive/2016/06/27/5619071.html
-Advertisement-
Play Games

為什麼要有內部類?都有哪些內部類?它們都適合用在什麼場景?內部類最後都會轉換為獨立的類,它們是如何轉換的?為什麼內部類可以訪問外部類的私有變數和方法?為什麼方法內部類可以訪問方法參數?但參數又為什麼必須要聲明為final? ... ...


內部類

之前我們所說的類都對應於一個獨立的Java源文件,但一個類還可以放在另一個類的內部,稱之為內部類,相對而言,包含它的類稱之為外部類。

為什麼要放到別的類內部呢?一般而言,內部類與包含它的外部類有比較密切的關係,而與其他類關係不大,定義在類內部,可以實現對外部完全隱藏,可以有更好的封裝性,代碼實現上也往往更為簡潔。

不過,內部類只是Java編譯器的概念,對於Java虛擬機而言,它是不知道內部類這回事的, 每個內部類最後都會被編譯為一個獨立的類,生成一個獨立的位元組碼文件。

也就是說,每個內部類其實都可以被替換為一個獨立的類。當然,這是單純就技術實現而言,內部類可以方便的訪問外部類的私有變數,可以聲明為private從而實現對外完全隱藏,相關代碼寫在一起,寫法也更為簡潔,這些都是內部類的好處。

在Java中,根據定義的位置和方式不同,主要有四種內部類:

  • 靜態內部類
  • 成員內部類
  • 方法內部類
  • 匿名內部類

方法內部類是在一個方法內定義和使用的,匿名內部類使用範圍更小,它們都不能在外部使用,成員內部類和靜態內部類可以被外部使用,不過它們都可以被聲明為private,這樣,外部就使用不了了。

接下來,我們逐個介紹這些內部類的語法、實現原理以及使用場景。

靜態內部類

語法

靜態內部類與靜態變數和靜態方法定義的位置一樣,也帶有static關鍵字,只是它定義的是類,示例代碼如下:

public class Outer {
    private static int shared = 100;
    
    public static class StaticInner {
        public void innerMethod(){
            System.out.println("inner " + shared);
        }
    }
    
    public void test(){
        StaticInner si = new StaticInner();
        si.innerMethod();
    }
}

外部類為Outer,靜態內部類為StaticInner,帶有static修飾符。語法上,靜態內部類除了位置放在別的類內部外,它與一個獨立的類差別不大,可以有靜態變數、靜態方法、成員方法、成員變數、構造方法等。

靜態內部類與外部類的聯繫也不大(與後面其他內部類相比)。它可以訪問外部類的靜態變數和方法,如innerMethod直接訪問shared變數,但不可以訪問實例變數和方法。在類內部,可以直接使用內部靜態類,如test()方法所示。

public靜態內部類可以被外部使用,只是需要通過"外部類.靜態內部類"的方式使用,如下所示:

Outer.StaticInner si = new Outer.StaticInner();
si.innerMethod();

實現原理

以上代碼實際上會生成兩個類,一個是Outer,另一個是Outer$StaticInner,它們的代碼大概如下所示:

public class Outer {
    private static int shared = 100;
    
    public void test(){
        Outer$StaticInner si = new Outer$StaticInner();
        si.innerMethod();
    }
    
    static int access$0(){
        return shared;
    }
}
public class Outer$StaticInner {
    public void innerMethod() {
        System.out.println("inner " + Outer.access$0());
    }
}

內部類訪問了外部類的一個私有靜態變數shared,而我們知道私有變數是不能被類外部訪問的,Java的解決方法是,自動為Outer生成了一個非私有訪問方法access$0,它返回這個私有靜態變數shared。

使用場景

靜態內部類使用場景是很多的,如果它與外部類關係密切,且不依賴於外部類實例,則可以考慮定義為靜態內部類。

比如說,一個類內部,如果既要計算最大值,也要計算最小值,可以在一次遍歷中將最大值和最小值都計算出來,但怎麼返回呢?可以定義一個類Pair,包括最大值和最小值,但Pair這個名字太普遍,而且它主要是類內部使用的,就可以定義為一個靜態內部類。

我們也可以看一些在Java API中使用靜態內部類的例子:

  • Integer類內部有一個私有靜態內部類IntegerCache,用於支持整數的自動裝箱。
  • 表示鏈表的LinkedList類內部有一個私有靜態內部類Node,表示鏈表中的每個節點。
  • Character類內部有一個public靜態內部類UnicodeBlock,用於表示一個Unicode block。

以上這些類我們在後續文章再介紹。

成員內部類

語法
成員內部類沒有static修飾符,少了一個static修飾符,但含義卻有很大不同,示例代碼如下:

public class Outer {
    private int a = 100;
    
    public class Inner {
        public void innerMethod(){
            System.out.println("outer a " +a);
            Outer.this.action();
        }
    }
    
    private void action(){
        System.out.println("action");
    }
    
    public void test(){
        Inner inner = new Inner();
        inner.innerMethod();
    }
}

Inner就是成員內部類,與靜態內部類不同,除了靜態變數和方法,成員內部類還可以直接訪問外部類的實例變數和方法,如innerMethod直接訪問外部類私有實例變數a。成員內部類還可以通過"外部類.this.xxx"的方式引用外部類的實例變數和方法,如Outer.this.action(),這種寫法一般在重名的情況下使用,沒有重名的話,"外部類.this."是多餘的。

在外部類內,使用成員內部類與靜態內部類是一樣的,直接使用即可,如test()方法所示。與靜態內部類不同,成員內部類對象總是與一個外部類對象相連的,在外部使用時,它不能直接通過new Outer.Inner()的方式創建對象,而是要先將創建一個Outer類對象,代碼如下所示:

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.innerMethod();

創建內部類對象的語法是"外部類對象.new 內部類()",如outer.new Inner()。

與靜態內部類不同,成員內部類中不可以定義靜態變數和方法 (final變數例外,它等同於常量),下麵介紹的方法內部類和匿名內部類也都不可以。Java為什麼要有這個規定呢?具體原因不得而知,個人認為這個規定不是必須的,Java這個規定大概是因為這些內部類是與外部實例相連的,不應獨立使用,而靜態變數和方法作為類型的屬性和方法,一般是獨立使用的,在內部類中意義不大吧,而如果內部類確實需要靜態變數和方法,也可以挪到外部類中。

實現原理

以上代碼也會生成兩個類,一個是Outer,另一個是Outer$Inner,它們的代碼大概如下所示:

public class Outer {
    private int a = 100;

    private void action() {
        System.out.println("action");
    }

    public void test() {
        Outer$Inner inner = new Outer$Inner(this);
        inner.innerMethod();
    }

    static int access$0(Outer outer) {
        return outer.a;
    }

    static void access$1(Outer outer) {
        outer.action();
    }
}
public class Outer$Inner {

    final Outer outer;
    
    public Outer$Inner(Outer outer){
        ths.outer = outer;
    }
    
    public void innerMethod() {
        System.out.println("outer a "
                + Outer.access$0(outer));
        Outer.access$1(outer);
    }
}

Outer$Inner類有個實例變數outer指向外部類的對象,它在構造方法中被初始化,Outer在新建Outer$Inner對象時傳遞當前對象給它,由於內部類訪問了外部類的私有變數和方法,外部類Outer生成了兩個非私有靜態方法,access$0用於訪問變數a,access$1用於訪問方法action。
使用場景
如果內部類與外部類關係密切,且操作或依賴外部類實例變數和方法,則可以考慮定義為成員內部類。

外部類的一些方法的返回值可能是某個介面,為了返回這個介面,外部類方法可能使用內部類實現這個介面,這個內部類可以被設為private,對外完全隱藏。

比如說,在Java API 類LinkedList中,它的兩個方法listIterator和descendingIterator的返回值都是介面Iterator,調用者可以通過Iterator介面對鏈表遍歷,listIterator和descendingIterator內部分別使用了成員內部類ListItr和DescendingIterator,這兩個內部類都實現了介面Iterator。關於LinkedList,後續文章我們還會介紹。

方法內部類

語法

內部類還可以定義在一個方法體中,示例代碼如下所示:

public class Outer {
    private int a = 100;
    
    public void test(final int param){
        final String str = "hello";
        class Inner {
            public void innerMethod(){
                System.out.println("outer a " +a);
                System.out.println("param " +param);
                System.out.println("local var " +str);
            }
        }
        Inner inner = new Inner();
        inner.innerMethod();
    }
}

類Inner定義在外部類方法test中,方法內部類只能在定義的方法內被使用。如果方法是實例方法,則除了靜態變數和方法,內部類還可以直接訪問外部類的實例變數和方法,如innerMethod直接訪問了外部私有實例變數a。如果方法是靜態方法,則方法內部類只能訪問外部類的靜態變數和方法。

方法內部類還可以直接訪問方法的參數和方法中的局部變數,不過,這些變數必須被聲明為final,如innerMethod直接訪問了方法參數param和局部變數str。

實現原理

系統生成的兩個類代碼大概如下所示:

public class Outer {
    private int a = 100;

    public void test(final int param) {
        final String str = "hello";
        OuterInner inner = new OuterInner(this, param);
        inner.innerMethod();
    }
    
    static int access$0(Outer outer){
        return outer.a;
    }
}
public class OuterInner {
    Outer outer;
    int param;
    
    OuterInner(Outer outer, int param){
        this.outer = outer;
        this.param = param;
    }
    
    public void innerMethod() {
        System.out.println("outer a "
                + Outer.access$0(this.outer));
        System.out.println("param " + param);
        System.out.println("local var " + "hello");
    }
}

與成員內部類類似,OuterInner類也有一個實例變數outer指向外部對象,在構造方法中被初始化,對外部私有實例變數的訪問也是通過Outer添加的方法access$0來進行的。

方法內部類可以訪問方法中的參數和局部變數,這是通過在構造方法中傳遞參數來實現的,如OuterInner構造方法中有參數int param,在新建OuterInner對象時,Outer類將方法中的參數傳遞給了內部類,如OuterInner inner = new OuterInner(this, param);。在上面代碼中,String str並沒有被作為參數傳遞,這是因為它被定義為了常量,在生成的代碼中,可以直接使用它的值。

這也解釋了,為什麼方法內部類訪問外部方法中的參數和局部變數時,這些變數必須被聲明為final,因為實際上,方法內部類操作的並不是外部的變數,而是它自己的實例變數,只是這些變數的值和外部一樣,對這些變數賦值,並不會改變外部的值,為避免混淆,所以乾脆強制規定必須聲明為final。

如果的確需要修改外部的變數,可以將變數改為只含該變數的數組,修改數組中的值,如下所示:

public class Outer {
    public void test(){
        final String[] str = new String[]{"hello"};
        class Inner {
            public void innerMethod(){
                str[0] = "hello world";
            }
        }
        Inner inner = new Inner();
        inner.innerMethod();
        System.out.println(str[0]);
    }
}

str是一個只含一個元素的數組。

使用場景

方法內部類都可以用成員內部類代替,至於方法參數,也可以作為參數傳遞給成員內部類。不過,如果類只在某個方法內被使用,使用方法內部類,可以實現更好的封裝。

匿名內部類

語法

匿名內部類沒有名字,在創建對象的同時定義類,語法如下:

new 父類(參數列表) {
   //匿名內部類實現部分
}

或者

new 父介面() {
   //匿名內部類實現部分
}

匿名內部類是與new關聯的,在創建對象的時候定義類,new後面是父類或者父介面,然後是圓括弧(),裡面可以是傳遞給父類構造方法的參數,最後是大括弧{},裡面是類的定義。

看個具體的代碼:

public class Outer {
    public void test(final int x, final int y){
        Point p = new Point(2,3){                
                                               
            @Override                              
            public double distance() {             
                return distance(new Point(x,y));     
            }                                      
        };                                       
                                                 
        System.out.println(p.distance());        
    }
}

創建Point對象的時候,定義了一個匿名內部類,這個類的父類是Point,創建對象的時候,給父類構造方法傳遞了參數2和3,重寫了distance()方法,在方法中訪問了外部方法final參數x和y。

匿名內部類只能被使用一次,用來創建一個對象。它沒有名字,沒有構造方法,但可以根據參數列表,調用對應的父類構造方法。它可以定義實例變數和方法,可以有初始化代碼塊,初始化代碼塊可以起到構造方法的作用,只是構造方法可以有多個,而初始化代碼塊只能有一份。

因為沒有構造方法,它自己無法接受參數,如果必須要參數,則應該使用其他內部類。

與方法內部類一樣,匿名內部類也可以訪問外部類的所有變數和方法,可以訪問方法中的final參數和局部變數

實現原理

每個匿名內部類也都被生成為了一個獨立的類,只是類的名字以外部類加數字編號,沒有有意義的名字。上例中,產生了兩個類Outer和Outer$1,代碼大概如下所示:

public class Outer {
    public void test(final int x, final int y){
        Point p = new Outer$1(this,2,3,x,y);                                            
        System.out.println(p.distance());        
    }
}
public class Outer$1 extends Point {
    int x2;
    int y2;
    Outer outer;
    
    Outer$1(Outer outer, int x1, int y1, int x2, int y2){
        super(x1,y1);
        this.outer = outer;
        this.x2 = x2;
        this.y2 = y2;
    }
    
    @Override                              
    public double distance() {             
        return distance(new Point(this.x2,y2));     
    }   
}

與方法內部類類似,外部實例this,方法參數x和y都作為參數傳遞給了內部類構造方法。此外,new時的參數2和3也傳遞給了構造方法,內部類構造方法又將它們傳遞給了父類構造方法。

使用場景

匿名內部類能做的,方法內部類都能做。但如果對象只會創建一次,且不需要構造方法來接受參數,則可以使用匿名內部類,代碼書寫上更為簡潔。

在調用方法時,很多方法需要一個介面參數,比如說Arrays.sort方法,它可以接受一個數組,以及一個Comparator介面參數,Comparator有一個方法compare用於比較兩個對象。

比如說,我們要對一個字元串數組不區分大小寫排序,可以使用Arrays.sort方法,但需要傳遞一個實現了Comparator介面的對象,這時就可以使用匿名內部類,代碼如下所示:

public void sortIgnoreCase(String[] strs){
    Arrays.sort(strs, new Comparator<String>() {

        @Override
        public int compare(String o1, String o2) {
            return o1.compareToIgnoreCase(o2);
        }
    });
}

Comparator後面的<Stirng>與泛型有關,表示比較的對象是字元串類型,後續文章會講解泛型。

匿名內部類還經常用於事件處理程式中,用於響應某個事件,比如說一個Button,處理點擊事件的代碼可能類似如下:

Button bt = new Button();
bt.addActionListener(new ActionListener(){
    @Override
    public void actionPerformed(ActionEvent e) {
        //處理事件
    }
});

調用addActionListener將事件處理程式註冊到了Button對象bt中,當事件發生時,會調用actionPerformed方法,並傳遞事件詳情ActionEvent作為參數。

以上Arrays.sort和Button都是上節提到的一種針對介面編程的例子,另外,它們也都是一種回調的例子。所謂回調是相對於一般的正向調用而言,平時一般都是正向調用,但Arrays.sort中傳遞的Comparator對象,它的compare方法並不是在寫代碼的時候被調用的,而是在Arrays.sort的內部某個地方回過頭來調用的。Button中的傳遞的ActionListener對象,它的actionPerformed方法也一樣,是在事件發生的時候回過頭來調用的。

將程式分為保持不變的主體框架,和針對具體情況的可變邏輯,通過回調的方式進行協作,是電腦程式的一種常用實踐。匿名內部類是實現回調介面的一種簡便方式。

小結

本節,我們談了各種內部類的語法、實現原理、以及使用場景,內部類本質上都會被轉換為獨立的類,但一般而言,它們可以實現更好的封裝,代碼上也更為簡潔。

我們一直沒有討論一個重要的問題,類放在哪裡?類文件是如何組織的?本節中,自動生成的方法如access$0沒有可見性修飾符,那可見性是什麼?這些都與有關,讓我們下節來探討。

----------------

未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心寫作,原創文章,保留所有版權。

-----------

更多相關原創文章

電腦程式的思維邏輯 (20) - 為什麼要有抽象類?

電腦程式的思維邏輯 (19) - 介面的本質

電腦程式的思維邏輯 (18) - 為什麼說繼承是把雙刃劍

電腦程式的思維邏輯 (17) - 繼承實現的基本原理

電腦程式的思維邏輯 (16) - 繼承的細節

電腦程式的思維邏輯 (15) - 初識繼承和多態

電腦程式的思維邏輯 (14) - 類的組合

電腦程式的思維邏輯 (13) - 類


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 /// <summary> 2 /// ************************************************* 3 /// 類名:MP3幫助類 4 /// 修改日期:2016/06/25 5 /// 作者:董兆生 6 /// 聯繫方式:QQ490412323 7 // ...
  • 在FormPanel中按回車按鍵,會觸發預設按鈕的click事件。設置方法為在FormPanel中設置DefaultButton屬性,如果沒有設置這個屬性,預設為最後一個按鈕。 1.預設最後一個按鈕為預設按鈕 2.以數字編號指點預設按鈕 3.用ID指定預設按鈕 4.用選擇器指定預設按鈕 視圖的完整代 ...
  • 使用VS2015進行C++開發的6個主要原因 使用Visual Studio 2015進行C++開發 在今天的 Build 大會上,進行了“將你的 C++ 代碼轉移至 VS2015 的 6 個原因”的演講,其中探討了 VS2015 中對於 C++ 開發者們更有用的新功能。自從它在 2015 年七月的 ...
  • C#——類 一、String 類 系統內置的處理字元串類型的函數方法類。方便我們對字元串類型進行一系列的處理。 1、Length:獲取字元串的長度,返回一個int類型的值 string x=Console.ReadLine();//小string是大String的快捷方式 int i = x.Len ...
  • 一、前言: 最近做一個簡單的線上升級Demo,使用了微軟較早的.Net Remoting技術來練手。 簡單的思路就是在伺服器配置一個Remoting對象,然後在客戶端來執行Remoting對象中的方法。 過程: (1) 讀取本地dll文件的名稱與版本號,與伺服器的進行對比 (2) 確認需要升級的文件 ...
  • Ext.Net版本:4.1.0 Ext.Net官網:ext.net Ext.Net官方演示:mvc.ext.net Ext.Net MVC Example 下載:github.com/extnet/Ext.NET.Examples.MVC Ext.Net Nuget 地址:www.nuget.org ...
  • 這次是對2.0的小修補,2.0交互幾乎沒有,這次添加了進度條,和文本框,同時由於取得的鏈接主要會出現錯誤是:webResponse錯誤。 針對這種情況,設置了 截取錯誤信息,這裡我們不處理,後續直接判定statecode屬性來決定是否還要執行下麵的程式。 另外一點變化就是以前是通過將所獲取的網頁存到 ...
  • 環境:vs2013+web api 2 問題:預設情況下新建的Web Api 2項目,自帶的Help頁下會顯示Api的相關信息,但Description那一欄無法獲取到數據,如下圖所示: 解決: 1.先啟用輸出的XML文檔文件: 在web api項目上右擊->屬性,在屬性頁"生成"標簽下,勾選輸出下 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...