入門大資料---Scala型別引數
一、泛型
Scala 支援型別引數化,使得我們能夠編寫泛型程式。
1.1 泛型類
Java 中使用 <>
符號來包含定義的型別引數,Scala 則使用 []
。
class Pair[T, S](val first: T, val second: S) {
override def toString: String = first + ":" + second
}
object ScalaApp extends App { // 使用時候你直接指定引數型別,也可以不指定,由程式自動推斷 val pair01 = new Pair("heibai01", 22) val pair02 = new Pair[String,Int]("heibai02", 33) println(pair01) println(pair02) }
1.2 泛型方法
函式和方法也支援型別引數。
object Utils {
def getHalf[T](a: Array[T]): Int = a.length / 2
}
二、型別限定
2.1 型別上界限定
Scala 和 Java 一樣,對於物件之間進行大小比較,要求被比較的物件實現 java.lang.Comparable
介面。所以如果想對泛型進行比較,需要限定型別上界為 java.lang.Comparable
,語法為 S <: T
,代表型別 S 是型別 T 的子類或其本身。示例如下:
// 使用 <: 符號,限定 T 必須是 Comparable[T]的子型別 class Pair[T <: Comparable[T]](val first: T, val second: T) { // 返回較小的值 def smaller: T = if (first.compareTo(second) < 0) first else second }
// 測試程式碼
val pair = new Pair("abc", "abcd")
println(pair.smaller) // 輸出 abc
擴充套件:如果你想要在 Java 中實現型別變數限定,需要使用關鍵字 extends 來實現,等價的 Java 程式碼如下:
public class Pair<T extends Comparable<T>> { private T first; private T second; Pair(T first, T second) { this.first = first; this.second = second; } public T smaller() { return first.compareTo(second) < 0 ? first : second; } }
2.2 檢視界定
在上面的例子中,如果你使用 Int 型別或者 Double 等型別進行測試,點選執行後,你會發現程式根本無法通過編譯:
val pair1 = new Pair(10, 12)
val pair2 = new Pair(10.0, 12.0)
之所以出現這樣的問題,是因為 Scala 中的 Int 類並沒有實現 Comparable 介面。在 Scala 中直接繼承 Comparable 介面的是特質 Ordered,它在繼承 compareTo 方法的基礎上,額外定義了關係符方法,原始碼如下:
// 除了 compareTo 方法外,還提供了額外的關係符方法
trait Ordered[A] extends Any with java.lang.Comparable[A] {
def compare(that: A): Int
def < (that: A): Boolean = (this compare that) < 0
def > (that: A): Boolean = (this compare that) > 0
def <= (that: A): Boolean = (this compare that) <= 0
def >= (that: A): Boolean = (this compare that) >= 0
def compareTo(that: A): Int = compare(that)
}
之所以在日常的程式設計中之所以你能夠執行 3>2
這樣的判斷操作,是因為程式執行了定義在 Predef
中的隱式轉換方法 intWrapper(x: Int)
,將 Int 型別轉換為 RichInt 型別,而 RichInt 間接混入了 Ordered 特質,所以能夠進行比較。
// Predef.scala
@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x)
要想解決傳入數值無法進行比較的問題,可以使用檢視界定。語法為 T <% U
,代表 T 能夠通過隱式轉換轉為 U,即允許 Int 型引數在無法進行比較的時候轉換為 RichInt 型別。示例如下:
// 檢視界定符號 <%
class Pair[T <% Comparable[T]](val first: T, val second: T) {
// 返回較小的值
def smaller: T = if (first.compareTo(second) < 0) first else second
}
注:由於直接繼承 Java 中 Comparable 介面的是特質 Ordered,所以如下的檢視界定和上面是等效的:
// 隱式轉換為 Ordered[T] class Pair[T <% Ordered[T]](val first: T, val second: T) { def smaller: T = if (first.compareTo(second) < 0) first else second }
2.3 型別約束
如果你用的 Scala 是 2.11+,會發現檢視界定已被標識為廢棄。官方推薦使用型別約束 (type constraint) 來實現同樣的功能,其本質是使用隱式引數進行隱式轉換,示例如下:
// 1.使用隱式引數隱式轉換為 Comparable[T]
class Pair[T](val first: T, val second: T)(implicit ev: T => Comparable[T])
def smaller: T = if (first.compareTo(second) < 0) first else second
}
// 2.由於直接繼承 Java 中 Comparable 介面的是特質 Ordered,所以也可以隱式轉換為 Ordered[T]
class Pair[T](val first: T, val second: T)(implicit ev: T => Ordered[T]) {
def smaller: T = if (first.compareTo(second) < 0) first else second
}
當然,隱式引數轉換也可以運用在具體的方法上:
object PairUtils{
def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b
}
2.4 上下文界定
上下文界定的形式為 T:M
,其中 M 是一個泛型,它要求必須存在一個型別為 M[T]的隱式值,當你宣告一個帶隱式引數的方法時,需要定義一個隱式預設值。所以上面的程式也可以使用上下文界定進行改寫:
class Pair[T](val first: T, val second: T) {
// 請注意 這個地方用的是 Ordering[T],而上面檢視界定和型別約束,用的是 Ordered[T],兩者的區別會在後文給出解釋
def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second
}
// 測試
val pair= new Pair(88, 66)
println(pair.smaller) //輸出:66
在上面的示例中,我們無需手動新增隱式預設值就可以完成轉換,這是因為 Scala 自動引入了 Ordering[Int]這個隱式值。為了更好的說明上下文界定,下面給出一個自定義型別的比較示例:
// 1.定義一個人員類
class Person(val name: String, val age: Int) {
override def toString: String = name + ":" + age
}
// 2.繼承 Ordering[T],實現自定義比較器,按照自己的規則重寫比較方法
class PersonOrdering extends Ordering[Person] {
override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1
}
class Pair[T](val first: T, val second: T) {
def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second
}
object ScalaApp extends App {
val pair = new Pair(new Person("hei", 88), new Person("bai", 66))
// 3.定義隱式預設值,如果不定義,則下一行程式碼無法通過編譯
implicit val ImpPersonOrdering = new PersonOrdering
println(pair.smaller) //輸出: bai:66
}
2.5 ClassTag上下文界定
這裡先看一個例子:下面這段程式碼,沒有任何語法錯誤,但是在執行時會丟擲異常:Error: cannot find class tag for element type T
, 這是由於 Scala 和 Java 一樣,都存在型別擦除,即泛型資訊只存在於程式碼編譯階段,在進入 JVM 之前,與泛型相關的資訊會被擦除掉。對於下面的程式碼,在執行階段建立 Array 時,你必須明確指明其型別,但是此時泛型資訊已經被擦除,導致出現找不到型別的異常。
object ScalaApp extends App {
def makePair[T](first: T, second: T) = {
// 建立以一個數組 並賦值
val r = new Array[T](2); r(0) = first; r(1) = second; r
}
}
Scala 針對這個問題,提供了 ClassTag 上下文界定,即把泛型的資訊儲存在 ClassTag 中,這樣在執行階段需要時,只需要從 ClassTag 中進行獲取即可。其語法為 T : ClassTag
,示例如下:
import scala.reflect._
object ScalaApp extends App {
def makePair[T : ClassTag](first: T, second: T) = {
val r = new Array[T](2); r(0) = first; r(1) = second; r
}
}
2.6 型別下界限定
2.1 小節介紹了型別上界的限定,Scala 同時也支援下界的限定,語法為:U >: T
,即 U 必須是型別 T 的超類或本身。
// 執行長
class CEO
// 部門經理
class Manager extends CEO
// 本公司普通員工
class Employee extends Manager
// 其他公司人員
class OtherCompany
object ScalaApp extends App {
// 限定:只有本公司部門經理以上人員才能獲取許可權
def Check[T >: Manager](t: T): T = {
println("獲得稽核許可權")
t
}
// 錯誤寫法: 省略泛型引數後,以下所有人都能獲得許可權,顯然這是不正確的
Check(new CEO)
Check(new Manager)
Check(new Employee)
Check(new OtherCompany)
// 正確寫法,傳入泛型引數
Check[CEO](new CEO)
Check[Manager](new Manager)
/*
* 以下兩條語句無法通過編譯,異常資訊為:
* do not conform to method Check's type parameter bounds(不符合方法 Check 的型別引數邊界)
* 這種情況就完成了下界限制,即只有本公司經理及以上的人員才能獲得稽核許可權
*/
Check[Employee](new Employee)
Check[OtherCompany](new OtherCompany)
}
2.7 多重界定
-
型別變數可以同時有上界和下界。 寫法為 :
T > : Lower <: Upper
; -
不能同時有多個上界或多個下界 。但可以要求一個型別實現多個特質,寫法為 :
T < : Comparable[T] with Serializable with Cloneable
; -
你可以有多個上下文界定,寫法為
T : Ordering : ClassTag
。
三、Ordering & Ordered
上文中使用到 Ordering 和 Ordered 特質,它們最主要的區別在於分別繼承自不同的 Java 介面:Comparable 和 Comparator:
- Comparable:可以理解為內建的比較器,實現此介面的物件可以與自身進行比較;
- Comparator:可以理解為外接的比較器;當物件自身並沒有定義比較規則的時候,可以傳入外部比較器進行比較。
為什麼 Java 中要同時給出這兩個比較介面,這是因為你要比較的物件不一定實現了 Comparable 介面,而你又想對其進行比較,這時候當然你可以修改程式碼實現 Comparable,但是如果這個類你無法修改 (如原始碼中的類),這時候就可以使用外接的比較器。同樣的問題在 Scala 中當然也會出現,所以 Scala 分別使用了 Ordering 和 Ordered 來繼承它們。
下面分別給出 Java 中 Comparable 和 Comparator 介面的使用示例:
3.1 Comparable
import java.util.Arrays;
// 實現 Comparable 介面
public class Person implements Comparable<Person> {
private String name;
private int age;
Person(String name,int age) {this.name=name;this.age=age;}
@Override
public String toString() { return name+":"+age; }
// 核心的方法是重寫比較規則,按照年齡進行排序
@Override
public int compareTo(Person person) {
return this.age - person.age;
}
public static void main(String[] args) {
Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)};
Arrays.sort(peoples);
Arrays.stream(peoples).forEach(System.out::println);
}
}
輸出:
bai:55
hei:66
ying:77
3.2 Comparator
import java.util.Arrays;
import java.util.Comparator;
public class Person {
private String name;
private int age;
Person(String name,int age) {this.name=name;this.age=age;}
@Override
public String toString() { return name+":"+age; }
public static void main(String[] args) {
Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)};
// 這裡為了直觀直接使用匿名內部類,實現 Comparator 介面
//如果是 Java8 你也可以寫成 Arrays.sort(peoples, Comparator.comparingInt(o -> o.age));
Arrays.sort(peoples, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.age-o2.age;
}
});
Arrays.stream(peoples).forEach(System.out::println);
}
}
使用外接比較器還有一個好處,就是你可以隨時定義其排序規則:
// 按照年齡大小排序
Arrays.sort(peoples, Comparator.comparingInt(o -> o.age));
Arrays.stream(peoples).forEach(System.out::println);
// 按照名字長度倒序排列
Arrays.sort(peoples, Comparator.comparingInt(o -> -o.name.length()));
Arrays.stream(peoples).forEach(System.out::println);
3.3 上下文界定的優點
這裡再次給出上下文界定中的示例程式碼作為回顧:
// 1.定義一個人員類
class Person(val name: String, val age: Int) {
override def toString: String = name + ":" + age
}
// 2.繼承 Ordering[T],實現自定義比較器,這個比較器就是一個外接比較器
class PersonOrdering extends Ordering[Person] {
override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1
}
class Pair[T](val first: T, val second: T) {
def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second
}
object ScalaApp extends App {
val pair = new Pair(new Person("hei", 88), new Person("bai", 66))
// 3.在當前上下文定義隱式預設值,這就相當於傳入了外接比較器
implicit val ImpPersonOrdering = new PersonOrdering
println(pair.smaller) //輸出: bai:66
}
使用上下文界定和 Ordering 帶來的好處是:傳入 Pair
中的引數不一定需要可比較,只要在比較時傳入外接比較器即可。
需要注意的是由於隱式預設值二義性的限制,你不能像上面 Java 程式碼一樣,在同一個上下文作用域中傳入兩個外接比較器,即下面的程式碼是無法通過編譯的。但是你可以在不同的上下文作用域中引入不同的隱式預設值,即使用不同的外接比較器。
implicit val ImpPersonOrdering = new PersonOrdering
println(pair.smaller)
implicit val ImpPersonOrdering2 = new PersonOrdering
println(pair.smaller)
四、萬用字元
在實際編碼中,通常需要把泛型限定在某個範圍內,比如限定為某個類及其子類。因此 Scala 和 Java 一樣引入了萬用字元這個概念,用於限定泛型的範圍。不同的是 Java 使用 ?
表示萬用字元,Scala 使用 _
表示萬用字元。
class Ceo(val name: String) {
override def toString: String = name
}
class Manager(name: String) extends Ceo(name)
class Employee(name: String) extends Manager(name)
class Pair[T](val first: T, val second: T) {
override def toString: String = "first:" + first + ", second: " + second
}
object ScalaApp extends App {
// 限定部門經理及以下的人才可以組隊
def makePair(p: Pair[_ <: Manager]): Unit = {println(p)}
makePair(new Pair(new Employee("heibai"), new Manager("ying")))
}
目前 Scala 中的萬用字元在某些複雜情況下還不完善,如下面的語句在 Scala 2.12 中並不能通過編譯:
def min[T <: Comparable[_ >: T]](p: Pair[T]) ={}
可以使用以下語法代替:
type SuperComparable[T] = Comparable[_ >: T]
def min[T <: SuperComparable[T]](p: Pair[T]) = {}
參考資料
- Martin Odersky . Scala 程式設計 (第 3 版)[M] . 電子工業出版社 . 2018-1-1
- 凱.S.霍斯特曼 . 快學 Scala(第 2 版)[M] . 電子工業出版社 . 2017-7