Scala入門到精通——第二十四節 高階型別 (三)
本節主要內容
- Type Specialization
- Manifest、TypeTag、ClassTag
- Scala型別系統總結
在scala中,類(class)與型別(type)是兩個不一樣的概念。我們知道類是對同一型別資料的抽象,而型別則更具體。比如定義class List[T] {}, 可以有List[Int] 和 List[String]等具體型別,稱List為類,而List[Int]、List[String]則為型別。從這方面看:型別一致的物件它們的類也是一致的;而類一致的,其型別不一定一致。例如:
//List[Int]與List[String]它們的類是相同的即List
scala> classOf[List [Int]] == classOf[List[String]]
res1: Boolean = true
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
//類相同,但它們的型別是不一樣的
scala> typeOf[List[Int]] == typeOf[List[String]]
res3: Boolean = false
1. Type Specialization
Type Specialization,一般被翻譯成型別專門化,它主要是用來解決泛型的型別擦除和自動裝箱拆箱的問題。在JAVA語言當中,泛型生成位元組碼檔案時會進行泛型型別擦除,型別擦除後利用上界型別(一般是Object)來替代,但這麼做的話有問題,這是因為在Java語言中基本型別與物件型別是不能相互引用的,java中的基本型別不能使用泛型。解決方案是利用對應的物件型別來進行替代,例如int對應Integer型別,但這種方式並不能解決根本問題。為方便後面Type Specialization的理解,我們先從java的型別擦除、自裝箱與拆箱講起。
1 型別擦除
假設我們利用Java泛型定義了下面的Person類:
//Java泛型類
public class Person<T> {
private T firstName;
private T secondName;
public Person(T firstName,T secondName){
this.firstName=firstName;
this.secondName=secondName;
}
public T getFirstName() {
return firstName;
}
public void setFirstName(T firstName) {
this.firstName = firstName;
}
public T getSecondName() {
return secondName;
}
public void setSecondName(T secondName) {
this.secondName = secondName;
}
}
經過型別擦除後,最終變為:
public class Person {
private Object firstName;
private Object secondName;
public Person(Object firstName,Object secondName){
this.firstName=firstName;
this.secondName=secondName;
}
public Object getFirstName() {
return firstName;
}
public void setFirstName(Object firstName) {
this.firstName = firstName;
}
public Object getSecondName() {
return secondName;
}
public void setSecondName(Object secondName) {
this.secondName = secondName;
}
}
經過型別擦除後的類稱為原始型別,從這點來看,java中的泛型其實是一個偽泛型,它只在編譯層次進行實現,在生成字碼碼這部分泛型資訊被擦除。下面的例子證明也證明了這一點:
public static void main(String[] args) {
Person<String> p1=new Person<String>("張", "三");
Person<Integer> p2=new Person<Integer>(1, 23);
//下面的程式碼返回的是true
System.out.println(p1.getClass()==p2.getClass());
}
2 自動裝箱與拆箱
在前面給的示例程式碼中,我們直接使用
Person<Integer> p2=new Person<Integer>(1, 23);
需要注意的是這裡使用的是java的基本型別進行物件的建立,而給定的具體型別是Integer,此時Java會幫我們自動進行轉換,這個轉換操作被稱為自動裝箱(autoboxing),上面的程式碼相當於:Person<Integer> p2=new Person<Integer>(Integer.valueOf(1), Integer.valueOf(23));
。
下面的程式碼演示了拆箱(unboxing)
//Integer firstName =Integer.valueOf(23)
Integer firstName = 23; //自動裝箱
//拆箱,實際執行 int name = firstName .intValue();
int name = firstName ;
自動裝箱與拆箱需要損耗一定的效能,當效能要求較高時需要程式設計師手動雲進行轉換。Scala中的Type Specialization解決了這些問題。它的語法很簡單,通過註解進行型別專門化宣告,如:
//在泛型引數前面加@specialized進行Type Specialization
abstract class List[@specialized T]{
def apply(x:T)
def map[S](f:T=>S)
}
上述程式碼編譯後會生成下列字程式碼檔案,如下圖
從圖中可以看到,共生成了九個版本的List,其中這九個檔案分別對應scala中的九種基本型別即Unit, Boolean, Byte, Short, Char, Int, Long, Float, Double。感興趣的可以利用javap命令進行檢視,這裡給出其Byte型別的實現:
D:\ScalaWorkspace\ScalaChapter24\bin\cn\scala\xtwy\advancedtype>javap -private L
ist$mcB$sp.class
Compiled from "TypeSpecilization.scala"
public abstract class cn.scala.xtwy.advancedtype.List$mcB$sp extends cn.scala.xt
wy.advancedtype.List<java.lang.Object> {
public abstract void apply(byte);
public abstract <S extends java/lang/Object> void map(scala.Function1<java.lan
g.Object, S>);
public cn.scala.xtwy.advancedtype.List$mcB$sp();
}
@specialized 還可以更細緻,限定某個或幾個基本型別,例如:
abstract class List[@specialized T]{
//指定生成Int型別的版本
def apply[@specialized (Int) S](x:S):S
////指定生成Boolean及Double型別的版本
def map[@specialized (Boolean,Double) S](f:T=>S)
}
在上一講中我們看到了Function1及Function2的類定義中也使用@specialize進行註解,如:
@annotation.implicitNotFound(msg = "No implicit view available from ${T1} => ${R}.")
trait Function1[@specialized(scala.Int, scala.Long,
scala.Float, scala.Double/*, scala.AnyRef*/) -T1,
scala.Float, scala.Long, scala.Double/*,
scala.AnyRef*/) +R] extends AnyRef
可以看到,Function1類也進行了型別專門化。
2. Manifest、TypeTag、ClassTag
由於型別擦除的影響,編譯期存在的型別資訊在編譯後不存在了,在程式執行時不能獲取該資訊,但某些場景下可能需要得到編譯期的型別資訊,scala能夠做到這一點,它通過Manifest和TypeTag來儲存型別資訊並在執行時使用該資訊。那Manifest與TypeTag有什麼區別呢?Manifest在scala.reflect包中,它在scala.reflect包中,而TypeTag 在scala.reflect.runtime.universe包中定義;TypeTag可以用來替代Manifest,功能更強大一點,Manifest不能識別路徑依賴型別,例如對於class Outter{ class Inner},假設分別建立了兩個不同的外部類,outter.Inner, outter2.Inner, Manifest就會識別為同一型別,而TypeTag不會,另外TypeTag可以使用typeOf[T] 來檢查型別引數。
下面的程式碼給出了Manifest的用法:
object ManifestType extends App {
def print1[T](x: List[T])(implicit m: Manifest[T]) = {
if (m <:< manifest[String])
println("字串型別的List")
else
println("非字串型別的List")
}
print1(List("one", "two"))
print1(List(1, 2))
print1(List("one", 2))
}
隱式引數m由編譯器根據上下文自動傳入,例如print1(List(“one”, “two”)) ,編譯器會根據”one”,”two” 實際型別推斷出 T 的型別是 String,再隱式地傳入了Manifest[String]型別的物件引數,使得執行時可以根據這個引數做更多的事情。
下面的程式碼演示瞭如何使用TypeTag
import scala.reflect.runtime.universe._
def getTypeTag[T: TypeTag](a: T) = typeTag[T]
//下列語句返回TypeTag[List[Int]]
println(getTypeTag(List(1, 2, 3)))
從上面的程式碼可以看到,typeTag返回的是具體的型別,而不是型別擦除之後的型別any,即TypeTag儲存所有具體的型別。在執行時可以通過模式匹配來精確地對型別進行判斷:
import scala.reflect.runtime.universe._
def patternMatch[A : TypeTag](xs: List[A]) = typeOf[A] match {
//利用型別約束進行精確匹配
case t if t =:= typeOf[String] => "list of strings"
case t if t <:< typeOf[Int] => "list of ints"
}
println(patterMatch(List(1,2)))
上邊的typeOf[A]在傳入引數為List(“String”)時,得到結果是java.lang.String。typeOf[A]接受一個型別為TypeTag[a]的隱式引數,編譯器生成的TypeTag隱式引數會被傳給typeOf[A] 。 有4種TypeTag:
1 scala.reflect.api.TypeTags#TypeTag. A full type descriptor of a Scala type. For example, a TypeTag[List[String]] contains all type information, in this case, of typescala.List[String].
2 scala.reflect.ClassTag. A partial type descriptor of a Scala type. For example, a ClassTag[List[String]] contains only the erased class type information, in this case, of type
3 scala.collection.immutable.List.ClassTags provide access only to the runtime class of a type. Analogous to scala.reflect.ClassManifest.
4 scala.reflect.api.TypeTags#WeakTypeTag. A type descriptor for abstract types (see corresponding subsection below).
這給出最常用的ClassTag的用法:ClassTag[T]儲存了被泛型擦除後的原始型別T,提供給執行時程式使用。
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> val tt = typeTag[Int]
tt: reflect.runtime.universe.TypeTag[Int] = TypeTag[Int]
scala>
scala> import scala.reflect._
import scala.reflect._
//得到具體型別
scala> val ct = classTag[String]
ct: scala.reflect.ClassTag[String] = java.lang.String
3. Scala型別系統總結
到此,Scala的型別系統基本介紹完畢,下表給出了Scala中常見的型別
型別 | 語法型別 |
---|---|
類 | class Person |
特質 | trait Closable |
元組型別 | (T1,T2,T3,…) |
函式型別 | (T1,T2,t3,…)=>T |
引數型別(泛型) | class Person[T1,T2,…] |
單例型別 | this.type |
型別投影 | Outter#Inner |
複合型別 | A with B with C… |
結構體型別 | {def f():Unit ….} |
中置型別 | T1 A T2 |
存在型別 | T forSome {} |
新增公眾微訊號,可以瞭解更多最新Spark、Scala相關技術資訊