摘要Scala 並不僅僅只給 JVM 引入了函數概念它還為我們提供了一種對於面向對象語言設計的現代視角在這一期的 面向 Java 開發人員的 Scala 指南 中Ted Neward 介紹了 Scala 如何利用特征(trait)使對象更加簡單更易於構建您將了解到特征與 Java? 接口和 C++ 多重繼承提供的傳統極性既有相似之處也有不同之處
著名科學家研究學者艾薩克牛頓爵士有這樣一句名言如果說我看得比別人遠一些那是因為我站在巨人的肩膀上作為一名熱心的歷史和政治學家我想對這位偉人的名言略加修改如果說我看得比別人遠一些那是因為我站在歷史的肩膀上而這句話又體現出另一位歷史學家 George Santayana 的名言忘記歷史必將重蹈覆轍換句話說如果我們不能回顧歷史從過去的錯誤(包括我們自己過去的經驗)中吸取教訓就沒有機會做出改進
您可能會疑惑這樣的哲學與 Scala 有什麼關系?繼承就是我們要討論的內容之一考慮這樣一個事實Java 語言的創建已經是近 年前的事情當時是 面向對象 的全盛時期它設計用於模仿當時的主流語言 C++嘗試將使用這種語言的開發人員吸引到 Java 平台上來毫無疑問在當時看來這樣的決策是明智而且必要的但回顧一下就會發現其中有些地方並不像創建者設想的那樣有益
例如在二十年前對於 Java 語言的創建者來說反映 C++ 風格的私有繼承和多重繼承是必要的自那之後許多 Java 開發人開始為這些決策而後悔在這一期的 Scala 指南中我回顧了 Java 語言中多重繼承和私有繼承的歷史隨後您將看到 Scala 是怎樣改寫了歷史為所有人帶來更大收益
C++ 和 Java 語言中的繼承
C++ 工作的人們能夠回憶起私有繼承是從基類中獲取行為的一種方法不必顯式地接受 ISA 關系將基類標記為 私有 允許派生類從該基類繼承而來而無需實際成為 一個基類但對自身的私有繼承是未得到廣泛應用的特性之一繼承一個基類而無法將它向下或向上轉換到基類的理念是不明智的
另一方面多重繼承往往被視為面向對象編程的必備要素在建模交通工具的層次結構時 SeaPlane 無疑需要繼承 Boat(使用其 startEngine() 和 sail() 方法)以及 Plane(使用其 startEngine() 和 fly() 方法)SeaPlane 既是 Boat也是 Plane難道不是嗎?
無論如何這是在 C++ 鼎盛時期的想法在快速轉向 Java 語言時我們認為多重繼承與私有繼承一樣存在缺陷所有 Java 開發人員都會告訴您SeaPlane 應該繼承 Floatable 和 Flyable 接口(或許還包括 EnginePowered 接口或基類)繼承接口意味著能夠實現該類需要的所有方法而不會遇到 虛擬多重繼承 的難題(遇到這種難題時要弄清楚在調用 SeaPlane 的 startEngine() 方法時應調用哪個基類的 startEngine())
遺憾的是徹底放棄私有繼承和多重繼承會使我們在代碼重用方面付出昂貴的代價Java 開發人員可能會因從虛擬多重繼承中解放出來而高興但代價是程序員往往要完成辛苦而易於出錯的工作
關於本系列
Ted Neward 將和您一起深入探討 Scala 編程語言在這個新的 developerWorks 系列 中您將深入了解 Sacla並在實踐中看到 Scala 的語言功能進行比較時Scala 代碼和 Java 代碼將放在一起展示但(您將發現)Scala 中的許多內容與您在 Java 編程中發現的任何內容都沒有直接關聯而這正是 Scala 的魅力所在!如果用 Java 代碼就能夠實現的話又何必再學習 Scala 呢?
回顧可重用行為
規范是 Java 平台的基礎它帶來了眾多 Java 生態系統作為依據的 POJO我們都明白一點Java 代碼中的屬性由 get()/set() 對管理如清單 所示
清單 Person POJO
//This is Java
public class Person
{
private String lastName;
private String firstName;
private int age;
public Person(String fn String ln int a)
{
lastName = ln; firstName = fn; age = a;
}
public String getFirstName() { return firstName; }
public void setFirstName(String v) { firstName = v; }
public String getLastName() { return lastName; }
public void setLastName(String v) { lastName = v; }
public int getAge() { return age; }
public void setAge(int v) { age = v; }
}
這些代碼看起來非常簡單編寫起來也不難但如果您希望提供通知支持 — 使第三方能夠使用 POJO 注冊並在變更屬性時接收回調事情會怎樣?根據 JavaBeans 規范必須實現 PropertyChangeListener 接口以及它的一個方法 propertyChange()如果您希望允許任何 POJO 的 PropertyChangeListener 都能夠對屬性更改 投票那麼 POJO 就需要實現 VetoableChangeListener 接口該接口的實現又依賴於 vetoableChange() 方法的實現
至少事情應該是這樣運作的
實際上希望成為屬性變更通知接收者的用戶必須實現 PropertyChangeListener 接口發送者(本例中的 Person 類)必須提供接收該接口實例的公共方法和監聽器需要監聽的屬性名稱最終得到更加復雜的 Person如清單 所示
清單 Person POJO第 種形式
//This is Java
public class Person
{
// rest as before except that inside each setter we have to do something
// like:
// public setFoo(T newValue)
// {
// T oldValue = foo;
// foo = newValue;
// pcsfirePropertyChange(foo oldValue newValue);
// }
public void addPropertyChangeListener(PropertyChangeListener pcl)
{
// keep a reference to pcl
}
public void removePropertyChangeListener(PropertyChangeListener pcl)
{
// find the reference to pcl and remove it
}
}
保持引用屬性變更監聽器意味著 Person POJO 必須保留某種類型的集合類(例如 ArrayList)來包含所有引用然後必須實例化插入並移除 POJO — 由於這些操作不是原子操作因此還必須包含恰當的同步保護
最後如果某個屬性發生變化屬性監聽器列表必須得到通知通常通過遍歷 PropertyChangeListener 的集合並對各元素調用 propertyChange() 來實現此過程包括傳入新的 PropertyChangeEvent 描述屬性原有值和新值這是 PropertyChangeEvent 類和 JavaBeans 規范的要求
在我們編寫的 POJO 中只有少數支持監聽器通知這並不意外在這裡要完成大量工作必須手動地重復處理所創建的每一個 JavaBean/POJO
除了工作還是工作 — 變通方法在哪裡?
有趣的是C++ 對於私有繼承的支持在 Java 語言中得到了延續今天我們用它來解決 JavaBeans 規范的難題一個基類為 POJO 提供了基本 add() 和 remove() 方法集合類以及 firePropertyChanged() 方法用於通知監聽器屬性變更
我們仍然可以通過 Java 類完成但由於 Java 缺乏私有繼承Person 類必須繼承 Bean 基類從而可向上轉換 到 Bean這妨礙了 Person 繼承其他類多重繼承可能使我們不必處理後續的問題但它也重新將我們引向了虛擬繼承而這是絕對要避免的
針對這個問題的 Java 語言解決方案是運用眾所周知的支持 類在本例中是 PropertyChangeSupport實例化 POJO 中的一個類為 POJO 本身使用必要的公共方法各公共方法都調用 Support 類來完成艱難的工作更新後的 Person POJO 可以使用 PropertyChangeSupport如下所示
清單 Person POJO第 種形式
//This is Java
import javabeans*;
public class Person
{
private String lastName;
private String firstName;
private int age;
private PropertyChangeSupport propChgSupport =
new PropertyChangeSupport(this);
public Person(String fn String ln int a)
{
lastName = ln; firstName = fn; age = a;
}
public String getFirstName() { return firstName; }
public void setFirstName(String newValue)
{
String old = firstName;
firstName = newValue;
propChgSupportfirePropertyChange(firstName old newValue);
}
public String getLastName() { return lastName; }
public void setLastName(String newValue)
{
String old = lastName;
lastName = newValue;
propChgSupportfirePropertyChange(lastName old newValue);
}
public int getAge() { return age; }
public void setAge(int newValue)
{
int old = age;
age = newValue;
propChgSupportfirePropertyChange(age old newValue);
}
public void addPropertyChangeListener(PropertyChangeListener pcl)
{
propChgSupportaddPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl)
{
propChgSupportremovePropertyChangeListener(pcl);
}
}
不知道您有何感想但這段代碼的復雜得讓我想去重拾匯編語言最糟糕的是您要對所編寫的每一個 POJO 重復這樣的代碼序列清單 中的半數工作都是在 POJO 本身中完成的因此無法被重用 — 除非是通過傳統的 復制粘貼 編程方法
現在讓我們來看看 Scala 提供什麼樣內容來實現更好的變通方法
Scala 中的特征和行為重用
Scala 使您能夠定義處於接口和類之間的新型結構稱為特征(trait)特征很奇特因為一個類可以按照需要整合許多特征這與接口相似但它們還可包含行為這又與類相似同樣與類和接口類似特征可以引入新方法但與類和接口不同之處在於在特征作為類的一部分整合之前不會檢查行為的定義或者換句話說您可以定義出這樣的方法在整合到使用特征的類定義之前不會檢查其正確性
特征聽起來十分復雜但一個實例就可以非常輕松地理解它們首先下面是在 Scala 中重定義的 Person POJO
清單 Scala 的 Person POJO
//This is Scala
class Person(var firstName:String var lastName:String var age:Int)
{
}
您還可以確認 Scala POJO 具備基於 Java POJO 的環境中需要的 get()/set() 方法只需在類參數 firstNamelastName 和 age 上使用 scalareflectBeanProperty 注釋即可現在為簡單起見我們暫時不考慮這些方法
如果 Person 類需要能夠接收 PropertyChangeListener可以使用如清單 所示的方式來完成此任務
清單 Scala 的 Person POJO 與監聽器
//This is Scala
object PCL
extends javabeansPropertyChangeListener
{
override def propertyChange(pce:javabeansPropertyChangeEvent):Unit =
{
Systemoutprintln(Bean changed its + pcegetPropertyName() +
from + pcegetOldValue() +
to + pcegetNewValue())
}
}
object App
{
def main(args:Array[String]):Unit =
{
val p = new Person(Jennifer Aloi )
paddPropertyChangeListener(PCL)
psetFirstName(Jenni)
psetAge()
Systemoutprintln(p)
}
}
注意如何使用清單 中的 object 實現將靜態方法注冊為監聽器 — 而在 Java 代碼中除非顯式創建並實例化 Singleton 類否則永遠無法實現這進一步證明了一個理論Scala 從 Java 開發的歷史 痛苦 中吸取了教訓
Person 的下一步是提供 addPropertyChangeListener() 方法並在屬性更改時對各監聽器觸發 propertyChange() 方法調用在 Scala 中以可重用的方式完成此任務與定義和使用特征一樣簡單如清單 所示我將此特征稱為 BoundPropertyBean因為在 JavaBeans 規范中已通知 的屬性稱為綁定屬性
清單 神聖的行為重用!
//This is Scala
trait BoundPropertyBean
{
import javabeans_
val pcs = new PropertyChangeSupport(this)
def addPropertyChangeListener(pcl : PropertyChangeListener) =
pcsaddPropertyChangeListener(pcl)
def removePropertyChangeListener(pcl : PropertyChangeListener) =
pcsremovePropertyChangeListener(pcl)
def firePropertyChange(name : String oldVal : _ newVal : _) : Unit =
pcsfirePropertyChange(new PropertyChangeEvent(this name oldVal newVal))
}
同樣我依然要使用 javabeans 包的 PropertyChangeSupport 類不僅因為它提供了約 % 的實現細節還因為我所具備的行為與直接使用它的 JavaBean/POJO 相同對 Support 類的其他任何增強都將傳播到我的特征不同之處在於 Person POJO 不需要再直接使用 PropertyChangeSupport如清單 所示
清單 Scala 的 Person POJO第 種形式
//This is Scala
class Person(var firstName:String var lastName:String var age:Int)
extends Object
with BoundPropertyBean
{
override def toString = [Person: firstName= + firstName +
lastName= + lastName + age= + age + ]
}
在編譯後簡單查看 Person 定義即可發現它有公共方法 addPropertyChangeListener()removePropertyChangeListener() 和 firePropertyChange()就像 Java 版本的 Person 一樣實際上Scala 的 Person 版本僅通過一行附加的代碼即獲得了這些新方法類聲明中的 with 子句將 Person 類標記為繼承 BoundPropertyBean 特征
遺憾的是我還沒有完全實現Person 類現在支持接收移除和通知監聽器但 Scala 為 firstName 成員生成的默認方法並沒有利用它們同樣遺憾的是這樣編寫的 Scala 沒有很好的注釋以自動地 生成利用 PropertyChangeSupport 實例的 get/set 方法因此我必須自行編寫如清單 所示
清單 Scala 的 Person POJO第 種形式
//This is Scala
class Person(var firstName:String var lastName:String var age:Int)
extends Object
with BoundPropertyBean
{
def setFirstName(newvalue:String) =
{
val oldvalue = firstName
firstName = newvalue
firePropertyChange(firstName oldvalue newvalue)
}
def setLastName(newvalue:String) =
{
val oldvalue = lastName
lastName = newvalue
firePropertyChange(lastName oldvalue newvalue)
}
def setAge(newvalue:Int) =
{
val oldvalue = age
age = newvalue
firePropertyChange(age oldvalue newvalue)
}
override def toString = [Person: firstName= + firstName +
lastName= + lastName + age= + age + ]
}
應該具備的出色特征
特征不是一種函數編程 概念而是十多年來反思對象編程的結果實際上您很有可能正在簡單的 Scala 程序中使用以下特征只是沒有意識到而已
清單 再見糟糕的 main()!
//This is Scala
object App extends Application
{
val p = new Person(Jennifer Aloi )
paddPropertyChangeListener(PCL)
psetFirstName(Jenni)
psetAge()
Systemoutprintln(p)
}
Application 特征定義了一直都是手動定義的 main() 的方法實際上它包含一個有用的小工具計時器如果系統屬性 scalatime 傳遞給了 Application 實現代碼它將為應用程序的執行計時如清單 所示
清單 時間就是一切
$ scala Dscalatime App
Bean changed its firstName from Jennifer to Jenni
Bean changed its age from to
[Person: firstName=Jenni lastName=Aloi age=]
[total ms]
JVM 中的特征
在這個時候有必要提出這樣一個問題這種看似魔術的接口與方法結構(即 特征)是如何映射到 JVM 的在清單 中我們的好朋友 javap 展示了魔術背後發生了什麼
清單 Person 內幕
$ javap classpath C:\Prg\scalafinal\lib\scalalibraryjar;classes Person
Compiled from Personscala
public class Person extends javalangObject implements BoundPropertyBeanscala
ScalaObject{
public Person(javalangString javalangString int);
public javalangString toString();
public void setAge(int);
public void setLastName(javalangString);
public void setFirstName(javalangString);
public void age_$eq(int);
public int age();
public void lastName_$eq(javalangString);
public javalangString lastName();
public void firstName_$eq(javalangString);
public javalangString firstName();
public int $tag();
public void firePropertyChange(javalangString javalangObject javalang
Object);
public void removePropertyChangeListener(javabeansPropertyChangeListener);
public void addPropertyChangeListener(javabeansPropertyChangeListener);
public final void pcs_$eq(javabeansPropertyChangeSupport);
public final javabeansPropertyChangeSupport pcs();
}
請注意 Person 的類聲明該 POJO 實現了一個名為 BoundPropertyBean 的接口這就是特征作為接口映射到 JVM 本身的方法但特征方法的實現又是什麼樣的呢?請記住編譯器可以容納所有技巧只要最終結果符合 Scala 語言的語義含義即可在這種情況下它會將特征中定義的方法實現和字段聲明納入實現特征的類 Person 中使用 private 運行 javap 會使這更加顯著 — 如果 javap 輸出的最後兩行體現的還不夠明顯(引用特征中定義的 pcs 值)
清單 Person 內幕第 種形式
$ javap private classpath C:\Prg\scalafinal\lib\scalalibraryjar;classes Person
Compiled from Personscala
public class Person extends javalangObject implements BoundPropertyBeanscala
ScalaObject{
private final javabeansPropertyChangeSupport pcs;
private int age;
private javalangString lastName;
private javalangString firstName;
public Person(javalangString javalangString int);
public javalangString toString();
public void setAge(int);
public void setLastName(javalangString);
public void setFirstName(javalangString);
public void age_$eq(int);
public int age();
public void lastName_$eq(javalangString);
public javalangString lastName();
public void firstName_$eq(javalangString);
public javalangString firstName();
public int $tag();
public void firePropertyChange(javalangString javalangObject javalangObject);
public void removePropertyChangeListener(javabeansPropertyChangeListener);
public void addPropertyChangeListener(javabeansPropertyChangeListener);
public final void pcs_$eq(javabeansPropertyChangeSupport);
public final javabeansPropertyChangeSupport pcs();
}
實際上這個解釋也回答了為何可以推遲特征方法的執行直至用該檢查的時候因為在類實現特征的方法之前它實際上並不是任何類的一 部分因此編譯器可將方法的某些邏輯方面留到以後再處理這非常有用因為它允許特征在不了解實現特征的實際基類將是什麼的情況下調用 super()
關於特征的備注
在 BoundPropertyBean 中我在 PropertyChangeSupport 實例的構建中使用了特征功能其構造方法需要屬性得到通知的 bean在早先定義的特征中我傳入了 this由於在 Person 上實現之前並不會真正定義特征this 將引用 Person 實例而不是 BoundPropertyBean 特征本身特征的這個具體方面 — 定義的推遲解析 — 非常微妙但對於此類的 遲綁定 來說可能非常強大
對於 Application 特征的情況有兩部分很有魔力Application 特征的 main() 方法為 Java 應用程序提供普適入口點還會檢查 Dscalatime 系統屬性查看是否應該跟蹤執行時間但由於 Application 是一個特征方法實際上會在子類上出現(App)要執行此方法必須創建 App 單體也就是說構造 App 的一個實例處理 類的主體這將有效地執行應用程序只有在這種處理完成之後特征的 main() 才會被調用並顯示執行所耗費的時間
雖然有些落後但它仍然有效盡管應用程序無權訪問任何傳入 main() 的命令行參數它還表明特征的行為如何 下放到 實現類
特征和集合
在將具體行為與抽象聲明相結合以便為實現者提供便捷時特征非常強大例如考慮經典的 Java 集合接口/類 List 和 ArrayListList 接口保證此集合的內容能夠按照插入時的次序被遍歷用更正規的術語來說位置語義得到了保證
ArrayList 是 List 的具體類型在分配好的數組中存儲內容而 LinkedList 使用的是鏈表實現ArrayList 更適合列表內容的隨機訪問而 LinkedList 更適合在除了列表末尾以外的位置進行插入和刪除操作無論如何這兩種類之間存在大量相同的行為它們繼承了公共基類 AbstractList
如果 Java 編程支持特征它們應已成為出色的結構能夠解決 可重用行為而無需訴諸於繼承公共基類 之類的問題特征可以作為 C++ 私有繼承 機制避免出現新 List 子類型是否應直接實現 List(還有可能忘記實現 RandomAccess 接口)或者擴展基類 AbstractList 的迷惑這有時在 C++ 中稱為 混合與 Ruby 的混合(或後文中探討的 Scala 混合)有所不同
在 Scala 文檔集中經典的示例就是 Ordered 特征它定義了名字很有趣的方法以提供比較(以及排序)功能如清單 所示
清單 順序順序
//This is Scala
trait Ordered[A] {
def compare(that: A): Int
def < (that: A): Boolean = (this compare that) <
def > (that: A): Boolean = (this compare that) >
def <= (that: A): Boolean = (this compare that) <=
def >= (that: A): Boolean = (this compare that) >=
def compareTo(that: A): Int = compare(that)
}
在這裡Ordered 特征(具有參數化類型采用 Java 泛型方式)定義了一個抽象方法 compare它應獲得一個 A 作為參數並需要在 小於 的情況下返回小於 的值在 大於 的情況下返回大於 的值在相等的情況下返回 然後它繼續使用 compare() 方法和更加熟悉的 compareTo() 方法(javautilComparable 接口也使用該方法)定義關系運算符(< 和 > 等)
Scala 和 Java 兼容性
實際上偽實現繼承並不是 Scala 內特征的最常見應用或最強大用法與此不同特征在 Scala 內作為 Java 接口的基本替代項希望使用 Scala 的 Java 程序員也應熟悉特征將其作為使用 Scala 的一種機制
我在本系列的文章中一直強調編譯後的 Scala 代碼並非總是能夠保證 Java 語言的特色例如回憶一下Scala 的 名字很有趣的方法(例如 + 或 \)這些方法往往會使用 Java 語言語法中不直接可用的字符編碼($ 就是一個需要考慮的嚴重問題)出於這方面的原因創建 Java 可調用 的接口往往要求深入研究 Scala 代碼
這個特殊示例有些憋足Scala 主義者 通常並不需要特征提供的間接層(假設我並未使用 名字很有趣的方法)但概念在這裡十分重要在清單 中我希望獲得一個傳統的 Java 風格工廠生成 Student 實例就像您經常在各種 Java 對象模型中可以看到的那樣最初我需要一個兼容 Java 的接口接合到 Student
清單 我學生
//This is Scala
trait Student
{
def getFirstName : String;
def getLastName : String;
def setFirstName(fn : String) : Unit;
def setLastName(fn : String) : Unit;
def teach(subject : String)
}
在編譯時它會轉換成 POJIPlain Old Java Interface查看 javap 會看到這樣的內容
清單 這是一個 POJI!
$ javap Student
Compiled from Studentscala
public interface Student extends scalaScalaObject{
public abstract void setLastName(javalangString);
public abstract void setFirstName(javalangString);
public abstract javalangString getLastName();
public abstract javalangString getFirstName();
public abstract void teach(javalangString);
}
接下來我需要一個類成為工廠本身通常在 Java 代碼中這應該是類上的一個靜態方法(名稱類似於 StudentFactory)但回憶一下Scala 並沒有此類的實例方法我認為這就是我在這裡希望得到的結論因此我創建了一個 StudentFactory 對象將我的 Factory 方法放在那裡
清單 我構造 Students
//This is Java
object StudentFactory
{
class StudentImpl(var first:String var last:String var subject:String)
extends Student
{
def getFirstName : String = first
def setFirstName(fn: String) : Unit = first = fn
def getLastName : String = last
def setLastName(ln: String) : Unit = last = ln
def teach(subject : String) =
Systemoutprintln(I know + subject)
}
def getStudent(firstName: String lastName: String) : Student =
{
new StudentImpl(firstName lastName Scala)
}
}
嵌套類 StudentImpl 是 Student 特征的實現因而提供了必需的 get()/set() 方法對切記盡管特征可以具有行為但它根據 JVM 作為接口建模這一事實意味著嘗試實例化特征將產生錯誤 —— 表明 Student 是抽象的
當然這個簡單示例的目的在於編寫出一個 Java 應用程序使之可以利用這些由 Scala 創建的新對象
清單 學生 Neo
//This is Java
public class App
{
public static void main(String[] args)
{
Student s = StudentFactorygetStudent(Neo Anderson);
steach(Kung fu);
}
}
運行此代碼您將看到I know Kung fu(我知道我們經過了漫長的設置過程只是得到了一部廉價電影的推介)
結束語
特征提供了在 Scala 中分類和定義的強大機制目的在於定義一種接口供客戶端使用按照 傳統 Java 接口的形式定義同時提供一種機制根據特征內定義的其他行為來繼承行為或許我們需要的是一種全新的繼承術語用於 描述特征和實現類之間的關系
除了本文所述內容之外還有很多種方法可以使用特征但本系列文章的部分目的就在於提供關於這種語言的足夠信息鼓勵您在家中進一步開展實驗下載 Scala 實現親自試用查看 Scala 可插入當前 Java 系統的什麼位置此外如果您發現 Scala 非常有用
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26946.html