快學Scala學習筆記及習題解答(15-16註解與XML處理)
本文Scala使用的版本是2.11.8
第15章 註解
15.1 基本概念
註解是那些你插入到程式碼中以便有工具可以對它們進行處理的標籤。
在Scala裡,可以為類、方法、欄位、區域性變數和引數添加註解。可以同時新增多個註解(先後次序沒有影響)。
主構造器
需要將註解放置在構造器之前,如果不帶引數的話,需加上一對圓括號。
class Credentials @Inject() (var username: String, var password: String)
表示式
需要在表示式後加上冒號,然後是註解本身。
(myMap.get(key): @unchecked) match { ... }
型別引數
class MyContainer[@speciallized T]
實際型別
String @cps[Unit] // 在型別名稱之後,@cps帶一個型別引數
15.2 註解引數
可以有帶名引數,但如果引數名為value,則該名稱可以直接略去。如果註解不帶引數,則圓括號可以略去。
Java註解的引數型別只能是:
- 數值型的字面量
- 字串
- 類字面量
- Java列舉
- 其他註解
- 上述型別的陣列(但不能使陣列的陣列)
Scala註解的引數可以是任何型別。
15.3 註解實現
註解必須擴充套件Annotation特質。
class unchecked extends annotation.Annotation
註解類可以選擇擴充套件StaticAnnotation或ClassfileAnnotation特質。StaticAnnotation在編譯單元中可見——它將放置Scala特有的元資料到類檔案中。而ClassfileAnnotation的本意是在類檔案中生成Java註解元資料。
15.4 針對Java特性的註解
Java修飾符
Scala註解 | Java修飾符 | 描述 |
---|---|---|
@volatile | volatile | 欄位可以被多個欄位同時更新 |
@transient | transient | 欄位不會被序列化 |
@strictfp | strictfp | 使用IEEE的double值進行浮點計算,而不是80位擴充套件精度(Intel處理器預設使用的實現) |
@native | native | 標記那些在C或C++程式碼中實現的方法 |
標記介面
Scala註解 | Java介面 | 描述 |
---|---|---|
@cloneable | Cloneable | 標記可被克隆的物件 |
@remote | java.rmi.Remote | 遠端物件 |
@SerialVersionUID | Serialization | @SerialVersionUID已過時,需擴充套件scala.Serialization特質 |
受檢異常
如果從Java程式碼中呼叫Scala的方法,其簽名應包含那些可能被丟擲的受檢異常。用@throws註解來生成正確的簽名。
class Book {
@throws(classOf[IOException]) def read(fileName: String) {
...
}
...
}
變長引數
def process(args: String*)
// 將被編譯成
def process(args: Seq[String])
使用@varargs
@varargs def process(args: String*)
// 將被編譯成如下Java方法
void process(String... args)
JavaBeans
新增上@scala.reflect.BeanProperty註解,編譯器將生成JavaBean風格的getter和setter方法。
@BooleanBeanProperty生成帶有is字首的getter方法,用於Boolean。
15.5 用於優化的註解
尾遞迴
遞迴呼叫有時能被轉化為迴圈。
例如
def sum(xs: Seq[Int], partial: BigInt): BigInt =
if (xs.isEmpty) partial else sum(xs.tail, xs.head + partial)
Scala編譯器會自動應用“尾遞迴”優化。
有時Scala編譯器無法進行尾遞迴優化,則應該給你的方法加上@tailrec註解。
跳轉表生成與內聯
@switch註解讓你檢查Scala的match語句是否真的被編譯成了跳轉表
(n: @switch) match {
case 0 => "Zero"
case 1 => "One"
case _ => "?"
}
@inline和@noinline來告訴Scala編譯器要不要內聯。
可省略方法
@elidable註解給那些可以在生產程式碼中移除的方法上打標記,elidable物件定義瞭如下數值常量:
- MAXIMUM 或 OFF = Int.MaxValue
- ASSERTION = 2000
- SERVERE = 1000
- WARNING = 900
- INFO = 800
- CONFIG = 700
- FINE = 500
- FINER = 400
- FINEST = 300
- MINIMUM 或 ALL = Int.MinValue
預設註解的值低於1000的方法會被省略,剩下SEVERE的方法和斷言。
可以使用-Xelide-below修改省略等級
scalac -Xelide-below INFO myprog.scala
對被省略的方法呼叫,編譯器會替換成Unit物件。如果使用了返回值,則會丟擲ClassCastException。
基本型別的特殊化
def allDifferent[T](x: T, y: T, z: T) = x != y && x != z && y != z
如果呼叫allDifferent(2, 3, 4)時,每個整數值都被包裝成一個java.lang.Integer。當然可以給出過載版本,以及其他7個基本型別的過載方法。
而使用@specialized註解,編譯器會自動生成這些方法。
def allDifferent[@specialized T](x: T, y: T, z: T) = x != y && x != z && y != z
// 或者限定子集
def allDifferent[@specialized(Long, Double) T](x: T, y: T, z: T) = x != y && x != z && y != z
15.6 用於錯誤和警告的註解
@deprecated註解,當編譯器遇到對這個特性的使用時都會生成一個警告資訊。
@deprecated(message="Use factorial(n: BigInt) instead"
def factorial(n: Int): Int = ...
@deprecatedName可以被應用到引數上。
@implicitNotFound註解用於在某個隱式引數不存在的時候生成有意義的錯誤提示。
@unchecked註解用於在匹配不完整時取消警告資訊。
@uncheckedVariance註解會取消與型變相關的錯誤提示。
15.7 習題解答
未完待續
1. 編寫四個JUnit測試用例,分別使用帶或不帶某個引數的@Test註解。用JUnit執行這些測試。
// 實體類
package com.zw.demo.fifteen
/**
* Created by zhangws on 17/1/28.
*/
object One {
def main(args: Array[String]) {
}
}
class ScalaTest {
def test1(): Unit = {
}
def test2(): Unit = {
}
}
// JUnit測試類
package com.zw.demo.fifteen
import org.junit.Test
/**
* Created by zhangws on 17/1/28.
*/
class ScalaTestTest {
@Test
def testTest1() {
System.out.println("test1");
}
@Test(timeout = 1L)
def testTest2() {
System.out.println("test2");
}
}
2. 建立一個類的示例,展示註解可以出現的所有位置。用@deprecated作為你的示例註解。
@deprecated
class Two {
@deprecated
var t: String = _
@deprecated(message = "unuse")
def hello() {
println("hello")
}
}
@deprecated
object Two extends App {
val t = new Two()
t.hello()
t.t
}
3. Scala類庫中的哪些註解用到了元註解@param,@field,@getter,@setter,@beanGetter或@beanSetter?
略
4. 編寫一個Scala方法sum,帶有可變長度的整型引數,返回所有引數之和。從Java呼叫該方法。
// Four.scala
class Four {
@varargs def sum(nums: Int*): Int = {
nums.sum
}
}
// FourTest.java
public class FourTest {
public static void main(String[] args) {
Four t = new Four();
System.out.println(t.sum(1, 2, 3));
}
}
5. 編寫一個返回包含某檔案所有行的字串的方法。從Java呼叫該方法。
// Five.scala
class Five {
def read() = {
Source.fromFile("Four.scala").mkString
}
}
// FiveTest.java
public class FiveTest {
public static void main(String[] args) {
Five t = new Five();
System.out.println(t.read());
}
}
6. 編寫一個Scala物件,該物件帶有一個易失(volatile)的Boolean欄位。讓某一個執行緒睡眠一段時間,之後將該欄位設為true,列印訊息,然後退出。而另一個執行緒不停的檢查該欄位是否為true。如果是,它將列印一個訊息並退出。如果不是,則它將短暫睡眠,然後重試。如果變數不是易失的,會發生什麼?
待續
7. 給出一個示例,展示如果方法可被重寫,則尾遞迴優化為非法
待續
8. 將allDifferent方法新增到物件,編譯並檢查位元組碼。@specialized註解產生了哪些方法?
待續
9. Range.foreach方法被註解為@specialized(Unit)。為什麼?通過以下命令檢查位元組碼:
javap -classpath /path/to/scala/lib/scala-library.jar scala.collection.immutable.Range
並考慮Function1上的@specialized註解。點選Scaladoc中的Function1.scala連結進行檢視。
待續
10. 新增assert(n >= 0)到factorial方法。在啟用斷言的情況下編譯並校驗factorial(-1)會拋異常。在禁用斷言的情況下編譯。會發生什麼?用javap檢查該斷言呼叫
待續
第16章 XML處理
處理XML的jar包(參考):
<dependency>
<groupId>org.scala-lang.modules</groupId>
<artifactId>scala-xml_${scala.binary.version}</artifactId>
<version>1.0.6</version>
</dependency>
<dependency>
<groupId>org.scala-lang.modules</groupId>
<artifactId>scala-parser-combinators_${scala.binary.version}</artifactId>
<version>1.0.4</version>
</dependency>
<dependency>
<groupId>org.scala-lang.modules</groupId>
<artifactId>scala-swing_${scala.binary.version}</artifactId>
<version>1.0.2</version>
</dependency>
16.1 XML字面量
Scala對XML有內建支援。
val doc = <html><head><title>XML字面量</title></head><body>...</body></html>
doc的型別為scala.xml.Elem,表示一個XML元素。
也可以表示一系列節點,如下型別為scala.xml.NodeSeq。
val items = <li>節點一</li><li>節點二</li>
16.2 XML節點
16.3 元素屬性
可以使用attributes屬性訪問某個元素的屬性鍵和值,它產出一個型別為MetaData的物件。
val elem = <a href="https://scala-lang.org/">The Scala Language</a>
val url = elem.attributes("href")
但如果文字中有無法解析的字元時,會有問題
如果不存在未被解析的實體,可以使用text方法。
val url = elem.attributes("href").text
// 如果不喜歡處理null,可以使用get,它返回Option[Seq[Node]]
val url = elem.attributes.get("href").getOrElse(Text(""))
遍歷所有屬性
for (attr <- elem.attributes)
// 處理attr.key和attr.value.text
// 或者
val map = elem.attributes.asAttrMap
16.4 內嵌表示式
可以在XML字面量中包含Scala程式碼塊,動態地計算出元素內容,例如:
def main(args: Array[String]) {
val s = "Hello"
val v = <ul>{for (i <- 0 until s.length) yield <li>{i}</li>}</ul>
println(v)
}
16.5 在屬性中使用表示式
如果內嵌程式碼塊返回null或None,該屬性不會被設定。
<img alt={if (description == "TODO" null else description} ... />
16.6 特殊節點型別
PCData
如果輸出中帶有CDATA,可以包含一個PCData節點。
val code = """if (temp < 0) alert("Cold!")"""
val js = <script>{PCData(code)}</script>
Unparsed
可以在Unparsed節點中包含任意文字,它會被原樣保留。
val n1 = <xml:unparsed><&></xml:unparsed>
val n2 = Unparsed("<&>")
group
val g1 = <xml:group><li>Item 1</li><li>Item 2</li></xml:group>
val g2 = Group(Seq(<li> Item 1</li>, <li>Item 2</li>))
遍歷這些組時,它們會自動被解開:
val items = <li>Item 1</li><li>Item 2</li>
for (n <- <xml:group>{items}</xml:group>) yield n
// 產生出兩個li元素
// <li>Item 1</li>
// <li>Item 2</li>
for (n <- <col>{items}</col>) yield n
// 產生一個col元素
// <col><li>Item 1</li><li>Item 2</li></col>
16.7 類XPath表示式
NodeSeq類提供了類似XPath中/
和//
操作符的方法,只不過在Scala中用\
和\\
來代替。
\
定位某個節點或節點序列的直接後代:
val list = <dl><dt>Java</dt><dd>Gosling</dd><dt>Scala</dt><dd>Odersky</dd></dl>
println(list \ "dt")
// 結果
<dt>Java</dt><dt>Scala</dt>
萬用字元可以匹配任何元素:
doc \ "body" \ "_" \ "li"
// 將找到所有li元素
\\
可以定位任何深度的後代:
doc \\ "img"
// 將定位doc中任何位置的所有img元素
@
開頭的字串可以定位屬性
img \ "@alt"
// 將返回給定節點的alt屬性
doc \\ "@alt"
// 將定位到doc中任何元素的所有alt屬性
16.8 模式匹配
node match {
case <img/> => ... // 匹配帶有任何屬性但沒有後代的img元素
case <li>{_}</li> => ... // 匹配單個後代
case <li>{_*}</li> => ... // 匹配任意多的項
case <li>{child}</li> => ... // 使用變數名,成功匹配到的內容會被繫結到該變數上
case <li>{Text(item)}</li> => item // 匹配一個文字節點
case <li>{children @ _*}</li> => for (c <- children) yield c // 把節點序列綁到變數,children是一個Seq[Node]
case <p>{_*}</p><br/> => ... // 非法,只能用一個節點
case <img alt="TODO"/> => ... // 非法,不能帶有屬性
case n @ <img/> if (n.attributes("alt").text == "TODO") => ... // 使用守衛,匹配屬性
}
16.9 修改元素和屬性
在Scala中,XML節點和節點序列是不可變。所以編輯只能通過拷貝(copy方法),它有五個帶名引數:label、attributes、child、prefix和scope。
val list = <ul><li>Fred</li><li>Wilma</li></ul>
val list2 = list.copy(label = "ol")
// 結果
// <ol><li>Fred</li><li>Wilma</li></ol>
// 新增後代
list.copy(child = list.child ++ <li>Another item</li>)
// 新增或修改一個屬性,使用%操作符
val image = <img src="hamster.jpg"/>
val image2 = image % Attribute(null, "alt", "An image of a hamster", scala.xml.Null)
// 第一個引數名稱空間,最後一個是額外的元資料列表
// 新增多個屬性
val image3 = image % Attribute(null, "alt", "An image of a frog", Attribute(null, "src", "frog.jpg", scala.xml.Null))
16.10 XML變換
XML類庫提供了一個RuleTransformer類,該類可以將一個或多個RewriteRule例項應用到某個節點及其後代。
例如:把文件中所有的ul節點都修改為ol
val rule1 = new RewriteRule {
override def transform(n: Node) = n match {
case e @ <ul>{_*}</ul> => e.asInstanceOf[Elem].copy(label = "ol")
case _ => n
}
}
val transformer = new RuleTransformer(rule1, rule2, rule3);
// transform方法遍歷給定節點的所有後代,應用所有規則,最後返回經過變換的樹
16.11 載入和儲存
16.11.1 載入
loadFile
import scala.xml.XML
val root = XML.loadFile("myfile.xml")
InputStream、Reader或URL載入
val root2 = XML.load(new FileInputStream("myfile.xml"))
val root3 = XML.load(new InputStreamReader(new FileInputStream("myfile.xml", "UTF-8"))
val root4 = XML.load(new URL("http://horstmann.com/index.html"))
ConstructingParser
該解析器可以保留註釋、CDATA節和空白(可選):
import scala.xml.parsing.ConstructingParser
import java.io.File
val parser = ConstructingParser.fromFile(new File("myfile.xml"), perserveWS = true)
val doc = parser.document
val root = doc.docElem
16.11.2 儲存
save
XML.save("myfile.xml", root)
有以下三個可選引數:
- enc指定編碼(預設“IOS-8859-1”)
- xmlDecl用來指定輸出中最開始是否要生成XML宣告( <?xml...?> )(預設為false)
- doctype是樣例類scala.xml.dtd.DocType的物件(預設為null)
// 示例
XML.save("myfile.xhtml", root,
enc = "UTF-8",
xmlDecl = true,
doctype = DocType("html",
PublicID("-//W3C//DTD XHTML 1.0 Strict//EN",
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"),
Nil))
Writer
XML.write(writer, root, "UTF-8", false, null)
// 沒有內容的元素不會被寫成自結束的標籤,預設樣式
// <img src="hamster.jpg"></img>
// 如果想要<img src="hamster.jpg"/>,使用
val str = xml.Utility.toXML(node, minimizeTags = true)
// 如果想排版美觀,可以用PrettyPrinter類
val printer = new PrettyPrinter(width = 100, step = 4)
val str = printer.formatNodes(nodeSeq)
16.12 名稱空間
XML的名稱空間是一個URI(通常也是URL)。xmlns屬性可以宣告一個名稱空間:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>...</head>
<body>...</body>
</html
// html元素及其後代(head、body等)將被放置在這個名稱空間當中。
可以引入自己的名稱空間
<svg xmlns="http://www.w3.org/2000/svg" ...>
...
</svg>
在Scala中,每個元素都有一個scope屬性,型別為NamespaceBinding。該類的uri屬性將輸出名稱空間的URI。
名稱空間字首
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg">
<svg:svg width="100" height="100">
...
</svg>
程式設計生成XML元素
val scope = new NamespaceBinding("svg", "http://www.w3.org/2000/svg", TopScope)
val attrs = Attribute(null, "width", 100,
Attribute(null, "height", 100, Null))
val elem = Elem(null, "body", Null, TopScope,
Elem("svg", "svg", attrs, scope))
16.13 習題解答
未完待續
1. <fred/>(0)
得到什麼?<fred/>(0)(0)
呢?為什麼?
scala> println(<fred/>(0))
<fred/>
scala> println(<fred/>(0)(0))
<fred/>
因為都是scala.xml.Node,是NodeSeq的子類,等同於長度為1的序列。
2. 如下程式碼的值是什麼?
<ul>
<li>Opening bracket: [</li>
<li>Closing bracket: ]</li>
<li>Opening bracket: {</li>
<li>Closing bracket: }</li>
</ul>
<ul>
<li>Opening bracket: [</li>
<li>Closing bracket: ]</li>
<li>Opening bracket: {{</li>
<li>Closing bracket: }}</li>
</ul>
花括號作為字面量,需要連寫兩個。
3. 對比
<li>Fred</li> match { case <li>{Text(t)}</li> => t }
和
<li>{"Fred"}</li> match { case <li>{Text(t)}</li> => t }
為什麼它們的行為不同?
前一個輸出Fred,後一個異常scala.MatchError: <li>Fred</li> (of class scala.xml.Elem)
4. 讀取一個XHTML檔案並列印所有不帶alt屬性的img元素。
5. 列印XHTML檔案中所有影象的名稱,即列印所有位於img元素內的src屬性值。
6. 讀取XHTML檔案並列印一個包含了檔案中給出的所有超連結及其url的表格。即,列印所有a元素的child文字和href屬性。
7. 編寫一個函式,帶一個型別為Map[String,String]的引數,返回一個dl元素,其中針對對映中每個鍵對應有一個dt,每個值對應有一個dd,例如:
Map(“A”->”1”,”B”->”2”)
應該產出
<dl><dt>A</dt><dd>1</dd><dt>B</dt><dd>2</dd></dl>
8. 編寫一個函式,接受dl元素,將它轉成Map[String,String]。該函式應該是前一個練習中的反向處理,前提是所有dt後代都是唯一的。
9. 對一個XHTML文件進行變換,對所有不帶alt屬性的img元素新增一個alt=”TODO”屬性,其他內容完全不變。
10. 編寫一個函式,讀取XHTML文件,執行前一個練習中的變換,並儲存結果。確保保留了DTD以及所有CDATA內容。