真慘!連各大編程語言都擺起地攤了!

困難年年有,今年特別多。

公司要做一個新的網站,可預算有限,聽說為了生計,各大編程語言們都擺起了地攤兒,我決定去瞧瞧,看看能不能淘點做網站需要的東西。

選擇靜態web服務器

一進集市,這煙火氣就撲面而來,平時一個個端着架子的C++、Java、Python居然能放下身段,招呼叫賣,我還是頭一回見。

“老哥,需要來點什麼?”,C語言給我打起了招呼。

“我想要建個網站”,我回答到。

“那你可算來對地方了”,C語言攤主起身說到,“建網站總得需要一個Web服務器吧,你看這裏,apacheweb服務器,賣的可好了”

我搖了搖頭,“這個apache,之前有用過,是用的多進程模型,連接多了有些吃力啊?”

“老哥是行家啊,來看這一款我們最新推出的nginx服務器,採用epoll多路復用+事件驅動,性能強勁!上萬連接不在話下”,C語言攤主自豪的說到。

隨後攤主給我展示了這個nginx服務器的能力,果然不錯,我加入了購物車,繼續往前逛。

挑選web應用開發框架

沒走幾步來到 C# 的攤前。

“喲,老哥,你這是要做網站啊?”,C#攤主主動給我打起了招呼。

“你怎麼知道的?”,我好奇的問到。

“你這購物車裡不是裝了一個nginx嘛!既然做網站,可得試試我們家的.NET Framework哦,各種裝備,應有盡有。”,C#熱情的拉着我過去。

不過我還是拒絕了他:“實在不好意思,聽說你們家產品只能在Windows系統上面運行,不支持Linux,還是算了,我再看看別家”

C#攤主不肯放棄,“別呀,我們已經支持Linux了,您再看看,現在搞活動,免費送IIS服務器哦,你把那nginx退了吧,喂,再考慮一下啊·····”

不等他說完,我就溜走了,來到了Python的攤前。

Python攤主也看出了我要做網站,也推銷起他家的產品來。

“大哥,你做網站,肯定不想只做一個靜態的吧,來試試咱們家的Web框架做一個動態網站?咱Python家的產品,簡單、輕量又實惠。”,攤主熱情的說到。

“有哪些推薦的呢?”,我問到。

Python攤主指着攤位上的幾個產品說道:“有DjangoFlaskTornado這三款是現在主打拳頭產品,用了的都說好”

我正想蹲下仔細看看,背後傳來一個聲音:“這位大哥,擱這選Web開發框架吶?快來我這邊看看”

一邊說,一邊硬把我往後面拽。

來到他的攤位上,我一看原來是PHP攤主。

“咱PHP產品琳琅滿目,就是專門為做網站而生的,現在做活動,跳樓價只要9.9,錯過不再有!”

這PHP攤主好生能說,一頓猛誇把我說的暈頭轉向,不知怎的竟然就加入了購物車。

繼續向前,來到了Java的攤位,一個好大的攤位,擺放的東西也是看的人眼花繚亂。

“你這個攤位不錯啊,又寬敞人流又多”

“可不是咋的,剛為了搶這個攤位,跟PHP那傢伙還幹了一架呢。”,Java攤主笑着說到。

看到我購物車裡的東西,Java攤主也開始推銷起來:“大哥,這年頭怎麼還用PHP那傢伙的東西,趕緊去退了吧,咱Java攤里的東西都是大品牌,質量有保障!”

“這,不太好吧,這PHP也是大品牌啊”

Java攤主搖了搖頭,“他一個腳本語言怎麼跟我們比啊?大哥你看,我們有Spring、SpringMVC、SpringBoot、SpringCloud等等明星產品,用戶眾多,售後工作也到位。而且現在搞活動可以送tomcat服務器,你要是用戶量不多都可以把nginx退掉,省一筆錢。”

“看起來很厲害的樣子呢,我考慮一下”,我打算再去別的地方看看比較一下。

Java攤主一把拉住了我,“大哥,不說了,咱今天碰到是緣分,你做網站有很多服務是吧,得用到RPC吧,你今天下單,我再送你一套netty框架,又能幫你省一筆了”

Java攤主盛情難卻,我一時興起,買下了好幾個,購物車都裝滿了一大半了。

挑選數據庫

剛付完錢準備離開,背後又傳來一個聲音:“大哥,做網站你得用數據庫吧,快來這看看”

我尋聲望去,原來是 C++ 攤主在叫我。

“來看看我的MySQL數據庫,做網站必備!”

我看了一下產品說明書,感覺還不錯,看了下錢包,剛才在Java攤主那裡花費不少,有些囊中羞澀了,問到:“能不能優惠一點”

C++攤主一聽,臉上的笑容少了一半,“如果你選個MongoDB組個套餐,可以給你8折優惠”

“MongoDB?我要這個幹嘛”

攤主一聽來了勁頭,開始滔滔不絕:“有些數據啊他不適合存在數據庫里,比如文檔啊,JSON啊,這些東西你要用數據庫存儲,增加字段和查詢,可麻煩了,你用MongoDB就方便都多了······”

被他說了一通,感覺是得要個這個玩意兒。

攤主見我有些心動,又繼續推銷:“大哥看來真是行家,您做網站是不是有圖片音頻視頻需要存儲,我這裏還有一個對象存儲(OSS)系統CEPH,你看看要不要也一併帶上,我還是給您八折,怎麼樣?”

“實在不好意思,我這預算有些吃緊了,這個就算了吧”,我婉拒到。

“哎哎大哥您往這瞧,咱家也有對象存儲minio,現在市場推廣期,免費送了!”,旁邊的Golang攤主招呼了起來。

居然有免費這好事,我倒是想去看看。

C++攤主見狀小聲說到:“免費的你敢用,出了問題都找不到人,還是看看我的吧,直接給你六折,怎麼樣?”

我一想也是,正想下單買下,背後傳來一聲“且慢”!

我回頭一看,原來是剛才的Java攤主,“大哥,咱Java家的ElasticSearch也考慮一下唄。”

我回到Java攤主這邊,問到:“這又是個什麼?我需要用到嗎?”

Java攤主也開始給我掰扯起來:“咱家的ElasticSearch那可是搜索行家,你網站內容多了是不是需要個搜索功能啊,咱家的這個ES,全文搜索不在話下,秒級響應,做網站必備啊。看你是回頭客,給你九折!”

我正想做一個搜索功能,看來這個也是必不可少,也一起拿下了。

緩存服務器

我推着購物車準備回家了,今天真是滿載而歸。

來到集市出口,又碰到了一開始的C語言攤主,攤主一瞧揮着手喊道:“大哥,你還差個內存緩存系統,過來看看,Redis搞活動呢!哎,別走啊,Memcached虧本處理了,過來看看啊”

我一摸錢包,完蛋,嚴重超支了!我加快了步伐,匆忙離開······

彩蛋

看着我採購回來的一堆東西,老闆是氣不打一處來。

“咱們就做一個內網論壇,全公司不過100號人,你給我搞這麼多,幾個意思?”

“老闆,您聽我解釋···”

“解釋個啥,明天不用來了”

哦豁,丟了飯碗,我也去擺地攤了···

往期熱門回顧

因為一個跨域請求,我差點丟了飯碗

一個神秘URL釀大禍,差點讓我背鍋!

就為了一個原子操作,其他CPU核心都罷工了

完了!CPU一味求快出事兒了!

可怕!CPU竟成了黑客的幫凶!

哈希表哪家強?幾大編程語言吵起來了!

震撼!全網第一張源碼分析全景圖揭秘Nginx

一網打盡!每個程序猿都該了解的黑客技術大匯總

DDoS攻擊:無限戰爭

一個Java對象的回憶錄:垃圾回收

誰動了你的HTTPS流量?

路由器里的廣告秘密

一個HTTP數據包的奇幻之旅

我是一個流氓軟件線程

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

網頁設計最專業,超強功能平台可客製化

8000字長文讓你徹底了解 Java 8 的 Lambda、函數式接口、Stream 用法和原理

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!
文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。公眾號回復『666』獲取高清大圖。

就在今年 Java 25周歲了,可能比在座的各位中的一些少年年齡還大,但令人遺憾的是,竟然沒有我大,不禁感嘆,Java 還是太小了。(難道我會說是因為我老了?)

而就在上個月,Java 15 的試驗版悄悄發布了,但是在 Java 界一直有個神秘現象,那就是「你發你發任你發,我的最愛 Java 8」.

據 Snyk 和 The Java Magazine 聯合推出發布的 2020 JVM 生態調查報告显示,在所有的 Java 版本中,仍然有 64% 的開發者使用 Java 8。另外一些開發者可能已經開始用 Java 9、Java 11、Java 13 了,當然還有一些神仙開發者還在堅持使用 JDK 1.6 和 1.7。

儘管 Java 8 發布多年,使用者眾多,可神奇的是竟然有很多同學沒有用過 Java 8 的新特性,比如 Lambda表達式、比如方法引用,再比如今天要說的 Stream。其實 Stream 就是以 Lambda 和方法引用為基礎,封裝的簡單易用、函數式風格的 API。

Java 8 是在 2014 年發布的,實話說,風箏我也是在 Java 8 發布后很長一段時間才用的 Stream,因為 Java 8 發布的時候我還在 C# 的世界中掙扎,而使用 Lambda 表達式卻很早了,因為 Python 中用 Lambda 很方便,沒錯,我寫 Python 的時間要比 Java 的時間還長。

要講 Stream ,那就不得不先說一下它的左膀右臂 Lambda 和方法引用,你用的 Stream API 其實就是函數式的編程風格,其中的「函數」就是方法引用,「式」就是 Lambda 表達式。

Lambda 表達式

Lambda 表達式是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應於其中的lambda抽象,是一個匿名函數,即沒有函數名的函數。Lambda表達式可以表示閉包。

在 Java 中,Lambda 表達式的格式是像下面這樣

// 無參數,無返回值
() -> log.info("Lambda")

 // 有參數,有返回值
(int a, int b) -> { a+b }

其等價於

log.info("Lambda");

private int plus(int a, int b){
  	return a+b;
}

最常見的一個例子就是新建線程,有時候為了省事,會用下面的方法創建並啟動一個線程,這是匿名內部類的寫法,new Thread需要一個 implements 自Runnable類型的對象實例作為參數,比較好的方式是創建一個新類,這個類 implements Runnable,然後 new 出這個新類的實例作為參數傳給 Thread。而匿名內部類不用找對象接收,直接當做參數。

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("快速新建並啟動一個線程");
    }
}).run();

但是這樣寫是不是感覺看上去很亂、很土,而這時候,換上 Lambda 表達式就是另外一種感覺了。

new Thread(()->{
    System.out.println("快速新建並啟動一個線程");
}).run();

怎麼樣,這樣一改,瞬間感覺清新脫俗了不少,簡潔優雅了不少。

Lambda 表達式簡化了匿名內部類的形式,可以達到同樣的效果,但是 Lambda 要優雅的多。雖然最終達到的目的是一樣的,但其實內部的實現原理卻不相同。

匿名內部類在編譯之後會創建一個新的匿名內部類出來,而 Lambda 是調用 JVM invokedynamic指令實現的,並不會產生新類。

方法引用

方法引用的出現,使得我們可以將一個方法賦給一個變量或者作為參數傳遞給另外一個方法。::雙冒號作為方法引用的符號,比如下面這兩行語句,引用 Integer類的 parseInt方法。

Function<String, Integer> s = Integer::parseInt;
Integer i = s.apply("10");

或者下面這兩行,引用 Integer類的 compare方法。

Comparator<Integer> comparator = Integer::compare;
int result = comparator.compare(100,10);

再比如,下面這兩行代碼,同樣是引用 Integer類的 compare方法,但是返回類型卻不一樣,但卻都能正常執行,並正確返回。

IntBinaryOperator intBinaryOperator = Integer::compare;
int result = intBinaryOperator.applyAsInt(10,100);

相信有的同學看到這裏恐怕是下面這個狀態,完全不可理喻嗎,也太隨便了吧,返回給誰都能接盤。

先別激動,來來來,現在咱們就來解惑,解除蒙圈臉。

Q:什麼樣的方法可以被引用?

A:這麼說吧,任何你有辦法訪問到的方法都可以被引用。

Q:返回值到底是什麼類型?

A:這就問到點兒上了,上面又是 Function、又是Comparator、又是 IntBinaryOperator的,看上去好像沒有規律,其實不然。

返回的類型是 Java 8 專門定義的函數式接口,這類接口用 @FunctionalInterface 註解。

比如 Function這個函數式接口的定義如下:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

還有很關鍵的一點,你的引用方法的參數個數、類型,返回值類型要和函數式接口中的方法聲明一一對應才行。

比如 Integer.parseInt方法定義如下:

public static int parseInt(String s) throws NumberFormatException {
    return parseInt(s,10);
}

首先parseInt方法的參數個數是 1 個,而 Function中的 apply方法參數個數也是 1 個,參數個數對應上了,再來,apply方法的參數類型和返回類型是泛型類型,所以肯定能和 parseInt方法對應上。

這樣一來,就可以正確的接收Integer::parseInt的方法引用,並可以調用Funcitonapply方法,這時候,調用到的其實就是對應的 Integer.parseInt方法了。

用這套標準套到 Integer::compare方法上,就不難理解為什麼即可以用 Comparator<Integer>接收,又可以用 IntBinaryOperator接收了,而且調用它們各自的方法都能正確的返回結果。

Integer.compare方法定義如下:

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

返回值類型 int,兩個參數,並且參數類型都是 int

然後來看ComparatorIntBinaryOperator它們兩個的函數式接口定義和其中對應的方法:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}

對不對,都能正確的匹配上,所以前面示例中用這兩個函數式接口都能正常接收。其實不止這兩個,只要是在某個函數式接口中聲明了這樣的方法:兩個參數,參數類型是 int或者泛型,並且返回值是 int或者泛型的,都可以完美接收。

JDK 中定義了很多函數式接口,主要在 java.util.function包下,還有 java.util.Comparator 專門用作定製比較器。另外,前面說的 Runnable也是一個函數式接口。

自己動手實現一個例子

1. 定義一個函數式接口,並添加一個方法

定義了名稱為 KiteFunction 的函數式接口,使用 @FunctionalInterface註解,然後聲明了具有兩個參數的方法 run,都是泛型類型,返回結果也是泛型。

還有一點很重要,函數式接口中只能聲明一個可被實現的方法,你不能聲明了一個 run方法,又聲明一個 start方法,到時候編譯器就不知道用哪個接收了。而用default 關鍵字修飾的方法則沒有影響。

@FunctionalInterface
public interface KiteFunction<T, R, S> {

    /**
     * 定義一個雙參數的方法
     * @param t
     * @param s
     * @return
     */
    R run(T t,S s);
}

2. 定義一個與 KiteFunction 中 run 方法對應的方法

在 FunctionTest 類中定義了方法 DateFormat,一個將 LocalDateTime類型格式化為字符串類型的方法。

public class FunctionTest {
    public static String DateFormat(LocalDateTime dateTime, String partten) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
        return dateTime.format(dateTimeFormatter);
    }
}

3.用方法引用的方式調用

正常情況下我們直接使用 FunctionTest.DateFormat()就可以了。

而用函數式方式,是這樣的。

KiteFunction<LocalDateTime,String,String> functionDateFormat = FunctionTest::DateFormat;
String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");

而其實我可以不專門在外面定義 DateFormat這個方法,而是像下面這樣,使用匿名內部類。

public static void main(String[] args) throws Exception {
  
    String dateString = new KiteFunction<LocalDateTime, String, String>() {
        @Override
        public String run(LocalDateTime localDateTime, String s) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s);
            return localDateTime.format(dateTimeFormatter);
        }
    }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
    System.out.println(dateString);
}

前面第一個 Runnable的例子也提到了,這樣的匿名內部類可以用 Lambda 表達式的形式簡寫,簡寫后的代碼如下:

public static void main(String[] args) throws Exception {

        KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
            return dateTime.format(dateTimeFormatter);
        };
        String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
        System.out.println(dateString);
}

使用(LocalDateTime dateTime, String partten) -> { } 這樣的 Lambda 表達式直接返回方法引用。

Stream API

為了說一下 Stream API 的使用,可以說是大費周章啊,知其然,也要知其所以然嗎,追求技術的態度和姿勢要正確。

當然 Stream 也不只是 Lambda 表達式就厲害了,真正厲害的還是它的功能,Stream 是 Java 8 中集合數據處理的利器,很多本來複雜、需要寫很多代碼的方法,比如過濾、分組等操作,往往使用 Stream 就可以在一行代碼搞定,當然也因為 Stream 都是鏈式操作,一行代碼可能會調用好幾個方法。

Collection接口提供了 stream()方法,讓我們可以在一個集合方便的使用 Stream API 來進行各種操作。值得注意的是,我們執行的任何操作都不會對源集合造成影響,你可以同時在一個集合上提取出多個 stream 進行操作。

我們看 Stream 接口的定義,繼承自 BaseStream,機會所有的接口聲明都是接收方法引用類型的參數,比如 filter方法,接收了一個 Predicate類型的參數,它就是一個函數式接口,常用來作為條件比較、篩選、過濾用,JPA中也使用了這個函數式接口用來做查詢條件拼接。

public interface Stream<T> extends BaseStream<T, Stream<T>> {
  
  Stream<T> filter(Predicate<? super T> predicate);
  
  // 其他接口
}  

下面就來看看 Stream 常用 API。

of

可接收一個泛型對象或可變成泛型集合,構造一個 Stream 對象。

private static void createStream(){
    Stream<String> stringStream = Stream.of("a","b","c");
}

empty

創建一個空的 Stream 對象。

concat

連接兩個 Stream ,不改變其中任何一個 Steam 對象,返回一個新的 Stream 對象。

private static void concatStream(){
    Stream<String> a = Stream.of("a","b","c");
    Stream<String> b = Stream.of("d","e");
    Stream<String> c = Stream.concat(a,b);
}

max

一般用於求数字集合中的最大值,或者按實體中数字類型的屬性比較,擁有最大值的那個實體。它接收一個 Comparator<T>,上面也舉到這個例子了,它是一個函數式接口類型,專門用作定義兩個對象之間的比較,例如下面這個方法使用了 Integer::compareTo這個方法引用。

private static void max(){
    Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
    Integer max = integerStream.max(Integer::compareTo).get();
    System.out.println(max);
}

當然,我們也可以自己定製一個 Comparator,順便複習一下 Lambda 表達式形式的方法引用。

private static void max(){
    Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
    Comparator<Integer> comparator =  (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1);
    Integer max = integerStream.max(comparator).get();
    System.out.println(max);
}

min

與 max 用法一樣,只不過是求最小值。

findFirst

獲取 Stream 中的第一個元素。

findAny

獲取 Stream 中的某個元素,如果是串行情況下,一般都會返回第一個元素,并行情況下就不一定了。

count

返回元素個數。

Stream<String> a = Stream.of("a", "b", "c");
long x = a.count();

peek

建立一個通道,在這個通道中對 Stream 的每個元素執行對應的操作,對應 Consumer<T>的函數式接口,這是一個消費者函數式接口,顧名思義,它是用來消費 Stream 元素的,比如下面這個方法,把每個元素轉換成對應的大寫字母並輸出。

private static void peek() {
    Stream<String> a = Stream.of("a", "b", "c");
    List<String> list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList());
}

forEach

和 peek 方法類似,都接收一個消費者函數式接口,可以對每個元素進行對應的操作,但是和 peek 不同的是,forEach 執行之後,這個 Stream 就真的被消費掉了,之後這個 Stream 流就沒有了,不可以再對它進行後續操作了,而 peek操作完之後,還是一個可操作的 Stream 對象。

正好藉著這個說一下,我們在使用 Stream API 的時候,都是一串鏈式操作,這是因為很多方法,比如接下來要說到的 filter方法等,返回值還是這個 Stream 類型的,也就是被當前方法處理過的 Stream 對象,所以 Stream API 仍然可以使用。

private static void forEach() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.forEach(e->System.out.println(e.toUpperCase()));
}

forEachOrdered

功能與 forEach是一樣的,不同的是,forEachOrdered是有順序保證的,也就是對 Stream 中元素按插入時的順序進行消費。為什麼這麼說呢,當開啟并行的時候,forEachforEachOrdered的效果就不一樣了。

Stream<String> a = Stream.of("a", "b", "c");
a.parallel().forEach(e->System.out.println(e.toUpperCase()));

當使用上面的代碼時,輸出的結果可能是 B、A、C 或者 A、C、B或者A、B、C,而使用下面的代碼,則每次都是 A、 B、C

Stream<String> a = Stream.of("a", "b", "c");
a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase()));

limit

獲取前 n 條數據,類似於 MySQL 的limit,只不過只能接收一個參數,就是數據條數。

private static void limit() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.limit(2).forEach(e->System.out.println(e));
}

上述代碼打印的結果是 a、b。

skip

跳過前 n 條數據,例如下面代碼,返回結果是 c。

private static void skip() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.skip(2).forEach(e->System.out.println(e));
}

distinct

元素去重,例如下面方法返回元素是 a、b、c,將重複的 b 只保留了一個。

private static void distinct() {
    Stream<String> a = Stream.of("a", "b", "c","b");
    a.distinct().forEach(e->System.out.println(e));
}

sorted

有兩個重載,一個無參數,另外一個有個 Comparator類型的參數。

無參類型的按照自然順序進行排序,只適合比較單純的元素,比如数字、字母等。

private static void sorted() {
    Stream<String> a = Stream.of("a", "c", "b");
    a.sorted().forEach(e->System.out.println(e));
}

有參數的需要自定義排序規則,例如下面這個方法,按照第二個字母的大小順序排序,最後輸出的結果是 a1、b3、c6。

private static void sortedWithComparator() {
    Stream<String> a = Stream.of("a1", "c6", "b3");
    a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))?1:-1).forEach(e->System.out.println(e));
}

為了更好的說明接下來的幾個 API ,我模擬了幾條項目中經常用到的類似數據,10條用戶信息。

private static List<User> getUserData() {
    Random random = new Random();
    List<User> users = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
        User user = new User();
        user.setUserId(i);
        user.setUserName(String.format("古時的風箏 %s 號", i));
        user.setAge(random.nextInt(100));
        user.setGender(i % 2);
        user.setPhone("18812021111");
        user.setAddress("無");
        users.add(user);
    }
    return users;
}

filter

用於條件篩選過濾,篩選出符合條件的數據。例如下面這個方法,篩選出性別為 0,年齡大於 50 的記錄。

private static void filter(){
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e));

    /**
     *等同於下面這種形式 匿名內部類
     */
//    stream.filter(new Predicate<User>() {
//        @Override
//        public boolean test(User user) {
//            return user.getGender().equals(0) && user.getAge()>50;
//        }
//    }).forEach(e->System.out.println(e));
}

map

map方法的接口方法聲明如下,接受一個 Function函數式接口,把它翻譯成映射最合適了,通過原始數據元素,映射出新的類型。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Function的聲明是這樣的,觀察 apply方法,接受一個 T 型參數,返回一個 R 型參數。用於將一個類型轉換成另外一個類型正合適,這也是 map的初衷所在,用於改變當前元素的類型,例如將 Integer 轉為 String類型,將 DAO 實體類型,轉換為 DTO 實例類型。

當然了,T 和 R 的類型也可以一樣,這樣的話,就和 peek方法沒什麼不同了。

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

例如下面這個方法,應該是業務系統的常用需求,將 User 轉換為 API 輸出的數據格式。

private static void map(){
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList());
}

private static UserDto dao2Dto(User user){
    UserDto dto = new UserDto();
    BeanUtils.copyProperties(user, dto);
    //其他額外處理
    return dto;
}

mapToInt

將元素轉換成 int 類型,在 map方法的基礎上進行封裝。

mapToLong

將元素轉換成 Long 類型,在 map方法的基礎上進行封裝。

mapToDouble

將元素轉換成 Double 類型,在 map方法的基礎上進行封裝。

flatMap

這是用在一些比較特別的場景下,當你的 Stream 是以下這幾種結構的時候,需要用到 flatMap方法,用於將原有二維結構扁平化。

  1. Stream<String[]>
  2. Stream<Set<String>>
  3. Stream<List<String>>

以上這三類結構,通過 flatMap方法,可以將結果轉化為 Stream<String>這種形式,方便之後的其他操作。

比如下面這個方法,將List<List<User>>扁平處理,然後再使用 map或其他方法進行操作。

private static void flatMap(){
    List<User> users = getUserData();
    List<User> users1 = getUserData();
    List<List<User>> userList = new ArrayList<>();
    userList.add(users);
    userList.add(users1);
    Stream<List<User>> stream = userList.stream();
    List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList());
}

flatMapToInt

用法參考 flatMap,將元素扁平為 int 類型,在 flatMap方法的基礎上進行封裝。

flatMapToLong

用法參考 flatMap,將元素扁平為 Long 類型,在 flatMap方法的基礎上進行封裝。

flatMapToDouble

用法參考 flatMap,將元素扁平為 Double 類型,在 flatMap方法的基礎上進行封裝。

collection

在進行了一系列操作之後,我們最終的結果大多數時候並不是為了獲取 Stream 類型的數據,而是要把結果變為 List、Map 這樣的常用數據結構,而 collection就是為了實現這個目的。

就拿 map 方法的那個例子說明,將對象類型進行轉換后,最終我們需要的結果集是一個 List<UserDto >類型的,使用 collect方法將 Stream 轉換為我們需要的類型。

下面是 collect接口方法的定義:

<R, A> R collect(Collector<? super T, A, R> collector);

下面這個例子演示了將一個簡單的 Integer Stream 過濾出大於 7 的值,然後轉換成 List<Integer>集合,用的是 Collectors.toList()這個收集器。

private static void collect(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList());
}

很多同學表示看不太懂這個 Collector是怎麼一個意思,來,我們看下面這段代碼,這是 collect的另一個重載方法,你可以理解為它的參數是按順序執行的,這樣就清楚了,這就是個 ArrayList 從創建到調用 addAll方法的一個過程。

private static void collect(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(ArrayList::new, ArrayList::add,
            ArrayList::addAll);
}

我們在自定義 Collector的時候其實也是這個邏輯,不過我們根本不用自定義, Collectors已經為我們提供了很多拿來即用的收集器。比如我們經常用到Collectors.toList()Collectors.toSet()Collectors.toMap()。另外還有比如Collectors.groupingBy()用來分組,比如下面這個例子,按照 userId 字段分組,返回以 userId 為key,List 為value 的 Map,或者返回每個 key 的個數。

// 返回 userId:List<User>
Map<String,List<User>> map = user.stream().collect(Collectors.groupingBy(User::getUserId));

// 返回 userId:每組個數
Map<String,Long> map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));

toArray

collection是返回列表、map 等,toArray是返回數組,有兩個重載,一個空參數,返回的是 Object[]

另一個接收一個 IntFunction<R>類型參數。

@FunctionalInterface
public interface IntFunction<R> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    R apply(int value);
}

比如像下面這樣使用,參數是 User[]::new也就是new 一個 User 數組,長度為最後的 Stream 長度。

private static void toArray() {
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new);
}

reduce

它的作用是每次計算的時候都用到上一次的計算結果,比如求和操作,前兩個數的和加上第三個數的和,再加上第四個數,一直加到最後一個數位置,最後返回結果,就是 reduce的工作過程。

private static void reduce(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    Integer sum = integerStream.reduce(0,(x,y)->x+y);
    System.out.println(sum);
}

另外 Collectors好多方法都用到了 reduce,比如 groupingByminBymaxBy等等。

并行 Stream

Stream 本質上來說就是用來做數據處理的,為了加快處理速度,Stream API 提供了并行處理 Stream 的方式。通過 users.parallelStream()或者users.stream().parallel() 的方式來創建并行 Stream 對象,支持的 API 和普通 Stream 幾乎是一致的。

并行 Stream 默認使用 ForkJoinPool線程池,當然也支持自定義,不過一般情況下沒有必要。ForkJoin 框架的分治策略與并行流處理正好契合。

雖然并行這個詞聽上去很厲害,但並不是所有情況使用并行流都是正確的,很多時候完全沒這個必要。

什麼情況下使用或不應使用并行流操作呢?

  1. 必須在多核 CPU 下才使用并行 Stream,聽上去好像是廢話。
  2. 在數據量不大的情況下使用普通串行 Stream 就可以了,使用并行 Stream 對性能影響不大。
  3. CPU 密集型計算適合使用并行 Stream,而 IO 密集型使用并行 Stream 反而會更慢。
  4. 雖然計算是并行的可能很快,但最後大多數時候還是要使用 collect合併的,如果合併代價很大,也不適合用并行 Stream。
  5. 有些操作,比如 limit、 findFirst、forEachOrdered 等依賴於元素順序的操作,都不適合用并行 Stream。

最後

Java 25 周歲了,有多少同學跟我一樣在用 Java 8,還有多少同學再用更早的版本,請說出你的故事。

壯士且慢,先給點個贊吧,總是被白嫖,身體吃不消!

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

大話計算機網絡一 聊聊UDP

引言

UDP是一個簡單的面向數據報運輸層協議

UDP不提供可靠性,它把應用程序傳給IP層得數據發送出去,不保證它們能達到目的地

UDP首部

端口號表示發送進程和接受進程

UDP長度字段指的是UDP首部和UDP數據的字節長度,該字段最小值為8字節

UDP長度是全長減去IP首部的長度

UDP檢驗和是一個端到端的檢驗和。它由發送端計算,然後由接收端驗證。其目的是為了發現UDP首部和數據在發送端到接收端之間發生的任何改動。

 

最大UDP數據報長度

理論上,IP數據報的最大長度是65535字節,這是由IP首部(圖3-1)16比特總長度字段所限制的。去除20字節的IP首部和8個字節的UDP首部,UDP數據報中用戶數據的最長長度為65507字節。但是,大多數實現所提供的長度比這個最大值小。

 

UDP校驗和

 

UDP和TCP在首部中都有覆蓋它們首部和數據的檢驗和。UDP的檢驗和是可選的,而TCP的檢驗和是必需的。

儘管UDP檢驗和的基本計算方法與我們在3.2節中描述的IP首部檢驗和計算方法相類似(16 bit字的二進制反碼和),但是它們之間存在不同的地方。首先,UDP數據報的長度可以為奇数字節,但是檢驗和算法是把若干個16 bit字相加。解決方法是必要時在最後增加填充字節0,這隻是為了檢驗和的計算(也就是說,可能增加的填充字節不被傳送)。

其次,UDP數據報和TCP段都包含一個12字節長的偽首部,它是為了計算檢驗和而設置的。偽首部包含IP首部一些字段。其目的是讓UDP兩次檢查數據是否已經正確到達目的地(例如,IP沒有接受地址不是本主機的數據報,以及IP沒有把應傳給另一高層的數據報傳給UDP)。UDP數據報中的偽首部格式如圖11-3所示。

 

在該圖中,我們特地舉了一個奇數長度的數據報例子,因而在計算檢驗和時需要加上填充字節。注意,UDP數據報的長度在檢驗和計算過程中出現兩次。

如果檢驗和的計算結果為0,則存入的值為全1(65535),這在二進制反碼計算中是等效的。如果傳送的檢驗和為0,說明發送端沒有計算檢驗和。

如果發送端沒有計算檢驗和而接收端檢測到檢驗和有差錯,那麼UDP數據報就要被悄悄地丟棄。不產生任何差錯報文(當IP層檢測到IP首部檢驗和有差錯時也這樣做)。

UDP檢驗和是一個端到端的檢驗和。它由發送端計算,然後由接收端驗證。其目的是為了發現UDP首部和數據在發送端到接收端之間發生的任何改動。

 

 

這個系列主要是對自己讀TCP/IP詳解 卷一 協議的筆記,推薦看完以後去閱讀一下這本又臭又厚的書

电子書的鏈接地址http://www.52im.net/topic-tcpipvol1.html

感謝這位站長的開源 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

飛機停飛郵輪不航行 氣象專家憂心:難發現「颱風熱點」

摘錄自2020年7月18、19日鏡週刊、自由時報報導

歐洲中程天氣預報中心表示,未來若所有航班都消失,預報準確率將會降低多達15%。為了準確預測天氣變化,氣象中心仰賴各種監測工具蒐集到的資訊來演算和預測,包括飛機、郵輪、衛星、浮標、氣象氣球、地面站和雷達。然而,近來受到疫情影響,從飛機和郵輪獲得的數據銳減,水面上的觀測也受到限制。

CNN 報導,蘭卡斯特大學生態中心(Lancaster University’s Environment Centre)研究發現,疫情之下各地航班密度降低,使得今年3月到5月的地面天氣預報準確性下降。接下來颱風季即將來臨,蘭卡斯特大學生態中心的研究員陳穎(Dr.Ying Chen)表示,若無法精準掌握氣溫,就無法即時發現颱風熱點。

陳穎也提到,在疫情之下不同地區所面臨到的天氣預報準確率降幅也大有不同,像是一些難以用既有設施觀測的地區,如格陵蘭和西伯利亞地區等,在航班減少的情況下,將會更加難以準確地進行天氣預測。

氣候變遷
國際新聞
美國
氣象預測
天氣監測
疫情下的食衣住行
颱風
氣象

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

比衣服纖維掉更多 研究:輪胎塑膠微粒隨風入海、傳送全球

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

馬來西亞登嘉樓立法緊急保護海龜蛋

摘錄自2020年7月19日法廣報導

在馬來西亞,臨海的登嘉樓州(Terengganu)頒布了一項新法律,禁止販賣海龜蛋。當地出名的原因之一,也是因為這裡可以品嘗得到瀕臨滅絕的珍稀動物海龜的蛋。儘管現在可以看到思維開始轉變,但動物保護工作依然複雜。

就在海龜保護機構九年來致力於搶救小海龜的同時,20公里外的Chukai市場上,海龜蛋被混在水果中,一起銷售。當地一名女商販指出,儘管這一做法沒有得到大家一致認同,但這種生意依然火紅。這名商販表示,海龜蛋的氣味在近距離真的很難聞,但它有益於防範AVC腦血管意外(中風),有人就是因此而購買,價格為2歐元三個。

登嘉樓州的新法對保護瀕臨滅絕物種是一個進步,但當局尚未公布對違法分子如何量刑。

生物多樣性
國際新聞
馬來西亞
海龜蛋

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

荷研發植物製塑膠瓶 一年內可生物分解

摘錄自2020年7月21日公視報導

荷蘭一間生化公司開發出以植物原料製造的塑膠瓶,使用完不僅可以回收,它還能在一年內自行分解。

荷蘭生化公司技術總監古魯特表示:「而且因為從隔離膜的角度來看,PEF(生質聚酯塑膠)性能確實很好,因為紙瓶的優勢來自於紙質製造,只需要一層薄薄的PEF即可實現阻隔(液體)的性能,能好好將內容物妥善保存一段時間。」

這種植物塑膠,是由玉米、小麥,和甜菜根作成,可以用來盛裝包括氣泡型的飲料,能大幅減少塑膠污染,跟市場對化石燃料的依賴。經過將瓶子放入淡水、鹽水、泥土跟沉澱物的實驗證明,這種塑膠經過堆肥處理後,一年內就可以完全分解,就算放在戶外,也只要幾年時間就能分解。

生化公司認為這些瓶子,可以回收再利用,因此爭取到了美國可口可樂公司,和丹麥啤酒製造商「嘉士伯」的支持,持續開發。由於植物塑膠的製作成本,生化公司了解一開始無法在價格上占有市場優勢,因此預計先每年生產5000公噸,預計將在2023年前,會與飲料公司合作,讓產品上架。

污染治理
國際新聞
荷蘭
可生物分解
廢棄物

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

研究:提高空調能源效率、改用氣候友善製冷劑 將可省下數千億噸碳排

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

.NetCore對接各大財務軟件憑證API——用友系列(2)

一. 前言

今天我們繼續來分析用友系列的第二個產品–U8Cloud2.5 ,apilink方式的API.官網的API文檔地址如下:U8API文檔 因為我們主要是憑證對接,所以使用到的模塊有總賬、基礎檔案這兩個模塊。

Ps:2.5的財務系統如果不是最新補丁的話,要記得打補丁,不然後續的科目接口會有問題。

二. API參數

2.1 遠程訪問財務系統

如果我們對接的財務系統是公有雲的U8C的話,你會得到一個遠程的財務系統的地址,接着使用UClient工具,即 通過集成友戶通,為企業應用提供了統一的單點登陸支持,支持CA登陸、短信登陸、用戶名/密碼登陸,支持企業用戶系統與友戶通進行綁定,實現統一的用戶登陸服務 的這麼一個工具。

具體的添加應用的步驟為

2.2 全局請求頭

首先,我們必須要在網站內註冊賬號,API集市上有各個接口的詳細說明,我們需要獲取一個apicode參數,每個API模塊在點擊購買后系統會自動分配該模塊的apicode,所以這也就是我們需要兩個不同的apicode.

基本上U8cloud2.5的版本接口,需要涉及到的請求參數就是這個了,接着我們就可以愉快的進行開發工作了。

如圖,固定的全局請求頭參數有以下幾個:

1.authoration–驗證方式;默認是apicode

2.apicode—模塊的apicode.也就是我們上文中購買模塊后得到的參數.

3.system—系統類型. 1—測試. 2—正式.

4.trantype–翻譯類型; 默認為code.即採用編碼模式.

2.3 基礎檔案

基礎檔案,我們主要使用到的API接口有科目查詢.

會計主體賬簿編碼–我們可以從財務系統里獲取,具體的獲取方式如下

打開U8Client,使用正確的用戶名和密碼登錄財務系統.在企業建模平台–》基礎檔案–》組織機構–》會計主體 一欄,可以看到我們使用的會計主體賬簿編碼.如我們要使用的就是40001-9999

其中40001為公司編碼,9999為會計方案.可以看到是採用分頁形式來訪問的,所以如果我們要一次性獲取到所有的會計科目,可以採用以下方法。

        public AccountQueryResponse QueryAccount(string pk_subjscheme, string pageIndex, string glorgBookCode)
        {
            var request = new AccountQueryRequest();
            var pms = new Dictionary<string, object>();
            pms.Add("pk_subjscheme", pk_subjscheme);
            pms.Add("glorgbookcode", glorgBookCode);
            pms.Add("page_now", pageIndex);
            pms.Add("page_size", "100");
            request.SetPostParameters(pms);
            return _Client.Excute(request);
        }

        public List<U8AccountResult> GetAccountQueryResult(string pk_subjscheme, string pageIndex, string glorgBookCode)
        {
            var list = new List<U8AccountResult>();
            var response = QueryAccount(pk_subjscheme, pageIndex, glorgBookCode);
            if (response != null && response.status == "success" && response.data != null)
            {
                var result = JsonConvert.DeserializeObject<AccountQueryResult>(response.data);
                list = result.datas == null ? new List<U8AccountResult>() : result.datas.ToList().Select(x => new U8AccountResult
                {
                    balanorient = x.accsubjParentVO.balanorient,
                    subjcode = x.accsubjParentVO.subjcode,
                    subjname = x.accsubjParentVO.subjname,
                    dispname = x.accsubjParentVO.dispname,
                    remcode = x.accsubjParentVO.remcode,
                    subjId = x.accsubjParentVO.pk_accsubj,
                    endflag = x.accsubjParentVO.endflag,
                    subjectAssInfos = x.subjass == null ? new List<AccSubjectAssInfo>() : x.subjass.ToList().Select(t => new AccSubjectAssInfo
                    {
                        bdcode = t.bdcode,
                        bddispname = t.bddispname,
                        bdname = t.bdname
                    }).ToList()
                }).ToList();
            }
            return list;
        }

///獲取所有的會計科目
        public List<U8AccountResult> GetAllAccount(string pk_subjescheme, string glorgBookCode)
        {
            var pageNo = "1";
            var list = new List<U8AccountResult>();
            var response = QueryAccount(pk_subjescheme, pageNo, glorgBookCode);
            if (response != null && response.status == "success" && response.data != null)
            {
                var result = JsonConvert.DeserializeObject<AccountQueryResult>(response.data);
                var allCount = Math.Ceiling(Convert.ToDouble(result.allcount) / result.retcount);
                if (allCount >= 1)
                {
                    for (int i = 1; i <= allCount; i++)
                    {
                        var resultList = GetAccountQueryResult(pk_subjescheme, i.ToString(), glorgBookCode);
                        list.AddRange(resultList);
                    }
                }
            }
            return list;
        }

allCount為總條數,retCount為當次請求的分頁條數,默認最大值為100,即接口每次只能返回100條數據,超過100條的數據量,我們就要採用分頁的形式來獲取了。

這裏,有兩個隱藏的坑需要注意一下

1.如果沒有打過類似“patch_會計科目查詢api查詢條件增加會計主體賬簿編碼”這樣的補丁,我們無法傳入會計主體賬簿編碼,就默認返回該集團下所有公司的會計科目,這樣顯然達不到我們的目的。

2.返回的會計科目中沒有輔助核算明細,這對於我們傳輸憑證也是有影響的。所以這兩個補丁,如果我們在對接的過程中發現有接口有問題,那麼就要聯繫總部的老師幫忙打相應的補丁了.

2.4 總賬

總賬模塊,主要是我們的憑證傳輸了.

我們先來看憑證的保存,憑證保存要傳入相應的憑證json串.

        public GL_VoucherInsertResponse InsertVoucher(List<object> models)
        {
            var request = new GL_VoucherInsertRequest();
            var pms = new Dictionary<string, object>();
            pms.Add("voucher", models);
            request.SetPostParameters(pms);
            return _Client.Excute(request);
        }
///憑證新增結果
        public List<U8GLVoucherResult> GetVoucherInsertResult(List<object> models)
        {
            var list = new List<U8GLVoucherResult>();
            var response = InsertVoucher(models);
            if (response != null && response.status == "success")
            {
                if (response.data != null && !response.data.IsNullOrEmpty())
                {
                    var result = JsonConvert.DeserializeObject<List<VoucherResult>>(response.data);
                    list = result.Select(x => new U8GLVoucherResult
                    {
                        explanation = x.explanation,
                        glorgbook_code = x.glorgbook_code,
                        glorgbook_name = x.glorgbook_name,
                        no = x.no,
                        pk_glorgbook = x.pk_glorgbook,
                        pk_voucher = x.pk_voucher,
                        totalcredit = x.totalcredit,
                        totaldebit = x.totaldebit,
                        pk_vouchertype = x.pk_vouchertype,
                        vouchertype_code = x.vouchertype_code,
                        vouchertype_name = x.vouchertype_name,
                        prepareddate = Convert.ToDateTime(x.prepareddate),
                        errorMsg = ""
                    }).ToList();
                }
            }
            else
            {
                list.Add(new U8GLVoucherResult { errorMsg = response.errormsg });
            }
            return list;
        }

借貸方,憑證字主要用於我們新增后回執進行憑證記錄的.

接着我們來看憑證保存的實體類.

 public class U8VoucherModel
    {
        /// <summary>
        /// 是否差異憑證
        /// </summary>
        public bool ISDIFFLAG { get; set; }
        /// <summary>
        /// 附單據數
        /// </summary>
        public string attachment { get; set; }
        public Detail[] details { get; set; }
        /// <summary>
        /// 憑證摘要
        /// </summary>
        public string explanation { get; set; }
        /// <summary>
        /// 憑證號
        /// </summary>
        public string no { get; set; }
        /// <summary>
        /// 公司
        /// </summary>
        public string pk_corp { get; set; }
        /// <summary>
        /// 賬簿
        /// </summary>
        public string pk_glorgbook { get; set; }
        /// <summary>
        /// 制單人編碼
        /// </summary>
        public string pk_prepared { get; set; }
        /// <summary>
        /// 憑證類別簡稱
        /// </summary>
        public string pk_vouchertype { get; set; }
        /// <summary>
        /// 制單日期
        /// </summary>
        public string prepareddate { get; set; }
        /// <summary>
        /// 憑證類型
        /// </summary>
        public int voucherkind { get; set; }
    }

    public class Detail
    {
        /// <summary>
        /// 原幣貸方金額
        /// </summary>
        public string creditamount { get; set; }
        /// <summary>
        /// 貸方數量
        /// </summary>
        public string creditquantity { get; set; }
        /// <summary>
        /// 原幣借方金額
        /// </summary>
        public string debitamount { get; set; }
        /// <summary>
        /// 借方數量
        /// </summary>
        public string debitquantity { get; set; }
        /// <summary>
        /// 分錄號
        /// </summary>
        public string detailindex { get; set; }
        /// <summary>
        /// 匯率
        /// </summary>
        public string excrate1 { get; set; }
        /// <summary>
        /// 摘要
        /// </summary>
        public string explanation { get; set; }
        /// <summary>
        /// 本幣貸方金額
        /// </summary>
        public string localcreditamount { get; set; }
        /// <summary>
        /// 本幣借方金額
        /// </summary>
        public string localdebitamount { get; set; }
        /// <summary>
        /// 科目
        /// </summary>
        public string pk_accsubj { get; set; }
        /// <summary>
        /// 幣別編碼
        /// </summary>
        public string pk_currtype { get; set; }
        /// <summary>
        /// 單價
        /// </summary>
        public string price { get; set; }
        public Ass[] ass { get; set; }
        public Cashflow[] cashflow { get; set; }
    }

    public class Ass
    {
        /// <summary>
        /// 輔助核算類型編碼
        /// </summary>
        public string checktypecode { get; set; }
        /// <summary>
        /// 輔助核算值編碼
        /// </summary>
        public string checkvaluecode { get; set; }
    }

    public class Cashflow
    {
        public string cashflow_code { get; set; }
        public string currtype_code { get; set; }
        public int money { get; set; }
    }

整個憑證對接下來,其實坑不是很多,主要在於前期接口文檔的研究,參數的獲取以及測試接口連通性上面.

三.結束語

希望文章能在你開發API接口對接的路上一些幫助和解疑,也希望同樣做API對接的小夥伴,我們可以多多交流。祝你在開發的道路上勇往直前。

我是程序猿貝塔,一個分享自己對接過財務系統API經歷和生活感悟的程序員。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

被迫重構代碼,這次我幹掉了 if-else

本文收錄在個人博客:www.chengxy-nds.top,技術資源共享,一起進步

最近公司貌似融到資了!開始發了瘋似的找渠道推廣,現在終於明白為啥前一段大肆的招人了,原來是在下一盤大棋,對員工總的來看是個好事,或許是時候該跟boss提一提漲工資的話題了。

不過,漲工資還沒下文,隨之而來的卻是一車一車的需求,每天都有新渠道接入,而且每個渠道都要提供個性化支持,開發量陡增。最近都沒什麼時間更文,準點下班都成奢望了!

由於推廣渠道的激增,而每一個下單來源在下單時都做特殊的邏輯處理,可能每两天就會加一個來源,已經把之前的下單邏輯改的面目全。出於長遠的考慮,我決定對現有的邏輯進行重構,畢竟長痛不如短痛。

傳統的實現方式

我們看下邊的偽代碼,大致就是重構前下單邏輯的代碼,由於來源比較少,簡單的做if-else邏輯判斷足以滿足需求。

現在每種訂單來源的處理邏輯都有幾百行代碼,看着已經比較臃腫,可我愣是遲遲沒動手重構,一方面業務方總像催命鬼一樣的讓你趕工期,想快速實現需求,這樣寫是最快;另一方面是不敢動,面對古董級代碼,還是想求個安穩。

但這次來源一下子增加幾十個,再用這種方式做已經無法維護了,想象一下那種臃腫的if-else代碼,別說開發想想都頭大!

public class OrderServiceImpl implements IOrderService {
    @Override
    public String handle(OrderDTO dto) {
        String type = dto.getType();
        if ("1".equals(type)) {
            return "處理普通訂單";
        } else if ("2".equals(type)) {
            return "處理團購訂單";
        } else if ("3".equals(type)) {
            return "處理促銷訂單";
        }
        return null;
    }
}

策略模式的實現方式

思來想去基於當前業務場景重構,還是用策略模式比較合適,它是oop中比較著名的設計模式之一,對方法行為的抽象。

策略模式定義了一個擁有共同行為的算法族,每個算法都被封裝起來,可以互相替換,獨立於客戶端而變化。

一、策略模式的使用場景:

  • 針對同一問題的多種處理方式,僅僅是具體行為有差別時;
  • 需要安全地封裝多種同一類型的操作時;
  • 同一抽象類有多個子類,而客戶端需要使用if-else 或者 switch-case 來選擇具體子類時。

這個是用策略模式修改後代碼:

@Component
@OrderHandlerType(16)
public class DispatchModeProcessor extends AbstractHandler{

	@Autowired
	private OrderStencilledService orderStencilledService;
	
	@Override
	public void handle(OrderBO orderBO) {
		
		/**
    	 * 訂單完結廣播通知(1 - 支付完成)
    	 */
    	orderStencilledService.dispatchModeFanout(orderBO);
		
    	/**
    	 *  SCMS 出庫單
    	 */
    	orderStencilledService.createScmsDeliveryOrder(orderBO.getPayOrderInfoBO().getLocalOrderNo());
	}
}

每個訂單來源都有自己單獨的邏輯實現類,而每次需要添加訂單來源,直接新建實現類,修改@OrderHandlerType(16)的數值即可,再也不用去翻又臭又長的if-lese

不僅如此在分配任務時,每個人負責開發幾種訂單來源邏輯,都可以做到互不干擾,而且很大程度上減少了合併代碼的衝突。

二、具體的實現過程:

1、定義註解

定義一個標識訂單來源的註解@OrderHandlerType

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface OrderHandlerType {
	int value() default 0;
}

2、抽象業務處理器

向上抽象出來一個具體的業務處理器

public abstract class AbstractHandler {
	abstract public void handle(OrderBO orderBO);
}

3、項目啟動掃描 handler 入口

@Component
@SuppressWarnings({"unused","rawtypes"})
public class HandlerProcessor implements BeanFactoryPostProcessor {
	
	private String basePackage = "com.ecej.order.pipeline.processor";
	
    public static final Logger log = LoggerFactory.getLogger(HandlerProcessor.class);
	
	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		
		Map<Integer,Class> map = new HashMap<Integer,Class>();
		
		ClassScaner.scan(basePackage, OrderHandlerType.class).forEach(x ->{
			int type = x.getAnnotation(OrderHandlerType.class).value();
			map.put(type,x);
		});
		
		beanFactory.registerSingleton(OrderHandlerType.class.getName(), map);
		
		log.info("處理器初始化{}", JSONObject.toJSONString(beanFactory.getBean(OrderHandlerType.class.getName())));
	}
}

4、掃描需要用到的工具類

public class ClassScaner {
	private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

	private final List<TypeFilter> includeFilters = new ArrayList<TypeFilter>();

	private final List<TypeFilter> excludeFilters = new ArrayList<TypeFilter>();

	private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
	
	/**
	 * 添加包含的Fiter
	 * @param includeFilter
	 */
	public void addIncludeFilter(TypeFilter includeFilter) {
		this.includeFilters.add(includeFilter);
	}

	/**
	 * 添加排除的Fiter
	 * @param includeFilter
	 */
	public void addExcludeFilter(TypeFilter excludeFilter) {
		this.excludeFilters.add(excludeFilter);
	}
	
	/**
	 * 掃描指定的包,獲取包下所有的Class
	 * @param basePackage 包名
	 * @param targetTypes 需要指定的目標類型,可以是pojo,可以是註解
	 * @return Set<Class<?>>
	 */
	public static Set<Class<?>> scan(String basePackage,
			Class<?>... targetTypes) {
		ClassScaner cs = new ClassScaner();
		for (Class<?> targetType : targetTypes){
			if(TypeUtils.isAssignable(Annotation.class, targetType)){
				cs.addIncludeFilter(new AnnotationTypeFilter((Class<? extends Annotation>) targetType));
			}else{
				cs.addIncludeFilter(new AssignableTypeFilter(targetType));
			}
		}
		return cs.doScan(basePackage);
	}
	
	/**
	 * 掃描指定的包,獲取包下所有的Class
	 * @param basePackages 包名,多個
	 * @param targetTypes 需要指定的目標類型,可以是pojo,可以是註解
	 * @return Set<Class<?>>
	 */
	public static Set<Class<?>> scan(String[] basePackages,
			Class<?>... targetTypes) {
		ClassScaner cs = new ClassScaner();
		for (Class<?> targetType : targetTypes){
			if(TypeUtils.isAssignable(Annotation.class, targetType)){
				cs.addIncludeFilter(new AnnotationTypeFilter((Class<? extends Annotation>) targetType));
			}else{
				cs.addIncludeFilter(new AssignableTypeFilter(targetType));
			}
		}
		Set<Class<?>> classes = new HashSet<Class<?>>();
		for (String s : basePackages){
			classes.addAll(cs.doScan(s));
		}
		return classes;
	}
	
	/**
	 * 掃描指定的包,獲取包下所有的Class
	 * @param basePackages 包名
	 * @return Set<Class<?>>
	 */
	public Set<Class<?>> doScan(String [] basePackages) {
		Set<Class<?>> classes = new HashSet<Class<?>>();
		for (String basePackage :basePackages) {
			classes.addAll(doScan(basePackage));
		}
		return classes;
	}
	
	/**
	 * 掃描指定的包,獲取包下所有的Class
	 * @param basePackages 包名
	 * @return Set<Class<?>>
	 */
	public Set<Class<?>> doScan(String basePackage) {
		Set<Class<?>> classes = new HashSet<Class<?>>();
		try {
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
					+ ClassUtils.convertClassNameToResourcePath(
							SystemPropertyUtils.resolvePlaceholders(basePackage))+"/**/*.class";
			Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
			for (int i = 0; i < resources.length; i++) {
				Resource resource = resources[i];
				if (resource.isReadable()) {
					MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
					if ((includeFilters.size() == 0 && excludeFilters.size() == 0)|| matches(metadataReader)) {
						try {
							classes.add(Class.forName(metadataReader.getClassMetadata().getClassName()));
						} catch (ClassNotFoundException ignore) {}
					}
				}
			}
		} catch (IOException ex) {
			throw new RuntimeException("I/O failure during classpath scanning", ex);
		}
		return classes;
	}
	
	/**
	 * 處理 excludeFilters和includeFilters
	 * @param metadataReader
	 * @return boolean
	 * @throws IOException
	 */
	private boolean matches(MetadataReader metadataReader) throws IOException {
		for (TypeFilter tf : this.excludeFilters) {
			if (tf.match(metadataReader, this.metadataReaderFactory)) {
				return false;
			}
		}
		for (TypeFilter tf : this.includeFilters) {
			if (tf.match(metadataReader, this.metadataReaderFactory)) {
				return true;
			}
		}
		return false;
	}
}

5、根據類型實例化抽象類


@Component
public class HandlerContext {

	@Autowired
	private ApplicationContext beanFactory;

	public  AbstractHandler getInstance(Integer type){
		
		Map<Integer,Class> map = (Map<Integer, Class>) beanFactory.getBean(OrderHandlerType.class.getName());
		
		return (AbstractHandler)beanFactory.getBean(map.get(type));
	}
	
}

6、調用入口

我這裡是在接受到MQ消息時,處理多個訂單來源業務,不同訂單來源路由到不同的業務處理類中。


@Component
@RabbitListener(queues = "OrderPipelineQueue")
public class PipelineSubscribe{
 
	private final Logger LOGGER = LoggerFactory.getLogger(PipelineSubscribe.class);
	
	@Autowired
	private HandlerContext HandlerContext;
	
	@Autowired
	private OrderValidateService orderValidateService;
	
    @RabbitHandler
    public void subscribeMessage(MessageBean bean){
    	
    	OrderBO orderBO = JSONObject.parseObject(bean.getOrderBO(), OrderBO.class);
    	
    	if(null != orderBO &&CollectionUtils.isNotEmpty(bean.getType()))
    	{
    		for(int value:bean.getType())
    		{
       		 AbstractHandler handler = HandlerContext.getInstance(value);
       		 handler.handle(orderBO);
    		}
		}
	}
}

接收實體 MessageBean 類代碼

public class MessageBean implements Serializable {
    private static final long serialVersionUID = 5454831432308782668L;
    private String cachKey;
    private List<Integer> type;
    private String orderBO;

    public MessageBean(List<Integer> type, String orderBO) {
        this.type = type;
        this.orderBO = orderBO;
    }
}

以上設計模式方式看着略顯複雜,很些小夥伴提出質疑:“你為了個if-else,弄的如此的麻煩,又是自定義註解,又弄這麼多類不麻煩嗎?” 還有一些小夥伴糾結於性能問題,策略模式的性能可能確實不如if-else

但我覺得吧增加一點複雜度、犧牲一丟丟性能,換代碼的整潔和可維護性還是值得的。不過,一個人一個想法,怎麼選還是看具體業務場景吧!

策略模式的優缺點

優點

  • 易於擴展,增加一個新的策略只需要添加一個具體的策略類即可,基本不需要改變原有的代碼,符合開放封閉原則
  • 避免使用多重條件選擇語句,充分體現面向對象設計思想 策略類之間可以自由切換,由於策略類都實現同一個接口,所以使它們之間可以自由切換
  • 每個策略類使用一個策略類,符合單一職責原則 客戶端與策略算法解耦,兩者都依賴於抽象策略接口,符合依賴反轉原則
  • 客戶端不需要知道都有哪些策略類,符合最小知識原則

缺點

  • 策略模式,當策略算法太多時,會造成很多的策略類
  • 客戶端不知道有哪些策略類,不能決定使用哪個策略類,這點可以通過封裝common公共包解決,也可以考慮使IOC容器依賴注入的方式來解決。

以下是訂單來源策略類的一部分,不得不說策略類確實比較多。

總結

凡事都有他的兩面性,if-else多層嵌套和也都有其各自的優缺點:

  • if-else的優點就是簡單,想快速迭代功能,邏輯嵌套少且不會持續增加,if-else更好些,缺點也是顯而易見,代碼臃腫繁瑣不便於維護。

  • 策略模式 將各個場景的邏輯剝離出來維護,同一抽象類有多個子類,需要使用if-else 或者 switch-case 來選擇具體子類時,建議選策略模式,他的缺點就是會產生比較多的策略類文件。

兩種實現方式各有利弊,如何選擇還是要依據具體業務場景,還是那句話設計模式不是為了用而用,一定要用在最合適的位置。

閑聊

平常和粉絲私下聊天,好多人對於學設計模式的感受:設計模式背了一大堆,可平常開發還不是成天寫if-else業務邏輯,根本就用不到。

學設計模式也不是用不到,只是有時候沒有合適它的場景而已,像我們今天說的這種業務場景,用設計模式就可以完美的解決嘛。

學了N多技術可工作用不到是一種很常見的事情,一個穩定的項目使用一種技術會有諸多考量的,新技術會不會提升系統複雜度?它有哪些性能瓶頸?這些都必須考慮到,畢竟項目穩定才是最重要,誰也不敢輕易冒險嘗試。

而我們學習技術可不僅為了眼下項目中是否會用到,是要做一個技術積累,做長遠打算,人往高處走,沒點能力可不行。

原創不易,燃燒秀髮輸出內容,希望你能有一丟丟收穫!

整理了幾百本各類技術电子書,送給小夥伴們。關公眾號回復【666】自行領取。和一些小夥伴們建了一個技術交流群,一起探討技術、分享技術資料,旨在共同學習進步,如果感興趣就掃碼加入我們吧!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!