1. 程式人生 > >JAVA核心技術筆記總結--第6章 抽象類、接口、內部類和Lambda表達式

JAVA核心技術筆記總結--第6章 抽象類、接口、內部類和Lambda表達式

重載方法 lam 類變量 cat 而在 訪問 負責 3.4 第一次

6.1 抽象類

抽象類是指定義時有 abstract 修飾的類,例如:

public abstract class Person{
  ...
    public abstract String getDescription();//註意末尾沒有花括號,而有分號
}

在定義中有abstract修飾符的方法是抽象方法。抽象類中可以包含實例變量和實例方法,甚至可以沒有抽象方法,但是有抽象方法的類一定要定義為抽象類。

抽象方法充當著占位的角色,它們的具體實現在子類中。抽象方法不能有方法體,即沒有花括號,但必須有分號,方法定義變成了方法聲明。擴展抽象類可以有兩種選擇:

  1. 若子類只實現了父類中部分抽象方法,此時子類也為抽象類
  2. 若子類中實現了父類中所有抽象方法,此時子類為抽象類的實現類,子類是一個普通類,不再是抽象類。

抽象類不能實例化(因為抽象方法沒有具體實現,即使抽象類中不包含抽象方法),可以定義抽象類的變量,但它只能引用其實現類的對象。

抽象類可以包含實例變量、實例方法、類變量、靜態方法、構造器、靜態初始化塊、普通初始化塊、內部類。抽象類的構造器不能用於創建實例,主要是用於被其子類調用。在抽象類中,實例方法可以調用抽象方法。以下面的代碼為例,因為子類CarSpeedMeter實現了抽象方法getRadius,相當於子類覆蓋了該方法,從而具有多態性,子類對象調用getSpeed時,getSpeed中調用的getRadius

方法是子類中的getRadius

public abstract class SpeedMeter{
  private double turnRate;
  public abstract double getRadius(); 
  public double getSpeed(){
    return Math.PI * 2 * getRadius();
  }
}
public class CarSpeedMeter extends SpeedMeter{
  public double getRadius(){
    ...
  }
}

6.2 接口

6.2.1 Java 8中接口的定義

定義接口時不再使用class關鍵字,而使用interface關鍵字。接口定義的基本語法如下:

[修飾符] interface 接口名 extends 父接口1,父接口2,...{
  零個到多個靜態常量定義。。。
  零個到多個抽象方法定義...
  零個到多個默認方法定義...//僅在java8中允許
  零個到多個內部類、接口、枚舉定義
}

由於接口裏定義的是多個類共同的公共行為規範,因此接口裏所有成員都是默認public訪問權限。接口裏不包含成員變量,構造器和初始化塊。接口裏只能包含靜態常量、抽象實例方法、類方法、默認方法、內部類、內部接口、內部枚舉。

靜態常量可以省略public static final,系統默認添加。且靜態常量只能在定義時初始化。

對於抽象方法,系統默認添加public abstract修飾,因此,抽象方法不能有方法體。從而,實現類覆蓋這些抽象方法時,訪問權限必須是public

默認方法需要加default修飾符,但不能有static修飾符,否則,和類方法沒有區別。系統默認添加public修飾。以鼠標監聽接口MouseListener來說明默認方法存在的目的,MouseListener包含5個接口:

interface MouseListener{
  void mouseClicked(MouseEvent event);
  void mousePressed(MouseEvent event);
  void mouseReleased(MouseEvent event);
  void mouseEntered(MouseEvent event);
}

大多數情況下,只需要關心前兩個接口,在Java8中可以把所有方法聲明為默認方法,這些方法什麽都不做:

interface MouseListener{
  default void mouseClicked(MouseEvent event){}
  default void mousePressed(MouseEvent event){}
  default void mouseReleased(MouseEvent event){}
  default void mouseEntered(MouseEvent event){}
}

從而,實現此接口的程序員只需要重寫他們真正關心的方法。默認方法可以調用其他的默認或抽象方法。

靜態方法是java8增加的功能,在此之前,Java多會給接口實現一個伴隨類中,並將靜態方法放在伴隨類中。當允許在接口中定義靜態方法時,這些伴隨類將不再需要。

接口裏的內部類、內部接口、內部枚舉,系統默認添加public static修飾,因為不能創建接口實例。

定義接口的示例如下:

public interface Output{
  int MAX_CACHE_LINE = 50;
  void out();
  static String staticTest(){
    return "類方法";
  }
  default void test(){
    System.out.println("默認方法");
  }
}
6.2.2 接口的繼承

接口支持多繼承,一個接口可以有多個直接父接口,但接口只能繼承接口,不能繼承類。子接口擴展父接口時,將會獲得父接口所有的默認方法和抽象方法。

一個接口繼承多個接口時,多個接口排在extends關鍵字之後,多個父接口之間以英文逗號隔開。示例如下:

interface A{
  int PRO_A = 5;
  void testA();
}
interface B{
  int PRO_B = 6;
  void testB();
}
interface C extends A,B{
  int PRO_C = 7;
  void testC();
}
6.2.3 使用接口

接口不能創建實例,但接口可以聲明變量。但接口聲明的變量必須引用到其實現類的對象。除此之外,接口的主要功能是被實現類實現。接口的主要用途歸納如下:

  • 定義變量,也可用於強制類型轉換。
  • 調用接口中定義的靜態常量和靜態方法。
  • 被其他類實現。

一個類可以實現一個或多個接口,繼承使用extends,實現則使用implements。允許一個類實現多個接口,可以彌補java單繼承的不足,同時避免多繼承的復雜性和低效性。類實現接口的語法格式如下:

[修飾符] class 類名 extends 父類 implements 接口1,接口2...{
  類體部分
}

實現接口與繼承父類相似,一樣可以獲得所實現接口裏定義的靜態常量、方法(抽象方法和默認方法)。

讓類實現接口需要類定義後增加implements部分,當需要實現多個接口時,多個接口之間以英文逗號隔開,一個類可以繼承父類,並同時實現多個接口,implements部分必須方法extends部分之後。

一個類實現類一個或多個接口之後,這個類必須完全實現這些接口裏定義的全部抽象方法(即重寫這些抽象方法,包括接口從父接口繼承得到的抽象方法);否則,該類必須定義為抽象類。

接口不能顯式繼承任何類,但所有接口類型的變量都可以直接賦給Object類型的變量,因為編譯器知道接口類型變量的運行時類型必定是實現類對象,而任何Java對象都必須是Object或其子類的實例。

6.2.4 接口和抽象類

接口和抽象類很像,他們的共同點有:

  • 接口和抽象類都不能被實例化。
  • 接口和抽象類都可以包含抽象方法,實現接口或繼承抽象類的子類都必須實現這些方法。

但接口和抽象類的差別很大,主要體現在兩者的設計目的上:

  • 接口定義了一組規範,要求實現者必須提供哪些服務。當在程序中使用接口時,接口是多個模塊間的耦合標準。
  • 抽象類作為多個子類的共同父類,所體現的是一種模板式設計。

接口和抽象類在用法上的區別如下:

  • 接口中所有成員的訪問權限默認為public,而抽象類中各種訪問權限都可以。
  • 接口裏只能包含抽象方法、默認方法和類方法。不能為普通方法提供實現;抽象類可以包含實例方法。
  • 接口裏只能定義靜態常量;抽象類可以定義成員變量、類變量。
  • 接口不能包含構造器和初始化塊;抽象類中可以包含構造器和初始化塊。
  • 一個類最多只能有一個直接父類,但一個接口可以繼承多個直接父接口。
6.2.5 解決默認方法沖突
  • 當一個類繼承了父類,並實現了接口,若接口中的默認方法和父類中的方法,名稱相同且形參列表相同時,父類的方法會覆蓋接口的默認方法。這樣做的目的是與java8之前的代碼兼容。
  • 當一個類繼承了父類,並實現了接口,若接口中的抽象方法和父類中的方法,名稱相同且形參列表相同時,那麽子類相當於已經有了接口抽象方法的默認實現。
  • 當一個類實現了多個接口,若其中兩個及以上的接口有同名且形參列表相同的默認方法,或者一個接口的默認方法和另一個接口的抽象方法滿足覆蓋條件時,編譯器會報錯,解決方法是:在子類中覆蓋此方法,方法體中可以選擇執行哪個接口的默認方法。語法格式為:"接口名.super.方法名"
6.2.6 接口與回調

回調指對象調用某個方法時,此方法需要的實參是一個接口,而這個接口和對象有關,從而,對象調用方法後,方法反過來調用這個接口。

6.2.7 Java中常用的接口
- Comparable<T>
  接口中定義了:
  public int comPareTo(T other);
- Comparator<T>(比較器)
  接口中定義了
  public int compare(T first, T second);
- Clonable<T>
  public T clone();

深克隆與淺克隆。

Object類實現了基本的clone()方法,但由於不了解對象的域,所以只能逐個域地進行拷貝,對於引用類型的域,拷貝域就會得到相同子對象的另一個引用,從而,原對象和克隆得到的對象仍會共享一些信息,此為淺克隆。

深克隆的做法是:對於基本數據類型,直接拷貝,對於引用類型的域,遞歸拷貝引用對象的每個域。使得原對象和克隆得到的對象不再共享任何信息(對於不可變類,可直接復制值,只有可變類,才需要這麽處理)。調用此類型的clone()進行拷貝。

自定義克隆方法時,需要確定:

  1. 默認的Object類的克隆方法是否滿足要求。
  2. 是否可以在可變的子對象上調用克隆方法來修補默認的克隆方法。
  3. 是否不該使用克隆方法。

如果選擇第1項或第2項,類必須:

  1. 實現Cloneable接口
  2. 重新定義clone(),並指定public訪問修飾符。

由於Object類中的clone()聲明為protected,所以子類只能調用此方法來克隆自己的對象,必須重新定義clone()public才能允許所有方法克隆對象。

深克隆示例如下:

class Employee implements Cloneable{
  ...
    public Employee clone() throws CloneNotSupportedException{
        Employee cloned = (Employee) super.clone();
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
  }
}

所有數組類型都有一個publicclone方法。

6.3 內部類

在一個類內部定義的另一個類稱為內部類,此處的“類內部”包括類中的任何位置,甚至在方法中也可以定義內部類(方法裏定義的內部類稱為局部內部類和匿名內部類)。包含內部類的類稱為外部類。內部類的主要作用有:

  • 內部類提供了更好的封裝,可以把內部類隱藏在外部類之內,不允許同一個包中的其他類訪問該類。
  • 因為內部類被當成外部類成員,而同一個類的成員之間可以相互訪問,所以內部類方法可以直接訪問外部類的私有數據。但外部類要訪問內部類的實現細節,必須先定義內部類對象。
  • 匿名內部類適合用於創建那些僅需要使用一次的抽象類或接口的實現類對象。

定義內部類與定義外部類的語法大致相同,內部類除了需要定義在其他類裏面之外,還存在如下兩點區別:

  • 內部類比外部類可以多使用三個修飾符:private、protected、static
  • 非靜態內部類不能有靜態成員。
6.3.1 成員內部類
6.3.1.1 非靜態內部類

? 多數情況下,內部類作為成員內部類定義,而不是作為局部內部類。成員內部類是一種與成員變量、方法、構造器和初始化塊相似的類成員;局部內部類和匿名內部類不是類成員。

成員內部類分為兩種:靜態內部類和非靜態內部類,使用static修飾的成員內部類是靜態內部類,沒有使用static修飾的成員內部類是非靜態內部類。

因為內部類是作為其外部類的成員,所以可以使用任意訪問控制符如private,protected 和 public 等修飾。編譯器生成的成員內部類的class文件格式為:OuterClass$InnerClass.class。

在非靜態內部類裏可以直接訪問外部類的private成員。這是因為在非靜態內部類對象裏,保存了一個它所寄生的外部類對象的引用,同時編譯器會在外部類中添加對相應私有域的訪問器和更改器(當調用非靜態內部類的實例方法時,必須有一個非靜態內部類實例,非靜態內部類實例必須寄生在外部類實例裏)。

當在非靜態內部類的方法內訪問某個變量時,系統優先在該方法內查找是否存在該名字的局部變量,如果存在就使用改變了;如果不存在,則到該方法所在的內部類中查找是否存在該名字的成員變量,如果存在則使用該成員變量;如果不存在,則到該內部類所在的外部類中查找是否存在該名字的成員變量,如果存在則使用該成員變量,如果依然不存在,系統將出現編譯錯誤,提示找不到該變量。

因此,如果外部類成員變量、內部類成員變量與內部類方法的局部變量同名,則可通過使用外部類名.this,this作為限定區分。如下程序所示:

public class TestVar{
    private String prop = "外部類的實例變量";
    private class InClass{
        private String prop = "內部類的實例變量";
        public void info(){
            String prop = "局部變量";
            System.out.println("輸出的是外部實例變量" + TestVar.this.prop);
            System.out.println("輸出的是內部類的實例變量" + this.prop);
            System.out.println("輸出的是局部變量" + prop);
        }        
    }
    public void test(){
      Inclass in = new InClass();
      in.info();
    }
  public static void main(String[] args){
    new TestVar.test();
  }
}

非靜態內部類的成員可以訪問外部類的private成員,但反過來不成立。因為外部類對象存在時,非靜態內部類對象不一定存在。如果外部類需要訪問非靜態內部類的成員,必須顯式地創建內部類對象來訪問其實例成員,外部類方法可以訪問內部類的私有成員。(和普通的一個類的方法訪問另一個類的方法存在區別,外部類訪問內部類的實例成員,不需要通過內部類的公有方法來訪問,這還是因為,大家都是外部類的成員,成員之間可以相互訪問)。如下所示:

public class Outer{
  private int outprop = 9;
  class Inner{
    private int inprop = 5;
    public void accessOuterprop{
      System.out.println("外部類的outprop值:" + outprop);
    }
  }
  public void accessInnerProp(){
    //System.out.println("內部類的inprop值" + inprop);
    System.out.println("內部類的inprop值" + new().inprop);
  }
  public static void main(String[] args){
    Outer out = new Outer();//註釋2  
    out.accessInnerProp();
  }
}

第一處註釋試圖在外面類方法裏訪問非靜態內部類的實例變量,將引起編譯錯誤。外部類不允許訪問非靜態內部類的實例成員的原因是:上面main方法的第二處註釋代碼只創建了一個外部類對象,並調用外部類對象的accessInnerProp方法。此時非靜態內部類對象根本不存在,如果允許accessInnerProp方法訪問非靜態內部類對象,將引起錯誤。

非靜態內部類對象和外部類對象的關系

如果存在一個非靜態內部類對象,則一定存在一個被它寄生的外部類對象。但當外部類對象存在時,外部類對象裏不一定寄生了非靜態內部類對象。所以外部類對象訪問非靜態內部類對象時,可能非靜態內部類對象根本不存在,而非靜態內部類對象訪問外部類對象時,外部類對象一定存在。

根據靜態成員不能訪問非靜態成員的規則,外部類的靜態方法、靜態代碼塊不能訪問非靜態內部類,尤其是不能使用非靜態內部類創建實例等(原因是:使用非靜態內部類時,外部類的對象並不存在,此時,非靜態內部類對象無處寄生):

public class StaticTest{
    private class In{}  
    piblic static void main(String[] args){
      //無法訪問非靜態成員 In類
      new In();
    }
}

java不允許在非靜態內部類裏定義靜態成員、靜態方法、靜態初始化塊。否則可以通過OutClass.InClass的方法來調用,此時,外部類對象並未創建。

非靜態內部類裏不能有靜態初始化塊,但可以有普通初始化塊,非靜態內部類的普通初始化塊的作用與外部類初始化塊的作用相同。

6.3.1.2 靜態內部類

如果使用static來修飾一個內部類,則這個內部類就屬於外部類本身,而不屬於外部類的某個對象,相當於外部類的類成員。因此使用static修飾的內部類被稱為靜態內部類。

static關鍵字的作用是把類的成員變成類相關,而不是實例相關,即static修飾的成員屬於整個類,而不屬於單個對象。對象類的上一級程序單元是包,所以不可使用static修飾;而內部類的上一級程序單元是外部類,使用static修飾可以將內部類變成外部類相關,而不是外部類實例相關。

靜態內部類可以包含靜態成員,也可以包含非靜態成員。靜態內部類不能訪問外部類的實例成員,只能訪問外部類的靜態成員。即使是靜態內部類的實例方法也不能訪問外部類的實例成員,只能訪問外部類中的靜態成員。原因是:靜態內部類不需要寄生在外部類實例中,靜態內部類的實例創建時,外部類的實例不一定存在。

因為靜態內部類是外部類的一個靜態成員,因此外部類的所有方法,所有初始化塊中可以使用靜態內部類來定義變量、創建對象等。

外部類依然不能直接訪問靜態內部類的成員,但可以使用靜態內部類的類名作為調用者訪問靜態內部類的靜態成員,也可以使用靜態內部類的對象訪問靜態內部類的實例成員。

public lass AccessStaticInnerClass{
    static class StaticInnerClass{
        private static int prop1 = 5;
        private int prop2 = 9;
    }
    public void accessInnerProp(){
        System.out.println(StaticIneerClass.prop1);
        System.out.println(new StaticIneerClass().prop2);
    }
}
6.3.2 使用內部類
6.3.2.1 在外部類裏面使用非靜態內部類

在外部類裏面可以調用內部類對象的私有成員,但是需要先創建內部類對象。

6.3.2.2 在外部類的實例方法中使用內部類
  • 定義內部類變量

    內部類名 變量名

  • 創建內部類對象

    new 內部類名(實參列表)

  • 訪問內部類成員

    內部類對象名.成員名

6.3.2.3 在外部類的靜態方法中使用內部類
  • 定義內部類變量

    內部類名 變量名

  • 創建內部類對象

    在外部類靜態方法中創建內部類實例時,需要先有外部類實例。然後:外部類實例名.new 內部類名(實參列表)

  • 訪問內部類成員

    內部類對象名.成員名

6.3.2.4 在外部類外面使用非靜態內部類

如果希望在外部類外面使用內部類,則內部類不能使用private訪問控制權限,private修飾的內部類只能在外部類裏面使用,內部類的訪問權限如下:

  • 省略訪問控制符的內部類,只能被與外部類處於同一個包中的其他類訪問
  • 使用protected修飾的內部類,可被與外部類處於同一個包中的其他類和外部類的子類訪問
  • 使用public修飾的內部類,可以在任何地方被訪問。

在外部類外訪問內部類的格式如下:

  • 定義內部類變量

    外部類名.內部類名 變量名

  • 創建內部類對象

    在外部類外面創建內部類實例時,需要先有外部類實例。然後:外部類實例名.new 內部類名(實參列表)//此處不需要再寫外部類.內部類。定義變量需要這麽做是保證類的唯一性。

  • 訪問內部類成員

    內部類對象名.成員名。此時只能訪問內部類的公有成員

6.3.2.5 在外部類裏面使用靜態內部類

由於靜態內部類是外部類相關的。所以內部類對象無需寄生於外部類對象。從而在外部類的靜態方法和非靜態方法中使用靜態內部類格式相同,均為:

  • 定義內部類變量

    內部類名 變量名

  • 創建內部類對象

    new 內部類名(實參列表)

  • 訪問內部類成員

    內部類對象名.成員名

6.3.2.6 在外部類外使用靜態內部類

在外部類外使用靜態內部類的格式如下:

  • 定義內部類變量

    外部類名.內部類名 變量名

  • 創建內部類對象

    new 外部類名.內部類名(實參列表)

  • 訪問內部類成員

    內部類對象名.成員名。此時只能訪問內部類的公有成員

6.3.3 定義內部類的子類

內部類的子類不一定是內部類,可以是一個外部類。

6.3.3.1 定義非靜態內部類的子類

當創建一個非靜態內部類的子類時,子類構造器總會調用父類的構造器,而調用非靜態內部類的構造器時,必須存在一個外部類對象。因此在創建非靜態內部類的子類時,必須給子類構造器傳一個外部類對象作為參數。所以定義非靜態內部類子類的格式為:

class 子類名 extends 外部類名.內部類名{
  [修飾符] 子類名(外部類名 外部類實例,實參){
    外部類實例名.super(實參);
    ...
  }
  ...
}

示例如下:

public class SubClass extends Out.In{
    public SubClass(Out obj){
        obj.super("hello");
    }
}

非靜態內部類對象和其子類對象都必須持有寄生的外部類對象的引用,區別是創建兩種對象時傳入外部類對象的方式不同:當創建非靜態內部類對象時,通過外部類對象來調用new關鍵字;當創建內部類子類對象時,將外部類對象作為子類構造器的參數。

非靜態內部類的子類實例仍然需要保留一個引用,即如果一個非靜態內部類的子類的對象存在,也一定存在一個寄生的外部類對象。

6.3.3.2 定義靜態內部類的子類

因為調用靜態內部類的構造器時無需使用外部類對象,所以創建靜態內部類的子類比較簡單,格式如下:

clsss 子類名 extends 外部類名.內部類名{
  ...
}

可以看出,當定義一個靜態內部類時,其外部類非常像一個包空間。

相比之下,使用靜態內部類比使用非靜態內部類簡單很多,只要把外部類當成靜態內部類的包空間即可,因此當程序需要使用內部類時,應該優先考慮使用靜態內部類。

外部類的子類中如果定義一個與父類內部類同名的內部類時,子類創建的是子類內部類的對象,父類創建的是父類內部類的對象,如果把子類對象賦給父類引用,再創建內部類對象,此時創建的是父類內部類的對象。可以把內部類看成事外部類的成員變量,通過靜態分派確定符號引用。

6.3.4 局部內部類

如果在方法裏定義內部類,則這個內部類是一個局部內部類,局部內部類僅在該方法裏有效。由於局部內部類不能在此方法以外的地方使用,因此局部內部類不需要訪問控制符和static修飾符修飾。

對於局部成員而言,不管是局部變量還是局部類,他們的上一級程序單元都是方法,而不是類,使用static修飾他們沒有任何意義;不僅如此,因為局部成員的作用域是所在方法,其他程序單元永遠也不可能訪問一個方法中的局部成員,所以,所有的局部成員不能使用訪問控制符和static修飾符。

如果需要用局部內部類定義變量、創建實例或派生子類,那麽都只能在局部內部類所在的方法內進行。

public class LocalInnerClass{
  public static void main(String args){
    class InnerBase{
      int a;
    }
    class InnerSub extends InnerBase{
      int b;
    }
    InnerSub is = new InnerSub();
    is.a = 5;///////////////////////////////方法中可以直接訪問局部內部類的域。
    is.b = 8;
    System.out.println(is.b + " " + is.a);
  }
}
  

編譯程序,生成三個class文件:LocalInnerClass.class、LocalInnerClass$InnerBase.class、LocalInnerClass$InnerSub.class。註意到局部內部類的class文件的文件名比成員內部類的class文件的文件名多了一個數字,這是因為同一個類裏不可能有兩個同名的成員內部類,而同一個類裏可能有兩個以上的同名的局部內部類(處於不同的方法中)。

局部內部類在實際開發中很少使用,因為局部內部類的作用域太小了,只能在當前方法中使用。

6.3.5 匿名內部類

匿名內部類適合創建只需要使用一次的類,創建匿名內部類時會立即創建一個該類的實例。定義格式如下:

new 實現接口() |父類構造器(實參列表)
{
  //匿名內部類的類體部分
}

從定義可知,匿名內部類必須繼承一個父類,或實現一個接口,但最多只能繼承一個父類或實現一個接口。

關於匿名內部類有如下兩條規則:

  • 匿名內部類不能是抽象類,因為系統在創建匿名內部類時,會立即創建匿名內部類的對象。

  • 匿名內部類不能定義構造器。由於匿名內部類沒有類名,所以無法定義構造器。取而代之的是,將構造器參數傳遞給父類構造器。同時匿名內部類可以定義初始化塊,可以通過實例初始化塊完成構造器需要完成的事情。

最常用的創建匿名內部類的方式是需要創建某個接口類型的對象。

interface Product{ 
  public String getName();
}
public class AnonyTest{
  public void test(Product p){
    System.out.println("購買了" + p.getName());    
  }
  public static void main(String[] args){
    AnonyTest obj = new AnonyTest();
    obj.test( new Product(){
        public String getName(){
          return "tom";
        }                      
      });
  }
}

上述程序中的AnonyTest類定義了一個test方法,該方法需要一個Product對象作為參數,但Product只是一個接口,無法直接創建對象,因此考慮創建一個Product接口實現類的對象傳入該方法---如果這個Product接口實現需要重復使用,則應該將實現類定義成一個獨立類;如果這個Product接口實現類只需要一次使用,就可以定義一個匿名內部類。

由於匿名內部類不能是抽象類,所以匿名內部類必須實現它的抽象父類或接口裏包含的所有抽象方法。

當通過實現接口來創建匿名內部類時,由於結構沒有構造器。因此new接口名後的括號裏不能傳入參數值。

但是如果通過繼承父類來創建匿名內部類時,匿名內部類將擁有和父類相似的構造器,此處的相似指的是擁有相同的形參列表。

匿名內部類繼承抽象父類的示例:

abstract class Device{
  private String name;
  public abstract double getPrice();
  public Device(){}
  public Device(String name){
    this.name = name;
  }
  //省略name的訪問器和修改器
}
public class AnonyTest{
  public void test(Device p){
    System.out.println("花費" + p.getPrice());      
  }
  public static void main(String[] args){
    AnonyTest obj = new AnonyTest();
    obj.test(new Device("honey"){
               public double getPrice(){
                 return 56.3;
               }  
    });
    Device p = new Device(){
      {//初始化塊
        System.out.println("匿名內部類的初始化塊:");
      }
      //實現抽象方法
      public double getPrice(){
        return 56.3;
      }
      //覆蓋父類方法
      public String getName(){
        return "鍵盤";
      }
    }
    obj.test(p);
  }
}

當創建匿名內部類時,必須實現接口或抽象父類裏的所有抽象方法,如果有需要,也可以重寫父類的普通方法。

在java8之前,java要求被局部內部類、匿名內部類訪問的局部變量,在方法中定義時必須用final修飾,從java8開始這個限制被取消了,由編譯器進行處理:如果局部變量被匿名內部類訪問,那麽該局部變量相當於自動使用了final修飾符。此局部變量在第一次賦值後,值不能再修改,否則編譯器將報錯。例如:

public class PairTest{
    public static void main(String[] args){
        int age = 0 ;
        age =3;
        class Device{
            void test(){
                System.out.println(age);
            }
        }
        Device d = new Device();
        d.test();
    }
}

age在初始化為0後,被賦值為3,所以編譯器將會報錯。

java8將這個功能稱為“effective final”,意思是對於匿名內部類訪問的局部變量,可以用final修飾,也可以不同final修飾,但必須按照有final修飾的方式來用,也就是一次賦值後,以後不能重新賦值。

內部類是一種編譯器現象,與虛擬機無關。編譯器會把內部類翻譯成用$分隔外部類名與內部類名的常規類文件,而這個操作對虛擬機是透明的。編譯階段,編譯器會對內部類進行處理,轉化為外部類,內部類對外部類對象的訪問是因為編譯器會在內部類的構造器中添加一個外部類引用的參數。內部類對外部類實例域的訪問:編譯器會在外部類中添加相關實例域的訪問器方法,從而內部類對外部類實例域的訪問將轉化為調用外部類的訪問器方法來實現。

局部內部類對方法內的局部變量的訪問:由於方法在創建局部內部類實例後,可能程序執行結束,局部變量會釋放,此時局部內部類中將無法訪問到局部變量,所以編譯器會在局部內部類中添加實例域,在創建局部內部類實例時,將局部變量的值保存到添加的實例域中。

在內部類不需要訪問外部類對象時,應該使用靜態內部類。

6.4 Lambda表達式

6.4.1 Lambda表達式的定義

函數式接口:只有一個抽象方法的接口。函數式接口可以包含多個默認方法、類方法,但只能聲明一個抽象方法。

匿名內部類的實現,以及Lambda表達式使用示例:

public class CommandTest{
  public static void main(String[] args){
    ProcessArray pa = new ProcessArray();
    int[] target = {3, -4, 6, 4};
    pa.process(target, new Command(){
      public int process(int[] target){
        int sum = 0;
        for(int tmp : target){
          sum += temp;
        }
        return sum;
      }     
    });
  }
}

Lambda表達式可以對上述代碼進行簡化:

public class CommandTest{
  public static void main(String[] args){
    ProcessArray pa = new ProcessArray();
    int[] target = {3, -4, 6, 4};
    pa.process(target, (int[] target)->{     
        int sum = 0;
        for(int tmp : target){
          sum += temp;
        }
        return sum;
      });         
  }
}

可以看出,Lambda表達式的作用就是簡化匿名內部類的繁瑣語法。它有三部分構成:

  • 形參列表。形參列表允許省略形參類型。如果形參類別中只有一個參數,甚至連形參列表的圓括號都可以省略。
  • 箭頭(->)。必須通過英文中畫線號和大於符號組成。
  • 代碼塊。如果代碼塊只包含一條語句,Lambda表達式允許省略代碼塊的花括號。同時,當Lambda代碼塊只有一條return語句時,甚至可以省略return關鍵字。此時,如果lambda表達式需要返回值,而它的代碼塊中只有一條省略了return的語句,Lambda表達式會自動返回這條語句的值。

總結起來,lambda共有如下幾種省略情況:

  • 可以省略參數類型。
  • 只有一個參數時,可以省略參數的圓括號
  • 語句體只有一條語句時,可以省略花括號,此時語句末尾的分號也省略。
  • 只有一條return語句時,可以省略return關鍵字,語句末尾的分號也省略。

示例如下:

interface Flyable{
  void fly(String weather);
}
public class LambdaQs{ 
  public void drive(Flyable f){
    System.out.println("我正在駕駛:" + f);
    f.fly("晴天");
  } 
  public static void main(String[] args){
    LambdaQs lq = new LambdaQs();    
    lq.drive(weather -> 
             {
               System.out.println("今天天氣是:" + weather);  
               System.out.println("直升機飛行平穩");
             });   
  }
}
6.4.2 Lambda表達式與函數式接口

定義Lambda表達式時即創建了一個對象,對象的類型要求是一個函數式接口,具體由賦值號左邊的操作數類型決定。可以使用Lambda表達式進行賦值。用Lambda表達式進行賦值的示例如下:

Runnable r = ()->{
  for(int i = 0;i < 100 ; i++){
    System.out.println();
  }
};

Lambda表達式的限制如下:

  • Lambda表達式的目標類型必須是明確的函數式接口,甚至不能賦給Object類型的變量,否則,無法確定lambda表達式的運行時類型。
  • Lambda表達式只能實現一個方法,因此它只能為函數式接口創建對象。

示例如下:

Object obj = ()->{
    for(int i = 0;i < 100 ; i++){
        System.out.println();
    }
};

上述代碼的Lambda表達式賦給的是Object對象而不是函數式接口。所以,編譯器會報錯。

為了保證Lambda表達式的目標類型是明確的函數式接口,可以有如下三種常見方式:

  • 將Lambda表達式賦值給函數式接口類型的變量
  • 將Lambda表達式作為函數式接口類型的參數傳給某個方法。
  • 使用函數式接口對Lambda表達式進行強制類型轉換。

因此上述代碼,可修改

Object obj = (Runnable)()->{
    for(int i = 0;i < 100 ; i++){
        System.out.println();
    }
};

易知,Lambda表達式的目標類型完全可能是變化的(即可能會利用強制類型轉換,將Lambda表達式賦給另一個抽象方法相同的接口變量),唯一的要求是,Lambda表達式實現的匿名方法與函數式接口中的抽象方法有相同的形參列表和返回值。示例如下:

interface FKTest{
  public void run();
}
Runnable obj = ()->{
    for(int i = 0;i < 100 ; i++){
        System.out.println();
    }  
};
FKTest fk = (FKTest)obj;//賦值合法

Java 8在java.util.function包下預定義了大量的函數式接口,典型 地包含如下4類接口。

  • XxxFunction:這類接口中通常包含一個apply()抽象方法,該方法對參數進行處理、轉換,然後返回一個新的值。該函數式接口通常用於對指定的數據進行轉換處理。
  • XxxConsumer:這類接口中通常包含一個accept()抽象方法,該方法也負責對參數進行處理,只是該方法不會返回處理結果。
  • XxxPredicate:這類接口中通常包含一個test()抽象方法,該方法通常用來對參數進行某種判斷,然後返回一個boolean值。該接口通常用於判斷參數是否滿足特定條件,經常用於涮選數據。
  • XxxSupplier:這類接口中通常包含一個getAsXxx()抽象方法,該方法不需要輸入參數,該方法會按某種邏輯算法返回一個數據。

綜上所述,不難發現Lambda表達式的本質很簡單,就是使用簡潔的語法來創建函數式接口的實例------這種語法避免了匿名函數類的繁瑣。

6.4.3 方法引用與構造器引用

有時,現有方法可以完成抽象方法的功能,此時可以直接調用 現有類的方法或構造器,稱為方法引用和構造器引用。

方法引用和構造器引用可以讓Lambda表達式的代碼塊更加簡潔。方法引用和構造器引用都需要使用兩個英文冒號。Lambda表達式支持如下表所示的幾種引用方式。

種類 示例 說明 對應的Lambda表達式
引用類方法 類名::類方法 函數式接口中被實現方法的全部參數傳給該類方法作為參數 (a,b,...)->類名.類方法(a,b,...)
引用特定對象的實例方法 特定對象::實例方法 函數式接口中被實現方法的全部參數傳給該類方法作為參數 (a,b,...)->特定對象.實例方法(a,b,...)
引用某類對象的實例方法 類名::實例方法 函數式接口中被實現方法的第一個參數作為調用者,後面的參數全部傳給該方法作為參數 (a,b,...)->a.實例方法(a,b,...)
引用構造器 類名::new 函數式接口中被實現方法的全部參數傳給該構造器作為參數 (a,b,...)->new 類名(a,b,...)
6.4.3.1 引用類方法
@FunctionInterface
interface Converter{
  Ingeter convert(String from);
}
//使用Lambda表達式創建Conveter對象
Converter converter1 = from -> Integer.ValueOf(from);
Integer val = converter1.convert("99");

上面代碼調用converter1對象的convert()方法時------由於converter1對象是由Lambda表達式創建的,convert()方法執行體就是Lambda表達式的代碼部分。

上述的Lambda表達式的代碼塊只有一行調用類的方法的代碼,因此可以替換為:

Converter converter1 = Integer::ValueOf;
6.4.3.2 引用特定對象的實例方法
Converter converter2 = from -> "fkit.org".indexOf(from);
Integer value = converter1.convert("it");

上述的Lambda表達式的代碼塊只有一行調用"fkit.org"的indexOf()實例方法的代碼,因此可以替換為:

Converter converter2 = "fkit.org"::indexOf;
6.4.3.3 引用某類對象的實例方法
@FunctionalInterface
interface MyTest{
  String test(String a, int b, int c);
}
MyTest mt = (a, b, c)-> a.subString(b, c);
String str = mt.test("Java I love you", 2,9);

上述的Lambda表達式的代碼塊只有一行a.subString(b, c);因此可以替換為:

MyTest mt = String::subString;
6.4.3.4 引用構造器
@FunctionInterface
interface YourTest{
  JFrame win(String title);
}
YourTest yt = (String a)->new JFrame(a);
JFrame jf = yt.win("我的窗口");

上述Lambda表達式的代碼塊只有一行new JFrame(a);因此可以替換為:

YourTest yt = JFrame::new;

方法引用和構造器引用中,如果有多個同名的重載方法,編譯器會依據表達式實際轉換的函數式接口中聲明的方法進行選擇。可以使用數組類型建立構造器引用,例如int[]::new,它有一個參數,即數組的長度,這等價於lambda表達式x->new int[x]

可以在方法中使用this參數,例如this::equals等同於x-> this.equals(x),使用super也是合法的。this表示lambda表達式所在方法的對象。例如:

class Greeter{
  public void greet(){
    System.out.println("hello world!");
  }
}
class TimedGreeter extends Greeter{
  public void greet(){
    Timer t = new Timer(1000, super::greet);
    t.start();
  }
}
6.4.4 Lambda 表達式與匿名內部類的聯系和區別

Lambda 表達式是匿名內部類的一種簡化,因此它可以部分取代匿名內部類的作用,Lambda 表達式與匿名內部類存在以下相同點:

  • Lambda 表達式與匿名內部類一樣,都可以直接訪問“effectively final”的局部變量,以及外部類的成員變量(包括實例變量和類變量);
  • Lambda 表達式創建的對象與匿名內部類生成的對象一樣,都可以直接調用從接口中繼承的默認方法。
interface Displayable{
  void display();
  default int add(int a, int b){
    return a + b;
  }
}
public class LabdaAndInner{
  private int age = 12;
  private static String name = "i‘m here";
  public void test(){
      String book = "瘋狂java";
      Displayable dis = ()->{
        System.out.println("book 局部變量為:" + book);
        System.out.println("age 局部變量為:" + age);  
        System.out.println("name 局部變量為:" + name);  
    }
    dis.display();
    System.out.println(dis.add(3, 5));
  }
}

上述代碼示範了Lambda表達式分別訪問“effective final”的局部變量、外部類的實例變量和類變量。Lambda表達式訪問局部變量時,編譯器隱式為Lambda表達式添加一個私有域常量,並將局部變量放入Lambda表達式的默認構造器中以初始化私有域常量。Lambda表達式對於外部類實例域的訪問是編譯器將外部類實例引用作為參數傳入Lambda表達式的默認構造器,同時在Lambda表達式中定義一個實例域保存外部類實例引用實現的。

Lambda表達式與匿名內部類的主要區別:

  • 匿名內部類可以為任意接口創造實例;不管接口有多少個抽象方法們只要匿名內部類實現了所有的抽象方法即可;但是Lambda表達式只能為函數式接口創建實例。
  • 匿名內部類可以為抽象類甚至普通類創建實例;但Lambda表達式只能為函數式接口創建實例。
  • 匿名內部類實現的抽象方法的方法體允許調用接口中定義的默認方法;但Lambda表達式的代碼塊不允許調用接口中定義的默認方法。因為定義匿名內部類時已知其實現的接口或父類,而定義Lambda表達式時,其目標類型未知,雖然可以通過賦值號左邊的類型推斷,但由於此Lambda表達式還可以賦給定義了相同抽象方法的其他函數式接口,而那些接口中未必也定義了相同的默認方法。

例如在Lambda表達式的代碼塊中增加如下一行,編譯器將會報錯。

System.out.println(add(3, 5));

java中,lambda表達式就是閉包,如果在lambda表達式中使用了所在方法中的局部變量,稱lambda表達式捕獲了此局部變量。易知被捕獲的局部變量都必須是effectively final(最終變量,即變量初始化之後就不會再為它賦新值),且在lambda表達式中也不能改變,否則,當多個變量同時引用此lambda表達式時,會出現並發的安全問題。

lambda表達式的體與嵌套塊有相同的作用域。同樣適用命名沖突和遮蔽的規則。因此lambda表達式中不能聲明與局部變量同名的參數或局部變量。

使用lambda表達式的重點是延遲執行。

在設計接口時,如果接口中只有一個抽象方法,就可以用@FunctionInterface來標記這個接口。這樣,如果無意中增加了另一個非抽象方法,編譯器會產生一個錯誤的信息。

6.5 垃圾回收

垃圾回收機制具有如下特點:

  • 垃圾回收機制只負責回收堆內存中的對象,不會回收物理資源。
  • 程序無法精確控制垃圾回收的運行,垃圾回收會在合適的時候進行。當對象永久性地失去引用後,系統就會在合適的時候回收它所占的內存。
  • 在垃圾回收機制回收任何對象之前,總會調用它的finalize()方法,該方法可能使該對象復活(讓一個引用變量重新引用該對象),從而導致垃圾回收機制取消回收。
6.5.1 對象在內存中的狀態

當一個對象在堆內存中運行時,根據它被引用變量所引用的狀態,可以把它所處的狀態分成如下三種:

  • 可達狀態:當一個對象被創建後,若有引用變量引用它,則該對象處於可達狀態。
  • 可恢復狀態:如果某個對象不再有任何引用變量引用它,就處於可恢復狀態。在這種狀態下,系統的垃圾回收機制準備回收該對象占用的內存,在回收該對象之前,系統會調用對象的finalize()方法進行資源清理。如果系統在調用所有可恢復對象的finalize()時重新讓一個引用變量引用該對象,則這個對象再次變為可達狀態;否則該對象將進入不可達狀態。
  • 不可達狀態:當對象與所有引用變量的關聯都被切斷,且系統已經調用所有對象的finalize()方法後,依然沒有使該對象變為可達狀態,那麽該對象將永久性地失去引用,變成不可達狀態。只有當對象處於不可達狀態時,系統才會真正回收該對象占有的資源。
6.5.2 強制垃圾回收

當一個對象失去引用後,系統何時調用它的finalize()對其進行資源清理,它何時變為不可達狀態,系統何時回收它占有的內存,對於程序完全透明。程序只能控制一個對象何時不再被任何引用變量引用,決不能控制它何時被回收。

程序無法精確控制Java垃圾回收的時間,但依然可以強制系統進行垃圾回收--這種強制只是通知系統進行垃圾回收,但系統是否進行垃圾回收依然不確定。大部分時候,程序強制系統垃圾回收後總會有一些效果。強制系統垃圾回收有如下兩種方式:

  • 調用System類的靜態方法gc():System.gc()
  • 調用Runtime對象的實例方法gc()Runtime.getRuntime().gc()

示例如下:

public class GcTest{
  public static void main(String[] args){
    for(int i = 0; i < 4; i++){
      new GcTest();
      //下面兩種方法完全相同
      System.gc();
      //Runtime.getRuntime().gc();
    }
  }
  public void finalize(){
    System.out.println("系統正在清理");
  }
}
6.5.3 finalize()

finalize()是定義在Object類裏的實例方法,方法原型為:

protected void finalize() throws Throwable

finalize()方法返回後,對象消失,垃圾回收機制開始執行。方法原型中的throws Throwable表示可以拋出任何異常。

任何java類都可以重寫Object類的finalize()方法,在該方法中清理對象占用的資源。只有當程序認為需要更多的額外內存時,垃圾回收機制才會進行垃圾回收。

finalize()具有如下4個特點:

  • 永遠不要主動調用某個對象的finalize()方法,該方法應交給垃圾回收機制調用。
  • finalize()方法何時被調用,是否被調用具有不確定性,不要把finalize()當成一定會被執行的方法。
  • JVM執行可恢復對象的finalize()方法時,可能使該對象或系統中其他對象重新變為可達狀態。
  • JVM執行finalize()方法出現異常時,垃圾回收機制不會報告異常,程序繼續執行。

示例如下:

public class FinalizeTest{
  private static final FinalizeTest ft = null;
  public void info(){
    System.out.println("測試finalize方法");
  }
  public static void main(String[] args){
    new FinalizeTest();
    System.gc();
    System.runFinalization();
    ft.info();
  }
  public void finalize(){
    ft = this;
  }
}

代碼中的finalize()把需要清理的可恢復對象重新賦給靜態變量,從而讓該可恢復對象重新變成可達狀態。通常finalize()方法的最後一句是調用父類的finalize():super.finalize()

6.5.4 對象的軟、弱和虛引用

對大部分對象而言,程序裏會有一個引用變量引用該對象,這是最常見的引用方式。除此之外,java.lang.ref包下提供了3個類:SoftReference、PhantomReferenceWeakReference,他們分別代表了系統對對象的3種引用方式:軟引用、弱引用和虛引用。因此Java語言對對象的引用有如下4種方式:

  1. 強引用(StrongReference)

    這是Java程序中最常見的引用方法。程序創建一個對象,並把這個對象賦給一個引用變量。當一個對象被引用變量引用時,它處於可達狀態,不可能被垃圾回收機制回收。

  2. 軟引用

    軟引用需要通過SoftReference類來實現,當一個對象只有軟引用時,它可能被垃圾回收機制回收。對於只有軟引用的對象而言,當系統內存足夠時,它不會被回收,程序也可使用該對象;當系統內存不足時,系統可能會回收它。

  3. 弱引用

    弱引用通過WeakReference類實現,弱引用和軟引用類似,但弱引用的引用級別更低。對於只有弱引用的對象而言,當垃圾回收機制運行時,不管系統內存是否足夠,總會回收該對象的內存。

  4. 虛引用

    虛引用通過PhantomReference類實現,虛引用完全類似於沒有引用。虛引用對對象本身沒有太大影響,對象甚至感覺不到虛引用的存在。如果一個對象只有虛引用時,那麽它和沒有引用效果大致相同。虛引用主要用於跟蹤對象被垃圾回收的狀態,虛引用不能單獨使用,虛引用必須和引用隊列(ReferenceQueue)聯合使用。

上面三個引用類都包含一個get()方法,用於獲取他們引用的

對象。但虛引用太弱了,無法獲取到引用的對象。

引用隊列ReferenceQueuejava.lang.ref.ReferenceQueue類表示,用於保存被回收後對象的引用。當聯合使用軟引用、弱引用和虛引用時,系統在回收被引用的對象之後,將把被回收對象的引用添加到關聯的引用隊列中。

軟引用和弱引用可以單獨使用,但虛引用不能單獨使用,單獨使用虛引用沒有意義。虛引用的主要作用就是跟蹤對象被垃圾回收的狀態,程序通過檢查與虛引用關聯的引用隊列是否包含了該虛引用,從而了解虛引用所引用的對象是否即將被回收。

弱引用用法示例:

class Test{
  public static void main(String[] args){
    String str = new String("java");
    //創建弱引用,使其引用str對象
    WeakReference wr = new WeakReference(str);
    str = null;
    //取出弱引用wr所引用的對象
    System.out.println(wr.get());
    System.gc();
    System.runFinalization();
    //輸出結果為null,表示對象已被回收
    System.out.println(wr.get());
  }
}

虛引用和引用隊列用法示例:

class Test{
  public static void main(String[] args){
    String str = new String("java");
    RefenceQueue rq = new RefenceQueue();
    //創建虛引用,使其引用str對象
    PhantomReference pr = new PhantomReference(str,rq);
    str = null;
    //取出虛引用wr所引用的對象,此處並不能獲取虛引用所引用的對象
    System.out.println(pr.get());
    System.gc();
    System.runFinalization();
    //垃圾回收後,虛引用將被放入引用隊列
    //取出引用隊列中最先進入隊列的引用與pr比較
    System.out.println(rq.poll() == pr);
  }
}

使用這些引用類就可以避免在程序執行期間將對象留在內存中。如果以軟引用、弱引用或虛引用的方式引用對象,垃圾回收器就能隨機地釋放對象。

由於垃圾回收的不確定性,當程序希望從軟、弱引用中取出引用對象時,可能這個對象已經被釋放。如果程序需要使用被引用的對象,則必須重新創建該對象。這個過程有如下兩種方式:

obj = wr.get();
if(obj == null){
  wr = new WeakRefence(recreatIt());////  1
  obj = wr.get();////////      2
}
//操作對象obj
//再次切斷obj與對象的關聯
obj = null
//方法二:
obj = wr.get();
if(obj == null){
  obj = recreatIt();
  wr = new WeakRefence(obj);
  //操作對象obj
  //再次切斷obj與對象的關聯
  obj = null
}

第一種方法,若垃圾回收機制在代碼1和2之間回收了弱引用的對象,那麽obj仍可能為null。而方法二不會出現這種情況。

JAVA核心技術筆記總結--第6章 抽象類、接口、內部類和Lambda表達式