302
社区成员
发帖
与我相关
我的任务
分享借助底层度量工具对最终版(第三次作业)代码进行了静态分析,核心数据如下:
| 类名 | 属性数 | 方法数 | 平均方法规模(LOC) | 最大控制分支数 | 总代码行数(LOC) |
| Main | 0 | 5 | 25 | 8 (parseRecursiveFunctions) | 135 |
| Term | 4 | 8 | 15 | 6 (compareTo) | 128 |
| Expr | 1 | 9 | 16 | 7 (deriveTerm) | 145 |
| Parser | 4 | 14 | 18 | 7 (parseFactor) | 230 |
| 总计 | 9 | 36 | - | - | ~638 |
内聚与耦合分析:
内聚性:Term 和 Expr 具有极高的内聚性。它们作为不可变(逻辑上)的数据载体,只负责自身多项式的乘法、加法、求导和化简逻辑。
耦合性:耦合主要集中在 Parser 与 Expr/Term 之间。由于我采用了直接在解析过程中求值的策略(边解析边算),Parser 高度依赖 Expr 的各种运算方法。此外,Expr 和 Term 构成了互相依赖的递归嵌套结构(Expr 包含 Term 的 List,而 Term 的指数函数底数又是一个 Expr),这在处理含 exp 的表达式时不可避免,但增加了理解成本。
codePlantuml
@startuml
skinparam classAttributeIconSize 0
class Main {
+ main(args: String[]): void
- parseNormalFunctions(...)
- parseRecursiveFunctions(...)
- preprocessSigns(...)
}
class Parser {
- inputString: String
- currentPos: int
- functionMap: Map<String, String>
- xValue: Expr
+ parseExpr(): Expr
- parseTerm(): Expr
- parseFactor(): Expr
- parseFuncCall(): Expr
- parseDerivative(var: String): Expr
- parseSelect(): Expr
}
class Expr {
- termList: List<Term>
+ add(other: Expr): Expr
+ multiply(other: Expr): Expr
+ pow(exponent: int): Expr
+ derive(variable: String): Expr
+ normalize(): void
+ isFactor(): boolean
}
class Term {
- coefficient: BigInteger
- xpower: int
- ypower: int
- innerExpr: Expr
+ multiply(other: Term): Term
+ compareTo(other: Term): int
}
Main ..> Parser : creates >
Parser ..> Expr : creates & evaluates >
Expr "1" *-- "*" Term : contains >
Term "1" *-- "0..1" Expr : exp(...) inner >
@enduml
优点:结构极致扁平、清晰。没有构建庞大复杂的AST(抽象语法树)节点树,而是将“化简”与“解析”同步进行,解析完毕即得到标准多项式形式,极大降低了树遍历和后期优化的难度。
缺点:可扩展性存在隐患。Term 类的属性被写死为 (系数, x的幂, y的幂, exp内部表达式)。这属于面向过程的残余。如果后续引入三角函数(如 sin(x)),Term 必须修改其内部结构,违反了开闭原则 (OCP)。理想的设计应当抽象出一个 Factor 接口,让 Variable, Constant, Exp, Trigonometric 都实现它。
第一次作业(基础多项式):确立了 Expr (多项式) 和 Term (单项式) 的基本结构。此时 Term 仅有系数和 x 的指数,解析器 Parser 实现了基础的递归下降(Expr -> Term -> Factor)。
第二次作业(引入嵌套、指数与自定义函数):遭遇了第一次阵痛。为了支持 exp(...) 嵌套,我让 Term 内部包含了一个 Expr 属性。同时,在 Parser 中引入了 Map 来存储自定义函数,遇到函数调用时,直接开启一个新的 Parser 子实例去解析函数体。这是一个绝佳的体验,避免了复杂的 AST 替换逻辑。
第三次作业(递推函数、求导与选择因子):架构经受住了考验,未发生重构。求导操作通过给 Expr 和 Term 分别增加 derive(String var) 方法优雅地实现了数学分配律;递推函数只需在 Main 中提前打表展开即可;选择因子 [...] ? ... : ... 仅仅是 Parser 中的一次条件判断分支。
新情景:引入偏导数算子内部嵌套,以及求原函数(积分)算子 intg(expr, x)。
可扩展性:对于求偏导数的嵌套(如 dx(dy(x*y))),当前递归下降解析器完全原生支持,无需修改。
对于积分算子,目前的架构会遇到瓶颈:因为当前的解析器是边解析边求值,如果遇到积分,由于积分可能无法得到初等函数(多项式形式),强行化简会失效。这就需要重构,真正引入 AST 树,将“解析”和“计算”两个阶段完全解耦。
在第三次作业中,我曾遇到了一个致命的 NumberFormatException(正是此前求助修复的问题)。
特征:当输入数据中省略了递推函数数量 m 时,程序会错把表达式字符串当做整数读取从而崩溃。
问题所在:Main 类的输入解析逻辑 parseRecursiveFunctions。
根本原因:代码对输入格式做出了“强假设”(认为总是有特定的行数规律),而没有基于 Scanner 的剩余行数做动态探查。
设计优化:这暴露了我的 Main 类职责过重。更好的设计是剥离出一个专职的 InputReader 类,将输入流抽象为 FunctionDefinition 和 ExpressionString 的实体返回,从而将 I/O 逻辑和业务流彻底隔离。
对比 Main.parseRecursiveFunctions(出现了Bug)和 Term.multiply(零Bug)。
前者的圈复杂度(Cyclomatic Complexity)高达 8 以上,大量使用 if-else 判断前缀和行数;而后者仅仅是一次线性的系数乘法和对象构造,复杂度极低。
降低复杂度的方法:对于复杂的解析逻辑,应该使用“卫语句 (Guard Clauses)”尽早 return,或者将其拆分为 parseFnTemplate()、expandRecursion() 等细粒度方法,保证单一职责。
在互测环节,我主要采取了基于边界值驱动的黑盒测试与局部白盒审查相结合的策略:
构造极端嵌套:例如 exp(exp(exp(x))) 或者极深的括号嵌套,这能有效击穿部分同学使用正则表达式替换而不是递归下降解析的程序(导致栈溢出或正则超时)。
符号边界:例如输入 0,+0,-0,空因子,以及 x^0 等。这能发现化简逻辑中对符号判断失效的Bug。
结合代码设计的针对性测试:在阅读同房间同学的代码时,如果发现他的 Expr.add() 方法内部使用了对象引用的直接修改(深浅拷贝问题),我就会针对性地构造如 (x+1)*(x+1) 这样的表达式,诱导其发生引用污染,果然成功Hack到了两位同学。
做到的优化:
化简合并:通过重写 Term 的 compareTo 方法,并在 Expr.normalize() 中使用 TreeMap<Term, BigInteger>,利用树结构的特性,极其优雅地在 O(NlogN)
时间内自动合并了所有的同类项,同时清除了系数为 0 的项。常数折叠:x^0、y^0 直接视为常数 1;exp(0) 转化为常数 1。
是否保证了正确性与简洁性?
能保证。这得益于 compareTo 方法逻辑的严密性。只要指数函数的内部 Expr 判等逻辑(依赖递归的 compareTo)正确,同类项就能被准确识别。使用 TreeMap 消除了 HashMap 可能带来的遍历不确定性,保证了无论底层如何操作,输出的表达式恒等变形式都是稳定唯一的。
代码生成率:在三次作业中,核心逻辑(解析器、化简算法)完全由个人构思,AI生成代码率约为 20%。主要让AI负责了:去除 Checkstyle 报错的代码风格重构、冗长且重复的 getter/setter 生成、以及预处理符号 preprocessSigns 中的正则替换实现。
辅助测试搭建:我使用大模型编写了一套基于 Python sympy 库的自动化对拍评测机。效果极佳,大模型不仅帮我处理了 Java 子进程调用的繁琐代码,还准确处理了 sympy 对 exp 的化简对比逻辑。
互测观察:在我的互测房间中,同学 “天枢星” 显然大量使用了 AI 生成代码。理由是其类中包含了极其冗长、标准且带有 Markdown 格式的英文 Javadoc 注释,并且其在处理多项式相乘时,写出了类似“循环展开”的莫名其妙的奇特优化,这显然超出了普通第一单元学生的正常手写逻辑。同学 “开阳星” 在词法分析部分使用了极度规整的 Pattern Matching 枚举,变量命名呈现出强烈的 LLM 风格(如 matchedResultString)。
面向对象第一单元是一次真正的“洗礼”。从面向过程的“数组存多项式”,到彻底理解“递归下降分析法”,我深刻体会到了将数学表达式映射为面向对象结构的奇妙之处。
最让我受用的教训是:不可变对象(Immutable Object)保平安。在 Expr 和 Term 的方法中,无论进行加减乘除还是求导,我都坚持 new 一个新对象返回,绝不修改原对象的属性。虽然牺牲了微小的内存开销,但彻底杜绝了深浅拷贝带来的“幽灵Bug”,让后期调试顺畅无比。严格的 Checkstyle 虽然让人痛苦,但当最终看到满篇整齐划一、命名优雅的代码时,那种成就感是无与伦比的。
对于未来第一单元课程的改进,我有以下建议:
提供官方的轻量级 AST 接口定义:很多同学在第一次作业采用了直接解析化简的偏门方法,导致第三次作业极其痛苦。如果在第一次作业就给出类似 interface Factor 的强制性架构建议,能帮助大家少走很多弯路。
前置评测机搭建的引导:第一周互测时大家都很盲目,如果能在先导课或者第一周的指导书中,简单介绍一下如何使用 Python 的 subprocess 和 sympy 搭建最基础的黑盒评测机,将会极大提高同学们的测试能力和代码质量。