注意
本文适合有C、C++、Java等编程语言基础的人查看。
伊始
为什么是Scala
毕业设计所逼,我课题是《基于spark的实时音乐推荐系统》,而Spark是用Scala开发的,使用Scala必然最合适。虽然Spark提供了Java、Python等的API,但貌似效果不如Scala。
且Scala自身较为独特,和Java还有很深的渊源,毕竟可以说Scala和Java8.0都出自一人之手。
网课
自己找了个网课,开干。
环境
电脑环境安装
xxx
关于版本的声明
注意,不同的Scala版本,可能会有不同的运行结果。选择版本时,要兼顾Spark等其它框架、软件的要求
我的环境和版本
后文所有内容都将基于以下版本展开,最新情况请参考官方文档
- java version “1.8.0_201”
- Scala code runner version 2.11.8
- IntelliJ IDEA 2018.2.4 (Ultimate Edition)
构建 #IU-182.4505.22, 建于 September 18, 2018
Subscription is active until January 1, 2100
JRE: 1.8.0_152-release-1248-b8 amd64
JVM: OpenJDK 64-Bit Server VM JetBrains s.r.o - Windows 10 10.0
补充:于2021年4月2日将IDEA切换为IntelliJ IDEA 2020.3.3 (Ultimate Edition)
补充:于2021年4月5日将Scala由2.11.8切换为2.13.5
IDEA插件安装
若在idea内无法下载,需要前往idea官网下载后再安装,注意插件版本问题!!! 插件需要根据idea版本选择
IDEA关联Scala源码
xxxx
基础部分1
文档注释和生成
注释
对于方法(函数)的注释,写好方法后,在其上一行键入/**
并按回车,其注释模板会自动补全:
/**
* @example
* 输入10和20 输出30
* @param number1 数字1
* @param number2 数字2
* @return 两数之和
*/
def plus(number1: Int, number2: Int): Int = {
return number1 + number2;
}
文档生成
在powershell中(保证路径无误):
scaladoc.bat -d d:\scala\mydoc\ TestScala.scala
字符串的三种输出
var name = "A_Nan"
var age = 20
// 三种输出方式
println("1. hello" + name + age)
printf("2. hello name: %s\tage: %d\n", name, age)
println(s"3. hello name: $name\tage: ${age + 20}")
结果:
变量和常量
var
生成变量
var age: Int = 10
var age2 = 20 //可省略数据类型 自动推导
var num: Double = 10.9
var name: String = "hello"
var isPass: Boolean = true
var score: Float = 11.8f
val
生成常量,不可再次修改,该种生成方法效率更高(因为在底层不用考虑线程安全问题)
Scala设计者推荐使用val
例如:一般生成对象后就不再重新指定对象,因此生成对象时可使用val
val Dog = new Dog //生成val对象 提高效率
Dog.name = "hello" //对象的属性可正常修改
Dog.name = "123"
class Dog {
var name: String = ""
}
数据类型
- Scala与Java有着相同的数据类型,但Scala中数据类型都是对象,即:Scala中没有Java的原生类型
- Scala数据类型分为值类型(AnyVal)和引用类型(AnyRef),这两者都是对象
这样就有一个好处——变量有大量方法可以使用:
注:无参方法可省略括号
var age: Int = 10
age.toString()
age.toString//无参方法可省略括号
数据类型体系图
小结
- 在Scala中有一个根类型Any,它是所有类的父类
- 在Scala中一切皆为对象,分两大类——值类型和引用类型,均为Any子类
- Null类型时Scala的特别类型,只有一个null值,它是bottom class,是所有引用类型的子类
- Nothing类型也是bottom class,它是所有类的子类,在开发中可将Nothing类型的值返回给任意变量和函数,在抛出异常中使用很多
- 在Scala中存在隐式转换(implicit conversion):低精度的值向高精度的值自动转换
具体数据类型
整形
var i = 10 //Int
var j = 10l //Long
var k = 10L //Long
注意:
这里报错的直接原因不是数字大小超过了变量number
的范围,而是因为默认情况下数字均是int类型,错误发生在赋值号之后,将数字改为Long即可:
Long最大值和最小值:
println("MinLong:" + Long.MinValue)
println("MaxLong: " + Long.MaxValue)
处理大数据会有专用类型,Long的范围远远不够!
浮点型
-
默认为Double,声明为Float要在数字后加“f”或“F”。
-
浮点型常量表示形式(两种):
十进制形式:5.12, 512.0f,. 512(必须要有小数点)
科学计数法:5.12e2, 5.12E-2
字符型(char)
-
字符常量是用单引号括起来的单个字符。
例如:var ch = 'a'
-
也允许使用转义字符
例如:var ch = '\n' //表示换行符
-
可以直接给char变量赋予整数,会按照Unicode编码保存
-
char可以进行运算
var ch: Char = 'a' var number: Int = 10 + ch + 'b' println("number: " + number)
结果:
以下为错误赋值:
Unit,Null,Nothing
自动数据类型转换
高级隐式转换和隐式函数
Scala提供了非常强大的隐式转换机制(隐式函数,隐式类等),请看高级部分
强制类型转换
将容量大的数据类型转为容量小的数据类型,但可能会造成精度降低或溢出。
var num: Int = 2.7.toInt //使用强转方法
细节说明
值类型和String类型的转换
基本数据类型转String
var str: String = 123 + ""
String转基本数据类型
使用String的.toXxx
方法
var str: String = "1234"
var num: Int = str.toInt
var num_double: Double = str.toDouble
注意:
转换时确保能够将String转为有效数据,例如:
- 不能将
"hello"
转为整数 - 不能将
"12.2"
转为Int
标识符
标识符概念
- Scala对各种变量、方法、函数等命名时使用的字符序列称为标识符
- 凡是自己可以起名字的地方都叫标识符
标识符的命名规则
- 首字符为字母,后续字符任意字母和数字,美元符号,可后接下划线_
- 数字不可以开头。
- 首字符为操作符(比如+ - * /),后续字符也需跟操作符,至少一个(反编译)
- 操作符(比如+ - * /)不能在标识符中间和最后.
- 用反引号``包括的任意字符串,即使是关键字(39个)也可以,如
var `true` = 123
运算符
/
var num1: Int = 10 / 3
var num2: Double = 10 / 3
var num3: Double = 10.0 / 3
println("num1: " + num1)
println("num2: " + num2)
println("num3: " + num3)
println("num3: " + num3.formatted("%.2f"))
%
自增自减
var num = 1
num += 1 //代替++
num -= 1 //代替--
关系运算符
生成的值均为Boolean(true或false)
规则同C、Java,此处不做介绍
逻辑运算符
- 与
&&
- 或
||
- 非
!
赋值运算符
位运算符
运算符优先级
基础部分2
读取控制台输入
package com.test.scala01
import scala.io.StdIn
object Test2 {
def main(args: Array[String]): Unit = {
val name: String = StdIn.readLine()
println(name)
}
}
流程控制
顺序
整体类似于Java,从前到后、从上到下执行
变量是向前引用,但对象、方法无限制
分支
单分支
val num = 12
if(num > 10) {
print("yes")
}
双分支
val num = 122
if (num > 1000) {
print("yes")
} else {
print("no")
}
多分支
val num = 122
if (num == 100) {
print("100")
} else if (num == 111) {
print("111")
} else {
print("no")
}
分支嵌套
同Java、C等语言,此处不做介绍,推荐嵌套不要超过三层
Scala中没有switch
在Scala中用模式匹配来处理,该方法需要知识较多,后文进行介绍
注意
在if else
中:
-
若大括号内只有一行代码,那么大括号可以省略
-
Scala中任何表达式都有返回值,包括
if else
表达式,if else
的返回内容取决于满足条件的代码体的最后一行内容,以下为特殊情况:val num = if (12 == 100) { 200 } print(num)
这里小括号()
表示返回值为Unit
,即空 -
Scala中没有三元运算符,因为可以用
if else
代替:val num = if (12 > 100) 12 else 100
循环
for
第一种:
for (i <- 1 to 5) {
//[1, 5]
println(i) //一共循环五次
}
结果:
第二种:
for (i <- 1 until 5) {
//[1,5)
println(i) //循环4次
}
在for
循环中引入变量:
可以在原括号内的后面加上分号;
后再创建新的变量,让编程更灵活
for (i <- 1 until 5; j = 6 - i) {
//分号不可省略
println(j)
}
运行结果:
注意,在Scala的for
循环中,分号;
的意义和C、Java等语言中for
循环的分号不一样了,以至于Scala的for循环嵌套可以这么写:
for (i <- 1 to 3; j <- 1 until 3) {
println("i=" + i + " j=" + j)
}
运行结果如下:
这种写法相当于:
for (i <- 1 to 3) {
for (j <- 1 until 3) {
println("i=" + i + " j=" + j)
}
}
在实际的使用中,还是这种原始的方法更为灵活。说实话,把两个循环条件写一个括号里没求用,花里胡哨反而影响代码阅读。。。。。。
注意1:
-
{}
和()
对于for来说都可以for (i <- 1 to 5) { //两个for循环功能相同 print(i) } for { i <- 1 to 5} { //两个for循环功能相同 print(i) }
-
不成文规定(非强制性):
for
推导式仅包含单一表达式用小括号,多个表达式用大括号 -
当使用大括号
{}
换行写表达式时,分号可省略for { i <- 1 to 10 j = 2 * i } { println(j) }
注意2:
要想理解1 to 10
,不妨先想想以下代码可否执行?结果是怎样?
print(1 to 10)
运行结果:
有意思吧,Range()
这个东西用处多多,下面还会提到。
注意3:
for
循环步长的控制
- 使用
Range()
for (i <- Range(1, 10, 2)) {
//范围[1, 10),步长为2
println(i)
}
- 使用循环守卫(见下文)
reverse的作用
reverse
查一下单词意思就明白了
reverse
适用于List
:
val list = List(1, 2, 3)
println(list)//1, 2, 3
println(list.reverse)//3, 2, 1
因此,如果要反着循环,可以通过如下实现:
-
使用
reverse
for (i <- 0 to 10 reverse) { println(i) }
当然,前文提到了
0 to 10
就是Range(0, 11)
,因此也可以这么写:for (i <- (0 to 10).reverse) { println(i) }
或:
for (i <- Range(0, 11).reverse) { println(i) }
-
当然,通过减法运算也可实现一样的效果
for (i <- 0 to 10) { println(10 - i) }
for循环返回值
如同if
语句一样,for
循环语句也有返回值,但要借助yield
:
val num = for (i <- 1 to 3) yield i;
print(num)
结果如下:
这就表明,yield
返回的对象是Vector
,也就是会把每次循环得到的变量i
放入Vector
中,循环结束后将Vector
赋值给变量num
。同时,在yield
后面可以是一个代码块,这样就会有更强的灵活性:
val num = for (i <- 1 to 3) yield {
if (i * i > 2 * i) {
i * i
} else {
2 * i
}
};
print(num)
结果如下:
这种特性,正体现了Scala中的一个重要特点,就是Scala擅长将一个集合中的每一个元素都进行处理后,返回给新的集合。
for循环守卫
循环守卫也叫循环保护式、条件判断式、守卫。保护式为true则进入循环体,为false则跳过,类似C语言中的continue
例如:
for (i <- 1 until 5 if i != 2) {
println(i)
}
该代码一共会循环四次,其中i等于2时跳过
故也可以通过此方法来实现循环步长的控制:
for (i <- 1 to 10 if i % 2 != 0) {
println(i)
}
while循环
同C、Java等语言相似:
- 循环条件是返回一个布尔值的表达式
- while循环是先判断再执行语句
- 与
if
语句不同,while
语句本身没有值,即整个while
语句的结果是Unit
类型的,即()
- 因为没有返回值,所以当要用该语句来计算并返回结果时,就需要使用外部变量,即在
while
循环的外部声明变量,所以会造成循环内部对外部的变量造成了影响,而这是与Scala的设计理念相违背的,因此更推荐使用for
循环
例子如下:
var i = 0
while (i < 10) {
i += 1
print("hello:" + i)
}
do-while循环
同样不推荐使用,理由同上
例子:
var i = 0
do {
i += 1
println(i + " hello")
} while (i < 5)
循环中断
在Scala中是没有break
和continue
这两个关键字的,所以无法像C、Java等语言一样通过它们来实现循环控制。这么做是为了更好地适应函数化编程。
break功能的解决方案
在Scala中,虽然没有break
关键字,但是存在break函数,即break()
。注意,关键字和函数是不一样的。
在使用break()
时,会在循环中抛出一个异常:
package com.test.scala01
import util.control.Breaks._
object Test2 {
def main(args: Array[String]): Unit = {
while (true) {
break();//此处抛出异常
}
}
}
其实,大家看到这应该就有些眉目了,Scala是通过异常处理这种方式来实现和break关键字
一样的功能。所以,既然抛出了异常,就要异常处理。
当然,这里需要使用breakable()
来接收异常,这是一个高阶函数
,高阶函数可以接受代码段作为参数:
package com.test.scala01
import util.control.Breaks._
object Test2 {
def main(args: Array[String]): Unit = {
breakable(
while (true) {
break();
}
)
}
}
这样异常就被捕获了:
不过在breakable()
的小括号中写好几行代码怪怪的,所以也可以用大括号:
package com.test.scala01
import util.control.Breaks._
object Test2 {
def main(args: Array[String]): Unit = {
breakable {
while (true) {
break();
}
}
}
}
查看breakable()
源码就能看出这种方式实现的原理:
图中的op
就是自己写的代码块,在程序运行时,直接把这些括号内的代码都作为参数传至try语句
的后面,通过try-catch
来实现循环中断。
注意:不是所有的函数的小括号都可以换为大括号!!
continue功能的解决方案
- 循环守卫(
while循环
没有守卫。。。) - 在循环体内使用
if语句
加以解决
函数
注意:函数式编程和函数是两个概念
函数的基本语法
def 函数名 ([参数:参数类型],······)[[:返回值类型]=] {
语句······
return 返回值
}
- 函数声明关键字为
def
(definition) [参数名:参数类型],···
表示函数的输入(也就是参数列表)可以有也可以没有。如果有,多个参数使用逗号间隔- 函数返回值可有可无
- 若函数体只有一行,则大括号可省略
返回值有多种情况:
- 明确给出返回值
:返回值类型=
- 返回值类型不确定,使用类型推导完成
=
- 没有返回值,return不生效
空
- 如果没有
return
,默认以执行到最后一行的结果作为返回值 - 若声明返回值为
Unit
,那么不论return
什么东西,返回值一定为Unit
,即空
,也就是()
如果写了return
,那么就必须要指明返回类型,而不能只写=
让编译器推导类型
错误写法(浅灰色内容是IDEA的自动类型显示,不是代码部分):
正确的写法:
可变参数
顾名思义,可变参数是指向函数中传入的参数的数量是可以变化的,底层是通过集合(类似于Java中的数组)来进行接收参数的。
格式
用*
表示此参数为可变参数
def main(args: Array[String]): Unit = {
printString("aaa", "bbb", "ccc")
}
def printString(s: String*): Unit = {
println(s)
}
运行结果:
注意
- 一个函数中的可变参数最多只能有一个
- 可变参数必须置于最后
思考为何如此?
参数默认值
def printString(name: String = "nan"): Unit = {
println(name)
}
使用时可以直接调用无需赋值:printString()
带名参数
在调用函数时带有参数的名称
例如:
函数主体
def printString(name: String = "nan", age: Int): Unit = {
println(name + age)
}
调用:
printString(age = 12)
这种方式可以指定向某参数传值而无需考虑参数的先后顺序。
实际使用中,函数可能参数众多,且均带有默认值
,此时若只修改某一两个参数,便可通过带名参数
的方法实现。
无参数
情况一
声明时有()
,但参数列表为空:
def he(): Unit = {
println(111)
}
调用时括号可以省略:
def main(args: Array[String]): Unit = {
he()
he
}
情况二
声明时无()
:
def he: Unit = {
println(111)
}
那么在调用时()
必须省略:
def main(args: Array[String]): Unit = {
he
}
过程
过程(procedure
)也是函数,但其返回类型为Unit
,此时等号可以省略。
def he(num: Int) {
print(num)
}
匿名函数
如果只关心函数的逻辑处理过程,而不关心函数名称,那么对于函数名称
的声明也可以省略!当然,形式上要发生一些变化。
普通函数:
def printName(name: String): Unit = {
println(name)
}
匿名函数:
(name: String) => {
println(name)
}
通常而言,匿名函数会结合高阶函数使用。高阶函数可以接受函数作为参数,如下所示:
object myClass {
def main(args: Array[String]): Unit = {
demo((name: String) => {
println(name)
})
}
//高阶函数 接受函数作为参数
//func为接受的函数名 String为接受的函数的参数
//第一个Unit为接受的函数的返回值类型
def demo(func: String => Unit): Unit = {
func("nan")
}
}
匿名函数的简化原则
- 参数类型可省略,由形参自动推导
例:
object myClass {
def main(args: Array[String]): Unit = {
//name类型省略
demo((name) => {
println(name)
})
}
def demo(func: String => Unit): Unit = {
func("nan")
}
}
- 在参数类型省略后,有且只有一个参数时,
()
可以省略,若无参数或参数数量大于1则不能省略
例:
object myClass {
def main(args: Array[String]): Unit = {
//name类型省略
//只有一个参数 圆括号可省略
demo(name => {
println(name)
})
}
def demo(func: String => Unit): Unit = {
func("nan")
}
}
- 匿名函数体若只有一行则大括号可以省略
例:
object myClass {
def main(args: Array[String]): Unit = {
//name类型省略
//只有一个参数 圆括号可省略
//函数体只有一行 大括号可以省略
demo(name => println(name))
}
def demo(func: String => Unit): Unit = {
func("nan")
}
}
- 若参数只出现