学习Scala是因为Spark框架就是有Scala编写的,想学习Spark,首先需要对Scala有一定的了解。
Scala的语言特点:
Scala是一门以Java虚拟机(JVM)为运行环境并将面向对象和函数式编程的最佳特性结合在一起的静态类型编程语言(静态语言需要提前编译的如:Java、c、c++等,动态语言如:js)。
(1)Scala是一门多范式的编程语言,Scala支持面向对象和函数式编程。(多范式,就是多种编程方法的意思。有面向过程、面向对象、泛型、函数式四种程序设计方法。)
(2)Scala源代码(.scala)会被编译成Java字节码(.class),然后运行于JVM之上,并可以调用现有的Java类库,实现两种语言的无缝对接。
(3)Scala单作为一门语言来看,非常的简洁高效。
可以说Scala就是在Java的基础上,加上了自己函数式编程思想的语言。Scala的SDK中有Java的类库,有Scala自己特有的类库,也对Java的部分类库做了特定的包装。Scala同样的经过SDK编译后,会生成.class文件,然后可以在各种环境(windows,linux,unix)的JVM上面运行。也能说Scala就是兼容Java的,所以Scala同时需要Java的JDK和自己的SDK的支持。
(1)首先需要按照对应版本的Java的JDK,并配置好环境变量
(2)下载对应的Scala安装文件,并解压到不带中文的目录下
(3)对应的配置Scala的环境变量,SCALA_HOME,开启cmd,输入scala测试即可
(4)IDEA默认不支持Scala的开发,需要安装插件;下载插件(scala-intellij-bin-2017.2.6.zip)并按照File->Setting->Plugins->Install plugin from disk的步骤找到并安装插件
(5)Maven默认也不支持Scala的开发,需要引入Scala框架,在项目上右击,按照Add Framework Support->选择 Scala->点击 OK步骤引入Scala框架
object Hello {
def main(args: Array[String]): Unit = {
println("HelloScala")
System.out.println("hello scala from java")
}
}
object:关键字,声明一个单例对象(或者伴生对象)跟同名的类形成伴生关系;
Scala中无Static关键字,由Object实现类似静态方法的功能。
class:关键字,声音一个伴生类,伴生类和伴生对象的私有属性是可以共有的。
def:关键字,声明方法,形式如:
def 方法名(参数名:参数类型):返回值类型 = { 方法体 }
Unit:空返回值类型,Java中空返回类型用Void,Scala中空返回有Unit。
[String] 表示泛型,字符串类型,Scala用中括号表示泛型,Java中用尖括号<>表示泛型。
Scala 中不用添加分号作为每行的结束,当然添加了也是不会报错,建议按照规范不必要添加。
scala 中可以直接调用Java中的类库,当然使用类库需要先导包,system.out.println() 这种常用的系统默认预先导入包了。
Java的编译命令:
javac HelloScala.java
Java的运行命令:
java HelloScala
Scala的编译命令:
scalac HelloScala.scala
Scala的运行命令:
scala HelloScala
Scala 注释使用和 Java 完全一样。
(1)单行注释://
(2)多行注释:/* */
(3)文档注释:
/***
*
*/
scala中有变量和常量之分,变量的值可以被修改,常量的值不能被修改;变量使用var 定义,常量使用val 定义。
java中变量和常量的定义如下,java中使用final关键字,来定义常量:
int a = 10
final int b = 20
scala中变量和常量的定义格式如下:
var 变量名[:变量类型] = 初始值
val 常量名[:常量类型] = 初始值
var i:Int = 10
val j:Int = 20
关于var 和val 定义的一些规则如下:
(1)声明变量时,类型可以省略,编译器会自动推导,即类型推导
var a = 10 //系统自动推导为Int类型
var name = "alice" //系统自动推导为String类型
(2)类型确定后,就不能修改,说明 Scala 是强数据类型语言
var a:Int = 123 //Int类型不能修改为String类型,或者String的值不能赋给Int类型的变量
a = "123" // error
(3)变量声明时,必须要有初始值
var a1: Int // error 必须要有初始值,这两种方式都是错误的
var a2 // error
(4)在声明/定义一个变量时,可以使用 var 或者 val 来修饰,var 修饰的变量可变,val 修饰的变量不可改。
var a: Int = 15
val b: Int = 25
a = 18
b = 20 // error,val 修饰的值不能改变
(5)var 修饰对象时,对象和属性均可变;val 修饰对象时,对象不可变,对象里面的属性可变
// var 修饰的常规变量可以修改,修饰的引用变量也是可以修改的,如下:
var alice = new Student(name="alice",age=20)
alice = new Student(name="Alice",age=18)
alice = NULL
// val 修饰的引用变量不能修改,但是类里面的属性可以修改,同时必须在创建类时在属性上面有定义,如:
Student(name:String,var age:Int)
val bob = new Student(name="Bob",age=13)
// bob = new Student(name="bob",age=20) // error,对象不能重新赋值,不能修改
bob.age = 14 // 类里面的属性可以修改,下面打印将会打印出年龄为14
bob.printInfo()
Scala的命名跟java的命名规则一样,以字母或者下划线开头,后接字母、数字、下划线。有以下两个特例:
(1)以操作符开头,且只包含操作符(+ - * / # !)等为合法命名,如下命名为合法:
var -+/#:String = "hello"
(2)用反引号
包括的任意字符串,即使是 Scala 关键字(39 个)也属合法
var `for` = 123
var `def` = "123"
Java基本类型:
char、byte、short、int、long、float、double、boolean
Java引用类型:(对象类型)
由于Java有基本类型,而且基本类型不是真正意义的对象,即使后面产生了基本类型的包装类,但是仍然存在基本数据类型,所以Java语言并不是真正意思的面向对象。
Java基本类型的包装类:
Character、Byte、Short、Integer、Long、Float、Double、Boolean
注意:Java中基本类型和引用类型没有共同的祖先类。但是scala是有的。
1、Scala数据类型
Scala的所有类型的祖先类是Any,下面分2大类,分别是AnyVal类(数值类型)和AnyRef类(引用类型),并且不管是数值类型还是引用类型都是对象。
Nothing: 是所有数据类型的子类,主要用在一个函数没有明确返回值时使用,因为这样我们可以把抛出的返回值,返回给任何的变量或者函数。
需要注意的是:
(1)Scala中所有类型,包括数据都是对象,且都是Any的子类
(2)Scala数据类型仍然遵守自动转换,低精度的值类型向高精度值类型转换时会自动转换(隐式转换)
(3)Null 是所有引用类型的子类
(4)Nothing 是所有数据类型的子类
2、整数类型
整数类型(Byte[1]、Short[2]、Int[4]、Long[8]),Scala默认的整数类型为Int型,声明其他类型需要加上类型定义,且长整型需要在后面加L或者l;
val a3 = 10 // int型
val a4:Long = 32333333333L // 长整型定义
val a5:Byte = (10+20) // byte型
3、浮点类型
浮点类型(Float,Double),scala中默认是Double类型,声明Float类型,需要在后面添加f或F。
val a6 = 3.6 //Double型
val a7:Float = 3.8f //Float型
4、字符类型
用单引号‘’扣起来的表示字符。
5、布尔类型
(1)布尔类型也叫Boolean 类型,Booolean 类型数据只允许取值 true 和 false
(2)boolean 类型占 1 个字节
6、Unit类型,Null类型和Nothing类型
Unit:表示无值,和Java语言中 void 等同。用作不返回任何结果的方法的结果的返回值类型。Unit 只有一个实例值,写成(),可以查看里面的toString()方法。
Null:null , Null 类型只有一个实例值 null,null可以赋值给任意的引用类型AnyRef,不能赋值给数值类型AnyAvl
Nothing:Nothing 类型在 Scala 的类层级最低端;它是任何其他类型(AnyRef、AnyAvl)的子类型。当一个函数,我们确定没有正常的返回值时(也就是发生异常情况时),可以用 Nothing 来指定返回类型,这样有一个好处,就是我们可以把返回的值(异常)赋给其它所有的函数或者变量(兼容性)
当 Scala 程序在进行赋值或者运算时,精度小的类型自动转换为精度大的数值类型,这个就是自动类型转换(隐式转换)。按照顺序可以有以下顺序:
Byte => Short => Int => Long => Float => Double
一般自动转换的规则如下:
(1)自动提升原则:有多种类型的数据混合运算时,系统首先自动将所有数据转换成精度大的那种数据类型,然后再进行计算。
(2)把精度大的数值类型赋值给精度小的数值类型时,就会报错,反之就会进行自动类型转换。
(3)byte,short和 char 之间不会相互自动转换,char在自动转换时,会自动转换成int类型
(4)byte,short,char 他们三者可以计算,在计算时首先会转换为 int 类型,然后再计算
精度高的想转成精度低的就需要用到强制转换,但要注意会造成精度降低或者溢出问题,且强转只针对于最近的操作数有效,常常会使用括号提升优先级。
val a1:Int = 10*3.5.toInt // 结果为30,先将3.5转为Int型的3
val a2:Int = (10*3.5).toInt // 结果为35,将整体结果35.0转为35
var b1:Byte = 1
// 此处编译报错,因为b1+1时,系统默认会将b1转化成int类型,再相加;
// 但加完需要再赋给b1的时候,变成了int类型赋给Byte类型,因为是精度大的类型赋给精度小的类型,所以报错
// 此处需要加括号强转 b1 = (b1+1).toByte
b1 = b1+1
// 此处不会报错,因为直接将b1转成了Int类型
b1 += 1
(1)基本类型转 String 类型(语法:将基本类型的值+“” 即可)
var num: Int = 123
var strNum1 = num.toString // 或者
var strNum2 = num+""
(2)String 类型转基本数值类型(语法:s1.toInt、s1.toFloat、s1.toDouble、s1.toByte、s1.toLong、s1.toShort)但要注意非数值的字符串转成数值会报错。
var str1 = "123"
val str2 = "a123"
val num1 = str1.toInt
val num2 = str2.toInt //报错
输出:
(1)字符串连接,通过+号连接,* 重复
val name: String = alice
val age: Int = 20
println(age + "年龄" + name + "姓名")
println(name * 3) // 多次打印字符串
(2)printf 用法:字符串,通过%占位符传值,并跟上对应的类型
println("%d年龄 %s姓名",age,name)
(3)双引号前面加上s""为字符串模板(插值字符串),通过 获取变量值,后面的 {}获取变量值,后面的 获取变量值,后面的{}即可对应相应的值
println(s"${age}年龄 ${name}姓名")
(4)双引号前面加上f"“为格式化输出,%2.2f中%号为占位,小数点前面的2为前面保留2位,小数点后面为后面保留2位;双引号前面加上raw”"为原格式输出,不管后面带的是什么
val num:Double = 2.5365
println(f"the number is ${num}%2.2f") //格式化模板字符串
# the number is 2.54
println(raw"the number is ${num}%2.2f") //raw 原样输出
# the number is 2.5365%2.2f
(5)多行字符串,在Scala中可以使用三个双引号包围多行字符串实现,应用 scala 的 stripMargin 方法,并使用“|”作为连接符,可以保留多行字符串的格式,如需加上引用,可以在加上s即可。
val sql = """
|select
| name,
| age
|from user
|where name="zhangsan" """.stripMargin
println(sql)
// 加上引用时
val sql1 = s"""
|select
| name,
| age
|from user
|where name="$name" and age=${age+2} """.stripMargin
println(sql1)
标准输入:
基本的输入有StdIn.readLine()、StdIn.readShort()、StdIn.readDouble() 。
// 1 输入姓名
println("input name:")
var name = StdIn.readLine()
// 2 输入年龄
println("input age:")
var age = StdIn.readShort()
// 3 输入薪水
println("input sal:")
var sal = StdIn.readDouble()
文件读取,并在控制台输出:
Source.fromFile("D:/Tootls/abc.txt").foreach(print)
也可以直接调用Java的接口实现文件读取的功能:
val writer = new PrintWriter(new File(pathname="D:/Tootls/abc.txt"))
weiter.write("hello scala from java writer")
1、基本语法
(1)对于除号“/”,它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分。需要保留小数可以在前面将数值转换成Double类型,或者数字后面加小数点。
(2)对一个数取模 a%b,和 Java 的取模规则一样。
运算符 | 运算 | 范例 | 结果 |
---|---|---|---|
+ | 正号 | +3 | 3 |
- | 负号 | b=4; -b | -4 |
+ | 加 | 5+5 | 10 |
- | 减 | 6-4 | 2 |
* | 乘 | 3*4 | 12 |
/ | 除 | 5/5 | 1 |
% | 取模(取余) | 7%5 | 2 |
+ | 字符串相加 | “He”+”llo” | “Hello” |
2、关系运算法
运算符 | 运算 | 范例 | 结果 |
---|---|---|---|
== | 相等于 | 4==3 | false |
!= | 不等于 | 4!=3 | true |
< | 小于 | 4<3 | false |
> | 大于 | 4>3 | true |
<= | 小于等于 | 4<=3 | false |
>= | 大于等于 | 4>=3 | true |
Java 和Scala 中关于“==”的区别:
Java:
== 比较两个变量本身的值,即两个对象在内存中的首地址;
equals 比较字符串中所包含的内容是否相同。
Scala:
== 更加类似于 Java 中的 equals,参照 jd 工具
eq 而比较两个对象之间在内存的地址使用的是eq函数 str1.eq(str2)
3、逻辑运算符
其中&&和||是二元运算符,也就是需要前面后面两个参数,!为单元运算符,只需要后面接一个参数
(A && B)和(A || B) 也是惰性匹配,当A为false时,&&不会再判断后面条件,因为一个为false结果只能为false,所以后面B不会执行。
当A为true时,|| 同样不会再判断后面的B条件,因为只要有一个为true结果只能为true,这种不会再执行后面条件成为惰性匹配。
-- 判断字符串是否为空
isNotEmpty(String s){
//如果按位与,s 为空,会发生空指针
return s!=null && !"".equals(s.trim());
}
4、scala运算符的本质
在 Scala 中其实是没有运算符的,所有运算符都是方法。前面讲过可以使用运算法作为方法的命名,常用的运算符已经给scala命名为方法了。
方法有以下的省略规则:
(1)当调用对象的方法时,点.可以省略
(2)如果函数参数只有一个,或者没有参数,()可以省略
// 标准的加法运算
val i:Int = 1.+(1)
// 当调用对象的方法时,.可以省略
val j:Int = 1 + (1)
// 如果函数参数只有一个,或者没有参数,()可以省略,这样就跟平时的运算一样的,也可以跟java或其他语言兼容
val k:Int = 1 + 1
5、赋值运算符和位运算符跟Java基本一致。
(1)单分支
if (表达式) {
执行代码块
}
(2)双分支
if (表达式) {
执行代码块1
} else {
执行代码块2
}
(3)多分支
if (表达式) {
执行代码块1
} else if (表达式2){
执行代码块2
} else {
执行代码块3
}
(4)跟Java或其他语言不同的是,scala的if-else是有返回值的,具体返回值取决于满足条件的代码体的最后一行内容。
(4.1)在if-else的多分支里面,每个分支的返回值类型一样的话,可以写成下面形式:
// 返回值参数为res,类型为String
val res: String = if (age < 18) {
return "18"
} else {
return "19"
}
(4.2)Scala 中返回值类型不一致时,取它们共同的祖先类型,这里返回两个类型Int和String,但整体的返回为Unit,即为空()
val res: Unit = if (age < 18) {
return 18
} else {
return "19"
}
(5)Java中的三元运算符可以使用if-else来实现,scala中如果大括号{}内的逻辑代码只有一行,大括号可以省略。如果省略大括号,if 只对最近的一行逻辑代码起作用。
//java中:
int result = flag ? 1 : 0
scala中省略大括号后,用if-else实现:
println("input age:")
var age = StdIn.readInt()
val res: String = if (age > 18) "成年" else "童年"
println(res)
基本语法如下:
for(i <- 1 to 3) // 将1到3的值依次赋给i
(1)i 表示循环的变量,<- to 都是规定的格式
(2)i 将会从 1-3 循环,前后闭合
(3)当想后面不闭合时,使用range()或者使用until
(1) 范围遍历
for ( i <- 1 to 10) {} // 1-10 包含10
for ( i <- range(1,10) {} // 1-10 不包含10
for ( i <- 1 until 10) {} // 1-10 不包含10
(2) 集合遍历,可以直接使用集合,数组,列表等
for ( i <- Array(2, 5, 8)){}
for ( i <- List(2, 5, 8)){}
for ( i <- Set(2, 5, 8)){}
(3) 循环守卫,当不需要循环里面的某个值时,使用循环守卫,相当于Java中的continue
for ( i <- 1 to 10 if i != 5){} // 1-10 包含10,不要5
(4) 循环步长,默认的步长都是1,当需要其他步长时,在后面的by关键字后面添加
for ( i <- 1 to 10 by 2){} // 取 1,3,5,7,9,步长间隔为2
(5) 反转,当需要取值从大到小时,需将大的数放在前面,并且步长设置为-1,或者直接在后面添加反转的关键字,reverse
for ( i <- 10 to 1 by -1){} // 从10到1
for ( i <- 1 to 10 reverse){} // 从10到1
(6) 小数,当取的步长不为整数,为小数时,因为后面的步长是跟前面数值的类型相关的,所以需将前面的类型转成Double类型,才能设置小数步长
for ( i <- 1.0 to 10.0 by 0.5){}
(7) 循环嵌套,类似于java当中的嵌套
for ( i <- 1 to 3){
for (j <- 1 to 3){}
}
// scala当中的循环嵌套也可以写在同一行,用分号隔开,两个处在相同的地位
for (i <- 1 to 4; j <- 1 to 5) {} //输出4*5的矩阵
(8) 引入变量,引入另一个变量j,j跟i是有相关的关系的
for ( i <- 1 to 10; j = i * 2){}
(9) 循环返回值
默认的返回值都是unit,当需要返回值的时候,需要加关键字 yield
// 返回的是一个Vector向量,可以直接对集合或者列表里面的值做操作。
val b: immutable.IndexedSeq[Int] = for ( i <- 1 to 10) yield i * 2
while基本语法
循环变量初始化
while (循环条件) {
循环体(语句)
循环变量迭代
}
while说明:
(1)循环条件是返回一个布尔值的表达式
(2)while 循环是先判断再执行语句
(3)与 for 语句不同,while 语句没有返回值,即整个 while 语句的结果是Unit 类型()
(4)因为 while 中没有返回值,所以当要用该语句来计算并返回结果时,就不可避免的使用变量,而变量需要声明在 while 循环的外部,那么就等同于循环的内部对外部的变量造成了影响,所以不推荐使用,而是推荐使用 for 循环。
do.while基本语法:
循环变量初始化;
do{
循环体(语句)
循环变量迭代
} while(循环条件)
do.while说明:
(1)循环条件是返回一个布尔值的表达式
(2)do…while 循环是先执行,再判断
scala内置结构去掉了 break 和 continue 关键字,是为了更好的适应函数式编程,推荐使用函数式的风格解决 break 和 continue 的功能,而不是一个关键字。Scala 中使用 breakable 控制结构来实现 break 和 continue 功能。break功能也就是相当于程序异常退出,却没有任何的异常信息,这里可以使用抛出异常,但不捕获异常的方式实现退出功能。
def main(args: Array[String]): Unit = {
try {
for (elem <- 1 to 10) {
println(elem)
if (elem == 5) throw new RuntimeException // 随便捕获异常
}
}catch {
case e => // 捕获异常后,不做任何处理,可以实现break的功能
}
println("正常结束循环")
}
使用scala自带的函数,来实现退出功能,将需要break的代码块包裹在Breaks.breakable()里面,需要时,调用Breaks.break()方法。
import scala.util.control.Breaks
def main(args: Array[String]): Unit = {
Breaks.breakable {
for (elem <- 1 to 10) {
println(elem)
if (elem == 5) Breaks.break()
}
}
println("正常结束循环")
}
导入包后,其实包名可以省略,当方法的参数为空时,括号也是可以省略,从而简化成下面的格式:
import scala.util.control.Breaks._
object TestBreak {
def main(args: Array[String]): Unit = {
breakable {
for (elem <- 1 to 10) {
println(elem)
if (elem == 5) break
}
}
println("正常结束循环")
}
}
(1)面向对象编程
解决问题,分解对象,行为,属性,然后通过对象的关系以及行为的调用来解决问题。对象的本质:就是对数据和行为的一个封装。
Scala 语言是一个完全面向对象编程语言,万物皆对象。
(2)函数式编程
解决问题时,将问题分解成一个一个的步骤,将每个步骤进行封装(函数),通过调用这些封装好的步骤,解决问题。函数的本质:函数可以当做一个值进行传递。
Scala 语言是一个完全函数式编程语言,万物皆函数。
Scala就是将面向对象和函数编程完美融合在一起了。
基本语法如下所示:
def sum( x: Int ,y: Int ): Int = {
x + y
}
其中def是定义函数的关键字,sum 是函数名,x,y 是函数的参数名,Int 是参数的类型,第三个Int是函数的返回值类型,x+y是函数体的具体实现。
函数和方法的区别:
(1)为完成某一功能的程序语句的集合,称之为函数,在类中的函数称之方法。
(2)Scala 语言可以在任何的语法结构中声明任何的语法,也就是与位置没有关系,但要注意语法的生命周期
(3)函数没有重载和重写的概念;方法可以进行重载和重写,Object(类)下面的方法可以重载,def 里面的函数不能重载,也就是不能重名
(4)Scala 中函数可以嵌套定义
(1)函数 1:无参,无返回值
(2)函数 2:无参,有返回值
(3)函数 3:有参,无返回值
(4)函数 4:有参,有返回值
(5)函数 5:多参,无返回值
(6)函数 6:多参,有返回值
//(1)函数 1:无参,无返回值
def f1():Unit = {}
//(2)函数 2:无参,有返回值
def f2(): String = { return "name" }
//(3)函数 3:有参,无返回值
def f3(age: Int): Unit = {}
//(4)函数 4:有参,有返回值
def f4(age: Int): Int = { return age }
//(5)函数 5:多参,无返回值
def f5(name:String, age:Int): Unit = {}
//(6)函数 6:多参,有返回值
def f6(name:String, age:Int): Int = { return age}
(1)可变参数(可以在使用前不确定参数的个数,定义可变参数,可变参数将以数组的形式传入)
(2)如果参数列表中存在多个参数,那么可变参数一般放置在最后
(3)参数默认值,一般将有默认值的参数放置在参数列表的后面
(4)带名参数
//(1)可变参数,可变参数在类型后面添加*号
def f1(s: Strring*): Unit = {}
//(2)如果参数列表中存在多个参数,那么可变参数一般放置在最后,不放置在最后无法给后面的参数赋值
def f2(name:String, s:String*): Unit = {}
//(3)参数默认值,一般将有默认值的参数放置在参数列表的后面,如果调用的时候传递了age的值,就会覆盖默认值
def f3(name:String, age:Int = 18): Unit = {}
//(4)带名参数
def f4(name:String, age:Int = 18): Unit = {}
// 带名参数调用是可以不按照顺序调用,一般调用:
f4("bob",20)
// 带名参数调用:
f4(age = 20,name = "bob")
// 或者
f4(name = "bob")
函数至简原则,就是四个字,能省则省。
(1)return 可以省略,Scala 会使用函数体的最后一行代码作为返回值,当不是最后一行的值需要返回时,return不能省
(2)如果函数体只有一行代码,可以省略花括号
(3)返回值类型如果能够推断出来,那么可以省略(:冒号和返回值类型一起省略)
(4)如果有 return,则不能省略返回值类型,必须指定
(5)如果函数明确声明unit,那么即使函数体中使用 return 关键字也不起作用
(6)Scala 如果期望是无返回值类型,可以省略等号
(7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
(8)如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
(9)如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略
//函数标准写法
def f( s : String ) : String = {
return s + " hello"
}
//(1)return可以省略,最后一行作为返回值
def f( s : String ) : String = {
s + " hello"
}
// (2)如果函数体只有一行代码,可以省略花括号
def f( s : String ) : String = s + " hello"
// (3)返回值类型如果能够推断出来,那么可以省略(:冒号和返回值类型一起省略)
def f( s : String ) = s + " hello"
//(4)如果有 return,则不能省略返回值类型,必须指定
def f( s : String ) : String = {
return s
}
//(5)如果函数明确声明unit,那么即使函数体中使用 return 关键字也不起作用
def f( s : String ) : Unit = {
return s //此处的return毫无意义,编译器也会提醒,因为不会返回
}
//(6)Scala 如果期望是无返回值类型,可以省略等号,将无返回值的函数称之为过程,可以跟其他语言做兼容
def f() {
println("hello") //无参数,无返回值类型
}
//(7)如果函数无参,但是声明了参数列表(就是函数名后面加了小括号()),那么调用时,小括号可加可不加
def f() = "hello" // 函数只有一行,省略花括号
println(f()) //两种都可以调用该函数,一个带()
println(f) //两种都可以调用该函数,一个不带()
//(8)如果函数没有参数列表(就是函数名后面不加小括号()),那么小括号可以省略,定义时没有小括号,则调用时也不能带小括号
def f8 = "hello"
//println(f8()) // 该调用会报错
println(f8) // 正确的调用方式
//(9)如果不关心名称,只关心逻辑处理,那么函数名def 和函数名称都可以省略
def f9(name:String): Unit = {
println(name)
}
// 可以写成下面这样:也就是匿名函数,相当于lambda表达式
// 省略了函数定义,函数名,返回值类型
(name:String) => { println(name) }
Scala的函数相对其他语言,有如下几个高阶用法,这也是Scala语言灵活的重要部分。
(1)函数可以作为值进行传递
(2)函数可以作为参数进行传递
(3)函数可以作为返回值进行传递
当函数作为值进行传递的时候,或者作为返回值返回的时候,其实传递的就是该函数的引用地址,其实就是将函数的引用地址进行赋值,从而可以将函数进行传递。另外函数式编程有个特点就是函数中有自变量和因变量,简单理解就是x到y的过程,这个过程用符号来表示可以表示为 f(x) = y,而Scala的匿名函数或者说Scala的函数类型定义就是跟这个类似,只是将符号换成了 => 。例如上面的匿名函数(name:String) => { println(name) },符号左边是输入,右边就是输出,跟函数的定义很类似,而Scala简化的原则就是将编程尽可能的写成函数的形式。
//(1)函数可以作为值进行传递
def f1(n:Int):Int = { // 有参函数
println("f1被调用")
n + 1
}
def f2():Int = { // 无参函数
println("f2被调用")
1
}
// 正常的调用
println(f1(5))
println(f2())
// 函数作为值进行传递
val f3 = f1 _
val f4:Int=>Int = f1
val f5 = f2 _
val f6:()=>Int = f2
println(f3) // 直接打印函数的引用地址
println(f3(6)) // 传参,调用函数f3,也就是调用f1
println(f6)
println(f6())
函数作为值进行传递,函数名接上小括号就是函数的调用,而函数接上**空格下划线 _**代表的就是函数本身,也就是函数的引用地址。当我们想直接使用函数的名称表示函数本身时,需要在定义函数时,加上函数的类型,也就是Int=>Int,表示参数为Int类型,返回值也为Int类型的参数。并且当参数为空时,小括号不能省略,也就是()=>Int。
//(2)函数可以作为参数进行传递
// 函数f里面的参数为:fun: (Int,Int) => Int
// 其中fun是参数名,表示参数是函数类型的意思
// (Int,Int) => Int 为参数类型,表示输入参数为两个Int型,返回值为Int型的函数作为参数
def f1(fun: (Int,Int) => Int):Int = {
fun(1, 2)
}
// 定义一个函数,参数和返回值类型和fun要求的类型一致,两个参数为Int型,返回值也为Int型
def add(a: Int, b: Int): Int = a + b
// 将 add 函数作为参数传递给 f1 函数,因为传递的是f1函数本身,所以需要加空格下划线 _
// 但如果能够推断出来不是函数调用,而是函数传递,空格下划线可以省略
// 此处可以自动推断,因为f1中已经定义了函数参数的类型
println(f1(add _))
println(f1(add))
函数作为参数简单理解就是之前的函数,函数是固定的,也就是处理的流程固定,但数据是可变的。比如计算加法,可以传递1,2或者2,3实现加法效果,加法这个逻辑本身不变。函数作为参数进行传递时,可以看做数据不变,操作是可变的。比如也是计算1和2的加法,数据看成1和2不变,操作可以传递加法,也能传递减法。实际应用中函数和数据都会是可变的状态,这样就更加灵活了。
//(3)函数可以作为函数返回值返回
def f1():Int=>Unit = {
def f2(a: Int): Unit = {
println("f2调用" + a)
}
f2 // 将函数直接返回
}
println(f1()) // 打印的是f2的引用地址,因为调用了f1,里面返回的函数f2
printfln(f1()()) // f1() = f2,所以f1()() = f2(),这才是调用了f2,且f2的返回值为空
其中 Int=>Unit 是返回值的类型,因为返回的是函数f2,所以Int=>Unit就是f2的函数类型,也就是f2的参数为Int型,返回值为空。
由上面的省略原则,我们可以得到匿名函数的基本定义和语法:
没有名字的函数就是匿名函数,其语法定义为:
(x:Int)=>{ 函数体 }
x:表示输入参数名称;Int:表示输入参数类型;函数体:表示具体代码逻辑。并且匿名函数还能再简化,也就是匿名函数的简化原则。
匿名函数简化规则:
(1)参数的类型可以省略,会根据形参进行自动的推导
(2)类型省略之后,发现只有一个参数时,则圆括号可以省略;其他情况:没有参数和参数超过 1 的则不能省略圆括号
(3)匿名函数如果只有一行,则大括号也可以省略
(4)如果参数在调用时只出现一次,则参数省略且后面参数可以用_代替
// 定义函数f1,里面的参数为op操作函数
def f1(op:Int => Int) = {
op(1)
}
// 定义op操作函数
def op(n:Int):Int = {
n + 1
}
// 直接传入op调用f1函数
val result = f1(op)
// 采用不定义op操作函数,直接匿名函数的方式
val result1 = f1((n:Int) => { n+1 })
//(1)参数的类型可以省略,会根据形参进行自动的推导,因为f1中已经规定了op的参数为Int型
val result2 = f1((n) => { n+1 })
//(2)只有一个参数时,则参数圆括号可以省略;其他情况:没有参数和参数超过 1 的则不能省略圆括号。
val result3 = f1(n => { n+1 })
//(3)匿名函数如果只有一行,则大括号也可以省略,就是相当于传入参数的操作,中间用 => 连接
val result4 = f1(n => n+1)
//(4)如果参数在调用时只出现一次,则前面参数省略且后面参数可以用_代替,参数n在调用时只用一次
val result5 = f1(_+1)
//(5)如果可以推断出,当前传入的是一个函数体,而不是调用语句,可以直接省略下划线
示例代码,简单实现1和2的相加和相减:
// 定义a和b相加和相减的匿名函数,并将其返回值赋给addFun和defFun
var addFun = (a: Int, b: Int) => { a + b }
var defFun = (a: Int, b: Int) => { a - b }
// 定义函数f,里面的参数是函数,返回值是Int类型
// 参数的函数类型为:两个Int参数,返回值为Int类型
def f(fun: (Int,Int) => Int):Int = {
fun(1, 2)
}
// 使用匿名函数addFun和defFun作为参数,传递给f,并将f的返回值打印出来
println(f(addFun))
println(f(defFun))
// 其实匿名函数addFun和defFun等价于里面的实现语句,并省略了参数类型和花括号
// 因为f函数里面已经规定了a,b只能会Int类型,所以可以省略参数类型,函数体在一行所以可以省略花括号
println(f((a, b) => a + b ))
println(f((a, b) => a - b ))
// 参数a和参数b只在后面调用的时候调用1次,所以可以用下划线_来代替
println(f(_ + _))
println(f(_ - _))
闭包:
如果一个函数,访问到了它的外部变量或者外部参数,那么这个函数和它所处的环境(也就是外部变量的值)一起,称之为闭包。
有如下函数:
def func(i:Int):String=>(Char=>Boolean) = {
def f1(s:String):Char=>Boolean = {
def f2(c:Char):Boolean = {
if (i == 0 && s == "" and c == '0') false else true
}
f2
}
f1
}
// 正常调用如下:
func(0)("")('0')
// 如果将函数作为值传递,则有如下:
f3 = func(0)
println(f3("")('0')
在将 func(0) 的值赋值给 f3 之后,在调用 f3 的时候,这时候的 func 中的参数 i 在内存中还存在吗?
闭包就是为了解决这个问题,因为func已经运行完了,在栈内存中,i 的值是不存在的,但是系统将func中的参数 i 跟函数 f1 一起打包了,并将其存放在堆内存中,这样才能确保运行f1,f2时能正常用到外部的变量而不会导致出错。这个就叫做闭包。
// 正常多个参数的写法
def add(a:Int,b:Int):Int = {
a + b
}
// addByB 使用局部变量(外部变量)的闭包写法
def addByFour():Int=>Int = {
val a = 4
def addByB(b:Int) => Int = {
a + b
}
addByB
}
// addByB 使用外部参数的闭包写法
def addByA(a:Int):Int=>Int = {
def addByB(b:Int) => Int = {
a + b
}
addByB
}
// 调用
println(addByFour()(5))
println(addByA(4)(5))
柯里化:
把一个参数列表的多个参数,变成多个参数列表的过程,称之为函数柯里化。上面介绍了闭包就是为了引入柯里化,因为柯里化的底层就是用闭包来实现的。
柯里化其实就是为了更符合函数式编程的思想,因为在函数式编程里面,是没有多个参数对应的,例如f(x1,x2) => y 。看中的是单个值的对应关系,例如 f(x1)(x2) => y 的表达式,这里f(x1)返回的另一个函数然后参数是x2,就形成了一对一的嵌套关系。
// 上面使用外部参数的柯里化写法
def addCurrying addByAB(a:Int)(b:Int):Int = {
a + b
}
// 调用
println(addCurrying(4)(5))
柯里化的写法,我们发现简介很多,并且跟调用的写法如出一辙,而柯里化的内部写法其实就是用了闭包的策略,但我们更推荐这种写法,因为这种写法更符合函数式编程的思想,也更容易理解。
递归的含义其实很简单:一个函数/方法在函数/方法体内又调用了本身,我们称之为递归调用。
递归的一些注意事项:
(1) 方法调用自身
(2) 方法必须要有跳出的逻辑
(3) 方法调用自身时,传递的参数应该有规律
(4) scala 中的递归必须声明函数返回值类型
// 用递归实现阶乘
def test(i : Int) : Int = {
if (i == 1) {
1
} else {
i * test(i - 1)
}
}
println(test(5))
在 test 函数的内部,又调用了 test 本身,这样的实现方式就叫做递归。但递归本身有一个缺点,就是每次调用内部 test 的时候,需要先将 i 的值保存下来,这样每递归一次,就要保存外部的变量一次,当递归次数很多时,就会耗费很大的资源进行保存。
尾递归:
正常的递归,内层需要用到外层的参数,导致在执行内层的时候,需要先将外层参数的值保留下来。
尾递归,就是将外层的参数作为内层的参数,传递进去给内层的参数,进而内层函数在调用时,就可以保留函数和参数,从而不需要外层函数的任何信息,从而不管递归多少次,都达到只保留一个栈信息的效果。
@tailrec
def tailFact(n:Int):Int = {
def loop(n:Int, curRes:Int):Int = {
if (n == 0) return curRes
loop(n - 1,curRes * n)
}
loop(n, curRes = 1)
}
在 loop 内部又调用了 loop,递归调用,但是每次调用 loop 时,会将上一次的结果 curRes * n 作为参数在传递给 loop,就能实现每次只保存一次参数的效果,更推荐使用。
上面的 @tailrec 是一个尾递归的注解,也是一个尾递归的判断,当下面写的函数不是尾递归的时候,编译器就会报错,是个很好的检查工具。
(1)传值调用:把值传递过去
// 传值参数
def f0(a:Int):Unit = {
println("a = " + a)
println("a = " + a)
}
f0(23)
def f1():Int = {
println("f1被调用")
12
}
// f1 调用1次之后,将f1函数的值传递给f0,f0输出两次12
f0(f1())
(2)传名调用:把代码块传递过去(不同于上面的函数作为参数传递,这里是代码块,不是函数)
// 传名参数
def f1():Int = {
println("f1被调用")
12
}
def f2(a: =>Int):Unit = {
println("a = " + a)
println("a = " + a)
}
// 打印2次 a = 23
f2(23)
// 打印2次f1 被调用,并打印2次a = 12
f2(f1())
这里相当于将整个f1的代码块,当成参数传入f2中,所以a在f2中出现多少次,也就运行了f1的代码块多少次。=>Int 整个类型表示代码块,不像函数,函数要求无参数的话,小括号不能省略()=>Int,这里只要求返回值为Int型即可,不要求有参数或者其他。
当函数返回值被声明为 lazy 时,函数的执行将被推迟,直到我们首次对此取值,该函数才会执行。这种函数我们称之为惰性函数。
def main(args: Array[String]) = {
lazy val res1 = sum1(10, 30)
val res2 = sum2(15, 20)
println("------------------------------")
println("res1 =" + res1)
println("------------------------------")
println("res2 =" + res2)
}
def sum1(a:Int,b:Int):Int = {
println("sum1 被执行。。。")
a + b
}
def sum2(a:Int,b:Int):Int = {
println("sum2 被执行。。。")
a + b
}
如上,res2在赋值的时候,sum2就已经运行了,而res1需要在res1要使用时,在println(“res1 =” + res1)时,才会去执行sum1的语句。
注意:lazy 不能修饰 var 类型的变量