1. 程式人生 > 實用技巧 >CORE JAVA 第六章 介面、lambda表示式和內部類

CORE JAVA 第六章 介面、lambda表示式和內部類

第六章 介面、lambda表示式與內部類

​ 介面(interface)技術主要用來描述類具有什麼功能,而並不給出每個功能的具體實現。一個類可以實現(implement)一個或多個介面,並在需要介面的地方,隨時使用實現了相應介面的物件。

​ lambda表示式是一種表示可以在將來某個時間點執行的程式碼塊的簡潔方法。使用lambda表示式,可以用一種精巧而簡潔的方式表示使用回撥或變數行為的程式碼。

​ 內部類(inner class)定義在另外一個類的內部,其中的方法可以訪問包含它們的外部類的域。內部類技術主要用於設計具有相互協作關係的類集合。

​ 代理(proxy),是一種實現任意介面的物件。

6.1 介面

6.1.1 介面概念

​ 介面不是類,而是對類的一組需求描述,這些類要遵從介面描述的統一格式進行定義。

​ “如果類遵從某個特定介面,那麼就履行這項服務”。

​ 例如:

Arrays類中的sort方法承諾可以對物件陣列進行排序,但要求滿足下列前提:物件所屬的類必須實現了Comparable介面。

​ 下面是Comparable介面的程式碼:

public interface Comparable
{
    int compareTo(Object other);
}

這就是說,任何實現Comparable介面的類都需要包含compareTo方法,並且這個方法的引數必須是一個Object物件,返回一個整型數值。

註釋:在Java SE 5.0中,Comparable介面已經改進為泛型型別。

​ 介面中的所有方法自動的屬於public。因此,在介面中宣告方法時,不必提供關鍵字public。

​ 介面可以包含一個或多個方法,還可以定義常量。

​ 介面絕不能含有例項域。在Java SE 8之前,也不能在介面中實現方法。提供例項域和方法實現的任務應該由實現介面的那個類來完成。

​ 為了讓類實現一個介面,通常需要下面兩個步驟:

  1. 將類宣告為實現給定的介面。
  2. 對介面中的所有方法進行定義。

要將類宣告為實現某個介面,需要使用關鍵字implements:

class Employee implements Comparable

以下是compareTo方法的實現:

public int compareTo(Object otherObject)
{
    Employee other = (Employee) otherObject;
    return Double.compare(salary, other.salary);
}
// 靜態Double.compare方法:如果第一個引數小於第二個引數,它會返回一個負值;二者相等,返回0;否則返回一個正值、

​ 在實現介面時,必須把方法宣告為public。

​ 為泛型Comparable介面提供一個型別引數。

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

這樣就不用對Object引數進行型別轉換了。

提示:Comparable介面中的compareTo方法將返回一個整型數值。如果兩個物件不相等,則返回一個正值或者一個負值。在對兩個整數域進行比較時,這點非常有用。例如,假設每個僱員都有一個唯一整數id,並希望根據ID對僱員進行重新排序,那麼就可以返回id - other.id。如果第一個ID小於另一個ID,則返回一個負值;如果兩個ID相等,則返回0;否則返回一個正值。但有一點需要注意:整數的範圍不能過大,以避免造成減法運算的溢位。如果能夠確信ID為非負整數,或者它們的絕對值不會超過(Inetger.MAX_VALUE-1)/2,就不會出現問題。否則,呼叫靜態Integer.compare方法。

​ 當然,這裡的減法技巧不適用於浮點值。可以使用Double.compare方法。

​ 要讓一個類使用排序服務,必須讓它實現compareTo方法。但是為什麼不能在Employee類直接提供一個compareTo方法,而必須實現Comparable介面呢?

​ 主要原因在於Java是一種強型別語言。在呼叫方法的時候,編譯器將會檢查這個方法是否存在。在sort方法中可能存在下面這樣的語句:

if (a[i].compareTo(a[j]) > 0)
{
    // rearrange a[i] and a[j]
    ...
}

為此,編譯器必須確認a[i]一定有compareTo方法。如果a是一個Comparable物件的陣列,就可以確保擁有compareTo方法,因為每個實現Comparable介面的類都必須提供這個方法的定義。

註釋:語言標準規定,對於任意的x和y,實現必須能夠保證sgn(x.compareTo(y)) = -sgn(y.compareTo(x))。這裡的sgn是一個數值的符號。如果n是負值,sgn(n) = -1;如果n是0,sgn(n) = 0;如果n是正值,sgn(n) = 1。

​ 與equals方法一樣,在繼承過程中有可能會出現問題。

​ 這是因為Manager擴充套件了Employee,而Employee實現的是Comparable,而不是Comparable。如果Manager覆蓋了compareTo方法,就必須要有經理與僱員進行比較的思想準備(即改變原方法的預期行為的具體實現方法),絕不能僅僅將僱員轉換成經理(像下面這樣)。

class Manager extends Employee
{
    public int compareTo(Employee other)
    {
        Manager otherManager = (Manager) other;	// NO!
    }
}

這樣的話不符合反對稱的規則。如果x是一個Employee物件,y是一個Manager物件,呼叫x.compareTo(y)不會丟擲異常,它只是將x和y都作為僱員進行比較。但是反過來,y.compareTo(x)將會丟擲一個ClassCastException。

​ 如果子類之間的比較含義不一樣,那就屬於不同類物件的非法比較。每個compareTo方法都應該在開始時進行下列檢測:

if (getClass() != other.getClass())	throw new ClassCastException();

​ 如果存在通用演算法,能夠對兩個不同的子類物件進行比較,則應該在超類中提供一個compareTo方法,並將這個方法宣告為final。

6.1.2 介面的特性

  • 介面不是類,尤其不能使用new運算子例項化一個介面(不能構造介面的物件):
x = new Comparable();//ERROR
  • 能宣告介面的變數:
Comparable x;	//OK

介面變數必須引用實現了介面的類物件:

x = new Employee();//OK provided Employee implements Comparable
  • 可以使用instanceof檢測一個物件是否實現了某個特定的介面:
if (anObject 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。

  • 有些介面只定義了常量,而沒有定義方法。
  • 儘管每個類只能夠擁有一個超類,但卻可以實現多個介面。

6.1.3 介面與抽象類

​ 使用抽象類表示通用屬性存在這樣一個問題:每個類只能擴充套件於一個類。但每個類可以實現多個介面。

6.1.4 靜態方法

​ 在Java SE 8中,允許在介面中增加靜態方法。

​ 目前為止,通常的做法都是將靜態方法放在伴隨類中。在標準庫中,你會看到成對出現的介面和實用工具類,如Collection/Collections或Path/Paths。

​ 實現自己的介面時,不再需要為實用工具方法另外提供一個伴隨類。

6.1.5 預設方法

​ 可以為介面方法提供一個預設實現。必須使用default修飾符標記這樣一個方法。

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

當然,這並沒有太大用處,因為Comparable的每一個實際實現都要覆蓋這個方法。不過有些情況下,預設方法可能很有用。比如在實現一個有很多方法的介面時,你只需要關心其中的1、2個方法,可以把其餘的方法設定成預設方法,什麼也不做(在Java SE 8中,可以把所有方法宣告為預設方法)。這樣,實現這個介面只需要為關心的方法進行覆蓋。

​ 預設方法可以呼叫任何其他方法。

​ 預設方法的一個重要用法是“介面演化”(interface evolution)。假設很久之前你提供了這樣一個類:

public class Bag implements Collection

後來,在Java SE 8中,又為Collection介面增加了一個stream方法。

​ 假設stream方法不是一個預設方法。那麼Bag類將不能編譯,因為它沒有實現這個新方法。為介面增加一個非預設方法不能保證“原始碼相容”。

​ 不過,假設不重新編譯這個類,而只是使用原先的一個包含這個類的JAR檔案。這個類仍能正常載入,儘管沒有這個新方法。程式仍然可以正常構造Bag例項,不會有意外發生。不過,如果程式在一個Bag例項上呼叫stream方法,就會出現一個AbstractMethodError。

​ 將方法實現為一個預設方法就可以解決這兩個問題。Bag類可以重新編譯。另外如果沒有重新編譯而直接載入這個類,並在一個Bag例項上呼叫stream方法,將呼叫Collection.stream方法。

6.1.6 解決預設方法衝突

​ 如果先在一個介面中將一個方法定義為預設方法,然後又在超類或另一個介面定義了同樣的方法,會發生什麼情況?規則如下:

  1. 超類優先。如果超類提供了一個具體方法,同名而且有相同引數型別的預設方法會被忽略。
  2. 介面衝突。如果一個超介面提供了一個預設方法,另一個介面提供了一個同名而且引數型別(不論是否是預設引數)相同的方法(不管是不是預設方法),必須覆蓋這個方法來解決衝突。

即如果至少有一個介面提供了一個實現,編譯器就會報告錯誤,而程式設計師就必須解決這個二義性。

​ 如果兩個介面都沒有為共享方法提供預設實現,那麼這裡不存在衝突。

警告:千萬不要讓一個預設方法重新定義Object類中的某個方法。例如,不能為toString或equals定義預設方法。由於類優先規則,這樣的方法絕對無法超越Object.toString或Objects.equals。

6.2 介面例項

6.2.1 介面與回撥

​ 回撥(callback)是一種常見的程式設計模式。在這種模式中,可以指出某個特定事件發生時應該採取的動作。例如,可以指出在按下滑鼠或選擇某個選單項時應該採取什麼行動。

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

​ 在構造定時器時,需要設定一個時間間隔,並告知定時器,當到達時間間隔時需要做些什麼操作。

​ 如何告知定時器做什麼呢?在Java中,將某個類的物件傳遞給定時器,然後定時器呼叫這個物件的方法。由於物件可以攜帶一些附加的資訊,所以傳遞一個物件比傳遞一個函式要靈活的多。

​ 當然,定時器需要知道呼叫哪一個方法,並要求傳遞的物件所屬的類實現了java.awt.event包的ActionListener介面。

public interface ActionListener
{
    void actionPerformed(ActionEvent event);
}

當到達指定的時間間隔時,定時器就呼叫actionPerformed方法。

​ 假設希望每隔10秒鐘列印一條資訊“At the tone,the time is……”,然後響一聲,就應該定義一個實現ActionListener介面的類,然後將需要執行的語句放在actionPerformed方法中。

class TimePrinter implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
        System.out.println("At the tone, the time is" + New Date());
        Toolkit.getDefaultToolkit().beep();
    }
}

actionPerformed方法的ActionEvent引數提供了事件的相關資訊,例如,產生這個事件的源物件。在這個程式中,事件的詳細資訊並不重要,因此,可以放心地忽略這個引數。

​ 接下來, 構造這個類的一個物件,並將它傳遞給Timer構造器。

// Timer構造器簽名:
Timer(int interval, ActionListener listener)
ActionListener listener = new TimePrinter();
Timer t = new Timer(10000, listener);	//第二個引數是監聽器物件

​ 最後,啟動定時器:

t.start();	//啟動定時器,一旦啟動成功,定時器將呼叫監聽器的actionPerformed。

每隔10秒鐘列印一條資訊“At the tone,the time is……”,然後響一聲。

6.2.2 Comparator介面

​ 之前,我們已經瞭解瞭如何對一個物件陣列排序,前提是這些物件是實現了Comparable介面的類的例項。例如可以對一個字串陣列排序,因為String類實現了Comparable,而且String.compareTo方法可以按字典順序比較字串。

​ 現在假設我們希望按長度遞增的順序對字串進行排序,而不是按字典順序進行排序。Arrays.sort方法還有第二個版本,有一個數組和一個比較器(comparator)作為引數,比較器是實現了Comparator介面的類的例項。

public interface Comparator<T>
{
    int compare(T first, T second);
}

要按長度比較字串,可以如下定義一個實現Comparator的類:

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

具體完成比較時,需要建立一個例項:

Comparator<String> comp = new LengthComparator();
if (comp.compare(words[i], words[j]) > 0) ……

這個compare方法要在比較器物件上呼叫,而不是在字串本身上(words[i].compareTo(words[j]))呼叫。

註釋:我們需要通過建立LengthComparator物件的一個例項來呼叫compare方法。

​ 要對一個數組排序,需要為Arrays.sort方法傳入一個LengthComparator物件:

String[] friends = {"Peter", "Paul", "Mary"};
Arrays.sort(friends, new LengthComparator());

6.2.3 物件克隆

​ Cloneable介面指示一個類提供了一個安全的clone方法。

​ 一個包含物件引用的變數建立副本時,原變數和副本都是同一個物件的引用。這說明,任何一個變數改變都會影響另一個變數。

Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10);	// also changed original

​ 如果希望copy是一個新物件,它的初始狀態與original相同,但是之後它們各自會有自己不同的狀態,這種情況下就可以使用clone方法。

Employee copy = original.clone();
copy.raiseSalary(10);	//  original unchanged
//Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
//	Type mismatch: cannot convert from Object to Employee
//	The method clone() from the type Object is not visible

​ 不過並沒有這麼簡單。clone方法是Object的一個protected方法,這說明你的程式碼不能直接呼叫這個方法。只有Employee類(在Employee類體裡)可以克隆Employee物件。這個限制是有原因的。想想看Object類如何實現clone。它對於這個物件一無所知,所以只能逐個域的進行拷貝。如果物件中的所有資料域都是數值或其他基本型別,拷貝這些域沒有任何問題。但是如果物件包含子物件的引用,拷貝域就會得到相同子物件的另一個引用,這樣一來,原物件和克隆的物件仍然會共享一些資訊。

​ 預設的克隆操作是淺拷貝,並沒有克隆物件中引用的其他物件。如果原物件和淺克隆物件共享的子物件是不可變的,那麼這種共享就是安全的,例如子物件屬於一個不可變的類,如String。或者在物件的生命期中,子物件一直包含不變的常量,沒有更改器方法會改變它,也沒有方法會生成它的引用,這種情況下同樣是安全的。

​ 不過通常子物件都是可變的,必須重新定義clone方法來建立一個深拷貝,同時克隆出所有子物件。

​ 對於每一個類,需要確定:

  1. 預設的clone方法是否滿足要求;
  2. 是否可以在可變的子物件上呼叫clone來修補預設的clone方法(深拷貝)。
  3. 是否不該使用clone。

​ 第三個選項是預設選項。如果選擇第1項或第2項,類必須:

  1. 實現Cloneable介面。
  2. 重新定義clone方法,並指定public修飾符。

註釋:clone方法是Object的一個protected方法,這說明你的程式碼不能直接呼叫anObject.clone()。子類只能呼叫受保護的clone方法來克隆它自己的物件。必須重新定義clone為public才能允許所有方法克隆物件。

​ 在這裡,Cloneable介面的出現與介面的正常使用並沒有關係。具體來說,它沒有指定clone方法,這個方法是從Object類繼承的。這個介面只是作為一個標記,指示類設計者瞭解克隆過程。如果一個物件請求克隆,但沒有實現這個介面,就會生成一個受查異常。

註釋:Cloneable介面是Java提供的一組標記介面之一。標記介面不包含任何方法;它唯一的作用就是允許在型別查詢中使用instanceof。

​ 即使clone的預設(淺拷貝)實現能夠滿足要求,還是需要實現Cloneable介面,將clone重新定義為public,再呼叫super.clone()。

class Employee implements Cloneable
{
    // raise visibility level to public
    public Employee clone() throws CloneNotSupportedException
    {
        return (Employee) super.clone();
    }
}

註釋:在Java SE 1.4之前,clone方法的返回型別總是Object,而現在可以為你的clone方法指定正確的返回型別。這是協變返回型別的一個例子。

協變返回型別:

​ 在面向物件程式設計中,協變返回型別指的是子類中的成員函式的返回值型別不必嚴格等同於父類中被重寫的成員函式的返回值型別,而可以是更 "狹窄" 的型別。

​ Java 5.0添加了對協變返回型別的支援,即子類覆蓋(即重寫)基類方法時,返回的型別可以是基類方法返回型別的子類。協變返回型別允許返回更為具體的型別。

​ 與Object.clone提供的淺拷貝相比,前面看到的clone方法並沒有為它增加任何功能。這是隻是讓這個方法是公有的。要建立深拷貝,需要克隆物件中可變的例項域。

class Employee implements Cloneable
{
    ……
    public Employee clone() throws CloneNotSupportedException
    {
        // call Object.clone()
        Employee cloned = (Employee) super.clone();
        
        // clone mutable fields
        cloned.hireDay = (Date) hireDay.clone();
        
        return cloned;
    }
}

​ 如果在一個物件上呼叫clone,但這個物件的類並沒有實現Cloneable介面,Object類的clone方法就會丟擲一個CloneNotSupportedException。

​ 必須當心子類的克隆。子類可能會有需要深拷貝或不可克隆的域。不能保證子類的實現者一定會修正clone方法讓它正常工作。出於這個原因,在Object類中clone方法宣告為protected。

​ 所有的陣列型別都有一個public的clone方法,而不是protected。建立一個新陣列,包含原陣列的所有副本。

int[] nums = {2, 3, 5, 7, 11, 13};
int[] cloned = nums.clone();
cloned[5] = 12;	// doesn't change nums[5]

6.3 lambda表示式

​ lambda表示式,採用一種簡潔的語法定義程式碼塊。

6.3.1 為什麼引入lambda表示式

​ lambda表示式是一個可傳遞的程式碼塊,可以在以後執行一次或多次。

​ 首先回憶一下我們在哪些地方傳遞過程式碼塊:

  1. ActionListener的actionPerformed方法包含希望以後執行的程式碼。將例項提供到一個Timer物件。
  2. 用一個定製比較器完成排序,按照長度對字串排序,可以向sort方法傳入一個Comparator物件。Comparator中的compare方法不是立即呼叫。實際上,在陣列完成排序之前,sort方法會一直呼叫compare方法。

這兩個例子都是將一個程式碼塊傳遞到某個物件(一個定時器,或者一個sort方法)。這個程式碼塊會在將來某個時間呼叫。

​ 到目前為止,不能直接傳遞程式碼段。Java是一種面嚮物件語言,所以必須構造一個物件,這個物件的類需要有一個方法能包含所需的程式碼。

6.3.2 lambda表示式的語法

​ 考慮上一節討論的排序的例子。傳入程式碼檢測一個字串是否比另一個字串短。這裡要計算:

first.length() - second.length()

​ first和second都是字串。Java是一種強型別語言,所以我們還要指定它們的型別:

(String first, String second)
	-> first.length() - second.length();

​ 以上就是lambda表示式。lambda表示式就是一個程式碼塊,以及必須傳入程式碼的變數規範。

​ 以上就是一種lambda表示式形式:引數,箭頭(->)以及一個表示式。

​ 如果程式碼要完成的計算無法放在一個表示式中,就可以像寫方法一樣,把這些程式碼放在{}中,幷包含顯示的return語句。

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

​ 若lambda表示式沒有引數,仍然要提供空括號,像無引數方法一樣:

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

​ 如果可以推匯出一個lambda表示式的引數型別,則可以忽略其型別。

Comparator<String> comp 
    = (first, second)
    	-> first.length() - second.length();

在這裡,編譯器可以推匯出first和second必然是字串,因為這個lambda表示式將賦值給一個字串比較器。

​ 如果方法只有一個引數,而且這個引數的型別可以推導得出,那麼甚至還可以省略小括號。

Actionlistener listener = event ->
    System.out.println("The time is " + new Date());

​ 無需指定lambda表示式的返回型別。lambda表示式的返回型別總是會由上下文推導得出。

註釋:如果一個lambda表示式只在某些分支返回一個值,而在另外一些分支不返回值,是不合法的。

​ 下面顯示如何在一個比較器和一個動作監聽器中使用lambda表示式。

//planets為陣列名
//Arrays.sort(planets, new LengthComparator());
Arrays.sort(planets,(first, second) -> first.length() - second.length());

//Timer t = new Timer(10000, listener);
Timer t = new Timer(10000, event -> System.out.println("The time is " + new Date()));

個人理解:

lambda表示式的作用就是代替方法中的函式式介面的物件引數,其中物件的某個方法會在將來執行一次或多次。

lambda表示式的內容就是方法中的內容。

​ 雖然使用 Lambda 表示式可以對某些介面進行簡單的實現,但並不是所有的介面都可以使用 Lambda 表示式來實現。Lambda 規定介面中只能有一個需要被實現的方法,不是規定介面中只能有一個方法

​ 可以把lambda表示式賦值給介面變數。因為lambda表示式可以轉換為介面,下文會提到。

6.3.3 函式式介面

​ Java中有很多封裝程式碼塊的介面,如ActionListener或Comparator。lambda表示式與這些介面是相容的。

​ 對於只有一個抽象方法的介面,需要這種介面的物件時,就可以提供一個lambda表示式。這種介面稱為函式式介面

lambda表示式轉換為函式式介面

​ 展示如何轉換為函式式介面,考慮Arrays.sort方法,它的第二個引數需要一個Comparator例項,Comparator就是隻有一個方法的介面,所以可以提供一個lambda表示式:

Arrays.sort(words,
           (first, second) -> first.length() - second.length());

在底層,Arrays.sort方法會接收實現了Comparator的某個類的物件。在這個物件上呼叫compare方法會執行這個lambda表示式的體。

​ 最好把lambda表示式看作是一個函式,而不是一個物件。另外要接受lambda表示式可以傳遞到函式式介面。

​ lambda表示式可以轉換為介面:

Timer t = new Timer(1000, event ->
                    {
                        System.out.println("At the tone, the time is " + new Date());
                        Toolkit.getDefaultToolkit().beep();
                    });

​ 實際上,在Java中,對lambda表示式所能做的也只是能轉換為函式式介面。

lambda表示式和函式式介面的關係

​ Java API在java.util.function包中定義了很多非常通用的函式式介面。其中一個介面BiFunction<T,U,R>描述了引數型別為T和U而且返回型別為R的函式。可以把我們的字串比較lambda表示式儲存在這個型別的變數中:

BiFunction<String, String, Integer> comp 
    = (first, second) -> first.length() - second.length();

不過,這對於排序並沒有幫助。沒有哪個Arrays.sort方法想要接受一個BiFunction。類似Comparator的介面往往有一個特定的用途,而不只是提供一個有指定引數和返回型別的方法。

​ 想要用lambda表示式做某些處理,還是要謹記表示式的用途,為它建立一個特定的函式式介面

Predicate介面

​ java.util.function包中有一個尤其有用的介面Predicate:

public interface Predicate<T>
{
    boolean test(T t);
    
    // additional default and static methods
}

​ ArrayList類有一個removeIf方法,它的引數就是一個Predicate。這個介面專門用來傳遞lambda表示式。例如,下面語句將從一個數組列表刪除所有的null值:

list.removeIf(e -> e == null);

6.3.4 方法引用

​ 有時,可能已經有現成的方法可以完成你想要傳遞到其他程式碼的某個動作。例如,假設你希望只要出現一個定時器事件就列印這個事件物件。為此可以呼叫:

Time t = new Timer(1000, event -> System.out.println(event));

​ 但是,如果直接把println方法傳遞到Timer構造器就更好了。具體做法如下:

Timer t = new Timer(1000, System.out::println);

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

​ 再來看一個例子,假設你想對字串排序,而不考慮字母的大小寫。

Arrays.sort(strings, String::compareToIgnoreCase)

方法引用的三種情況

​ 要用::操作符分隔方法名與物件或類名。主要有三種情況:

  1. object::instanceMethod
  2. Class::staticMethod
  3. Class::instanceMethod

​ 在前兩種情況中,方法引用等價於提供方法引數的lambda表示式。 System.out::println等價於x -> System.out.println(x);類似地,Math::pow等價於(x,y) -> Math.pow(x,y)

​ 對於第三種情況,第1個引數會成為方法的目標。例如,String::compareToIgnoreCase等同於(x,y) -> x.compareToIgnoreCase(y)

註釋:如果有多個同名的過載方法,編譯器就會嘗試從上下文中找出你指的那一個方法。選擇哪一個方法取決於方法引用轉換為哪個函式式介面的方法引數。

​ 類似於lambda表示式,方法引用不能獨立存在,總是會轉換成函式式介面的例項

方法引用與this引數

​ 可以在方法引用中使用this引數。例如,this::equlas等價於x -> this.equals(x)

​ 使用super也是合法的。super::instanceMethod使用this作為目標,會呼叫給定方法的超類版本。

個人理解

​ 方法引用是lambda表示式一種簡寫的方式。

6.3.5 構造器引用

​ 構造器引用與方法引用很類似,只不過方法名為new。例如Person::new是Person構造器的一個引用。具體是哪一個構造器,取決於上下文。

陣列型別構造器引用

​ 可以用陣列型別建立構造器引用。例如,int[]::new是一個構造器引用,它有一個引數:即陣列的長度。這等價於lambda表示式x -> new int[x]

泛型型別陣列與陣列構造器引用 ?

​ Java有一個限制,無法構造泛型型別T的陣列。陣列構造器引用對於克服這個限制很有用。

​ 表示式new T[n]會產生錯誤,因為這會改成new Object[n]。這是一個問題。例如,我們需要一個Person物件陣列。Stream介面有一個toArray方法可以返回Object陣列:

Object[] people = stream.toArray();

​ 不過,這並不讓人滿意。使用者希望得到一個Person引用陣列,而不是Object引用陣列。流庫利用構造器引用解決了這個問題。可以把Person[]::new傳入toArray方法:

Person[] people = stream.toArray(Person[]::new);

​ toArray方法呼叫這個構造器來得到一個正確型別的陣列,然後填充這個陣列並返回。

6.3.6 變數作用域

​ 通常,你可能希望能夠在lambda表示式中訪問外圍方法或類中的變數。例如:

public static void repeatMessage(String text, int delay)
{
    ActionListener listener = event ->
    {
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay, listener).start();
}

​ 來看這樣一個呼叫:

repeatMessage("Hello", 1000);

​ 現在來看lambda表示式中的變數text。注意這個變數並不是在這個lambda表示式中定義的,而是repeatMessage方法的一個引數變數。

​ 這裡好像會有問題:lambda表示式的程式碼可能會在repeatMessage呼叫返回很久以後才執行,而那時這個引數變數已經不存在了。如何保留text變數呢?

​ 要了解到底會發生什麼,下面來鞏固我們對lambda表示式的理解。lambda表示式有3個部分:

  1. 一個程式碼塊
  2. 引數
  3. 自由變數的值,這是指非引數而且不在程式碼中定義的變數

​ 在我們的例子中,這個lambda表示式有一個自由變數text。表示lambda表示式的資料結構必須儲存自由變數的值,在這裡就是字串"Hello"。我們說它被lambda表示式捕獲。(下面來看具體的實現細節。例如,可以把一個lambda表示式轉換為包含一個方法的物件,這樣自由變數的值就會複製到這個物件的例項變數中。)

註釋:關於程式碼塊以及自由變數值有一個術語:閉包(closure)。在Java中,lambda表示式就是閉包。

​ 可以看到,lambda表示式可以捕獲外圍作用域中變數的值。在Java中,要確保所捕獲的值是明確定義的,這裡有一個重要的限制。在lambda表示式中,只能引用值不會改變的變數。例如,下面的做法是不合法的:

public static void countDown(int start, int delay)
{
    ActionListener listener = event ->
    {
        start--;	//Error: Can't mutate captured varible
        System,out.pirntln(start);
    };
    new Timer(delay, listener).start();
}

​ 之所以有這個限制是有原因的。如果在lambda表示式中改變變數,併發執行多個動作時就會不安全。

​ 另外如果在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: Cannot refer to changing i
        };
        new Timer(1000, listener).start();
    }
}

​ 這裡有一條規則:lambda表示式中捕獲的變數必須實際上是最終變數(effectively final)。實際上的最終變數是指,這個變數初始化之後就不會再為它賦新值。

​ lambda表示式的體與巢狀塊有相同的作用域。這裡同樣適用命名衝突和遮蔽的有關規則。在lambda表示式中宣告與一個區域性變數同名的引數或區域性變數是不合法的。

​ 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表示式的作用域巢狀在init方法中,與出現在這個方法中的其他位置一樣,lambda表示式中this的含義並沒有變化。

6.3.7 處理lambda表示式

​ 目前為止,已經瞭解瞭如何生成lambda表示式,以及如何把lambda表示式傳遞到需要一個函式式介面的方法。下面來看如何編寫方法處理lambda表示式。

​ 使用lambda表示式的重點是延遲執行。之所以希望以後再執行程式碼,有很多原因,如:

  • 在一個單獨的執行緒中執行程式碼;
  • 多次執行程式碼;
  • 在演算法的適當位置執行程式碼(排序中的比較操作);
  • 發生某種情況時執行程式碼(點選了一個按鈕,資料 到達,等等);
  • 只在必要時才執行程式碼。

​ 下面來看一個簡單的例子。假設你想要重複一個動作n次。現在將這個動作和重複次數傳遞到一個repeat方法:

repeat(10, () -> System.out.println("Hello world"));

​ 要接受這個lambda表示式,需要選擇(偶爾可能需要提供)一個函式式介面。表6-1列出了Java API中提供的最重要的函式式介面。在這裡,我們可以使用Runnable介面:

public static void repeat(int n, Runnable action)
{
    for(int i = 0; i < n; i++)
        action.run();
}

​ 呼叫action.run()時會執行這個lambda表示式的主體。

​ 現在讓這個例子更復雜一些。我們希望告訴這個動作它出現在哪一次迭代中。為此,需要選擇一個合適的函式式介面。其中要包含一個方法,方法有一個int引數而且返回型別為void。處理int值的標準介面如下:

public interface IntConsumer
{
    void accept(int value);
}

​ 下面給出repeat方法的改進版本:

public static void repeat(int n, IntConsumer action)
{
    for(int i = 0; i < n; i++)
        action.accept(i);
}

​ 可以如下呼叫它:

repeat(10, i -> System.out.println("Countdown: " + (9 - i)));

​ 最好使用特殊化規範來減少自動裝箱。出於這個原因,使用了IntConsumer而不是Consumer

表6-1 常用函式式介面:

函式式介面 引數型別 返回型別 抽象方法名 描述 其他方法
Runnable void run 作為無引數或返回值的動作執行
Supplier T get 提供一個T型別的值
Consumer T void accept 處理一個T型別的值 andThen
BiConsumer<T,U> T,U void accept 處理T和U型別的值 andThen
Function<T,R> T R apply 有一個T型別引數的函式 compose,andThen,identity
BiFunction<T,U,R> T,U R apply 有T和U型別引數的函式 andThen
UnaryOperator T T apply 型別T的一元操作符 compose,andThen,identity
BinaryOperator T,T T apply 型別T的二元操作符 andThen,maxBy,minBy
Predicate T boolean test 布林值函式 and,or,negate,isEqual
BiPredicate<T,U> T,U boolean test 有兩個引數的布林值函式 and,or,negate

表6-2 基本型別的函式式介面 略

提示:最好使用表6-1或6-2中的介面。

註釋

​ ?

​ 大多數標準函式式介面都提供了非抽象方法來生成或合併函式。例如,Predicate.isEqual(a)等同於a::equals,如果a為null也能正常工作。已經提供了預設方法and、or和negate來合併謂詞。例如,Predicate.isEqual(a).or(Predicate.isEqual(b))就等同於x -> a.equals(x) || b.equals(x)

註釋:如果設計你自己的介面,其中只有一個抽象方法,可以用@FunctionalInterface註解來標記這個介面。這樣做有兩個優點。如果你無意中增加了另一個非抽象方法,編譯器會產生一個錯誤訊息。另外Javadoc頁裡會指出你的介面是一個函式式介面。

​ 並不是必須使用註解。根據定義,任何有一個抽象方法的介面都是函式式介面。

6.3.8 再談Comparator

​ Comparator介面包含很多方便的靜態方法來建立比較器。這些方法可以用於lambda表示式或方法引用。

​ 靜態comparing方法取一個“鍵提取器”函式,它將型別T對映為一個可比較的型別(如String)。對要比較的物件應用這個函式,然後對返回的鍵完成比較。例如,假設有一個Person物件陣列,可以如下按名字對這些物件排序:

Arrays.sort(people,Comparator.comparing(Person::getName));
// comparing 方法接收一個 函式式介面 ,通過一個 lambda 表示式傳入

​ 可以把比較器與thenComparing方法串起來。例如,

Arrays.sort(people,
           Comparator.comparing(Person::getName)
           .thenComparing(Person::getFirstName));

​ 如果兩個人的姓相同,就會使用第二個比較器。

​ 這些方法有很多變體形式。可以為comparing和thenComparing方法提取的鍵指定一個比較器。例如,可以根據人名長度完成排序:

Arrays.sort(people, Comparator.comparing(Person::getName,
                                        (s,t) -> Integer.compare(s.length(), t.length())));

​ 另外,comparing和thenComparing方法都有變體形式,可以避免int、long或double值的裝箱。要完成前一個操作,還有一種更容易的做法:

Arrays.sort(people, Comparator.comparaingInt(p -> p.getName().length()));

​ 如果鍵函式可以返回null,可能就要用到nullsFirst和nullsLast介面卡。這些靜態方法會修改現有的比較器,從而在遇到null值時不會丟擲異常,而是將這個值標記為小於或大於正常值。例如,假設一個人沒有中名時getMiddleName會返回一個null,就可以使用Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(...))

​ nullsFirst方法需要一個比較器,在這裡就是比較兩個字串的比較器。naturalOrder方法可以為任何實現了Comparable的類建立一個比較器。在這裡,Comparator.<String>naturalOrder()正是我們需要的。下面是一個完整的呼叫,可以按可能為null的中名進行排序。這裡使用了一個靜態匯入java.util.Comparator.*,以便理解這個表示式。注意naturalOrder的型別可以推導得出。

Arrays.sort(people, comparing(Person::getMiddleName, nullsFirst(naturalOrder())));

​ 靜態reverseOrder方法會提供自然順序的逆序。要讓比較器逆序比較,可以使用reversed例項方法。例如naturalOrder().reversed()等同於reverseOrder()

6.4 內部類

​ 內部類是定義在另一個類中的類。

​ 為什麼需要使用內部類:

  • 內部類方法可以訪問該類定義所在的作用域中的資料,包括私有的資料。
  • 內部類可以對同一個包中的其他類隱藏起來。
  • 當想要定義一個回撥函式且不想編寫大量程式碼時,使用匿名內部類比較便捷。

​ 我們將這個比較複雜的內容分幾部分介紹:

  • 6.4.1節,給出一個簡單的內部類,訪問外圍類的例項域。
  • 6.4.2節,給出內部類的特殊語法規則。
  • 6.4.3節,探討如何將內部類的內部轉換成常規類。
  • 6.4.4節,討論區域性內部類,它可以訪問外圍作用域中的區域性變數。
  • 6.4.5節,介紹匿名內部類,說明在Java有lambda表示式之前用於實現回撥的基本方法。
  • 6.4.6節,介紹如何將靜態內部類巢狀在輔助類中。

6.4.1 使用內部類訪問物件狀態

​ 下面進一步分析TimerTest示例,並抽象出一個TalkingClock類。構造一個語音時鐘需要提供兩個引數:釋出通告的間隔和開關鈴聲的標誌。

public class TalkingClock
{
    private int interval;
    private boolean beep;
    
    public TalkingClock(int interval, boolean beep){……}
    public void start(){……}
    
    public class TimePrinter implements ActionListener
        // an inner class
    {
        ……
    }
}

​ 需要注意,這裡的TimePrinter類位於TalkingClock類內部。這並不意味著每個TalkingClock都有一個TimePrinter例項域。如前所示,TimePrinter物件是由TalkingClock類的方法構造

​ 下面是TimePrinter類的詳細內容。需要注意一點,actionPerformed方法在發出鈴聲之前檢查了beep標誌。

 public class TimePrinter implements ActionListener
        // an inner class
    {
        public void actionPerformed(ActionEvent event)
        {
            System.out.println("At the tone, the time is " + new Date());
            if (beep)	Toolkit.getDefaultToolkit().beep();
        }
    }

​ TimePrinter類沒有例項域或名為beep的變數,取而代之的是beep引用了建立TimePrinter的TalkingClock物件的域。從傳統意義上講,一個方法可以引用呼叫這個方法的物件資料域。內部類既可以訪問自身的資料域,也可以訪問建立它的外圍類物件的資料域

內部類的物件總是有一個隱式引用,它指向了建立它的外部類物件。這個引用在內部類的定義中是不可見的。然而,為了說明這個概念,我們將外圍類物件的引用稱為outer。於是actionPerformed方法將等價於下列形式:

	public void actionPerformed(ActionEvent event)
        {
            System.out.println("At the tone, the time is " + new Date());
            if (outer.beep)	Toolkit.getDefaultToolkit().beep();
        }

外圍類的引用在構造器中設定。編譯器修改了所有的內部類的構造器,新增一個外圍類引用的引數。因為TimePrinter類沒有定義構造器,所以編譯器為這個類生成了一個預設的構造器:

public TimePrinter(TalkingClock clock)	// automatically generated code
{
    outer = clock;
}

​ outer不是Java的關鍵字。我們只是用它說明內部類中的機制。

​ 當在start方法中建立了TimePrinter物件後,編譯器就會將this引用傳遞給當前的語音時鐘的構造器:

ActionListener listener = new TimePrinter(this);	// parameter automatically added

​ 如果有一個TimePrinter類是一個常規類,它就需要通過TalkingClock類的公有方法訪問beep標誌。而使用內部類可以給予改進,即不必提供僅用於訪問其他類的訪問器。

註釋:TimerPrinter類宣告為私有的。這樣一來,只有TalkingClock的方法才能夠構造TimePrinter物件。只有內部類可以是私有類,而常規類只可以具有包可見性,或公有可見性。

6.4.2 內部類的特殊語法規則

​ 上一節中已經講述了內部類有一個外圍類的引用outer。事實上,使用外圍類引用的正規語法還要複雜一些。

​ 表示式OuterClass.this表示外圍類引用。例如:

		public void actionPerformed(ActionEvent event)
        {
            ……
            if (TalkingClock.this.beep)	Toolkit.getDefaultToolkit().beep();
        }

​ 反過來,可以採用下列語法格式更加明確地編寫內部物件的構造器

outerObject.new InnerClass(construction parameters)

​ 例如,

ActionListener listener = this.new TimePrinter();

​ 在這裡,最新構造的TimePrinter物件的外圍類引用被設定為建立內部類物件的方法中的this引用。通常,this限定詞是多餘的。不過,可以通過顯式地命名將外圍類引用設定為其他的物件。例如,如果TimePrinter是一個公有內部類,對於任意的語音時鐘都可以構造一個TimePrinter:

TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

​ 需要注意,在外圍類的作用域之外,可以這樣引用內部類:

OuterClass.InnerClass

註釋:內部類中宣告的所有靜態域都必須是final。內部類不能有static方法(也可以有靜態方法,但是隻能訪問外圍類的靜態域和方法)。

6.4.3 內部類是否有用、必要和安全

​ 內部類是一種編譯器現象,與虛擬機器無關。編譯器會把內部類翻譯成用$分隔外部類名與內部類名的常規類檔案,而虛擬機器則對此一無所知。

​ 通過對TalkingClock.TimePrinter進行反射,可以看到編譯器為了引用外圍類,生成了一個附加的例項域。另外,還可以看到構造器的TalkingClock引數。

​ 由於內部類擁有訪問特權,可以訪問外圍類的私有資料,所以與常規類比較起來功能更加強大。

​ 既然內部類可以被翻譯成名字很古怪的常規類(而虛擬機器對此一點也不瞭解),內部類如何管理那些額外的訪問特權呢?

​ 利用反射檢視TalkingClock類,可以看到編譯器會在外圍類新增靜態方法,它將返回作為引數的beep域。而內部類會呼叫這個方法。

​ 這樣做會有安全風險,任何人都可以通過呼叫新增的靜態方法讀取到私有域beep。

​ 總而言之,如果內部類訪問了私有資料域,就有可能通過附加在外圍類所在包中的其他類訪問它們。但程式設計師不可能無意之中就獲得對類的訪問許可權,而必須刻意地構建或修改類檔案才有可能達到這個目的。

6.4.4 區域性內部類

​ 在之前TalkingClock示例的程式碼中,TimePrinter這個類名字只在start方法中建立這個型別的物件時使用了一次。

​ 當遇到這類情況時,可以在一個方法中定義區域性類

public void start()
{
    class TimePrinter implements ActionListener
    {
        ……
    }
    
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval, listener);
    t.start();
}

​ 區域性類不能用public或private訪問說明符進行宣告。它的作用域被限定在宣告這個區域性類的塊中。

​ 區域性類有一個優勢,即對外部世界可以完全地隱藏起來。即使TalkingClock類中的其他程式碼也不能訪問它。除start方法之外,沒有任何方法知道TimePrinter類的存在。

6.4.5 由外部方法訪問變數

​ 與其他內部類相比較,區域性類還有一個優點。它們不僅能夠訪問包含它們的外部類,還可以訪問區域性變數。不過,那些區域性變數必須事實上為final。它們一旦賦值就絕不會改變。

​ 下面的示例將TalkingClock構造器的引數interval和beep移至start方法中。

public void start(int interval, boolean beep)
{
    class TimePrinter implements ActionListener
    {
        public void actionPerformed(ActionEvent event)
        {
            System,out.println("At the tone, the time is " + new Date());
            if (beep) Toolkit.getDefaultToolkit().beep();
        }
    }
    
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval, listener);
    t.start();
}

​ 請注意,TalkingClock類不再需要儲存例項變數beep了,它只是引用start方法中的beep引數變數。

​ 程式行if (beep) ……畢竟在start方法內部,為什麼還要研究能不能訪問beep變數的值呢?

​ 為了能夠更清楚的看到內部的問題,考查一下控制流程:

  1. 呼叫start方法。
  2. 呼叫內部類TimePrinter的構造器,以便初始化物件變數listener。
  3. 將listener引用傳遞給Timer構造器,定時器開始計時,start方法結束。此時,start方法的beep引數變數不復存在。
  4. 然後,actionPerformed方法執行if (beep)……。

​ 為了能讓actionPerformed方法工作,TimePrinter類在beep域釋放之前將beep域用start方法的區域性變數進行備份。當建立一個物件時,編譯器必須檢測對區域性變數的訪問,為每一個變數建立相應的資料域,並將區域性變數拷貝到構造器中,以便將這些資料域初始化為區域性變數的副本。

​ 從程式設計師的角度看,區域性變數的訪問非常容易。它減少了需要顯示編寫的例項域,從而使得內部類更加簡單。

區域性類與final

​ 區域性類的方法只可以引用定義為final的區域性變數。鑑於此情況,在列舉的示例中。將beep引數宣告為final,對它進行初始化後不能夠再進行修改。因此,就使得區域性變數與在區域性類內建立的拷貝保持一致。

註釋:在Java SE 8之前,必須把從區域性類訪問的區域性變數宣告為final。例如,start方法原本就應當這樣宣告,從而使內部類能夠訪問beep引數:

public void start(int interval, final boolean beep)

​ 有時,final限制顯得不太方便。例如,想更新在一個封閉作用域內的計數器。int counter = 0; counter++;

​ 由於知道counter需要更新,所以不能將counter宣告為final。由於Integer物件是不可變的,也不能用Integer代替它。補救的方法是使用一個長度為1的陣列:

int[] counter = new int[1]; counter[0]++;

​ 在內部類被首次提出時,原型編譯器對內部類中修改的區域性變數自動地進行轉換。不過,後來這種做法被廢棄。畢竟,這裡存在一個危險。同時在多個執行緒中執行內部類中的程式碼時,這種併發更新會導致競態條件(14章)。

6.4.6 匿名內部類

​ 將區域性內部類的使用再深入一步。假如只建立這個類的一個物件,就不必命名了。這種類被稱為匿名內部類

public void start(int interval, boolean beep)
{
    ActionListener listener = new ActionListener()
    {
        public void actionPerformed(ActionEvent event)
        {
            System,out.println("At the tone, the time is " + new Date());
            if (beep) Toolkit.getDefaultToolkit().beep();
        }
    };
    
    Timer t = new Timer(interval, listener);
    t.start();
}

​ 它的含義是:建立一個實現ActionListener介面的類的新物件,需要實現的方法actionPerformed定義在括號{}內。

​ 通常的語法格式是:

new superType(construction parameters)
{
    //inner class methods and data
}

​ 其中,superType可以是ActionListener這樣的介面,於是內部類就要實現這個介面。superType也可以是一個類,於是內部類就要擴充套件它。

​ 由於匿名類沒有類名,所以匿名類不能有構造器。取而代之的是,將構造器引數傳遞給超類構造器。尤其是在內部類實現介面的時候,不能有任何構造引數。

​ 多年來,Java程式設計師習慣的做法是用匿名內部類實現事件監聽器和其他回撥。如今最好還是使用lambda表示式。例如:

public void start(int interval, boolean beep)
{

    Timer t = new Timer(interval, event ->
                        {
                            System,out.println("At the tone, the time is " + new Date());
            				if (beep) Toolkit.getDefaultToolkit().beep();
                        });
    t.start();
}

註釋:雙括號初始化:

​ 利用內部類語法。假設構造一個數組列表,並將它傳遞到一個方法:

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

​ 如果不再需要這個陣列列表,最好讓它作為一個匿名列表。為匿名列表新增元素:

invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }});

​ 注意這裡的雙括號。外層括號建立了ArrayList的一個匿名子類。內層括號則是一個物件構造塊。

警告:建立一個與超類大體類似的匿名子類通常會很方便。不過,對於equals方法要特別當心。第五章中我們曾建議equals方法最後使用以下測試:

if (getClass() != other.getClass()) return false;

​ 但是對匿名子類做這個測試時會失效。

提示:生成日誌或除錯訊息時,通常希望包含當前類的類名,如

System.err.println("Something awful happened in " + getClass());

​ 不過,這對於靜態方法不奏效。呼叫getClass的時候呼叫的是this.getClass,而靜態方法沒有this。

​ 應該使用:

new Object(){}.getClass().getEnclosingClass();	// get class of static method

​ 在這裡,new Object(){}會建立Object的一個匿名子類的一個匿名物件,getEnclosingClass則得到其外圍類,也就是包含這個靜態方法的類。

6.4.7 靜態內部類

​ 有時候,使用內部類只是為了把一個類隱藏在另外一個類的內部,並不需要內部類引用外圍類物件。為此,可以把內部類宣告為static,以便取消產生的引用。

​ 只有內部類能宣告為static。靜態內部類的物件除了沒有對生成它的外圍類物件的引用特權外,與其它所有內部類完全一樣。

​ 若內部類物件是在靜態方法中構造的,則必須使用靜態內部類。如果沒有將內部類宣告為static,那麼編譯器將會給出錯誤報告:沒有可用的隱式外部類型別物件初始化內部類物件。

註釋:與常規內部類不同,靜態內部類可以有靜態域和方法。

註釋:宣告在介面中的內部類自動稱為static和public類。

6.5 代理

​ 利用代理,可以在執行時建立一個實現了一組給定介面的新類。這種功能只有在編譯時無法確定需要實現哪個介面時才有必要使用。

6.5.1 何時使用代理

​ 假設有一個表示介面的Class物件(有可能只包含一個介面),它的確切型別在編譯時無法知道。要想構造一個實現這些介面的類,就需要使用newInstance方法或反射找出這個類的構造器。但是,不能例項化一個介面,需要在程式處於執行狀態時定義一個新類。

​ 為了解決這個問題,有些程式將會生成程式碼;將這些程式碼放置在一個檔案中;呼叫編譯器;然後再載入結果類檔案。這樣做速度比較慢,而且需要將編譯器與程式放在一起。

​ 代理機制是一種更好的解決方案。代理類可以在執行時建立全新的類。這樣的代理類能夠實現指定的介面。尤其是,它具有下列方法:

  • 指定介面所需要的全部方法。
  • Object類中的全部方法。

​ 然而,不能在執行時定義這些方法的新程式碼。而是要提供一個呼叫處理器(invocation handler)。呼叫處理器是實現了InvocationHandler介面的類物件。在這個介面中只有一個方法:

​```Object invoke(Object proxy, Method method, Object[] args)```
// 定義了代理物件呼叫方法時希望執行的動作

​ 無論何時呼叫代理物件的方法,呼叫處理器的invoke方法都會被呼叫,並向其傳遞Method物件和原始的呼叫引數。呼叫處理器必須給出處理呼叫的方式。

6.5.2 建立代理物件

​ 要想建立一個代理物件,需要使用Proxy類的newProxyInstance方法。這個方法有三個引數:

  • 一個類載入器。目前用null表示使用預設的類載入器。
  • 一個Class物件陣列,每個元素都是需要實現的介面。
  • 一個呼叫處理器。

​ 還有兩個需要解決的問題。如何定義一個處理器?能夠用結果代理物件做些什麼?這兩個問題的答案取決於打算使用代理機制解決什麼問題。使用代理可能出於很多原因:

  • 路由對遠端伺服器的方法呼叫。
  • 在程式執行期間,將使用者介面事件與動作關聯起來。
  • 為除錯,跟蹤方法呼叫。

​ 在示例中,使用代理和呼叫處理器跟蹤方法呼叫,並且定義了一個TraceHandler包裝器類implements InvocationHandler介面,儲存包裝的物件。其中的invoke方法打印出被呼叫方法的名字和引數,隨後用包裝好的物件作為隱式引數呼叫這個方法。

​ 下面說明如何構造用於跟蹤方法呼叫的代理物件:

Object value = ……;
//	construct wrapper
InvocationHandler handler = new TraceHandler(value);
// construct proxy for one or more interfaces
Class[] interfaces = new Class[] {Comparable.class};
Object proxy = Proxy.newProxyInstance(null, interfaces, handler);

​ 現在,無論何時用proxy呼叫某個方法,這個方法的名字和引數就會打印出來,之後再用value呼叫它。

​ 示例見書。

個人理解

​ 建立了代理類物件後,每對代理類物件呼叫他要實現的介面中的方法或Object類中的某些方法時,這些方法都會呼叫呼叫處理器中的invoke方法。之後包裝好的物件再會呼叫介面中的方法或Object類中的某個方法。

6.5.3 代理類的特性

​ 代理類是在程式執行過程中建立的。然而,一旦被建立,就變成了常規類,與虛擬機器中的任何其他類沒有什麼區別。

​ 所有的代理類都擴充套件於Proxy類。一個代理類只有一個例項域——呼叫處理器,它定義在Proxy的超類中。為了履行代理物件的職責,所需要的任何附加資料都必須儲存在呼叫處理器中。

​ 所有的代理類都覆蓋了Object類中的方法toString、equals和hashCode。如同所有的代理法一樣,這些方法僅僅呼叫了呼叫處理器的invoke。Object類中的其他方法(如clone和getClass)沒有被重新定義。

​ 沒有定義代理類的名字,Sun虛擬機器中的Proxy類將生成一個以字串$Proxy開頭的類名。

​ 對於特定的類載入器和預設的一組介面來說,只能有一個代理類。也就是說,如果使用同一個類載入器和介面陣列呼叫兩次newProxyInstance方法的話,那麼只能夠得到同一個類的兩個物件。可以利用getProxyClass方法獲得這個類:

Class proxyClass = Proxy.getProxyClass(null, interfaces);

​ 代理類一定是public和final。

​ 可以通過呼叫Proxy類中的isProxyClass方法檢測一個特定的Class物件是否代表一個代理類。