Java中规模软件开发实训——简单计算器制作
'# Java中规模软件开发实训——简单计算器制作
一、背景与问题
在软件开发实训中,计算器作为常见的应用场景,常被用作练习面向对象设计、异常处理、表达式解析等技术的载体。传统实现方式往往将计算逻辑简单封装为静态方法,但随着功能复杂度增加(如支持括号、函数嵌套、错误处理等),这种做法会暴露诸多问题。
在中规模开发中,我们需要考虑:
- 表达式解析的算法选择
- 多线程处理的潜在需求
- 用户输入的健壮性处理
- 历史记录的持久化机制
- 跨平台的UI实现
传统单体计算器设计存在以下局限:
- 无法处理复杂的运算符优先级
- 无法验证表达式合法性
- 缺乏异常处理机制
- 无法扩展高级功能(如函数计算)
二、基本原理
1. 表达式解析的算法选择
计算器的核心挑战在于表达式解析。常见的解决方案有:
- 递归下降解析法(Recursive Descent Parsing)
- 基于栈的Shunting Yard算法
- 使用第三方库(如Javaluator)
Shunting Yard算法因其对运算符优先级的处理能力,成为中规模计算器的优选方案。该算法通过维护两个栈(运算符栈和输出栈)实现表达式转换,具体流程如下:
- 从左到右扫描表达式
- 数字直接入输出栈
- 运算符根据优先级与栈顶元素比较决定入栈或弹出
- 左括号直接入栈,右括号触发栈顶元素弹出直到左括号
- 最后将运算符栈中剩余元素弹出
2. 健壮性设计原则
在中规模系统中,需要考虑:
- 输入验证:对非法字符、非法格式的处理
- 异常处理:捕获除零、无效操作等异常
- 代码可维护性:遵循单一职责原则
- 扩展性:支持后续功能扩展
三、环境准备
开发环境要求:
- JDK 17+
- IDE(推荐IntelliJ IDEA或Eclipse)
- Maven项目结构(推荐使用Spring Boot简化开发)
项目结构建议:
src
├── main
│ ├── java
│ │ └── calculator
│ │ ├── core
│ │ │ ├── ExpressionEvaluator.java
│ │ │ ├── Tokenizer.java
│ │ │ └── Parser.java
│ │ └── service
│ │ └── CalculatorService.java
│ └── resources
│ └── application.properties
└── test
└── java
└── calculator
└── core
└── ExpressionEvaluatorTest.java四、核心实现
1. 表达式分词器(Tokenizer)
public class Tokenizer {
public static List<Token> tokenize(String expression) {
List<Token> tokens = new ArrayList<>();
StringBuilder currentNumber = new StringBuilder();
boolean isNegative = false;
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (Character.isDigit(c) || c == '.') {
currentNumber.append(c);
} else if (c == '+' || c == '-' || c == '*' || c == '/') {
if (!currentNumber.isEmpty()) {
tokens.add(new Token(TokenType.NUMBER, currentNumber.toString()));
currentNumber.setLength(0);
}
tokens.add(new Token(TokenType.OPERATOR, String.valueOf(c)));
} else if (c == '(' || c == ')') {
if (!currentNumber.isEmpty()) {
tokens.add(new Token(TokenType.NUMBER, currentNumber.toString()));
currentNumber.setLength(0);
}
tokens.add(new Token(TokenType.PARENTHESIS, String.valueOf(c)));
} else if (c == ' ') {
continue;
} else {
throw new IllegalArgumentException("Invalid character: " + c);
}
}
if (!currentNumber.isEmpty()) {
tokens.add(new Token(TokenType.NUMBER, currentNumber.toString()));
}
return tokens;
}
}关键点解析:
- 识别数字和小数点
- 处理运算符和括号
- 严格校验非法字符
- 支持负数处理(需额外扩展)
2. 表达式解析器(Parser)
public class Parser {
private List<Token> tokens;
private int index = 0;
public Parser(List<Token> tokens) {
this.tokens = tokens;
}
public List<String> parse() {
List<String> postfix = new ArrayList<>();
Deque<String> operatorStack = new ArrayDeque<>();
while (index < tokens.size()) {
Token token = tokens.get(index);
switch (token.getType()) {
case NUMBER:
postfix.add(token.getValue());
index++;
break;
case OPERATOR:
String op = token.getValue();
while (!operatorStack.isEmpty() &&
getPrecedence(op) <= getPrecedence(operatorStack.peek())) {
postfix.add(operatorStack.pop());
}
operatorStack.push(op);
index++;
break;
case PARENTHESIS:
if (token.getValue().equals("(")) {
operatorStack.push(token.getValue());
index++;
} else {
while (!operatorStack.isEmpty() &&
!operatorStack.peek().equals("(")) {
postfix.add(operatorStack.pop());
}
if (!operatorStack.isEmpty() && operatorStack.peek().equals("(")) {
operatorStack.pop(); // 弹出左括号
}
index++;
}
break;
default:
throw new IllegalArgumentException("Unknown token: " + token.getType());
}
}
while (!operatorStack.isEmpty()) {
postfix.add(operatorStack.pop());
}
return postfix;
}
private int getPrecedence(String op) {
return switch (op) {
case "+" -> 1;
case "-" -> 1;
case "*" -> 2;
case "/" -> 2;
default -> 0;
};
}
}关键点解析:
- 使用双栈算法处理运算符优先级
- 正确处理括号嵌套
- 支持多层运算符
- 异常处理机制
3. 表达式计算器(Evaluator)
public class ExpressionEvaluator {
public static double evaluate(String expression) {
List<Token> tokens = Tokenizer.tokenize(expression);
List<String> postfix = new Parser(tokens).parse();
Deque<Double> stack = new ArrayDeque<>();
for (String token : postfix) {
if (token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/")) {
if (stack.size() < 2) {
throw new IllegalArgumentException("Invalid expression: insufficient operands");
}
double b = stack.pop();
double a = stack.pop();
switch (token) {
case "+": stack.push(a + b); break;
case "-": stack.push(a - b); break;
case "*": stack.push(a * b); break;
case "/":
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
stack.push(a / b);
break;
}
} else {
stack.push(Double.parseDouble(token));
}
}
if (stack.size() != 1) {
throw new IllegalArgumentException("Invalid expression: too many operands");
}
return stack.pop();
}
}关键点解析:
- 使用后缀表达式计算
- 异常处理机制
- 支持除零异常
- 数值精度控制
五、完整案例
1. 简易计算器应用(GUI版本)
public class CalculatorApp {
public static void main(String[] args) {
JFrame frame = new JFrame("Calculator");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 300);
JTextField inputField = new JTextField();
inputField.setEditable(false);
inputField.setBounds(10, 10, 300, 30);
JButton calculateButton = new JButton("Calculate");
calculateButton.setBounds(10, 50, 100, 30);
JTextArea resultArea = new JTextArea();
resultArea.setEditable(false);
resultArea.setBounds(10, 90, 300, 150);
calculateButton.addActionListener(e -> {
try {
String expression = inputField.getText();
double result = ExpressionEvaluator.evaluate(expression);
resultArea.setText("Result: " + result);
} catch (Exception ex) {
resultArea.setText("Error: " + ex.getMessage());
}
});
frame.setLayout(null);
frame.add(inputField);
frame.add(calculateButton);
frame.add(resultArea);
frame.setVisible(true);
}
}2. 控制台测试案例
public class TestCalculator {
public static void main(String[] args) {
String[] testExpressions = {
"3 + 5",
"10 * (2 + 3)",
"5 / 0",
"2 + 3 * (4 - 1)",
"invalid expression",
"(2 + 3) * 4",
"100 - 50 / 2"
};
for (int i = 0; i < testExpressions.length; i++) {
System.out.println("Test case " + (i+1) + ": " + testExpressions[i]);
try {
System.out.println("Result: " + ExpressionEvaluator.evaluate(testExpressions[i]));
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
}
}
}
}3. 异常处理测试
public class ExceptionTest {
public static void main(String[] args) {
String[] errorExpressions = {
"3 + * 5",
"10 + 5 /",
"2 + 3 * (",
"100 / 0",
"2 + 3 * (4 - 5)"
};
for (int i = 0; i < errorExpressions.length; i++) {
System.out.println("Error test " + (i+1) + ": " + errorExpressions[i]);
try {
ExpressionEvaluator.evaluate(errorExpressions[i]);
System.out.println("No error");
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
}
}
}
}六、源码解析
1. 表达式分词器的实现细节
在Tokenizer类中,通过逐字符处理实现分词:
- 数字和小数点直接加入数字token
- 运算符和括号单独处理
- 空格直接跳过
- 非法字符抛出异常
此设计确保了输入的合法性校验,避免后续解析阶段出现未处理的异常。
2. 表达式解析器的算法实现
Parser类中的parse方法实现了Shunting Yard算法:
- 使用双栈结构处理运算符优先级
- 正确处理括号嵌套
- 支持多层运算符
- 严格校验输入合法性
该算法的时间复杂度为O(n),适用于中规模计算需求。
3. 表达式计算器的异常处理
ExpressionEvaluator类中的异常处理机制:
- 除零异常检测
- 运算符数量校验
- 数值转换异常处理
- 末尾运算符检测
这些机制确保了计算过程的健壮性。
七、进阶使用
1. 支持函数计算
扩展ExpressionEvaluator以支持函数计算:
public class FunctionEvaluator {
public static double evaluateWithFunctions(String expression) {
// 实现函数解析逻辑
// 支持sin、cos、sqrt等数学函数
// 需要处理函数调用的语法结构
}
}2. 历史记录功能
添加历史记录功能:
public class HistoryManager {
private List<String> history = new ArrayList<>();
public void addHistory(String expression, double result) {
history.add("Expression: " + expression + ", Result: " + result);
}
public List<String> getHistory() {
return history;
}
}3. 跨平台UI实现
使用JavaFX实现跨平台UI:
public class JavaFXCalculator {
public static void main(String[] args) {
launch(args);
}
public static void start(Stage primaryStage) {
// 实现JavaFX界面
}
}八、性能与工程实践
1. 性能优化策略
- 使用更高效的字符串处理算法
- 预处理输入表达式
- 使用缓存机制
- 使用线程池处理并发计算
- 优化算法时间复杂度
2. 异常处理优化
- 增加详细的错误信息
- 使用自定义异常类
- 分层异常处理
- 日志记录异常信息
3. 安全性考虑
- 输入过滤机制
- 防止注入攻击
- 防止恶意输入导致程序崩溃
- 使用安全的数值转换方法
4. 代码可维护性
- 使用设计模式(如策略模式)
- 分层架构设计
- 单元测试覆盖
- 文档注释
九、常见问题与踩坑
1. 常见错误及解决办法
错误1:未处理括号导致计算错误
// 错误代码
ExpressionEvaluator.evaluate("10 * (2 + 3)");解决方法:确保解析器正确处理括号嵌套
错误2:运算符优先级错误
// 错误代码
ExpressionEvaluator.evaluate("2 + 3 * 4");解决方法:确保解析器正确识别运算符优先级
错误3:未处理负数
// 错误代码
ExpressionEvaluator.evaluate("-5 + 10");解决方法:在分词器中增加负数处理逻辑
2. 性能瓶颈分析
表达式解析过程中,字符串处理和栈操作可能导致性能瓶颈。可以通过以下方式优化:
- 预处理表达式
- 使用更高效的数据结构
- 减少不必要的字符串操作
- 使用缓存机制
3. 安全风险分析
用户输入可能包含恶意代码,需进行严格校验:
// 错误代码
ExpressionEvaluator.evaluate("Math.sqrt(-1)");解决方法:增加输入校验,防止非法函数调用
十、最佳实践
- 使用设计模式:采用观察者模式处理历史记录,使用策略模式处理不同计算方式
- 分层架构设计:将解析、计算、UI分离,提高可维护性
- 严格的输入校验:在分词阶段即进行合法性校验
- 异常处理机制:为每个操作增加异常处理
- 单元测试覆盖:为每个功能模块编写单元测试
- 文档注释:为每个类和方法添加详细注释
- 性能优化:针对关键路径进行优化
十一、总结
通过本实训项目,我们深入探讨了中规模计算器开发的关键技术点。从表达式解析的算法选择,到健壮性设计,再到性能优化和安全性考虑,每个环节都需要精心设计。
在实际开发中,这种计算器设计适用于:
- 金融计算工具
- 科学计算软件
- 教育类应用
- 工业控制软件
但需要注意:
- 无法处理超大规模计算
- 无法处理复杂的函数嵌套
- 不适合高并发场景
通过本项目,我们不仅掌握了中规模软件开发的技巧,更理解了如何在实际项目中应用这些技术。建议在开发复杂系统时,始终遵循良好的设计原则,注重代码可维护性和扩展性。
评论已关闭