首先,每個表達式內部都存在不可分割的字符組,好比一個不止一位的數字,或是一個sin三角函數,這樣不能分離的字符組我稱之爲詞法單元,依照其定義,能夠將第三次做業的表達式分割成以下詞法單元:java
將一串字符解析成詞法單元列表的形式,就已是一層處理了,這一步就能夠排查出一些錯誤,直接拋出異常,而後咱們獲得了一個由詞法單元組成的列表,如今就是採用遞歸遞降的方法線性解析這個列表並生成表達樹了。設計模式
仔細閱讀指導書,能夠概括出以下定義:api
** 其中[]表示其中的字符存在或不存在皆可;()*表示可選重複0至任意次;| 表示或 **架構
除Num單元內部,其它語法單元內部能夠任意插入SPACEide
寫做分析函數的原則有兩條:函數
固然,有人會說,這樣只是寫一堆遞歸函數(o(╯□╰)o,我就是這麼寫的),沒有實行面對對象,其實,遞歸遞降也是能夠面對對象的。this
首先須要一個Parser接口,實現以下(不必定)函數(爲清晰起見,寫的很簡單)atom
public interface Parser { public Atom toAtom(); public Integer getIndex(); public void addParser(Parser parser); }
分別是一個轉換函數,一個獲取新遍歷下標的函數,一個添加子Parser的函數。架構設計
根據不一樣層級Parser的須要,完成相應函數的重寫(Override)便可,舉個栗子:設計
public class ExprParser implements Parser { ArrayList<Parser> itemList; // Integer endIndex; // private ExprParser() { itemList = new ArrayList<>(); } @Override public Atom toAtom() { Atom root = new Atom(Atom.Type.PLUS, BigInteger.ZERO); for (Parser parser:itemList) { root.addChild(parser.toAtom()); } return root; } @Override public Integer getIndex() { return endIndex; } public static Parser newParser(String string) { Parser parser = new ExprParser(); for (int i = 0; i < string.length();) { if (string.charAt(i) == '-' || string.charAt(i) == '+' || string.charAt(i) != ' ' && string.charAt(i) != '\t') { Parser itemParser = ItemParser.newParser(string.substring(i)); parser.addParser(itemParser); i = itemParser.getIndex(); } else { i += 1; } } return parser; } @Override public void addParser(Parser parser) { itemList.add(parser); } }
依舊格式簡易,可是隻是爲了說明怎麼重寫這樣的函數,Atom即最後生成的表達樹的節點類或者接口,隨我的意。
另外沒有顯式拋出異常,真正寫時須要在返回parser時檢查item List是否爲空,若爲空,則顯式拋出異常,進行異常處理。
如今說明一下,這個newParser工廠函數爲何這麼寫,爲了清楚起見,我把Expr的定義從新拉過來:
Expr = [-|+]Item ([-|+]Item)*
這個語法定義告訴咱們Expr線性掃描,檢查是否有減號或是加號,而後須要獲取一個Item,這時調用ItemParser的工廠函數便可,格式異常由不一樣層級的Parser實現類的工廠函數負責拋出,只要語法分析合理,這樣寫做是簡單無誤的。
以此類推,相信讀懂了上述抽象的思惟方式,寫出Item和Factor的Parser對你易如反掌!
在第三次做業中,我採用了繼承的方案實現樹形結構節點的構造,Atom父類實現了很是多的函數,如今的視角來看,是很不合理的,由於Atom負擔了全部子類的函數,臃腫累贅,層次是分明瞭,只有父類Atom和子類各類Atom,可是在工程管理和邏輯架構設計上,仍是有所問題的。
下面是個人Atom類圖關係,其它類都繼承自Atom很複雜把。
舉個栗子,如今咱們有一串數字在列表origin中,咱們要對他們都平個方,生成一個新列表,正常作法以下:
List<Integer> after = new ArrayList<>(); for (Integer integer: origin) { after.add(integer * integer); }
可是有了Stream API和lambda表達式,就能夠這麼寫:
List<Integer> after = origin.stream().map(e -> e * e).collect(toList());
Stream流的主要功能有filter(篩選),map(映射),flatMap(平鋪映射),collect(收集返回列表或Map或其它)。
須要填充函數接口的參數的來源有兩種:
如今問題來了,爲何要這麼寫呢?何時要寫成哪種形式呢?
上述過程就是一個邏輯不斷分離的過程,對應到相加項和相乘項,惟一不一樣的就是Predicate接口,也是這麼思惟的意義所在。
這裏把後三步的合併加法與合併乘法共通的代碼貼出來:
public List<Atom> filter(Atom atom, Predicate<Atom> predicate) { return this.children.stream() .filter(predicate) .collect(Collectors.toList()); } public ArrayList<List<Atom>> classify(Predicate<Atom> predicate) { Map<String, List<Atom>> map = new HashMap<>(); for (Atom atom : children) { ArrayList<Atom> list = (ArrayList<Atom>) this.filter(atom, predicate); map.put(list.toString(), list); } return new ArrayList<>(map.values()); }
這裏舉個例子就把合併相加項的filter調用的函數接口寫一下吧:
Predicate<Atom> addPredicate = e -> !( e.getType().equals(Atom.Type.CONSTANT) && e.getValue().equals(BigInteger.ZERO) );
上述函數接口代表,若是元素e是一個常數0了話,返回false不然返回true。
綜上所述,若是你理解了分離邏輯的操做後,寫出清晰簡潔的代碼應該是易如反掌把!