程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java 下一代: 沒有繼承性的擴展(三)

Java 下一代: 沒有繼承性的擴展(三)

編輯:關於JAVA

Groovy 元編程為您提供常見問題的簡單解決方案

Java 下一代語言擴展現有的類和其他構件的方法有很多,前兩期 Java 下一代 文章探討了其中的一些方法。在本期文章中,我將繼續該探索,仔細查看在多種上下文中實現擴展的 Groovy 元編程技術。

在 “沒有繼承性的擴展,第 1  部分” 中,在討論使用類別類  和 ExpandoMetaClass 作為將新行為 “應用於” 現有類的機制時,我偶然接觸了一些 Groovy 元編程特性。Groovy 中的元編程特性更深入一些:它們使得集成 Java 代碼變得更容易,而且可以幫助您采用比 Java 語言更簡潔的方式來執行常見任務。

接口強制轉換(Interface coercion)

接口是 Java 語言中常見的語義重用機制。嘗試以簡潔的方式集成 Java 代碼的其他語言應該提供簡單的方法來具體化接口。在 Groovy 中,類可以通過傳統的 Java 方式來擴展接口。但是,Groovy 還使得在方便時輕松地將閉包和映射強制轉換成接口實例變得很容易。

單一方法強制轉換

清單 1 中的 Java 代碼示例使用 FilenameFilter 接口來定位文件:

清單 1. 在 Java 中使用    FilenameFilter 接口列出文件

import java.io.File;
import java.io.FilenameFilter;
    
public class ListDirectories {
    public String[] listDirectoryNames(String root) {
        return new File(root).list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return new File(name).isDirectory();
            }
        });
    }
}

在 清單 1 中,我創建了一個新的匿名內部類,它覆蓋了指定過濾條件的 accept() 方法。在 Groovy 中,我可以跳過創建一個新類的步驟,只將一個閉包強制轉換成接口,如清單 2 所示:

清單 2. 在 Groovy 中通過使用閉包強制轉換來模擬    FilenameFilter 接口

new File('.').list(
    { File dir, String name -> new File(name).isDirectory() }
     as FilenameFilter).each { println it }

在 清單 2 中,list() 方法想使用一個 FilenameFilter 實例作為參數。但我卻創建了一個與接口的 accept() 簽名相匹配的閉包,並在閉包的正文中實現接口的功能。在定義了閉包之後,我通過調用 as  FilenameFilter 將閉包強制轉換成適當的 FilenameFilter 實例。Groovy 的 as 運算符將閉包具體化為一個實現接口的類。該技術對於單一方法接口非常適用,因為方法和閉包之間存在一個自然映射。

對於指定多個方法的接口,被具體化的類為每個方法都調用了相同的閉包塊。但只在極少數情況下,用相同代碼來處理所有方法調用才是合理的。當您需要使用多個方法時,可以使用包含方法名稱/閉包對的  Map,而不是使用單一的閉包。

映射

在 Groovy 中,還可以使用映射來表示接口。映射的鍵是代表方法名稱的字符串,鍵值是實現方法行為的代碼塊。清單 3 中的示例將一個映射具體化為一個  Iterator 實例:

清單 3. 在 Groovy 中使用映射來具體化接口

h = [hasNext:{ h.i > 0 }, next:{h.i--}]
h.i = 10
def iterator = h as Iterator
                                                      
while (iterator.hasNext())
  print iterator.next() + ", "
// 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,

在 清單 3 中,我創建了一個映射 (h),它包括 hasNext 和  next 鍵,以及它們各自的代碼塊。Groovy 假設映射鍵是字符串,所以我不需要用引號來包圍該鍵。在每個代碼塊中,我用點符號  (h.i) 引用 h 映射的第三個鍵 (i)。這個點符號借鑒自人們所熟悉的對象語法,它是 Groovy 中的另一個語法糖示例。在使用 h 作為一個迭代器之前,不會執行代碼塊,我必須首先確保 i 有一個值,然後再使用  h 作為一個迭代器。我用 h.i = 10 設置 i 的值。然後,我將 h 選作一個 Iterator,並使用從 10 開始的整數集合。

通過使得映射能夠動態地作為接口實例,Groovy 極大地減少了 Java 語言有時導致的一些語法問題。此特性很好地說明了 Java 下一代語言如何改進開發人員的體驗。

ExpandoMetaClass

正如我在 "沒有繼承性的擴展,第  1 部分" 中所述,您可以使用 ExpandoMetaClass 將新方法添加到類 — 包括核心類,比如  Object 和 String。ExpandoMetaClass 對於其他一些用途也是有用的,比如將方法添加到對象實例,以及改善異常處理。

將方法添加到對象和從對象中刪除方法

從將行為附加到類的那一刻起,使用 ExpandoMetaClass 對類執行的更改就會在全局生效。普遍性是這種方法的優勢 — 這並不奇怪,因為這種擴張機制源自 Grails Web 框架的創建。Grails 依賴於對核心類的全局變更。但有時您需要在不影響所有實例的情況下,采用有限的方式為一個類添加語義。對於這些情況,Groovy 提供了可以與對象的元類實例 交互的方式。例如,您可以將方法只添加到某個特定的對象實例,如清單 4 所示:

清單 4. 將行為附加到一個對象實例

def list = new ArrayList()
list.metaClass.randomize = { ->
    Collections.shuffle(delegate)
    delegate
}
    
list << 1 << 2 << 3 << 4
println list.randomize() // [2, 1, 4, 3]
println list             // [2, 1, 4, 3]

在 清單 4 中,我創建了 ArrayList 的一個實例 (list)。然後我訪問了該實例以懶惰方式實例化的 metaClass 屬性。我添加了一個方法  (randomize()),該方法返回執行 shuffle  之後的集合。在元類的方法聲明中,delegate 代表對象實例。

不過,我的 randomize() 方法改變了底層集合,因為 shuffle() 是一個變異調用。在 清單 4 的第二行輸出中,請注意,該集合被永久性地更改為新的隨機順序。令人高興的是,通過解決這些問題,可以輕松地改變  Collections.shuffle() 等內置方法的默認行為。例如,清單 5 中的 random 屬性是對 清單 4 的 randomize() 方法的改進:

清單 5. 改進不良語義

def list2 = new ArrayList()
list2.metaClass.getRandom = { ->
  def l = new ArrayList(delegate)
  Collections.shuffle(l)
  l
}
    
list2 << 1 << 2 << 3 << 4
println list2.random // [4, 1, 3, 2]
println list2        // [1, 2, 3, 4]

在 清單 5 中,我讓 getRandom() 方法的正文先復制列表,然後再改變它,這樣就可以讓原始列表保持不變。通過使用 Groovy 的命名約定,將屬性自動映射到 get 和  set 方法,我讓 random 也成為一個屬性,而不是一個方法。

本欄目

使用屬性技術來減少額外的括號干擾,導致了最近在 Groovy 中將方法鏈接在一起的方式的改變。該語言的版本 1.8 引入了命令鏈 的概念,支持創建更流暢的域特定語言(DSL)。DSL 通常擴充現有的類或對象實例來添加特殊的行為。

混合

Ruby 和類似語言中的一個流行特性是混合。混合讓您能夠不使用繼承,而是將新的方法和字段添加到現有的層次結構中。Groovy 支持混合特性,如清單 6 所示:

清單 6. 使用混合特性來附加行為

class ListUtils {
  static randomize(List list) {
    def l = new ArrayList(delegate)
    Collections.shuffle(l)
    l
  }
}
List.metaClass.mixin ListUtils

在 清單 6 中,我創建了一個輔助類 (ListUtils) 並為其添加了一個  randomize() 方法。在最後一行中,我將 ListUtils 類與  java.util.List 混合在一起,讓我的 randomize() 方法對  java.util.List 可用。也可以在對象實例中使用 mixin。這種技術通過將變更限制到某個單獨的代碼構件來幫助執行調試和跟蹤,所以,對於將行為附加到類而言,這是最好的方式。

結合擴展點

Groovy 的元編程特性不僅在單獨使用時非常強大,結合起來使用也非常有效。在動態語言中的一個常見細節是方法缺失(method missing) 鉤 — 一個類能夠以可控的方式響應尚未定義的方法,而不是拋出異常。如果出現未知的方法調用,Groovy 會在一個包含 methodMissing() 的類上調用該方法。您可以在通過 ExpandoMetaClass 增加的附加物中包含 methodMissing()。通過結合使用  methodMissing() 與 ExpandoMetaClass,您可以使得 Logger 等現有的類更加靈活。清單 7 顯示了一個示例:

清單 7. 混合 ExpandoMetaClass 和    methodMissing

import java.util.logging.*
    
Logger.metaClass.methodMissing = { String name, args ->
    println "inside methodMissing with $name"
    int val = Level.WARNING.intValue() +
        (Level.SEVERE.intValue() - Level.WARNING.intValue()) * Math.random()
    def level = new CustomLevel(name.toUpperCase(),val)
    def impl = { Object... varArgs ->
        delegate.log(level,varArgs[0])
    }
    Logger.metaClass."$name" = impl
    impl args
}
    
Logger log = Logger.getLogger(this.class.name)
log.neal "really messed this up"
log.minor_mistake "can fix later"

在 清單 7 中,我使用 ExpandoMetaClass 將一個  methodMissing() 方法附加到 Logger 類。現在,無論此 Logger 類在范圍中的哪個位置,我在以後的代碼中都可以通過有創意的方法調用日志,如 清單 7 中最後三行所示。

面向方面的編程

面向方面的編程(AOP)是一種流行的、實用的方法,可以超越 Java 技術的原有設計對其進行擴展。通過操縱字節碼的編譯過程,方面可以將新的代碼 “編織” 到現有方法中。AOP 定義了一些術語,包括 切入點(pointcut),這是執行補充的位置。例如,前 切入點是指在方法調用前添加的代碼。

因為 Groovy 編譯生成了 Java 字節碼,所以在 Groovy 中也支持 AOP。但通過元編程可以在 Groovy 中復制 AOP,而且沒有 Java 語言所要求的繁瑣過程。 ExpandoMetaClass 使您能夠訪問一個方法,這樣就無需引用該方法。之後,您可以重新定義該方法,也可以仍然調用方法的原始版本。AOP 的這種 ExpandoMetaClass 用法如清單 8 所示:

清單 8. 對 ExpandoMetaClass  使用面向方面的切入點

class Bank {
  def transfer(Account to, Account from, BigDecimal amount) {
    from.balance -= amount
    to.balance += amount

  }
}
    
class Account {
  def name, balance;
    
  @Override
  public String toString() {
    "Account{name:${name}, balance:${balance}}"
  }
}
    
def oldTransfer = 
  Bank.metaClass.getMetaMethod("transfer", [Account, Account, BigDecimal] as Object[])
    
Bank.metaClass.transfer = { Account to, Account from, BigDecimal amount ->
  println "Logging transfer: to:${to}, from:${from}, amount:${amount}"
  oldTransfer.invoke(delegate, [to, from, amount] as Object[])
}
    
def bank = new Bank()
def acctA = new Account(name:"A", balance:100.00)
def acctB = new Account(name:"B", balance:200.00)
println("Balances:A = ${acctA.balance}, B = ${acctB.balance}")
bank.transfer(acctA, acctB, 10.00)
println("Balances:A = ${acctA.balance}, B = ${acctB.balance}")
//Balances:A = 100.00, B = 200.00
//Logging transfer: to:Account{name:A, balance:100.00},
//    from:Account{name:B, balance:200.00}, amount:10.00
//Balances:A = 110.00, B = 190.00

在 清單 8,我創建了一個典型的 Bank 類,它只有一個 transfer() 方法。輔助的 Account 類包含簡單的帳戶信息。ExpandoMetaClass 包含一個  getMetaMethod()方法,用於檢索對某個方法的引用。我使用了 清單 8 中的  getMetaMethod(),檢索對現有 transfer() 方法的引用。然後,通過使用  ExpandoMetaClass,我創建了一個新的 transfer() 方法來取代舊的方法。在新方法的主體內,在寫完日志語句後,我調用了原來的方法。

清單 8 包含一個前切入點 示例:我執行了 “額外的” 代碼,然後再調用原來的方法。這在 Ruby 等動態語言中是一種常見的技術,其社區將該技術稱為 Monkey Patching。(原來使用的術語是 Guerilla Patching,但它被錯聽為 Gorilla Patching,然後被更名為 Monkey Patching,就像是一種文字游戲。)其結果與 AOP 一樣,但在 Groovy 中的動態擴展使您能夠在語言本身內執行這個增強。

AST 轉換

雖然 ExpandoMetaClass 及其相關特性如此強大,它們也不能覆蓋所有擴展點。最終,最強大的元編程能夠修改編譯器的 Abstract Syntax Tree(AST,抽象語法樹) — 由編譯進程維護的內部數據結構。注釋是其中一種掛鉤位置,在這裡可以插入轉換操作。Groovy 預定義了一些有用的語言擴展,比如 AST 轉換。

例如,@Lazy 注釋(比如 @Lazy pets = ['Cat', 'Dog',  'Bird'])將數據結構的實例化推遲到必須評估它們的時候。Groovy 1.8 引入了一系列有用的結構性注釋,其中一些會出現在清單 9 中:

清單 9. 在 Groovy 中有用的結構性注釋

import groovy.transform.*;
    
@Immutable
@TupleConstructor
@EqualsAndHashCode
@ToString(includeNames = true, includeFields=true)
final class Point {
  int x
  int y
}

在 清單 9 中,Groovy 運行時會自動執行以下操作:

生成元組風格構造函數

生成 equals() 和 hashCode() 方法

使 Point 類不可變

生成一個 toString() 方法

使用 AST 變換遠遠優於使用 IDE 或反射來生成基礎架構方法。在使用 IDE 的時候,如果發生更改,則必須始終牢記重新生成一些方法。而反射比在編譯時發生的代碼生成更慢。

除了使用豐富的預定義 AST 轉換之外,還可以使用 Groovy 提供的一個完整 API 來構建自己的 AST 轉換。通過這個 API,可以訪問最細粒度的底層抽象,從而改變生成代碼的方式。

結束語

在本期文章中,您了解到 Groovy 通過其元編程特性提供的一系列令人眼花缭亂的擴展選項。在下一期 Java 下一代 文章中,我會探索特征(混合功能)和 Scala 中的其他元編程。

本欄目

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