Scala有一个先Java推出的快捷编写格式,就是高阶方法,也叫高阶函数。本质是将一个方法当做参数传入另一个方法中,常见的是匿名方法
举个例子,用Java的话想要过滤出一个集合中的东西,常见的是写一个循环或者stream流处理,当然stream在大数据集或者复杂场景中会有风险所以很少用。而在Scala中你可以写如下的代码,传入一个匿名函数
package com.wy
object CollectionTest {
def main(args: Array[String]): Unit = {
//数据集合
var mymap = Map("book"->5,"pen"->2,"chizi"->6)
//tmp=>(tmp._2>=5) 一个匿名方法
//val filter: Map[String, Int] = mymap.filter(tmp=>(tmp._2>=5))
//缩写格式
val filter: Map[String, Int] = mymap.filter(_._2>=5)
println(filter)
//用来map也可以
val mapreuslt: Map[String, Int] = mymap.map(tmp => tmp._1->tmp._2*2)
println(mapreuslt)
//用来便利也行
val forechresult: Unit = mymap.foreach(tmp => println(tmp._1+"的值是:"+tmp._2))
}
}
除了map、foreach等等,这些常见的架构自带的高级函数,我们也可以自定义自己的。例如当前有如下方法
def p(f: Int => String, v: Int){
println(v)
}
首先不看参数,那么p方法其实就是一个接受两个参数,内部打印的普通方法。使它成为高阶方法的是f: Int => String
这个参数方法表达式,也是高级方法接受其他方法的格式,下面简称参数方法
f: Int => String
f
是参数方法的形参名字,按需自定义即可,Int
指的是这个方法有一个Int型参数,=> String
表示返回值是String,当然如果参数不止一个那么定义的时候,参数方法的参数位要用小括号包裹参数,比如
f: (Int,Float) => String
当你需要传入该类型参数时,只需要按照格式定义即可,比如
val fun=(x:Int)=>{x+"---"}
//单参数或者方法体代码只有一个或者一行时括号和花括号都可以看情况省略
//如果写的时候在赋值上报错,这是scala jdk版本影响,用一个括号包裹匿名方法就可以赋值了
为了方便使用我将它赋值给了常量fun,下面我们来使用它
package com.dtdream.driver
object TTT {
def main(args: Array[String]): Unit = {
//1、一个匿名方法,作用是拼接之后返回
val fun=(x:Int)=>{x+"高阶函数内部拼接"}
//2、上面的方法做参 调用高阶方法
println(p(fun,21))
}
//3、高阶方法 指定返回值是因为要把值返回到main方法中
def p(fun: Int => String, v: Int):String= {
//4、实现逻辑
fun(v)
}
}
效果:
当然,日常开发,不会写上面这样繁琐的代码,就拿我这个例子来说,其实完全可以不用显示的写那个匿名方法的定义,直接在调用高阶函数的时候写成如下格式
//平常的写法写
println(p(_+"高阶函数内部拼接",21))
//完整的写法
println(p(m=>{m+"高阶函数内部拼接"},21))
上面是最基础的高阶方法概念,在具体的开发中会有很多有意思的写法,但是核心的概念的不会变。比如如果你看过Spark的源码,你会发现SparkSQL的逻辑执行数生成源码中有如下的代码
protected def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
logDebug(s"Parsing command: $command")
val lexer = new SqlBaseLexer(new UpperCaseCharStream(CharStreams.fromString(command)))
lexer.removeErrorListeners()
lexer.addErrorListener(ParseErrorListener)
val tokenStream = new CommonTokenStream(lexer)
val parser = new SqlBaseParser(tokenStream)
parser.addParseListener(PostProcessor)
parser.addParseListener(UnclosedCommentProcessor(command, tokenStream))
parser.removeErrorListeners()
parser.addErrorListener(ParseErrorListener)
parser.legacy_setops_precedence_enabled = conf.setOpsPrecedenceEnforced
parser.legacy_exponent_literal_as_decimal_enabled = conf.exponentLiteralAsDecimalEnabled
parser.SQL_standard_keyword_behavior = conf.ansiEnabled
try {
try {
// first, try parsing with potentially faster SLL mode
parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
toResult(parser)
}
catch {
case e: ParseCancellationException =>
// if we fail, parse with LL mode
tokenStream.seek(0) // rewind input stream
parser.reset()
// Try Again.
parser.getInterpreter.setPredictionMode(PredictionMode.LL)
toResult(parser)
}
}
catch {
case e: ParseException if e.command.isDefined =>
throw e
case e: ParseException =>
throw e.withCommand(command)
case e: AnalysisException =>
val position = Origin(e.line, e.startPosition)
throw new ParseException(Option(command), e.message, position, position,
e.errorClass, e.messageParameters)
}
}
override def parseDataType(sqlText: String): DataType = parse(sqlText) { parser =>
astBuilder.visitSingleDataType(parser.singleDataType())
}
这个源码你看不懂没关系,我给你拆解成一个最简单的例子你就明白了
package com.duxiaoman.wangyang.datav
object Test2 {
/**
* f1 方法返回一个字符串,这个字符串由 s2 入参的函数来处理
* @param s1 一个普通的字符串
* @param s2 一个高阶方法,形参String,返回值也是String
* @return 返回String类型数据
*/
def f1(s1:String)(s2:String => String):String={
s2(s1)
}
/**
* f2 方法调用 f1 方法将自身的 nr 入参作为 f1 方法的第一个参数
* @param nr 调用时的入参
* @param {nr1 => nr1} 这个就是一个匿名方法 ,日常习惯用括号,但是用花括号也可以,而且支持多行代码
* @return
*/
def f2(nr: String): String = f1(nr){nr1 => nr1}
def main(args: Array[String]): Unit = {
println(f2("main方法入参"))
}
}
他为什么怎么写呢?是因为上面的Spark的源码中,维护团队需要让parse
能支持其他parse*
的方法多态,说白了parse
就是核心逻辑,但是把一个复杂逻辑的方法以普通参数方法的格式定义,会导致放不下,从而使得代码不好运维,所以parse
方法用了柯里化,因此有了parse*
方法后面的 {parse => 方法体}
这种格式。当然柯里化本身不能用来省略高阶方法,会有编译检测报错,所以大家如果仿写这种格式的时候要注意,不要邯郸学步