一、函数式编程
1.1 概述
函数式编程(FP,即Functional Programming)也是近几年才逐渐为人们所知,但它并不是新的概念。函数式编程语言的鼻主Lisp语言的产生时间甚至比C语言还早,它拥有和面向对象编程(OOP)几乎等长的历史。
函数式编程,其实就是以 纯函数 的方式编写代码。
纯函数:一个函数在程序的执行过程中除了根据输入参数给出运算结果之外没用其他影响,就可以说是没有 副作用 的,我们就可以将这一类函数称之为纯函数。
纯函数最核心的目的是为了编写无副作用的代码,它的很多特性,包括不变量,惰性求值等等都是为了这个目标。
函数式编程的优点
- 可以以更少的代码实现同样的功能,可极大的提升生产效率
- 更容易编写多并发或多线程的应用,更易于编写利用多核的应用程序
- 可以帮助写出健壮的代码
- 更容易写出易于阅读、理解的优雅代码
函数式编程的基本特征
- 函数是一等公民:函数也有数据类型,函数与其他数据类型的变量或值一样,处于平等地位,可以赋值给其它变量,也可以作为函数参数,传入另一个函数,或者作为别的函数的返回值。
-
不可变数据:所有的状态(或变量)都是不可变的。你可以声明一个状态,但是不能改变这个状态。如果要变化,只能复制一个。
纯函数式编程语言不使用任何可变数据结构或变量。但在Scala等编程语言中,即支持不可变的数据结构或变量,也支持可变的。 -
函数没有 “副作用”:函数要保持独立,一旦函数的输入确定,输出就是确定的,函数的执行不会影响系统的状态,不会修改外部状态。
如果函数没有副作用,那函数的执行就可以缓存起来了,一旦函数执行过一次,如果再次执行,当输入和前面一样的情况下,就直接可以用前面执行的输出结果,就不用再次运算了,可大大提高程序运行的效率。 - 一切皆是表达式:在函数式编程语言中,每一个语句都是一个表达式,都会有返回值。
1.2 基本语法
函数和方法的区别
object TestFunction {
// (2)方法可以进行重载和重写,程序可以执行
def main(): Unit = {
}
def main(args: Array[String]): Unit = {
// (1)Scala语言的语法非常灵活,可以在任何的语法结构中声明任何的语法
import java.util.Date
new Date()
// (2)函数没有重载和重写的概念,程序报错
def test(): Unit ={
println("无参,无返回值")
}
test()
def test(name:String):Unit={
println()
}
//(3)scala中函数可以嵌套定义
def test2(): Unit ={
def test3(name:String):Unit={
println("函数可以嵌套定义")
}
}
}
}
1.3 函数的参数
// (1) 可变参数
def test(s: String*): Unit = {
println(s)
}
// 有输入参数:输出Array
test("Hello", "Scala")
// 无输入参数:输出List()
test()
// (2) 如果参数列表中存在多个参数,name可变参数一般放置在后面
def test2(name: String, s: String*): Unit = {
println(name + "," + s)
}
// (3) 默认参数,一般情况下,将有默认值的参数放置在参数列表的后面
def test3(name: String, age: Int = 30): Unit = {
println(s"$name, $age")
}
// 如果要使用默认,在调用的时候,可以省略这个参数
test3("jerry")
// 如果参数传递了值,那么会覆盖默认值
test3("tom", 20)
// (4) 命名参数
def test4(name: String, age: Int = 30, gender: String): Unit = {
println(s"$name, $age, $gender")
}
// 可以指明参数名称,进行传参
test4("alex", gender = "male")
1.4 函数的至简原则
- 函数体内可以省略 return,Scala 会自动把最后一行代码作为返回值
若函数使用 return 关键字,那么函数就不能自行推断了,需要声明返回值类型 - 返回值如果能够推断出来,那么可以省略
- 函数体只有一行代码,括号可以省略
- 若函数无参数,声明函数时可以省略小括号;若声明函数时省略小括号,则调用该函数时,也需要省略小括号
- 若函数明确声明 Unit,那么即使函数体中使用 return 关键字也不起作用
- Scala 如果想要自动推断无返回值,可以省略等号
// 将无返回值的函数称之为过程
def f7() {
println("dalang")
}
- 如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略,省略名字的函数称为匿名函数
- 纯函数:纯函数天然的支持高并发!
纯函数的特点:1. 不产生副作用(常见的副作用:打印到控制台,修改了外部变量的值,向磁盘写入文件...)2. 引用透明(函数的返回值,只和形参有关,和其它任何的值没有关系) - 惰性求值:类似于懒加载
val a = { 10 } // 立即赋值
lazy val b = { 20 } // 惰性求值,调用时赋值一次,以后不用赋值
def c = 30 // 每次调用都会赋值一次
1.5 高阶函数
参数或返回值为函数的函数称为高阶函数(高阶算子)
//高阶函数 ———— 函数作为参数
def calculator(a: Int, b: Int, operater: (Int, Int) => Int): Int = {
operater(a, b)
}
//函数(求和)
def plus(x: Int, y: Int): Int = {
x + y
}
// 函数(求积)
def multiply(x: Int, y: Int): Int = {
x * y
}
//函数作为参数
println(calculator(2, 3, plus))
println(calculator(2, 3, multiply))
1.6 匿名函数
没有名字的函数就是匿名函数,直接通过函数字面量(lambda表达式:( ) => x + y)来表示匿名函数
匿名函数的简写:当传入的参数只被使用了一次时,可以使用 _ 指代,多个 _ 代表多个参数。
object TestFunction {
//高阶函数 ———— 函数作为参数
def calculator(a: Int, b: Int, operator: (Int, Int) => Int): Int = {
operator(a, b)
}
//函数————求和
def plus(x: Int, y: Int): Int = {
x + y
}
def main(args: Array[String]): Unit = {
//函数作为参数
println(calculator(2, 3, plus))
//匿名函数作为参数
println(calculator(2, 3, (x: Int, y: Int) => x + y))
//匿名函数简写形式
println(calculator(2, 3, _ + _))
}
}
1.7 函数闭包&柯里化
闭包:如果一个函数, 访问到了它的外部(局部)变量的值, 那么这个函数和他所处的环境, 称为闭包
//外部变量
var x: Int = 10
//闭包
def f(x: Int, y: Int): Int = {
x + y
}
函数柯里化:将接收多个参数的函数转化成接受一个参数的函数过程,可以理解为把函数的参数列表的多个参数, 变成多个参数列表一个参数的过程。
// 1.原始函数,一次传入两个函数
def add1(a:Int, b:Int): Int = a + b
// 2.将原函数分解成两层函数,外函数传入第一个参数,内函数使用外函数的参数,同时还需要在传入一个函数,
// 这样其实是将一次传入两个参数的函数解耦,变成内外两个每次接收一个参数的函数。
def add2(a: Int): Int => Int = {
(b: Int) => a + b
}
// 3.第二步中add2(a:Int)返回一个函数,那么直接将(b:Int)作为其参数列表就可以简写成如下:
def add3(a: Int)(b: Int) = a + b
// 同时,add3可以看做一个函数,而 (a:Int)(b:Int) 是它的两个函数列表,但是每次只接收一个参数,这就是函数最终柯里化的状态。
// 理解柯里化:将接收多个参数的函数转化成接受一个参数的函数过程
1.8 递归函数
一个函数/方法在函数/方法体内又调用了本身,我们称之为递归调用
递归函数四要素:
- 函数调用自身
- 函数必须要有跳出的逻辑
- 函数的递归过程要逼近跳出逻辑
- 递归函数的返回值要手动声明
// 求 10 的阶乘
def factorial(n: Long): Long = {
if (n == 1) 1
else n * factorial(n - 1)
}
1.9 惰性函数
当函数返回值被定义为 lazy 时,函数的执行将被推迟,直到我们首次调用此值,该函数才会被执行,这种函数称之为惰性函数。
def main(args: Array[String]): Unit = {
lazy val res = sum(10, 30) // 注意:lazy 不能修饰 var 类型的变量
println("----------------")
println("res=" + res)
}
def sum(n1: Int, n2: Int): Int = {
println("sum被执行。。。")
return n1 + n2
}
// 输出结果
----------------
sum被执行。。。
res=40
1.10 值调用&名调用
- 值调用:把计算后的值传入
println(3 + 4)
- 名调用:把一段代码传入,调用时,运行代码,调用几次运行几次
def main(args: Array[String]): Unit = {
// 注意:这里调用时,要使用 a() 作为参数才可以代表a接收到的匿名函数,而 a 函数 本身内部什么都没有定义
foo(a())
}
// 定义 a 返回一个匿名函数 () => {}
def a:() => Int = () => {
println("f...")
10
}
// 将 a 以名调用的方式传入foo中,实际是把匿名函数 () => {} 传入 foo 中
def foo(a: => Int): Unit = {
println(a)
println(a)
println(a)
}
// 输出结果
f...
10
f...
10
f...
10
1.11 控制抽象
Scala 中可以自己定义类似于 if-else,while 的流程控制语句,即所谓的控制抽象。
提示:scala 中 { code...... } 结构称为代码块(block),可视为无参函数,作为 =>Unit 类型的参数值。
def main(args: Array[String]): Unit = {
var i =2
// 调用自己写的循环,这里传入两段代码
// myWhile({i <= 100})({println(i += 1);i += 1}) 可行
// 最终写法:
myWhile(i <= 100) {
println(i += 1)
i += 1
}
}
// 使用名调用、柯里化传入两个段代码,这两段代码都在调用时执行
def myWhile (condition: => Boolean)(op: => Unit): Unit = {
if (condition) {
op
myWhile(condition)(op)
}
}
以上就是一个自定义循环函数。
二、面向对象
2.1 Scala 的包
- 包的声明方式
// 1.直接在文件首行声明,和java一样
package com.alibaba.scalamaben.obj
// 2. 在代码中嵌套声明
package com {
package alibaba {
.....
}
}
// 这种方式可以使一个源文件中声明多个 package; 子包的类可以直接访问父包中的内容,而无需导包
// 但是很少使用
- 包对象
可以为包定义同名的包对象,定义在包对象中的成员,作为其对应包下所有 class 和 object 的共享变量,可以被直接访问。
package object com{
val shareValue="share"
def shareMethod()={}
}
- 导包
普通导入、通配符导入、起别名导入、屏蔽类、指定多个类导入、局部导入
// 1.通配符导入util下的所有类
import java.util._
// 2.给类起别名,给 ArrayList 起别为JAL,防止类名冲突
import java.util.{ArrayList => JAL}
// 3.屏蔽类:导入util下的所有类,但不包含ArrayList
import java.util.{ArrayList => _, _}
// 4.导入多个类,指定导入某几个类
import java.util.{HashSet, ArrayList}
object PckDemo2 {
// 5.局部导入,在局部免去写 List
import java.util.List
def foo() {
}
}
- Scala 中默认导入的包
scala._
scala.Predef._
java.lang._
- 权限修饰符
Scala 中属性和方法的默认访问权限为 'public',Scala 中不加修饰符即为 'public'
private 为私有权限,只在类的内部和伴生对象中可用
protected 为受保护权限,Scala 中受保护权限更为严格,在同类、子类中可以访问,同包内无法访问
private [ 包名 ] 增加包访问权限,包名下的其他类也可以使用
2.2 类和对象
- 类的属性
class User(var name: String, val age: Int, sex: String)
// var 修饰的 name 会有 scala 形式的 get/set 方法
// val 修饰的 age,由于不可变,只有 get 方法
// sex 则只是一个私有属性,没有对外使用的方法
实际生产中,很多 java 框架会利用反射调用 get/set 方法,有时候为了兼容这些框架,会为 scala 的属性设置 java 形式的 get和set 方法(通过@BeanProperty注解实现)。
class User(@BeanProperty var name: String, @BeanProperty val age: Int, sex: String)
// var 类型的属性会额外的生成 java 形式的 get/set 方法
// val 类型的属性只会额外生成 java 形式的 get 方法
// 当类中的属性不加 var/val 时,该属性默认为私有属性,没有公共的 get/set 方法,也无法在外部访问。
- 类的方法
语法:def 方法名 (参数列表) [: 返回值类型] = { 方法体 }
// 类中的方法
class Person {
def sum(n1:Int, n2:Int) : Int = {
n1 + n2
}
}
// 对象中的方法
object Person {
def main(args: Array[String]): Unit = {
val person = new Person()
println(person.sum(10, 20))
}
}
- 创建对象
语法:var / val 对象名 [: 类型] = new 类型 ( )
val 修饰的对象,不能改变对象的引用地址,可以改变对象的属性的值。
var 修饰的对象,可以修改对象的引用和属性值。
val person = new Person()
person.name = 'aa'
person.name = 'bb'
person = new Person() // person 不可变,故报错
- 构造器
Scala 类的构造器包含:主构造器 和 辅助构造器
主构造器:
1.主构造器只能有一个,当没有形参时可以省略 ( )
2.主构造器的 形参自动成为类的属性
辅助构造器:
1.辅助构造器的函数名为,多个辅助构造器之间构成重载
2.辅助构造器 首行必须直接或间接的调用主构造器
3.辅助构造器调用其他辅助构造器的收后面的调用前面的
object ObjDemo1 {
def main(args: Array[String]): Unit = {
val person2 = new Person(18)
}
}
class Person (val name: String, var age: Int, sex: String) {
var name: String = "lisi"
var age: Int = _ // 默认值 0
def this(age: Int) {
this()
this.age = age
println("辅助构造器")
}
def this(age: Int, sex: String) {
this(age)
// this.name = name 柱构造器中 val 修饰,无法修改
this.sex = sex
}
println("主构造器") // 主构造中的语句先被执行
}
2.3 三大特征
- 封装,同 java
- 继承,Scala 是单继承,函数和属性都是动态绑定,同时支持函数和属性的覆写
函数的覆写:必须添加 override 关键字(java 中可以省略)
属性的覆写:只支持覆写 val 类型,而不支持 var 类型
继承的构造器调用:只有子类的主构造器才有权利去调用父类的构造器
object Test {
def main(args: Array[String]): Unit = {
new Emp("z3", 11, 1001)
}
}
class Person(nameParam: String) {
var name = nameParam
var age: Int = _
def this(nameParam: String, ageParam: Int) {
this(nameParam)
this.age = ageParam
println("父类辅助构造器")
}
println("父类主构造器")
}
class Emp(nameParam: String, ageParam: Int) extends Person(nameParam, ageParam) {
var empNo: Int = _
def this(nameParam: String, ageParam: Int, empNoParam: Int) {
this(nameParam, ageParam)
this.empNo = empNoParam
println("子类的辅助构造器")
}
println("子类主构造器")
}
----------------输出
父类主构造器
父类辅助构造器
子类主构造器
子类的辅助构造器
观察以上例子在创建子类对象时,构造器的调用顺序。
- 多态
父类(编译时类型) = 子类(运行时类型)
子类的对象赋值给父类的引用
val person: Person = new Emp("李四", "23")
2.4 抽象属性和抽象方法
- 抽象属性和抽象方法
抽象类: abstract class Person {} // 通过 abstract 关键字标记抽象类
抽象方法: val/var name: String // 一个属性没有初始化,就是抽象属性
抽象方法: def hello(): String // 只声明,没有函数体
覆写抽象方法时可以不加 override
abstract class Person {
val name: String
var age: Int
def hello(): Unit
}
class Teacher extends Person {
// 覆写 val 类型需要加 override
override val name: String = "lzc"
var age = 1
// 覆写抽象方法可以不加 override
def hello(): Unit = {
println(s"hello $name")
}
}
- 抽象内部类(匿名子类)
和 Java 一样,Scala 可以通过包含带有定义或重写的代码的方式创建一个匿名的子类
abstract class Person {
val name: String
def hello(): Unit
}
object Test {
def main(args: Array[String]): Unit = {
// 创建匿名内部类的对象,这个类是 Person 的实现类
val person = new Person {
override val name: String = "teacher"
override def hello(): Unit = println("hello teacher")
}
}
}
2.5 单例对象(伴生对象)
- 伴生对象与伴生类
Scala语言是完全面向对象的语言,所以并没有静态的操作(即在Scala中没有静态的概念)。而是产生了一种特殊的对象来模拟静态类对象,该对象为单例对象。若在一个文件中单例对象名与类名一致,则该单例对象与这个类与互为 伴生对象和伴生类,这个类的所有“静态”内容都可以放置在它的伴生对象中声明。
因此,在 Scala 中, 单例模式已经在语言层面完成了解决。创建一个单例要使用关键字 object,因为不能实例化一个单例对象,所以不能传递参数给它的构造器。
伴生对象和伴生类必须位于同一文件中,伴生对象和伴生类可以相互访问私有成员 - apply 方法
使用伴生对象 ( 参数 )的形式,其实是在调用伴生对象的 apply(参数) 的方法
通过伴生对象的 apply 方法,可以实现不使用 new 方法来创建对象。
apply方法 可以重载
object SingleDemo1 {
def main(args: Array[String]): Unit = {
// 默认调用 aplly 方法,不使用new创建对象
Car("red", "JAPAN")
Car("blue")
}
}
// 伴生对象
object Car {
def apply(color: String, country: String):Car = {
val car = new Car(color, country) // 调用伴生类的私有化构造器
car // 返回对象
}
// 重载的 apply 方法
def apply(color: String): Car = new Car(color) // 调用伴生类的私有化构造器
}
// 伴生类,参数列表前加 private 私有化主构造器
class Car private (val color: String, country: String = "CHNIA"){
println(s"a $color Car made in $country")
}
---------------- 输出
a red Car made in JAPAN
a blue Car made in CHNIA
- 小结
1.Scala 中伴生对象采用 object 关键字声明,所谓的伴生对象其实就是类的静态方法和静态属性的集合
2.伴生对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致,且在同一个源码文件中
3.伴生对象中的属性和方法都可以通过伴生对象名(类名)直接调用访问
4.类和伴生对象之间可以互相访问对方的私有成员
5.apply 方法,可以实现不使用 new 方法来创建对象
2.6 特质(trait)
- 特质的基本使用
Scala 是纯面向对象的语言,在 Scala 中,没有接口,采用特质trait
(特征, 特质)来代替接口的概念,当多个类具有相同的特质时,就可以将这个特质独立出来,采用关键字trait
声明。
特质可以有抽象方法, 也可以有实体方法, 相比抽象类最大的优点是特质可以实现多继承, 抽象类是只能实现单继承。
特质可以有非抽象方法
一个类已经继承了一个类,或已经混入了一个特质,要再混入一个特质时应该使用 with
覆写特质的方法时可以不加 override
object TraitDemo {
def main(args: Array[String]): Unit = {
val dog = new Dog
dog.move("lag")
}
}
trait Animal {
// 1.特质也可以有非抽象的方法
def run(useWhat: String): Unit = {
println(s"an Animal run in $useWhat")
}
}
trait Creature {
def move(useWhat: String)
}
// 2.已经继承了一个类,或已经混入了一个特质,要再混入一个特质时应该使用 with
class Dog extends Animal with Creature {
// 3.覆写特质的方法时可以不加 override
def move(useWhat: String): Unit = {
println(s"a dog move in $useWhat")
}
}
- 特质的动态混入
除了可以在类声明时继承特质以外,还可以在构建对象时混入特质
,扩展目标类的功能。
动态混入是 Scala 特有的方式,动态混入可以在不影响原有的继承关系的基础上,给指定的类扩展功能
object TraitDemo2 {
def main(args: Array[String]): Unit = {
val mysql = new Mysql with BetterConnectDB // 创建 Mysql 对象的时候, 指定一个新的特质
mysql.connectToMysql()
// 如果再创建新的对象, 也可以换另外的特质 ———— 这就叫动态混入
}
}
trait ConnectToDB { // 这个特质用来连接数据库
def getConn() {} // 什么都不做的实现方法
}
class Mysql extends ConnectToDB { // 连接到Mysql
def connectToMysql(): Unit = {
// 获取连接
getConn()
//其他代码
}
}
// 上面的 getConn其实什么都没有做, 感觉没有什么用
// 但是我们创建 Mysql对象的时候, 可以给他指定一个更好的特质
trait BetterConnectDB extends ConnectToDB { // 一个更好的特质
override def getConn(): Unit = { // 注意这个时候需要添加override
println("更好的获取连接....")
}
}
- 叠加特质
构建对象的同时如果混入多个特质,称之为叠加特质
object OverflowTrait {
def main(args: Array[String]): Unit = {
val test = new Test with A with B with C // 叠加特质
test.foo() // 最先调用 C 中的 foo 方法(C 又会掉 B中的 foo 一次类推,直到调用到 Fater 中的 foo)
}
}
class Test { }
trait Father{
def foo(): Unit ={
println("Father...")
}}
trait A extends Father{
override def foo(): Unit = { // 以下省略每个 trait 中的 foo 方法(自行脑补)
println("A...")
super.foo() // 不想按照顺序向上找, 可以直接指定该特质的直接父特质 super[Father].foo()
}}
trait B extends Father{
override def foo(): Unit = {
println("B...")
super.foo()
}}
trait C extends Father{
override def foo(): Unit = {
println("C...")
super.foo()
}}
---------------输出
C...
B...
A...
Fater...
一个对象混入多个特质,且特质的构建顺序(从上到下)和混入的顺序一致(从左到右),对象中的 foo 方法一定是最后一个特质的方法。
如果不想按照顺序向上找, 可以直接指定该特质的直接父特质super[Father].foo()
- 特质中的具体字段
特质中的字段可以是抽象的也可以是具体的
混入该特质(或者继承该特质)的类就具有了该字段,字段不是继承,而是直接加入类,成为自己的字段。 - 特质继承类
Scala 中还有一种不太常用的手法: 特质继承类
将来这个被继承的类会自动成为所有混入了该特质的类的直接超类。
注:如果混入该特质的类,已经继承了另一个类(A类),则要求A类是特质超类的子类,否则就会出现了多继承现象,发生错误 - 自身类型
当特质A 继承类B 的时候, 编译器能够确保的是所有混入该特质A 的类都认类B 作为自己的超类。
Scala 还有另外一套机制也可以保证这一点:自身类型(self type)
trait Logger {
// 声明该特质就是 Exception,后面的 getMessage 才可以调用
this: Exception => // 表示混入该特质的类必须继承 Exception 或 Exception 的子类
def log(): Unit = {
println(getMessage)
}
}
class Console extends Exception with Logger {
}
2.7 补充
- 类型检测和转换
obj.isInstanceOf [ T ] 判断 obj 是不是 T 类型
obj.asInstanceOf [ T ] 将 obj 强转成 T 类型
classOf 获取对象的类名 - 枚举类和应用类
枚举类:需要继承 Enumeration
应用类:需要继承 App - Type 定义新类型
使用 type 关键字可以定义新的数据数据类型名称,本质上就是类型的一个别名
object Test {
def main(args: Array[String]): Unit = {
type S = String // 将 String 类型定义为 S 类型
var v: S = "abc"
def test(): S = "xyz"
}
}