雙指針技巧匯總

我認為雙指針技巧還可以分為兩類,一類是「快慢指針」,另一類是「左右指針」。前者解決主要解決鏈表中的問題,比如典型的判定鏈表中是否包含環;後者主要解決數組(或者字符串)中的問題,比如二分查找。

一、快慢指針的常見算法

快慢指針一般都初始化指向鏈表的頭結點 head,前進時快指針 fast 在前,慢指針 slow 在後,巧妙解決一些鏈表中的問題。

1、判定鏈表中是否含有環

這應該屬於鏈表最基本的操作了,如果讀者已經知道這個技巧,可以跳過。

單鏈表的特點是每個節點只知道下一個節點,所以一個指針的話無法判斷鏈表中是否含有環的。

如果鏈表中不包含環,那麼這個指針最終會遇到空指針 null 表示鏈表到頭了,這還好說,可以判斷該鏈表不含環。

boolean hasCycle(ListNode head) {
    while (head != null)
        head = head.next;
    return false;
}

但是如果鏈表中含有環,那麼這個指針就會陷入死循環,因為環形數組中沒有 null 指針作為尾部節點。

經典解法就是用兩個指針,一個每次前進兩步,一個每次前進一步。如果不含有環,跑得快的那個指針最終會遇到 null,說明鏈表不含環;如果含有環,快指針最終會超慢指針一圈,和慢指針相遇,說明鏈表含有環。

boolean hasCycle(ListNode head) {
    ListNode fast, slow;
    fast = slow = head;
    while(fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        
        if (fast == slow)
            return true;
    }
    return false;
}

2、已知鏈表中含有環,返回這個環的起始位置

這個問題其實不困難,有點類似腦筋急轉彎,先直接看代碼:

ListNode detectCycle(ListNode head) {
    ListNode fast, slow;
    fast = slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow)
            break;
    }
    
    slow = head;
    while (slow != fast) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

可以看到,當快慢指針相遇時,讓其中任一個指針重新指向頭節點,然後讓它倆以相同速度前進,再次相遇時所在的節點位置就是環開始的位置。這是為什麼呢?

第一次相遇時,假設慢指針 slow 走了 k 步,那麼快指針 fast 一定走了 2k 步,也就是說比 slow 多走了 k 步(也就是環的長度)。

設相遇點距環的起點的距離為 m,那麼環的起點距頭結點 head 的距離為 k – m,也就是說如果從 head 前進 k – m 步就能到達環起點。

巧的是,如果從相遇點繼續前進 k – m 步,也恰好到達環起點。

所以,只要我們把快慢指針中的任一個重新指向 head,然後兩個指針同速前進,k – m 步后就會相遇,相遇之處就是環的起點了。

3、尋找鏈表的中點

類似上面的思路,我們還可以讓快指針一次前進兩步,慢指針一次前進一步,當快指針到達鏈表盡頭時,慢指針就處於鏈表的中間位置。

ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
    fast = fast.next.next;
    slow = slow.next;
}
// slow 就在中間位置
return slow;

當鏈表的長度是奇數時,slow 恰巧停在中點位置;如果長度是偶數,slow 最終的位置是中間偏右:

尋找鏈表中點的一個重要作用是對鏈表進行歸併排序。

回想數組的歸併排序:求中點索引遞歸地把數組二分,最後合併兩個有序數組。對於鏈表,合併兩個有序鏈表是很簡單的,難點就在於二分。

但是現在你學會了找到鏈表的中點,就能實現鏈表的二分了。關於歸併排序的具體內容本文就不具體展開了。

4、尋找鏈表的倒數第 k 個元素

我們的思路還是使用快慢指針,讓快指針先走 k 步,然後快慢指針開始同速前進。這樣當快指針走到鏈表末尾 null 時,慢指針所在的位置就是倒數第 k 個鏈表節點(為了簡化,假設 k 不會超過鏈表長度):

ListNode slow, fast;
slow = fast = head;
while (k-- > 0) 
    fast = fast.next;

while (fast != null) {
    slow = slow.next;
    fast = fast.next;
}
return slow;

二、左右指針的常用算法

左右指針在數組中實際是指兩個索引值,一般初始化為 left = 0, right = nums.length – 1 。

1、二分查找

前文 二分查找算法詳解 有詳細講解,這裏只寫最簡單的二分算法,旨在突出它的雙指針特性:

int binarySearch(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while(left <= right) {
        int mid = (right + left) / 2;
        if (nums[mid] == target)
            return mid;
        else if (nums[mid] < target)
            left = mid + 1;
        else if (nums[mid] > target)
            right = mid - 1;
    }
}

2、兩數之和

直接看一道 LeetCode 題目吧:

只要數組有序,就應該想到雙指針技巧。這道題的解法有點類似二分查找,通過調節 left 和 right 可以調整 sum 的大小:

3、反轉數組

void reverse(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        // swap(nums[left], nums[right])
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        left++;
        right--;
    }
}

4、滑動窗口算法

這也許是雙指針技巧的最高境界了,如果掌握了此算法,可以解決一大類子字符串匹配的問題,不過「滑動窗口」算法比上述的這些算法稍微複雜些。

幸運的是,這類算法是有框架模板的,下篇文章就準備講解「滑動窗口」算法模板,幫大家秒殺幾道 LeetCode 子串匹配的問題。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

由老同事學習SAP所想到的

前段時間一位老同事在微信上跟我說他們公司正計劃導SAP系統,但整個IT中心幾乎無人使用過SAP,知道我在這行業幹了多年了,所以想問我怎麼開始學習。於是我約他今天出來聊聊,順便把手裡的SAP ECC EHP6版本的虛擬機拷給他自己先自學。 

他們公司一直都是在用九二年版的QAD系統(美國ERP廠商),跟之前我們同事的那家企業系統一致,非常古老的系統,不支持鼠標操作,基本上現在ERP系統該有的功能它都沒有,唯一好處的是開源可開發。公司老闆不知道從哪裡交流了一下,然後打算大刀闊斧大幹一場,改革目前信息化現狀,為將來業務擴展做信息化支撐。 

一直以來他都是做ERP行業,接觸過多個模塊,現在這個公司可能是因為體量小的原因,一個人幾乎全管了所有的模塊,業務能力很紮實,對企業的流程和供應鏈非常熟悉。看我給他演示了一下基礎的SAP操作和邏輯,一直驚呼SAP的強大。

 

 SAP的龐大複雜對於一個從來沒接觸到人來說門檻還是相當高的,這個門檻並不是看幾本PDF、看幾個視頻、上上培訓機構就能越過得了的,其中包含的後台邏輯配置和各種強關聯絕對會把一個人打蒙。想起前幾年碰到一個啥都不懂的信息化管理者,在ERP選型會議上跟演示系統的供應商要求在企業內部安裝一套空白的ERP試用,想想這真是一大笑柄。

 這持續枯燥乏味的學習過程絕對非常考驗一個人的毅力。想起十多年前,為了學習SAP,我從騰訊拍拍上花了600元買SAP ECC的安裝包,含視頻教程差不多三十多張DVD光盤,升級了老爺筆記本配置(酷睿雙核、4G內存、500G机械硬盤),安裝Windows Server,安裝Java,安裝MSSQL,安裝SAP,通宵安裝了十五六個小時才搞定,佔用硬盤空間220G,一開啟SAP服務整個電腦就得卡死半個小時,CPU直接100%,內存爆滿。

之後對着SAP GUI界面一臉懵逼,根本不知道怎麼下手。雖然我知道部分ERP的流程和功能,但我根本不知道怎麼弄。看購買回來的視頻也是一臉懵逼,因為系統裏面的組織配置跟視頻教程里根本就不一樣,真要操作起來困難重重,各種紅燈錯誤,這也不行那也不行,那種深深的絕望感至今歷歷在目。

 

後來跌跌撞撞學了一點ABAP開發,由於沒有實際的工作經歷,也只是懂個ABAP開發的一絲絲皮毛而已。那時候沒有SAP前輩先驅可以交流,沒有QQ群,連熱鬧一點的論壇都沒有,夜以繼日枯燥得學習才進步這麼點,支撐起我這份毅力恆心的大概就是“生存”壓力吧。一心想離開那時候的工作環境,不願被溫水煮死。

後來在廈門面試了一家正在實施SAP的企業,面試的主管給我出了一道SAP開發的題目,非常簡單的數據查詢我都沒能做出來,好在他們給了我機會讓我回去用自己的電腦做題。回去之後我狂惡補知識,當晚做題到凌晨,將源碼發郵件給那位主管,第二天早上接到他們複試的通知,於是第二輪面試的時候我也很幸運成功解決了ABAP的問題,就這樣開始跟SAP結緣了。

 

為了不讓主管失望,覺得我SAP技術是半桶水,那時候我瘋狂加班,下班回來也利用自己電腦的SAP狂學習,不停研究顧問開發的代碼,看到不熟悉的語法就記下來百度,做各種嘗試測試。恰好那時候公司要開發三支程序,顧問那邊報價十多萬台幣。於是我自告奮勇,跟主管說我來開發。然後就是瘋狂的查閱資料,查看SAP官方英文文檔,系統測試,順利得完成了任務。短短2個月就給公司省了十多萬的開發費用,且提前了一個月轉正。不得不說,不逼一下自己都不知道自己原來可以如此優秀。

再後來跳槽去做業務模塊做項目了,開始是做MM模塊,實施和運維過程中遇到過各種各樣的問題,也深深感受到了SAP的強大,後來又接觸了SD模塊,Basis模塊等。我覺得一個SAP顧問如果不精通一兩個模塊,其他模塊如果不熟悉的話,是很沒優勢的。這個過程中累積的各種筆記和實施運維實錄有五六百兆,上千篇文檔。

就這樣曲曲折折這麼些年,非常成功的項目也有,失敗的項目也有,見識到了形形色色的SAP顧問和關鍵用戶,這些都變成了自己非常寶貴的經驗。一個顧問如果沒有經歷過失敗的項目,那就是失敗的!

 

當然,之前兩年半的QAD運維並非全是沒用的,至少讓我懂得了部分業務,知道了如何敏捷高效開發(這點得感謝那時候的主管領導,至今讓我受益無窮,很遺憾現在絕大多數只是有開發的語法並沒有開發的思維觀念),也讓我明白系統固然重要但企業流程和業務分析能力更重要。我曾經不止一次說過考驗一個SAP顧問的能力並不在於他會多少事務代碼,知道後台表是什麼,不在於他知道SAP這個功能如何配置,而是他對業務的分析水平的高低以及需求溝通的能力大小,這才是一個資深的SAP顧問跟一個培訓機構培訓出來的人的區別。

很多人來信問我該如何入行SAP這個行業,每個人成長的道路不同,但我還是很忌諱培訓機構的,他們只會弄虛作假,投機取巧,教你如何在簡歷上謊報項目經驗,也只會教一些系統層級的東西,隨便甲方稍微面試一下就露馬腳了。我覺得時刻準備着,好好學習,找機會入職甲方或者乙方才是正道,別去花冤枉錢。

老同事如今也面臨“生存”壓力,我想他應該是有毅力堅持下去的,但能學到什麼程度就不知道了。不過他懂開發,懂業務,學起SAP應該可以輕鬆不少。要知道一個人能集業務分析、開發、項目管理、系統配置於一身,那真的不得了!

 

 

 

 

  本文作者 | SAP夢心

  聯繫方式 | 微信:W150112458(瘋狂的程序員)

  特別敬告 | 歡迎轉載,轉載請註明出處並保持原文不動,謝謝

 

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

使用React Hook后的一些體會

一、前言

距離React Hook發布已經有一段時間了,筆者在之前也一直在等待機會來嘗試一下Hook,這個嘗試不是像文檔中介紹的可以先在已有項目中的小組件和新組件上嘗試,而是嘗試用Hook的方式構建整個項目,正好新的存儲項目啟動了,需要一個新的基於web的B/S管理系統,機會來了。在項目未進入正式開發前的時間里,筆者和小夥伴們對官方的Hook和Dan以及其他優秀開發者的關於Hook的文檔和文章都過了至少一遍,當時的感覺就是:之前學的又沒用了,新的一套又來了。目前這個項目已經成功搭起來了,主要組件和業務已具規模,UT也對應完成了。是時候寫一下對Hook使用后的初步體會了,在這裏,筆者不會做太多太深入的Hook API和原理講解,因為很多其他優秀的文章可以已經講得足夠多了。再者因為雖然重構了項目,但代碼組織方式可能還不是最Hook的方式。本文內容大多為筆者認為使用Hook最需要明白的地方。

 

二、怎麼替代之前的生命周期方法?

這個問題在筆者粗略地過了一遍Hook的API后自然而然地產生了,因為畢竟大多數關注Hook新特性的開發者們,都是從生命周期的開發方式方式過來的,從 createClass 到ES2015的 class ,再到Hook。很少有人是從Hook出來才使用React的。這也就是說,大家在使用初期,都會首先用生命周期的思維模式來探究Hook的使用,就像我們對英語沒熟練使用之前,英文對話都是先在心裏準備出中文語句,在心裏翻譯出英文語句再說出來。筆者已有3年的生命周期方式的開發經驗,慣性的思維改變起來最為困難。

筆者在之前使用生命周期的方式開發組件時,使用最多的、對要實現業務最依賴的生命周期是 componentDidMount 、 componentWillReceiveProps 、 shouldComponentUpdate 。

對於 componentDidMount 的替代方式很簡單: useEffect(() => {/* code */}, []); ,使用 useEffect hook,依賴給空數組就行,空數組在這裏表示有依賴的存在,但依賴實際上又為空,會是這個hook在初次render完成的時候調用一次足矣。如果有需要在組件卸載的生命周期內 componentWillUnmount 乾的事情,只需要在 useEffect 內部返回一個函數,並在這個函數內部做這些事情即可。但要記住的時候,考慮到函數的Capture Value的特性,對值的獲取等情況與生命周期方法的表現並非完全一致。

對於 componentWillReceiveProps 這個生命周期。首先這裏說說筆者自己的歷史原因。在React16.3版本以後,生命周期API被大幅修改,16.4又在16.3上改了一把,為了後期的Async Render的出現,原有的 componentWillReceiveProps 被預先重命名為unsafe方法,並引入了 getDerivedStateFromPorps 的靜態方法,為了不重構項目,筆者把React和對應打包工具都停留在了16.2和適配16.2的版本。現有的Hook文檔也忽略了怎麼替代 componentWillReceiveProps 。其實這個生命周期的替代方式最為簡單,因為像 useEffect 、 useCallback 、 useMemo 等hook都可以指定依賴,當依賴變化后,回調函數會重新執行,或者返回一個根據依賴產生的新的函數,或者返回一個根據依賴產生的新的值。

對於 shouldComponentUpdate 來說,它和 componentWillReceiveProps 的替換方式其實差不多。說實話,筆者在項目中,至少是在目前跑在PC瀏覽器的項目中,不太經常使用這個生命周期。因為在目前的業務中,從redux導致的props更新基本都有數據變化進而導致有視圖更新的需要,可能從觸發父到子的prop更新的時候,會出現不太必要的沖渲染需要,這個時候可能需要這個生命周期對當前和歷史狀態進行判斷。也就是說,如果對於某個組件來說,差不多每次的props變化大概率可能是值真的變了,其實做比較是無意義的,因為比較也需要耗時,特別是數據量較大的情況。最後耗時去比較了,結果還是數據發生了變化,需要衝渲染,那麼這是很操蛋的。所有說不能濫用 shouldComponentUpdate ,真的要看業務情況而定,在PC上多幾次小範圍的無意義的重渲染對性能影響不是很大,但在移動端的影響就很大,所以得看時機情況來決定。

Hook帶來的改變,最重要的應該是在組織一個組件代碼的時候,在思維方式上的變化,這也是官方文章中有提到的:”忘記你已經學會的東西”,所以我們在熟悉Hook以後,在書寫組件邏輯的時候應該不要先考慮生命周期是怎麼實現這個業務的,再轉成Hook的實現,這樣一來,一是還停留在生命周期的方式上,二是即便實現了業務功能,可能也不是很Hook的最優方式。所以,是時候用Hook的方式來思考組件的設計了。

 

三、不要忘記依賴、不要打亂Hook的順序

先說Hook的順序,在很多文章中,都有介紹Hook的基本實現或模擬實現原理,筆者這裏不再多講,有興趣可以自行查看。總結來說就是,Hook實現的時候依賴於調用索引,當某個Hook在某一次渲染時因條件不滿足而未能被調用,就會造成調用索引的錯位,進而導致結果出錯。這是和Hook的實現方式有關的原因,只要記住Hook不能書寫在 if 等條件判斷語句內部即可。

對於某個hook的依賴來說,一定要記住寫,因為函數式組件是沒有 componentWillReceive 、 shouldComponentUpdate 生命周期的。任何在重渲染時,一個函數是否需要重新創建、一個值是否需要重新計算,都和依賴有關係,如果依賴變了,就需要計算,沒變就不需要計算,以節省重渲染的成本。這裏特別需要注意的是函數依賴,因為函數內部可能會使用到 state 和 props 。比如,當你在 useEffect 內部引用了某些 state 和 props ,你可能會很容易的查看到,但是不太容易查看到其內部調用的其他函數是否也用到了 state 和 props 。所以函數的依賴一定不要忘記寫。當然官方的CRA工具已經集成了ESlint配置,來幫我們檢測某個hook是否存在有遺漏的依賴沒有寫上。PS. 這裏我也推薦大家使用CRA進行項目初始化,並eject出配置文件,這樣可以按照我們的業務要求自定義修改配置,然後將一些框架代碼通過yeoman打包成generator,這樣我們就有了自己的種子項目生成器,當開新項目的時候,可以進行快速的初始化。

 

四、Cpature Value特性

捕獲值的這個特性並非函數式組件特有,它是函數特有的一種特性,函數的每一次調用,會產生一個屬於那一次調用的作用域,不同的作用域之前不受影響。筆者看過的有關Hook的文檔中,大多都引述過這個經典的例子:

function App (){
    const [count, setCount] = useState(0);
    
    function increateCount (){
        setCount(count + 1);
    }
    
    function showCount (){
        setTimeout(() => console.log(`你點擊了${count}次`), 3000);
    }
    
    return (
        <div>
            <p>點擊了{count}次</p>
            <button onClick={increateCount}>增加點擊次數</button>
            <button onClick={showCount}>显示點擊次數</button>
        </div>
    );
}

當我們點擊了一次”增加點擊次數”按鈕后,再點擊”显示點擊次數”按鈕,在大約3s后,我們可以看到點擊次數會在控制台輸上出來,在這之前我們再次點擊”增加點擊次數”按鈕。3s后,我們看到控制台上輸出的是1,而我們期望的是2。當你第一次接觸Hook的時候看到這個結果,你一定會大吃一驚,WTF?

可以驚,但不要慌,聽我細細道來:

1. 當App函數組件初次渲染完后,生成了第一個scope。在這個scope中, count 的值為0。

2. 我們第一次點擊”增加點擊次數”按鈕的時候,調用了 setCount 方法,並將 count 的值加1,觸發了重渲染,App組件函數因重渲染的需要而被重新調用,生成了第二個scope。在這個scope中,count為1。頁面也更新到最新的狀態,显示”點擊了1次”。

3. 緊接着我們點擊了”显示點擊次數”按鈕,將調用 showCount 方法,延遲3s后显示 count 的值。請注意這裏,我們這次操作是在第二次渲染生成的這個scope(第二個scope)中進行的,而在這個scope中, count 的值為1。

4. 在3s的異步宏任務還未被推進主線程執行之前,我們又再次點擊了”增加點擊次數”按鈕,再次調用了 setCount 方法,並加 count 的值再次加1,又觸發了重渲染,App組件函數因重渲染的需要而被重新調用,生成了第三個scope。在這個scope中,count為2。頁面也更新到最新的狀態,显示”點擊了2次”。

5. 3s到了以後,主線程也出於空閑狀態,之前壓入異步隊列的宏任務被推入主線程中執行,重要的地方來了,這個異步任務所處的作用域是屬於第二個scope,也就是說它會使用那一次渲染scope的 count 值,也就是1。而不是和界面最新的渲染結果2一樣。

當你使用類組件來實現這個小功能並進行相同操作的時候,在控制台得到的結果都不同,但是在界面上最終的結果是一致的。在類組件中,我們在是生命周期方法 componentDidMount 、 componentDidUpdate 通過 this.state 去獲取狀態,得到的一定是其最新的值。這就是最大的不同之處,也是讓初學者很困惑,很容易踩入坑中的地方,當然這個坑並不是說函數式組件和Hook設計上的問題,而是我們對其的不了解,進而導致使用上的錯誤和對結果的誤判,進而導致代碼出現BUG。

Capture Value這個特性在Hook的編碼中一定要記住,並且理解。

如果說想要跳出每個重渲染產生的scope會固化自己的狀態和值的特性,可以使用Hook API提供的 useRef hook,讓所有的渲染scope中的某個狀態,都指向一個統一的值的一個Key(API中採用current)。這個對象是引用傳遞的,ref的值記錄在這個Key中,我們並不直接改變這個對象本身,而是通過修改其的一個Key來修記錄的值。讓每次重渲染生成的scope都保持對同一個對象的引用,來跳出Cpature Value帶來的限制。

 

五、Hook的優勢

在Hook的官方文檔和一些文章中也提到了類組件的一些不好的地方,比如:HOC的多層嵌套,HOC和Render Props也不是太理想的復用代碼邏輯,有關狀態管理的邏輯代碼很難在組件之間復用、一個業務邏輯的實現代碼被放到了不同的生命周期內、ES2015與類有關語法和this指向等困擾初級開發者的問題等都有提到。還有像上一段落中提到的一些問題一樣。這些都是需要改革和推動的地方。

這裏筆者對HOC的多層嵌套確實覺得很噁心,因為筆者之前的項目就是這樣的,一旦進入開發者工具的React Dev Tool的Tab,犹如地獄般的connect、asyncLoad就出現了,你會發現每個和Redux有關的組件都有一個connect,做了代碼分割以後,異步加載的組件都有一個asyncLoad(雖然後面可以用原生的 lazy 和 suspense 替代),很多因使用HOC而帶來的負面影響,對強迫症患者來說這不可接受,只能不看了之。

而對於類組件生命周期的開發方式來說,一個業務邏輯的實現,需要多個生命周期的配合,也就是邏輯代碼會被放到多個生命周期內部,在一個組件比較稍微龐大和複雜以後,維護起來較為困難,有些時候可能會忘記修改某個地方,而採用Hook的方式來實現就比較好,可以完全封裝在一個自定hook內部,需要的組件引入這個hook即可,還可以做到邏輯的復用。比如這個簡單的需求:在頁面渲染完成后監聽一個瀏覽器網絡變化的事件,並給出對應提示,在組件卸載后,我們再移除這個監聽,通常使用生命周期的實現方式為:

class App (){
    browserOnline () {
        notify('瀏覽器網絡已恢復正常!');  
    }   

    browserOffline () {
        notify('瀏覽器發生網絡異常!');  
    }  

    componentDidMount (){
        window.addEventListener('online', this.browserOnline);
        window.addEventListener('offline', this.browserOffline);
    }  

    componentWillUnmount (){
        window.removeEventListener('online', this.browserOnline);
        window.removeEventListener('offline', this.browserOffline);
    }
}

使用Hook方式實現:

function useNetworkNotification (){
    const browserOnline = () => notify('瀏覽器網絡已恢復正常!');

    const browserOffline = () => notify('瀏覽器發生網絡異常!');

    useEffect(() => {
        window.addEventListener('online', browserOnline);
        window.addEventListener('offline', browserOffline);

        return () => {
            window.removeEventListener('online', browserOnline);
            window.removeEventListener('offline', browserOffline);
        };
    }, []);
}
function App (){
    useNetworkNotification();
}    

function AnotherComp (){
    useNetworkNotification();
}

所以,採用Hook實現的代碼不僅管理起來方便(無需將相關的代碼散布到不同的生命周期方法內),可以封裝成自定義的hook,便於邏輯的在不同組件間復用,組件在使用的時候也不需要關注其內部的實現方式。這僅僅是實現了一個很簡單功能的例子,如果項目變得更加複雜和難以維護,通過自定義Hook的方式來抽象邏輯有助於代碼的組織質量。

 

六、為啥會推動Hook

筆者認為上個段落中提到的函數式組件配合Hook相較於類組件配合生命周期方法是存在有一定優勢的。再者,React團隊最開始發布Hook的時候,其實是頂着很大的壓力的,因為這對於開發者來說實在就是以前的白學了,除了底層某些思想不變外,上層API全部變完。筆者最開始了解Hook后,最直接感受就是這東西是不是在給後面的Async Render填坑用的,為啥會這麼說呢?因為React的這種更新機制就是全部樹做Diff然後更新patch。而Vue是依賴收集方式的,數據變化后,哪些地方需要更新是明確的,所以更新是精準的。React的這種設計機制,就導致更新的成本很高,即便有虛擬樹,但是一旦應用很龐大以後,遍歷新舊虛擬樹做Diff也是很耗時的,並且沒有Async Render前,一旦開啟協調,就只能一條路走到底,代碼又不能控制JS引擎的函數調用棧,在主線程長時間運行腳本又不歸還控制權,會阻塞線程造成界面友好度下降,特別是當應用運行在移動端設備等性能不太強的計算機上時效果特別顯著。而基於Fiber的鏈表式樹結構可以模擬出函數調用棧,並能夠由代碼控制工作的開始和暫停,可以有效解決上述問題,但它會破壞原本完整的生命周期方式,因為一個協調的任務,可能會放在不同的線程空閑時間內去完成,進而導致一個生命周期可能會被調用多次,導致實際運行的結果並不像代碼書寫的那樣,這也是在16.3及以後版本將某些生命周期重命名為unsafe的原因。生命周期基本廢掉了,雖然後來引入了一些靜態方法用來解決一些問題,但存在感太低了,基本都屬於過度階段的產物。生命周期廢了,就需要有東西來替代,並支持Async Render的實現,Hook這種模式就是一個不錯的選擇。當然這可能並不全面,或者說的不絕對正確,但筆者認為是有這個原因的。

 

七、單元測試

筆者目前的項目對穩定性要求高,屬於LTS類型,不像創業型的互聯網項目,可能上線幾個月就下了,所以UT是必須的。筆者給新項目的模塊寫單元測試的時候,比較完好的支持Hook的Enzyme3.10版本在8天前才發布:(。從目前測試的體驗來看,相對於類組件時代確實有進步。在類組件時代,除了生命周期外,其他的一切基本都靠HOC來完成,這就造成了我們在測試的時候,必須套上HOC,而當測試組件業務邏輯的時候,又必須扒開之前套上的HOC,找到裏面的真實組件,再進行各種模擬和打樁操作。而函數式組件是沒有這個問題的,有Hook加持后,一切都是扁平化的,總之就是比之前好測了。有一點稍微麻煩點的就是:

1. 涉及到會觸發重渲染,會執行useEffect 和 useState 的操作,需要放入 react-dom/test-utils 的act 方法內,並且還需要注意源代碼是同步還是異步執行,並且在 act 方法執行后,需要執行wrapper的 update 來更新wrapper。遇到這類問題不難解決,到React、Enzyme的Github上搜對應issue即可。

2. 測試中,Capture Value的特性也會存在,所以有些之前緩存的東西,並不是最新的:(。

當然類組件時代也有好處,就是能夠訪問instance,但對於函數組件來說,無法從函數外面訪問函數作用域內的東西。

 

八、總結

就像官方團隊的文章中寫道的一樣:“如果你太不能夠接受Hook,我們還是能夠理解的,但請你至少不要去噴它,可以適當宣傳一下。”。我們還是可以大膽嘗試一下Hook的,至少現在2019年年中的時候,因為在這個時間點,一切有關Hook的支持和文檔應該都比去年年底甚至是年初的時候更加完善了,雖然可能還不是太完全,但至少官方還在繼續摸索,社區也很活躍,造輪子的人也很多。之前也有消息說Vue3.0大版本也會出Hook,哈哈,又是一片腥風血雨。總之,風口來了,能折騰的、喜歡折騰的就跟着風吹唄。入門簡單,但完全、徹底地掌握和熟練運用,還是需要時間的。

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

[WPF自定義控件庫]使用TextBlockHighlightSource強化高亮的功能,以及使用TypeConverter簡化調用

1. 強化高亮的功能

上一篇文章介紹了使用附加屬性實現TextBlock的高亮功能,但也留下了問題:不能定義高亮(或者低亮)的顏色。為了解決這個問題,我創建了TextBlockHighlightSource這個類,比單純的字符串存儲更多的信息,這個類的定義如下:

相應地,附加屬性的類型也改變為這個類,並且屬性值改變事件改成這樣:

private static void OnHighlightTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    var oldValue = (TextBlockHighlightSource)args.OldValue;
    var newValue = (TextBlockHighlightSource)args.NewValue;
    if (oldValue == newValue)
        return;

    void OnPropertyChanged(object sender,EventArgs e)
    {
        if (obj is TextBlock target)
        {
            MarkHighlight(target, newValue);
        }
    };

    if(oldValue!=null)
        newValue.PropertyChanged -= OnPropertyChanged;

    if (newValue != null)
        newValue.PropertyChanged += OnPropertyChanged;

    OnPropertyChanged(null, null);
}

MarkHighlight的關鍵代碼修改為這樣:

if (highlightSource.LowlightForeground != null)
    run.Foreground = highlightSource.LowlightForeground;

if (highlightSource.HighlightForeground != null)
    run.Foreground = highlightSource.HighlightForeground;

if (highlightSource.HighlightBackground != null)
    run.Background = highlightSource.HighlightBackground;

使用起來就是這樣:

<TextBlock Text="Git hub"
           TextWrapping="Wrap">
    <kino:TextBlockService.HighlightText>
        <kino:TextBlockHighlightSource Text="hub"
                                       LowlightForeground="Black"
                                       HighlightBackground="#FFF37D33" />
    </kino:TextBlockService.HighlightText>
</TextBlock>

2. 使用TypeConverter簡化調用

TextBlockHighlightSource提供了很多功能,但和直接使用字符串比起來,創建一個TextBlockHighlightSource要複雜多。為了可以簡化調用可以使用自定義的TypeConverter

首先來了解一下TypeConverter的概念。XAML本質上是XML,其中的屬性內容全部都是字符串。如果對應屬性的類型是XAML內置類型(即Boolea,Char,String,Decimal,Single,Double,Int16,Int32,Int64,TimeSpan,Uri,Byte,Array等類型),XAML解析器直接將字符串轉換成對應值賦給屬性;對於其它類型,XAML解析器需做更多工作。

<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>

如上面這段XAML中的”Auto”和”*”,XAML解析器將其分別解析成GridLength.Auto和new GridLength(1, GridUnitType.Star)再賦值給Height,它相當於這段代碼:

grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

為了完成這個工作,XAML解析器需要TypeConverter的協助。XAML解析器通過兩個步驟查找TypeConverter:
1. 檢查屬性聲明上的TypeConverterAttribute。
2. 如果屬性聲明中沒有TypeConverterAttribute,檢查類型聲明中的TypeConverterAttribute。

屬性聲明上TypeConverterAttribute的優先級高於類型聲明。如果以上兩步都找不到類型對應的TypeConverterAttribute,XAML解析器將會報錯:屬性”*”的值無效。找到TypeConverterAttribute指定的TypeConverter后,XAML解析器調用它的object ConvertFromString(string text)函數將字符串轉換成屬性的值。

WPF內置的TypeConverter十分十分多,但有時還是需要自定義TypeConverter,自定義TypeConverter的基本步驟如下:

  • 創建一個繼承自TypeConverter的類;
  • 重寫virtual bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType);
  • 重寫virtual bool CanConvertTo(ITypeDescriptorContext context, Type destinationType);
  • 重寫virtual object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value);
  • 重寫virtual object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType);
  • 使用TypeConverterAttribute 指示XAML解析器可用的TypeConverter;

到這裏我想TypeConverter的概念已經介紹得夠詳細了。回到本來話題,要簡化TextBlockHighlightSource的調用我創建了TextBlockHighlightSourceConverter這個類,它繼承自TypeConverter,裏面的關鍵代碼如下:

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    if (sourceType == typeof(string))
    {
        return true;
    }

    return base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
    switch (value)
    {
        case null:
            throw GetConvertFromException(null);
        case string source:
            return new TextBlockHighlightSource { Text = value.ToString() };
    }

    return base.ConvertFrom(context, culture, value);
}

然後在TextBlockHighlightSource上使用TypeConverterAttribute:

[TypeConverter(typeof(TextBlockHighlightSourceConverter))]
public class TextBlockHighlightSource : FrameworkElement

這樣在XAML中TextBlockHighlightSource的調用方式就可以和使用字符串一樣簡單了。

<TextBlock Text="Github"
           kino:TextBlockService.HighlightText="hub" />

3. 使用Style

有沒有發現TextBlockHighlightSource繼承自FrameworkElement?這種奇特的寫法是為了讓TextBlockHighlightSource可以使用全局的Style。畢竟要在應用程序里統一Highlight的顏色還是全局樣式最好使,但作為附加屬性,TextBlockHighlightSource並不是VisualTree的一部分,它拿不到VisualTree上的Resources。最簡單的解決方案是讓TextBlockHighlightSource繼承自FrameworkElement,把它放到VisualTree里,用法如下:

<StackPanel>
    <FrameworkElement.Resources>
        <Style TargetType="kino:TextBlockHighlightSource">
            <Setter Property="LowlightForeground" Value="Blue"/>
        </Style>
    </FrameworkElement.Resources>
    <TextBox x:Name="FilterElement3"/>
    <kino:TextBlockHighlightSource Text="{Binding ElementName=FilterElement3,Path=Text}" 
                                   HighlightForeground="DarkBlue"
                                   HighlightBackground="Yellow"
                                   x:Name="TextBlockHighlightSource2"/>
    <TextBlock Text="A very powerful projector with special features for Internet usability, USB" 
               kino:TextBlockService.HighlightText="{Binding ElementName=TextBlockHighlightSource2}"
               TextWrapping="Wrap"/>
</StackPanel>

也許你會覺得這種寫法有些奇怪,畢竟我也覺得在View上放一個隱藏的元素真的很怪。其實在一萬二千年前微軟就已經有這種寫法,在DomainDataSource的文檔里就有用到:

<Grid x:Name="LayoutRoot" Background="White">  
    <Grid.RowDefinitions>
        <RowDefinition Height="25" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <riaControls:DomainDataSource x:Name="source" QueryName="GetProducts" AutoLoad="true">
        <riaControls:DomainDataSource.DomainContext>
            <domain:ProductDomainContext />
        </riaControls:DomainDataSource.DomainContext>   
        <riaControls:DomainDataSource.FilterDescriptors>
            <riaData:FilterDescriptorCollection LogicalOperator="And">
              <riaData:FilterDescriptor PropertyPath="Color" Operator="IsEqualTo" Value="Blue" />
              <riaData:FilterDescriptor PropertyPath="ListPrice" Operator="IsLessThanOrEqualTo">
                  <riaControls:ControlParameter 
                      ControlName="MaxPrice" 
                      PropertyName="SelectedItem.Content" 
                      RefreshEventName="SelectionChanged" />
              </riaData:FilterDescriptor>
            </riaData:FilterDescriptorCollection>
        </riaControls:DomainDataSource.FilterDescriptors>
    </riaControls:DomainDataSource>
    <ComboBox x:Name="MaxPrice" Grid.Row="0" Width="60" SelectedIndex="0">
        <ComboBoxItem Content="100" />
        <ComboBoxItem Content="500" />
        <ComboBoxItem Content="1000" />
    </ComboBox>
    <data:DataGrid Grid.Row="1" ItemsSource="{Binding Data, ElementName=source}" />
</Grid>

把DataSource放到View上這種做法可能是WinForm的祖傳家訓,結構可恥但有用。

4. 結語

寫這篇博客的時候我才發覺這個附加屬性還叫HighlightText好像不太好,但也懶得改了。

這篇文章介紹了使用TypeConverter簡化調用,以及繼承自FrameworkElement以便使用Style。

5. 參考

TypeConverter 類
TypeConverters 和 XAML
Type Converters for XAML Overview
TypeConverterAttribute Class
如何:實現類型轉換器

6. 源碼

TextBlock at master · DinoChan_Kino.Toolkit.Wpf

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

flink DataStream API使用及原理

傳統的大數據處理方式一般是批處理式的,也就是說,今天所收集的數據,我們明天再把今天收集到的數據算出來,以供大家使用,但是在很多情況下,數據的時效性對於業務的成敗是非常關鍵的。

Spark 和 Flink 都是通用的開源大規模處理引擎,目標是在一個系統中支持所有的數據處理以帶來效能的提升。兩者都有相對比較成熟的生態系統。是下一代大數據引擎最有力的競爭者。

Spark 的生態總體更完善一些,在機器學習的集成和易用性上暫時領先。

Flink 在流計算上有明顯優勢,核心架構和模型也更透徹和靈活一些。

本文主要通過實例來分析flink的流式處理過程,並通過源碼的方式來介紹流式處理的內部機制。

DataStream整體概述

主要分5部分,下面我們來分別介紹:

 1.運行環境StreamExecutionEnvironment

StreamExecutionEnvironment是個抽象類,是流式處理的容器,實現類有兩個,分別是

LocalStreamEnvironment:
RemoteStreamEnvironment:
/**
 * The StreamExecutionEnvironment is the context in which a streaming program is executed. A
 * {@link LocalStreamEnvironment} will cause execution in the current JVM, a
 * {@link RemoteStreamEnvironment} will cause execution on a remote setup.
 *
 * <p>The environment provides methods to control the job execution (such as setting the parallelism
 * or the fault tolerance/checkpointing parameters) and to interact with the outside world (data access).
 *
 * @see org.apache.flink.streaming.api.environment.LocalStreamEnvironment
 * @see org.apache.flink.streaming.api.environment.RemoteStreamEnvironment
 */

2.數據源DataSource數據輸入

包含了輸入格式InputFormat

    /**
     * Creates a new data source.
     *
     * @param context The environment in which the data source gets executed.
     * @param inputFormat The input format that the data source executes.
     * @param type The type of the elements produced by this input format.
     */
    public DataSource(ExecutionEnvironment context, InputFormat<OUT, ?> inputFormat, TypeInformation<OUT> type, String dataSourceLocationName) {
        super(context, type);

        this.dataSourceLocationName = dataSourceLocationName;

        if (inputFormat == null) {
            throw new IllegalArgumentException("The input format may not be null.");
        }

        this.inputFormat = inputFormat;

        if (inputFormat instanceof NonParallelInput) {
            this.parallelism = 1;
        }
    }

 flink將數據源主要分為內置數據源和第三方數據源,內置數據源有 文件,網絡socket端口及集合類型數據;第三方數據源實用Connector的方式來連接如kafka Connector,es connector等,自己定義的話,可以實現SourceFunction,封裝成Connector來做。

 

3.DataStream轉換

DataStream:同一個類型的流元素,DataStream可以通過transformation轉換成另外的DataStream,示例如下

@link DataStream#map

@link DataStream#filter

 StreamOperator:流式算子的基本接口,三個實現類

AbstractStreamOperator:

OneInputStreamOperator:

TwoInputStreamOperator:

/**
 * Basic interface for stream operators. Implementers would implement one of
 * {@link org.apache.flink.streaming.api.operators.OneInputStreamOperator} or
 * {@link org.apache.flink.streaming.api.operators.TwoInputStreamOperator} to create operators
 * that process elements.
 *
 * <p>The class {@link org.apache.flink.streaming.api.operators.AbstractStreamOperator}
 * offers default implementation for the lifecycle and properties methods.
 *
 * <p>Methods of {@code StreamOperator} are guaranteed not to be called concurrently. Also, if using
 * the timer service, timer callbacks are also guaranteed not to be called concurrently with
 * methods on {@code StreamOperator}.
 *
 * @param <OUT> The output type of the operator
 */

 4.DataStreamSink輸出

    /**
     * Adds the given sink to this DataStream. Only streams with sinks added
     * will be executed once the {@link StreamExecutionEnvironment#execute()}
     * method is called.
     *
     * @param sinkFunction
     *            The object containing the sink's invoke function.
     * @return The closed DataStream.
     */
    public DataStreamSink<T> addSink(SinkFunction<T> sinkFunction) {

        // read the output type of the input Transform to coax out errors about MissingTypeInfo
        transformation.getOutputType();

        // configure the type if needed
        if (sinkFunction instanceof InputTypeConfigurable) {
            ((InputTypeConfigurable) sinkFunction).setInputType(getType(), getExecutionConfig());
        }

        StreamSink<T> sinkOperator = new StreamSink<>(clean(sinkFunction));

        DataStreamSink<T> sink = new DataStreamSink<>(this, sinkOperator);

        getExecutionEnvironment().addOperator(sink.getTransformation());
        return sink;
    }

5.執行

/**
     * Executes the JobGraph of the on a mini cluster of ClusterUtil with a user
     * specified name.
     *
     * @param jobName
     *            name of the job
     * @return The result of the job execution, containing elapsed time and accumulators.
     */
    @Override
    public JobExecutionResult execute(String jobName) throws Exception {
        // transform the streaming program into a JobGraph
        StreamGraph streamGraph = getStreamGraph();
        streamGraph.setJobName(jobName);

        JobGraph jobGraph = streamGraph.getJobGraph();
        jobGraph.setAllowQueuedScheduling(true);

        Configuration configuration = new Configuration();
        configuration.addAll(jobGraph.getJobConfiguration());
        configuration.setString(TaskManagerOptions.MANAGED_MEMORY_SIZE, "0");

        // add (and override) the settings with what the user defined
        configuration.addAll(this.configuration);

        if (!configuration.contains(RestOptions.BIND_PORT)) {
            configuration.setString(RestOptions.BIND_PORT, "0");
        }

        int numSlotsPerTaskManager = configuration.getInteger(TaskManagerOptions.NUM_TASK_SLOTS, jobGraph.getMaximumParallelism());

        MiniClusterConfiguration cfg = new MiniClusterConfiguration.Builder()
            .setConfiguration(configuration)
            .setNumSlotsPerTaskManager(numSlotsPerTaskManager)
            .build();

        if (LOG.isInfoEnabled()) {
            LOG.info("Running job on local embedded Flink mini cluster");
        }

        MiniCluster miniCluster = new MiniCluster(cfg);

        try {
            miniCluster.start();
            configuration.setInteger(RestOptions.PORT, miniCluster.getRestAddress().get().getPort());

            return miniCluster.executeJobBlocking(jobGraph);
        }
        finally {
            transformations.clear();
            miniCluster.close();
        }
    }

6.總結

  Flink的執行方式類似於管道,它借鑒了數據庫的一些執行原理,實現了自己獨特的執行方式。

7.展望

Stream涉及的內容還包括Watermark,window等概念,因篇幅限制,這篇僅介紹flink DataStream API使用及原理。

下篇將介紹Watermark,下下篇是windows窗口計算。

參考資料

【1】https://baijiahao.baidu.com/s?id=1625545704285534730&wfr=spider&for=pc

【2】https://blog.51cto.com/13654660/2087705

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

Spring IoC容器與應用上下文的設計與實現

一、前言

  寫這篇博文的主要目的如下:

  • 通過相關類和接口分析IoC容器到底長什麼樣。
  • 闡述筆者對Spring上下文和容器的理解。
  • 介紹重要的類輔助理解SpringBoot的啟動流程。

 

二、Spring IoC容器的設計

  看看下面這張圖(摘自《Spring技術內幕》),IoC容器的設計分為兩條線,

  1.   BeanFactory ==> HierarchicalBeanFactory ==>ConfigurableBeanFactory ,這條線可以理解成IoC容器的設計路線。
  2.   BeanFactory ==> ListableBeanFactory ==> ApplicationContext ==> ConfigurableApplicationContext ,這條可以成為Spring應用上下文的設計路線。

  為什麼這樣要分兩條線呢,主要是將容器和上下文區分開來。因為在在Spring項目中,上下文對容器不僅是擴展的關係,更重要的是持有的關係,上下文是以屬性的形式持有了容器,開發者可以通過上下文對象獲取到容器。筆者十分傾向於將二者分開來理解。當然也可以將應用上下文理解成容器的高級表現形式。

 

2.1,IoC容器的設計線路

  BeanFactory定義了IoC容器的基本規範,包括getBean()按類型和按名稱的獲取Bean的方法。

   

  HierarchicalBeanFactory 在BeanFactory的基礎上增加了getParentBeanFactory()方法,使BeanFactory具備了雙親IoC容器管理的功能。

  ConfigurableBeanFactory接口提供了配置BeanFactory的各種方法。比如setParentBeanFactory()方法,配置上面提到的雙親IoC容器,addBeanPostProcessor()方法,配置Bean後置處理器等。

  到這裏先埋個包袱:到ConfigurableBeanFactory接口為止,IoC容器還沒有具備作為“容器”最基本的功能,那就是能裝東西。

 

2.2、應用上下文設計路線

  上面說了應用上下文是IoC容器的高級表現形式,ListableBeanFactory具備了操作BeanDefinition 的能力,比如getBeanDefinitionCount()方法,可以獲取Bean的總數等。

  ApplicationContext 類那就厲害了,如下代碼所示,實現了一大堆接口

1 public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
2         MessageSource, ApplicationEventPublisher, ResourcePatternResolver
  • MessageSource,支持不同的信息源。具備支持國際化的實現,為開發多語言版本的應用提供服務。
  • ResourcePatternResolver,訪問數據源。具備了從不同地方得到Bean定義資源的能力,比如:xml,java config,註解等等。
  • ApplicationEventPublisher,發布事件。使應用上下文具備了事件機制。事件機製為Bean聲明周期的管理提供了便利。

  WebApplicationContext擴展了對web應用的支持。

  ConfigurableApplicationContext就更重要了,看過Spring源碼的都知道一個重要的方法叫refresh,沒錯就是在這個接口中定義的。最重要的是擴展了配置上下文的功能,和控制上下文生命周期的能力等等。

 

三、IoC容器的具體實現類 DefaultListableBeanFactory(重點)

  首先證明一點,為什麼說DefaultListableBeanFactory類是具體的實現類呢?

  隨便啟動一個SpringBoot項目找到第25行代碼(在SpringBoot的啟動流程系列博文中有介紹)

 1 public ConfigurableApplicationContext run(String... args) {
 2     //記錄程序運行時間
 3     StopWatch stopWatch = new StopWatch();
 4     stopWatch.start();
 5     // ConfigurableApplicationContext Spring 的上下文
 6     ConfigurableApplicationContext context = null;
 7     Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
 8     configureHeadlessProperty();
 9     //從META-INF/spring.factories中獲取監聽器
10     //1、獲取並啟動監聽器
11     SpringApplicationRunListeners listeners = getRunListeners(args);
12     listeners.starting();
13     try {
14         ApplicationArguments applicationArguments = new DefaultApplicationArguments(
15                 args);
16         //2、構造容器環境
17         ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
18         //處理需要忽略的Bean
19         configureIgnoreBeanInfo(environment);
20         //打印banner
21         Banner printedBanner = printBanner(environment);
22         ///3、初始化容器
23         context = createApplicationContext();
24         //實例化SpringBootExceptionReporter.class,用來支持報告關於啟動的錯誤
25         exceptionReporters = getSpringFactoriesInstances(
26                 SpringBootExceptionReporter.class,
27                 new Class[]{ConfigurableApplicationContext.class}, context);
28         //4、刷新容器前的準備階段
29         prepareContext(context, environment, listeners, applicationArguments, printedBanner);
30         //5、刷新容器
31         refreshContext(context);
32         //刷新容器后的擴展接口
33         afterRefresh(context, applicationArguments);
34         stopWatch.stop();
35         if (this.logStartupInfo) {
36             new StartupInfoLogger(this.mainApplicationClass)
37                     .logStarted(getApplicationLog(), stopWatch);
38         }
39         listeners.started(context);
40         callRunners(context, applicationArguments);
41     } catch (Throwable ex) {
42         handleRunFailure(context, ex, exceptionReporters, listeners);
43         throw new IllegalStateException(ex);
44     }
45 
46     try {
47         listeners.running(context);
48     } catch (Throwable ex) {
49         handleRunFailure(context, ex, exceptionReporters, null);
50         throw new IllegalStateException(ex);
51     }
52     return context;
53 }

  debug

  如2標註點所示IoC容器的真實面孔就是這個DefaultListableBeanFactory類了。當然他還有一個子類XmlBeanFactory,不過都已經被標註為棄用了(@Deprecated)在《Spring技術內幕》這本書裏面也是着重的講的這個類,可能當時作者是以SpringMVC項目來講解的吧。XmlBeanFactory顧名思義就是提供了對xml配置方式的支持。呃。。。又勾起了用SpringMVC的痛苦回憶。

  言歸正傳,

  如下圖,看看他的繼承關係

   章節二中提到了很多的IoC容器系列,這樣總結一下吧,俗話說一流企業做標準,二流企業做產品,章節二中的那一坨就是IoC容器的實現標準,本章節我們要總結的類DefaultListableBeanFactory就是IoC容器的具體產品。

  看見上圖中那一堆接口和類是不是有點懵,沒關係,咱們慢慢梳理一下。

 

3.1,作為IoC容器的基礎設計路線

  這條線路在上一章節中已經梳理過了。只是多出了ConfigurableListableBeanFactory接口,ConfigurableListableBeanFactory接口主要是增加指定忽略類型和接口等

 

3.2、作為IoC容器的高級設計路線

  這條設計路線乍一看還是挺複雜的,的確是這樣。

  1, BeanFactory ==> AutowireCapableBeanFactory ==> AbstractAutowireCapableBeanFactory ==> DefaultListableBeanFactory 

  在這條線路中,AutowireCapableBeanFactory接口定義了自動注入bean(autowireBean()),創建bean(createBean()),初始化bean(initializeBean())方法等。那麼真正實現這些方法的類便是AbstractAutowireCapableBeanFactory。

  AbstractAutowireCapableBeanFactory抽象類中實現了AutowireCapableBeanFactory接口定義的方法。在此基礎上通過繼承AbstractBeanFactory具備了操作Bean的能力。

  2, SingletonBeanRegistry ==> DefaultSingletonBeanRegistry ==> FactoryBeanRegistrySupport ==> AbstractBeanFactory ==> AutowireCapableBeanFactory ==> DefaultListableBeanFactory 

  這條關係鏈有點長,在這條鏈中我們要關心的是SingletonBeanRegistry接口,顧名思義,這個接口是單例Bean的註冊接口。當然也不止註冊這麼簡單。如下圖中所示,除了註冊單例之外,還定義獲取單例的方法。

  注意:為什麼只有singleton的註冊中心,而沒有prototype類型的Bean的註冊中心呢?因為單例Bean(singleton)是Spring幫我們創建的並維護的,原型Bean(prototype)是每次獲取都會創建出來一個實例。本質是不同的。

  3, AliasRegistry ==> SimpleAliasRegistry ==> DefaultSingletonBeanRegistry ==> FactoryBeanRegistrySupport ==> AbstractBeanFactory ==> AutowireCapableBeanFactory ==> DefaultListableBeanFactory 

   這條路線呢,主要是提供管理別稱的能力。因為不是重點,在此就不詳細分析了。

  4, AliasRegistry ==> BeanDefinitionRegistry ==> DefaultListableBeanFactory 

  BeanDefinitionRegistry接口要重點說一下,該接口是BeanDefinition的註冊中心。使DefaultListableBeanFactory具備操作BeanDefinition的能力。看一下它有什麼方法。

  包括了註冊,刪除,獲取BeanDefinition的方法。當然這隻是個接口,這些方法的具體實現在DefaultListableBeanFactory中。

 

3.3、DefaultListableBeanFactory幾個重要的父類和接口

3.3.1, AbstractBeanFactory 抽象類

  如上圖所示,AbstractBeanFactory中實現了BeanFactory中定義的幾個重要的方法。常用的註解 @Autowired @Resource(name = “xxx”) 大家都知道一個是按類查找,一個是按名獲取。具體實現這兩個註解的方法就是上圖中圈出來的幾個方法。幾個getBean()方法最終都進入了doGetBean()方法。doGetBean()方法是實際獲得Bean的地方,也是觸發依賴注入發生的地方。在SpringBoot啟動流程總會對這個方法進行詳細的介紹。

3.3.2, AbstractAutowireCapableBeanFactory 抽象類

  AbstractBeanFactory中實現了getBean()方法,AbstractAutowireCapableBeanFactory中實現了Bean的創建方法。

  當我們需要定義一個Bean通常會有這樣寫 @Bean(name = “test”, initMethod = “init”, destroyMethod = “destroy”) 。AbstractAutowireCapableBeanFactory中完成了一個Bean從 create(createBean()) ==> createInstance(createBeanInstance()) ==> init(invokeInitMethods()) 的所有工作。所以這個抽象類的作用不言而喻。具體的創建過程,會在SpringBoot的啟動流程中詳細介紹。

3.3.3, DefaultSingletonBeanRegistry 讓IoC容器擁有作為“容器”的能力

  其實我們經常說的Spring 容器,這個容器其實更多的是BeanFactory所代表的意義:Bean生產工廠。是滴,通常我們理解的容器應該是能裝東西的,但是spring 容器不是代表代表水桶一樣的東西,而是像富士康一樣,生產東西的地方。比如我們需要一個Bean,我們只需要告訴spring,spring就會給我們。所以到目前為止我們還沒有看到IoC作為“容器”的能力。以上言論純屬自己理解,不喜勿噴。

  DefaultSingletonBeanRegistry屬性先貼出來

 1 public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
 2     /**
 3      * Cache of singleton objects: bean name --> bean instance
 4      * 緩存 單例對象
 5      */
 6     private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);//好習慣,創建ConcurrentHashMap,指定初始化因子,縱觀Spring源碼,創建HashMap,都有初始化因子。get
 7 
 8     /**
 9      * Cache of singleton factories: bean name --> ObjectFactory
10      * 緩存 單例工廠
11      */
12     private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
13 
14     /**
15      * Cache of early singleton objects: bean name --> bean instance
16      * 緩存 提前暴露的對象
17      */
18     private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
19 
20     /**
21      * Set of registered singletons, containing the bean names in registration order
22      * 已經註冊的單例對象集合,按照註冊順序排序的,並且是不可重複的。
23      */
24     private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
25 
26     /**
27      * Names of beans that are currently in creation
28      *
29      */
30     private final Set<String> singletonsCurrentlyInCreation =
31             Collections.newSetFromMap(new ConcurrentHashMap<>(16));
32 
33     /**
34      * Names of beans currently excluded from in creation checks
35      */
36     private final Set<String> inCreationCheckExclusions =
37             Collections.newSetFromMap(new ConcurrentHashMap<>(16));
38 
39     /**
40      * List of suppressed Exceptions, available for associating related causes
41      */
42     @Nullable
43     private Set<Exception> suppressedExceptions;
44 
45     /**
46      * Flag that indicates whether we're currently within destroySingletons
47      */
48     private boolean singletonsCurrentlyInDestruction = false;
49 
50     /**
51      * Disposable bean instances: bean name --> disposable instance
52      * spring是作為一個註冊中心的樣子,在容器shutdown的時候,直接從這裏面找到需要執行destory鈎子的Bean
53      */
54     private final Map<String, Object> disposableBeans = new LinkedHashMap<>();
55 
56     /**
57      * Map between containing bean names: bean name --> Set of bean names that the bean contains
58      * 名稱為name的bean,所持有的beans 的映射關係
59      */
60     private final Map<String, Set<String>> containedBeanMap = new ConcurrentHashMap<>(16);
61 
62     /**
63      * Map between dependent bean names: bean name --> Set of dependent bean names
64      * 名稱為name的bean與其所依賴的bean的映射關係
65      */
66     private final Map<String, Set<String>> dependentBeanMap = new ConcurrentHashMap<>(64);
67 
68     /**
69      * Map between depending bean names: bean name --> Set of bean names for the bean's dependencies
70      */
71     private final Map<String, Set<String>> dependenciesForBeanMap = new ConcurrentHashMap<>(64);
72 }

 

  屬性singletonObjects ,沒錯,就是這個東東,最終存儲單例(singleton)Bean的地方,在SpringBoot啟動流程中,會詳細介紹存取的過程。上面說了原型(prototype)Bean是不需要緩存的,不解釋了。到這裏我們初步看到了IoC作為“容器”該有的樣子。

  DefaultSingletonBeanRegistry上面提到的SingletonBeanRegistry接口的相關方法,並且增加了很多對單例的操作的方法。

3.3.4, DefaultListableBeanFactory (重點)

  上面我們從IoC容器的宏觀設計角度闡述了DefaultListableBeanFactory作為IoC容器的具體實現的設計思想。在這裏來分析一下這個類本身的設計。

  首先看看該類的屬性

 1 public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
 2         implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
 3     /**
 4      * Map from serialized id to factory instance
 5      * 緩存 序列化ID到 DefaultListableBeanFactory 實例的映射
 6      */
 7     private static final Map<String, Reference<DefaultListableBeanFactory>> serializableFactories =
 8             new ConcurrentHashMap<>(8);
 9 
10     /**
11      * Optional id for this factory, for serialization purposes
12      */
13     @Nullable
14     private String serializationId;
15 
16     /**
17      * Whether to allow re-registration of a different definition with the same name
18      */
19     private boolean allowBeanDefinitionOverriding = true;
20 
21     /**
22      * Whether to allow eager class loading even for lazy-init beans
23      */
24     private boolean allowEagerClassLoading = true;
25 
26     /**
27      * Optional OrderComparator for dependency Lists and arrays
28      */
29     @Nullable
30     private Comparator<Object> dependencyComparator;
31 
32     /**
33      * Resolver to use for checking if a bean definition is an autowire candidate
34      * 被用來解決去校驗一個BeanDefinition是不是自動裝載的候選人
35      */
36     private AutowireCandidateResolver autowireCandidateResolver = new SimpleAutowireCandidateResolver();
37 
38     /**
39      * Map from dependency type to corresponding autowired value
40      * 緩存 類型對應的自動裝載的Bean
41      */
42     private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<>(16);
43 
44     /**
45      * Map of bean definition objects, keyed by bean name
46      * 緩存 beanName到BeanDefinition的映射關係
47      */
48     private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
49 
50     /**
51      * Map of singleton and non-singleton bean names, keyed by dependency type
52      * 緩存 類型 和 beanName的映射關係
53      */
54     private final Map<Class<?>, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64);
55 
56     /**
57      * Map of singleton-only bean names, keyed by dependency type
58      * 緩存 類型 和 單例Bean names的映射
59      */
60     private final Map<Class<?>, String[]> singletonBeanNamesByType = new ConcurrentHashMap<>(64);
61 
62     /**
63      * List of bean definition names, in registration order
64      * 緩存 beanDefinition name的list
65      */
66     private volatile List<String> beanDefinitionNames = new ArrayList<>(256);
67 
68     /**
69      * List of names of manually registered singletons, in registration order
70      */
71     private volatile Set<String> manualSingletonNames = new LinkedHashSet<>(16);
72 
73     /**
74      * Cached array of bean definition names in case of frozen configuration
75      */
76     @Nullable
77     private volatile String[] frozenBeanDefinitionNames;
78 
79     /**
80      * Whether bean definition metadata may be cached for all beans
81      */
82     private volatile boolean configurationFrozen = false;
83 }

  在Spring中,實際上是把DefaultListableBeanFactory作為一個默認的功能完整的IoC容器來使用。 DefaultListableBeanFactory作為一個功能完整的容器具備了除以上父類所具有功能外,還加入了對BeanDefinition的管理和維護。從上面的代碼可以看到一個重要的屬性:beanDefinitionMap。beanDefinitionMap緩存了Bean name到 BeanDefinition的映射。到這裡是不是發現了IoC容器另外一個作為“容器”的能力。在我的理解範圍內,IoC容器作為“容器”真正裝的兩個最總要的能力算是總結完了,一個是裝單例(Singleton)Bean,一個是裝BeanDefinition。

 

3.3.5, BeanDefinition 

  Spring通過定義BeanDefinition來管理基於Spring的應用中的各種對象以及他們之間的相互依賴關係。BeanDefinition抽象了我們對Bean的定義,是讓容器起作用的主要數據類型。我么都知道在計算機世界里,所有的功能都是建立在通過數據對現實進行抽象的基礎上的。IoC容器是用來管理對象依賴關係的,對IoC容器來說,BeanDefinition就是對依賴反轉模式中管理的對象依賴關係的數據抽象,也是容器實現依賴反轉功能的核心數據結構,依賴反轉功能都是圍繞對這個BeanDefinition的處理來完成的。這些BeanDefinition就像是容器里裝的水,有了這些基本數據,容器才能發揮作用。簡單一句話來說,BeanDefinition就是Bean的元數據,BeanDefinition存放了對Bean的基本描述,包括Bean擁有什麼屬性,方法,Bean的位置等等Bean的各種信息。IoC容器可以通過BeanDefinition生成Bean。

  BeanDefinition究竟長什麼樣呢?

  在同第三章debug的地方一樣,點開beanFactory,然後查看beanDefinitionMap屬性。

  OK,BeanDefinition就是長這樣了。具體怎麼通過它生成Bean,在SpringBoot啟動流程中會詳細介紹。

 

四、SpringBoot web工程中的上下文 AnnotationConfigServletWebServerApplicationContext

  在SpringBoot工程中,應用類型分為三種,如下代碼所示。

 1 public enum WebApplicationType {
 2     /**
 3      * 應用程序不是web應用,也不應該用web服務器去啟動
 4      */
 5     NONE,
 6     /**
 7      * 應用程序應作為基於servlet的web應用程序運行,並應啟動嵌入式servlet web(tomcat)服務器。
 8      */
 9     SERVLET,
10     /**
11      * 應用程序應作為 reactive web應用程序運行,並應啟動嵌入式 reactive web服務器。
12      */
13     REACTIVE
14 }

  對應三種應用類型,SpringBoot項目有三種對應的應用上下文,我們以web工程為例,即其上下文為AnnotationConfigServletWebServerApplicationContext

 1 public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework.boot."
 2         + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
 3 public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
 4         + "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";
 5 public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
 6         + "annotation.AnnotationConfigApplicationContext";
 7         
 8 protected ConfigurableApplicationContext createApplicationContext() {
 9     Class<?> contextClass = this.applicationContextClass;
10     if (contextClass == null) {
11         try {
12             switch (this.webApplicationType) {
13                 case SERVLET:
14                     contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
15                     break;
16                 case REACTIVE:
17                     contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
18                     break;
19                 default:
20                     contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
21             }
22         } catch (ClassNotFoundException ex) {
23             throw new IllegalStateException(
24                     "Unable create a default ApplicationContext, "
25                             + "please specify an ApplicationContextClass",
26                     ex);
27         }
28     }
29     return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
30 }

  我們先看一下AnnotationConfigServletWebServerApplicationContext的設計。 

   在2.2中已經介紹了ApplicationContext的設計

  關於AnnotationConfigServletWebServerApplicationContext詳細的設計路線在這裏就不像DefaultListableBeanFactory容器那麼詳細的去講解了。在第二章說過,應用上下文可以理解成IoC容器的高級表現形式,拿上圖和DefaultListableBeanFactory的繼承關係圖,不難發現,應用上下文確實是在IoC容器的基礎上豐富了一些高級功能。在第二章中,我們還說過應用上下文對IoC容器是持有的關係。繼續看第二章debug的截圖,context就是AnnotationConfigServletWebServerApplicationContext的神秘面孔,他的一個屬性beanFactory就是IoC容器(DefaultListableBeanFactory)。所以他們之間是持有,和擴展的關係。

  接下來看GenericApplicationContext類

1 public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
2     private final DefaultListableBeanFactory beanFactory;
3     ...
4 }

   第一行赫然定義了beanFactory屬性,正是DefaultListableBeanFactory對象。

  關於上下文還有另外一個最重要的方法refresh,上文中說道該方法是在ConfigurableApplicationContext接口中定義的,那麼在哪實現的該方法呢?

  看AbstractApplicationContext類。

 1 @Override
 2 public void refresh() throws BeansException, IllegalStateException {
 3     synchronized (this.startupShutdownMonitor) {
 4         // Prepare this context for refreshing.
 5         //刷新上下文環境
 6         prepareRefresh();
 7 
 8         // Tell the subclass to refresh the internal bean factory.
 9         //這裡是在子類中啟動 refreshBeanFactory() 的地方
10         ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
11 
12         // Prepare the bean factory for use in this context.
13         //準備bean工廠,以便在此上下文中使用
14         prepareBeanFactory(beanFactory);
15 
16         try {
17             // Allows post-processing of the bean factory in context subclasses.
18             //設置 beanFactory 的後置處理
19             postProcessBeanFactory(beanFactory);
20 
21             // Invoke factory processors registered as beans in the context.
22             //調用 BeanFactory 的后處理器,這些處理器是在Bean 定義中向容器註冊的
23             invokeBeanFactoryPostProcessors(beanFactory);
24 
25             // Register bean processors that intercept bean creation.
26             //註冊Bean的后處理器,在Bean創建過程中調用
27             registerBeanPostProcessors(beanFactory);
28 
29             // Initialize message source for this context.
30             //對上下文中的消息源進行初始化
31             initMessageSource();
32 
33             // Initialize event multicaster for this context.
34             //初始化上下文中的事件機制
35             initApplicationEventMulticaster();
36 
37             // Initialize other special beans in specific context subclasses.
38             //初始化其他特殊的Bean
39             onRefresh();
40 
41             // Check for listener beans and register them.
42             //檢查監聽Bean並且將這些監聽Bean向容器註冊
43             registerListeners();
44 
45             // Instantiate all remaining (non-lazy-init) singletons.
46             //實例化所有的(non-lazy-init)單件
47             finishBeanFactoryInitialization(beanFactory);
48 
49             // Last step: publish corresponding event.
50             //發布容器事件,結束Refresh過程
51             finishRefresh();
52         } catch (BeansException ex) {
53             if (logger.isWarnEnabled()) {
54                 logger.warn("Exception encountered during context initialization - " +
55                         "cancelling refresh attempt: " + ex);
56             }
57 
58             // Destroy already created singletons to avoid dangling resources.
59             destroyBeans();
60 
61             // Reset 'active' flag.
62             cancelRefresh(ex);
63 
64             // Propagate exception to caller.
65             throw ex;
66         } finally {
67             // Reset common introspection caches in Spring's core, since we
68             // might not ever need metadata for singleton beans anymore...
69             resetCommonCaches();
70         }
71     }
72 }

   OK,應用上下文就介紹到這裏。

 

 五、IoC容器的初始化過程

   在這裏我們先口述一下IoC容器的初始化過程吧,源碼分析,請移步SpringBoot啟動流程分析。

  簡單來說IoC容器的初始化過程是由前面介紹的refresh()方法啟動的,這個方法標志著IoC容器的正式啟動。具體來說,這個啟動包括三個過程

1 BeanDefinition的Resource定位
2 BeanDefinition的載入
3 向IoC容器註冊BeanDefinition

 

   1、第一個過程:Resource定位

  這個定位指的是BeanDefinition的資源定位,它由ResourceLoader通過統一的Resource接口完成,這個Resource對各種形式的BeanDefinition的使用都提供了統一接口。對於這些BeanDefinition的存在形式,可以是通過像SpringMVC中的xml定義的Bean,也可以是像在類路徑中的Bean定義信息,比如使用@Component等註解定義的。這個過程類似於容器尋找數據的過程,就像用水桶裝水先要把水找到一樣。

  結合SpringBoot說一下這個過程,對於SpringBoot,我們都知道他的包掃描是從主類所在的包開始掃描的,那這個定位的過程在SpringBoot中具體是這樣的,在refresh容器之前(prepareContext()方法中),會先將主類解析成BeanDefinition,然後在refresh方法中並且是掃描Bean之前,解析主類的BeanDefinition獲取basePackage的路徑。這樣就完成了定位的過程。(先不討論SpringBoot中指定掃描包路徑和自動裝配)

  2、第二個過程:BeanDefinition的載入

  這個載入過程是把用戶定義好的Bean表示成IoC容器內部的數據結構,而這個容器內部的數據結構就是BeanDefinition。

  在SpringBoot中,上面我們說到通過主類找到了basePackage,SpringBoot會將該路徑拼接成:classpath*:org/springframework/boot/demo/**/*.class這樣的形式,然後一個叫做PathMatchingResourcePatternResolver的類會將該路徑下所有的.class文件都加載進來,然後遍歷判斷是不是有@Component註解,如果有的話,就是我們要裝載的BeanDefinition。大致過程就是這樣的了。

  注意:@Configuration,@Controller,@Service等註解底層都是@Component註解,只不過包裝了一層罷了。

   3、第三個過程:註冊BeanDefinition

   這個過程通過調用上文提到的BeanDefinitionRegister接口的實現來完成。這個註冊過程把載入過程中解析得到的BeanDefinition向IoC容器進行註冊。通過上文的分析,我們可以看到,在IoC容器中將BeanDefinition注入到一個ConcurrentHashMap中,IoC容器就是通過這個HashMap來持有這些BeanDefinition數據的。比如DefaultListableBeanFactory 中的beanDefinitionMap屬性。

六、IoC容器的依賴注入

  上面對IoC容器的初始化過程進行了詳細的介紹,這個過程完成的主要的工作是在IoC容器中建立BeanDefinition數據映射。在此過程中並沒有看到IoC容器對Bean的依賴關係進行注入。依賴注入是Spring實現“控制反轉”的重要一環。Spring將依賴關係交給IoC容器來完成。

  依賴控制反轉的實現有很多種方式。在Spring中,IoC容器是實現這個模式的載體,它可以在對象生成或者初始化時直接將數據注入到對象中,也可以通過將對象注入到對象數據域中的方式來注入對方法調用的依賴。這種依賴注入是可以遞歸的,對象被逐層注入。

 

 

  原創不易,轉載請註明出處。

  如有錯誤的地方還請留言指正。

 

參考文獻:

  《Spring技術內幕–深入解析Spring框架與設計原理(第二版)》

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Asp.NETCore讓FromServices回來

起因

這两天,我忽然有點懷念 Asp.NET MVC 5 之前的時代,原因是我看到項目裏面有這麼一段代碼(其實不止一段,幾乎每個 Controller 都是)

    [Route("home")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        private readonly IConfiguration configuration;
        private readonly IHostingEnvironment environment;
        private readonly CarService carService;
        private readonly PostServices postServices;
        private readonly TokenService tokenService;
        private readonly TopicService topicService;
        private readonly UserService userService;

        public HomeController(IConfiguration configuration,
                              IHostingEnvironment environment,
                              CarService carService,
                              PostServices postServices,
                              TokenService tokenService,
                              TopicService topicService,
                              UserService userService)
        {
            this.configuration = configuration;
            this.environment = environment;
            this.carService = carService;
            this.postServices = postServices;
            this.tokenService = tokenService;
            this.topicService = topicService;
            this.userService = userService;
        }

        [HttpGet("index")]
        public ActionResult<string> Index()
        {
            return "Hello world!";
        }
    }

在構造函數裏面聲明了一堆依賴注入的實例,外面還得聲明相應的接收字段,使用代碼克隆掃描,零零散散的充斥在各個 Controller 的構造函數中。在 Asp.NET MVC 5 之前,我們可以把上面的代碼簡化為下面的形式:

    [Route("home")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        [FromServices] public IConfiguration Configuration { get; set; }
        [FromServices] public IHostingEnvironment Environment { get; set; }
        [FromServices] public CarService CarService { get; set; }
        [FromServices] public PostServices PostServices { get; set; }
        [FromServices] public TokenService TokenService { get; set; }
        [FromServices] public TopicService TopicService { get; set; }
        [FromServices] public UserService UserService { get; set; }

        public HomeController()
        {
        }

        [HttpGet("index")]
        public ActionResult<string> Index()
        {
            return "Hello world!";
        }
    }

但是,在 .NETCore 中,上面的這斷代碼是會報錯的,原因就是特性:FromServicesAttribute 只能應用於 AttributeTargets.Parameter,導航到 FromServicesAttribute 查看源碼

namespace Microsoft.AspNetCore.Mvc
{
    /// <summary>
    /// Specifies that an action parameter should be bound using the request services.
    /// </summary>
    /// <example>
    /// In this example an implementation of IProductModelRequestService is registered as a service.
    /// Then in the GetProduct action, the parameter is bound to an instance of IProductModelRequestService
    /// which is resolved from the request services.
    ///
    /// <code>
    /// [HttpGet]
    /// public ProductModel GetProduct([FromServices] IProductModelRequestService productModelRequest)
    /// {
    ///     return productModelRequest.Value;
    /// }
    /// </code>
    /// </example>
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
    public class FromServicesAttribute : Attribute, IBindingSourceMetadata
    {
        /// <inheritdoc />
        public BindingSource BindingSource => BindingSource.Services;
    }
}

那麼問題來了,AttributeUsage 是什麼時候移除了 AttributeTargets.Property 呢?答案是:2015年11月17日,是一個叫做 Pranav K 的哥們革了 FromServiceAttribute 的命,下面是他的代碼提交記錄

Limit [FromServices] to apply only to parameters
https://github.com/aspnet/Mvc/commit/2a89caed05a1bc9f06d32e15d984cd21598ab6fb

這哥們的 Commit Message 很簡潔:限制 FromServices 僅作用於 parameters 。高手過招,人狠話不多,刀刀致命!從此,廣大 .NETCore 開發者告別了屬性注入。經過我不懈努力的搜索后,發現其實在 Pranav K 提交代碼兩天後,他居然自己開了一個 Issue,你說氣人不?

關於廢除 FromServices 的討論
https://github.com/aspnet/Mvc/issues/3578

在這個貼子裏面,許多開發者表達了自己的不滿,我還看到了有人像我一樣,表達了自己想要一個簡潔的構造函數的這樣樸素的請求;但是,對於屬性注入可能導致濫用的問題也產生了激烈的討論,還有屬性注入要求成員必須標記為 public 這些硬性要求,不得不說,這個帖子成功的引起了人們的注意,但是很明顯,作者不打算修改 FromServices 支持屬性注入。

自己動手,豐衣足食

沒關係,官方沒有自帶的話,我們自己動手做一個也是一樣的效果,在此之前,我們還應該關注另外一種從 service 中獲取實例的方式,就是常見的通過 HttpContext 請求上下文獲取服務實例的方式:

 var obj = HttpContext.RequestServices.GetService(typeof(Type));

上面的這種方式,其實是反模式的,官方也建議盡量避免使用,說完了廢話,就自動動手擼一個屬性注入特性類:PropertyFromServiceAttribute

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class PropertyFromServiceAttribute : Attribute, IBindingSourceMetadata
{
    public BindingSource BindingSource => BindingSource.Services;
}

沒有多餘的代碼,就是標記為 AttributeTargets.Property 即可

應用到類成員
    [Route("home")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        [PropertyFromService] public IConfiguration Configuration { get; set; }
        [PropertyFromService] public IHostingEnvironment Environment { get; set; }
        [PropertyFromService] public CarService CarService { get; set; }
        [PropertyFromService] public PostServices PostServices { get; set; }
        [PropertyFromService] public TokenService TokenService { get; set; }
        [PropertyFromService] public TopicService TopicService { get; set; }
        [PropertyFromService] public UserService UserService { get; set; }

        public HomeController()
        {

        }

        [HttpGet("index")]
        public ActionResult<string> Index()
        {
            return "Hello world!";
        }
    }

請大聲的回答,上面的代碼是不是非常的乾淨整潔!但是,像上面這樣使用屬性注入有一個小問題,在對象未初始化之前,該屬性為 null,意味着在類的構造函數中,該成員變量不可用,不過不要緊,這點小問題完全可用通過在構造函數中注入解決;更重要的是,並非每個實例都需要在構造函數中使用,是吧。

示例代碼

託管在 Github 上了 https://github.com/lianggx/Examples/tree/master/Ron.DI

** 如果你喜歡這篇文章,請給我點贊,讓更多同學可以看到,筆芯~

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

JS數據結構第二篇—鏈表

一、什麼是鏈表 

鏈表是一種鏈式存儲的線性表,是由一組節點組成的集合,每一個節點都存儲了下一個節點的地址;指向另一個節點的引用叫鏈;和數組中的元素內存地址是連續的相比,鏈表中的所有元素的內存地址不一定是連續的。結構模擬如圖:

一般來說,說到鏈表,就要提下數組,一般鏈表都是和數組進行對比。

在很多編程語言中,數組的長度時固定的,所以數組中的增加和刪除比較麻煩,需要頻繁的移動數組中的其他元素。

然而,JavaScript中的數組並不存在上述問題,JS中的數組相對其他語言使用上更方便,因為JS中的數組本質是一個類似數組的對象,這就使得JS的數組雖然使用更方便,但比其他語言(C++、Java、C#)的數組效率要低。

所以,在實際應用中如果發現數組很慢,就可以考慮使用鏈表來替代它。除了對數據的隨機訪問,鏈表幾乎可以用在任何可以使用一維數組的情況中。如果需要隨機訪問,數組仍然是更好的選擇。

 

二、鏈表的設計

為了對鏈表更好的使用,我們設計了類LinkedList, 對鏈表中節點的增刪改查方法進行了封裝。結構如圖:

其中size和head為LinkedList構造函數私有屬性,size記錄鏈表中有多少個節點,head指向鏈表的頭結點。

根據需要對外暴露了以下方法(可以根據需要自定義其他方法):

 單向LinkedList完整設計代碼:

/**
 * 自定義鏈表:對外公開的方法有
 * append(element) 在鏈表最後追加節點
 * insert(index, element) 根據索引index, 在索引位置插入節點
 * remove(element)  刪除節點
 * removeAt(index)  刪除指定索引節點
 * removeAll(element) 刪除所有匹配的節點
 * set(index, element) 根據索引,修改對應索引的節點值
 * get(index)  根據索引獲取節點信息
 * indexOf(element) 獲取某個節點的索引位置
 * clear()  清空所有節點
 * length()   返回節點長度
 * print() 打印所有節點信息
 * toString() 打印所有節點信息,同print
 * */
const LinkedList = function(){
    let head = null;
    let size = 0;   //記錄鏈表元素個數

    //Node模型
    function LinkNode(element, next){
        this.element = element;
        this.next = next;
    }

    //元素越界檢查, 越界拋出異常
    function outOfBounds(index){
        if (index < 0 || index >= size){
            throw("抱歉,目標位置不存在!");
        }
    }

    //根據索引,獲取目標對象
    function node(index){
        outOfBounds(index);

        let obj = head;
        for (let i = 0; i < index; i++){
            obj = obj.next;
        }

        return obj;
    }

    //新增一個元素
     function append(element){
        if (size == 0){
            head = new LinkNode(element, null);
        }
        else{
            let obj = node(size-1);
            obj.next = new LinkNode(element, null);
        }
         size++;
    }

    //插入一個元素
     function insert(index, element){
        if (index == 0){
            head = new LinkNode(element, head);
        }
        else{
            let obj = node(index-1);
            obj.next = new LinkNode(element, obj.next);
        }
         size++;
    }

    //修改元素
    function set(index, element){
        let obj = node(index);
        obj.element = element;
    }

    //根據值移除節點元素
    function remove(element){
        if (size < 1) return null;

        if (head.element == element){
            head = head.next;
            size--;
            return element;
        }
        else{
            let temp = head;
            while(temp.next){
                if (temp.next.element == element){
                    temp.next = temp.next.next;
                    size--;
                    return element;
                }
                else{
                    temp = temp.next;
                }
            }
        }
        return null;
    }

    //根據索引移除節點
     function removeAt(index){
         outOfBounds(index);
         let element = null;

         if (index == 0){
             element = head.element;
             head = head.next;
         }
         else{
             let prev = node(index-1);
             element = prev.next.element;
             prev.next = prev.next.next;
         }
         size--;
        return element;
    }

    //移除鏈表裡面的所有匹配值element的元素
     function removeAll(element){

        let virHead = new LinkNode(null, head); //創建一個虛擬頭結點,head為次節點
         let tempNode = virHead, ele = null;

         while(tempNode.next){
             if (tempNode.next.element == element){
                 tempNode.next = tempNode.next.next;
                 size--;
                 ele = element;
             }
             else{
                tempNode = tempNode.next;
             }
         }

         //重新賦值
         head = virHead.next;

        return ele;
    }

    //獲取某個元素
    function get(index){
        return node(index).element;
    }

    //獲取元素索引
    function indexOf(element){
        let obj = head, index = -1;

        for (let i = 0; i < size; i++){
            if (obj.element == element){
                index = i;
                break;
            }
            obj = obj.next;
        }
        return index;
    }

    //清除所有元素
    function clear(){
        head = null;
        size = 0;
    }

    //屬性轉字符串
    function getObjString(obj){

        let str = "";

        if (obj instanceof Array){
            str += "[";
            for (let i = 0; i < obj.length; i++){
                str += getObjString(obj[i]);
            }
            str = str.substring(0, str.length - 2);
            str += "], "
        }
        else if (obj instanceof Object){
            str += "{";
            for (var key in obj){
                let item = obj[key];
                str += "\"" + key + "\": " + getObjString(item);
            }
            str = str.substring(0, str.length-2);
            str += "}, "
        }
        else if (typeof obj == "string"){
            str += "\"" + obj + "\"" + ", ";
        }
        else{
            str += obj + ", ";
        }

        return str;
    }
    function toString(){
        let str = "", obj = head;
        for (let i = 0; i < size; i++){
            str += getObjString(obj.element);
            obj = obj.next;
        }
        if (str.length > 0) str = str.substring(0, str.length -2);
        return str;
    }
    //打印所有元素
    function print(){
        console.log(this.toString())
    }

    //對外公開方法
    this.append = append;
    this.insert = insert;
    this.remove = remove;
    this.removeAt = removeAt;
    this.removeAll = removeAll;
    this.set = set;
    this.get = get;
    this.indexOf = indexOf;
    this.length = function(){
        return size;
    }
    this.clear = clear;
    this.print = print;
    this.toString = toString;
}


////測試
// let obj = new LinkedList();
// let obj1 = { title: "全明星比賽", stores: [{name: "張飛vs岳飛", store: "2:3"}, { name: "關羽vs秦瓊", store: "5:5"}]};
//
// obj.append(99);
// obj.append("hello")
// obj.append(true)
// obj.insert(3, obj1);
// obj.insert(0, [12, false, "Good", 81]);
// obj.print();
// console.log("obj1.index: ", obj.indexOf(obj1));
// obj.remove(0);
// obj.removeAll(obj1);
// obj.print();

////測試2
console.log("\n\n......test2.....")
var obj2 = new LinkedList();
obj2.append(8); obj2.insert(1,99); obj2.append('abc'); obj2.append(8); obj2.append(false);
obj2.append(12); obj2.append(8); obj2.append('123'); obj2.append(8);
obj2.print();
obj2.removeAll(8); //刪除所有8
obj2.print();

View Code

 

另外,可以在LinkedList中增加一個虛擬節點,即在頭結點之前增加一個節點,一直保留,結構如圖:

這裏代碼就不提供了,在上一份鏈表代碼中的removeAll(刪除鏈表中指定值的所有節點)方法中有用到虛擬頭結點, 下面的練習題中也有應用到虛擬頭結點,應用場景還是蠻多的。

 

三、鏈表練習題

推薦一個神奇的網站,可以以動畫的方式演示各種數據結構增刪改查變化,先來張展示鏈表的增刪效果圖看看:

網址:https://visualgo.net/zh

 

接下來做幾個鏈表的練習題,題目來自力扣,可以先自己先做一下,看看自己得分,再對比下官方提供的代碼demo

3.1 刪除排序鏈表中的重複元素_第83題

參考demo:

/**
 * 給定一個排序鏈表,刪除所有重複的元素,使得每個元素只出現一次。
 示例 1:
 輸入: 1->1->2
 輸出: 1->2

 示例 2:
 輸入: 1->1->2->3->3
 輸出: 1->2->3

 力扣得分:
 執行用時 :108 ms, 在所有 JavaScript 提交中擊敗77.12%的用戶
 內存消耗 :37.4 MB, 在所有 JavaScript 提交中擊敗了5.03%的用戶
 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {

    let virHead = new ListNode(0); //增加一個虛擬節點
    virHead.next = head;
    let temp = virHead, obj = {};

    while(temp.next){
        if (obj[temp.next.val]){ //表示為重複節點,刪除這個節點
            temp.next = temp.next.next;
        }
        else{ //
            obj[temp.next.val] = 1;
            temp = temp.next;
        }
    }
    return virHead.next;
}

//測試
var obj = new ListNode(1);
obj.next = new ListNode(2);
obj.next.next = new ListNode(1);
obj.next.next.next = new ListNode(3);
obj.next.next.next.next = new ListNode(1);
obj.next.next.next.next.next = new ListNode(2);
obj.next.next.next.next.next.next = new ListNode(3);
console.log(obj);
console.log(".>>>>>>刪除重複節點:")
console.log(deleteDuplicates(obj));

View Code

 

3.2 判斷是否環形鏈表_第141題

參考demo:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    //快慢指針,快指針每次走兩步,慢指針每次走一步
    let obj1 = head, obj2 = head; //obj1快指針,obj2為慢指針

    while(obj2){
      obj2 = obj2.next;

      if (obj1){
          obj1 = obj1.next;
      }

      if (obj1){
          obj1 = obj1.next;
      }

      if (obj2 == obj1 && obj1) return true;
    }
    return false;
};

function ListNode(val){
    this.val = val;
    this.next = null;
}

//測試
console.log(">>>>>>環形鏈表》》測試》》")
let node1 = new ListNode(1);
let node2 = new ListNode(2);
let node3 = new ListNode(3);
let node4 = new ListNode(4);

node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2;

let res = hasCycle(node1);
console.log("res: ", res);

View Code

 

3.3 移除鏈表中給定值的所有元素_第203題

 

參考demo1:

/**
 刪除鏈表中等於給定值 val 的所有節點。
 示例:
 輸入: 1->2->6->3->4->5->6, val = 6
 輸出: 1->2->3->4->5

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * 在力扣中得分:耗時160ms, 打敗Javascript中17.87%; 內存消耗37.5M, 打敗JavaScript中24.79% , 更優化的寫法是?
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    let newHead = null, curNode = null;
    while(head){
        if (head.val != val){
            if (curNode){
                curNode.next = new ListNode(head.val);
                curNode = curNode.next;
            }
            else{
                curNode = new ListNode(head.val);
                newHead = curNode;
            }
        }
        head = head.next;
    }
    return newHead;
}

function ListNode(val){
    this.val = val;
    this.next = null;
}


//測試
console.log(">>>>移除鏈表元素測試》》》")
var node = new ListNode(1);
node.next = new ListNode(2);
// node.next.next = new ListNode(5);
// node.next.next.next = new ListNode(4);
// node.next.next.next.next = new ListNode(6);
// node.next.next.next.next.next = new ListNode(8);
// node.next.next.next.next.next.next = new ListNode(4);

// var newNode = removeElements(node, 6);
// console.log(newNode);

var newNode = removeElements(node, 2);
console.log(newNode);

View Code

參考demo2:

/**
 刪除鏈表中等於給定值 val 的所有節點。
 示例:
 輸入: 1->2->6->3->4->5->6, val = 6
 輸出: 1->2->3->4->5

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/** 第二種寫法
 * 在力扣中得分:耗時112ms, 打敗Javascript中90.28%; 內存消耗37.5M, 打敗JavaScript中24.79%
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    if (!head) return head;

    let newHead = new ListNode(-1);
    newHead.next = head; //把head作為newHead的下一個
    let tmpNode = newHead;

    while(tmpNode.next){
        if (tmpNode.next.val == val){
            tmpNode.next = tmpNode.next.next;
        }
        else{
            tmpNode = tmpNode.next;
        }
    }
    return newHead.next; //返回newHead的下一個,就是我們想要的結果
}

function ListNode(val){
    this.val = val;
    this.next = null;
}


//測試
console.log(">>>>移除鏈表元素測試》》》")
var node = new ListNode(1);
node.next = new ListNode(2);
// node.next.next = new ListNode(5);
// node.next.next.next = new ListNode(4);
// node.next.next.next.next = new ListNode(6);
// node.next.next.next.next.next = new ListNode(8);
// node.next.next.next.next.next.next = new ListNode(4);

// var newNode = removeElements(node, 6);
// console.log(newNode);

var newNode = removeElements(node, 2);
console.log(newNode);

View Code

 

3.4 反轉鏈表_第206題

 

參考demo1_迭代方式:

/*
 反轉一個單鏈表。使用迭代方式實現
 示例:
 輸入: 1->2->3->4->5->NULL
 輸出: 5->4->3->2->1->NULL

 力扣中測試執行用時 : 76 ms, 在所有 JavaScript 提交中擊敗了97.74%的用戶
 內存消耗 :36 MB, 在所有 JavaScript 提交中擊敗了6.92%的用戶
 * */

function ListNode(val){
    this.val = val;
    this.next = null;
}
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let newHead = null;
    while(head){
        let tmpNode= newHead;
        newHead = new ListNode(head.val);
        newHead.next = tmpNode;
        head = head.next;
    }
    return newHead;
}


////測試
var node = new ListNode(9);
node.next = new ListNode(99);
node.next.next = new ListNode(999);
node.next.next.next = new ListNode(33);

console.log("原鏈表:", node);
console.log(".....反轉....")
console.log(reverseList(node))

View Code

參考demo2_遞歸方式:

/*
 反轉一個單鏈表。 使用遞歸方式實現
 示例:
 輸入: 1->2->3->4->5->NULL
 輸出: 5->4->3->2->1->NULL

 力扣測試得分:
 執行用時 :80 ms, 在所有 JavaScript 提交中擊敗了95.56%的用戶
 內存消耗 :36.3 MB, 在所有 JavaScript 提交中擊敗了5.03%的用戶
* */

function ListNode(val){
    this.val = val;
    this.next = null;
}
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    return getNewNode(head).first;
}

/**
 * 遞歸,好繞啊:
 * 推演:加入2->3->4->5 遞歸:
 * @param node
 */
function getNewNode(node){

    if (!node) return {first: null, cur: null };

    var cur = new ListNode(node.val);

    ////一直遞歸遞歸,拿到原鏈表最後一個元素開始返回
    var res = getNewNode(node.next);

    if (res.first) {
        res.cur.next = cur; //設置

        return {
            first: res.first, //反轉鏈表的第一個元素
            cur: cur
        }
    }

    console.log("666_node.val: ", node.val);
    /**
     * 原鏈表最後一個元素會執行到這裏,最後一個元素作為反轉鏈表的第一個元素返回
     */

    return {
        first: cur, //反轉鏈表的第一個元素
        cur: cur    //每次遞歸返回的一個元素
    };
}

//測試
var node = new ListNode(2);
node.next = new ListNode(3);
node.next.next = new ListNode(4);
node.next.next.next = new ListNode(5);
console.log("\n\n*****原鏈表****")
console.log(node);
console.log("......反轉.....")
console.log(reverseList(node));

View Code

 

3.5 查找鏈表的中間結點_第876題

參考代碼demo1_迭代方式:

/**
 * 給定一個帶有頭結點 head 的非空單鏈表,返回鏈表的中間結點。
 如果有兩个中間結點,則返回第二个中間結點。

 示例 1:
 輸入:[1,2,3,4,5]
 輸出:此列表中的結點 3 (序列化形式:[3,4,5])
 返回的結點值為 3 。 (測評系統對該結點序列化表述是 [3,4,5])。
 注意,我們返回了一個 ListNode 類型的對象 ans,這樣:
 ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

 示例 2:
 輸入:[1,2,3,4,5,6]
 輸出:此列表中的結點 4 (序列化形式:[4,5,6])
 由於該列表有兩个中間結點,值分別為 3 和 4,我們返回第二個結點。
  

 提示:
 給定鏈表的結點數介於 1 和 100 之間。

 力扣得分:
 執行用時 :108 ms, 在所有 JavaScript 提交中擊敗了19.44%的用戶
 內存消耗 :33.6 MB, 在所有 JavaScript 提交中擊敗了74.60%的用戶
 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var middleNode = function(head) {

    if (!head) return head;

    let arr = [];
    while(head){
        arr.push(head);
        head = head.next;
    }

    let len = arr.length;
    return len % 2 == 0 ? arr[len/2] : arr[(len-1)/2];
};

//測試
var obj = new ListNode(1), temp = obj;
for (let i = 0; i < 6; i++){
    temp.next = new ListNode(2+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));

View Code

參考代碼demo2_快慢指針:

/**
 * 給定一個帶有頭結點 head 的非空單鏈表,返回鏈表的中間結點。
 如果有兩个中間結點,則返回第二个中間結點。

 示例 1:
 輸入:[1,2,3,4,5]
 輸出:此列表中的結點 3 (序列化形式:[3,4,5])
 返回的結點值為 3 。 (測評系統對該結點序列化表述是 [3,4,5])。
 注意,我們返回了一個 ListNode 類型的對象 ans,這樣:
 ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

 示例 2:
 輸入:[1,2,3,4,5,6]
 輸出:此列表中的結點 4 (序列化形式:[4,5,6])
 由於該列表有兩个中間結點,值分別為 3 和 4,我們返回第二個結點。
  

 提示:
 給定鏈表的結點數介於 1 和 100 之間。

 力扣得分:
 執行用時 :120 ms, 在所有 JavaScript 提交中擊敗了12.22%的用戶
 內存消耗 :34.1 MB, 在所有 JavaScript 提交中擊敗了11.11%的用戶

 官方答案,官方這個確實簡潔:
 let slow = fast = head;
 while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
 return slow;

 官方力扣得分:
 執行用時 :64 ms, 在所有 JavaScript 提交中擊敗了99.44%的用戶
 內存消耗 :34.1 MB, 在所有 JavaScript 提交中擊敗了11.11%的用戶

 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/** 用快慢指針來處理下
 * @param {ListNode} head
 * @return {ListNode}
 */
var middleNode = function(head) {
    // let slow = head, fast = head;
    // while(slow){
    //     if (fast){
    //         fast = fast.next;
    //         if (fast){
    //             fast = fast.next;
    //         }
    //         else{
    //             return slow;
    //         }
    //     }
    //     else{
    //         return slow;
    //     }
    //     slow = slow.next;
    // }
    // return head;

    //官方答案:簡潔明了
    let slow = fast = head;
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
};

//測試
var obj = new ListNode(1), temp = obj;
for (let i = 0; i < 6; i++){
    temp.next = new ListNode(2+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));

obj = new ListNode(90), temp = obj;
for (let i = 0; i < 5; i++){
    temp.next = new ListNode(91+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));

View Code

 

參考Demo地址:https://github.com/xiaotanit/Tan_DataStruct

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

Spring5源碼深度解析(一)之理解Configuration註解

代碼地址:https://github.com/showkawa/spring-annotation/tree/master/src/main/java/com/brian

1.Spring體繫結構

1.1、Spring Core:主要組件是BeanFactory,創建JavaBean的工廠,使用控制反轉(IOC) 模式  將應用程序的配置和依賴性規範與實際的應用程序代碼分開。

1.2、Spring AOP:集成了面向切面的編程功能(AOP把一個業務流程分成幾部分,例如權限檢查、業務處理、日誌記錄,每個部分單獨處理,然後把它們組裝成完整的業務流程。每個部分被稱為切面),

 可以將聲明性事物管理集成到應用程序中。

1.3、Spring Cntext:一個核心配置文件,為Spring框架提供上下文信息。

1.4、Spring Do:Spring操作數據庫的模塊。

1.5、Spring ORM:Spring集成了各種orm(object relationship mapping 對象關係映射)框架的模塊,集成mybatis

1.6、Spring Web集成各種優秀的web層框架的模塊(Struts、Springmvc)

1.7、Spring web MVC:Spring web層框架

 2.Configuration註解分析內容(@Configuration,@ComponentScan,@Scope,@Lazy)

2.1 @Configuration

 @Configuration用於定義配置類,可替換xml配置文件,被註解的類內部包含有一個或多個被@Bean註解的方法,這些方法將會被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext類進行掃描,並用於構建bean定義,初始化Spring容器。

2.1.1 @Configuration標註在類上,相當於把該類作為spring的xml配置文件中的<beans>,作用為:配置spring容器(應用上下文)

@Configuration
public class MainConfigOfLifeCycle { }

//測試方法

public static void main(String[] args) {
ApplicationContext acac =
new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("ioc容器創建成功");

//關閉ioc容器
((AnnotationConfigApplicationContext) acac).close();
}
 

相當於spring的xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc"  
    xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:util="http://www.springframework.org/schema/util" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-5.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-5.0.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-5.0.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-5.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-5.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-5.0.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-5.0.xsd" default-lazy-init="false">


</beans>

2.2 @ComponentScan用法 

 ComponentScan字面意思就是組件掃描,就是根據定義的掃描路徑,把符合掃描規則的類裝配到spring容器中

  2.2.1 ComponentScan參數說明

/*
* @ComponentScan
* value:只當於掃描的的包
* excludeFilters = 指定掃描的時候按照什麼規則排除哪些組件
* includeFilters = 指定掃描的時候只需要包含哪些組件
* Filter.ANNOTATION:按照註解
* Filter.ASSIGNABLE_TYPE: 按照給定的類型
* */

FilterType 有五種類型

ANNOTATION:註解類型

ASSIGNABLE_TYPE:ANNOTATION:指定的類型

ASPECTJ:按照Aspectj的表達式,基本上不會用到

REGEX:按照正則表達式

CUSTOM:自定義規則

package com.brian.config;

import com.brian.bean.Alan;
import com.brian.bean.Brian;
import com.brian.bean.BrianBeanFactory;
import com.brian.bean.Person;
import com.brian.condition.BrianCondition;
import com.brian.condition.BrianSelector;
import com.brian.service.BookService;
import org.springframework.context.annotation.*;
import org.springframework.stereotype.Controller;

@Configuration //告訴spring這是一個配置類
/*
* @ComponentScan
*   value:只當於掃描的的包
*   excludeFilters = 指定掃描的時候按照什麼規則排除哪些組件
*   includeFilters = 指定掃描的時候只需要包含哪些組件
*   Filter.ANNOTATION:按照註解
*   Filter.ASSIGNABLE_TYPE: 按照給定的類型
* */

@ComponentScans(value = {
        @ComponentScan(value = "com.brian",includeFilters = {
//                @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class}),
//                @ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE,classes = {BookService.class}),
                @ComponentScan.Filter(type = FilterType.CUSTOM,classes = {BrianTypeFilter.class})
        },useDefaultFilters = false)
})
@Import({Brian.class,Alan.class,BrianSelector.class})
public class MainConfig {

    @Bean("person") //給容器中註冊一個Bean;類型為返回值的類型;id默認是方法名作為id
    public Person person(){
        return new Person("Alan",18);
    }


    /*
    * @Conditional() 按照條件註冊
    *
    * */
    @Conditional({BrianCondition.class})
    @Bean("person01")
    public Person person01() {
        return new Person("Brian",17);
    }

    @Conditional({BrianCondition.class})
    @Bean("person02")
    public Person person02() {
        return new Person("wenTao",19);
    }

    /*
    *
    *給容器中註冊組件
    * 1,包掃描+ 組件標註註解(@Controller/@Service/@Repository/@Component)[自己寫的方法]
    * 2, @Bean [導入的第三方包裏面的組件]
    * 3,@Import [快速的給容器導入一個組件]
    *       1.@Import(要導入的組件class)
    *       2.ImportSelector:返回需要導入的組件的全類名數組
    *       3.ImportBeanDefinitionRegistrar: 手動註冊bean到容器
    *  4. 使用Spring提供的FactoryBean
    * */
    @Bean
    public BrianBeanFactory brianBeanFactory() {
        return new BrianBeanFactory();
    }

}

2.3 @Scope

默認情況Spring容器是單例的

singleton單例模式:全局有且僅有一個實例。

prototype原型模式:每次獲取Bean的時候都會有一個新的實例。

request

request表示針對每次請求都會產生一個新的Bean對象,並且該Bean對象僅在當前Http請求內有效。

session

session作用域表示煤氣請求都會產生一個新的Bean對象,並且該Bean僅在當前Http session內有效。

測試@Scopeprototype原型模式

Configuration配置類

@Configuration
@ComponentScan("com.brian.bean")
public class MainConfigOfLifeCycle {
    @Scope("prototype")
    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Alan getAlan () {
        return new Alan();
    }
}

測試類

public class MainTest {
    public static void main(String[] args) {
         /*ApplicationContext acac =
                 new AnnotationConfigApplicationContext(MainConfig.class);*/
         ApplicationContext acac =
                 new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
        System.out.println("ioc容器創建成功");
        Alan alan1 =  acac.getBean(Alan.class);
        Alan alan2 =  acac.getBean(Alan.class);
        System.out.println("比較兩個Alan實例: " + (alan1 == alan2));

        //關閉ioc容器
        ((AnnotationConfigApplicationContext) acac).close();
    }
}

2.4 @Lazy

Lazy表示為懶加載,當真正需要引用獲取的時候才會被加載

True 表示為懶加載 false表示為在IOC容器加載的時候被創建。

 

測試@Lazy(false)餓漢模式加載

Configuration配置類

@Configuration
@ComponentScan("com.brian.bean")
public class MainConfigOfLifeCycle {
    //@Scope("prototype")
    @Lazy(false)
    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Alan getAlan () {
        return new Alan();
    }


}

測試類

public class MainTest {
    public static void main(String[] args) {
         /*ApplicationContext acac =
                 new AnnotationConfigApplicationContext(MainConfig.class);*/
         ApplicationContext acac =
                 new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
        System.out.println("ioc容器創建成功");
      //  Alan alan1 =  acac.getBean(Alan.class);
       // Alan alan2 =  acac.getBean(Alan.class);
        //System.out.println("比較兩個Alan實例: " + (alan1 == alan2));

        //關閉ioc容器
        ((AnnotationConfigApplicationContext) acac).close();
    }
}

 看下結果會發現在餓漢模式下,即使沒用使用AnnotationConfigApplicationContext.getBean()獲取對象,對象也被加載進了IOC容器

測試@Lazy默認懶加載

 Configuration配置類

@Configuration
@ComponentScan("com.brian.bean")
public class MainConfigOfLifeCycle {
    //@Scope("prototype")
    @Lazy
    @Bean(initMethod = "init", destroyMethod = "destroy")
    public Alan getAlan () {
        return new Alan();
    }


}

測試類保持不表

測試結果中,沒有輸出Alan這個對象創建和銷毀的打印信息

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

大話Spark(8)-源碼之DAGScheduler

DAGScheduler的主要作用有2個:

一、把job劃分成多個Stage(Stage內部并行運行,整個作業按照Stage的順序依次執行)
二、提交任務

以下分別介紹下DAGScheduler是如何做這2件事情的,然後再跟源碼看下DAGScheduler的實現。

一、如何把Job劃分成多個Stage

1) 回顧下寬依賴和窄依賴

窄依賴:父RDD的每個分區只被子RDD的一個分區使用。(map,filter,union操作等)
寬依賴:父RDD的分區可能被多個子RDD的分區使用。(reduceByKey,groupByKey等)

如下圖所示,左側的算子為窄依賴, 右側為寬依賴


窄依賴可以支持在同一個集群Executor上,以管道形式順序執行多條命令,例如在執行了map后,緊接着執行filter。分區內的計算收斂,不需要依賴所有分區的數據,可以并行地在不同節點進行計算。所以它的失敗回復也更有效,因為它只需要重新計算丟失的parent partition即可。最重要的是窄依賴沒有shuffle過程,而寬依賴由於父RDD的分區可能被多個子RDD的分區使用,所以一定伴隨着shuffle操作。

2) DAGScheduler 如何把job劃分成多個Stage

DAGScheduler會把job劃分成多個Stage,如下圖sparkui上的截圖所示,job 0 被劃分成了3個stage

DAGScheduler劃分Stage的過程如下:
DAGScheduler會從觸發action操作的那個RDD開始往前倒推,首先會為最後一個RDD創建一個stage,然後往前倒推的時候,如果發現對某個RDD是寬依賴(產生Shuffle),那麼就會將寬依賴的那個RDD創建一個新的stage,那個RDD就是新的stage的最後一個RDD。然後依次類推,繼續往前倒推,根據窄依賴或者寬依賴進行stage的劃分,直到所有的RDD全部遍歷完成為止。

3) wordcount的Stage劃分

在前面大話spark(3)-一圖深入理解WordCount程序在Spark中的執行過程中,我畫過一張wordcount作業的Stage的劃分的圖,如下:

可以看出上圖中,第一個stage的3個task并行執行,遇到reduceByKey這個產生shuffle的操作開始劃分出新的Stage。但是其實這張圖是不準確的。
其實對於每一種有shuffle的操作,比如groupByKey、reduceByKey、countByKey的底層都對應了三個RDD:MapPartitionsRDD、ShuffleRdd、MapPartitionsRDD
(寬依賴shuffle生成的rdd為ShuffleRdd)
其中Shuffle發生在第一個RDD和第二個RDD之間,前面說過如果發現對某個RDD是寬依賴(產生Shuffle),那麼就會將寬依賴的那個RDD創建一個新的stage
所以說上圖中 reduceByKey操作其實對應了3個RDD,其中第一個RDD會被劃分到Stage1中!

4) DAGScheduler劃分Stage源碼

RDD類中所有的action算子觸發計算都會調用sc.runjob方法, 而sc.runjob方法底層都會調用到SparkContext中的dagscheduler對象的runJob方法
例如count這個action操作
def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum

一直追着runJob方法往底層看最終調用dagScheduler.runJob,傳入調用這個方法的rdd

dagScheduler.runJob內部調用submitJob提交當前的action到scheduler
submitJob內部調用DAGSchedulerEventProcessLoop發送JobSubmitted的信息,
在JobSubmitted內部最終調用dagScheduler的handleJobSubmitted(dagScheduler的核心入口)。

handleJobSubmitted方法如下:

上面代碼中submitStage提交作業,其內代碼如下:

submitStage方法中調用getMissingParentStages方法獲取finalStage的父stage,
如果不存在,則使用submitMissingTasks方法提交執行;
如果存在,則把該stage放到waitingStages中,同時遞歸調用submitStage。通過該算法把存在父stage的stage放入waitingStages中,不存在的作為作業運行的入口。

其中最重要的getMissingParentStages中是stage劃分的核心代碼,如下:

這裏就是前面說到的stage劃分的方式,查看最後一個rdd的依賴,如果是窄依賴,則不創建新的stage,如果是寬依賴,則用getOrCreateShuffledMapStage方法創建新的rdd,依次往前推。

所以Stage的劃分算法最核心的兩個方法為submitStage何getMissingParentStage

二、提交任務

當Stage提交運行后,在DAGScheduler的submitMissingTasks方法中,會根據Stage的Partition個數拆分對應個數任務,這些任務組成一個TaskSet提交到TaskScheduler進行處理。
對於ResultStage(最後一個Stage)生成ResultTask,對於ShuffleMapStage生成ShuffleMapTask。
每一個TaskSet包含對應Stage的所有task,這些Task的處理邏輯完全一樣,不同的是對應處理的數據,而這些數據是對應其數據分片的(Partition)。
submitMissingTasks如下:

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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