《java in action》2的一些記錄

java迭代的趨勢

更好的并發(fā)

流的引入,lambda,一切都是在為了適應(yīng)現(xiàn)在的硬件架構(gòu): 多核,分布式網(wǎng)絡(luò)架構(gòu)。即期望給用戶提供“輕松”,“安全”的并發(fā)編程接口。 流處理,幾乎免費的并行,用戶在高層次寫邏輯代碼,具體的執(zhí)行由底層的lib來選擇最合適的執(zhí)行方式,比如把計算分布到不同的cpu核上。吸取函數(shù)式語言里高階函數(shù),增加了lambda表達式和函數(shù)Function,增強了java語言的抽象能力,表達能力,“把行為作為參數(shù)傳遞給函數(shù)”,即高階函數(shù)的能力,又稱為“行為參數(shù)化“(與此對應(yīng)的還有”類型參數(shù)化“,即泛型),自此函數(shù)也成為了java語言的一等公民,但是還有方法和類仍然是二等公民,不過提供了一些設(shè)施,將二等公民轉(zhuǎn)化為一等公民。
基于Stream的并發(fā),很少使用synchronized關(guān)鍵字,因為是不同的指導(dǎo)思想,stream的并發(fā)關(guān)注數(shù)據(jù)分塊 而不是 協(xié)調(diào)訪問 , 這樣就像函數(shù)式編程在靠齊,“無共享可變數(shù)據(jù)” + “高階函數(shù)” ,不使用synchronized,來協(xié)調(diào)共享數(shù)據(jù)的訪問(互斥與并發(fā)),而是將數(shù)據(jù)拆分,不共享。

image.png

流的與集合的一個差異是: 流用于表達計算,集合的元素是計算完之后添加進來或者刪除的,但是流是在固定的數(shù)據(jù)結(jié)構(gòu)上,不能直接刪除和增加,按需計算,定義流的時候計算并不發(fā)生,且只能遍歷一次生成新的流(Java里是這樣,scala里面的流可以多次使用)。
使用流的好處,是能寫出具有如下特點的代碼:

  • 聲明式:
  • 可復(fù)合:
    • 抽象度高
  • 免費并行:

流的定義是: 從支持數(shù)據(jù)處理的源生成特定的元素序列。定義決定設(shè)計


image.png

如果自己設(shè)計一條流,也應(yīng)該按照這樣的思路來。

所以,流的使用一般就是三件事:

  • 定義數(shù)據(jù)源
  • 定義中間操作鏈 形成一條流水線
  • 終端操作 執(zhí)行流水線(按需計算)生成結(jié)果

無狀態(tài)流有狀態(tài)流的區(qū)別
有:流內(nèi)部的算子有用戶提供的lambda, 或者 方法引用(方法不是純函數(shù))
無:沒有內(nèi)部狀態(tài),沒有用戶提供的lambda或者方法引用,沒有內(nèi)部可變狀態(tài)。
無狀態(tài)流對并行友好,無縫切換到parallel,而有狀態(tài)的流不行,比如求和,如果用外部變量進行累加,則parallel很容易出錯,但是如果是利用reduce的分開累加,最終將每個累加結(jié)果再累加,就不會有并發(fā)問題。

數(shù)值流存在的原因不是流的復(fù)雜性,而是 基本類型和對應(yīng)的對象類型 之間的裝箱和拆箱性能。

流的生成: 萬物皆可流

萬物都可以作為流的元素,也可以作為流的源頭生成流元素。

  • 數(shù)值
  • 集合
  • 文件
  • 空對象
  • 函數(shù) (比如無限流,Stream.iterate(0, n -> n + 2)偶數(shù)的無限流, Stream.iterate(new int[]{0,1}, t-> new int[]{t[1], t[0] + t[1]}) 斐波那契流, Stream.generate(Math::random) 隨機流

流收集:最終計算: 歸約

流水線是lazy的數(shù)據(jù)集的計算迭代器,最終的計算由 terminal action出發(fā),通用的操作即collect,collect接受一個參數(shù)Collector來表示最終的流元素去往何處。
Collectors工具類提供了許多直接的預(yù)定義的歸約器,也提供了一些高階方法生成歸約器,而這一切都離不開背后的基本歸約方法:java.util.stream.Collectors#reducing(U, java.util.function.Function<? super T,? extends U>, java.util.function.BinaryOperator<U>)
U 歸約的初始元素
Function是將流內(nèi)元素轉(zhuǎn)化為待歸約的元素
BinaryOperator是待歸約元素的計算

歸約計算的一個目的,收集,也可由歸約完成。這就涉及到范疇論的理論來,以數(shù)組的收集舉例:

reducing(new List<>(),
                    (l, e) -> { l.add(e); return l;}, 
                  (l1, l2) -> { List l = new List(); 
                                    l.addAll(l1);
                                    l.addAll(l2); 
                                    return l;} }

并且上面的歸約屬于“無狀態(tài)”,可以輕松的用來做 并行。
reducing這個方法之所以能夠作為基本方法是它提供了兩個基本能力:

  1. 元素到范疇的映射
  2. 范疇到范疇的映射

代碼實際實現(xiàn)是Collector類,另外Stream提供了一個collect方法,接受三個參數(shù)- supplier, accumulator 和 combiner,來自定義收集,其語義和Collector接口相應(yīng)方法返回的函數(shù)完全相同。

分組: Collector的連接 : groupingBy( , [ toList toSet]), collectAndThen,

復(fù)雜的歸約,可以通過groupingBy以及partitioningBy完成,并且他們之間可以通過多個Collector的連接完成
如:

Map<Type, List<String>>  = 
   collect(groupingBy(Dish::getType,
                              mapping(Dish:getName, toList())));

或者多級分組

Map<Type, Map<CaloricLevel, List<Dish>>>  = 
.collect( groupingBy(Dish::getType,
                  groupingBy( dish -> {
                              if (dish.getCateGory <= 400 ) return CaloricLevel.DIET;
                              ......
                              })
                              ));

或者分組統(tǒng)計

Map<Dish.Type, Long>  = .collect( groupintBy(Dish::getType, counting() ));

或者分組統(tǒng)計后計算

Map<Dish.Type, Optional<Dish>> = .collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));

groupingBy(Dish::getType, collectAndThen( maxBy(comparingInt(Dish::getCalories)), Optional::get));

分區(qū) partitioningBy

將流的元素,利用一個謂詞做分類。分區(qū)可以理解為產(chǎn)生了兩個流,并且由 partitioningBy實現(xiàn)的Map實現(xiàn)(特殊map)更高效,緊湊,因為只包含兩個鍵: true 和 false

分區(qū)和分組一樣,可以多級分區(qū),只要 連接 partitioningBy 就可以

收集器性能對比

實現(xiàn)自定義的Collector的目的是為了獲得 比 ’使用內(nèi)置工廠方法創(chuàng)建的收集器‘ 更好的性能,但是也可能讓性能更拉垮,只能說:提供了設(shè)置,讓有能力的同學(xué)可以自由發(fā)揮。
JMH框架來測試。

并行

在Java8之前的并行流,需要主動的拆分數(shù)據(jù),分配給不同的線程,然后還要進行協(xié)調(diào)和同步以避免可能發(fā)生的競爭條件,等到每個線程都完成后,再合并結(jié)果。并且java7引入了一個 “分支/合并”的框架,可以更穩(wěn)定,更不容易出錯的完成這一件事。
不過,相對于java8的并行流還是落后了些,Java8的Stream接口讓用戶免費使用并行,一個指令就可以將順序流轉(zhuǎn)化為并行流(當然,前提是你的數(shù)據(jù)能夠接受并行處理),控制數(shù)據(jù)切分的過程主要是: Spliterator (splitable , iterator)

someStream.parallel() 方法并不會改變實際的流,而是設(shè)置了一個flag,表示 parallel之后的所有操作都是可以并行執(zhí)行的,指需要再調(diào)用 sequential 就可以再變回順序流。

并行流內(nèi)部使用的還是:ForJoinPool,默認的個數(shù)等于cpu的個數(shù)。

java.util.concurrent.ForkJoinPool. common.parallelism 來修改線程池大小, 缺點是jvm級別的,會修改所有并行流的線程池大小,所以一般不修改,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");

并行流的思想是:數(shù)據(jù)分塊,任務(wù)分塊,然后利用Fork/Joinpool, 實現(xiàn)任務(wù)的計算。
任務(wù)的分塊由Spliterator實現(xiàn)。

Fork/Join Pool框架

思想分治,將任務(wù)拆分,最后合并。

  • 可拆分的任務(wù)抽象ForkJoinTask<T> <= RecursiveTask<T>, 或者 ForkJoinTask<Void> <= RecursiveAction
    • 任務(wù)執(zhí)行: compute, 任務(wù)在compute內(nèi)部再次拆分,不能拆的計算然后返回結(jié)果,將之前的拆分都合并。
  • 執(zhí)行任務(wù)的框架: work stealing。由于任務(wù)拆分的子任務(wù)的計算負載不均衡,導(dǎo)致雖然每個線程的隊列都只有兩個任務(wù),但是其中一個隊列的任務(wù)特別簡單,瞬間完成,而另一個隊列的任務(wù)可能特別耗時,第二個任務(wù)就在等待第一個完成,不能并發(fā)。steal能夠讓空閑/或者負載低的線程偷取忙碌線程的雙端隊列的task來幫助執(zhí)行,這也是為什么 任務(wù) 要分細的原因。

流的拆分: Spliterator

參考:

interface Spliterator<T> {
  boolean tryAdvance(COnsumer< ? super T> action);

Spliterator<T> trySplit();

long estimateSize(); // 不是很準確

int characteristics();

}

trySplit會遞歸調(diào)用,調(diào)用的時候,它會劃分一些元素給它返回的第二個Spliterator,讓他們兩個并行處理。

遞歸的拆分,拆分的過程會影響整個的執(zhí)行效率,在自定義Spliterator的時候,注意效率,采用物理分割,比如將數(shù)據(jù)復(fù)制一份,顯然沒有邏輯分割保存原數(shù)據(jù)引用,修改數(shù)據(jù)范圍來的效率高。另外,拆分的過程收到了 “特性”影響(characteristics方法聲明)


image.png

如果當前的Spliterator實例X是可分割的,trySplit()方法會分割X產(chǎn)生一個全新的Spliterator實例Y,原來的X所包含的元素(范圍)也會收縮,類似于X = [a,b,c,d] => X = [a,b], Y = [c,d];如果當前的Spliterator實例X是不可分割的,此方法會返回NULL),具體的分割算法由實現(xiàn)類決定

todo: 對于無限流的并發(fā),是怎么split的?

集合與lambda帶來的高效編程和改變

集合工廠的增強,對于List,Set,map等,SomeCollection.of() 方法會生成 小規(guī)模的高性能的不可變集合類,即集合常量 ,雖然生成的數(shù)據(jù)結(jié)構(gòu)不可變,但是效率更高,就類似于: Arrays.asList() 方法一樣,生成的是視圖。
另外,對于大家常用集合類時候,用到的一些常用操作,都增加了相應(yīng)語義的方法:

  • removeIf: list 和set提供,可以刪除滿足條件的元素,而不會觸發(fā)ConcurrentModificationException,不然就要顯式的使用迭代器和迭代器的刪除方法;


    image.png

    正確的代碼如下


    image.png
  • replaceAll: 替換集合的元素,而不用新生成集合。

  • remove: 刪除map里面指定的 key和value 對

  • merge: 把兩個map對元素進行合并的邏輯

  • computeIfXXXX,compute:高效的計算和填充

ConcurrentHashMap

  • set視圖:一個可以時刻同步map的set視圖,xxx.keySet()方法,隨時在set里看到map的變化。
  • 基礎(chǔ)類型的歸約:使用對應(yīng)的基礎(chǔ)類型方法會更快,少去了裝箱拆箱的步驟 : reduceValuesToInt、reduce-KeysToLong

lambda重構(gòu)設(shè)計模式

新的語言特性常常讓現(xiàn)存的編程模式或設(shè)計黯然失色,對設(shè)計經(jīng)驗的總結(jié)陳稱為“設(shè)計模式”。
lambda之所以可以重構(gòu)設(shè)計模式,是因為:將行為參數(shù)化,傳遞給高階函數(shù)。而策略模式,模板模式的核心就是封裝了不同的行為; 觀察者模式,是在發(fā)生一些事件之后,觸發(fā)一些行為的執(zhí)行;責(zé)任鏈模式本質(zhì)是對數(shù)據(jù)多次的操作,完全可以用函數(shù)式編程的組合模式完成;工廠模式,也只是某種特定的函數(shù)罷了:Function<someParameter, SomeClassInstance>;

基于lambda的DSL todo

精讀此章節(jié)內(nèi)容后,最好搭配:
英文版本的: Domain-Specific Languages Martin的書,中文翻譯賊拉垮
DSLs in Action,有引進的話,先看中文看看
還有 antlr4的兩本,因為 martin的書是設(shè)計思想,指導(dǎo)原則,用的工具還是 antlr4 做解析。

Optional,時間 : 非常簡單,略

默認方法和模塊系統(tǒng)

默認方法

模塊系統(tǒng)

模塊系統(tǒng)比較復(fù)雜: 搭配:Nicolai Parlog《 The Java Module System 》

設(shè)計的高層次(軟件架構(gòu)層次)設(shè)計模式:
關(guān)注點分離(separation of concern,SoC)和信息隱藏(information hiding)

  • 關(guān)注點分離 推崇的是將:單體的計算機程序分解為一個個相互獨立的特性

采用關(guān)注點分離,可以將軟件的功能,作用等劃分到名為“模塊“的獨立組成部分中去,所以,模塊是具有“內(nèi)聚”特質(zhì)的一組代碼,它與其他模塊的代碼很少耦合;通過模塊組織類,可以清晰地描繪出應(yīng)用程序類與類之間的可見性關(guān)系。
Java的包機制并為從本質(zhì)上支持模塊化,它的粒度太粗。而模塊化的粒度更細,且控制檢查是編譯期的。帶來的好處就是:可以使得各項工作獨立開展,減少組件之間的依賴,便于團隊合作,有利于推動組建重用,系統(tǒng)整體的維護性更好。

  • 信息隱藏: 隱藏信息能夠減少局部變更對其他部分程序的影響,從而避免“變更傳遞”。

在低層次(代碼層次) 的表現(xiàn)就是封裝。雖然我們具有private,protected,public 關(guān)鍵字,但是就語言層面而言,Java 9 出現(xiàn)之前, 編譯器無法依據(jù)語言結(jié)構(gòu)判斷某個類或者包僅供某個特定目標訪問。

Java9之前的Java內(nèi)置模塊化的問題:

  1. 有限的可見性控制:三個描述符只能控制包級別的類訪問,無法描述 包之間的訪問。比如:“希望一個包中的某個類或接口可以被另外一個包中的類或接口訪問,那么只能將它聲明為 public。這樣一來,任何人都可以訪問這些類和接口了”。這樣就可能讓代碼被隨意使用,給開發(fā)者演進自己的代碼帶來困難。
  2. 類路徑的問題:Java編譯器把所有的類都打入一個扁平的jar包中,并且把jar包放到class path上,jvm可以動態(tài)一句類的路徑從中定位并且加載相關(guān)的類。然后,這樣存在了幾個嚴重的問題:
  • 無法通過路徑指定版本。比如如果類路徑上存在同一個庫的兩個版本,會發(fā)生什么。而大型項目中很常見,它的不同組件使用同一個庫的不同版本:解決方法有
    1. 使用自定義ClassLoader來隔離: http://www.blogjava.net/landon/category/54860.html(值得注意的兩點是:公有接口可以由系統(tǒng)類加載器加載,舊的類的實例和class很難被卸載)。比如: elasticsearch中的插件加載機制,實現(xiàn)了自定義的classLoader 。 甚至可以用ClassLoader來實現(xiàn)熱部署:https://cloud.tencent.com/developer/article/1915650
    2. 或者是OSGI
    3. sofa-ark是動態(tài)熱部署和類隔離框架,支付寶開源
  • 類路徑不支持顯式的依賴:


    image.png

Java9提供了一個新的單位: 模塊。通過 module聲明,緊接著的是模塊的名字和主體的內(nèi)容,定義在特殊的文件:module-info.class, 下圖可知,module的層級是高于package的。

image.png

image.png

使用命令可以指導(dǎo)哪些目錄和類文件會被打包進入生成的JAR文件中:
javac module-info.java xxx/yyyy/zzz.java -d target
jar cvfe xxxxx.jar xxx.yyy.zzz -C target
然后運行執(zhí)行命令:
java --module-path xxxxx.jar --module moduleName.xxx.yyy.zzz

image.png

并發(fā)性

無論是基于 Future 的異步 API 還是反應(yīng)式異步 API,被調(diào)方法的概念體(conceptual body) 都在另一個線程中執(zhí)行,調(diào)用方很可能已經(jīng)退出了執(zhí)行,不在調(diào)用異常處理器的作用域內(nèi)。很明顯,這種非常規(guī)行為觸發(fā)的異常需要通過其他的動作來處理。

CompletableFuture

和Scala的Future很類似

反應(yīng)式編程

直接看《反應(yīng)式設(shè)計模式》就好了

函數(shù)式

個人了解較多,忽略。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容