scala 系列
scala 函数
前言
前几篇博客已经给大家介绍了 scala 入门基础和数组集合的,相信大家都已经对 scala 有了更进一步的了解。本篇博客将为大家带来 scala 方法和函数的介绍。
本篇博客将为大家带来 scala 方法和函数的介绍。
思维导图
Java Lambda表达式
scala 中经常会涉及函数式接口以及后续 Spark/Flink 的大量业务代码都会使用到函数式编程都建立在函数式接口之上,而函数式接口是一种只含有一个抽象方法声明的接口,也可以是使用匿名内部类来实例化函数式接口的对象等。这些都可以通过通过 Java Lambda表达式可以进一步简化代码。
Java Lambda表达式的本质只是一个"语法糖",由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能。
Java Lambda表达式是 jdk1.8 之后提供的一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。
Java Lambda表达式的语法
(parameters) -> expression
或者
(parameters) ->{ statements; }
Java Lambda表达式的实例
- Java Lambda表达式可以将我们的代码缩减到一行,具体如下:
public static void main(String[] args) { List<String> subjects = Arrays.asList("hadoop","scala","spark","flink","hive","hbase","sqoop"); // 以前的循环方式 for (String subject : subjects) { System.out.print(subject + "\t"); } // 使用 lambda 表达式 subjects.forEach((subject) -> System.out.print(subject + "\t")); // 在 Java 8 中使用双冒号操作符 subjects.forEach(System.out::println); }
- 匿名类可以使用lambda表达式来代替,如下所示:
// 使用匿名内部类 new Thread(new Runnable() { @Override public void run() { System.out.println("Hello world !"); } }).start(); // 使用 lambda expression new Thread(() -> System.out.println("Hello scala!")).start();
Java8 四大内置函数式接口(了解)
JDK 1.8 API 中包含了很多内置的函数式接口。有些是在以前版本的 Java 中大家耳熟能详的,例如 Comparator 接口,或者 Runnable 接口。对这些现成的接口进行实现,可以通过@FunctionalInterface
标注来启用 Java Lambda 功能支持。
1. 功能性接口 Function
Function 接收一个功能参数t,并返回一个功能结果R。默认方法可以将多个函数串在一起(compse, andThen),例如:
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
2. 断言性接口 Predicate
Predicate 主要用到 test 方法 其中参数t为输入参数,如果输入参数符合断言则返回true,否则false。Predicate是一个布尔类型的函数,该函数只有一个输入参数。Predicate接口包含了多种默认方法,用于处理复杂的逻辑动词(and, or,negate),例如:
Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
3. 供给性接口 Supplier
Supplier 不接收任何参数 但有返回值,Supplier接口产生一个给定类型的结果。例如:
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
4. 消费性接口 Consumer
Consumer 只接收一个参数t,但是没有返回值。Consumer代表了在一个输入参数上需要进行的操作。例如:
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
这一部分,只需了解即可。
基本函数
函数定义
函数是Scala的核心,scala 中提供以下四种方式定义函数:
// 法一:调用系统函数Function1-Function22函数,普通创建
al fun1 = new Function2[Int,Int,Int] {
override def apply(v1: Int, v2: Int): Int = {
v1 + v2
}
}
// 法二:调用系统函数Function1-Function22函数,使用java lambda表达式创建
val fun2 = new ((Int,Int)=>Int)(){
override def apply(v1: Int, v2: Int): Int = {
v1 + v2
}
}
// 法三:直接使用java lambda表达式创建自定义函数(最常用)
val fun3 = (v1:Int,v2:Int) => v1 + v2
// 法四:直接使用java lambda表达式创建自定义函数,并将函数以值的形式传递给函数名
def fun4Method(v1:Int,v2:Int): Int = {
v1 + v2
}
val fun4 = fun4Method _
上面说过java lambda表达式可以优化我们的代码,并且实际开发中经常使用自定义的函数,因此方法三这种创建方式是最常用的方式,而方法四 一般情况下都是对对象进行操作的,又称方法。下面主要讲一下方法三和方法四的创建语法。
函数定义语法一:
var 函数变量名 = (参数名:测试类型,参数名:参数类型,...) => 函数体
注意:
- 函数是一个对象(变量),源于 scala 一切皆值
- 类似于方法,函数也有输入函数和返回值,和方法一样返回值可以不写return
- 函数定义不需要使用 def 定义
- 无需指定返回值类型
- 返回值为 Unit 的函数也称过程
函数定义语法二(多数用来定义方法):
def methodName (paramName:paramType,paramName:paramType,...):[returnType] = {
// 函数体
}
备注:
- 如果有参数,参数列表的参数类型不能省略
- 返回值类型可以省略,由scala编译器自动推断;定义递归方法,不能省略返回值类型
- 返回值可以不写return,如果使用return返回r的值,那么需要明确指定函数返回类型,默认就是{}块表达式的值
实例:
// 普通定义
scala> def a(x:Int, y:Int):Int = {
| x+y
| }
a: (x: Int, y: Int)Int
// 返回值类型可以省略
scala> def a(x:Int, y:Int) = {
| x+y
| }
a: (x: Int, y: Int)Int
// 定义递归方法,不能省略返回值类型
scala> def a(x:Int, y:Int) = {
| x+y
| if (x+y > 10)
| a(x,y)
| }
<console>:23: error: recursive method a needs result type
a(x,y)
此外,在scala中,+ - * / %等这些操作符和Java一样,但在scala中,
- 所有的操作符都是方法
- 操作符是一个 方法名字是符号 的方法
函数参数
1. 默认参数:
在定义函数时可以给参数定义一个默认值。调用该方法,可以不传已有默认值的任何参数。
scala> def a(x:Int=0, y:Int=1) = {
| x+y
| }
a: (x: Int, y: Int)Int
scala> a()
res31: Int = 1
2. 带名参数
在调用函数的时候,可以指定参数的名称来进行调用。调用该函数,只设置第一个参数的值。
scala> def a(x:Int=0, y:Int=1) = {
| x+y
| }
a: (x: Int, y: Int)Int
scala> a(x=1)
res32: Int = 2
通常情况下,传入参数与函数定义的参数列表一一对应,命名参数允许使用任意顺序传入参数,如下例所示:
def printName(first:String, last:String) = {
println(first + " " + last)
}
def main(args: Array[String]): Unit = {
printName("John","Smith")
printName(first = "John",last = "Smith")
printName(last = "Smith",first = "John")
}
Scala函数允许指定参数的缺省值,从而允许在调用函数时不指明该参数,如下例所示:
def printName(first:String="John", last:String="Smith") = {
println(first + " " + last)
}
def main(args: Array[String]): Unit = {
printName()
}
3. 可变参数
如果函数的参数是不固定的,可以定义一个函数的参数是可变参数。
def methodName (paramName:paramType*,...):[returnType] = {
// 方法体
}
在参数类型后面加一个 * 号,表示参数可以是0个或者多个,实际上是告诉编译器希望把这个参数当做序列处理。
函数调用
后缀调用法,就是.
函数名称
对象名.函数名(参数)
中置操作符,如果有多个参数,使用括号括起来,常用于 运算符方法重载
对象名 函数名 参数
注意:
和中缀表达式case ::
的区别如下:
# 中缀表达式 case :: 切割集合,最后一个four 放剩余的集合元素,可以为空
List(1,2,3,4,5) match {
case one :: two :: three :: four => println(one,four);
case _=> println("hehe")
}
# 结果
(1,List(4, 5))
花括号调用法,函数只有一个参数,才能使用花括号调用法
对象名.函数名{
参数表达式
}
无括号调用法,如果函数没有参数,可以省略函数名后面的括号
对象名.函数名
实例:
scala> Math.max(0,1)
res33: Int = 1
scala> Math max (0,1)
res34: Int = 1
scala> Math.abs{1}
res40: Int = 1
scala> def a() = {println}
a: ()Unit
scala> a
参数传递
1. 传值调用
```scala
def square(x: Int): Int = {
println(x) //3
x * x //计算3*3
}
square(1+2) //先计算1+2
```
传值调用时,参数只在调用时计算一次,后续重复使用计算的结果。
2. 传名调用
```scala
def square(x: => Int): Int = {
println(x) //计算1+2
x * x //计算(1+2)*(1+2)
}
square(1+2) //调用时不计算
```
传名调用时,参数在调用时不会计算,只有真正用到参数时才计算。
方法和函数的区别
- 方法是隶属于类或者对象的,在运行时,它是加载到JVM的方法区中
- 可以将函数对象赋值给一个变量,在运行时,它是加载到JVM的堆内存中
- 函数是一个对象,继承自FunctionN,函数对象有apply,curried,toString,tupled这些方法。而方法则没有,方法是对对象进行操作
- 使用
_
即可将方法转换为函数
// 定义方法 add
scala> def add(x:Int,y:Int) = x+y
add: (x: Int, y: Int)Int
// 调用方法add,把方法add的值赋给a
scala> val a = add(1,2)
a: Int = 3
// 不能将方法add直接赋值给变量a
scala> val a = add
<console>:12: error: missing argument list for method add
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `add _` or `add(_,_)` instead of `add`.
val a = add
^
// 使用 _ 即可将方法add转换为函数
scala> val a = add _
a: (Int, Int) => Int = <function2>
高阶函数
Scala混合了面向对象和函数式的特性,我们通常将可以作为参数传递到方法中的表达式叫做高阶函数,也就是说高阶函数可以将其他函数作为参数或者使用函数作为输出结果。在函数式编程语言中,函数是“头等公民”,高阶函数包含:作为值的函数、匿名函数、嵌套函数、闭包、柯里化、偏函数等等。
作为值的函数
scala 一切皆值,故而把函数作为值是合乎情理的,最重要的是可以动态传入特性,这个效果非常实用。
定义函数时格式:val 变量名 = (输入参数类型和个数) => 函数实现和返回值类型
注意:
- “=”表示将函数赋给一个变量
- “=>”左面表示输入参数名称、类型和个数,右边表示函数的实现和返回值类型
例如:
// 定义一个去除字符串首尾空格的函数字面量trim
val trim = (str:String) =>{
str.trim
}
// 定义一个截取字符串首字母的函数字面量initial
val initial = (str:String) =>{
str.substring(0,1)
}
匿名函数
匿名函数是指不含函数名称的函数。
定义函数时格式:(输入参数类型和个数) => 函数实现和返回值类型
// 匿名高阶函数 函数体传参
def values(fun:(Int) => Int,low:Int,high:Int) ={
for (i <- low to high){
println((i,fun(i)))
}
}
def main(args: Array[String]): Unit = {
//匿名函数 函数回调 局部变量
val x = (f:Int,g:Int) => {
println("hello, world")
20*f+g
}
kkk(x(20,30))
}
匿名函数通常作为函数为值的“=”右边的部分。
嵌套函数
Scala函数内可以定义函数,函数内的函数也称局部函数或者内嵌函数。
// 实现斐波纳契数列
object MyQueue {
def fib(num:Int) = {
var num1:Int = 0
var num2:Int = 1
var res:Int = 0
var n = num
def neiFib() : Unit = {
if (n > 0){
res = num1 + num2
println(res+"\t")
num1 = num2
num2 = res
n -= 1
neiFib()
}
}
neiFib()
}
def main (args: Array[String]): Unit ={
MyQueue.fib(10)
}
}
柯里化(Currying)
柯里化(Currying)方法可以定义多个参数列表,当使用较少的参数列表调用多参数列表的方法(把原来接受多个参数的函数变换成接受一个参数的函数过程)时,会产生一个新的函数,该函数接收剩余的参数列表作为其参数。
object Test {
// 一个普通的非柯里化的函数定义,实现一个求余函数 单参数列表
def _modN(n:Int,x:Int) = {
x % n == 0
}
// 使用“柯里化”技术来定义这个求余函数 多参数列表
// 当你调用modN(1)(2)时,实际上是依次调用两个普通函数(非柯里化函数),
// 第一次调用使用一个参数n,返回一个函数类型的值,
// 第二次使用参数x调用这个函数类型的值。
def modN(n:Int)(x:Int) = {
x % n == 0
}
// 新函数接收剩余的参数列表作为其参数 相当于提供部分参数默认值
def f1(x:Int) = modN(10)(x)
def f2(n:Int) = modN(n)(10)
// 下划线“_” 作为第二参数列表的占位符
def f3 = modN(10)(_)
def main(args: Array[String]): Unit = {
println(f1(10))
println(f2(10))
println(f3)
}
}
scala 柯里化风格的使用可以简化主函数的复杂度,提高主函数的自闭性,提高功能上的可扩张性、灵活性。可以编写出更加抽象,功能化和高效的函数式代码。
闭包
闭包是依照包含自由变量的函数字面量在运行时创建的函数值,闭包是对函数本身及其所使用的自由变量的统一定义。闭包可捕获自由变量的变化,闭包对捕获变量作出的改变在闭包之外也可见。
上述是不是非常难以理解,实际上,闭包和 js 里的闭包是类似的, 闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。闭包通常来讲可以简单的认为是可以访问不在当前作用域范围内的一个函数。
package cn.itcast.closure
/**
* scala中的闭包
* 闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。
*/
object ClosureDemo {
def main(args: Array[String]): Unit = {
val n=10
//变量n不处于其有效作用域时,函数还能够对变量进行访问
val modN =(x:Int)=>{
x % n
}
//在modN中有两个变量:x和n。其中的一个x是函数的形式参数,
//在modN方法被调用时,x被赋予一个新的值。
//然而,n不是形式参数,而是自由变量
println(modN(5)) // 结果5
}
}
偏函数
函数在执行时,要带上所有必要的参数进行调用。但是,有时参数可以在函数被调用之前提前获知。这种情况下,一个函数有一个或多个参数预先就能用上,以便函数能用更少的参数进行调用。
-
偏函数被包在花括号内没有match的一组case语句是一个偏函数。
-
偏函数是PartialFunction[A, B]的一个实例:A代表输入参数类型,B代表返回结果类型。
举例:
scala> val a = Array(1,2,3,4)
a: Array[Int] = Array(1, 2, 3, 4)
scala> val fun:PartialFunction[Int,Int]={
| case x if x%2==0 => x+2
| case x => x+1
| }
fun: PartialFunction[Int,Int] = <function1>
scala> a.collect(fun)
res45: Array[Int] = Array(2, 4, 4, 6)
scala> val fun:PartialFunction[Int,Int]={
| case x if x%2==0 => x+2
| case _ => 1
| }
fun: PartialFunction[Int,Int] = <function1>
scala> a.collect(fun)
res46: Array[Int] = Array(1, 4, 1, 6)
隐式转换(隐式函数和隐式类)和隐式参数
Scala提供的隐式转换和隐式参数功能,是非常有特色的功能。
隐式转换
Scala的隐式转换,其实最核心的就是定义隐式转换方法。Scala会根据隐式转换方法的签名,在程序中使用到隐式转换方法接收的参数类型定义的对象时,会自动将其传入隐式转换方法,转换为另外一种类型的对象并返回。这就是“隐式转换”。其中所有的隐式值和隐式方法必须放到object中。
解析机制
即编译器是如何查找到缺失信息的,借鉴 Scala 隐式(implicit)详解 解析具有以下两种规则:
- 首先会在当前代码作用域下查找隐式实体(隐式方法 隐式类 隐式对象)
- 如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找类型的作用域是指与该类型相关联的全部伴生模块,一个隐式实体的类型T它的查找范围如下:
(1)如果T被定义为T with A with B with C,那么A,B,C都是T的部分,在T的隐式解析过程中,它们的伴生对象都会被搜索
(2)如果T是参数化类型,那么类型参数和与类型参数相关联的部分都算作T的部分,比如 List[String] 的隐式搜 索会搜索List的伴生对象和String的伴生对象
(3)如果T是一个单例类型p.T,即T是属于某个p对象内,那么这个p对象也会被搜索
(4)如果T是个类型注入S#T,那么S和T都会被搜索
转换前提与限制
- Scala默认会使用两种隐式转换,一种是源类型或者目标类型的伴生对象内的隐式转换方法;一种是当前程序作用域内的可以用唯一标识符表示的隐式转换方法。
- 隐式转换的方法在当前范围内才有效。如果隐式转换不在当前范围内定义(比如定义在另一个类中或包含在某个对象中),那么必须通过import语句将其导。
- 隐式操作不能嵌套使用,即一次编译只隐式转换一次。
- 代码能够在不使用隐式转换的前提下能编译通过,就不会进行隐式转换。
- 不存在歧义性。
转换方式
- 将方法或变量标记为implicit,即隐式方法或隐式参数
- 将方法的参数列表标记为implicit,即隐式参数
- 将类标记为implicit,即隐式类
转换时机
- 当方法中的参数的类型与目标类型不一致时,编译器会自动将类型进行隐式转换
- 当对象调用类中不存在的方法或成员时,编译器会自动将对象进行隐式转换
隐式参数
所谓的隐式参数,指的是在函数或者方法中,定义一个用 implicit 修饰的参数,此时 Scala 会尝试找到一个指定类型的,用implicit修饰的参数,即隐式值,并注入参数。
函数或者方法可以具有隐式参数列表,由参数列表开头的implicit 关键字标记,具体要求如下:
- implict只能修改最尾部的参数列表,应用于其全部参数
- Scala可自动传递正确类型的隐式值
- 通常与柯里化函数结合使用
Scala会在两个范围内查找:
- 当前作用域内可见的val或var定义的隐式变量;
- 一种是隐式参数类型的伴生对象内的隐式值;
- 隐式参数不能存在歧义性。
举例:
object MyImp {
// 隐式参数
implicit var k = 40
def abc(x:Int)(implicit y:Int) = x+y
def main(args: Array[String]): Unit = {
println(abc(10)) // 50
}
}
上述代码当调用abc方法时,会发现缺失y参数的值(在方法省略隐式参数的情况)没有给,此时编译器会搜索作用域内的隐式值k作为缺少参数。
但是如果此时你又在中MyImp 定义一个隐式变量,再次调用方法时就会报错,如下:
implicit var g = 40
Error:(36, 16) ambiguous implicit values:
both method k in object MyImp of type => Int
and method g in object MyImp of type => Int
match expected type Int
println(abc(10))
因此,隐式转换必须满足无歧义规则,在声明隐式参数的类型是最好使用特别的或自定义的数据类型,不要使用Int,String这些常用类型,避免碰巧匹配。
隐式函数
隐式转换为目标类型:把一种类型自动转换到另一种类型:
implicit def doubleToInt(x:Double)=x.toInt
def main(args: Array[String]): Unit = {
val i:Int=3.5 // 3
}
上述将小数Double转换成了整数类型,注意这里 scala 没有像 java 一样的强制类型转换,而是使用了隐式方法,进行类型转换。
类型增强
implicit def bool2Int(x:Boolean)=if(x) 1 else 0
def main(args: Array[String]): Unit = {
println(1+true) // 2
}
上述类型增强,实际上是类型转换的一种(将布尔Boolean转换成了整数类型)。
隐式类
隐式类的声明注意点如下:
- 其所带的构造参数有且只能有一个
- 隐式类必须被定义在类,伴生对象和包对象里
- 隐式类不能是case class(case class在定义会自动生成伴生对象与2矛盾)
- 作用域内不能有与之相同名称的标示符
隐式转换调用类中本不存在的方法
object MyImp {
// 隐式类
implicit class MyExtend(num:Int){
var n:Int = num
def sayHi = println(s"Hello,${n}")
}
def main(args: Array[String]): Unit = {
456.sayHi // Hello,456
}
}
上述整数 Int 类型的对象 456 根本没有 sayHi 方法,编译器就会在作用域范围内搜索隐式实体,发现有符合的隐式类 MyExtend 可以用来转换成 MyExtend 对象 获得了sayHi 方法。
参考文章
Java中Lambda表达式的使用 https://www.cnblogs.com/franson-2016/p/5593080.html
Scala 隐式(implicit)详解 https://www.cnblogs.com/xia520pi/p/8745923.html