首先,每个表达式内部都存在不可分割的字符组,好比一个不止一位的数字,或是一个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。
综上所述,若是你理解了分离逻辑的操做后,写出清晰简洁的代码应该是易如反掌把!