新聞中心
最近,讀者的反饋?zhàn)屛乙庾R(shí)到在制作本系列的過程中我遺漏了 Scala 的語言的一個(gè)重要方面:Scala 的包和訪問修飾符功能。所以在研究該語言的函數(shù)性元素 apply 機(jī)制前,我將先介紹包和訪問修飾符。

創(chuàng)新互聯(lián)專注于企業(yè)網(wǎng)絡(luò)營銷推廣、網(wǎng)站重做改版、東昌網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、成都h5網(wǎng)站建設(shè)、商城系統(tǒng)網(wǎng)站開發(fā)、集團(tuán)公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站建設(shè)公司、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為東昌等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
打包
為了有助于隔離代碼,使其不會(huì)相互沖突,Java 代碼提供了 package 關(guān)鍵詞,由此創(chuàng)建了一個(gè)詞法命名空間,用以聲明類。本質(zhì)上,將類 Foo 放置到名為 com.tedneward.util 包中就將正式類名修改成了 com.tedneward.util.Foo;同理,必須按該方法引用類。如果沒有,Java 編程人員會(huì)很快指出,他們會(huì) import 該包,避免鍵入正式名的麻煩。的確如此,但這僅意味著根據(jù)正式名引用類的工作由編譯器和字節(jié)碼完成。快速瀏覽一下 javap 的輸出,這點(diǎn)就會(huì)很明了。
然而,Java 語言中的包還有幾個(gè)特殊的要求:一定要在包所作用的類所在的 .java 文件的頂端聲明包(在將注釋應(yīng)用于包時(shí),這一點(diǎn)會(huì)引發(fā)很嚴(yán)重的語言問題);該聲明的作用域?yàn)檎麄€(gè)文件。這意味著兩個(gè)跨包進(jìn)行緊密耦合的類一定要在跨文件時(shí)分離,這會(huì)致使兩者間的緊密耦合很容易被忽略。
Scala 在打包方面所采取的方法有些不同,它結(jié)合使用了 Java 語言的 declaration 方法和 C# 的 scope(限定作用域)方法。了解了這一點(diǎn),Java 開發(fā)人員就可以使用傳統(tǒng)的 Java 方法并將 package 聲明放在 .scala 文件的頂部,就像普通的 Java 類一樣;包聲明的作用域?yàn)檎麄€(gè)文件,就像在 Java 代碼中一樣。而 Scala 開發(fā)人員則可以使用 Scala 的包 “(scoping)限定作用域” 方法,用大括號(hào)限制 package 語句的作用域,如清單 1 所示:
清單 1. 簡(jiǎn)化的打包
- package com
- {
- package tedneward
- {
- package scala
- {
- package demonstration
- {
- object App
- {
- def main(args : Array[String]) : Unit =
- {
- System.out.println("Howdy, from packaged code!")
- args.foreach((i) => System.out.println("Got " + i) )
- }
- }
- }
- }
- }
- }
這個(gè)代碼有效地聲明了類 App,或者更確切的說是一個(gè)稱為 com.tedneward.scala.demonstration.App 的單個(gè)類。注意 Scala 還允許用點(diǎn)分隔包名,所以清單 1 中的代碼可以更簡(jiǎn)潔,如清單 2 所示:
清單 2. 簡(jiǎn)化了的打包(redux)
- package com.tedneward.scala.demonstration
- {
- object App
- {
- def main(args : Array[String]) : Unit =
- {
- System.out.println("Howdy, from packaged code!")
- args.foreach((i) => System.out.println("Got " + i) )
- }
- }
- }
用哪一種樣式看起來都比較合適,因?yàn)樗鼈兌季幾g出一樣的代碼構(gòu)造(Scala 將繼續(xù)編譯并和 javac 一樣在聲明包的子目錄中生成 .class 文件)。
#p#
導(dǎo)入
與包相對(duì)的當(dāng)然就是 import 了,Scala 使用它將名稱放入當(dāng)前詞法名稱空間。本系列的讀者已經(jīng)在此前的很多例子中見到過 import 了,但現(xiàn)在我將指出一些讓 Java 開發(fā)人員大吃一驚的 import 的特性。
首先,import 可以用于客戶機(jī) Scala 文件內(nèi)的任何地方,并非只可以用在文件的頂部,這樣就有了作用域的關(guān)聯(lián)性。因此,在清單 3 中,java.math.BigInteger 導(dǎo)入的作用域被完全限定到了在 App 對(duì)象內(nèi)部定義的方法,其他地方都不行。如果 mathfun 內(nèi)的其他類或?qū)ο笠胧褂?java.math.BigInteger,就需要像 App 一樣導(dǎo)入該類。如果 mathfun 的幾個(gè)類都想使用 java.math.BigInteger,可以在 App 的定義以外的包級(jí)別導(dǎo)入該類,這樣在包作用域內(nèi)的所有類就都導(dǎo)入 BigInteger 了。
清單 3. 導(dǎo)入的作用域
- package com
- {
- package tedneward
- {
- package scala
- {
- // ...
- package mathfun
- {
- object App
- {
- import java.math.BigInteger
- def factorial(arg : BigInteger) : BigInteger =
- {
- if (arg == BigInteger.ZERO) BigInteger.ONE
- else arg multiply (factorial (arg subtract BigInteger.ONE))
- }
- def main(args : Array[String]) : Unit =
- {
- if (args.length > 0)
- System.out.println("factorial " + args(0) +
- " = " + factorial(new BigInteger(args(0))))
- else
- System.out.println("factorial 0 = 1")
- }
- }
- }
- }
- }
- }
不只如此,Scala 還不區(qū)分高層成員和嵌套成員,所以您不僅可以使用 import 將嵌套類型的成員置于詞法作用域中,其他任何成員均可;例如,您可以通過導(dǎo)入 java.math.BigInteger 內(nèi)的所有名稱,使對(duì) ZERO 和 ONE 的限定了作用域的引用縮小為清單 4 中的名稱引用:
清單 4. 靜態(tài)導(dǎo)入
- package com
- {
- package tedneward
- {
- package scala
- {
- // ...
- package mathfun
- {
- object App
- {
- import java.math.BigInteger
- import BigInteger._
- def factorial(arg : BigInteger) : BigInteger =
- {
- if (arg == ZERO) ONE
- else arg multiply (factorial (arg subtract ONE))
- }
- def main(args : Array[String]) : Unit =
- {
- if (args.length > 0)
- System.out.println("factorial " + args(0) +
- " = " + factorial(new BigInteger(args(0))))
- else
- System.out.println("factorial 0 = 1")
- }
- }
- }
- }
- }
- }
您可以使用下劃線(還記得 Scala 中的通配符吧?)有效地告知 Scala 編譯器 BigInteger 內(nèi)的所有成員都需要置入作用域。由于 BigInteger 已經(jīng)被先前的導(dǎo)入語句導(dǎo)入到作用域中,因此無需顯式地使用包名限定類名。實(shí)際上,可以將所有這些都結(jié)合到一個(gè)語句中,因?yàn)?import 可以同時(shí)導(dǎo)入多個(gè)目標(biāo),目標(biāo)間用逗號(hào)隔開(如清單 5 所示):
清單 5. 批量導(dǎo)入
- package com
- {
- package tedneward
- {
- package scala
- {
- // ...
- package mathfun
- {
- object App
- {
- import java.math.BigInteger, BigInteger._
- def factorial(arg : BigInteger) : BigInteger =
- {
- if (arg == ZERO) ONE
- else arg multiply (factorial (arg subtract ONE))
- }
- def main(args : Array[String]) : Unit =
- {
- if (args.length > 0)
- System.out.println("factorial " + args(0) +
- " = " + factorial(new BigInteger(args(0))))
- else
- System.out.println("factorial 0 = 1")
- }
- }
- }
- }
- }
- }
這樣您可以節(jié)省一兩行代碼。注意這兩個(gè)導(dǎo)入過程不能結(jié)合:先導(dǎo)入 BigInteger 類本身,再導(dǎo)入該類中的各種成員。
也可以使用 import 來引入其他非常量的成員。例如,考慮一下清單 6 中的數(shù)學(xué)工具庫(或許不一定有什么價(jià)值):
清單 6. Enron 的記帳代碼
- package com
- {
- package tedneward
- {
- package scala
- {
- // ...
- package mathfun
- {
- object BizarroMath
- {
- def bizplus(a : Int, b : Int) = { a - b }
- def bizminus(a : Int, b : Int) = { a + b }
- def bizmultiply(a : Int, b : Int) = { a / b }
- def bizdivide(a : Int, b : Int) = { a * b }
- }
- }
- }
- }
- }
使用這個(gè)庫會(huì)越來越覺得麻煩,因?yàn)槊空?qǐng)求它的一個(gè)成員,都需要鍵入 BizarroMath,但是 Scala 允許將 BizarroMath 的每一個(gè)成員導(dǎo)入最高層的詞法空間,因此簡(jiǎn)直就可以把它們當(dāng)成全局函數(shù)來使用(如清單 7所示):
清單 7. 計(jì)算 Enron的開支
- package com
- {
- package tedneward
- {
- package scala
- {
- package demonstration
- {
- object App2
- {
- def main(args : Array[String]) : Unit =
- {
- import com.tedneward.scala.mathfun.BizarroMath._
- System.out.println("2 + 2 = " + bizplus(2,2))
- }
- }
- }
- }
- }
- }
還有其他的一些構(gòu)造很有趣,它們?cè)试S Scala 開發(fā)人員寫出更自然的 2 bizplus 2,但是這些內(nèi)容本文不予討論(想了解 Scala 潛在的可以用于其他用途的特性的讀者可以看一下 Odersky、Spoon 和 Venners 所著的 Programming in Scala 中談到的 Scala implicit 構(gòu)造)。
#p#
訪問
打包(和導(dǎo)入)是 Scala 封裝的一部分,和在 Java 代碼中一樣,在 Scala 中,打包很大一部分在于以選擇性方式限定訪問特定成員的能力 — 換句話說,在于 Scala 將特定成員標(biāo)記為 “公有(public)”、“private(私有)” 或介于兩者之間的成員的能力。
Java 語言有四個(gè)級(jí)別的訪問:公有(public)、私有(private)、受保護(hù)的(protected )和包級(jí)別(它沒有任何關(guān)鍵詞)訪問。Scala:
廢除了包級(jí)別的限制(在某種程度上)
默認(rèn)使用 “公有”
指定 “私有” 表示 “只有此作用域可訪問”
相反,Scala 定義 “protected” 的方式與在 Java 代碼中不同;Java protected 成員對(duì)于子類和在其中定義成員的包來說是可訪問的,Scala 中則僅有子類可訪問。這意味著 Scala 版本的 protected 限制性要比 Java 版本更嚴(yán)格(雖然按理說更加直觀)。
然而,Scala 真正區(qū)別于 Java 代碼的地方是 Scala 中的訪問修飾符可以用包名來 “限定”,用以表明直到 哪個(gè)訪問級(jí)別才可以訪問成員。例如,如果 BizarroMath 包要將成員訪問權(quán)限授權(quán)給同一包中的其他成員(但不包括子類),可以用清單 8 中的代碼來實(shí)現(xiàn):
清單 8. Enron 的記帳代碼
- package com
- {
- package tedneward
- {
- package scala
- {
- // ...
- package mathfun
- {
- object BizarroMath
- {
- def bizplus(a : Int, b : Int) = { a - b }
- def bizminus(a : Int, b : Int) = { a + b }
- def bizmultiply(a : Int, b : Int) = { a / b }
- def bizdivide(a : Int, b : Int) = { a * b }
- private[mathfun] def bizexp(a : Int, b: Int) = 0
- }
- }
- }
- }
- }
注意此處的 private[mathfun] 表達(dá)。本質(zhì)上,這里的訪問修飾符是說該成員直到 包 mathfun 為止都是私有的;這意味著包 mathfun 的任何成員都有權(quán)訪問 bizexp,但任何包以外的成員都無權(quán)訪問它,包括子類。
這一點(diǎn)的強(qiáng)大意義就在于任何包都可以使用 “private” 或者 “protected” 聲明甚至 com(乃至 _root_,它是根名稱空間的別名,因此本質(zhì)上 private[_root_] 等效于 “public” 同)進(jìn)行聲明。這使得 Scala 能夠?yàn)樵L問規(guī)范提供一定程度的靈活性,遠(yuǎn)遠(yuǎn)高于 Java 語言所提供的靈活性。
實(shí)際上,Scala 提供了一個(gè)更高程度的訪問規(guī)范:對(duì)象私有 規(guī)范,用 private[this] 表示,它規(guī)定只有被同一對(duì)象調(diào)用的成員可以訪問有關(guān)成員,其他對(duì)象里的成員都不可以,即使對(duì)象的類型相同(這彌合了 Java 訪問規(guī)范系統(tǒng)中的一個(gè)缺口,這個(gè)缺口除對(duì) Java 編程問題有用外,別無他用。)
注意訪問修飾符必須在某種程度上在 JVM 之上映射,這致使定義中的細(xì)枝末節(jié)會(huì)在從正規(guī) Java 代碼中調(diào)用或編譯時(shí)丟失。例如,上面的 BizarroMath 示例(用 private[mathfun] 聲明的成員 bizexp)將會(huì)生成清單 9 中的類定義(當(dāng)用 javap 來查看時(shí)):
Listing 9. Enron 的記帳庫,JVM 視圖
- Compiled from "packaging.scala"
- public final class com.tedneward.scala.mathfun.BizarroMath
- extends java.lang.Object
- {
- public static final int $tag();
- public static final int bizexp(int, int);
- public static final int bizdivide(int, int);
- public static final int bizmultiply(int, int);
- public static final int bizminus(int, int);
- public static final int bizplus(int, int);
- }
在編譯的 BizarroMath 類的第二行很容易看出,bizexp() 方法被賦予了 JVM 級(jí)別的 public 訪問修飾符,這意味著一旦 Scala 編譯器結(jié)束訪問檢查,細(xì)微的 private[mathfun] 區(qū)別就會(huì)丟失。因此,對(duì)于那些要從 Java 代碼使用的 Scala 代碼,我寧愿堅(jiān)持傳統(tǒng)的 “private” 和 “public” 的定義(甚至 “protected” 的定義有時(shí)最終映射到 JVM 級(jí)別的 “public”,所有不確定的時(shí)候,請(qǐng)對(duì)照實(shí)際編譯的字節(jié)碼參考一下 javap,以確認(rèn)其訪問級(jí)別。)
#p#
應(yīng)用
在本系列上一期的文章中(“集合類型”),當(dāng)談及 Scala 中的數(shù)組時(shí)(確切地說是 Array[T])我說過:“獲取數(shù)組的第 i 個(gè)元素” 實(shí)際上是 “那些名稱很有趣的方法中的一種……”。盡管當(dāng)時(shí)是因?yàn)槲也幌肷钊爰?xì)節(jié),但不管怎么說事實(shí)證明這種說法嚴(yán)格來說 是不對(duì)的。
好吧,我承認(rèn),我說謊了。
技術(shù)上講,在 Array[T] 類上使用圓括號(hào)要比使用 “名稱有趣的方法” 復(fù)雜一點(diǎn);Scala 為特殊的字符序列(即那些有左右括號(hào)的序列)保留了一個(gè)特殊名稱關(guān)聯(lián),因?yàn)樗兄厥獾氖褂靡鈭D :“做”……(或按函數(shù)來說,將……“應(yīng)用” 到……)。
換句話說,Scala 有一個(gè)特殊的語法(更確切一些,是一個(gè)特殊的語法關(guān)系)來代替 “應(yīng)用” 操作符 “()”。更精確地說,當(dāng)用 () 作為方法調(diào)用來調(diào)用所述對(duì)象時(shí),Scala 將稱為 apply() 的方法作為調(diào)用的方法。例如,一個(gè)想充當(dāng)仿函數(shù)(functor)的類(一個(gè)充當(dāng)函數(shù)的對(duì)象)可以定義一個(gè) apply 方法來提供類似于函數(shù)或方法的語義:
清單 10. 使用 Functor!
- class ApplyTest
- {
- import org.junit._, Assert._
- @Test def simpleApply =
- {
- class Functor
- {
- def apply() : String =
- {
- "Doing something without arguments"
- }
- def apply(i : Int) : String =
- {
- if (i == 0)
- "Done"
- else
- "Applying... " + apply(i - 1)
- }
- }
- val f = new Functor
- assertEquals("Doing something without arguments", f() )
- assertEquals("Applying... Applying... Applying... Done", f(3))
- }
- }
好奇的讀者會(huì)想是什么使仿函數(shù)不同于匿名函數(shù)或閉包呢?事實(shí)證明,它們之間的關(guān)系相當(dāng)明顯:標(biāo)準(zhǔn) Scala 庫中的 Function1 類型(指包含一個(gè)參數(shù)的函數(shù))在其定義上有一個(gè) apply 方法。快速瀏覽一些為 Scala 匿名函數(shù)生成的 Scala 匿名類,您就會(huì)明白生成的類是 Function1(或者 Function2 或 Function3,這要看該函數(shù)使用了幾個(gè)參數(shù))的后代。
這意味著當(dāng)匿名的或者命名的函數(shù)不一定適合期望設(shè)計(jì)方法時(shí),Scala 開發(fā)人員可以創(chuàng)建一個(gè) functor 類,提供給它一些初始化數(shù)據(jù),保存在字段中,然后通過 () 執(zhí)行它,無需任何通用基類(傳統(tǒng)的策略模式實(shí)現(xiàn)需要這個(gè)類):
清單 11. 使用 Functor!
- class ApplyTest
- {
- import org.junit._, Assert._
- // ...
- @Test def functorStrategy =
- {
- class GoodAdder
- {
- def apply(lhs : Int, rhs : Int) : Int = lhs + rhs
- }
- class BadAdder(inflateResults : Int)
- {
- def apply(lhs : Int, rhs : Int) : Int = lhs + rhs * inflateResults
- }
- val calculator = new GoodAdder
- assertEquals(4, calculator(2, 2))
- val enronAccountant = new BadAdder(50)
- assertEquals(102, enronAccountant(2, 2))
- }
- }
任何提供了被適當(dāng)賦予了參數(shù)的 apply 方法的類,只要這些參數(shù)都按數(shù)字和類型排列了起來,它們都會(huì)在被調(diào)用時(shí)運(yùn)行。
結(jié)束語
Scala 的打包、導(dǎo)入和訪問修飾符機(jī)制提供了傳統(tǒng) Java 編程人員從未享受過的更高級(jí)的控制和封裝。例如,它們提供了導(dǎo)入一個(gè)對(duì)象的選擇方法的能力,使它們看起來就像全局方法一樣,而且還克服了全局方法的傳統(tǒng)的缺點(diǎn);它們使得使用那些方法變得極其簡(jiǎn)單,尤其是當(dāng)這些方法提供了諸如本系列早期文章(“Scala 控制結(jié)構(gòu)內(nèi)部揭密”)引入的虛構(gòu)的 tryWithLogging 函數(shù)這樣的高級(jí)功能時(shí)。
同樣,“應(yīng)用” 機(jī)制允許 Scala 隱藏函數(shù)部分的執(zhí)行細(xì)節(jié),這樣,編程人員可能會(huì)不知道(或不在乎)他們正調(diào)用的東西 事實(shí)上不是一個(gè)函數(shù),而是一個(gè)非常復(fù)雜的對(duì)象。該機(jī)制為 Scala 機(jī)制的函數(shù)特性提供了另一個(gè)方面,當(dāng)然 Java 語言(或者 C# 或 C++)也提供了這個(gè)方面,但是它們提供的語法純度沒有 Scala 的高。
【相關(guān)閱讀】
- Scala編程語言專題
- 面向Java開發(fā)人員的Scala指南:使用元組、數(shù)組和列表
- 面向Java開發(fā)人員的Scala指南:當(dāng)繼承中的對(duì)象遇到函數(shù)
- 面向Java開發(fā)人員的Scala指南:使用Scala版本的Java接口
- 面向Java開發(fā)人員的Scala指南:Scala控制結(jié)構(gòu)內(nèi)部揭密
當(dāng)前名稱:從Java走進(jìn)Scala:包和訪問修飾符
網(wǎng)站網(wǎng)址:http://fisionsoft.com.cn/article/djoeshd.html


咨詢
建站咨詢
