Scala 语言入门:初学者的基础语法指南
本文已收录至 GitHub,推荐阅读 👉 Java随想录
微信公众号:Java 随想录
原创不易,注重版权。转载请注明原作者和原文链接
在计算机编程的世界里,Scala 是一个不可或缺的语言。
作为一种在 Java 虚拟机(JVM)上运行的静态类型编程语言,Scala 结合了面向对象和函数式编程的特性,使它既有强大的表达力又具备优秀的型态控制。
对于初学者来说,理解 Scala 的基本语法是掌握这门语言的关键步骤。本文将带领大家逐步了解 Scala 的基础知识,无论你是编程新手还是想要扩展技能集的专业开发者,都可以在这篇文章中找到有用的信息。
先分享 Scala 的官方网站:https://docs.scala-lang.org/。
大部分的学习资料都可以在这找到,语言支持切换中文,非常友好。
另外我们可以使用 Scastie 网站,在浏览器上直接运行 Scala 代码进行调试:https://scastie.scala-lang.org/。
Scala & Java
Scala 语言和 Java 语言有许多相似之处,但也有一些明显的区别。
Scala 语言来源于 Java,它以 Java 虚拟机(JVM)为运行环境,Scala 源码 (.scala)会编译成.class 文件。这意味着 Scala 程序可以与 Java 程序互操作,并且可以利用 JVM 的优化和性能。
在语法上,Scala 和 Java 有一些区别。
例如,在 Scala 中,一切皆为对象,而在 Java 中,基本类型、null、静态方法等不是对象。在 Scala 中,成员变量/属性必须显示初始化,而在 Java 中可以不初始化。此外,在 Scala 中,异常处理采用 Try-catch {case-case}-finally 的方式,而在 Java 中采用 Try-catch-catch-finally 的方式。
Scala 还有一些特有的概念,例如:惰性函数、伴生对象、特质、偏函数等。这些概念都为 Scala 语言提供了更多的灵活性和表达能力。使得 Scala 语言非常适合用来开发大数据处理框架。此外,Scala 语言的语法糖也非常甜,可以用更少的代码量来实现相同的功能。
Scala 安装
先从安装 Scala 说起,Scala 的安装也很简单。
首先 Idea 安装 Scala 插件。
项目结构里点击全局库,添加 Scala SDK 进行下载。
右键点击添加到你要使用 Scala 的项目的项目库,项目的库里就会多出 Scala 的 SDK。
到这就结束了,然后我们就可以在项目里使用 Scala 了。
新建一个 Scala 项目,运行 Hello Wrold 试一下。
数据类型
Scala 中的数据类型可以分为两大类:值类型(AnyVal
)和引用类型(AnyRef
)。这两种类型都是 Any
类型的子类。
值类型包括 9 种基本数据类型,分别是 Byte
、Short
、Int
、Long
、Float
、Double
、Char
、Boolean
和 Unit
。其中,前 8 种类型与 Java 中的基本数据类型相对应,而 Unit
类型表示无值,类似于 Java 中的 void
。
引用类型包括所有非值类型的数据类型,例如字符串、数组、列表等。它们都是 AnyRef
类型的子类。
在 Scala 的数据类型层级结构的底部,还有两个特殊的数据类型: Nothing
和 Null
。其中, Nothing
类型是所有类型的子类型,它没有任何实例。而 Null
类型是所有引用类型的子类型,它只有一个实例: null
。
语法
主方法是一个程序的入口点。JVM 要求一个名为main
的主方法,接受一个字符串数组的参数。你可以如下所示来定义一个主方法。
在 Scala 2 中,也可以通过创建一个扩展App
类的对象来定义主程序。例如:
需要注意的是,这种方法在 Scala 3 中不再推荐使用。它们被新的@main
方法取代了,这是在 Scala 3 中生成可以从命令行调用的程序的推荐方法。App
目前仍以有限的形式存在,但它不支持命令行参数,将来会被弃用。
val & var
在 Scala 中,val
和 var
都可以用来定义变量,但它们之间有一些重要的区别。
val
用于定义不可变变量,也就是说,一旦定义了一个 val
变量,它的值就不能再被改变。例如:
而 var
用于定义可变变量,它的值可以在定义后被改变。例如:
val 和 var 的类型可以被推断,或者你也可以显式地声明类型,例如:
在实际编程中,我们应该尽量使用 val
来定义不可变变量,这样可以提高代码的可读性和可维护性。只有在确实需要改变变量值的情况下,才应该使用 var
来定义可变变量。
泛型
在 Scala 中,使用方括号 []
来定义泛型类型。而在 Java 中是使用<>
。
例如,下面这段代码:
这个示例中,我们定义了一个 Animal
特质和三个实现了该特质的类:Dog
,Cat
和 Parrot
。然后我们定义了一个 AnimalShelter
类,它使用了泛型类型参数 A
,并且限制了 A
必须是 Animal
的子类型。这样我们就可以创建不同类型的动物收容所,比如 dogShelter
,catShelter
和 parrotShelter
,并且在添加和获取动物时保证类型安全。
包导入
import
语句用于导入其他包中的成员(类,特质,函数等)。 使用相同包的成员不需要 import
语句。 导入语句可以有选择性:
Scala 不同于 Java 的一点是 Scala 可以在任何地方使用导入:
如果存在命名冲突并且你需要从项目的根目录导入,请在包名称前加上 _root_
:
注意:包 scala
和 java.lang
以及 object Predef
是默认导入的。
包对象
在 Scala 中,包对象(Package Object)是一种特殊的对象,它与包同名,并且可以在包中定义一些公共的成员和方法,供包中的其他类和对象直接使用。包对象可以解决在包级别共享常量、类型别名、隐式转换等问题。
在 Scala 中,可以使用 package
关键字定义一个包对象。包对象的文件名必须为 package.scala
,并与包名一致。
下面是关于包对象的解释和示例代码:
在上述示例中,定义了一个包对象 myapp
,位于包 com.example
下。在包对象中,我们定义了一个名为 appName
的常量和一个名为 printAppName
的方法。
这样,我们就可以在包中的其他类和对象中直接使用 appName
和 printAppName
,而无需导入或限定符。
下面是一个使用包对象的示例代码:
在上述示例中,我们在 Main
对象中直接访问了包对象 myapp
中的常量 appName
和方法 printAppName
。由于包对象与包同名且位于同一包中,因此可以直接使用它们。
特质
在 Scala 中,类是单继承的,但是特质(trait)可以多继承。
这意味着,一个类只能继承一个父类,但可以继承多个特质。这样,从结果上看,就实现了多重继承。
下面是一个例子:
例子中,定义了两个特质 A
和 B
,它们分别有一个方法 printA
和 printB
。然后我们定义了一个类 C
,它继承了特质 A
和 B
。这样,类 C
就可以使用特质 A
和 B
中定义的方法了。
特质也可以有默认的实现:
你可以使用extends
关键字来继承特质,使用override
关键字来覆盖默认的实现。
凡是需要特质的地方,都可以由该特质的子类型来替换。
在这里 trait Pet
有一个抽象字段 name
,name
由 Cat 和 Dog 的构造函数中实现。最后一行,我们能调用pet.name
的前提是它必须在特质 Pet 的子类型中得到了实现。
运算符
在 Scala 中,运算符是用于执行特定操作的符号或标记。Scala 具有丰富的运算符,并且允许用户自定义运算符,以及在自定义类中使用运算符。下面是关于定义和使用运算符的解释和示例代码:
在 Scala 中,可以使用 def
关键字定义自定义运算符。自定义运算符可以是任何由字母、数字或下划线组成的标识符,以及一些特殊字符,例如 +
、-
、*
等。要定义一个运算符,可以在方法名前面加上一个操作符,然后在方法体中实现相应的逻辑。
下面是一个示例代码:
在上述示例中,定义了一个 Vector2D
类,表示二维向量。我们通过 val
关键字定义了 x
和 y
作为向量的坐标。
然后,我们定义了一个自定义运算符 +
,它接受另一个 Vector2D
对象作为参数,并返回一个新的 Vector2D
对象。在方法体内,我们实现了向量的加法操作。
在主程序中,我们创建了两个 Vector2D
对象 v1
和 v2
。然后,我们使用自定义的运算符 +
来执行向量的加法,并将结果赋值给 sum
。
最后,我们打印出 sum
的 x
和 y
坐标,验证加法操作的结果。
我们可以像使用内置运算符一样使用自定义运算符。它们可以用于相应类型的实例上,并按照定义的逻辑执行操作。
下面是一个示例代码:
在上述示例中,我们定义了两个整数变量 num1
和 num2
。然后,我们使用内置的运算符 +
、-
和 *
来执行加法、减法和乘法操作,并将结果分别赋值给 sum
、difference
和 product
。
传名参数
传名参数(Call-by-Name Parameters)是一种特殊的参数传递方式,它允许我们将表达式作为参数传递给函数,并在需要时进行求值。传名参数使用 =>
符号来定义,以表示传递的是一个表达式而不是具体的值。
传名参数的特点是,在每次使用参数时都会重新求值表达式,而不是在调用函数时进行求值。这样可以延迟表达式的求值,只在需要时才进行计算。传名参数通常用于需要延迟计算、惰性求值或者需要按需执行的场景。
下面是一个示例代码:
在上述示例中,定义了一个名为 callByName
的函数,它接受一个传名参数 param
。在函数体内,我们打印出两次参数的值。
另外,定义了一个名为 randomNumber
的函数,它用于生成随机数。在该函数内部,我们打印出生成随机数的消息,并使用 scala.util.Random.nextInt
方法生成一个介于 0 到 100 之间的随机数。
在主程序中,我们调用 callByName
函数,并将 randomNumber()
作为传名参数传递进去。
当程序执行时,会先打印出 "Inside callByName" 的消息,然后两次调用 param
,即 randomNumber()
。在每次调用时,都会重新生成一个新的随机数,并打印出相应的值。
这说明传名参数在每次使用时都会重新求值表达式,而不是在调用函数时进行求值。这样可以实现按需执行和延迟计算的效果。
implicit
implicit
关键字用于定义隐式转换和隐式参数。它可以用来简化代码,让编译器自动执行一些操作。
下面是一些使用 implicit
关键字的示例:
隐式转换:可以使用
implicit
关键字定义隐式转换函数,让编译器自动将一种类型的值转换为另一种类型的值。
在这个例子中,定义了一个隐式转换函数 intToString
,它接受一个 Int
类型的参数,并返回它的字符串表示。由于这个函数被定义为 implicit
,因此编译器会在需要时自动调用它。
在主程序中,我们将一个 Int
类型的值赋值给一个 String
类型的变量。由于类型不匹配,编译器会尝试寻找一个隐式转换函数来将 Int
类型的值转换为 String
类型的值。在这个例子中,编译器找到了我们定义的 intToString
函数,并自动调用它将 1
转换为 "1"
。
隐式参数:可以使用
implicit
关键字定义隐式参数,让编译器自动为方法提供参数值。
在这个例子中,定义了一个隐式值 x
并赋值为 1
。然后我们定义了一个方法 foo
,它接受一个隐式参数 x
。
在主程序中,我们调用了方法 foo
,但没有显式地传入参数。由于方法 foo
接受一个隐式参数,因此编译器会尝试寻找一个隐式值来作为参数传入。在这个例子中,编译器找到了我们定义的隐式值 x
并将其作为参数传入方法 foo
。
Object & Class
在 Scala 中,class
和 object
都可以用来定义类型,但它们之间有一些重要的区别。class
定义了一个类,它可以被实例化。每次使用 new
关键字创建一个类的实例时,都会创建一个新的对象。
构造器可以通过提供一个默认值来拥有可选参数:
在这个版本的Point
类中,x
和y
拥有默认值0
所以没有必传参数。然而,因为构造器是从左往右读取参数,所以如果仅仅要传个y
的值,你需要带名传参。
而 object
定义了一个单例对象。它不能被实例化,也不需要使用 new
关键字创建。在程序中,一个 object
只有一个实例。此外,object
中定义的成员都是静态的,这意味着它们可以在不创建实例的情况下直接访问。而 class
中定义的成员只能在创建实例后访问。
另外,在 Scala 中,如果一个 object
的名称与一个 class
的名称相同,那么这个 object
被称为这个 class
的伴生对象。伴生对象和类可以相互访问彼此的私有成员:
在这个例子中,定义了一个类 MyClass
和它的伴生对象 MyClass
。类 MyClass
中定义了一个私有成员变量 secret
和一个方法 printCompanionSecret
,用于打印伴生对象中的私有成员变量 companionSecret
。而伴生对象 MyClass
中定义了一个私有成员变量 companionSecret
和一个方法 printSecret
,用于打印类 MyClass
的实例中的私有成员变量 secret
。
在主程序中,创建了一个类 MyClass
的实例 a
,并调用了它的 printCompanionSecret
方法。然后我们调用了伴生对象 MyClass
的 printSecret
方法,并将实例 a
作为参数传入。
这就是 Scala 中类和伴生对象之间互相访问私有成员的基本用法。
样例类
样例类(case class)是一种特殊的类,**常用于描述不可变的值对象(Value Object) **。
它们非常适合用于不可变的数据。定义一个样例类非常简单,只需在类定义前加上case
关键字即可。例如,下面是一个简单的样例类定义:
创建样例类的实例时,不需要使用new
关键字,直接使用类名即可。例如,下面是一个创建样例类实例并修改其成员变量的示例:
_(下划线)
在 Scala 中,下划线 _
是一个特殊的符号,它可以用在许多不同的地方,具有不同的含义。
作为通配符:下划线可以用作通配符,表示匹配任意值。例如,在模式匹配中,可以使用下划线来表示匹配任意值。
作为忽略符:下划线也可以用来忽略不需要的值。例如,在解构赋值时,可以使用下划线来忽略不需要的值。
作为函数参数占位符:下划线还可以用作函数参数的占位符,表示一个匿名函数的参数。例如,在调用高阶函数时,可以使用下划线来简化匿名函数的定义。
将方法转换为函数:在方法名称后加一个下划线,会将其转化为偏应用函数(partially applied function),就能直接赋值了。
这只是下划线在 Scala 中的一些常见用法。由于下划线在不同的上下文中具有不同的含义,因此在使用时需要根据具体情况进行判断。
println
println
函数用于向标准输出打印一行文本。它可以接受多种不同类型的参数,并将它们转换为字符串进行输出。
下面是一些常见的使用 println
函数进行输出的方式:
输出字符串:直接将字符串作为参数传入
println
函数,它会将字符串原样输出。
输出变量:将变量作为参数传入
println
函数,它会将变量的值转换为字符串并输出。
输出表达式:将表达式作为参数传入
println
函数,它会计算表达式的值并将其转换为字符串输出。
使用字符串插值:可以使用字符串插值来格式化输出。在字符串前加上
s
前缀,然后在字符串中使用${expression}
的形式来插入表达式的值。
这些是 println
函数的一些常见用法。你可以根据需要使用不同的方式来格式化输出。
集合
在 Scala 中,集合有三大类:序列 Seq、集 Set、映射 Map,所有的集合都扩展自 Iterable,所以 Scala 中的集合都可以使用 foreach 方法。在 Scala 中集合有可变(mutable)和不可变(immutable)两种类型。
List
如我们可以使用如下方式定义一个 List,其他集合类型的定义方式也差不多。
下面是一些 List 的常用方法:
更多方法不再赘述,网上很容易查阅到相关文章。
Map
下面是 map 常用的一些方法:
这里的case
关键字起到匹配的作用。
Range
Range
属于序列(Seq
)这一类集合的子集。它表示一个整数序列,可以用来遍历一个整数区间内的所有整数。例如,1 to 5
表示一个从 1 到 5 的整数序列,包括 1 和 5。
Range 常见于 for 循环中,如下可定义一个 Range:
如果我们想把 Range 转为 List,我们可以这样做:
Range
继承自Seq
,因此它拥有Seq
的所有常用方法,例如length
、head
、last
、tail
、init
、reverse
、isEmpty
、contains
、filter
、map
、foldLeft
和foldRight
等。它还拥有一些特殊的方法,例如:
迭代器
迭代器(Iterator)是一种用于遍历集合中元素的工具。它提供了一种方法来访问集合中的元素,而不需要暴露集合的内部结构。在 Scala 中,你可以使用 iterator
方法来获取一个集合的迭代器。
特别注意:迭代器是一次性的,所以在使用完毕后就不能再次使用。因此,在上面的代码中,我们在调用 next
方法后就不能再使用其他方法来访问迭代器中的元素了。所以 size1 输出为 0。
Tuple
把Tuple
从集合中抽出来讲述是因为Tuple
不属于集合。它是一种用来将多个值组合在一起的数据结构。一个Tuple
可以包含不同类型的元素,每个元素都有一个固定的位置。Scala 中的元组包含一系列类:Tuple2,Tuple3 等,直到 Tuple22。
示例如下:
下面是一些 Tuple 的常用方法:
提取器对象
提取器对象是一个包含有 unapply
方法的单例对象。apply
方法就像一个构造器,接受参数然后创建一个实例对象,反之 unapply
方法接受一个实例对象然后返回最初创建它所用的参数。提取器常用在模式匹配和偏函数中。
下面是一个使用提取器对象(Extractor Object)的 Scala 代码示例:
在上述示例中,定义了一个名为Email
的提取器对象。提取器对象具有两个方法:apply
和unapply
。
apply
方法接收用户名和域名作为参数,并返回一个完整的电子邮件地址。在这个示例中,我们简单地将用户名和域名拼接成电子邮件地址的字符串。
unapply
方法接收一个电子邮件地址作为参数,并返回一个Option
类型的元组。在这个示例中,我们使用split
方法将电子邮件地址分割为用户名和域名两部分,并通过Some
将它们封装到一个Option
中返回。如果分割后的部分不是两部分,即电子邮件地址不符合预期的格式,我们返回None
。
在测试部分,我们创建了一个电子邮件地址字符串address
。然后,我们使用match
表达式将address
与提取器对象Email
进行匹配。如果匹配成功,我们提取出用户名和域名,并打印出对应的信息。如果匹配失败,即电子邮件地址无效,我们打印出相应的错误信息。
流程判断
while & if
Scala 中的 while 和 if 跟 Java 中的方法几乎没有区别。
for
for 循环跟 Java 略微有点区别。其中i <- 1 to 5
是 Scala 中 for 循环的一种常见形式。它表示遍历一个序列,序列中的元素依次为 1、2、3、4、5。
多重 for 循环简写
Scala 中对于多重 for 循环可以进行简写,例如我们要用 Java 写多重 for 循环是下面这样:
而用 Scala 我们可以直接简写为下面这样:
可以看出 scala 的 for 循环语法更加的精简。代码行数更少。
yield
在 for 循环的过程中我们可以使用 yield 来对 for 循环的元素进行 操作收集:
模式匹配(pattern matching)
在 Scala 语言中,没有switch
和case
关键字。相反,我们可以使用模式匹配(pattern matching)来实现类似于switch
语句的功能。它是 Java 中的switch
语句的升级版,同样可以用于替代一系列的 if/else 语句。下面是一个简单的例子,它展示了如何使用模式匹配来实现类似于switch
语句的功能:
在上面的例子中,定义了一个名为matchTest
的函数,它接受一个类型为Any
的参数x
。在函数体中,我们使用了一个模式匹配表达式来匹配参数x
的值。
在模式匹配表达式中,我们定义了四个case
子句。第一个case
子句匹配值为 1 的情况;第二个case
子句匹配值为"two"的情况;第三个case
子句匹配类型为Int
的情况;最后一个case
子句匹配所有其他情况。
样例类(case classes)的匹配
样例类非常适合用于模式匹配。
这段代码定义了一个抽象类 Notification
,以及两个扩展自 Notification
的样例类 Email
和 SMS
。然后定义了一个函数 showNotification
,它接受一个 Notification
类型的参数,并使用模式匹配来检查传入的通知是 Email
还是 SMS
,并相应地生成一条消息。
最后,我们创建了两个实例:一个 SMS
和一个 Email
,并使用 showNotification
函数来显示它们的消息。
模式守卫(Pattern guards)
为了让匹配更加具体,可以使用模式守卫,也就是在模式后面加上if <boolean expression>
。
在上述示例中,我们定义了一个名为checkNumberType
的方法,它接收一个整数参数number
并返回一个描述数字类型的字符串。
通过使用模式守卫,我们可以对number
进行多个条件的匹配,并根据条件来返回相应的结果。在每个case
语句中,我们使用模式守卫来进一步过滤匹配的数字。
例如,case n if n > 0 && n % 2 == 0
表示当 number
大于 0 且为偶数时执行该分支。类似地,其他的 case
语句也使用了模式守卫来进行更精确的匹配。
在测试部分,我们调用了checkNumberType
方法并传入不同的整数进行测试。根据不同的输入,方法将返回相应的字符串描述数字类型。
仅匹配类型
当不同类型对象需要调用不同方法时,仅匹配类型的模式非常有用
在上述示例中,定义了一个名为processValue
的方法,它接收一个任意类型的参数value
,并返回一个描述值类型的字符串。
通过使用类型模式匹配,我们可以根据不同的值类型来执行相应的逻辑。在每个case
语句中,我们使用类型模式匹配来匹配特定类型的值。
例如,case str: String
表示当 value
的类型为 String
时执行该分支,并将其绑定到变量 str
。类似地,其他的 case
语句也使用了类型模式匹配来匹配不同的值类型。
在测试部分,我们调用了processValue
方法并传入不同类型的值进行测试。根据值的类型,方法将返回相应的描述字符串。
Scala 的模式匹配是我觉得非常实用和灵活的一个功能,比 Java 的switch
语句更加强大和灵活。Scala 的模式匹配可以匹配不同类型的值,包括数字、字符串、列表、元组等。而 Java 的switch
语句只能匹配整数、枚举和字符串类型的值。
密封类
特质(trait)和类(class)可以用sealed
标记为密封的,这意味着其所有子类都必须与之定义在相同文件中,从而保证所有子类型都是已知的。密封类限制了可扩展的子类类型,并在模式匹配中确保所有可能的类型都被处理,提高了代码的安全性和可靠性。
下面是一个使用密封类(sealed class)和模式匹配的 Scala 代码示例:
在上述示例中,我们定义了一个密封类Shape
,它是一个抽象类,不能直接实例化。然后,我们通过扩展Shape
类创建了Circle
、Rectangle
和Square
这三个子类。
在calculateArea
方法中,我们使用模式匹配对传入的shape
进行匹配,并根据不同的Shape
子类执行相应的逻辑。在每个case
语句中,我们根据具体的形状类型提取相应的属性,并计算出面积。
在测试部分,我们创建了一个Circle
对象、一个Rectangle
对象和一个Square
对象,并分别调用calculateArea
方法计算它们的面积。
嵌套方法
当在 Scala 中定义一个方法时,我们可以选择将其嵌套在另一个方法内部。这样的嵌套方法只在外部方法的作用域内可见,而对于外部方法以外的代码是不可见的。这可以帮助我们组织和封装代码,提高代码的可读性和可维护性。
在上述示例中,定义了一个外部方法calculateDiscountedPrice
,它接收原始价格originalPrice
和折扣百分比discountPercentage
作为参数,并返回最终价格。
在calculateDiscountedPrice
方法的内部,我们定义了两个嵌套方法:applyDiscount
和validateDiscount
。applyDiscount
方法用于计算折扣后的价格,它接收价格和折扣作为参数,并返回折扣后的价格。validateDiscount
方法用于验证折扣百分比是否超过最大折扣限制,并返回一个有效的折扣百分比。
在外部方法中,我们首先调用validateDiscount
方法来获取有效的折扣百分比,然后将其与原始价格一起传递给applyDiscount
方法,计算最终价格。最后,我们打印出最终价格。
正则表达式模型
正则表达式是用来找出数据中的指定模式(或缺少该模式)的字符串。.r
方法可使任意字符串变成一个正则表达式。
在上述示例中,我们首先创建了一个名为emailPattern
的正则表达式对象,用于匹配电子邮件地址的模式。
然后,定义了一个名为validateEmail
的方法,它接收一个字符串类型的电子邮件地址作为参数,并使用正则表达式模式匹配来验证电子邮件地址的有效性。
在模式匹配的case
语句中,我们使用emailPattern
对传入的电子邮件地址进行匹配,并将匹配结果中的用户名、域名和扩展提取到相应的变量中。如果匹配成功,我们打印出验证通过的消息,并返回true
表示电子邮件地址有效。如果没有匹配成功,则打印出验证失败的消息,并返回false
表示电子邮件地址无效。
在测试部分,我们调用validateEmail
方法分别传入一个有效的电子邮件地址和一个无效的电子邮件地址进行测试。根据匹配结果,我们打印出相应的验证消息。
型变
在 Scala 中,协变(covariance)和逆变(contravariance)是用来描述类型参数在子类型关系中的行为的概念。协变和逆变是用来指定泛型类型参数的子类型关系的方式,以确保类型安全性。
协变
协变(Covariance): 协变表示类型参数在子类型关系中具有相同的方向。如果一个泛型类的类型参数是协变的,那么子类型的关系将保持不变,即父类型可以被替换为子类型。在 Scala 中,可以使用 +
符号来表示协变。
下面是一个使用协变的示例代码,使用 +
符号表示类型参数 A
是协变的:
在上述示例中,我们定义了一个协变类 Cage[+A]
,它接受一个类型参数 A
,并使用 +
符号来表示 A
是协变的。我们创建了一个 dogCage
,它是一个 Cage[Dog]
类型的实例。然后,我们将 dogCage
赋值给一个类型为 Cage[Animal]
的变量 animalCage
,这是合法的,因为 Cage[+A]
的协变性允许我们将子类型的 Cage
赋值给父类型的 Cage
。
逆变
逆变(Contravariance): 逆变表示类型参数在子类型关系中具有相反的方向。如果一个泛型类的类型参数是逆变的,那么子类型的关系将反转,即父类型可以替换为子类型。在 Scala 中,可以使用 -
符号来表示逆变。
下面是一个使用逆变的示例代码,使用 -
符号表示类型参数 A
是逆变的:
在上述示例中,定义了一个逆变类 Cage[-A]
,它接受一个类型参数 A
,并使用 -
符号来表示 A
是逆变的。我们创建了一个 animalCage
,它是一个 Cage[Animal]
类型的实例。然后,我们将 animalCage
赋值给一个类型为 Cage[Dog]
的变量 dogCage
,这是合法的,因为 Cage[-A]
的逆变性允许我们将父类型的 Cage
赋值给子类型的 Cage
。通过协变和逆变,我们可以在 Scala 中实现更灵活的类型关系,并确保类型安全性。这在处理泛型集合或函数参数时特别有用。下面是一个更具体的示例:
在上述示例中,定义了一个抽象类 Animal
,以及它的两个子类 Dog
和 Cat
。Dog
和 Cat
类都实现了 name
方法。
然后,定义了一个协变类 Cage[+A]
,它接受一个类型参数 A
,并使用协变符号 +
表示 A
是协变的。Cage
类有一个名为 animal
的属性,它的类型是 A
,也就是动物的类型。我们定义了一个名为 showAnimal()
的方法,它打印出 animal
的名称。
接下来,定义了一个名为 printAnimalNames()
的函数,它接受一个类型为 Cage[Animal]
的参数,并打印出其中动物的名称。
我们创建了一个 Dog
类型的对象 dog
和一个 Cat
类型的对象 cat
。然后,我们分别创建了一个 Cage[Dog]
类型的 dogCage
和一个 Cage[Cat]
类型的 catCage
。
最后,我们分别调用 printAnimalNames()
函数,并传入 dogCage
和 catCage
。由于 Cage
类是协变的,所以可以将 Cage[Dog]
和 Cage[Cat]
赋值给 Cage[Animal]
类型的参数,而不会产生类型错误。
类型限界
在 Scala 中,类型上界(Upper Bounds)和类型下界(Lower Bounds)是用于限制泛型类型参数的范围的概念。它们允许我们在泛型类或泛型函数中指定类型参数必须满足某种条件。下面是关于类型上界和类型下界的解释和示例代码:
类型上界
类型上界(Upper Bounds): 类型上界用于指定泛型类型参数必须是某个类型或其子类型。我们使用 <:
符号来定义类型上界。例如,A <: B
表示类型参数 A
必须是类型 B
或其子类型。
下面是一个使用类型上界的示例代码:
在上述示例中,定义了一个抽象类 Animal
,以及它的子类 Dog
。Dog
类继承自 Animal
类,并实现了 name
方法。
然后,定义了一个泛型类 Cage[A <: Animal]
,它接受一个类型参数 A
,并使用类型上界 A <: Animal
来确保 A
是 Animal
类型或其子类型。Cage
类有一个名为 animal
的属性,它的类型是 A
。我们定义了一个名为 showAnimal()
的方法,它打印出 animal
的名称。
创建了一个 Dog
类型的对象 dog
。然后,我们创建了一个 Cage[Animal]
类型的 cage
,并将 dog
对象作为参数传递给它。
最后,调用 cage
的 showAnimal()
方法,它成功打印出了 Dog
对象的名称。
类型下界
类型下界(Lower Bounds): 类型下界用于指定泛型类型参数必须是某个类型或其父类型。我们使用 >
符号来定义类型下界。例如,A >: B
表示类型参数 A
必须是类型 B
或其父类型。
下面是一个使用类型下界的示例代码:
在上述示例中,定义了一个基类 Animal
,以及两个子类 Dog
和 Cat
。这些类都有一个 sound()
方法,用于输出不同的动物声音。
接下来,定义了一个泛型函数 makeSound[A >: Dog](animal: A)
,其中类型参数 A
的下界被定义为 Dog
,即 A >: Dog
。这意味着 A
必须是 Dog
类型或其父类型。
在 makeSound()
函数内部,我们调用传入的 animal
对象的 sound()
方法。
然后,创建了一个 Dog
对象 dog
和一个 Cat
对象 cat
。
最后,分别调用 makeSound()
函数,并将 dog
和 cat
作为参数传递进去。由于类型下界被定义为 Dog
,所以 dog
参数符合条件,而 cat
参数被隐式地向上转型为 Animal
,也满足条件。因此,调用 makeSound()
函数时,输出了不同的声音。
通过类型上界和类型下界,我们可以对泛型类型参数的范围进行限制,以确保类型的约束和类型安全性。这使得我们能够编写更灵活、可复用且类型安全的代码。
内部类
在 Scala 中,内部类是一个定义在另一个类内部的类。内部类可以访问外部类的成员,并具有更紧密的关联性。下面是一个关于 Scala 中内部类的解释和示例代码:
在 Scala 中,内部类可以分为两种类型:成员内部类(Member Inner Class)和局部内部类(Local Inner Class)。
成员内部类:成员内部类是定义在外部类的作用域内,并可以直接访问外部类的成员(包括私有成员)。成员内部类可以使用外部类的实例来创建和访问。
下面是一个示例代码:
在上述示例中,定义了一个外部类 Outer
,它包含一个私有成员 outerField
。内部类 Inner
定义在 Outer
的作用域内,并可以访问外部类的成员。
在主程序中,创建了外部类的实例 outer
。然后,我们使用 outer.Inner
来创建内部类的实例 inner
。注意,我们需要使用外部类的实例来创建内部类的实例。
最后,调用内部类 inner
的 printOuterField()
方法,它成功访问并打印了外部类的私有成员 outerField
。
局部内部类: 局部内部类是定义在方法或代码块内部的类。局部内部类的作用域仅限于所在方法或代码块内部,无法从外部访问。
下面是一个示例代码:
在上述示例中,定义了一个外部方法 outerMethod
。在方法内部,我们定义了一个局部变量 outerField
和一个局部内部类 Inner
。
在方法内部,创建了内部类 Inner
的实例 inner
。注意,内部类的作用域仅限于方法内部。
最后,调用内部类 inner
的 printOuterField()
方法,它成功访问并打印了外部变量 outerField
。
通过使用内部类,我们可以在 Scala 中实现更紧密的关联性和封装性,同时允许内部类访问外部类的成员。内部类在某些场景下可以提供更清晰和组织良好的。
复合类型
在 Scala 中,复合类型(Compound Types)允许我们定义一个类型,它同时具有多个特质(Traits)或类的特性。复合类型可以用于限制一个对象的类型,以便它同时具备多个特性。下面是关于复合类型的解释和示例代码:
复合类型使用 with
关键字将多个特质或类组合在一起,形成一个新的类型。
下面是一个示例代码:
在上述示例中,定义了两个特质 Flyable
和 Swimmable
,分别表示可飞行和可游泳的特性。然后,我们定义了两个类 Bird
和 Fish
,分别实现了相应的特质。
接下来,定义了一个方法 action
,它接受一个类型为 Flyable with Swimmable
的参数。这表示参数必须同时具备 Flyable
和 Swimmable
的特性。
在主程序中,创建了一个 Bird
对象 bird
和一个 Fish
对象 fish
。
最后,分别调用 action
方法,并将 bird
和 fish
作为参数传递进去。由于它们都同时具备 Flyable
和 Swimmable
的特性,所以可以成功调用 fly()
和 swim()
方法。
通过使用复合类型,可以在 Scala 中定义一个类型,它同时具备多个特质或类的特性,从而实现更灵活和精确的类型约束。这有助于编写更可靠和可复用的代码。
多态方法
在 Scala 中,多态方法(Polymorphic Methods)允许我们定义可以接受多种类型参数的方法。这意味着同一个方法可以根据传入参数的类型执行不同的逻辑。下面是关于多态方法的解释和示例代码:
多态方法使用类型参数来定义方法的参数类型,并使用泛型来表示可以接受多种类型参数。在方法内部,可以根据类型参数的实际类型执行不同的逻辑。
下面是一个示例代码:
在上述示例中,定义了一个多态方法 printType
,它接受一个类型参数 T
。根据传入参数的类型,我们使用模式匹配来判断其实际类型,并执行相应的逻辑。
在方法内部,使用 match
表达式对传入的参数 value
进行模式匹配。对于不同的类型,我们分别输出相应的类型信息。
在主程序中,多次调用 printType
方法,并传入不同类型的参数。根据传入的参数类型,方法会执行相应的逻辑并输出对应的类型信息。
函数
Scala 中一个简单的函数定义如下,我们可以在 Scala 中使用 JDK 的类:
函数默认值
在 Scala 中,可以为函数参数指定默认值。这样,当调用函数时如果没有提供参数值,将使用默认值。下面是一个简单的示例:
高阶函数
高阶函数是指使用其他函数作为参数、或者返回一个函数作为结果的函数。在 Scala 中函数是“一等公民”,所以允许定义高阶函数。这里的术语可能有点让人困惑,我们约定,使用函数值作为参数,或者返回值为函数值的“函数”和“方法”,均称之为“高阶函数”。
在这个例子中,applyFuncToList
函数接受一个整数列表和一个函数 f
,该函数将一个整数作为输入并返回一个整数。然后,applyFuncToList
函数使用 map
方法将函数 f
应用于列表中的每个元素。在上面的代码中,我们定义了一个 double
函数,它将输入乘以 2,并将其传递给 applyFuncToList
函数以对数字列表中的每个元素进行加倍。
匿名函数
在 Scala 中,匿名函数是一种没有名称的函数,可以用来创建简洁的函数字面量。它们通常用于传递给高阶函数,或作为局部函数使用。
例如,下面是一个简单的匿名函数,它接受两个整数参数并返回它们的和:
偏应用函数
简单来说,偏应用函数就是一种只对输入值的某个子集进行处理的函数。它只会对符合特定条件的输入值进行处理,而对于不符合条件的输入值则会抛出异常。
举个例子:
这个例子中,divide
是一个偏应用函数,它只定义了对非零整数的除法运算。如果我们尝试用 divide
函数去除以零,它会抛出一个异常。其中isDefinedAt
是一个方法,它用于检查偏应用函数是否在给定的输入值上定义。如果偏应用函数在给定的输入值上定义,那么 isDefinedAt
方法会返回 true
,否则返回 false
。
为了避免这种情况,我们可以使用 divideSafe
函数,它返回一个 Option
类型的结果。如果除数为零,它会返回 None
而不是抛出异常。
柯里化函数
柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术。我们可以使用柯里化来定义函数,例如:
这个 add
函数接受两个参数 a
和 b
,并返回它们的和。由于它是一个柯里化函数,所以我们可以将它看作是一个接受单个参数 a
的函数,它返回一个接受单个参数 b
的函数。
我们可以这样调用这个函数:
或者这样:
在上面的例子中,我们首先调用 add
函数并传入第一个参数 1
,然后我们得到一个新的函数 addOne
,它接受一个参数并返回它与 1 的和。最后,我们调用 addOne
函数并传入参数 2
,得到结果 3。
柯里化函数可以帮助我们实现参数复用和延迟执行等功能。
柯里化函数的好处之一是它可以让我们给一个函数传递较少的参数,得到一个已经记住了某些固定参数的新函数。这样,我们就可以在不同的地方使用这个新函数,而不需要每次都传递相同的参数²。
此外,柯里化函数还可以帮助我们实现函数的延迟计算。当我们传递部分参数时,它会返回一个新的函数,可以在新的函数中继续传递后面的参数。这样,我们就可以根据需要来决定何时执行这个函数。
惰性函数
可以使用 lazy
关键字定义惰性函数。惰性函数的执行会被推迟,直到我们首次对其取值时才会执行。
下面是一个简单的例子,展示了如何定义和使用惰性函数:
在这个例子中,我们定义了一个函数 sum
,它接受两个参数并返回它们的和。然后我们定义了一个惰性值 res
并将其赋值为 sum(1, 2)
。
在主程序中,我们首先打印了一行分隔符。然后我们打印了变量 res
的值。由于 res
是一个惰性值,因此在打印它之前,函数 sum
并没有被执行。只有当我们首次对 res
取值时,函数 sum
才会被执行。
这就是 Scala 中惰性函数的基本用法。你可以使用 lazy
关键字定义惰性函数,让函数的执行被推迟。
总结
在总结之处,我希望强调 Scala 的美学和实用性。它是一种同时支持函数式编程和面向对象编程的语言,Scala 的语法设计使其对初学者非常友好,同时也为更深入地探索编程提供了空间。
学习 Scala 不仅能够帮助你提高编程效率,还能开阔你的编程视野。当你熟练掌握 Scala 后,你将发现一个全新的、充满无限可能的编程世界正在向你敞开。今天,我们只是轻轻掀开了 Scala 的神秘面纱,未来等待你去挖掘的还有更多。
请继续探索和尝试,让自己真正理解并掌握 Scala 的精髓。持续学习,不断思考,享受编程的乐趣。
最后,希望这篇文章能给你带来收获和思考。
感谢阅读,如果本篇文章有任何错误和建议,欢迎给我留言指正。
老铁们,关注我的微信公众号「Java 随想录」,专注分享 Java 技术干货,文章持续更新,可以关注公众号第一时间阅读。
一起交流学习,期待与你共同进步!
版权声明: 本文为 InfoQ 作者【码农BookSea】的原创文章。
原文链接:【http://xie.infoq.cn/article/9181e211b1b5f5de48abf74b0】。文章转载请联系作者。
评论