自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Scala講座:類型系統(tǒng)和相關(guān)功能

開發(fā) 后端
本文選自Scala講座的第六篇的內(nèi)容,介紹了Scala與Java相似之處,Scala的類層次,范型基礎(chǔ),實(shí)存類型,結(jié)構(gòu)類型,復(fù)合類型,希望大家喜歡。

本文節(jié)選自最近在日本十分流行的Scala講座系列的第六篇,由JavaEye的fineqtbull翻譯。本系列的作者牛尾剛在日本寫過不少有關(guān)Java和Ruby的書籍,相當(dāng)受歡迎。

概要

到上次為止由羽生田先生介紹了Scala語法的特點(diǎn),這一講我作為嘉賓來介紹一下Scala的類型系統(tǒng)和相關(guān)功能。本次介紹的重點(diǎn)是Java與Scala之間類層次的差異、范型的協(xié)變與逆變、實(shí)存類型(Existential Type)、結(jié)構(gòu)類型(Structural Type)和復(fù)合類型(Compound Type)。

與Java相似之處

Scala類型系統(tǒng)的基礎(chǔ)部分是與Java非常相像的。Scala與Java一樣有單一的根類,Java通過接口來實(shí)現(xiàn)多重繼承,而Scala則通過特征(trait)來實(shí)現(xiàn)(Scala的特征可以包含實(shí)現(xiàn)代碼,這當(dāng)然是與Java接口不同的。不過由于特征自己具有類型的功能,所以對(duì)于沒有包含實(shí)現(xiàn)代碼的特征,可以認(rèn)為與Java的接口是等價(jià)的)。

不過在幾點(diǎn)上面Scala具有與Java不同的部分,或者相比Java增加的功能部分。下文將以與Java相比的不同點(diǎn)或增加點(diǎn)為重點(diǎn)來說明一下Scala的類型系統(tǒng)。

Scala的類層次(1) - Any、AnyVal、AnyRef

本連載的第四回也提到過,Scala的類型層次與Java的相應(yīng)部分是非常相似的。首先,Scala中存在單一的根類Any,所有類型都直接或間接地由Any類繼承而來。這與Java中的所有引用類型的根類是java.lang.Object是一樣的。

另一方面也有與Java不同的部分。首先Scala不存在類似于Java中的基礎(chǔ)(Primitive)類型。那是

怎么一回事呢?Scala中所有與基礎(chǔ)類型相當(dāng)?shù)念愋投汲蔀榱祟悾⑶疫@些類都是繼承了Any的AnyVal類的子類。

另外,所有的引用類型都成AnyRef類的間接或直接子類。前面說了Any類類似于Java中的java.lang.Object類,但是從實(shí)際意義上來看,因該說對(duì)應(yīng)于java.lang.Object的因該是AnyRef(図 3-1的a)。

不過離題一下,對(duì)于Scala中相當(dāng)于基礎(chǔ)類型的類,可以把整數(shù)的(Byte、Short、Int、Long)和浮點(diǎn)數(shù)的(Float、Double)看作是相鄰的兄弟類關(guān)系,那Java中可以默認(rèn)轉(zhuǎn)換的比如byte->int,在Scala中是否需要顯示的類型轉(zhuǎn)換呢,如果需要的話那大家會(huì)覺得好麻煩呀。實(shí)際上Scala中具有使用戶能夠定義自己的默認(rèn)類型轉(zhuǎn)換功能的隱式轉(zhuǎn)換(implicit conversion)功能。對(duì)于類似于Java中的byte->int、float->double等默認(rèn)轉(zhuǎn)換功能,在Scala標(biāo)準(zhǔn)庫(kù)中定義了與此相當(dāng)?shù)碾[式轉(zhuǎn)換功能,所以用戶是不需要顯示轉(zhuǎn)換的。這個(gè)隱式轉(zhuǎn)換是非常有趣的功能,在以后的連載里可能會(huì)有詳細(xì)說明。

圖 3-1Scala的類型層次 

Scala的類層次(2) - Nothing、Null

與Java不同的是,Scala中存在所謂的底(bottom)類型,那就是Nothing類。Nothing是所有類型的子類,也就是說可以將Nothing類型賦值給任意類型的變量,但是Nothing類型的值并不存在。

大家可能認(rèn)為“沒有值的類型有什么用呢?”。但是Nothing類型絕對(duì)在,表示沒有返回值的函數(shù)的返回類型,或者在后述的范型中表示空的集等方面發(fā)揮著重要的作用。

另外還存在是所有的引用類型(AnyRef)的子類,可以賦值給所有引用類型變量的類型Null。Null類型的值只有null(實(shí)際上Java中也有Null類型,擔(dān)負(fù)著與Scala中Null類型相似的任務(wù)。與Scala不同的是,Java中沒有顯示定義Null類型的機(jī)會(huì),所以基本上沒有人會(huì)意識(shí)得到的)。(図 3-1的b)表示了上述類型間的關(guān)系。

范型基礎(chǔ)

一句話來說,范型就是定義以類型為參數(shù)的類或接口(Scala中為特征)的功能。Java里從JDK5開始就有了范型,想必知道的人應(yīng)該比較多了,下面就簡(jiǎn)單舉例說明一下。

例如,假設(shè)有如下的代碼片段。這里java.util.List是范型接口,String就是賦給它的類型參數(shù)。

  1. java.util.List< String> strs = new java.util.ArrayList< String>(); 

這樣,就可以用如下方法將String類型(或子類型)的對(duì)象加入List中了。

  1. strs.add("hoge"); 

如下所示,如將String以外的對(duì)象加入List則會(huì)發(fā)生編譯錯(cuò)誤。

  1. strs.add(new java.util.Date()); 

這樣一來,就可以開發(fā)類型安全的通用集(collection)庫(kù)了。在Java5之前的集庫(kù)是用Object來實(shí)現(xiàn)的。但是向集中加入元素時(shí)并沒有進(jìn)行正確的類型檢查,而且從集中取出元素時(shí)還要做強(qiáng)制的類型轉(zhuǎn)換,導(dǎo)致舊的集庫(kù)在類型安全方面有一些問題。進(jìn)一步來說,光從類型定義看不出該集包含的是何種元素,所以在可讀性方面也有不足。

Scala的范型與Java是非常相似的,基本上可以同樣地使用,只是在標(biāo)記方法上有些區(qū)別。以下是同剛才Java代碼基本相同的Scala代碼。

  1. var strs: java.util.List[String] = new java.util.ArrayList 

Scala中用[..]來代替了Java中的< ..>來表現(xiàn)類型參數(shù)表。附帶提一下,與Java有一點(diǎn)小的不同,Scala在new ArrayList時(shí)不需要指定String類型參數(shù),這是編譯器的類型推斷起了效用(顯示指定也是可以的)。

Scala中定義范型類的方法也基本與Java相同。下面是通過范型用Java定義的不可變單方向列表類。這里在類名Link后聲明了用< >括著的類型參數(shù)T。這個(gè)類型參數(shù)T在Link類的定義中可以像一般類型那樣使用。

  1. class Link< T> {  
  2. final T head;  
  3. final Link< T> tail;  
  4. Link(T head, Link< T> tail) {  
  5. this.head = head;  
  6. this.tail = tail;  
  7. }  
  8. }  

同樣可以用Scala來定義與上述完全相同的范型列表。

  1. class Link[T](val head: T, val tail: Link[T]) 

從此可知,除了一些細(xì)微的標(biāo)識(shí)差別,Scala中也可以方便地使用范型。

范型的協(xié)變與逆變

光從到此為止的說明來看,可能有人會(huì)以為Scala是僅僅把Java中的范型改變了一下標(biāo)識(shí)符號(hào)。但是Scala中的范型有幾個(gè)與Java不同的明顯差異,其中之一就是這里提到的協(xié)變與逆變。

協(xié)變

范型中所謂的協(xié)變大致來說是這樣的東西。首先假設(shè)有類G(或者接口和特征)和類型T1、T2。在T1是T2的子類的情況下如果G< T1>也是G< T2>的子類,那么類G就是協(xié)變的。

僅如此說明的話比較難以理解,那就舉例說明一下。如下所示,假設(shè)有類型為java.util.List< Object>的變量s1和類型為java.util.List< String>的變量s2。

  1. java.util.List< Object> s1 = ...;  
  2. java.util.List< String> s2 = ...; 

String是Object的子類,Java中并不允許將s2賦值給s1,將會(huì)產(chǎn)生編譯錯(cuò)誤。因此,雖然String是Object的子類,但是java.util.List< String>并不是java.util.List< Object>的子類,所以用Java的范型所定義的類或接口并不是協(xié)變的。這并不是由于Java范型的靈活性不好,而是因?yàn)閰f(xié)變的范型在保證類型的安全性上有一些問題。

假定允許s1=s2;。s1是容納Object類型的元素的,所以如下所示可以加入java.util.Date類型的對(duì)象。

  1. s1.add(new java.util.Date()); 

但是由于語句s1=s2;,s1被指向了s2,這樣容納String元素的List變量s2就可以加入java.util.Date對(duì)象了。這樣好不容易通過范型來保證的類型安全性(java.util.List< String>里只有String)就被破壞了。正因?yàn)橛腥绱藛栴}所以Java的范型不是協(xié)變的。

附帶提一下,對(duì)于Java5之前就存在的數(shù)組來說,數(shù)組的元素類型A如果是數(shù)組元素類型B的子類,那么A的數(shù)組類型也是B的數(shù)組類型的子類,也就是說Java中的數(shù)組是協(xié)變的。這樣一來,如下所示即使是違背了類型安全性的數(shù)組之間的賦值(沒有強(qiáng)制類型轉(zhuǎn)換)代碼也能通過編譯器檢查。

  1. String[] s2 = new String[1];  
  2. Object[] s1 = s2;  
  3. s1[0] = new java.util.Date(); //執(zhí)行時(shí)拋出ArrayStoreException異常 

如上所述,Java中的范型不是協(xié)變的是有理由的,但是有些情況下這種限制表現(xiàn)得過于強(qiáng)了。比如,以使用前述的不可變Link類為例。這種情況下,一旦創(chuàng)建不可變Link的實(shí)例之后,與Java的List不同,對(duì)于該實(shí)例是不能進(jìn)行寫操作(如add)的,這樣的話將Link< String>賦值給Link< Object>也就可以認(rèn)為沒有問題了,但是在Java中這是不允許的。

Scala的范型,在沒有特定指定的情況下也是和Java一樣,是非協(xié)變的。例如使用前述的Link類編寫如下代碼后將會(huì)出現(xiàn)編譯錯(cuò)誤。

  1. val link: Link[Any] = new Link[String]("FOO"null)  
  2. ... 

錯(cuò)誤提示如下。敘述的錯(cuò)誤原因是在應(yīng)該出現(xiàn)Ling[Any]的地方但是出現(xiàn)了Link[String],而這正是Link不是協(xié)變的結(jié)果。

  1. fragment of Link.scala):2: error: type mismatch;  
  2. found : this.Link[String]  
  3. required: this.Link[Any]  
  4. val link: Link[Any] = new Link[String]("FOO"null

但是,Scala的類或特征的范型定義中,如果在類型參數(shù)前面加入+符號(hào),就可以使類或特征變?yōu)閰f(xié)變了。下面是在Scala中定義協(xié)變類的實(shí)驗(yàn)。題材是前述的Link類,在類型參數(shù)T前加了一個(gè)+符號(hào)。

  1. class Link[+T](val head: T, val tail: Link[T]) 

把Link類如此定義之后,前面出現(xiàn)編譯錯(cuò)誤的代碼就可以順利通過編譯了。另外,如果試圖定義不能保證類型安全的協(xié)變范型將會(huì)出現(xiàn)編譯錯(cuò)誤。例如在定義非可變的數(shù)據(jù)結(jié)構(gòu)時(shí),這種限制就會(huì)帶來一些問題。例如對(duì)于前面的Link類,追加一個(gè)將作為參數(shù)傳入的元素放在列表頭并返回新列表的方法prepend。

  1. class Link[+T](val head: T, val tail: Link[T]) {  
  2. def prepend(newHead: T): Link[T] = new Link(newHead, this)  

prepend方法并沒有改變?cè)瓉鞮ink類實(shí)例的狀態(tài),因該是沒有問題的。但是,編譯之后會(huì)產(chǎn)生如下編譯錯(cuò)誤。

  1. ink.scala:2: error: covariant type T occurs in contravariant position in type T  
  2. of value newHead  
  3. def prepend(newHead: T): Link[T] = new Link(newHead, this

實(shí)際上,范型變?yōu)閰f(xié)變之后就不能把類型參數(shù)不加修改的放在成員方法的參數(shù)上(這里是newHead)了。但是,通過將成員方法定義為范型,并按照如下所示描述后就可以避免該問題了(具體原因這里略而不談)。

  1. class Link[+T](val head: T, val tail: Link[T]) {  
  2. def prepend[U >: T](newHead: U): Link[U] = new Link(newHead, this)  

在Java里也可以定義范型方法,正如范型類型定義,通過用類型參數(shù)來參數(shù)化方法,從而定義了類型安全的范型方法。例如連載第五回出場(chǎng)的List類的map方法就是范型方法。

  1. verride final def map[B](f : (A) => B) : List[B] 

map方法將以參數(shù)形式傳入的函數(shù)f應(yīng)用于List的所有元素,并將函數(shù)的應(yīng)用結(jié)果組成列表后返回。但是參數(shù)函數(shù)f的返回結(jié)果是什么在定義map方法是不知道的,所以用類型參數(shù)B來使map成為范型方法,從而使它可以通用于各種類型了。

范型方法是通過在方法名后直接用[..]來括住類型參數(shù)方式來定義的。用[]括住的類型參數(shù)在方法中可以作為一般類型來使用。而且在類型參數(shù)之后加上>:或< :符號(hào)后,可以將類型參數(shù)所表示的類型限制為某一類型子類或父類。例如,[U< :T]的情況下,U必須是T的子類;[U>:T]的情況下,U必須是T的父類。

逆變

另一方面,范型中的逆變是這樣的東西。首先假設(shè)有類G(或者接口和特征)和類型T1、T2。在T1是T2的子類的情況下如果G< T2>也是G< T1>的子類(注意左右與協(xié)變是相反的),那么類G就是逆變的。

與協(xié)變一樣,下面舉例說明一下。首先假設(shè)有類型為java.util.List< Object>的變量s1,類型為java.util.List< String>的變量s2。

  1. java.util.List< Object> s1 = ...;  
  2. java.util.List< String> s2 = ...; 

String是Object的子類,由于Java的范型規(guī)則不允許表達(dá)式s2=s1,所以將會(huì)出現(xiàn)編譯錯(cuò)誤。這里雖然String是Object的子類,但是java.util.List< Object>并不是java.util.List< String>的子類,所以Java的范型并不是逆變的。如果Java的范型是逆變的話,那同協(xié)變時(shí)情況一樣,將會(huì)產(chǎn)生類型安全上的問題。

假設(shè)允許表達(dá)式s2=s1。由于s2的元素類型是String,所以從列表中取出元素后返回的類型因該是String。因此,如下代碼因該是成立的。

  1. String str = s2.get(0); 

但是,s2所指的列表s1的元素類型是Object,所以s1列表中的取出的元素并不僅限于String,這在類型安全性上就有問題了。

對(duì)于Scala的范型,如果沒有特別指示,與Java一樣也不是逆變的。假設(shè)有如下含有apply方法的LessTan類(apply方法的邏輯是當(dāng)a小于b時(shí)返回true,否則返回false)。

  1. abstract class LessThan[T] {  
  2. def apply(a: T, b: T): Boolean  

如下使用了LessThan類的方法將會(huì)出現(xiàn)編譯錯(cuò)誤。

  1. val hashCodeLt: LessThan[Any] = new LessThan[Any] {  
  2. def apply(a: Any, b: Any): Boolean = a.hashCode <  b.hashCode  
  3. }  
  4. val strLT: LessThan[String] = hashCodeLt  
  5. ... 

編譯錯(cuò)誤的文本如下。顯示的錯(cuò)誤原因是在因該出現(xiàn)LessThan[String]的地方出現(xiàn)了LessThan[Any],由此看見LessThan類不是逆變的。

  1. (fragment of Comparator.scala):5: error: type mismatch;  
  2. found : this.LessThan[Any]  
  3. required: this.LessThan[String]  
  4. val strLT: LessThan[String] = hashCodeLt 

但是,在類或特征的定義中,在類型參數(shù)之前加上一個(gè)-符號(hào),就可定義逆變范型類和特征了。下面嘗試一下定義Scala的逆變類。題材是前面的LessThan類,如下所示在LessThan定義的類型參數(shù)前加上-符號(hào)。

  1. abstract class LessThan[-T] { def apply(a: T, b: T): Boolean } 

將LessThan類如此定義之后,前面錯(cuò)誤代碼的編譯就可以通過了。另外,如果將類型定義為逆變后會(huì)發(fā)生類型安全性問題,則編譯器將報(bào)編譯錯(cuò)誤。

實(shí)存(Existantial)類型

前面說過了Java范型沒有協(xié)變和逆變特性,但是通過使用Java的通配符功能后可以獲得與協(xié)變與逆變相近的效果。通配符不是標(biāo)記在類型定義的地方,而是在類型使用的地方,可以在使用類型處加上G< ? extends T1>或G< ? super T1>。

前者與協(xié)變相對(duì)應(yīng),當(dāng)T2是T1的子類時(shí),G< T2>是G< ? exnteds T1>的子類。后者與逆變相對(duì)應(yīng),T1是T2的子類時(shí),G< T2>是G< ? super T1>的子類。因此,以下的代碼將能正常編譯。

  1. java.util.List< String> s1 = ...;  
  2. java.util.List< ? extends Object> s2 = s1; //對(duì)應(yīng)協(xié)變  
  3. java.util.List< Object> s3 = ...;  
  4. java.util.List< ? super String> s4 = s3; //對(duì)應(yīng)逆變  
  5. ... 

由于通配符是標(biāo)記在使用類型的地方,所以每次定義協(xié)變或逆變的變量時(shí)都要使用它,缺點(diǎn)是比較麻煩。另一方面,即使是沒有定義為協(xié)變或逆變的范型類型,也可以將其以協(xié)變或逆變的方式處理是它的優(yōu)點(diǎn)。

Scala中也可以通過使用實(shí)存類型方法類實(shí)現(xiàn)與Java中通配符相同的功能。例如,下述Scala代碼可以實(shí)現(xiàn)與上述Java代碼相同的功能。

  1. //java.util.List[_ < : Any] (省略形式)  
  2. var s1: java.util.List[String] = new java.util.ArrayList  
  3. var s2: java.util.List[T] forSome { type T < : Any } = s1  
  4. //java.util.List[_ >: String] (省略形式)  
  5. var s3: java.util.List[Any] = new java.util.ArrayList  
  6. var s4: java.util.List[T] forSome { type T >: String} = s3  
  7. ... 

結(jié)構(gòu)(Structural)類型

在類似于Java的語言中只有在類定義好之后才能確定他們的繼承關(guān)系。假設(shè)有如下A、B、C三個(gè)Java類定義。

  1. class A {  
  2. void call() {}  
  3. }  
  4. class B extends A {  
  5. void call() {}  
  6. }  
  7. class C {  
  8. void call() {}  
  9. }  

這時(shí)假如有方法void foo(A a),那么類型A和B的實(shí)例可以作為參數(shù)傳遞給它,但是類型C卻不能傳遞(雖然C中同樣定義了方法call)。

這是由于,相比于類B通過exnteds A語句明確標(biāo)識(shí)了與A的繼承關(guān)系,而C則沒有明確標(biāo)示出與A的繼承關(guān)系,這樣C就不是A的子類了。這對(duì)于Java和C#(C++的情況特殊除外)這類靜態(tài)語言來說是理所當(dāng)然的事,所以意識(shí)到的人因該不多吧。另一方面,在動(dòng)態(tài)語言社區(qū)中,把這稱為duck typing,較普遍的看法是“無論是否有繼承關(guān)系,只要在對(duì)象中存在需要的方法就可以了?!薄R詣偛诺拇a為例,A、B和C中都定義了call方法,如果foo方法中只調(diào)用call方法的話,那么類C的實(shí)例也可以作為參數(shù)傳給foo。

時(shí)常聽到這種說法“正是由于動(dòng)態(tài)語言中沒有靜態(tài)類型檢查所以才可以使用duck typing功能?!?。但是稍微仔細(xì)想一下,雖然是靜態(tài)語言,但是在編譯時(shí)還是知道類型持有了哪些方法的。理論上,即使不犧牲靜態(tài)類型檢查,也應(yīng)該可以描述只要含有某一方法集合就OK的類型的。

Scala中通過結(jié)構(gòu)類型來描述這種類型。結(jié)構(gòu)類型的使用方法比較簡(jiǎn)單,只要在類型聲明的地方,用{}將所需方法的聲明括起來就可以了。

  1. def foo(callable: { def call: Unit }) = ... 

另外在列舉方法的個(gè)數(shù)比較多的情況下,可以如下所示來定義別名,這樣就不用每次都列出所有方法了,只需使用結(jié)構(gòu)類型的別名即可。

  1. type Callable = { def call: Unit } 

復(fù)合(Compound)類型

在使用Java時(shí),是否有過想用“即繼承了類型A又實(shí)現(xiàn)了接口B”的類型的想法呢?Java中除了類型參數(shù)的限制之外,對(duì)于這種類型也沒有定義方法。然而,Scala中則可以非常簡(jiǎn)單地描述這種類型。描述方法就是在一般的類名稱之后用with來連接附加的類型。

例如對(duì)于如下的變量f來說,只要是實(shí)現(xiàn)了java.io.Closeable和Readable接口的對(duì)象,誰都可以賦值給變量f。

  1. var f: java.io.Closeable with Readable = .. 

結(jié)束語

雖然稍顯簡(jiǎn)略,本文介紹了一下Scala的類型系統(tǒng)相關(guān)的功能,怎么樣?。肯衤穭乓蕾囶愋偷龋疚闹羞€有一些功能沒有能夠介紹完,如果有興趣的話,大家可以閱覽一下Scala的語言規(guī)范或者Scala官方網(wǎng)站上的論文。

【編輯推薦】

  1. Scala講座:面向?qū)ο蠛秃瘮?shù)式的特點(diǎn)總結(jié)
  2. Scala講座:函數(shù)式編程處理樹結(jié)構(gòu)數(shù)據(jù)
  3. Scala講座:編程的思考方法
  4. Scala講座:將函數(shù)作為***類對(duì)象來處理
  5. Scala講座:全局變量問題的解決
責(zé)任編輯:book05 來源: JavaEye博客
相關(guān)推薦

2009-12-11 10:43:00

Scala講座混入多重繼承類型層次

2009-12-11 10:42:00

Scala講座類定義構(gòu)造函數(shù)

2009-09-24 09:41:00

Scala講座Scala

2009-09-27 15:29:00

Scala講座面向?qū)ο?/a>Scala

2009-12-11 10:44:00

Scala講座函數(shù) scala

2009-09-24 09:28:00

Scala講座全局變量scala

2009-09-27 15:23:00

Scala講座函數(shù)式編程Scala

2009-06-10 09:29:40

Scala類型通配符

2009-06-03 09:08:20

ScalaJava類型

2009-12-11 10:43:00

Scala講座操作符函數(shù)

2009-07-22 09:53:57

Scala底層類型

2009-09-24 09:38:00

Scala講座第一類對(duì)象scala

2010-10-14 13:50:11

Scala

2010-06-21 13:32:57

OSPF協(xié)議

2017-12-04 14:13:24

物聯(lián)網(wǎng)ITOT

2009-11-25 17:48:18

PHP文件系統(tǒng)相關(guān)函數(shù)

2012-03-14 10:51:49

Tizen

2009-07-22 09:43:30

Scala類型

2009-07-21 09:31:00

Scala基本類型文本

2009-06-16 17:54:38

Scala類語法語義
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)