本節翻譯自html
綜述:本節中你將會學習如何使用Scala實現類,以及Scala相比Java更加精簡的表示法帶來的便利。同時介紹了object的語法結構(Scala沒有靜態方法或靜態字段,但object能夠達到一樣的效果)。java
Scala 中的類是建立對象的模板。它們能夠包含統稱爲成員的方法、值、變量、類型、對象、特徵和類。以後將介紹類型、對象和特徵。node
最小的類定義就是關鍵字 class
和標識符。類名應該大寫。程序員
class User val user = new User
關鍵字 new
用於建立類的一個實例。User
有一個不帶參數的默認構造函數,由於沒有定義構造函數。可是,你一般須要構造函數和類體。下面是一個示例類的定義:dom
class Point(var x: Int, var y: Int) { def move(dx: Int, dy: Int): Unit = { x = x + dx y = y + dy } override def toString: String = s"($x, $y)" } val point1 = new Point(2, 3) point1.x // 2 println(point1) // prints (x, y)
這個 Point
類有四個成員:變量 x
和 y
,以及方法 move
和 toString
。與其餘語言不一樣的是,主構造函數在類簽名中 (var x:Int, var y:Int)
。move
方法接受兩個整數參數,並返回 Unit
:不包含任何信息的值 ()
。這大體至關於java類語言中的 void
。另外一方面,toString
不接受任何參數,但返回一個字符串值。因爲 toString
覆蓋了 AnyRef
中的 toString
,它被關鍵字 override
所標記。ide
經過提供默認值,構造函數能夠具備可選參數,以下所示:函數
class Point(var x: Int = 0, var y: Int = 0) val origin = new Point // x and y are both set to 0 val point1 = new Point(1) println(point1.x) // prints 1
在這個版本的 Point
類中,x
和 y
的默認值爲 0
,因此不須要參數。可是,由於構造函數會讀取從左到右的參數,若是你只想傳遞一個 y
值,那麼你須要加上該參數的名稱。學習
class Point(var x: Int = 0, var y: Int = 0) val point2 = new Point(y=2) println(point2.y) // prints 2
這也是一種提升清晰度的好習慣。ui
默認狀況下,成員是公開的。使用 private
修飾符可以使得它們對類外部來講是不可見的。scala
class Point { private var _x = 0 private var _y = 0 private val bound = 100 def x = _x def x_= (newValue: Int): Unit = { if (newValue < bound) _x = newValue else printWarning } def y = _y def y_= (newValue: Int): Unit = { if (newValue < bound) _y = newValue else printWarning } private def printWarning = println("WARNING: Out of bounds") } val point1 = new Point point1.x = 99 point1.y = 101 // prints the warning
在這個版本的 Point
類中,數據存儲在私有變量 _x
和 _y
中。而定義的方法 def x
和 def y
則能夠訪問私有數據。def x_=
和 def y_=
用於驗證和設置 _x
和 _y
的值。請注意 setter
的特殊語法:方法將 _=
附加到 getter
的標識符後面,而且跟着參數。
使用 val
和 var
的主構造函數參數是公開的。可是,由於 val
是不可變的,因此不能寫下面的內容。
class Point(val x: Int, val y: Int) val point = new Point(1, 2) point.x = 3 // <-- does not compile
沒有 val
或 var
的參數是私有值,只在類中可見。
class Point(x: Int, y: Int) val point = new Point(1, 2) point.x // <-- does not compile
「混入」是用來組成類的特性。
abstract class A { val message: String } class B extends A { val message = "I'm an instance of class B" } trait C extends A { def loudMessage = message.toUpperCase() } class D extends B with C val d = new D d.message // I'm an instance of class B d.loudMessage // I'M AN INSTANCE OF CLASS B
類 D
有一個超類 B
和一個混入類 C
。類只能有一個超類但能夠有不少混入類(分別使用關鍵字 extend
和 with
)。混入類和超類可能具備相同的超類型。
如今讓咱們從一個抽象類開始,來看一個更有趣的例子:
abstract class AbsIterator { type T def hasNext: Boolean def next(): T }
這個類有一個抽象類型 T
和一個標準的迭代器方法。
接下來,咱們將實現一個具體類(全部的抽象成員 T
、hasNext
和 next
都被實現):
class StringIterator(s: String) extends AbsIterator { type T = Char private var i = 0 def hasNext = i < s.length def next() = { val ch = s charAt i i += 1 ch } }
StringIterator
接受一個 String
而且能夠對字符串進行遍歷(例如:要查看字符串是否包含某個字符)。
如今,讓咱們建立一個也擴展了 AbsIterator
的特質。
trait RichIterator extends AbsIterator { def foreach(f: T => Unit): Unit = while (hasNext) f(next()) }
只要還有其餘元素(while(hasNext)
),此特徵經過不斷調用提供的函數 f: T => Unit
在下一個元素(next()
)上來實現 foreach
。由於 RichIterator
是一個特質,它不須要去實現 AbsIterator
裏的抽象成員。
咱們但願將 StringIterator
和 RichIterator
的功能合併到一個類中。
object StringIteratorTest extends App { class RichStringIter extends StringIterator(args(0)) with RichIterator val richStringIter = new RichStringIter richStringIter foreach println }
新的 Iter
類有一個做爲超類的 StringIterator
和一個做爲混入類的 RichIterator
。
只有單一繼承的話,咱們就沒法達到這樣的靈活性。
在 Scala 中,可讓類做將其餘類做爲本身的成員。與java語言不一樣,嵌套類是封閉類的成員,在 Scala 中,嵌套類被綁定到外部對象。假設咱們但願編譯器在編譯時阻止咱們混合哪些 Node、屬於哪些 Graph。路徑依賴類型提供了一個解決方案。
爲了說明這一差別,咱們快速地概述了 Graph 數據類型的實現:
class Graph { class Node { var connectedNodes: List[Node] = Nil def connectTo(node: Node) { if (connectedNodes.find(node.equals).isEmpty) { connectedNodes = node :: connectedNodes } } } var nodes: List[Node] = Nil def newNode: Node = { val res = new Node nodes = res :: nodes res } }
這個程序表示一個 Graph 做爲 Node 列表(List[Node]
)。每一個 Node 都有一個它鏈接到的其餘 Node 的列表(connectedNodes
)。class Node
是路徑依賴類型,由於它嵌套在 class Graph
中。所以,connectedNodes
中的全部節點必須使用來自 newNode
同一實例的 Graph
建立。
val graph1: Graph = new Graph val node1: graph1.Node = graph1.newNode val node2: graph1.Node = graph1.newNode val node3: graph1.Node = graph1.newNode node1.connectTo(node2) node3.connectTo(node1)
爲了清楚起見,咱們明確聲明瞭 node1
,node2
和 node3
的類型爲 graph1.Node
,可是編譯器能夠推斷出它。這是由於當咱們調用 graph1.newNode
,它再調用 new Node
時,該方法使用特定於實例 graph1
的 Node
實例。
若是咱們如今有兩個 Graph,那麼 Scala 的類型系統不容許將一個 Graph 中定義的 Node 與另外一個 Graph 的 Node 混合,由於另外一個 Graph 的 Node 具備不一樣的類型。 這是一個非法程序:
val graph1: Graph = new Graph val node1: graph1.Node = graph1.newNode val node2: graph1.Node = graph1.newNode node1.connectTo(node2) // legal val graph2: Graph = new Graph val node3: graph2.Node = graph2.newNode node1.connectTo(node3) // illegal!
graph1.Node
類型與 graph1.Node
類型不一樣。在 Java 中,前一個示例程序中的最後一行是正確的。對於這兩個 Graph 的 Node,Java 將分配相同類型的 graph.node
。Node
的前綴是 Graph
類。在 Scala 中,這樣的類型也能夠表達,它被寫成 Graph#Node
。若是咱們想要鏈接不一樣 Graph 的 Node,咱們必須按照如下方式改變咱們初始 Graph
實現的定義:
class Graph { class Node { var connectedNodes: List[Graph#Node] = Nil def connectTo(node: Graph#Node) { if (connectedNodes.find(node.equals).isEmpty) { connectedNodes = node :: connectedNodes } } } var nodes: List[Node] = Nil def newNode: Node = { val res = new Node nodes = res :: nodes res } }
注意,這個程序不容許咱們將一個 Node 附加到兩個不一樣的 Graph 上。若是咱們想要刪除這個限制,咱們必須將變量 Node 的類型更改成
Graph#Node
一個對象是一個只有一個實例的類。它被引用時被懶惰地建立,就像懶惰的val同樣。
做爲頂級的值,一個對象是一個單例。
做爲封閉類或本地值的成員,它的行爲徹底像一個懶惰的val。
一個對象是一個值。定義一個對象看起來和定義一個類同樣,但使用的關鍵字是 object
:
object Box
下面是一個帶有方法的對象的例子:
package logging object Logger { def info(message: String): Unit = println(s"INFO: $message") }
方法 info
能夠從程序中的任何地方導入。像這樣建立實用程序方法是單例對象的常見用例。
讓咱們看看如何在另外一個包中使用 info
:
import logging.Logger.info class Project(name: String, daysToComplete: Int) class Test { val project1 = new Project("TPS Reports", 1) val project2 = new Project("Website redesign", 5) info("Created projects") // Prints "INFO: Created projects" }
因爲 import 語句,import logging.Logger.info
,info
方法是可見的。
導入須要++導入符號++的「穩定路徑」,而且對象是穩定的路徑。
注意:若是一個 object
不是頂層的,而是嵌套在另外一個類或對象中,那麼該對象就像任何其餘成員同樣是「路徑依賴的」。這意味着給定 class Milk
和 class OrangeJuice
兩個飲料類型,一個類成員 class NutritionInfo
「取決於」封閉的實例,牛奶或橙汁。milk.NutritionInfo
與 oj.NutritionInfo
徹底不一樣.
名稱與某個類相同的對象稱爲伴生對象。相反,該類是對象的伴生類。但伴生類或對象能夠訪問其伴生的私人成員。在伴生類實例裏使用伴生對象的方法和值是沒有效果的。
import scala.math._ case class Circle(radius: Double) { import Circle._ def area: Double = calculateArea(radius) } object Circle { private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0) } val circle1 = new Circle(5.0) circle1.area
class Circle
有一個特定於每一個實例的成員 area
,而單例 object Circle
有一個可用於每一個實例的方法 calculateArea
。
伴生對象也能夠包含工廠方法:
class Email(val username: String, val domainName: String) object Email { def fromString(emailString: String): Option[Email] = { emailString.split('@') match { case Array(a, b) => Some(new Email(a, b)) case _ => None } } } val scalaCenterEmail = Email.fromString("scala.center@epfl.ch") scalaCenterEmail match { case Some(email) => println( s"""Registered an email |Username: ${email.username} |Domain name: ${email.domainName} """) case None => println("Error: could not parse email") }
object Email
包含從一個 String 能夠建立一個 Email
的工廠 fromString
。在可能解析錯誤的狀況下,咱們將其做爲 Option[Email]
返回。
注意:若是類或對象具備伴生,則二者必須在同一個文件中定義。 要在REPL中定義伴生,請將它們定義在同一行上或輸入 :paste
模式。
Java 中的 static
成員被模仿爲 Scala 中伴生對象的普通成員。
當使用Java代碼中的伴生對象時,成員將在具備 static
修飾符的伴隨類中定義。這稱爲靜態轉發(static forwarding)。 即便您沒有本身定義伴生類,也會發生這種狀況。