溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Scala筆記整理(八):類型參數(shù)(泛型)與隱士轉(zhuǎn)換

發(fā)布時間:2020-07-02 03:53:12 來源:網(wǎng)絡(luò) 閱讀:9164 作者:xpleaf 欄目:大數(shù)據(jù)

[TOC]


概述

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

所以Java中提出了泛型的概念,其實也就是類型參數(shù)的概念,此時可以用泛型創(chuàng)建List,List list = new ArrayList[Integer](),那么,此時list.add(1)沒問題,而list.add("2")呢?就不行了,因為類型泛型會限制傳入的參數(shù),只能往集合中l(wèi)ist添加Integer類型,這樣就避免了上述的數(shù)值的問題。

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

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

泛型類

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

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

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

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

Scala會從構(gòu)造參數(shù)中推斷出實際類型:

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

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

package cn.xpleaf.bigdata.p5.mygeneric

/**
  * scala的類型參數(shù),即java中的泛型
  * 定義方式有異,java使用使用<>,scala使用[]
  * 泛型可以定義在類 特質(zhì) 方法 函數(shù)
  *     泛型的作用,就是將運行期間的異常,提前到了編譯器
  *     提高代碼的通用性
  */
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")
  }
}

輸出結(jié)果如下:

23  xpleaf
22  jieling
hadoop  spark

泛型函數(shù)

函數(shù)和方法也可以帶有類型參數(shù):

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

和泛型類一樣,你需要把類型參數(shù)放在方法名后面。

Scala會從調(diào)用該方法使用的實際類型來推斷出類型:

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函數(shù)中測試,輸出結(jié)果如下:

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方法調(diào)用了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并沒有指定實現(xiàn)compareTo方法,也沒有指定為某個類型的子類。在Java泛型里表示某個類型是Test類型的子類型,使用extends關(guān)鍵字:

<T extends Test>

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

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

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

下面的代碼結(jié)合了上限:

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)

注意,這相當(dāng)于是對類型T加了一條限制:T必須是Comparable[T]的子類型。原來給T指定什么類型都可以,現(xiàn)在就不行了。

這樣一來,我們可以實例化Student[String]。但是不能實例化Student[File],因為String是Comparable[String]的子類型,而File并沒有實現(xiàn)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的子類
                  * 說白了也就是要讓這個類型參數(shù)T是Comparable或者Ordered的子類
                  * 一個類型是某一個類的子類,寫法就要發(fā)生對應(yīng)的變化
                  * 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內(nèi)部,將Int(隱士)轉(zhuǎn)換為RichInt
          要想讓該程序運行通過,就需要使用視圖界定的方式
          [T <% Comparable[T]]
          使用這個%,其實就是強制指定將Int類型隱士轉(zhuǎn)換為RichInt,而RichInt間接實現(xiàn)了Comparable
          */
    val stu3 = new Student[Int](18, 19)
    println(stu3.bigger())

}

在main函數(shù)中執(zhí)行,輸出結(jié)果如下:

xpleaf
王五
19

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

視圖界定

其實上面已經(jīng)有說明和應(yīng)用,不過這里還是詳細介紹一下。

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

先看下面的一個例子:

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)五實現(xiàn),編譯器會報錯。因為Int和Integer不一樣,Integer是包裝類型,但是Scala的Int并沒有實現(xiàn)Comparable。
不過RichInt實現(xiàn)了Comparable[Int],同時還有一個Int到RichInt的隱士轉(zhuǎn)換。解決途徑就是視圖界定。

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

&lt;%關(guān)系意味著T可以被隱式轉(zhuǎn)換成Comparable[Int]。

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

協(xié)變和逆變

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

package cn.xpleaf.bigdata.p5.mygeneric

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

        val list1:List[Person] = List[Student]()    // scala中的協(xié)變,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{}

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

    }

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

    }

}

當(dāng)然還有很多的理論知識和細節(jié)知識,但目前掌握這些就可以了。

類型通配符

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的類型參數(shù)限定為[_<:Person],即輸入的類為Person及其子類
    //類型通配符和一般的泛型定義不一樣,泛型在類定義時使用,而類型通配符號在使用類時使用
    def makeFriends(p:Pair[_<:Person])={
        println(p.first +" is making friend with "+ p.second)
    }
    makeFriends(new Pair(new Student("john"),new Teacher("搖擺少年夢")))
}

隱士轉(zhuǎn)換

概述

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

2、在前一講的視圖界定中我們也提到,視圖界定可以跨越類層次結(jié)構(gòu)進行,它背后的實現(xiàn)原理就是隱式轉(zhuǎn)換,例如Int類型會視圖界定中會自動轉(zhuǎn)換成RichInt,而RichInt實現(xiàn)了Comparable接口,當(dāng)然這里面的隱式轉(zhuǎn)換也是scala語言為我們設(shè)計好的 。

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

Doube進行到Int的轉(zhuǎn)換:

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

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

2、Scala并不是第一個允許程序員提供自動類型轉(zhuǎn)換的語言。不過,Scala給了程序員相當(dāng)大的控制權(quán)在什么時候應(yīng)用這些模塊。

利用隱士函數(shù)豐富現(xiàn)在類庫的功能

隱式轉(zhuǎn)換功能十分強大,可以快速地擴展現(xiàn)有類庫的功能.

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

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

//隱式函數(shù)將java.io.File隱式轉(zhuǎn)換為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方法。

引入隱士轉(zhuǎn)換

1、Scala默認會考慮兩種隱式轉(zhuǎn)換,一種是源類型,或者目標(biāo)類型的伴生對象內(nèi)的隱式轉(zhuǎn)換函數(shù);一種是當(dāng)前程序作用域內(nèi)的可以用唯一標(biāo)識符表示的隱式轉(zhuǎn)換函數(shù)。

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

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

隱士轉(zhuǎn)換規(guī)則

1、隱式轉(zhuǎn)換可以定義在目標(biāo)文件當(dāng)中(一個Scala文件中)

//轉(zhuǎn)換函數(shù)
implicit def double2Int(x:Double)=x.toInt
val x:Int = 3.5

2、隱式轉(zhuǎn)換函數(shù)與目標(biāo)代碼在同一個文件當(dāng)中,也可以將隱式轉(zhuǎn)換集中放置在某個包中,在使用進直接將該包引入即可

//在com.sparkstudy.scala.demo包中定義了子包implicitConversion
//然后在object ImplicitConversion中定義所有的引式轉(zhuǎn)換方法
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
}

//隱士轉(zhuǎn)換規(guī)則
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中定義了很多隱式轉(zhuǎn)換函數(shù)

隱士轉(zhuǎn)換發(fā)生的時機

1、當(dāng)方法中參數(shù)的類型與實際類型不一致時

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

2、當(dāng)調(diào)用類中不存在的方法或成員時,會自動將對象進行隱式轉(zhuǎn)換

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

隱士參數(shù)

1、所謂的隱式參數(shù),指的是在函數(shù)或者方法中,定義一個用implicit修飾的參數(shù),此時Scala會嘗試找到一個指定類型的,用implicit修飾的對象,即隱式值,并注入?yún)?shù)。

2、Scala會在兩個范圍內(nèi)查找:一種是當(dāng)前作用域內(nèi)可見的val或var定義的隱式變量;一種是隱式參數(shù)類型的伴生對象內(nèi)的隱式值

//學(xué)生畢業(yè)報告
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隱士轉(zhuǎn)換操作
  *     將一種類型,轉(zhuǎn)化為另外的一種類型,這完成這一操作的背后就是隱士轉(zhuǎn)換函數(shù)
  *     所謂隱士轉(zhuǎn)換函數(shù),其實就是在普通函數(shù)前面加上一個關(guān)鍵字——implicit
  *
  *     隱士轉(zhuǎn)換函數(shù)的導(dǎo)入:
  *         1、如果隱士轉(zhuǎn)換函數(shù)和調(diào)用它的操作,在同一個文件中,我們不要做任何操作
  *         2、如果不在一個文件中,需要收到導(dǎo)入,和導(dǎo)包是一樣,唯一需要注意最后以._結(jié)尾,表導(dǎo)入該類中的所有的隱士轉(zhuǎn)換函數(shù)
  *
  */
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
    }

    /**
      * 隱士轉(zhuǎn)換參數(shù)
      * 其實就非常類似于之前學(xué)習(xí)過的柯里化
      */
    def implicitOps3: Unit = {
        /*  // 傳統(tǒng)操作方式
        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()
    */

    /**
      * 使用隱士轉(zhuǎn)換豐富現(xiàn)在類型的API
      */
    def implicitOps2: Unit ={

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

        var lines = file.read()

        println(lines)
    }

    /**
      * 隱士轉(zhuǎn)換操作
      */
    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)

    }

}
向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI