文章目录

  • 泛类型
  • 型变
    • 协变
    • 逆变
    • 不变
  • 类型上界
  • 类型下界
  • 内部类
  • 抽象类型
  • 复合类型
  • 自类型
  • 隐式参数
  • 隐式转换
  • 多态方法
  • 类型推断

Scala是静态类型的,它拥有一个强大的类型系统,静态地强制以安全、一致的方式使用抽象,我们通过下面几个特征来一一说明:

  • 泛类型
  • 型变
  • 类型上界
  • 类型下界
  • 内部类
  • 抽象类型
  • 复合类型
  • 自类型
  • 隐式参数
  • 隐式转换
  • 多态方法
  • 类型推断

通过这些特性,为安全可重用的编程抽象以及类型安全的扩展提供了强大的基础。

泛类型

和java一样,Scala也有泛型的概念,在scala里面泛型是使用方括号 [] 来接受类型参数的。通常使用字母A来作为参数标志符,当然你也可以使用其他任意的参数名称。

class Stack[A] {private var elements: List[A] = Nildef push(x: A) { elements = x :: elements }def peek: A = elements.headdef pop(): A = {val currentTop = peekelements = elements.tailcurrentTop}
}

要使用一个泛类型,将一个具体的类型替换掉A即可。

val stack = new Stack[Int]
stack.push(1)
stack.push(2)
println(stack.pop)  // prints 2
println(stack.pop)  // prints 1

上面的例子中,实例对象接收整型值,如果该类型有子类型,子类型也是可以传入的。

class Fruit
class Apple extends Fruit
class Banana extends Fruitval stack = new Stack[Fruit]
val apple = new Apple
val banana = new Bananastack.push(apple)
stack.push(banana)

型变

型变主要是针对泛类型来说的,用来表示这种复杂类型的相关性。型变主要有协变,逆变和不变三种情况。在类型系统中使用型变允许我们在复杂类型之间建立直观的连接,而缺乏型变则会限制类抽象的重用性。

class Foo[+A] // A covariant class
class Bar[-A] // A contravariant class
class Baz[A]  // An invariant class

协变

协变使用+A来表示。对于某些类 class List[+A],使 A 成为协变意味着对于两种类型 C 和 D,如果 C 是 D 的子类型,那么 List[C] 就是 List[D] 的子类型。 这允许我们使用泛型来创建非常有用和直观的子类型关系。

abstract class Animal {def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

上面的例子中类型 Cat 和 Dog 都是 Animal 的子类型。那么
List[Cat]和List[Dog]都是List[Animal]的子类。

下面看下协变的应用:

object CovarianceTest extends App {def printAnimalNames(animals: List[Animal]): Unit = {animals.foreach { animal =>println(animal.name)}}val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))printAnimalNames(cats)// Whiskers// TomprintAnimalNames(dogs)// Fido// Rex
}

在上面的例子中,方法 printAnimalNames 将接受动物列表作为参数,并且逐行打印出它们的名称。 如果 List[A] 不是协变的,最后两个方法调用将不能编译,这将严重限制 printAnimalNames 方法的适用性。

逆变

逆变和协变相反,使用-A来表示。对于某个类 class Writer[-A] ,使 A 逆变意味着对于两种类型 C 和 D,如果 C 是 D 的子类型,那么 Writer[D] 是 Writer[C] 的子类型。

考虑一下的例子:

abstract class Printer[-A] {def print(value: A): Unit
}

我们定义两个printer:

class AnimalPrinter extends Printer[Animal] {def print(animal: Animal): Unit =println("The animal's name is: " + animal.name)
}class CatPrinter extends Printer[Cat] {def print(cat: Cat): Unit =println("The cat's name is: " + cat.name)
}

如果 Printer[Cat] 知道如何在控制台打印出任意 Cat,并且 Printer[Animal] 知道如何在控制台打印出任意 Animal,那么 Printer[Animal] 也应该知道如何打印出 Cat 就是合理的。 反向关系不适用,因为 Printer[Cat] 并不知道如何在控制台打印出任意 Animal。 因此,如果我们愿意,我们应该能够用 Printer[Animal] 替换 Printer[Cat],而使 Printer[A] 逆变允许我们做到这一点。

object ContravarianceTest extends App {val myCat: Cat = Cat("Boots")def printMyCat(printer: Printer[Cat]): Unit = {printer.print(myCat)}val catPrinter: Printer[Cat] = new CatPrinterval animalPrinter: Printer[Animal] = new AnimalPrinterprintMyCat(catPrinter)printMyCat(animalPrinter)
}

不变

默认情况下,Scala中的泛型类是不变的。这意味着虽然Cat是Animal的子类,但是Container[Cat]和Container[Animal]之间没有任何关系。

类型上界

像T <: A这样声明的类型上界表示类型变量T应该是类型A的子类。下面是个例子:

abstract class Animal {def name: String
}abstract class Pet extends Animal {}class Cat extends Pet {override def name: String = "Cat"
}class Dog extends Pet {override def name: String = "Dog"
}class Lion extends Animal {override def name: String = "Lion"
}class PetContainer[P <: Pet](p: P) {def pet: P = p
}val dogContainer = new PetContainer[Dog](new Dog)
val catContainer = new PetContainer[Cat](new Cat)// this would not compile
val lionContainer = new PetContainer[Lion](new Lion)

类PetContainer接受一个必须是Pet子类的类型参数P。因为Dog和Cat都是Pet的子类,所以可以构造PetContainer[Dog]和PetContainer[Cat]。但在尝试构造PetContainer[Lion]的时候会得到下面的错误信息:

type arguments [Lion] do not conform to class PetContainer's type parameter bounds [P <: Pet]

这是因为Lion并不是Pet的子类。

类型下界

类型下界和类型上界相反,B >: A 表示类型参数 B 或抽象类型 B 是类型 A 的超类型。

下面看个它使用的例子:

trait Node[+B] {def prepend(elem: B): Node[B]
}case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {def prepend(elem: B): ListNode[B] = ListNode(elem, this)def head: B = hdef tail: Node[B] = t
}case class Nil[+B]() extends Node[B] {def prepend(elem: B): ListNode[B] = ListNode(elem, this)
}

这个例子会在编译的时候报错。因为方法 prepend 中的参数 elem 是协变的 B 类型。

在scala中函数的参数类型是逆变的,而返回类型是协变的。

要解决这个问题,我们需要将方法 prepend 的参数 elem 的型变翻转。 我们通过引入一个新的类型参数 U 来实现这一点,该参数具有 B 作为类型下界。

trait Node[+B] {def prepend[U >: B](elem: U): Node[U]
}case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem, this)def head: B = hdef tail: Node[B] = t
}case class Nil[+B]() extends Node[B] {def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem, this)
}

这样就可以了。

内部类

内部类就是class里面的class,在java里面,内部类被看成是外部类的成员。但是在scala中内部类是和外部类的对象进行绑定的。这意味着即使是同一个外部类的不同对象,其包含的内部类是不同类型的。 我们举个例子:

class Graph {class Node {var connectedNodes: List[Node] = Nildef connectTo(node: Node) {if (!connectedNodes.exists(node.equals)) {connectedNodes = node :: connectedNodes}}}var nodes: List[Node] = Nildef newNode: Node = {val res = new Nodenodes = res :: nodesres}
}

这里connectedNodes 中存储的所有节点必须使用同一个 Graph 的实例对象的 newNode 方法来创建。

val graph1: Graph = new Graph
val node1: graph1.Node = graph1.newNode
val node2: graph1.Node = graph1.newNode
val node3: graph1.Node = graph1.newNode
node1.connectTo(node2)
node3.connectTo(node1)

这里三个node都是graph1.Node类型。如果是非graph1.Node类型则无法编译成功。

val graph1: Graph = new Graph
val node1: graph1.Node = graph1.newNode
val node2: graph1.Node = graph1.newNode
node1.connectTo(node2)      // legal
val graph2: Graph = new Graph
val node3: graph2.Node = graph2.newNode
node1.connectTo(node3)      // illegal!

那如果想达到和java中内部内中一样的效果,不区分路径该怎么办呢?使用Graph#Node即可。

class Graph {class Node {var connectedNodes: List[Graph#Node] = Nildef connectTo(node: Graph#Node) {if (!connectedNodes.exists(node.equals)) {connectedNodes = node :: connectedNodes}}}var nodes: List[Node] = Nildef newNode: Node = {val res = new Nodenodes = res :: nodesres}
}

抽象类型

抽象类型通常用T来表示,用在特质和抽象类中,表示实际类型可以由具体的实现类来确认:

trait Buffer {type Tval element: T
}

通过抽象类来扩展这个特质后,就可以添加一个类型上边界来让抽象类型T变得更加具体。

abstract class SeqBuffer extends Buffer {type Utype T <: Seq[U]def length = element.length
}

复合类型

复合类型很简单,表示多个类型的交集,用with来表示。

假设我们定义了两个traits:

trait Cloneable extends java.lang.Cloneable {override def clone(): Cloneable = {super.clone().asInstanceOf[Cloneable]}
}
trait Resetable {def reset: Unit
}

假如我们需要接受一个对象它即可以是Cloneable又可以是Resetable,那么可以这样定义:

def cloneAndReset(obj: Cloneable with Resetable): Cloneable = {//...
}

自类型

自类型的意思是在一个trait中可以使用另外一个trait中定义的属性而不必去继承它。

要在特质中使用自类型,写一个标识符,跟上要混入的另一个特质,以及 =>(例如 someIdentifier: SomeOtherTrait =>)。

trait User {def username: String
}trait Tweeter {this: User =>  // 重新赋予 this 的类型def tweet(tweetText: String) = println(s"$username: $tweetText")
}class VerifiedTweeter(val username_ : String) extends Tweeter with User {  // 我们混入特质 User 因为 Tweeter 需要def username = s"real $username_"
}val realBeyoncé = new VerifiedTweeter("Beyoncé")
realBeyoncé.tweet("Just spilled my glass of lemonade")  // 打印出 "real Beyoncé: Just spilled my glass of lemonade"

因为我们在特质 trait Tweeter 中定义了 this: User =>,现在变量 username 可以在 tweet 方法内使用。 这也意味着,由于 VerifiedTweeter 继承了 Tweeter,它还必须混入 User(使用 with User)。

隐式参数

隐式参数由 implicit 关键字标记,在方法调用的时候,scala会去尝试获取正确的隐式类型值。

Scala查找参数的位置有两个地方:

  • 首先查找可以直接访问的隐式定义和隐式参数。
  • 然后,它在所有伴生对象中查找与隐式候选类型相关的有隐式标记的成员。

下面的例子定义了两个隐式类型,stringMonoid和intMonoid。

abstract class Monoid[A] {def add(x: A, y: A): Adef unit: A
}object ImplicitTest {implicit val stringMonoid: Monoid[String] = new Monoid[String] {def add(x: String, y: String): String = x concat ydef unit: String = ""}implicit val intMonoid: Monoid[Int] = new Monoid[Int] {def add(x: Int, y: Int): Int = x + ydef unit: Int = 0}def sum[A](xs: List[A])(implicit m: Monoid[A]): A =if (xs.isEmpty) m.unitelse m.add(xs.head, sum(xs.tail))def main(args: Array[String]): Unit = {println(sum(List(1, 2, 3)))       // uses IntMonoid implicitlyprintln(sum(List("a", "b", "c"))) // uses StringMonoid implicitly}
}

在 main 方法中我们调用了 sum 方法两次,并且只传入参数 xs。 Scala 会在上例的上下文范围内寻找隐式值。 第一次调用 sum 方法的时候传入了一个 List[Int] 作为 xs 的值,这意味着此处类型 A 是 Int。 隐式参数列表 m 被省略了,因此 Scala 将查找类型为 Monoid[Int] 的隐式值。

intMonoid 是一个隐式定义,可以在main中直接访问。 并且它的类型也正确,因此它会被自动传递给 sum 方法。

第二次调用 sum 方法的时候传入一个 List[String],这意味着此处类型 A 是 String。 与查找 Int 型的隐式参数时类似,但这次会找到 stringMonoid,并自动将其作为 m 传入。

隐式转换

简单点讲,隐式转换就是当需要的时候,将某个类型S转换到另外一个类型T。这是通过定义隐式函数来确定的。

下面提供了一个隐式方法 List[A] => Ordered[List[A]] 的例子。

import scala.language.implicitConversionsimplicit def list2ordered[A](x: List[A])(implicit elem2ordered: A => Ordered[A]): Ordered[List[A]] =new Ordered[List[A]] { //replace with a more useful implementationdef compare(that: List[A]): Int = 1}

如果需要Ordered[List[A]] 而你传入List[A]的时候,scala会自动去寻找隐式的类型转换方法。

下面是一个从scala.Int到java.lang.Integer的转换:

import scala.language.implicitConversionsimplicit def int2Integer(x: Int) =java.lang.Integer.valueOf(x)

多态方法

Scala中多态是通过类型和值的参数化来实现的。 如下所示:

def listOfDuplicates[A](x: A, length: Int): List[A] = {if (length < 1)Nilelsex :: listOfDuplicates(x, length - 1)
}
println(listOfDuplicates[Int](3, 4))  // List(3, 3, 3, 3)
println(listOfDuplicates("La", 8))  // List(La, La, La, La, La, La, La, La)

上例中第一次调用方法时,我们显式地提供了类型参数 [Int]。 因此第一个参数必须是 Int 类型,并且返回类型为 List[Int]。

上例中第二次调用方法,表明并不总是需要显式提供类型参数。 编译器通常可以根据上下文或值参数的类型来推断。 在这个例子中,“La” 是一个 String,因此编译器知道 A 必须是 String。

类型推断

Scala 编译器通常可以推断出表达式的类型,因此你不必显式地声明它。

val businessName = "Montreux Jazz Café"

编译器可以发现 businessName 是 String 类型。

你也可以省略方法返回类型:

def squareOf(x: Int) = x * x

编译器可以推断出方法的返回类型为 Int,因此不需要明确地声明返回类型。

当调用 多态方法 或实例化 泛型类 时,也不必明确指定类型参数。 Scala 编译器将从上下文和实际方法的类型/构造函数参数的类型推断出缺失的类型参数。

看下面两个例子:

case class MyPair[A, B](x: A, y: B)
val p = MyPair(1, "scala") // type: MyPair[Int, String]def id[T](x: T) = x
val q = id(1)              // type: Int

编译器使用传给 MyPair 参数的类型来推断出 A 和 B 的类型。对于 x 的类型同样如此。

更多精彩内容且看:

  • 区块链从入门到放弃系列教程-涵盖密码学,超级账本,以太坊,Libra,比特币等持续更新
  • Spring Boot 2.X系列教程:七天从无到有掌握Spring Boot-持续更新
  • Spring 5.X系列教程:满足你对Spring5的一切想象-持续更新
  • java程序员从小工到专家成神之路(2020版)-持续更新中,附详细文章教程

更多教程请参考 flydean的博客

Scala教程之:静态类型相关推荐

  1. scala教程之:可见性规则

    文章目录 public Protected private scoped private 和 scoped protected 和java很类似,scala也有自己的可见性规则,不同的是scala只有 ...

  2. Scala教程之:深入理解协变和逆变

    文章目录 函数的参数和返回值 可变类型的变异 在之前的文章中我们简单的介绍过scala中的协变和逆变,我们使用+ 来表示协变类型:使用-表示逆变类型:非转化类型不需要添加标记. 假如我们定义一个cla ...

  3. Scala教程之:Either

    在之前的文章中我们提到了Option,scala中Option表示存在0或者1个元素,如果在处理异常的时候Option就会有很大的限制,因为Option如果返回None,那么我并不知道具体的异常到底是 ...

  4. Scala教程之:Enumeration

    Enumeration应该算是程序语言里面比较通用的一个类型,在scala中也存在这样的类型, 我们看下Enumeration的定义: abstract class Enumeration (init ...

  5. Scala教程之:Option-Some-None

    文章目录 Option和Some Option和None Option和模式匹配 在java 8中,为了避免NullPointerException,引入了Option,在Scala中也有同样的用法. ...

  6. Scala教程之:可扩展的scala

    文章目录 隐式类 限制条件 字符串插值 s 字符串插值器 f 插值器 raw 插值器 自定义插值器 Scala是扩展的,Scala提供了一种独特的语言机制来实现这种功能: 隐式类: 允许给已有的类型添 ...

  7. Scala教程之:函数式的Scala

    文章目录 高阶函数 强制转换方法为函数 方法嵌套 多参数列表 样例类 比较 拷贝 模式匹配 密封类 单例对象 伴生对象 正则表达式模式 For表达式 Scala是一门函数式语言,接下来我们会讲一下几个 ...

  8. Scala教程之:Scala基础

    文章目录 常量 变量 代码块 函数 方法 类 case类 对象 trait main方法 这篇文章我们大概过一下Scala的基础概念,后面的文章我们会有更详细的讲解Scala的具体内容. 常量 在Sc ...

  9. Scala教程之:面向对象的scala

    文章目录 面向对象的scala Unified Types Classes Traits 面向对象的scala 我们知道Scala是一种JVM语言,可以合java无缝衔接,这也就大大的扩展了scala ...

最新文章

  1. AI一分钟 | 谷歌租下北京 6000 平米写字楼,或将发展AI项目;工信部就个人信息保护约谈百度、支付宝、今日头条
  2. 浅谈likely与unlikely
  3. 第六十一节,html超链接和路径
  4. fx-4500科学计算机用法,应用CASIOfx-4500PA计算器计算LC50的方法
  5. Perfect Permutation
  6. go-zero:微服务框架
  7. “10%时间”:优点和缺点——敏捷海滩会议上Elizabeth Pope的报告
  8. Visual Studio启用64位 IIS Express 解决 x64位的dll 而出现 未能加载文件或程序集“xxxxxxxx”或它的某一个依赖项。试图加载格式不正确的程序。...
  9. HTML5 桌面通知:Notification API
  10. Prism初研究之使用Prism 5.0开发模块化应用
  11. Bean的装配方式之xml装配--(超详细,适合小白入门)
  12. 示波器在Multisim仿真中如何看信号周期频率
  13. warning: array subscript has type ‘char‘
  14. 输入一系列整数,建立二叉排序树,并进行前序,中序,后序遍历。
  15. python PyQt5程序运行界面无响应
  16. 这些国外支付方式,你都听过或用过吗?
  17. VScode断点调试出现unbound breakpoint(断点是灰色)问题
  18. 摩斯电码php源码,利用PHP怎么编写一个摩斯电码生成器
  19. 尔宾团队角色(Belbin Team Roles),亦被称为贝尔宾团队角色表(Belbin Team Inventory)
  20. 假前端耍耍哒哒工作两年的感悟

热门文章

  1. PE文件结构详解(四)PE导入表
  2. 灵魂画手:漫画图解 SSH
  3. Linux 权限管理: 权限的概念、权限管理、文件访问权限的设置、 粘滞位
  4. 面试官给我挖坑:单台服务器并发TCP连接数到底可以有多少 ?
  5. C++实现图的深度优先遍历和广度优先遍历
  6. Hadoop之HDFS面试知识复习
  7. 云时代,我们需要怎样的数据库?
  8. 大型前端项目的断点调试共享化和复用化实践
  9. 常说的手机刷新率60Hz、120Hz有什么不同?
  10. LF AI Day中国站火热报名中