1. 程式人生 > >Scala筆記整理(八):類型參數(泛型)與隱士轉換

Scala筆記整理(八):類型參數(泛型)與隱士轉換

大數據 Scala

[TOC]


概述

類型參數是什麽?類型參數其實就是Java中的泛型。大家對Java中的泛型應該有所了解,比如我們有List list = new ArrayList(),接著list.add(1),沒問題,list.add("2"),然後我們list.get(1) == 2,對不對?肯定不對了,list.get(1)獲取的其實是個String——"2",String——"2"怎麽可能與一個Integer類型的2相等呢?

所以Java中提出了泛型的概念,其實也就是類型參數的概念,此時可以用泛型創建List,List list = new ArrayList[Integer]()

,那麽,此時list.add(1)沒問題,而list.add("2")呢?就不行了,因為類型泛型會限制傳入的參數,只能往集合中list添加Integer類型,這樣就避免了上述的數值的問題。

Scala中的類型參數和Java的泛型是一樣的,也是定義一種類型參數。

最後,Scala類型參數也是Spark源碼中非常常見的,因此同樣必須掌握,才能看懂spark源碼。

泛型類

Java 或 C++ 一樣,類和特質可以帶類型參數。在Scala中,我們用方括號類定義類型參數

class Student[T, S](val first: T, val second: S)

以上將定義一個帶有2個類型參數T和S的類。在類的定義中,你可以用類型參數來定義變量,方法參數,以及返回值的類型。

我們把帶有一個或者多個類型參數的類,叫作泛型類。如果你把類型參數替換成實際的類型,將得到一個普通的類。比如Student[Int,String]

Scala會從構造參數中推斷出實際類型:

val p = new Student(42, "String")

你也可以自己指定類型,測試代碼如下:

package cn.xpleaf.bigdata.p5.mygeneric

/**
  * scala的類型參數,即java中的泛型
  * 定義方式有異,java使用使用<>,scala使用[]
  * 泛型可以定義在類 特質 方法 函數
  *     泛型的作用,就是將運行期間的異常,提前到了編譯器
  *     提高代碼的通用性
  */
object _01GenericOps {
  def main(args: Array[String]): Unit = {
    genericOps1
  }

  /**
    * 泛型類的定義
    */
  def genericOps1: Unit = {
    class Student[T, S](val first: T, val second: S) {
      println(first + "\t" + second)
    }

    new Student(23, "xpleaf") // 可以做類型的自動推斷
    new Student[Int, String](22, "jieling")
    new Student[Any, Any]("hadoop", "spark")
  }
}

輸出結果如下:

23  xpleaf
22  jieling
hadoop  spark

泛型函數

函數和方法也可以帶有類型參數:

def getStudentInfo[T](stu: Array[T]) = stu(stu.length / 2)

和泛型類一樣,你需要把類型參數放在方法名後面。

Scala會從調用該方法使用的實際類型來推斷出類型:

def methodOps: Unit ={
    def getStudentInfo[T](stu: Array[T]) = stu(stu.length / 2)

    val student = getStudentInfo(Array("garry", "tom", "john", "lucy", "Richard"))
    println(student)
}

在main函數中測試,輸出結果如下:

john

類型變量界定—上限(upper bounds)

我們先看一個簡單的實例,用於判斷兩個變量中較大的值,其中兩個變量的類型均為Int型

/**
    * 類型變量界定
    */
def typeValueOps: Unit ={
    class StudentInt(val first: Int, val second: Int) {
        def bigger = {
            if (first.compareTo(second) > 0) first else second
        }
    }

    val studentInt = new StudentInt(1, 2)
    println(studentInt.bigger)
}

上述StudentInt類中的bigger方法調用了compare方法,如果我們想比較兩個String型的變量的大小,我們可以和上面一樣,添加StudentStr類:

class StudentStr(val first: String, val second: String) {
    def bigger = {
        if (first.compareTo(second) > 0) first else second
    }
}

如果我們針對每種基本類型都寫一個具體的類,則代碼量太大,同時也不夠簡潔,此時我們想到泛型能比較容易解決這個問題:

class Student[T](val first: T, val second: T) {
    def smaller = if (first.compareTo(second) < 0) first else second
}

然而與此同時,我們定義的泛型T並沒有指定實現compareTo方法,也沒有指定為某個類型的子類。在Java泛型裏表示某個類型是Test類型的子類型,使用extends關鍵字:

<T extends Test>

//或用通配符的形式:
<? extends Test>

這種形式也叫upper bounds (上限或上界),同樣的意思在Scala中的寫法為:

[T <: Test] //或用通配符: [_ <: Test]

下面的代碼結合了上限:

class Student[T <: Comparable[T]](val first: T, val second: T){
    def smaller = if (first.compareTo(second) < 0) first else second
}

val studentString = new Student[String]("limu","john")
println(studentString.smaller)

val studentInt = new Student[Integer](1,2)
println(studentInt.smaller)

註意,這相當於是對類型T加了一條限制:T必須是Comparable[T]的子類型。原來給T指定什麽類型都可以,現在就不行了。

這樣一來,我們可以實例化Student[String]。但是不能實例化Student[File],因為String是Comparable[String]的子類型,而File並沒有實現Comparable[File]接口。

一個包含視圖界定的完整案例如下:

/**
      * 泛型的上界
      * Upper Bound
      * [T <: 類] ---> [T <% 類] (視圖的界定)
      */
def genericOps3: Unit = {
    class Student[T <% Comparable[T]](private val first: T, private val second: T) {
        def bigger():T = {
            /**
                  * 如果要讓first和second有compareTo方法,必須要為Comparable的子類或者是Ordered的子類
                  * 說白了也就是要讓這個類型參數T是Comparable或者Ordered的子類
                  * 一個類型是某一個類的子類,寫法就要發生對應的變化
                  * java的寫法:<T/? extends Comparable>
                  * scala的寫法:[T <: Comparable]
                  */
            if(first.compareTo(second) > 0) {
                first
            } else {
                second
            }
        }
    }

    val stu = new Student[String]("xpleaf", "jieling")
    println(stu.bigger())
    val stu2 = new Student[String]("李四", "王五")
    println(stu2.bigger())
    /**
          * Error:(43, 13) type arguments [Int] do not conform to class Student‘s type parameter bounds [T <: Comparable[T]]
        val stu3 = new Student[Int](18, 19)
          說明Int不是Comparable的子類

          前面Int類型可以用,實際上是scala內部,將Int(隱士)轉換為RichInt
          要想讓該程序運行通過,就需要使用視圖界定的方式
          [T <% Comparable[T]]
          使用這個%,其實就是強制指定將Int類型隱士轉換為RichInt,而RichInt間接實現了Comparable
          */
    val stu3 = new Student[Int](18, 19)
    println(stu3.bigger())

}

在main函數中執行,輸出結果如下:

xpleaf
王五
19

下限很少使用,所以這裏就不進行說明了。

視圖界定

其實上面已經有說明和應用,不過這裏還是詳細介紹一下。

剛才將的類型變量界定建立在類繼承層次結構的基礎上,但有時候這種限定不能滿足實際要求,如果希望跨越類繼承層次結構時,可以使用視圖界定來實現的,其後面的原理是通過隱式轉換(我們在下一講中會詳細講解什麽是隱式轉換)來實現。視圖界定利用<%符號來實現。

先看下面的一個例子:

class Student[T <: Comparable[T]](val first: T, val second: T) {
    def smaller = if (first.compareTo(second) < 0) first else second
}
val student = new Student[Int](4,2)
println(student.smaller)

可惜,如果我們嘗試用Student(4,2)五實現,編譯器會報錯。因為Int和Integer不一樣,Integer是包裝類型,但是Scala的Int並沒有實現Comparable。
不過RichInt實現了Comparable[Int],同時還有一個Int到RichInt的隱士轉換。解決途徑就是視圖界定。

class Student[T <% Comparable[T]](val first: T, val second: T) {
    def smaller = if (first.compareTo(second) < 0) first else second
}

&lt;%關系意味著T可以被隱式轉換成Comparable[Int]

個人理解:不管是類型變量界定還是視圖界定,實際上都是在限制類型參數T,類型變量界定要求類型參數T必須是上界的子類或者是下界的父類;視圖界定則是要求類型參數T必須能夠隱式轉換成“類似上界”的界定,比如上面提到的,Int隱式轉換成RichInt,RichInt是Comparable[Int]的子類。這樣看來,類型變量界定對類型參數的限制比視圖界定對類型參數的限制是更大了。

協變和逆變

直接看下面的程序代碼就能很容易理解:

package cn.xpleaf.bigdata.p5.mygeneric

/**
  * scala類型參數的協變和逆變
  *     scala默認不支持協變和逆變
  *         要想讓scala的泛型支持協變,在泛型前面再加一個"+"
  *         要想讓scala的泛型支持逆變,在泛型前面再加一個"-"
  *     但是一個類不能同時支持協變和逆變
  */
object _02GenericOps {
    def main(args: Array[String]): Unit = {
        /*
        val list:List[Person] = List[Person]()  // 正常的定義

        val list1:List[Person] = List[Student]()    // scala中的協變,java不支持
        // val list2:List[Teacher] = List[Person]()    // 逆變,java不支持,但是scala需要在定義泛型類的時候指定
        */

        val myList1:MyList[Person] = new MyList[Person]()
        val myList2:MyList[Person] = new MyList[Student]()

        val yourList1:YourList[Person] = new YourList[Person]()
        val yourList2:YourList[Student] = new YourList[Person]()
    }

    class Person{}
    class Student extends Person{}
    class Teacher extends Person{}

    /**
      * 支持協變的泛型類
      */
    class MyList[+T] {

    }

    /**
      * 支持逆變的泛型類
      */
    class YourList[-T] {

    }

}

當然還有很多的理論知識和細節知識,但目前掌握這些就可以了。

類型通配符

1、類型通配符是指在使用時不具體指定它屬於某個類,而是只知道其大致的類型範圍,通過”_
<:” 達到類型通配的目的。

2、

def typeWildcard: Unit ={
    class Person(val name:String){
        override def toString()=name
    }
    class Student(name:String) extends Person(name)
    class Teacher(name:String) extends Person(name)
    class Pair[T](val first:T,val second:T){
        override def toString()="first:"+first+", second: "+second;
    }
    //Pair的類型參數限定為[_<:Person],即輸入的類為Person及其子類
    //類型通配符和一般的泛型定義不一樣,泛型在類定義時使用,而類型通配符號在使用類時使用
    def makeFriends(p:Pair[_<:Person])={
        println(p.first +" is making friend with "+ p.second)
    }
    makeFriends(new Pair(new Student("john"),new Teacher("搖擺少年夢")))
}

隱士轉換

概述

1、在scala語言當中,隱式轉換是一項強大的程序語言功能,它不僅能夠簡化程序設計,也能夠使程序具有很強的靈活性。它們存在固有的隱式轉換,不需要人工進行幹預,例如Float在必要情況下自動轉換為Double類型

2、在前一講的視圖界定中我們也提到,視圖界定可以跨越類層次結構進行,它背後的實現原理就是隱式轉換,例如Int類型會視圖界定中會自動轉換成RichInt,而RichInt實現了Comparable接口,當然這裏面的隱式轉換也是scala語言為我們設計好的 。

3、所謂隱士轉換函數(implicit conversion function)指的是那種以implicit關鍵字聲明的帶有單個參數的函數。正如它的名稱所表達的,這樣的函數將自動應用,將值從一種類型轉換成另一種類型。

Doube進行到Int的轉換:

val x:Int = 3.5
implicit def double2Int(x:Double)=x.toInt
def conversionFunc: Unit ={
    //Doube進行到Int的轉換
    val x:Int = 3.5
    println("x===> " + x)
}

1、隱式函數的名稱對結構沒有影響,即implicitdefdouble2Int(x:Double)=x.toInt函數可以是任何名字,只不過采用source2Target這種方式函數的意思比較明確,閱讀代碼的人可以見名知義,增加代碼的可讀性。

2、Scala並不是第一個允許程序員提供自動類型轉換的語言。不過,Scala給了程序員相當大的控制權在什麽時候應用這些模塊。

利用隱士函數豐富現在類庫的功能

隱式轉換功能十分強大,可以快速地擴展現有類庫的功能.

import java.io.File
import scala.io.Source

//RichFile類中定義了Read方法
class RichFile(val file:File){
    def read = Source.fromFile(file).getLines().mkString
}

//隱式函數將java.io.File隱式轉換為RichFile類
implicit def file2RichFile(file:File) = new RichFile(file)
val f = new File("E:/test/scala/wordcount.txt").read
println(f)

Java.io.File本身並沒有read方法。

引入隱士轉換

1、Scala默認會考慮兩種隱式轉換,一種是源類型,或者目標類型的伴生對象內的隱式轉換函數;一種是當前程序作用域內的可以用唯一標識符表示的隱式轉換函數。

2、如果隱式轉換不在上述兩種情況下的話,那麽就必須手動使用import語法引入某個包下的隱式轉換函數,比如import student._。

通常建議,僅僅在需要進行隱式轉換的代碼部分,比如某個函數或者方法內,用import導入隱式轉換函數,這樣可以縮小隱式轉換函數的作用域,避免不需要的隱式轉換。

隱士轉換規則

1、隱式轉換可以定義在目標文件當中(一個Scala文件中)

//轉換函數
implicit def double2Int(x:Double)=x.toInt
val x:Int = 3.5

2、隱式轉換函數與目標代碼在同一個文件當中,也可以將隱式轉換集中放置在某個包中,在使用進直接將該包引入即可

//在com.sparkstudy.scala.demo包中定義了子包implicitConversion
//然後在object ImplicitConversion中定義所有的引式轉換方法
package implicitConversion{
    object ImplicitConversion{
        implicit def double2Int(x:Double)=x.toInt
        implicit def file2RichFile(file:File) = new RichFile(file)
    }
}
class RichFile(val file:File){
    def read=Source.fromFile(file).getLines().mkString
}

//隱士轉換規則
def implicitConversionRuleOps: Unit ={
    //在使用時引入所有的隱式方法
    import com.sparkstudy.scala.demo.implicitConversion.ImplicitConversion._
    var x:Int=3.5
    println("x===> " + x)
    val f=new File("E:/test/scala/wordcount.txt").read
    println(f)
}

這種方式在scala語言中比較常見,在前面我們也提到,scala會默認幫我們引用Predef對象中所有的方法,Predef中定義了很多隱式轉換函數

隱士轉換發生的時機

1、當方法中參數的類型與實際類型不一致時

def f(x:Int)=x
//方法中輸入的參數類型與實際類型不一致,此時會發生隱式轉換
//double類型會轉換為Int類型,再進行方法的執行
f(3.14)

2、當調用類中不存在的方法或成員時,會自動將對象進行隱式轉換

我們上面進行的那個案例(File本身是沒有read方法)

隱士參數

1、所謂的隱式參數,指的是在函數或者方法中,定義一個用implicit修飾的參數,此時Scala會嘗試找到一個指定類型的,用implicit修飾的對象,即隱式值,並註入參數。

2、Scala會在兩個範圍內查找:一種是當前作用域內可見的val或var定義的隱式變量;一種是隱式參數類型的伴生對象內的隱式值

//學生畢業報告
class StudentSubmitReport {
    def writeReport(ctent: String) = println(ctent)
}

implicit val stuentSign = new StudentSubmitReport

def signForReport(name: String) (implicit studentSReport: StudentSubmitReport) {
    studentSReport.writeReport(name + "come to here")
}
signForReport ("jack")

完整案例

ImplicitUtil

package cn.xpleaf.bigdata.p5

import java.io.File

import scala.io.Source

object ImplicitUtil {

    implicit def double2Int(d: Double): Int = d.toInt

    implicit def str2Int(str: String): Int = str.length

    implicit def file2RichFile(file: File) = new RichFile(file)

    implicit val swr:StudentWriteReport = new StudentWriteReport()
}

class RichFile(file: File) {
    def read() = Source.fromFile(file).getLines().mkString
}

class StudentWriteReport {
    def writeReport(content:String) = println(content)
}

implicitOps

package cn.xpleaf.bigdata.p5.implicitz

/**
  * scala隱士轉換操作
  *     將一種類型,轉化為另外的一種類型,這完成這一操作的背後就是隱士轉換函數
  *     所謂隱士轉換函數,其實就是在普通函數前面加上一個關鍵字——implicit
  *
  *     隱士轉換函數的導入:
  *         1、如果隱士轉換函數和調用它的操作,在同一個文件中,我們不要做任何操作
  *         2、如果不在一個文件中,需要收到導入,和導包是一樣,唯一需要註意最後以._結尾,表導入該類中的所有的隱士轉換函數
  *
  */
import java.io.File

import cn.xpleaf.bigdata.p5.ImplicitUtil._
import cn.xpleaf.bigdata.p5.StudentWriteReport

import scala.io.Source
object implicitOps {
    def main(args: Array[String]): Unit = {
        //        implicitOps1
        //        implicitOps2
        implicitOps3
    }

    /**
      * 隱士轉換參數
      * 其實就非常類似於之前學習過的柯裏化
      */
    def implicitOps3: Unit = {
        /*  // 傳統操作方式
        def signReport(name:String, swr:StudentWriteReport): Unit = {
            swr.writeReport(name)
        }

        signReport("張三", new StudentWriteReport())*/

        def signForReport(name:String)(implicit swr:StudentWriteReport): Unit = {
            swr.writeReport(name)
        }

        signForReport("張三")
    }

    /*
    class StudentWriteReport {
        def writeReport(content:String) = println(content)
    }

    implicit val swr:StudentWriteReport = new StudentWriteReport()
    */

    /**
      * 使用隱士轉換豐富現在類型的API
      */
    def implicitOps2: Unit ={

        var file = new File("/Users/yeyonghao/test.txt")

        var lines = file.read()

        println(lines)
    }

    /**
      * 隱士轉換操作
      */
    def implicitOps1: Unit = {
        val x:Int = 3
        val y:Int = 3.5
        val z:Int = "klkelfldlkfj"

        println("x=" + x)
        println("y=" + y)
        println("z=" + z)

    }

}

Scala筆記整理(八):類型參數(泛型)與隱士轉換