函数基础
Scala 的函数式编程以及面向对象的特点,使它能够很好的应用到大数据场景下,比如 Spark、Kafka 的底层都是 Scala 编写的。
基本语法
在Java中,方法只能够在类下面声明,不可以在main方法以及其它方法内部声明,但是Scala可以,因为Scala是一个函数式编程,函数是一等公民。
所以,我们通常把main方法内部定义的方法叫做函数,main外部声明的方法才叫做对象的方法。
object Test{ def main(args: Array[String]): Unit = { //定义函数 def sayHello(name: String): Unit = { println("hello " + name) } //调用函数 sayHello("GGBond") //调用对象的方法 Test01_Function.sayHello("Tom") } //定义对象的方法 def sayHello(name: String): Unit = { println("hello " + name) } }
可以看到,我们可以直接通过函数名调用函数,但是在伴生类中,一切类变量和方法都是静态的,所以我们需要通过类名才可以调用方法。
函数参数
- 可变参数
- 如果参数列表中存在多个参数,那么可变参数一般放置在最后
- 参数默认值,一般将有默认值的参数放置在参数列表的后面
- 带名参数
def main(args: Array[String]): Unit = { //1.可变参数 def f1(names: String*): Unit = { println(names) } f1("alice","tom","GG bond") //2.如果参数列表中存在多个参数,那么可变参数一般放置在最后 def f2(grade: String, names: String*): Unit = { println(grade) println(names) } f2("大二","alice","tom","GG bond") //3.参数默认值,一般将有默认值的参数放置在参数列表的后面 def f3(name: String = "GG bond"): Unit = { println(name) } f3() f3("燕双鹰") //4.带名参数 def f4(name: String, age: Int): Unit = { println(s"${name}的年龄=${age}") } f4("GG bond",14) f4(age = 13,name = "熊大") }
函数至简原则
代码能省则省
至简原则细节
- return 可以省略,Scala 会使用函数体的最后一行代码作为返回值
- 如果函数体只有一行代码,可以省略花括号
- 返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
- 如果有 return,则不能省略返回值类型,必须指定
- 如果函数明确声明 unit,那么即使函数体中使用 return 关键字也不起作用
- Scala 如果期望是无返回值类型,可以省略等号
- 如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
- 如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
- 如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略
object Test{ def main(args: Array[String]): Unit = { //(1)return 可以省略,Scala 会使用函数体的最后一行代码作为返回值 def f0(name: String): String = { return name } println(f0("GG bond")) def f1(name: String): String = { name } println(f1("GG bond")) //(2)如果函数体只有一行代码,可以省略花括号 def sum(a: Int,b: Int): Int = a+b //(3)返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略) def add(a: Int,b: Int) = a+b //因为参数都是Int类型,所以可以自己推断返回值类型 //(4)如果有 return,则不能省略返回值类型,必须指定 def f3(name: String): String = { return name } //(6)Scala 如果期望是无返回值类型,可以省略等号 def f4(name: String){ println("hello") } //(7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加 def f5() = "name" println(f5) //(8)如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略 def f6 = "hi" //(9)如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略 def say_Hi(name: String): Unit = { println("hi" + name) } //匿名函数 匿名函数的返回值是一个函数 val say_Hello = (name: String) => {"hello " + name} //调用匿名函数 println(say_Hello("GG bond")) } }
函数高级
高阶函数
高阶函数指的是接受一个或多个函数作为参数,并返回一个函数作为结果的函数。函数是Scala中的一等公民,地位很高,这体现在它可以在main方法中直接定义并通过函数名来直接使用,而不用向Java一样,需要再对象下面声明方法,来通过对象或者类名来调用。
高阶函数可以减少代码的重复,并增加代码的灵活性和可重用性。
通常我们的业务数据在高阶函数中写死,等待传进来的函数参数来作处理。
函数的3种高阶用法:
- 函数作为值传递
- 函数作为参数传递
- 函数作为返回值
1、函数作为值传递
函数作为返回值赋值给一个变量,这个变量类型是一个函数对象。如果直接输出的话,如果这个函数没有参数,将直接输出这个函数的返回值,否则直接输出这个对象的地址(对象引用)。我们也可以通过返回值来直接使用,因为返回值本身就是一个函数对象,我们可以直接+(参数)来调用函数。
def main(args: Array[String]): Unit = { //定义一个普通函数 输入num返回num+1 def f(num: Int): Int = { num + 1 } //1.函数作为值传递 -两种方式返回函数对象 val f1: Int => Int = f val f2 = f _ //我们可以直接输出函数对象,也可以直接利用函数对象进行使用传递参数 println(f1) //输出这个对象的地址(也叫对象引用) println(f1(9)) println(f2) //输出这个对象的地址(也叫对象引用) println(f2(10)) //定义一个函数无参,直接返回1 def fun(): Int = { 1 } val f3 = fun //因为fun是无参的,所以这里返回的是返回值 1 val f4 = fun _ //返回函数体 val f5: () => Int = fun println(f3) //输出 1 println(f4) //输出这个对象的地址(也叫对象引用) println(f5) //输出这个对象的地址(也叫对象引用) }
2、函数作为参数传递
我们可以定义两个函数,分别实现两个数的加和减,然后我们可以将这两个函数作为参数传递给另一个函数,来实现对两个数的处理(数据是写死的)。
//定义一个函数 sum 求和 def sum(a: Int,b: Int): Unit = { println(s"${a} + ${b} = ${a+b}") } //定义一个函数 sub 求差,函数体只有一行可以省去花括号{} def sub(a: Int,b: Int): Unit = println(s"${a} - ${b} = ${-b}") def fun(f: (Int,Int) => Unit):Unit = { f(1,2) } fun(sum) //输出 1 + 2 = 3 fun(sub) //输出 1 - 2 = -1
前面我们数据是写死的,也就是说,我们的高阶函数只能够对 1和2进行处理,显然很没用!这里我们直接将要计算的数据页当做参数传递进来。
def doEval(f: (Int,Int) => Int,a: Int ,b: Int): Int = { f(a,b) } def add(a: Int,b: Int): Int = { a + b } //使用普通函数作为参数 doEval(add,1,2) //使用匿名函数做参数 doEval((a,b) => a + b,1,2) //简化代码 doEval(_ + _,1,2)
匿名函数作参数传递
我们可以直接将匿名函数作参数传入fun,更加简化代码。
//以函数作为参数 def fun(f: (Int,Int) => Unit): Unit = f(1,2) //直接传入匿名函数 fun((a: Int,b: Int) => println(s"${a} + ${b} = ${a+b}"))) //输出 1 + 2 = 3 //高阶函数定义了函数参数的类型,匿名函数这里就不需要再指定函数类型了 fun((a,b) => println(s"${a} - ${b} = ${a-b}"))) //输出 1 - 2 = -1
3、函数作为返回值传递
函数作为返回值后返回函数对象,我们可以通过函数对象来使用该函数。
//3.函数作为返回值传递 def f6(): Int => Unit = { def f7(a: Int): Unit = { println("f7被调用 " + a) } f7 //返回函数对象 } //获取f7函数对象 并使用 val res = f6() //输出: // f7被调用 10 // () // 因为f7的返回值为 Unit类型 println(res(10))
匿名函数的至简规则
- 参数的类型可以省略,会根据形参进行自动的推导
- 类型省略之后,发现只有一个参数,则圆括号可以省略;其他情况:没有参数和参 数超过 1 的永远不能省略圆括号。
- 匿名函数如果只有一行,则大括号也可以省略
- 如果参数只出现一次,则参数省略且后面参数可以用下划线 _ 代替
//定义一个函数,以函数作参数 def say_Name(f: String => Unit): Unit = f("GG Bond") //普通调用 say_Name((name: String) => {println(name)}) //1.参数的类型可以省略,会根据形参进行自动的推导 say_Name( (name) => {println(name)}) //2.如果参数只有一个 括号省略 say_Name( name => {println(name)} ) //3.匿名函数如果只有一行,则大括号可以省略 say_Name( name => println(name) ) //4.如果参数只出现一次,则参数省略且后面参数可以用_代替 say_Name( println(_) ) //5.如果可以推断出当前传入的println()是一个函数体,而不是调用语句,可以直接省略_ say_Name( println )
高阶函数的应用
高阶函数对集合的处理更加体现了Sala对于处理大数据的优势。
案例-将数组中的元素全部+1后返回
val arr: Array[Int] = Array(1,2,4,3,8) //对数组进行处理,把对数组的操作进行抽象 /** * * @param array 待处理的数组 * @param op 操作函数(Int => Int)将数组中的数处理后变为另一个数 * @return 返回新数组 */ def arrayOperation(array: Array[Int], op: Int => Int): Array[Int] = { for (elem <- array) yield op(elem) } //定义操作函数 def addOne(elem: Int): Int = elem + 1 //调用函数 val res_arr: Array[Int] = arrayOperation(arr,addOne) //用逗号间隔输出新数组的内容 println(res_arr.mkString(",")) //使用匿名函数实现 val res2_arr = arrayOperation(arr,elem => elem + 1) //这里的后面的elem可以用下划线代替 _ val res2_arr = arrayOperation(arr,elem => _ + 1) println(res2_arr.mkString(","))
匿名函数
前面已经用过了,这里在详细解释一下。
说明:没有函数名的函数就是匿名函数。
表达式:(a: Int,b: Int)=>{函数体}
注意:匿名函数的定义没有返回值类型,但是可以作为高阶函数的参数时指定。
//以函数作为参数 def fun(f: (Int,Int) => Int): Unit = f(1,2) //直接传入匿名函数 fun((a: Int,b: Int) => a + b )) //返回 3 //高阶函数中指定了参数函数的参数类似,所以这里可以省去匿名函数的参数类型 fun((a,b) => a - b )) //返回-1 //如果每个参数只出现一次,可以这样写 fun( _ + _) //对应 a + b fun( _ - _) //对应 a - b //如果要实现 b-a 可以写作 -a+b fun((a,b) => -a + b) fun( -_ + _) //对应 -a + b
练习1-匿名函数作为值传递:
定义一个匿名函数,并将它作为值赋给变量 fun。函数有三个参数,类型分别为 Int,String,Char,返回值类型为 Boolean。
要求:调用函数 fun(0, “”, ‘0’)得到返回值为 false,其它情况均返回 true。
//匿名函数在没被高阶函数指定返回值类型的时候,需要声明参数名和类型 val fun = (a: Int,b: String,c: Char) => { if (a == 0 && b == "" && c == '0') false else true } println(fun(1,"",'0')) //true
练习2-函数作为返回值
定义一个函数 func,它接收一个 Int 类型的参数,返回一个函数(记作 f1)。 它返回的函数 f1,接收一个 String 类型的参数,同样返回一个函数(记作 f2)。函数 f2 接 收一个 Char 类型的参数,返回一个 Boolean 的值。
要求调用函数 func(0) (“”) (‘0’)得到返回值为 false,其它情况均返回 true。
//表示返回一个输入类型为String输出类型为(Char => Boolean)的函数 def func(a: Int): String => (Char => Boolean) = { //表示返回一个输入类型为Char输出类型为Boolean的函数 def f1 (b: String): Char => Boolean = { //表示返回一个返回值类型为Boolean的值 def f2(c: Char): Boolean = { if (a == 0 && b == "" && c == '0') false else true } f2 } f1 } println(func(0)("")('0')) //false
使用匿名函数简写
函数柯里化&闭包
- 闭包:如果一个函数 ,访问到了它外部变量的值,那么这个函数和它所处的环境,称为闭包
- 函数柯里化:把一个参数列表的多个参数,变成多个参数列表。
我们学习闭包和柯里化是为了更加简洁地实现调用上层函数的参数,因为在上面的案例2中,我们通过嵌套函数来实现读取上层函数的参数显然代码很复杂,所以我们可以使用柯里化来实现。
案例2的闭包实现:
//匿名函数简写 def func1(a: Int): String => (Char => Boolean) = { //外层指定了返回值类型 内层的匿名函数就不需要指定参数类型 b => c => if (a == 0 && b == "" && c == '0') false else true } println(func(1)("")('0')) //tue
案例2的柯里化实现:
//柯里化 def func2(a: Int)(b: String)(c: Char): Boolean = { if (a == 0 && b == "" && c == '0') false else true } //调用 println(func2(0)("")('0')) //true
我们可以看到,柯里化更加简单,其实柯里化的底层就是闭包来实现的。
闭包案例 - 常量a+变量b
这并不是一个简单的两数相加,因为实际开发中,我们可能遇到大量的变量对常量不断运算,比如大量数字分别对4相加、对5相加,这就需要定义两个函数addByFour、addByFive;但是如果常量有几百种呢,这就需要定义几百个函数,显然过于复杂。所以这就需要使用双层嵌套函数,以返回值为函数:第一层函数输入常量,第二层函数输入变量。
//A是任意常量 def addByA(a: Int): Int=>Int = { def addB(b: Int): Int = { a + b } addB } //不断简化代码 //既然直接返回函数 我们直接定义一个匿名函数即可 scala默认返回最后一行(一个方法可以看作一行)作为返回值 def addByA1(a: Int): Int=>Int = { (b: Int) => a + b } //指定外层函数指定了返回值类型 内层就不需要写输入类型了 def addByA2(a: Int): Int=>Int = b => a + b //内层的变量 b 只在函数体出现了一次 所以可以用 _ 替换 def addByA3(a: Int): Int=>Int = a + _ //调用 val fun = addByA3(5) //接收一个函数(任意数+5)作为返回值 println(fun(10)) //调用函数 输出15
案例 - 柯里化
在纯粹的函数式编程中其实是不存在多参数这样的定义的,只是Scala为了兼容其它语言才不做限制的。所以引入柯里化来实现这样的一个效果,将一个含多个参数的参数列表转变为多个仅含一个参数的参数列表。
柯里化的底层就是闭包。
//柯里化 底层是闭包实现分层调用 def addCurrying(a: Int)(b: Int): Int = a + b //调用 println(addCurrying(4)(36))
递归
斐波那契数列
def fb(i: Int): Int = { if (i ==1 || i == 2) 1 else fb(i-1) + fb(i+2) }
阶乘
def jieC(n: Int): Int = { if (n==1) 1 else jieC(n-1)*n }
尾递归
我们知道,在做递归的时候,我们其实是方法不断调用,比如上面的阶乘案例,当我们希望得到5的阶乘时,函数的调用会不断压栈,到达阶乘的最小值1的时候原路返回。显然如果是10亿的阶乘,肯定会造成栈溢出的问题。
所以这就需要引入尾递归的概念,上面的普通递归需要不断压栈最后出栈,比如5的阶乘,它的返回值是 1 -> 2*1 -> 3*2*1 -> 4*3*2*1 -> 5*4*3*2*1
而我们的尾递归刚好相反,它的返回值是 5 -> 5*4 -> 5*4*3 -> 5*4*3*2 -> 5*4*3*2*1,这样,在调用函数时,不会创建新的栈帧,而是直接复用当前栈帧,因此占用的栈资源是固定的。
Scala实现尾递归求阶乘
def tailJieC(n: Int): Int = { def loop(n: Int,tmp: Int): Int = { if (n==0) return tmp loop(n - 1,tmp * n) } loop(n,1) }
Java实现尾递归求阶乘
public static int JC(int n){ return loop(n,1); } public static int loop(int n,int tmp){ if (n==0){ return tmp; }else { return loop(n-1,tmp*n); } }
控制抽象
传值调用
参数类型:普通参数
println("=====传值调用======") //1.传值参数 def f0(a: Int): Unit = { println("a = " + a) } //数值做参数 f0(1) def f1(): Int = { println("f1被调用") 2 } //函数返回值做参数 f0(f1())
输出结果
1 2
传名调用
参数类型:传递的不再是具体的值,而是代码块
println("=====传名调用======") def f1(): Int = { println("f1被调用") 2 } //2.传名参数 //f2的参数是代码块 要求代码块的返回值是Int def f2(a: => Int): Unit = { println("a = " + a) println("a = " + a) //a被调用几次 代码块就被执行几次 } f2(23) //输出两个23 f2(f1()) //执行两次f1() 相当于把f2的函数体中的 a 都替换做 f1() 这也说明传名调用传递的是代码块而不是返回值 f2({ println("这是一个代码块") 20 })
输出结果
=====传名调用====== a = 23 a = 23 f1被调用 a = 2 f1被调用 a = 2 这是一个代码块 a = 20 这是一个代码块 a = 20
应用案例 - 自定义函数实现while
//1.常规的while循环 var n = 10 while (n > 0){ println(n) n -= 1 } //2.自定义函数实现while // 用闭包实现一个函数,将代码块作为参数传入,递归调用 def myWhile(condition: =>Boolean): (=>Unit)=>Unit = { //内层实现递归 参数就是循环体-代码块 def doLoop(op: => Unit): Unit= { if (condition){ op myWhile(condition)(op) } } doLoop _ } //给n重新赋值 n = 10 myWhile(n >= 1)({ println(n) n -=1 })
简化代码
//3.用匿名函数简化代码 def myWhile2(condition: =>Boolean): (=>Unit)=>Unit = { //内层实现递归 参数就是循环体-代码块 op => { if (condition){ op myWhile2(condition)(op) } } } //4.用柯里化简化代码 def myWhile3(condition: => Boolean)(op: =>Unit): Unit = { if (condition){ op myWhile2(condition)(op) } }
惰性加载
懒加载(Lazy Loading)是一种常见的优化策略,它可以延迟对象的初始化时间,减少应用程序的启动时间和内存消耗。在懒加载的策略下,只有在需要访问具体的对象时才会进行初始化。在Scala中,我们使用关键字 'lazy' 来实现懒加载。
lazy val res: Int = compute() def compute(): Int = { // 进行复杂的计算 Thread.sleep(1000) //返回计算结果 100 }