现在的项目虽然都转到 java 来了,但旧的项目hyperloop、space-core依然在使用 scala 维护,这篇讲义只讲scala的简单使用,目的是使各位新来的同事能够首先看懂程序,因为 scala 有的语法对于之前使用习惯了 java 的人来说还是比较晦涩的。
网上对 scala 的介绍很多,都说从 java 转到 scala 很容易,我觉得说这句话的人90%都没有认真写过 scala 代码。
Scala like Java ,scala 和 java 很像,我总结下来 scala = java ++ --, Scala 等于 Java 加加减减
scala 介绍与安装
这个 ++ 体现在
以上2、3、4 是对现有 java中类型的补充和扩展,如早在 java5中就有 Future 、FutureTask 等异步接口了
这个 - - 就体现在
- 语法的精简,让你可以写更多简单易懂的 one line code (一行代码)
小结
Scala 类似于JAVA,设计初衷是实现可伸缩、融合面向对象编程特性与函式编程风格,可直译、可编译、静态、运行于JVM之上、可与Java互操作的一门多范式的编程语言。Scala 代码会先翻译成 Java 的 class 在执行
下面就讲讲这门多范式的编程语言!!!
函数编程的理解
啥叫函数编程? 写几个java8 的 stream 表达式就会函数编程了?
先理解啥叫函数,我类比 OOP 中的对象来说,对象是对事物的抽象,面向对象编程的特点就是抽象、继承、封装、多态等等
函数是对行为过程的抽象,要想理解什么是函数式编程,要先知道什么是函数.函数式数学中的概念,从一个状态与另一个状态的对应关系,或者从一个状态到另一个状态的映射就是函数,y=f(x) 这个 f 就是函数。就像谈什么是面向对象一样,继承、封装、多态、抽象一样,谈什么函数式编程就要说函数式编程的特点,函数编程的特点有很多:函数式第一公民、高阶函数、模式匹配、无副作用、不变性。
函数其实说到底是一个集合到另一集合的映射。函数有 N多个名字,在 java 的类中叫方法,在 scala 的类中叫函数,在 java 的方法参数中叫 lamda 这个 lamda 的类型叫函数接口,而这个 lamda 又有一个别名叫 匿名函数,在引用了外部环境变量的 lamda 中叫闭包 (js 中有这种叫法,不知道准不准),在 OC 中叫 block ,很像,像到我都不想去区分他们。
一些函数编程的特性
-
函数是第一公民: 函数可以传递,这也是语言支持高阶函数的先决条件
-
引用透明:函数式编程的一个特点就是变量状态不可变,无可变状态也就造成了函数的引用透明的特性,函数的引用透明指的是函数没有副作用。scala 还不算纯函数式编程语言,所以它有的函数是可以有副作用的。有一种说法说变量是万恶之源,因为不变的量就不用考虑多线程中线程间通信等问题了。
-
模式匹配:模式可能出现的一个地方就是 模式匹配表达式(pattern matching expression): 一个表达式 e ,后面跟着关键字 match 以及一个代码块,这个代码块包含了一些匹配样例; 而样例又包含了 case 关键字、模式、可选的 守卫分句(guard clause) ,以及最右边的代码块; 如果模式匹配成功,这个代码块就会执行。 写成代码,看起来会是下面这种样子:
e match {
case Pattern1 => block1
case Pattern2 if-clause => block2
...
}
很多特点自行百度,不在一一赘述。
基础语法
一切从 Hello word 开始
Scala
object ScalaMain {
def main(args: Array[String]): Unit = {
val variable = "hello word"
println(s"${variable}")
}
}
Java8
public class JavaMain {
public static void main(String[] args) {
String variable = "hello word!!";
System.out.println(variable);
}
}
第一行代码有点奇怪,object在这里表示单例类的声明,即ScalaMian是一个类,这个类只有一个实例,这个单一实例会在需要时由scala创建。
第二行是main函数的函数声明,def表示声明函数,main是函数名,小括号中是函数的参数列表,这里只有一个参数args,args后面跟:
和其数据类型,这里是String数组,这个函数没有返回值。
第三行是给变量 variable 赋值,可见 scala 的变量声明使用 val/var 变量的类型紧跟变量名中间用:
隔开。
第四行代码中调用了一个内置函数println,这个函数输出了Hello World!字符串,这个语句后面没有分号,在scala中分号不是必须的,只有在一行中输入两个语句时才需要用分号分隔。同时可以看到字符串可以向某些脚本语言如 groovy、shell 一样使用$
取值,这个用法叫做字符串插值。
运行上面程序会输出经典的“Hello world!”。
你可能会有几个疑惑的点?为什么声明变量的时候用 var/val ? 为什么不用声明变量的类型呢?object 是个什么鬼?
答: scala 中声明变量用 var ,声明常量用 val 。var 代表该变量在以后的使用中可以改变其值,而 val 修饰的变量一经定义便不再允许修改。变量可以不用显示的声明类型,其类型有类型推导得来
val x1="123" //自认而然就认识他是字符串类型而不是整型
val x2:String="123" //也可显示表明数据类型
var x3:Int=123 // var 表明 x3 是一个变量
x3=10 //这是正确的
x1="chenshang" // 立马编译报错,因为是 val 修饰的常量
scala 中用 def 声明函数
/**
* 这是最传统的声明方式,还有很多情况可以简写
*/
def add(x:Int,y:Int):Int={
x+y
}
def returnUnit():Unit={
println("another way to return void")
}
//写法二,省略非Unit返回值;如果没有写返回值,则根据等号后面的东西进行类型推演
def test(x:Int)={
x
}
//写法三,省略等号,返回Unit
def returnVoid(){
println("return void")
}
//写法四:省略花括号,如果函数仅包含一条语句,那么连花括号都可以选择不写
def max2(x: Int, y: Int) = if (x > y) x else y
def greet() = println("Hello, world!")
既然说到object 这个关键字了,我们先来说说 scala 的这个 object --单例对象。
Java 中的单例对象的概念,全局独一份的对象实例,scala 中也是这个意思,用 object 修饰的类就变成了单例类,单例类中的方法都是静态,不过 scala 中没有明确的静态方法的概念,因此他没有 staic 这样的关键字的!!所以这个 object 类就是 new 了一个单例对象罢了。我们看看 java 是如何实现一个单例类的呢:
/**
* 懒汉式
*/
public class Single1 {
private static Single1 instance;
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
/**
* 双重检查(Double-Check)版本
*/
public class Single3 {
private static Single3 instance;
private Single3() {}
public static Single3 getInstance() {
if (instance == null) {
synchronized (Single3.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}
scala 只用一个 object 关键字就搞定了!scala在实例化object 时使用的什么方式呢?大家可以试着反编译一下 scala 生成的 java class 看看。
object ScalaOBJ {
def get(x: String) = print(x)
}
scalac ScalaOBJ.scala
一个 scala 文件被编译成
.
├── ScalaOBJ$.class
└── ScalaOBJ.class
0 directories, 2 files
我们有 javap 来翻译一下其中一个.class 文件
chenshang@begon:~/learn$ javap -c ScalaOBJ\$.class
Compiled from "ScalaOBJ.scala"
public final class main.javavsscala.ScalaOBJ$ {
public static main.javavsscala.ScalaOBJ$ MODULE$;
public static {};
Code:
0: new #2 // class main/javavsscala/ScalaOBJ$
3: invokespecial #12 // Method "<init>":()V
6: return
public void get(java.lang.String);
Code:
0: getstatic #20 // Field scala/Predef$.MODULE$:Lscala/Predef$;
3: aload_1
4: invokevirtual #24 // Method scala/Predef$.print:(Ljava/lang/Object;)V
7: return
}
伴生类&伴生对象
上面介绍到了object,以此为引申来讲讲伴生对象和伴生类
所谓伴生对象, 也是一个Scala中的单例对象, 使用object关键字修饰。除此之外, 还有一个使用class关键字定义的同名类, 这个类和单例对象存在于同一个文件中,这个类就叫做这个单例对象的伴生类, 相对来说, 这个单例对象叫做伴生类的伴生对象。
// 下面这个类的伴生类
class OBJ(val id: String, age: Option[Int]) {
}
// 上面这个类的伴生对象
object OBJ {
private val name: String = "chenshang"
def apply(id: String, age: Option[Int]): OBJ = new OBJ(id, age)
def done() = {
println("haha")
}
}
Scala单例对象是十分重要的,没有像在Java一样,有静态类、静态成员、静态方法,但是Scala提供了object对象,这个object对象类似于Java的静态类,它的成员、它的方法都默认是静态的。
小结
里面的方法全是静态方法 相当于把 java 中静态类的方法单独抽取出来了一个对象
类的定义
上边我们已经自定义过object 单例对象了,它本质上也是定义了一个 class 然后实例化了一个类的对象出来,我们接下来看看 scala 面向对象的特性与 java的有那些写法上的不同。
定义scala的简单类
class Point (val x:Int, val y:Int) {
}
注意用 val 声明的属相是没有 set 方法的,因为 val 修饰的变量属于常量不可以修改,所以其相当于 java 的类种没有 set 方法。
上面一行代码就是一个scala类的定义:
- 首先是关键字class
- 其后是类名 Point
- 类名之后的括号中是构造函数的参数列表,这里相当于定义了对象的两个常量,其名称分别为x,y,类型都是Int
翻译成Java8
public class JavaPoint {
public int x;
public int y;
public int getX() {
return x;
}
public int getY() {
return y;
}
public JavaPoint(int x, int y) {
this.x = x;
this.y = y;
}
}
把上面那个简单的 scala 写成传统形式是这样的
class Point (xArg:Int, yArg:Int) {
val x = xArg
val y = yArg
}
这个是不是和 java 的构造方法很像呢?没错,scala将类的主构造方法绑定到一块了,它认为这样做更简洁吧,但 scala 同样支持想 java那样定义类。
带函数的 scala 类
class Point (var x:Int, var y:Int){
// 定义函数
def add= x+y
}
翻译成Java8
public class JavaPoint {
//省略上面的变量定义和 set/get 方法
public int add() {
return x + y;
}
}
继承
带继承的 scala 类
class TalkPoint(x:Int, y:Int) extends Point (x,y) {
def talk() = {
println("my position is ("+x+","+y+")")
}
}
extends Point(x,y) 之后会自动调用基类的构造函数。不用像 java 一样还得显示的用 super(x,y).注意这里说的是显示,其实 scala 还是这么干了
翻译成Java8
public class TalkPoint extend Point {
//省略上面的变量定义和 set/get 方法
public TalkPoin(int x, int y) {
super(x, y);
}
public void talk(){
println("my position is ("+x+","+y+")");
}
}
上面只是 scala 的 class 定义最基本的几种形式,大家可以自行学习一下 scala 的 class 在各种情况下翻译成对应的java 代码长什么样子?想要了解请点击这里
Trait
面向对象的本质是抽象,对事物的抽象是对象,对象中行为的抽象就是接口,scala 中没有 interface 在样的关键字,取而代之的是 trait ,翻译成中文叫特性特质,实现 trait 不用 implement 而是用 extends ,过个 trait 用 with 相连标示一个整体,我们先来看一下
Scala Trait(特征)
trait Equal {
def isEqual(x: Any): Boolean
def isNotEqual(x: Any): Boolean ={
!isEqual(x)
}
}
clase A extends Equal with TraitB{
...
}
Java8 Interface(接口)
interface Equal {
Boolean isEqual(Object x);
default Boolean isNotEqual(Object x) {
return !isEqual(x);
}
}
class A implement Equal,InterfaceB{
...
}
java8 中的默认方法用 default 关键字声明
特征构造顺序
特征也可以有构造器,由字段的初始化和其他特征体中的语句构成。这些语句在任何混入该特征的对象在构造是都会被执行。
构造器的执行顺序:
- 调用超类的构造器;
- 特征构造器在超类构造器之后、类构造器之前执行;
- 特征由左到右被构造;
- 每个特征当中,父特征先被构造;
- 如果多个特征共有一个父特征,父特征不会被重复构造
- 所有特征被构造完毕,子类被构造。
- 构造器的顺序是类的线性化的反向。线性化是描述某个类型的所有超类型的一种技术规格。
小结
- Scala Trait(特征) 相当于 Java 的接口,实际上它比接口还功能强大。
- 与接口不同的是,它还可以定义属性和方法的实现。
- 一般情况下Scala的类只能够继承单一父类,但是如果是 Trait(特征) 的话就可以继承多个,从结果来看就是实现了多重继承
臭名昭著的菱形问题
java 从 c++ 除去了多重继承,只允许单根继承,这个直接解决了菱形问题,但如今 scala 和 java8 都允许接口可以有默认的实现法法了,之所以打破以前的设计在接口中增加具体的方法, 是为了既有的成千上万的Java类库的类增加新的功能, 且不必对这些类重新进行设计。 因此不可避免的的就要花精力来解决这个问题,C++中解决的办法是虚拟基类,我么看看 java 和 scala 是如何处理的。
我们知道, 接口可以继承接口, 类可以继承类和实现接口。 一旦继承的类和实现的接口中有相同签名的方法, 会出现什么样的状况呢?
C++ 解决方法
-
在C++中是通过虚基类virtual实现,并按照深度优先,从左到右的顺序遍历调用
Java8 解决方法
类优先于接口。 如果一个子类继承的父类和接口有相同的方法实现。 那么子类继承父类的方法
子类型中的方法优先于父类型中的方法。
如果以上条件都不满足, 则必须显示覆盖/实现其方法,或者声明成abstract。
Scala解决方法
Scala 的基于混入的类构成(mixin class composition)体系是线性混入构成(linearmixin compostion)和对称的混入模块(mixin modules),以及traits这三者的融合。
Scala是通过类的全序化(Class Linearization),或称作类的线性化。线性化指出一个类的祖先类是一条线性路径的,包括超类(superclass)和特性(traits)。它通过两步来处理方法调用的问题:
① 使用右孩子优先的深度优先遍历搜索(right-first,depth-first search)算法进行搜索。
② 遍历得到的结构层次中,保留最后一个元素,其余删除。
线性混入,即是指使用右孩子优先的深度优先遍历搜索算法,列出层次结构(Scala class hierarchy),因此Scala多重继承的混入类中,如果包含有混入类(Mixins,或称为混入组合),则多重继承中总是选择最右边的(right-mostly)的实现方法。
项目中常用的语法
字符串插值
- Scala中的String类就是Java的String类,所以可以直接调用Java里String的所有方法。
- 字符串中的变量替换,Scala中基础的字符串插值就是在字符串前加字幕‘s’,然后在字符串中放入变量,每个变量都应以‘$’开头。字符串前加字母‘s’时,其实是在创建一个处理字符串字面量
- 在字符串字面量中使用表达式,“${}内可嵌入任何表达式”,包括等号表达式。
scala> println(s"Age next year: ${age + 1}")
Age next year: 34
scala> println(s"You are 33 years old:${age == 33}")
You are 33 years old:true
# 注意,在打印对象字段时使用花括号。
scala> case class Student(name: String, score: Int)
defined class Student
scala> val hannah = Student("Hannah", 95)
hannah: Student = Student(Hannah,95)
scala> println(s"${hannah.name} has a score of ${hannah.score}")
Hannah has a score of 95
for 表达式
Scala 定义list
val list01= 1 to 10
val list02= 1 to 100
Java8 定义 list
List<Integer> list01 = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
List<Integer> list02 = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
list.add(i);
}
Scala for 表达式
//第一种:普通的 for 循环
for (x <- list) {
println(x)
}
//第二种:带守卫的 for 循环
for (x <- list if (x / 2 == 0)) {
println(x + 2)
}
//第三种:中间变量绑定
for (x <- list; a = x + 1; y <- list2) {
println(s"$x:$a:$y")
}
//第四种:for yeild
val result = for {
x <- list;
result = x + 1
} yield result
/**
* yield 关键字的简短总结:
* • 针对每一次 for 循环的迭代, yield 会产生一个值,被循环记录下来 (内部实现上,像是一个缓冲区).
* • 当循环结束后, 会返回所有 yield 的值组成的集合.
* • 返回集合的类型与被遍历的集合类型是一致的.
*/
Java8 for 语法糖
//第一种
for (Integer x : list) {
System.out.println(x);
}
list.stream().forEach(x -> System.out.println(x));
list.forEach(x -> System.out.println(x));
//对应 scala 中带守卫的 for循环
ist.forEach(x -> {
if (x / 2 == 0) {
System.out.println(x);
}
});
for (Integer x : list) {
if (x / 2 == 0) {
System.out.println(x);
}
}
//中间变量绑定
for (Integer x : list) {
int a = x++;
for (Integer y : list2) {
System.out.println(x + ":" + a + ":" + y);
}
}
list.forEach(x -> {
int a = x++;
final Integer finalX = x;//注意这个地方有个坑,闭包要求传入的变量是不可变的额
list2.forEach(y -> {
System.out.println(finalX + ":" + a + ":" + y);
});
});
//for yield , scala 的表达式是有返回值的
List<Integer> result = new ArrayList<>();
list.forEach(x -> {
int res = x + 1;
result.add(res);
});
Option vs Optional
在使用 scala的函数编程之前先要讲解一个新的类型,这个类型在 scala 中叫 Option ,在Java 中叫 Optional。
以下内容原文地址
基本概念
Java 开发者一般都知道 NullPointerException(其他语言也有类似的东西), 通常这是由于某个方法返回了 null ,但这并不是开发者所希望发生的,代码也不好去处理这种异常。
值 null 通常被滥用来表征一个可能会缺失的值。 不过,某些语言以一种特殊的方法对待 null 值,或者允许你安全的使用可能是 null 的值。 比如说,Groovy 有 安全运算符(Safe Navigation Operator) 用于访问属性, 这样 foo?.bar?.baz 不会在 foo 或 bar 是 null 时而引发异常,而是直接返回 null, 然而,Groovy 中没有什么机制来强制你使用此运算符,所以如果你忘记使用它,那就完蛋了!
Clojure 对待 nil 基本上就像对待空字符串一样。 也可以把它当作列表或者映射表一样去访问,这意味着, nil 在调用层级中向上冒泡。 很多时候这样是可行的,但有时会导致异常出现在更高的调用层级中,而那里的代码没有对 nil 加以考虑。
Scala 试图通过摆脱 null 来解决这个问题,并提供自己的类型用来表示一个值是可选的(有值或无值), 这就是 Option[A] 特质。
Option[A] 是一个类型为 A 的可选值的容器: 如果值存在, Option[A] 就是一个 Some[A] ,如果不存在, Option[A] 就是对象 None 。
在类型层面上指出一个值是否存在,使用你的代码的开发者(也包括你自己)就会被编译器强制去处理这种可能性, 而不能依赖值存在的偶然性。
Option 是强制的!不要使用 null 来表示一个值是缺失的。
创建 Option
通常,你可以直接实例化 Some 样例类来创建一个 Option 。
val greeting: Option[String] = Some("Hello world")
或者,在知道值缺失的情况下,直接使用 None 对象:
val greeting: Option[String] = None
然而,在实际工作中,你不可避免的要去操作一些 Java 库, 或者是其他将 null 作为缺失值的JVM 语言的代码。 为此, Option 伴生对象提供了一个工厂方法,可以根据给定的参数创建相应的 Option :
val absentGreeting: Option[String] = Option(null) // absentGreeting will be None
val presentGreeting: Option[String] = Option("Hello!") // presentGreeting will be Some("Hello!")
使用 Option
目前为止,所有的这些都很简洁,不过该怎么使用 Option 呢?是时候开始举些无聊的例子了。
想象一下,你正在为某个创业公司工作,要做的第一件事情就是实现一个用户的存储库, 要求能够通过唯一的用户 ID 来查找他们。 有时候请求会带来假的 ID,这种情况,查找方法就需要返回 Option[User] 类型的数据。 一个假想的实现可能是:
case class User(
id: Int,
firstName: String,
lastName: String,
age: Int,
gender: Option[String]
)
object UserRepository {
private val users = Map(1 -> User(1, "John", "Doe", 32, Some("male")),
2 -> User(2, "Johanna", "Doe", 30, None))
def findById(id: Int): Option[User] = users.get(id)
def findAll = users.values
}
现在,假设从 UserRepository 接收到一个 Option[User] 实例,并需要拿它做点什么,该怎么办呢?
一个办法就是通过 isDefined 方法来检查它是否有值。 如果有,你就可以用 get 方法来获取该值:
val user1 = UserRepository.findById(1)
if (user1.isDefined) {
println(user1.get.firstName)
} // will print "John"
这和 Guava 库 中的 Optional 使用方法类似。 不过这种使用方式太过笨重,更重要的是,使用 get 之前, 你可能会忘记用 isDefined 做检查,这会导致运行期出现异常。 这样一来,相对于 null ,使用 Option 并没有什么优势。
你应该尽可能远离这种访问方式!
提供一个默认值
很多时候,在值不存在时,需要进行回退,或者提供一个默认值。 Scala 为 Option 提供了 getOrElse 方法,以应对这种情况:
val user = User(2, "Johanna", "Doe", 30, None)
println("Gender: " + user.gender.getOrElse("not specified")) // will print "not specified"
请注意,作为 getOrElse 参数的默认值是一个 传名参数 , 这意味着,只有当这个 Option 确实是 None 时,传名参数才会被求值。 因此,没必要担心创建默认值的代价,它只有在需要时才会发生。
模式匹配
Some 是一个样例类,可以出现在模式匹配表达式或者其他允许模式出现的地方。 上面的例子可以用模式匹配来重写:
val user = User(2, "Johanna", "Doe", 30, None)
user.gender match {
case Some(gender) => println("Gender: " + gender)
case None => println("Gender: not specified")
}
或者,你想删除重复的 println 语句,并重点突出模式匹配表达式的使用:
val user = User(2, "Johanna", "Doe", 30, None)
val gender = user.gender match {
case Some(gender) => gender
case None => "not specified"
}
println("Gender: " + gender)
你可能已经发现用模式匹配处理 Option 实例是非常啰嗦的,这也是它非惯用法的原因。 所以,即使你很喜欢模式匹配,也尽量用其他方法吧。
不过在 Option 上使用模式确实是有一个相当优雅的方式, 在下面的 for 语句一节中,你就会学到。
作为集合的 Option
到目前为止,你还没有看见过优雅使用 Option 的方式吧。下面这个就是了。
前文我提到过, Option 是类型 A 的容器,更确切地说,你可以把它看作是某种集合, 这个特殊的集合要么只包含一个元素,要么就什么元素都没有。
虽然在类型层次上, Option 并不是 Scala 的集合类型, 但,凡是你觉得 Scala 集合好用的方法, Option 也有, 你甚至可以将其转换成一个集合,比如说 List 。
那么这又能让你做什么呢?
执行一个副作用
如果想在 Option 值存在的时候执行某个副作用,foreach 方法就派上用场了:
UserRepository.findById(2).foreach(user => println(user.firstName)) // prints "Johanna"
如果这个 Option 是一个 Some ,传递给 foreach 的函数就会被调用一次,且只有一次; 如果是 None ,那它就不会被调用。
执行映射
Option 表现的像集合,最棒的一点是, 你可以用它来进行函数式编程,就像处理列表、集合那样。
正如你可以将 List[A] 映射到 List[B] 一样,你也可以映射 Option[A] 到 Option[B]: 如果 Option[A] 实例是 Some[A] 类型,那映射结果就是 Some[B] 类型;否则,就是 None 。
如果将 Option 和 List 做对比 ,那 None 就相当于一个空列表: 当你映射一个空的 List[A] ,会得到一个空的 List[B] , 而映射一个是 None 的 Option[A] 时,得到的 Option[B] 也是 None 。
让我们得到一个可能不存在的用户的年龄:
val age = UserRepository.findById(1).map(_.age) // age is Some(32)
Option 与 flatMap
也可以在 gender 上做 map 操作:
val gender = UserRepository.findById(1).map(_.gender) // gender is an Option[Option[String]]
所生成的 gender 类型是 Option[Option[String]] 。这是为什么呢?
这样想:你有一个装有 User 的 Option 容器,在容器里面,你将 User 映射到 Option[String] ( User 类上的属性 gender 是 Option[String] 类型的)。 得到的必然是嵌套的 Option。
既然可以 flatMap 一个 List[List[A]] 到 List[B] , 也可以 flatMap 一个 Option[Option[A]] 到 Option[B] ,这没有任何问题: Option 提供了 flatMap 方法。
val gender1 = UserRepository.findById(1).flatMap(_.gender) // gender is Some("male")
val gender2 = UserRepository.findById(2).flatMap(_.gender) // gender is None
val gender3 = UserRepository.findById(3).flatMap(_.gender) // gender is None
现在结果就变成了 Option[String] 类型, 如果 user 和 gender 都有值,那结果就会是 Some 类型,反之,就得到一个 None 。
要理解这是什么原理,让我们看看当 flatMap 一个 List[List[A] 时,会发生什么? (要记得, Option 就像一个集合,比如列表)
val names: List[List[String]] =
List(List("John", "Johanna", "Daniel"), List(), List("Doe", "Westheide"))
names.map(_.map(_.toUpperCase))
// results in List(List("JOHN", "JOHANNA", "DANIEL"), List(), List("DOE", "WESTHEIDE"))
names.flatMap(_.map(_.toUpperCase))
// results in List("JOHN", "JOHANNA", "DANIEL", "DOE", "WESTHEIDE")
如果我们使用 flatMap ,内部列表中的所有元素会被转换成一个扁平的字符串列表。 显然,如果内部列表是空的,则不会有任何东西留下。
现在回到 Option 类型,如果映射一个由 Option 组成的列表呢?
val names: List[Option[String]] = List(Some("Johanna"), None, Some("Daniel"))
names.map(_.map(_.toUpperCase)) // List(Some("JOHANNA"), None, Some("DANIEL"))
names.flatMap(xs => xs.map(_.toUpperCase)) // List("JOHANNA", "DANIEL")
如果只是 map ,那结果类型还是 List[Option[String]] 。 而使用 flatMap 时,内部集合的元素就会被放到一个扁平的列表里: 任何一个 Some[String] 里的元素都会被解包,放入结果集中; 而原列表中的 None 值由于不包含任何元素,就直接被过滤出去了。
记住这一点,然后再去看看 faltMap 在 Option 身上做了什么。
过滤 Option
也可以像过滤列表那样过滤 Option: 如果选项包含有值,而且传递给 filter 的谓词函数返回真, filter 会返回 Some 实例。 否则(即选项没有值,或者谓词函数返回假值),返回值为 None 。
UserRepository.findById(1).filter(_.age > 30) // None, because age is <= 30
UserRepository.findById(2).filter(_.age > 30) // Some(user), because age is > 30
UserRepository.findById(3).filter(_.age > 30) // None, because user is already None
for 语句
现在,你已经知道 Option 可以被当作集合来看待,并且有 map 、 flatMap 、 filter 这样的方法。 可能你也在想 Option 是否能够用在 for 语句中,答案是肯定的。 而且,用 for 语句来处理 Option 是可读性最好的方式,尤其是当你有多个 map 、flatMap 、filter 调用的时候。 如果只是一个简单的 map 调用,那 for 语句可能有点繁琐。
假如我们想得到一个用户的性别,可以这样使用 for 语句:
for {
user <- UserRepository.findById(1)
gender <- user.gender
} yield gender // results in Some("male")
可能你已经知道,这样的 for 语句等同于嵌套的 flatMap 调用。 如果 UserRepository.findById 返回 None,或者 gender 是 None , 那这个 for 语句的结果就是 None 。 不过这个例子里, gender 含有值,所以返回结果是 Some 类型的。
如果我们想返回所有用户的性别(当然,如果用户设置了性别),可以遍历用户,yield 其性别:
for {
user <- UserRepository.findAll
gender <- user.gender
} yield gender
// result in List("male")
在生成器左侧使用
也许你还记得,前一章曾经提到过, for 语句中生成器的左侧也是一个模式。 这意味着也可以在 for 语句中使用包含选项的模式。
重写之前的例子:
for {
User(_, _, _, _, Some(gender)) <- UserRepository.findAll
} yield gender
在生成器左侧使用 Some 模式就可以在结果集中排除掉值为 None 的元素。
链接 Option
Option 还可以被链接使用,这有点像偏函数的链接: 在 Option 实例上调用 orElse 方法,并将另一个 Option 实例作为传名参数传递给它。 如果一个 Option 是 None , orElse 方法会返回传名参数的值,否则,就直接返回这个 Option。
一个很好的使用案例是资源查找:对多个不同的地方按优先级进行搜索。 下面的例子中,我们首先搜索 config 文件夹,并调用 orElse 方法,以传递备用目录:
case class Resource(content: String)
val resourceFromConfigDir: Option[Resource] = None
val resourceFromClasspath: Option[Resource] = Some(Resource("I was found on the classpath"))
val resource = resourceFromConfigDir orElse resourceFromClasspath
如果想链接多个选项,而不仅仅是两个,使用 orElse 会非常合适。 不过,如果只是想在值缺失的情况下提供一个默认值,那还是使用 getOrElse 吧。
小结
在这一章里,你学到了有关 Option 的所有知识, 这有利于你理解别人的代码,也有利于你写出更可读,更函数式的代码。
这一章最重要的一点是:列表、集合、映射、Option,以及之后你会见到的其他数据类型, 它们都有一个非常统一的使用方式,这种使用方式既强大又优雅。
Java8 Optional
身为一名Java程序员,大家可能都有这样的经历:调用一个方法得到了返回值却不能直接将返回值作为参数去调用别的方法。我们首先要判断这个返回值是否为null,只有在非空的前提下才能将其作为其他方法的参数。这正是一些类似Guava的外部API试图解决的问题。一些JVM编程语言比如Scala、Ceylon等已经将对在核心API中解决了这个问题。
Optional类的Javadoc描述如下:
这是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
今天的主角是 scala ,java8 的 optional 用法请参考
相关文档
sclal集合
Tuple 元组
元组是在不使用类的前提下,将元素组合起来形成简单的逻辑集合。
scala> val hostPort = ("localhost", 80)
hostPort: (String, Int) = (localhost,80)
与样本类不同,元组不能通过名称获取字段,而是使用位置下标来读取对象;而且这个下标基于1,而不是基于0。
scala> hostPort._1
res1: String = localhost
scala> hostPort._2
res2: Int = 80
元组可以很好得与模式匹配相结合。
hostPort match {
case ("localhost", port) => ...
case (host, port) => ...
}
在创建两个元素的元组时,可以使用特殊语法:->
scala> 1 -> 2
res3: (Int, Int) = (1,2)
常用函数的
map
map对列表中的每个元素应用一个函数,返回应用后的元素所组成的列表。
foreach
foreach很像map,但没有返回值。foreach仅用于有副作用[side-effects]的函数。
filter
filter移除任何对传入函数计算结果为false的元素。返回一个布尔值的函数通常被称为谓词函数[或判定函数]。
flatMap
flatMap是一种常用的组合子,结合映射[mapping]和扁平化[flattening]。 flatMap需要一个处理嵌套列表的函数,然后将结果串连起来。
Future
scala.concurrent 包里的 Future[T] 是一个容器类型,代表一种返回值类型为 T 的计算。 计算可能会出错,也可能会超时;从而,当一个 future 完成时,它可能会包含异常,而不是你期望的那个值。
Future 只能写一次: 当一个 future 完成后,它就不能再被改变了。 同时,Future 只提供了读取计算值的接口,写入计算值的任务交给了 Promise,这样,API 层面上会有一个清晰的界限。 这篇文章里,我们主要关注前者,下一章会介绍 Promise 的使用。
之前遇到的坑
- 酌情在 List 中使用 map 或 flatMap 等操作, map中是一个数据库查询或者是一个远程接口调用,立马会出现并发问题,不是数据库连接数超出,就是接口返回失败,Java8的 stream 就不会这么严重,想想为甚么?
- 隐式转换,这个真就需要经验了
- Option 不是说就不会出现空指针问题,尤其在充斥着不是 Option 的环境中,赋默认值永远不失为一个好的方法
- 编写隐式转换转 json 的时候,json 如果使用下划线格式则使用JsonNaming.snakecase(Json.format[ClassA]),如果使用驼峰的时候记得用Json.format[ClassB]
- 编写 case class的时候记得分好类别,放到对应的文件中,不要随意定义
- 注释一定要写,写scala代码简直不要太爽,经常忘记写注释,后人再看的时候是很反感的
- hyperloop 中充斥着没有打 log 的代码,这一点让人很不爽
- 代码记得格式化,括号对不整齐很影响心情
- 需要一个良好的分支管理策略,切记覆盖别人的代码
- 不一定要求你一定写单元测试,但要求一定要做到自测,这也是对测试人员的负责
参考文档: