程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 計算機程序的思維邏輯 (21),思維邏輯

計算機程序的思維邏輯 (21),思維邏輯

編輯:JAVA綜合教程

計算機程序的思維邏輯 (21),思維邏輯


內部類

之前我們所說的類都對應於一個獨立的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) - 類

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved