Java 8 實戰 P4 Beyond Java 8

Chapter 13. Thinking functionally

13.1 實現和維護系統

有synchronized關鍵字的不要維護
容易使用的程序算法

  • Stream的無狀態的行爲(函數不會因爲須要等待從另外一個方法中讀取變量,或者因爲須要寫入的變量同時有另外一個方法正在寫入,而發生中斷)讓咱們
  • 最好類的 結構應該反映出系統的結構
  • 提供指標對結構的合理性進行評估,好比耦合性(軟件系統中各組件之間是否相互獨立)以及內聚性(系統的各相關部分之間如何協做)

不過對於平常事務,最關心的是代碼維護時的調試:代碼遭遇一些沒法預期的值就有可能發生崩潰。這些沒法預知的變量都源於共享的數據結構被你所維護的代碼中的多個方法讀取和更新。數據庫

對此,函數式編程提出的「無反作用」以及「不變性」編程

無反作用
函數:若是一個方法既不修改它內嵌類的狀態,也不修改其餘對象的狀態,使用return返回全部的計算結果,那麼咱們稱其爲無反作用的。函數若是拋出異常,I/O和對類中的數據進行任何修改(除構造器內的初始化)都是有反作用的。設計模式

  • 變量:final型

聲明式編程
通常經過編程實現一個系統,有兩種思考方式。一種專一於如何實現,另外一種方式則更加關注要作什麼。
前一種爲經典的面向對象編程,命令式;後一種爲內部迭代,聲明式。
第二種方式編寫的代碼更加接近問題陳述。緩存

函數式編程實現了上述的兩種思想:使用不相互影響的表達式,描述想要作什麼(由系統來選擇如何實現)。安全

13.2 函數式編程

1.函數式Java編程
Java語言沒法實現純粹函數式(徹底無反作用)的程序,只能接近(反作用不會被察覺)。
這種函數只能修改局部變量,它的引用對象(參數及其餘外部引用)都是不可修改對象(複製後再使用非函數式行爲,如add)。除此以外,不拋異常(用Optional,或者局部拋異常),不進行I/O。數據結構

2.引用透明性(上面規定的隱含)
一個函數只要傳遞一樣的參數值,它老是返回一樣(==)的結果。閉包

3.例子app

//給定一個List<value>,返回其子集,類型爲List<List<Integer>>,下面是總體算法,下下面是函數式的實踐
static List<List<Integer>> subsets(List<Integer> list)
    if (list.isEmpty()) {
        List<List<Integer>> ans = new ArrayList<>();
        ans.add(Collections.emptyList());
        return ans;
    }
    Integer first = list.get(0);
    List<Integer> rest = list.subList(1,list.size());
    List<List<Integer>> subans = subsets(rest);
    List<List<Integer>> subans2 = insertAll(first, subans);
    return concat(subans, subans2);

//    
static List<List<Integer>> insertAll(Integer first,
       List<List<Integer>> lists) {
    List<List<Integer>> result = new ArrayList<>();
    for (List<Integer> list : lists) {
        List<Integer> copyList = new ArrayList<>();//複製新list,而不是直接用參數調用.add
        copyList.add(first);
        copyList.addAll(list);
        result.add(copyList);
    }
    return result;
}
//下面方式相同
static List<List<Integer>> concat(List<List<Integer>> a,
    List<List<Integer>> b) {
        List<List<Integer>> r = new ArrayList<>(a);
        r.addAll(b);
        return r;
}

13.3 遞歸和迭代

將加強for改成迭代器方式沒有反作用?

Iterator<Apple> it = apples.iterator();
    while (it.hasNext()) {
       Apple apple = it.next();
        // ... 
}

利用遞歸而非迭代來消除沒步都需更新迭代變量(可是使用迭代在Java效率一般更差),如階乘:

//下面代碼除效率問題,還有StackOverflowError風險
static long factorialRecursive(long n) {
        return n == 1 ? 1 : n * factorialRecursive(n-1);
}

//尾迭代能解決StackOverflowError問題。每次調用函數時,把新的結果傳入函數。遺憾Java目前還不支持這種優化,Scala能夠。
static long factorialTailRecursive(long n) {
        return factorialHelper(1, n);
}

static long factorialHelper(long acc, long n) {
    return n == 1 ? acc : factorialHelper(acc * n, n-1);
}

//Stream更簡單
static long factorialStreams(long n){
        return LongStream.rangeClosed(1, n)
                         .reduce(1, (long a, long b) -> a * b);
}

總結:儘可能使用Stream取代迭代操做,從而避免變化帶來的影響。此外,若是遞歸能不帶任何反作用地讓你以更精煉的方式實現算法,你就應該用遞歸替換迭代,由於它更加易於閱讀、實現和理解。大多數時候編程的效率要比細微的執行效率差別重要得多。

Chapter 14. Functional programming techniques

函數式語言更普遍的含義是:函數能夠做參數、返回值,還能存儲。
1.高階函數
接受至少一個函數作參數,返回結果是一個函數

接收的做爲參數的函數可能帶來的反作用以文檔的方式記錄下來,最理想的狀況下,接收的函數參數應該沒有任何反作用。
2.科裏化
一種將具有n個參數(好比,x和y)的函數f轉化爲使用m(m < n)個參數的函數g,而且這個函數的返回值也是一個函數,它會做爲新函數的一個參數。後者的返回值和初始函數的 返回值相同。

//將下面函數科裏化,即預設各類f和b的組合,須要使用時只需調用相應的函數加上確實的x。
static double converter(double x, double f, double b) {
    return x * f + b;
}

//建立高階函數
static DoubleUnaryOperator curriedConverter(double f, double b){ 
    return (double x) -> x * f + b;
}
//其中一種組合
DoubleUnaryOperator convertUSDtoGBP = curriedConverter(0.6, 0);
//使用
double gbp = convertUSDtoGBP.applyAsDouble(1000);

14.2 持久化數據結構

這裏指的不是數據庫中的持久化
鏈表例子(火車旅行)

//TrainJourney類(火車站)有兩個公有變量,price和onward(下一站),構造函數以下
public TrainJourney(int p, TrainJourney t) {
    price = p;
    onward = t; 
}

//link方法把兩個單向鏈表(一列火車站)連成一體。下面代碼在a的基礎上鍊接,這樣會破壞原來a的結構。若是a本來在其餘地方有應用,那麼那些地方也會受到影響。
static TrainJourney link(TrainJourney a, TrainJourney b){
    if (a==null) return b;
    TrainJourney t = a;
    while(t.onward != null){
        t = t.onward;
    }
    t.onward = b;
    return a; 
}

//函數式實現。下面的實現的結果是a的副本,後面接上b。因此要確保結果不被修改,不然b也會沒修改。這也包括下面的tree例子
static TrainJourney append(TrainJourney a, TrainJourney b){
    return a==null ? b : new TrainJourney(a.price, append(a.onward, b));
}

//函數式不免會有必定程度的複製,上面例子至少只複製了a,而不是在一個全新的list上鍊接a和b

樹例子(我的信息)

//節點信息,若是強制遵照函數式編程,能夠將下面變量聲明爲final
class Tree { 
    private String key;
    private int val;
    private Tree left, right;
    public Tree(String k, int v, Tree l, Tree r) {
        key = k; val = v; left = l; right = r;
  } 
} 

//函數式的節點更新,每次更新都會建立一個新tree,一般而言,若是樹的深度爲d,而且保持必定的平衡性,那麼這棵樹的節點總數是2^d
public static Tree fupdate(String k, int newval, Tree t) {
    return (t == null) ?
        new Tree(k, newval, null, null) :
            k.equals(t.key) ?
                new Tree(k, newval, t.left, t.right) :
            k.compareTo(t.key) < 0 ?
                new Tree(t.key, t.val, fupdate(k,newval, t.left), t.right) :
                new Tree(t.key, t.val, t.left, fupdate(k,newval, t.right));
}

?實現部分函數式(某些數據更新對某些用戶可見):

  • 典型方式:只要你使用非函數式代碼向樹中添加某種形式的數據結構,請馬上建立它的一份副本
  • 函數式:改動前,複製修改處以前的部分,而後接上剩餘部分

14.3 Stream 的延遲計算

有一個延遲列表的實現例子
若是延遲數據結構能讓程序設計更簡單,就儘可能使用它們。若是它們會帶來沒法接受的性能損失,就嘗試以更加傳統的方式從新實現它們。

14.4 模式匹配(Java暫未提供)

1.訪問者設計模式
一個式子簡化的代碼,如5+0變爲5,使用Expr.simplify。但一開始要對expr進行各類檢查,如expr的類型,不一樣類型有不一樣變量,當符合條件才返回結果。這個過程涉及instanceof和cast等操做,比較麻煩。
而訪問者設計模式能獲得必定的簡化,它須要建立一個單獨的類(SimplifyExprVisitor),這個類封裝了一個算法(下面的visit),能夠「訪問」某種數據 類型。

class BinOp extends Expr{
    String opname; 
    Expr left, right;
    
    public Expr accept(SimplifyExprVisitor v){
            return v.visit(this);
    } 
}

public class SimplifyExprVisitor {
    ...
    public Expr visit(BinOp e){
        if("+".equals(e.opname) && e.right instanceof Number && ...){
            return e.left;
        }
        return e;
    }
}

//Java 中模式的判斷標籤被限制在了某些基礎類型、枚舉類型、封裝基礎類型的類以及String類型。
//Scala的簡單實現
def simplifyExpression(expr: Expr): Expr = expr match {
    case BinOp("+", e, Number(0)) => e
    case BinOp("*", e, Number(1)) => e
    case BinOp("/", e, Number(1)) => e
    case _ => expr
}

14.5 雜項

1.緩存或記憶表(並不是函數式方案)

final Map<Range,Integer> numberOfNodes = new HashMap<>();
Integer computeNumberOfNodesUsingCache(Range range) {
    Integer result = numberOfNodes.get(range);
    if (result != null){
        return result;
    }
    result = computeNumberOfNodes(range);
    numberOfNodes.put(range, result);
    return result;
}

這段代碼雖然是透明的,但並非線程安全的(numberOfNodes可變)
2.結合器

Chapter 15. comparing Java 8 and Scala

下面默認先寫Scala,或只寫Scala

15.1 Scala 簡介

1.你好

//命令式
object Beer {//單例對象
  def main(args: Array[String]/*Java先類型後變量*/){//不須要void,一般非遞歸方法不須要寫返回類型;對象聲明中的方法是靜態的
    var n : Int = 2
    while( n <= 6 ){
      println(s"Hello ${n} bottles of beer")
      n += 1 
    }
  } 
}

//函數式
2 to 6 /*Int的to方法,接受Int,返回區間,即也能夠用2.to(6)。後面foreach理解相同*/foreach { n => println(s"Hello ${n} bottles of beer") }

一樣一切爲對象,但沒有基本類型之分
Scala中用匿名函數或閉包指代lambda

2.數據結構
Map:
val authorsToAge = Map("Raoul" -> 23)Java須要建立後put
val authors = List("Raoul", "Mario")

Scala中的集合默認都是持久化的:更新一個Scala集合會生成一個新的集合,這個新的集合和以前版本的集合共享大部分的內容,最終的結果是數據儘量地實現了持久化。因爲這一屬性,代碼的隱式數據依賴更少:人們對代碼中集合變動的困惑(好比在何處更新了集合,何時作的更新)也會更少。
val newNumbers = numbers + 8numbers爲Set,添加元素是建立一個新Set對象

Java的不可變(immutable)比不可修改(unmodifiable)更完全

val fileLines = Source.fromFile("data.txt").getLines.toList() 
val linesLongUpper
      = fileLines.filter(l => l.length() > 10)
                 .map(l => l.toUpperCase())
//另外一種表達,多加.par表示並行
fileLines.par filter (_.length() > 10) map(_.toUpperCase())

元組
val book = (2014, "Java 8 in Action", "Manning")可不一樣類型,任意長度(上限23)
Java須要本身建pair類,�且3個以上元素的pair比較麻煩

Stream
Scala中能夠訪問以前計算的值,能夠經過索引訪問,同時內存效率會變低。

Option
和Java很像

def getCarInsuranceName(person: Option[Person], minAge: Int) = 
    person.filter(_.getAge() >= minAge)
          .flatMap(_.getCar)
          .flatMap(_.getInsurance)
          .map(_.getName)
          .getOrElse("Unknown")

15.2 函數

Scala多了「可以讀寫非本地變量」和對科裏化的支持
1.一等函數

//filter的函數簽名
def filter[T](p: (T) => Boolean/*Java用函數式接口Predicate<T>或 者Function<T, Boolean>,Scala直接用函數描述符或名爲函數類型*/): List[T]//參數類型

//定義函數
def isShortTweet(tweet: String) : Boolean = tweet.length() < 20
//使用函數,tweets是List[String]
tweets.filter(isShortTweet).foreach(println)

2.匿名函數和閉包

//上面代碼的匿名方式以下(都是語法糖)
val isLongTweet : String => Boolean
    = (tweet : String) => tweet.length() > 60

isLongTweet("A very short tweet")

//Java的匿名方式
Function<String, Boolean> isLongTweet = (String s) -> s.length() > 60;

boolean long = isLongTweet.apply("A very short tweet");

//閉包
var count = 0
val inc = () => count+=1
inc()
println(count)
//Java
int count = 0;
Runnable inc = () -> count+=1;//會出錯,count必須爲final或效果爲final
inc.run();

3.科裏化
Java須要手工地切分函數,麻煩在於多參數狀況

//Java
static Function<Integer, Integer> multiplyCurry(int x) {
    return (Integer y) -> x * y;
}

Stream.of(1, 3, 5, 7)
      .map(multiplyCurry(2))
      .forEach(System.out::println);

//Scala
def multiplyCurry(x :Int)(y : Int) = x * y

val multiplyByTwo : Int => Int = multiplyCurry(2)
val r = multiplyByTwo(10)

15.3 類和trait

1.類
Scala中的getter和setter都是隱式實現的

class Student(var name: String, var id: Int)

val s = new Student("Raoul", 1)
println(s.name)//getter
s.id = 1337//setter

2.trait
與interface相似,有抽象方法、默認方法、接口多繼承。但trait還有抽象類的字段。Java支持行爲的多繼承,但還不支持對狀態的多繼承。
Scala能夠在類實例化時才決定trait

val b1 = new Box() with Sized

Chapter 16. Conclusions

Java8的發展體現了兩種趨勢:多核處理的需求(獨立CPU速度瓶頸)->並行計算;更簡潔地對抽象數據進行操做。 1.行爲參數化:Lambda和方法引用 2.對大量數據的處理Stream:能在一次遍歷中完成多種操做,並且按需計算。 並行處理中的重點:無反作用、Lambda、方法引用、內部迭代 3.CompletableFuture提供了像thenCompose、thenCombine、allOf這樣的操做,避免Future中的命令式編程 4.Optional:能顯式表示缺失值。正確使用可以發現數據缺失的緣由。還有一些與Stream相似的方法。 5.默認方法

相關文章
相關標籤/搜索