摘要:在程式碼的世界中,是存在很多藝術般的寫法,這可能也是部分程式設計師追求程式設計這項事業的內在動力。
本文分享自華為雲社群《【雲駐共創】用4種程式碼中的藝術試圖喚回你對程式設計的興趣》,作者: breakDawn。
也許對於部分人來說,喚起他們程式設計興趣的起點可能是一些能快速實現某功能的python小指令碼。但作為一個多年的java開發,更多是在接觸工作中的業務程式碼,CURD寫久了,總會偶爾感到一絲絲的疲勞。
回望窗外,思索著在程式碼的世界中,是存在很多藝術般的寫法,這可能也是部分程式設計師追求程式設計這項事業的內在動力。
這裡將為你呈現4種程式碼中的藝術,試圖喚回你對程式碼最初的興趣。
設計模式的藝術:用狀態模式告別if-else
面對判斷分支異常多、狀態變化異常複雜的業務邏輯程式碼,在大量if-else中遨遊往往會犯惡心,甚至懷疑起了人生。
例如“手寫一個判斷函式,確認字串是否是一個合法的科學數字表示式”這種常見的業務邏輯問題。如果用if-else寫,就會變成如下醜陋的程式碼:
每次維護這種程式碼,總是都要從頭閱讀一遍,確認自己要在哪裡修改,彷彿在修補一個破舊的大棉襖。
但我們如果使用了設計模式中的狀態機模式來進行重構,整塊程式碼就會非常精妙。首先要畫出一副如下所示的狀態演變圖:
(圖源來自leetcode,連結見:https://leetcode.cn/problems/valid-number/solution/you-xiao-shu-zi-by-leetcode-solution-298l/)
狀態圖繪製完成之後,就可以根據狀態變化的合理性,確認狀態是否符合要求。
程式碼如下所示:
class Solution { public enum CharType { NUMBER, OP, POINT, E; public static CharType toCharType(Character c) { if (Character.isDigit(c)) { return NUMBER; } else if (c == '+' || c == '-') { return OP; } else if (c == '.') { return POINT; } else if (c =='e' || c == 'E') { return E; } else { return null; } } } public enum State { INIT(false), OP1(false), // 在.前面的數字 BEFORE_POINT_NUMBER(true), // 前面沒數字的點 NO_BEFORE_NUMBER_POINT(false), // 前面有數字的點 BEFORE_NUMBER_POINT(true), // 點後面的數字 AFTER_POINT_NUMBER(true), // e/E OPE(false), // E後面的符號 OP2(false), // e後面的數字 AFTER_E_NUMBER(true); // 是否可在這個狀態結束 private boolean canEnd; State(boolean canEnd) { this.canEnd = canEnd; } public boolean isCanEnd() { return canEnd; } } public Map<State, Map<CharType, State>> transferMap = new HashMap<>() {{ Map<CharType, State> map = new HashMap<>() {{ put(CharType.OP, State.OP1); put(CharType.NUMBER, State.BEFORE_POINT_NUMBER); put(CharType.POINT, State.NO_BEFORE_NUMBER_POINT); }}; put(State.INIT, map); map = new HashMap<>() {{ put(CharType.POINT, State.NO_BEFORE_NUMBER_POINT); put(CharType.NUMBER, State.BEFORE_POINT_NUMBER); }}; put(State.OP1, map); map = new HashMap<>() {{ put(CharType.POINT, State.BEFORE_NUMBER_POINT); put(CharType.NUMBER, State.BEFORE_POINT_NUMBER); put(CharType.E, State.OPE); }}; put(State.BEFORE_POINT_NUMBER, map); map = new HashMap<>() {{ put(CharType.NUMBER, State.AFTER_POINT_NUMBER); }}; put(State.NO_BEFORE_NUMBER_POINT, map); map = new HashMap<>() {{ put(CharType.NUMBER, State.AFTER_POINT_NUMBER); put(CharType.E, State.OPE); }}; put(State.BEFORE_NUMBER_POINT, map); map = new HashMap<>() {{ put(CharType.E, State.OPE); put(CharType.NUMBER, State.AFTER_POINT_NUMBER); }}; put(State.AFTER_POINT_NUMBER, map); map = new HashMap<>() {{ put(CharType.OP, State.OP2); put(CharType.NUMBER, State.AFTER_E_NUMBER); }}; put(State.OPE, map); map = new HashMap<>() {{ put(CharType.NUMBER, State.AFTER_E_NUMBER); }}; put(State.OP2, map); map = new HashMap<>() {{ put(CharType.NUMBER, State.AFTER_E_NUMBER); }}; put(State.AFTER_E_NUMBER, map); }}; public boolean isNumber(String s) { State state = State.INIT; for (char c : s.toCharArray()) { Map<CharType, State> transMap = transferMap.get(state); CharType charType = CharType.toCharType(c); if (charType == null) { return false; } if (!transMap.containsKey(charType)) { return false; } // 狀態變更 state = transMap.get(charType); } return state.canEnd; } }
從下面的程式碼可以看到,未來只需要維護transferMap 即可,非常方便,程式碼的優秀設計模式是一門造福懶人程式設計師們的藝術,重構出一個易於維護的程式碼也是程式設計師的成就感來源之一。
併發程式設計的藝術:詭異的Java程式碼揭示了cpu快取的原理
著名的Java併發程式設計大師Doug lea在JDK 7的併發包裡新增一個佇列集合類Linked-TransferQueue,它在使用volatile變數時,用一種追加位元組的方式來優化佇列出隊和入隊的效能。LinkedTransferQueue的程式碼如下,著重關注p0~pe的定義:
/** 佇列中的頭部節點 */ private transient final PaddedAtomicReference<QNode> head; /** 佇列中的尾部節點 */ private transient final PaddedAtomicReference<QNode> tail; static final class PaddedAtomicReference <T> extends AtomicReference T> { // 使用很多4個位元組的引用追加到64個位元組 Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe; PaddedAtomicReference(T r) { super(r); } } public class AtomicReference <V> implements java.io.Serializable { private volatile V value; // 省略其他程式碼 }
追加位元組能優化效能?這種方式看起來很神奇,但如果深入理解處理器架構就能理解其中的奧祕:(以下的解釋來自《Java併發程式設計的藝術一書》 )
“因為對於英特爾酷睿i7、酷睿、Atom和NetBurst,以及Core Solo和Pentium M處理器的L1、L2或L3快取的快取記憶體行是64個位元組寬,不支援部分填充快取行。
這意味著,如果佇列的頭節點和尾節點都不足64位元組的話,處理器會將它們都讀到同一個快取記憶體行中,在多處理器下每個處理器都會快取同樣的頭、尾節點。
當一個處理器試圖修改頭節點時,會將整個快取行鎖定,那麼在快取一致性機制的作用下,會導致其他處理器不能訪問自己快取記憶體中的尾節點,而佇列的入隊和出隊操作則需要不停修改頭節點和尾節點,所以在多處理器的情況下將會嚴重影響到佇列的入隊和出隊效率。
因此Doug lea使用追加到64位元組的方式來填滿高速緩衝區的快取行,避免頭節點和尾節點載入到同一個快取行,使頭、尾節點在修改時不會互相鎖定。
可以看到,在java的併發程式碼中能夠體現底層快取的設計。雖然這程式碼不太符合java希望遮蔽底層實現細節的設計理念,但是Doug lea大師對細節的考慮仍然讓人讚歎不已。
演算法的藝術:用搜尋解決迷宮問題
學習資料結構時,相信“深度優先搜尋”和“廣度優先搜尋”對初學者來說一度是一個噩夢,做練習題時也是用各種姿勢遍歷去二叉樹,無法感受到樂趣所在。
但是當你用搜尋來解決比較簡單的迷宮尋路問題時,便會感到演算法的魅力。
想起小時候玩一些RPG遊戲,往往會有各種迷宮,每次自己探索出口時,其實就是用的深度搜尋,找不到會回溯,然而這樣費時間也費腦子,當地圖過大,大腦的快取不足,或者思考深度不足時,解決起來就很困難。
但如果有計算機的幫忙,對於每次的移動,給定地圖輸入,使用搜尋演算法、A*等演算法,便能夠快速找到迷宮的離開路線。
下面給出一個虛擬碼,來簡單解釋搜尋問題是怎麼解決問題的:
搜尋(當前地圖,當前點) { If (是否已經搜尋過這個場景) { Return; } If(是否到達邊界) { 重新整理最新結果; return; } for(遍歷當前點的所有選擇) { if (是否是無效的選擇) { continue; } 將當前選擇帶來的變化更新到地圖中 進入後續的搜尋 回退當前選擇帶來的變化 } }
所以當你學習完搜尋演算法,卻還對其應用感到困惑時,不妨來做一道迷宮尋路題.(例如http://poj.org/problem?id=3984)
或者自己寫一個五子棋對戰程式與自己對戰。對戰程式除了搜尋演算法,還要考慮博弈論的思想,通過alpha-beta演算法來處理敵對雙方對結果的選擇,編寫評估函式來定義對局面好壞的判斷, 整個編寫過程會更加複雜而有趣,不妨作為自己對搜尋演算法更深層次的學習時嘗試一番。
二進位制的藝術:用數學節省了空間
“給定一個非空整數陣列,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素,而且不能用額外空間”
甚至還有升級版:
“給你一個整數陣列 ,除某個元素僅出現一次外,其餘每個元素都恰出現 三次 。請你找出並返回那個只出現了一次的元素。”
第一想法肯定是維護一個雜湊表或者陣列。但是問題要求不能用額外空間,這一般都是為了在有成本限制的環境下考慮和設計的,例如記憶體有限的某些硬體裝置中。
因此在最佳解法中,選擇藉助了二進位制來解決這個問題。通過同位異或得0,不同位異或得1的特性,快速過濾掉相同的數字:
class Solution { public int singleNumber(int[] nums) { int result = 0; for(int num : nums) { result ^= num; } return result; } }
是不是感覺非常巧妙有趣,利用數學的二進位制特性,簡單的異或就搞定了本來需要大量記憶體的問題,不禁令人拍案叫絕。