千家信息网

Scala笔记整理(八):类型参数(泛型)与隐士转换

发表于:2025-02-02 作者:千家信息网编辑
千家信息网最后更新 2025年02月02日,[TOC]概述类型参数是什么?类型参数其实就是Java中的泛型。大家对Java中的泛型应该有所了解,比如我们有List list = new ArrayList(),接着list.add(1),没问题
千家信息网最后更新 2025年02月02日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  xpleaf22  jielinghadoop  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关键字:

//或用通配符的形式:

这种形式也叫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]](privateval first: T, privateval second: T) {        def bigger():T = {            /**                  * 如果要让first和second有compareTo方法,必须要为Comparable的子类或者是Ordered的子类                  * 说白了也就是要让这个类型参数T是Comparable或者Ordered的子类                  * 一个类型是某一个类的子类,写法就要发生对应的变化                  * java的写法:                  * 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}

<%关系意味着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.5implicit def double2Int(x:Double)=x.toIntdef 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.Fileimport 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").readprintln(f)

Java.io.File本身并没有read方法。

引入隐士转换

1、Scala默认会考虑两种隐式转换,一种是源类型,或者目标类型的伴生对象内的隐式转换函数;一种是当前程序作用域内的可以用唯一标识符表示的隐式转换函数。

2、如果隐式转换不在上述两种情况下的话,那么就必须手动使用import语法引入某个包下的隐式转换函数,比如import student._。

通常建议,仅仅在需要进行隐式转换的代码部分,比如某个函数或者方法内,用import导入隐式转换函数,这样可以缩小隐式转换函数的作用域,避免不需要的隐式转换。

隐士转换规则

1、隐式转换可以定义在目标文件当中(一个Scala文件中)

//转换函数implicit def double2Int(x:Double)=x.toIntval 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 StudentSubmitReportdef signForReport(name: String) (implicit studentSReport: StudentSubmitReport) {    studentSReport.writeReport(name + "come to here")}signForReport ("jack")

完整案例

ImplicitUtil
package cn.xpleaf.bigdata.p5import java.io.Fileimport scala.io.Sourceobject 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.Fileimport cn.xpleaf.bigdata.p5.ImplicitUtil._import cn.xpleaf.bigdata.p5.StudentWriteReportimport scala.io.Sourceobject 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)    }}
0