Scala vs Java8

這篇講義只講scala的簡單使用,目的是使各位新來的同事能夠首先看懂程序,因為 scala 有的語法對于之前使用習慣了 java 的人來說還是比較晦澀的。

原文地址
網上對 scala 的介紹很多,都說從 java 轉到 scala 很容易,我覺得說這句話的人90%都沒有認真寫過 scala 代碼。

Scala like Java ,scala 和 java 很像,我總結下來 scala = java ++ --, Scala 等于 Java 加加減減

scala 介紹與安裝

這個 ++ 體現在

1. Fp(Function programming) 函數編程<br/>
2. Future 對異步任務的處理<br/>
3. Option 規避 null 指針異常<br/>
4. Tuple 元組 返回自由的對象組合<br/>
5. 以及無處不在的模式匹配<br/>
6. Trait 混入<br>



1. Java8 則加入了 lamda & stream api & 函數接口

2. Java8 則提供了 completableFuture 工具類

3. Java8 則提供了與之類似的Optional 包裝類

4. Java8 目前還不支持,但可以自定義

5. Java8 不支持

6. Java8 接口支持默認方法

以上2、3、4 是對現有 java中類型的補充和擴展,如早在 java5中就有 Future 、FutureTask 等異步接口了

這個 - - 就體現在

  1. 語法的精簡,讓你可以寫更多簡單易懂的 one line code (一行代碼)

小結

Scala 類似于JAVA,設計初衷是實現可伸縮、融合面向對象編程特性與函式編程風格,可直譯、可編譯、靜態、運行于JVM之上、可與Java互操作的一門多范式的編程語言。

Scala 代碼會先翻譯成 Java 的 class 在執行

下面就講講這門多范式的編程語言!!!

函數編程的理解

啥叫函數編程? 寫幾個java8 的 stream 表達式就會函數編程了?

先理解啥叫函數,我類比 OOP 中的對象來說,對象是對事物的抽象,面向對象編程的特點就是抽象、繼承、封裝、多態等等
函數是對行為過程的抽象,函數編程的特點就是抽象、模式匹配、惰性求值、引用透明等等。函數其實說到底是一個集合到另一集合的映射,這對應的是數學中的概念。

函數有 N多個名字,在 java 的類中叫方法,在 scala 的類中叫函數,在 java 的方法參數中叫 lamda 這個 lamda 的類型叫函數接口,而這個 lamda 又有一個別名叫 匿名函數,在引用了外部環境變量的 lamda 中叫閉包 (js 中有這種叫法,不知道準不準),在 OC 中叫 block ,很像,像到我都不想去區分他們。

一些函數編程的特性

  1. 函數是第一公民: 函數可以傳遞,這也是語言支持高階函數的先決條件

  2. 引用透明:函數式編程的一個特點就是變量狀態不可變,無可變狀態也就造成了函數的引用透明的特性,函數的引用透明指的是函數沒有副作用。scala 還不算純函數式編程語言,所以它有的函數是可以有副作用的。有一種說法說變量是萬惡之源,因為不變的量就不用考慮多線程中線程間通信等問題了。

  3. 模式匹配:模式可能出現的一個地方就是 模式匹配表達式(pattern matching expression): 一個表達式 e ,后面跟著關鍵字 match 以及一個代碼塊,這個代碼塊包含了一些匹配樣例; 而樣例又包含了 case 關鍵字、模式、可選的 守衛分句(guard clause) ,以及最右邊的代碼塊; 如果模式匹配成功,這個代碼塊就會執行。 寫成代碼,看起來會是下面這種樣子:

e match {
  case Pattern1 => block1
  case Pattern2 if-clause => block2
  ...
}

很多特點自行百度,不在一一贅述。

基礎語法

一切從 Hello word 開始

<h1>Scala</h1>

object ScalaMain {
  def main(args: Array[String]): Unit = {
    val variable = "hello word"
    println(s"${variable}")
  }
}

<h1>Java8</h1>

public class JavaMain {
    public static void main(String[] args) {
        String variable = "hello word!!";
        System.out.println(variable);
    }
}

上述代碼片段刨去關閉花括號的空行之后一共就4行,我們逐一分析。

第一行代碼有點奇怪,object在這里表示單例類的聲明,即ScalaMian是一個類,這個類只有一個實例,這個單一實例會在需要時由scala創建。

第二行是main函數的函數聲明,def表示聲明函數,main是函數名,小括號中是函數的參數列表,這里只有一個參數args,args后面跟:和其數據類型,這里是String數組,這個函數沒有返回值。

第三行是給變量 variable 賦值,可見 scala 的變量聲明使用 val/var 變量的類型緊跟變量名中間用:隔開。

第四行代碼中調用了一個內置函數println,這個函數輸出了Hello World!字符串,這個語句后面沒有分號,在scala中分號不是必須的,只有在一行中輸入兩個語句時才需要用分號分隔。同時可以看到字符串可以向某些腳本語言如 groovy、shell 一樣使用$取值,這個用法叫做字符串插值。

運行上面程序會輸出經典的“Hello world!”。
</div>

你可能會有幾個疑惑的點?為什么聲明變量的時候用 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的有那些寫法上的不同。

<h3>定義scala的簡單類</h3>

  class Point (val x:Int, val y:Int)

注意用 val 聲明的屬相是沒有 set 方法的,因為 val 修飾的變量屬于常量不可以修改,所以其相當于 java 的類種沒有 set 方法。

上面一行代碼就是一個scala類的定義:

  1. 首先是關鍵字class
  2. 其后是類名 Point
  3. 類名之后的括號中是構造函數的參數列表,這里相當于定義了對象的兩個常量,其名稱分別為x,y,類型都是Int

<h3>翻譯成Java8</h3>

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那樣定義類。

<h3>帶函數的 scala 類</h3>

class Point (var x:Int, var y:Int){
    // 定義函數
    def add= x+y

}

<h3>翻譯成Java8</h3>

public class JavaPoint {
  //省略上面的變量定義和 set/get 方法
  public int add() {
      return x + y;
  }
}

繼承

<h3>帶繼承的 scala 類</h3>

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 還是這么干了
<h3>翻譯成Java8</h3>

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 相連標示一個整體,我們先來看一下

<h3>Scala Trait(特征)</h3>

trait Equal {
  def isEqual(x: Any): Boolean

  def isNotEqual(x: Any): Boolean ={ 
      !isEqual(x)
  }
}

clase A extends Equal with TraitB{
  ...
}

<h3>Java8 Interface(接口)</h3>

interface Equal {
    Boolean isEqual(Object x);

    default Boolean isNotEqual(Object x) {
        return !isEqual(x);
    }
}

class A implement Equal,InterfaceB{
  ...
}

java8 中的默認方法用 default 關鍵字聲明

特征構造順序

特征也可以有構造器,由字段的初始化和其他特征體中的語句構成。這些語句在任何混入該特征的對象在構造是都會被執行。
構造器的執行順序:

  • 調用超類的構造器;
  • 特征構造器在超類構造器之后、類構造器之前執行;
  • 特征由左到右被構造;
  • 每個特征當中,父特征先被構造;
  • 如果多個特征共有一個父特征,父特征不會被重復構造
  • 所有特征被構造完畢,子類被構造。
  • 構造器的順序是類的線性化的反向。線性化是描述某個類型的所有超類型的一種技術規格。

小結

  1. Scala Trait(特征) 相當于 Java 的接口,實際上它比接口還功能強大。
  2. 與接口不同的是,它還可以定義屬性和方法的實現。
  3. 一般情況下Scala的類只能夠繼承單一父類,但是如果是 Trait(特征) 的話就可以繼承多個,從結果來看就是實現了多重繼承

臭名昭著的菱形問題

java 從 c++ 除去了多重繼承,只允許單根繼承,這個直接解決了菱形問題,但如今 scala 和 java8 都允許接口可以有默認的實現法法了,之所以打破以前的設計在接口中增加具體的方法, 是為了既有的成千上萬的Java類庫的類增加新的功能, 且不必對這些類重新進行設計。 因此不可避免的的就要花精力來解決這個問題,C++中解決的辦法是虛擬基類,我么看看 java 和 scala 是如何處理的。

我們知道, 接口可以繼承接口, 類可以繼承類和實現接口。 一旦繼承的類和實現的接口中有相同簽名的方法, 會出現什么樣的狀況呢?

<h3>C++ 解決方法</h3>

  • 在C++中是通過虛基類virtual實現,并按照深度優先,從左到右的順序遍歷調用

<h3>Java8 解決方法</h3>

  • 類優先于接口。 如果一個子類繼承的父類和接口有相同的方法實現。 那么子類繼承父類的方法
  • 子類型中的方法優先于父類型中的方法。
  • 如果以上條件都不滿足, 則必須顯示覆蓋/實現其方法,或者聲明成abstract。

<h3>Scala解決方法</h3>

  • 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 表達式

for 主要用來處理循環的,一般應用在 list、seq 等數據類型或者用來解Future 等包裝類,這個后面會說

先學會定義一個列表
<h3>Scala 定義list</h3>

val list01= 1 to 10
val list02= 1 to 100

<h3>Java8 定義 list</h3>

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);
}

<h3>Scala for 表達式</h3>

//第一種:普通的 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 的值組成的集合.
 * ?  返回集合的類型與被遍歷的集合類型是一致的.
 */

<h3>Java8 for 語法糖</h3>

//第一種
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 的代碼,這一點讓人很不爽
  • 代碼記得格式化,括號對不整齊很影響心情
  • 需要一個良好的分支管理策略,切記覆蓋別人的代碼
  • 不一定要求你一定寫單元測試,但要求一定要做到自測,這也是對測試人員的負責

參考文檔:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容