Java核心技術筆記 接口、lambda表達式與內部類

《Java核心技術 卷Ⅰ》 第6章 接口、lambda表達式與內部類前端

  • 接口
  • 接口示例
  • lambda表達式
  • 內部類

接口

接口技術,這種技術主要用來描述類具備什麼功能,而並不給出每一個功能的具體實現。一個類能夠實現(implement)一個或多個接口,並在須要接口的地方,隨時使用實現了相應接口的對象。java

接口概念

在Java程序設計語言中,接口不是類,是對類的一組需求的描述,這些類要聽從接口描述的統一格式進行定義。git

Arrays類中的sort方法承諾能夠對對象數組進行排序,但要求知足下列條件,對象所屬的類**必須實現了Comparable接口。程序員

public interface Comparable {
  int compareTo(Object other) } 複製代碼

這就是說,任何實現Comparable接口的類都須要包含compareTo方法,而且這個方法的參數必須是一個Object對象,返回一個整型數值;好比調用x.compareTo(y)時,當x小於y時,返回一個負數;當x等於y時,返回0;不然返回一個正數。github

在 Java SE 5中,Comparable接口改進爲泛型類型。數組

public interface Comparable<T> {
  int compareTo(T other); // 參數擁有類型T
}
複製代碼

例如在實現Comparable<Employee>接口類中,必須提供int compareTo(Employee other)方法。安全

接口中的全部方法自動地屬於public,所以,在接口中聲明方法時,沒必要提供關鍵字public數據結構

  • 接口能夠包含多個方法
  • 接口中能夠定義常量
  • 接口中不能含有實例域
  • Java SE 8 以前,不能在接口中實現方法

提供實例域和方法實現的任務應該由實現接口的那個類來完成。閉包

在這裏能夠將接口當作是沒有實例域的抽象類。併發

如今但願用Arrays類的sort方法對Employee對象數組進行排序,Employee類必須實現Comparable接口。

爲了讓類實現一個接口,一般須要下面兩個步驟:

  • 將類聲明爲實現給定的接口
  • 對接口中的全部方法進行定義

將類聲明爲實現某個接口,使用關鍵字implements

class Employee implements Comparable {
  ...
  public int compareTo(Object otherObject) {
    Employee other = (Employee) otherObject;
    return Double.compare(salary, other.salary);
  }
  ...
}
複製代碼

這裏使用了靜態Double.compare方法,若是第一個參數小於第二個參數,它會返回一個負值,相等返回0,不然返回一個正值。

雖然在接口聲明中,沒有將compareTo方法聲明爲publuc,這是由於接口中全部方法都自動地是public,可是,在實現接口時,必須把方法聲明爲public,不然編譯器將認爲這個方法的訪問屬性是包可見性,即類的默認訪問。

能夠爲泛型Comparable接口提供一個類型參數。

class Employee implements Comparable<Employee> {
  ...
  public int compareTo(Employee other) {
    return Double.compare(salary, other.salary);
  }
  ...
}
複製代碼

爲何不能再Employee類直接提供一個compareTo方法,而必須實現Comparable接口呢?

主要緣由是Java是一種強類型(strongly type)語言,在調用方法時,編譯器將會檢查這個方法是否存在。

在sort方法通常會用到compareTo方法,因此編譯器必須確認必定有compareTo方法,若是數組元素類實現了Comparable接口,就能夠確保擁有compareTo方法。

接口的特性

接口不是類,尤爲不能用new實例化接口:

x = new Comparable(...); // Error
複製代碼

儘管不能構造接口的對象,卻能聲明接口的變量:

Comparable x; // OK
複製代碼

接口變量必須引用實現了接口的類對象:

x = new Employee(...); // OK
複製代碼

也可使用instanceof檢查一個對象是否實現了某個特定的接口:

if(x instanceof Comparable) { ... }
複製代碼

與類的繼承關係同樣,接口也能夠被擴展。

這裏容許存在多臺從具備較高通用性的接口到較高專用性的接口的鏈。

假設有一個稱爲Moveable的接口:

public interface Moveable {
  void move(double x, double y);
}
複製代碼

而後,能夠以它爲基礎擴展一個叫作Powered的接口:

public interface Powered extends Moveable {
  double milesPerGallon();
}
複製代碼

雖然接口中不能包含實例域或者靜態域,可是能夠定義常量:

public interface Powered extends Moveable {
  double milesPerGallon();
  double SPEED_LIMIT = 95;
  // a public static final constant
}
複製代碼

與接口中的方法自動設置爲public同樣,接口中的域被自動設爲public static final

儘管每一個類只能擁有一個超類,但卻實現多個接口

class Employee implements Coneable, Comparable { ... }
複製代碼

接口與抽象類

你可能會問:爲何這些功能不能由一個抽象類實現呢?

由於使用抽象類表示通用屬性存在這樣的問題:每一個類只能擴展於一個類,沒法實現一個類實現多個接口的需求。

class Employee extends Person implements Comparable { ... }
複製代碼

靜態方法

在 Java SE 8 中,容許在接口中增長靜態方法。
雖說這沒有什麼不合法的,只是這有違接口做爲抽象規範的初衷。

一般的作法是將靜態方法放在伴隨類中。在標準庫中,有成對出現的接口和實用工具類,如Collection/CollectionsPath/Paths

雖然Java庫都把靜態方法放到接口中也是不太可能,可是實現本身接口時,不須要爲實用工具方法另外提供一個伴隨類。

默認方法

能夠爲接口方法提供一個默認實現,必須用default修飾符標記方法。

public interface Comparable<T> {
  default int compareTo(T other) { return 0; }
}
複製代碼

默認方法的一個重要用法是接口演化(interface evolution)。

Collection接口爲例,這個接口做爲Java的一部分已經好久了,假如好久之前提供了一個類:

public class Bag implements Collection { ... }
複製代碼

後來,在Java SE 8中,爲這個接口增長了一個stream方法。若是stream方法不是默認方法,那麼Bag類將不能編譯——由於它沒有實現這個新方法。

爲接口增長一個非默認方法不能保證「源代碼兼容」(source compatible)。

解決默認方法衝突

若是一個接口中把方法定義爲默認方法,而後又在超類或另外一個接口中定義了一樣的方法,會發生什麼狀況?

解決這種二義性,Java的規則是:

  • 超類優先,若是超類本身提供了一個具體方法,同名且有相同參數類型的默認方法會被忽略
  • 接口衝突,若是一個超接口提供了一個默認方法,另外一個接口提供了一個同名並且參數類型相同的方法,必須覆蓋這個方法來解決衝突

着重看一下第二個規則,考慮另外一個包含getName方法的接口:

interface Named {
  default String getName() {
    return getClass().getName() + "_" + hashCode();
  }
}
複製代碼

如今有一個類同時實現了這兩個接口,這個時候須要程序員來解決這個二義性,在這個實現的方法中提供一個接口的默認getName方法。

class Student implements Person, Named {
  public String getName() {
    return Person.super.getName();
  }
}
複製代碼

就算Named接口並無getName的默認方法,一樣須要程序員去解決這個二義性問題。

上面的是兩個接口的命名衝突

如今考慮另外一種狀況:一個類擴展了一個超類,同時實現了一個接口,並從超類和接口繼承了相同的方法。

class Student extends Person implements Named { ... }
複製代碼

這種狀況下只會考慮超類的方法,接口全部默認方法會被忽略。

接口示例

接口與回調

回調(callback),能夠指出某個特定事件時應該採起的動做。

java.swing包中有一個Timer類,可使用它在到達給定的時間間隔發送通告。

在構造定時器時,須要設置一個時間間隔,並告知定時器,達到時間間隔時須要作什麼。

其中一個問題就是如何告知定時器作什麼?在不少語言中,是提供一個函數名,可是,在Java標準類庫中的類採用的是面向對象方法,它將某個類的對象傳遞給定時器,而後定時器調用這個對象的方法。

定時器須要知道調用哪個方法,並要求傳遞的對象所屬的類實現了java.awt.event包的ActionListener接口:

public interface ActionListener {
  void actionPerformed(ActionEvent event);
}
複製代碼

當到達指定時間間隔,定時器就調用actionPerformed方法。

使用這個接口的方法:

class TimePrinter implements ActionListener {
  public void actionPerformed(ActionEvent event) {
    System.out.println(...);
    ...
  }
}
複製代碼

其中接口方法的ActionEvent參數提供了事件的相關信息。

接下來構造類的一個對象,並傳遞給Timer構造器。

ActionListener listener = new TimePrinter()
Timer t = new Timer(10000, listener);
t.start(); // 啓動定時器
複製代碼

Comparator接口

能夠對一個字符串數組排序,是由於String類實現了Comparable<String>,並且String.compareTo方法能夠按字典順序比較字符串。

如今須要按長度遞增的順序對字符串進行排序,咱們確定不能對String進行修改,就算能夠修改咱們也不能讓它用兩種不一樣的方式實現compareTo方法。

要處理這種狀況,Arrays.sort方法還有第二個版本,一個數組和一個比較器(comparator)做爲參數,比較器實現了Comparator接口的類的實例。

public interface Comparator<T> {
  int compare(T first, t second);
}
複製代碼

按字符串長度比較,能夠定義一個實現Comparator<String>的類:

class LengthComparator implements Comparator<String> {
  public int compare(String first, String second) {
    return first.length() - second.length();
  }
}
複製代碼

具體比較時,創建一個實例:

Comparator<String> comp = new LengthComparator();
// comp.compare(words[i], words[j])
Arrays.sort(friends, comp);
複製代碼

對象克隆

Cloneable接口,指示一個類提供了一個安全的clone方法。

Employee original = new Employee(...);
Employee copy = original.clone();
copy.raiseSalary(10); // no changes happen to original
複製代碼

clone方法是Object的一個protected方法,代碼不能直接調用這個方法(指的是Object的這個方法)。

固然,只有Employee類能夠克隆Employee對象,可是默認的克隆操做是淺拷貝,即並無克隆對象中引用的其餘對象

淺拷貝可能會產生問題麼?這取決於具體狀況:

  • 原對象和淺克隆對象共享的子對象是不可變的,那麼這種共享就是安全的
  • 在對象的生命期中,子對象一直包含不變的常量,沒有更改器方法會改變它,也沒有方法會生成它的引用,這種狀況下也是安全的

通常來講子對象都是可變的,因此須要定義clone方法來創建一個深拷貝,同時克隆全部子對象。

對於每個類,須要肯定:

  1. 默認的clone是否知足要求
  2. 是否能夠在可變子對象上調用clone來修補默認clone
  3. 是否不應使用clone

實際上第3個選項是默認選項(這句話沒有太讀懂)。

若是選第1個或者第2個,類必須:

  1. 實現Cloneable接口
  2. 從新定義clone方法,並指定public訪問修飾符

子類雖然能夠訪問Object受保護的clone方法,可是子類只能調用受保護的clone方法來克隆它本身的對象

必須從新定義clonepublic,才能容許全部方法克隆對象。

Cloneable接口是一組標記接口,其餘接口通常確保一個類實現一個或一組特定的方法,標記接口不包含任何方法,它的惟一做用就是容許在類型查詢中使用instanceof

即時clone的默認(淺拷貝)實現可以知足要求,仍是須要實現Cloneable接口,將clone從新定義爲public,再調用super.clone()

class Employee implements Cloneable {
  // raise visibility level to public, change return type
  public Employee clone() throws CloneNotSupportedExcption {
    return (Employee) super.clone();
  }
}
複製代碼

與淺拷貝相比,這個clone並無增長任何功能,只是讓方法變爲公有,要創建深拷貝。

class Employee implements Cloneable {
  ...
  public Employee clone() throws CloneNotSupportedExcption {
    // Obejct.clone()
    Employee cloned = (Employee) super.clone();
    //clone mutable fields
    cloned.hireDay = (Date) hireDay.clone();
    return cloned;
  }
}
複製代碼

若是一個對象調用clone,但這個對象類沒有實現Cloneable接口,Objectclone方法就會拋出一個CloneNotSupportedExcptionEmployeeDate類實現了Cloneable接口,因此不會拋出異常,可是編譯器並不知道這點,因此聲明異常最好還要加上捕獲異常。

class Employee implements Cloneable {
  // raise visibility level to public, change return type
  public Employee clone() throws CloneNotSupportedExcption {
    try
    {
      Employee cloned = (Employee) super.clone();
      ...
    }
    catch(CloneNotSupportedExcption e) { return null; }
    // 由於實現了Cloneable,因此這並不會發生
  }
}
複製代碼

必須小心子類的克隆

例如,一旦Employee類定義了clone,那麼就能夠用它來克隆Manager對象(由於在Employee類中的clone已是public了,能夠直接使用Manager.clone())。

Employeeclone必定能完成克隆Manager對象的工做麼?

這取決於Manager類的域:

  • 若是是基本類型域,那沒有問題
  • 若是是須要深拷貝或者不可克隆域,不能保證子類的實現者必定會修正clone方法讓它正常工做

出於後者的緣由,在Object類中的clone方法聲明protected

lambda表達式

一種表示在未來某個時間點執行的代碼塊的簡潔方法。

使用lambda表達式,能夠用一種精巧而簡潔的方式表示使用回調或變量行爲的代碼。

爲何引入lambda表達式

lambda表達式是一個可傳遞的代碼塊,能夠在之後執行一次或屢次。

以前的監聽器和後面的排序比較例子的共同點是:都是把一個代碼塊傳遞到某個對象(定時器或者是sort方法),而且這個代碼塊會在未來某個時間調用。

lambda表達式的語法

考慮以前的按字符串長度排序例子:

first.length() - second.length()
複製代碼

Java是一種強類型語言,因此還要指定他們的類型:

(String first, String second)
  -> first.length() - second.length()
  // 隱式return 默認返回這個表達式的結果
複製代碼

這就是一個lambda表達式,一個代碼塊以及變量規範。

若是代碼要完成的計算不止一條語句,能夠像寫方法同樣,把代碼放在{}中,幷包含顯式的return語句。

(String first, String second) ->
  {
    if(first.length() < second.length()) return -1;
    else if(first.length() > second.length()) return 1;
    else return 0;
  }
複製代碼

一些省略形式的表達:

  • 若是沒有參數,仍要提供空括號
  • 若是編譯器能夠推導出參數類型,能夠省略類型聲明
  • 若是隻有一個參數,而且參數類型能夠推導,則能夠省略小括號

須要注意的地方:

  • 不須要指定返回類型,返回類型老是由上下文推導出(通常在賦值語句裏)
  • 若是表達式裏只要有一個顯式return,那就要確保每一個分支都有一個return,不然是不合法的

函數式接口

Java中已經有不少封裝代碼塊的接口,好比ActionListenerComparator,lambda表達式與這些接口兼容。

對於只有一個抽象方法的接口,須要這種接口的對象時,就能夠提供一個lambda表達式,這種接口稱爲函數式接口(functional interface)。

考慮以前的Arrays.sort方法,其中第二個參數須要一個Comparator實例,函數式接口使用:

Arrays.sort(words,
  (first, second) -> first.length() - second.length());
複製代碼

在底層,Arrays.sort方法會接收實現了Comparator<Strng>的某個類的對象,在這個對象上調用compare方法會執行這個lambda表達式的體。

最好把lambda表達式看做一個函數,而不是一個對象,並且要接收lambda表達式能夠傳遞到函數式接口

lambda表達式能夠轉換爲接口,這讓lambda表達式頗有吸引力,具體的語法很簡單:

Timer t = new Timer(10000, event ->
  {
    System.out.println(...);
    ...
  });
複製代碼

與使用實現了ActionListener接口的類相比,這個代碼可讀性好不少。

實際上,在Java中,對lambda表達式所能作的也只是能轉換爲函數式接口,甚至不能把lambda表達式賦給類型爲Object的變量,Object不是一個函數式接口。

方法引用

有時,可能已經有現成的方法能夠完成你想要傳遞到其餘代碼的某個動做。

好比只要出現一個定時器事件就打印這個事件對象:

Timer t = new Timer(10000, event -> System.out.println(event));
複製代碼

可是若是直接把println方法傳遞給Timer構造器就更好了:

Timer t = new Timer(10000, System.out::println);
複製代碼

表達式System.out::println就是一個方法引用(method reference),它等價於lambda表達式x - > System.out.println(x)

考慮一個排序例子:

Arrays.sort(words, String::compareToIgnoreCase);
複製代碼

主要有3種狀況:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

前面兩種等價於提供方法參數的lambda表達式,好比System.out::println等價於x -> System.out.println(x),以及Math::power等價於(x, y) -> Math.power(x, y)

對於第3種,第1個參數會成爲方法的目標,例如String::compareToIgnoreCase等價於(x, y) -> x.compareToIgnoreCase(y)

能夠在方法引種中使用thissuper也是合法的,好比super::instanceMethod,使用this做爲目標,會調用給定方法的超類版本。

構造器引用

構造器引用與方法引用相似,只不過方法名爲new,例如Person::newPerson構造器的一個引用,具體選擇Person多個構造器中的哪個,這個取決於上下文。

如今有一個字符串列表,你能夠把它轉換爲一個Person對象數組,爲此要在各個字符串上調用構造器。

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
複製代碼

streammapcollect方法會在卷Ⅱ的第1章討論。

如今的重點是map方法會爲各個列表元素調用Person(String)構造器,這裏編譯器從上下文推導出這是在對一個字符串調用構造器。

能夠用數組類型創建構造器引用,int[]::new是一個構造器引用,有一個參數,就是數組的長度,這等價於x -> new int[x]

Java有一個限制:沒法構造泛型類型T的數組。

數組構造器引用對於克服這個限制頗有用。

假設須要一個Person對象數組,Stream接口有一個toArray方法能夠返回Object數組:

Object[] people = stream.toArray();
複製代碼

可是用戶想要一個Person引用數組,流庫利用構造器引用解決了這個問題:

Person[] people = stream.toArray(Person[]::new);
複製代碼

toArray方法調用構造器得到一個正確類型的數組,而後填充這個數組並返回。

變量做用域

一般可能想在lambda表達式中訪問外圍方法或類中的變量

public static void repeatMessage(String text, int delay) {
  ActionListener listener = event ->
    {
      System.out.println(text);
      ...
    };
  new Timer(delay, listener).start();
}
複製代碼

具體調用:

repeatMessage("Hello", 1000);
複製代碼

lambda表達式中的變量text,並非在這個lambda表達式中定義的,可是這其實有問題,由於代碼可能會調用返回好久之後才運行,而那時這個參數變量已經不存在了,該如何保留這個變量?

重溫一下lambda表達式的3個部分:

  1. 一個代碼塊
  2. 參數
  3. 自由變量的值,指非參數而且不在代碼中定義的變量

上面的例子中有1個自由變量text

表示lambda表達式的數據結構必須存儲自由變量的值,也被叫作自由變量被lambda表達式捕獲(captured)。

能夠把一個lambda表達式轉換爲包含一個方法的對象,這樣自由變量的值就會複製到這個對象的實例變量中。

關於代碼塊以及自由變量有一個術語:閉包(closure),Java中lambda表達式就是閉包。

在lambda表達式中,只能引用值不會改變的變量,好比下面這種就是不合法的:

public static void countDown(int start, int delat) {
  ActionListener listener = event ->
    {
      start--; // Error: Can't mutate captured variable
      System.out.println(text);
      ...
    };
  new Timer(delay, listener).start();
}
複製代碼

若是在lambda表達式中改變變量,併發執行多個操做時就會不安全(具體要見第14章併發)。

另外若是在lambda表達式中引用變量,而且這個變量在外部改變,這也是不合法的:

public static void repeat(String text, int count) {
  for(int i = 1; i <= count; i++)
  {
    ActionListener listener = event ->
      {
        System.out.println(i + ":" + text);
        // Error: Can't refer to changing i
        ...
      };
    new Timer(1000, listener).start();
  }
}
複製代碼

因此簡單來講規則就是:lambda表達式中捕獲的變量必須其實是最終變量(effectively final),即這個變量初始化以後就再也不賦新值

lambda表達式的體與嵌套塊有相同的做用域,因此在lambda表達式中聲明與一個局部變量同名的參數或局部變量是不合法的。

Path first = Path.get("/usr/bin");
Comparator<String> comp =
  (first, second) -> fisrt.length() - second.length();
  // Error: Variable first already defined
複製代碼

固然在lambda表達式中也不能有同名的局部變量。

在lambda表達式中使用this關鍵字時,是指建立這個lambda表達式的方法的this參數

public class Application() {
  public void init() {
    ActionListener listener = event ->
      {
        System.out.println(this.toString());
        ...
      }
  }
}
複製代碼

this.toString()會調用Application對象的toString方法,而不是ActionListener實例的方法,因此在lambda表達式中this的使用並無什麼特殊之處。

內部類

內部類(inner class)定義在另外一個類的內部,其中的方法能夠訪問包含它們的外部類的域。

內部類主要用於設計具備相互協做關係的類集合。

使用內部類的主要緣由:

  1. 內部類方法能夠訪問該類定義所在的做用域中的數據,包括私有數據
  2. 內部類能夠對同一個包中的其餘類隱藏起來
  3. 想定義一個回調函數且不想編寫大量代碼時,使用匿名(anonymous)內部類比較便捷。

將從如下幾部分介紹內部類:

  1. 簡單的內部類,它將訪問外圍類的實例域
  2. 內部類的特殊語法規則
  3. 內部類的內部,探討如何轉換成常規類
  4. 討論局部內部類,它能夠訪問外圍做用域中的局部變量
  5. 介紹匿名內部類,說明Java在lambda表達式以前怎麼實現回調的
  6. 介紹如何將靜態內部類嵌套在輔助類中

內部類訪問對象狀態

內部類的語法比較複雜。

選擇一個簡單的例子:

public class TalkingClock {
  private int interval;
  private boolean beep;

  public TalkingClock(int interval, boolean beep) { ... }
  public void start() { ... }

  // an inner class
  public class TimePrinter implements ActionListener {
    public void actionPerformed(ActionEvent event) {
      System.out.println(...);
      if(beep) Toolkit.getDefaultToolkit().beep();
    }
  }
}
複製代碼

TimePrinter類位於TalkingClock類內部,並不意味着每一個TalkingClock對象都有一個TimePrinter實例域。

TimePrinter類沒有實例域或者beep變量,而是引用了外部類的域裏的beep

其實內部類的對象總有一個隱式引用,它指向了建立它的外部類對象,這個引用在內部類的定義中不可見。

這個引用是在對象建立內部類對象的時候傳入的this,編譯器經過內部類的構造器傳入到內部類對象的域中。

// 由編譯器插入的語句
ActionListener listener = new TimePrinter(this);
複製代碼

TimePrinter類能夠聲明爲私有的,這樣只有TalkingClock方法才能構造TimePrinter對象。只有內部類能夠是私有的,常規類只能夠是包可見和公有可見。

內部類的特殊語法規則

使用外圍類引用的語法爲OuterClass.this

例如以前的actionPerformed方法:

public void actionPerformed(ActionEvent event) {
  ...
  if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}
複製代碼

反過來,能夠用`outerObject.new InnerClass(construction parameters)更加明確地編寫內部類對象的構造器:

// ActionListener listener = new TimePrinter(this);
ActionListener listener = this.new TimePrinter();
複製代碼

一般來講this限定詞是多餘的,可是能夠經過顯式命名將外圍類引用設置爲其餘對象,好比當TimePrinter是一個公有內部類時,對於任意的語音時鐘均可以構造一個TimePrinter

TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.ActionListener listener = jabberer.new TimePrinter();
複製代碼

上面的狀況是在外圍類的做用域以外,因此引用的方法是OuterClass.InnerClass

注意:內部類中聲明的全部靜態域都必須是final,由於咱們但願一個靜態域只有一個實例。不過對於每一個外部對象,會分別有一個單獨的內部類實例,若是這個域不是final,它可能就不是惟一的。

局部內部類

若是一個類只在一個方法中建立了對象,能夠這個方法中定義局部類。

public void start() {
  class TimePrinter implements ActionListener {
    public void actionPerformed(ActionEvent event) { ... }
  }

  ActionListener listener = new TimePrinter();
  Timer t = new Timer(interval, listener);
  t.start();
}
複製代碼

局部類不能用publicprivate,它的做用域被限定在生命這個局部類的塊中。

可是有很是好的隱蔽性,除了start方法,沒有任何方法知道TimePrinter類的存在。

由外部方法訪問變量

局部類還有一個優勢:他們還能訪問局部變量,可是這些局部變量必須是final,即一旦賦值就不會改變。

下面的例子相比以前進行了一些修改,beep再也不是外部類的一個實例域,而是方法傳入的參數變量:

public void start(int interval, final boolean beep) {
  class TimePrinter implements ActionListener {
    public void actionPerformed(ActionEvent event) {
      ...
      if(beep) ...;
      ...
    }
  }

  ActionListener listener = new TimePrinter();
  Timer t = new Timer(interval, listener);
  t.start();
}
複製代碼

先說明一下這裏的控制流程:

  1. 調用start(int, boolean)
  2. 調用局部內部類TimePrinter的構造器,初始化listener
  3. listener引用傳遞給Timer構造器
  4. 定時器t開始計時
  5. start(int, boolean)方法結束,此時beep參數變量不復存在
  6. 某個時刻actionPerformed方法執行if(beep) ...

爲了讓actionPerformed正常運行,TimePrinter類在beep域釋放以前將內部類中要用到的beep域用start方法的局部變量beep進行備份(具體實現方式是編譯器給內部類添加了一個final域用來保存beep)。

編譯器檢測對局部變量的訪問,爲每個量創建相應的數據域,並將局部變量拷貝到構造器中,以便將這些數據域初始化爲局部變量的副本

至於beep參數前的final,是由於局部類的方法只能引用定義爲final的局部變量,從而使得局部變量與局部類中創建的拷貝保持一致。

匿名內部類

假設只建立這個局部類的一個對象,就沒必要命名了,這種類稱爲匿名內部類(anonymous inner class)。

public void start(int interval, boolean beep) {
  ActionListener listener = new ActionListener()
  {
    public void actionPerformed(ActionEvent event) { ... }
  };
  Timer t = new Timer(interval, listener);
  t.start();
}
複製代碼

這種語法的含義是:建立一個實現AcitonListener接口的類的新對象,須要實現的方法定義在括號內。

一般的語法格式爲:

new SuperType(construction parameters)
  {
    methods and data
  }
複製代碼

SuperType能夠是一個接口,也能夠是一個類。

因爲構造器必需要有一個名字,因此匿名類不能有構造器,取而代之的是:

  • SuperType是一個超類時,將構造器參數傳遞給超類構造器
  • SuperType是一個接口時,不能有任何構造參數(括號()仍是要保留的)

構造一個類的新對象,和構造一個擴展這個類的匿名內部類的對象的區別:

Person queen = new Person("Mary");
Person count = new Person("Dracula") { ... };
複製代碼

多年來,Java程序員習慣用匿名內部類實現事件監聽器和其餘回調,現在最好仍是使用lambda表達式,好比:

public void start(int interval, boolean beep) {
  Timer t = new Timer(interval, event -> { ... });
  t.start();
}
複製代碼

可見,用lambda表達式寫會簡潔得多。

雙括號初始化

若是想構造一個數組列表,並傳遞到一個方法:

ArrayList<String> friends = new ArrayList<>();
friends.add("Harry");
friends.add("Tony");
invite(friends);
複製代碼

若是以後都沒有再須要這個數組列表,那麼最好使用一個匿名列表解決。

invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }};
複製代碼

注意這裏的雙括號:

  • 外層括號創建了ArrayList的一個匿名子類
  • 內層括號則是一個對象構造塊(見第4章)

靜態內部類

有時使用內部類只是爲了把一個類隱藏在另外一個類的內部,並不須要內部類引用外圍類對象,爲此能夠將內部類聲明static,取消產生的引用。

編寫一個方法同時計算出最大最小值:

double min = Double.POSITIV_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for(double v : values)
{
  if (min > v) min = v;
  if (max < v) max = v;
}
複製代碼

然而必須返回兩個數值,能夠頂一個包含兩個值的類Pair

class Pair {
  private double first;
  private double second;
  public Pair(double f, double s) {
    first = f;
    second = s;
  }
  public double getFirst() { return first; }
  public double getSecond() { return second; }
}
複製代碼

minmax方法能夠返回一個Pair類型的對象。

class ArrayAlg {
  public static Pair minmax(double[] values) {
    ...
    return new Pair(min, max);
  }
}
複製代碼

而後調用ArrayAlg.minmax得到最大最小值:

Pair p = ArrayAlg.minmax(data);
複製代碼

可是Pair是一個比較大衆化的名字,容易出現名字衝突,解決的方法是將Pair定義爲ArrayAlg的內部公有類,而後用ArrayAlg.Pair訪問它:

ArrayAlg.Pair p = ArrayAlg.minmax(data);
複製代碼

不過與前面的例子不一樣,Pair對象不須要引用任何其餘的對象,因此能夠把這個內部類聲明爲static

class ArrayAlg {
  public static class Pair { ... }
  ...
}
複製代碼

只有內部類能夠聲明爲static,靜態內部類的對象除了沒有對生成它的外圍類對象的引用特權外,其餘與全部內部類徹底同樣。

在上面的例子中,必須使用靜態內部類,這是由於返回的內部類對象是在靜態方法minmax中構造的。

若是沒有把Pair類聲明爲static,那麼編譯器將會給出錯誤報告:沒有可用的隱式ArrayAlg類型對象初始化內部類對象。

  • 註釋1:在內部類不須要訪問外圍類對象時,應該使用靜態內部類。
  • 註釋2:與常規內部類不一樣,靜態內部類能夠有靜態域和方法。
  • 註釋3:聲明在接口中的內部類自動成爲staticpublic類。

代理

代理(proxy),這是一種實現任意接口的對象。

利用代理能夠在運行時建立一個實現了一組給定接口新類

這種功能只有在編譯時沒法肯定須要實現哪一個接口時纔有必要使用。

對於應用程序設計人員來講,遇到的狀況不多,因此先跳過,若是後面有必要再開一個專題進行說明。

Java接口、lambda表達式與內部類總結

  • 接口概念、特性
  • 接口與抽象類
  • 靜態方法
  • 默認方法
  • 解決默認方法衝突
  • 接口示例
  • lambda表達式
  • 函數式接口
  • 方法引用
  • 構造器引用
  • lambda表達式變量總用域
  • 內部類
  • 局部內部類
  • 匿名內部類
  • 靜態內部類

我的靜態博客:

相關文章
相關標籤/搜索