目录
1. 程序介绍
2. 代码
3. 学习历程
一、程序介绍
此次项目没有采用传统的两个输入框传数字的方式,而是模拟真实的计算器,采用输入一行表达式,计算出最终结果。实现所用的工具类为ArrayList,Stack和StringBuilder,前后端通讯方法为Get。整个项目的后端代码分为三大部分:
-
对输入的表达式进行处理
-
该步骤的目的是把字符串切分成中缀表达式储存在ArrayList里
-
使用StringBuilder保存表达式,以便对单个字符进行增删
-
中文括号能自动转化成英文;非零数字开头即使有0,也能被parseDouble方法自动去掉
-
加入了对非法字符的检查,仅允许数字,运算符和括号进入下一环节
-
另创建了一个ArrayList变量,用于存储中缀表达式各个部分的变量类型,因为存进去的任何元素都会变成Object
-
-
中缀表达式转化成后缀表达式
- 运用数据结构课上所学,把难处里的中缀表达式转化成易处理的后缀表达式。该步骤能把括号去掉,仅保留数字和运算符,因此也能检查括号不匹配问题。
- 使用了栈的思想处理运算符和数字的顺序
- 用biggerThan方法来比较运算符优先级,决定是否将栈里的运算符存进最终的后缀表达式里
-
后缀表达式的计算
- 使用try-catch语句保护主体运算部分,一旦运算出错就终止程序。这可以解决运算符缺少或冗余的问题。
- 没有对除以0进行检查,浏览器会自动把除以0的式子的结果变成Infinity
- 没有对2147483647附近的溢出进行处理,因为数据类型是Double,可表示的范围够用,对Double判溢出的边界条件写起来也很麻烦
- 后缀表达式的运算方法是:检测到运算符,就对运算符前面的两个数字进行运算,并从该表达式中移除这三个变量,直到把所有的变量都移出去。该方法是线性运算,没有调整运算顺序的可能,时间复杂度为O(n)。
-
食用方法
- 前端只有一个输入框,输入形如5/(5*2+3)的式子,点击提交按钮就能输出结果,浏览器自动保留16位小数
二、我的代码
Class MainService :
package com.example.firstspringboot.service;
import java.util.ArrayList;
import java.util.Stack;
import org.springframework.stereotype.Service;
@Service
public class MainService {
public Boolean biggerThan(Character chA, Character chB){
if(chA == '(' || chB == '('){
return false;
} else if(chB == ')'){
return true;
} else if(chA == '+' || chA == '-'){
switch(chB){
case '+' :
case '-' :
return true;
case '*' :
case '/' :
return false;
}
} else if(chA == '*' || chA == '/'){
switch(chB){
case '+' :
case '-' :
return true;
case '*' :
case '/' :
return true;
}
}
return false;
}
public Double answer(Double d1, Double d2, Character op){
switch(op){
case '+' : return d1+d2;
case '-' : return d1-d2;
case '*' : return d1*d2;
case '/' : return d1/d2;
}
return 0.0;
}
public String calculator(String str){
/*
首先转化成中缀表达式,用ArrayList存储每个元素
*/
ArrayList middleStr = new ArrayList(); //中缀表达式存储
ArrayList<Integer> type = new ArrayList(); //type为0表示这个位置是数字,为1是运算符,为2是#,用于中缀转后缀判断数据类型
StringBuilder tempItem = new StringBuilder(); //提取字符,转化为数字
for(int i = 0 ; i < str.length() ; i++){ //逐个提取字符,遇到非数字或运算符则终止
Character temp = str.charAt(i);
if(temp == '('){ //中文括号调整成英文
temp = '(';
}else if(temp == ')'){
temp = ')';
}
if(temp >= '0' && temp <= '9'){ //是数字字符就存下来
tempItem.append(temp);
} else if(temp == '+' || temp == '-' || temp == '*' || temp == '/' || temp == '(' || temp == ')'){
if(tempItem.isEmpty()){ //如果数字暂存区没东西就直接把运算符保存下来
middleStr.add(temp);
type.add(1);
} else{ //数字暂存区有东西要先把数字存进去,再存运算符
Double d = Double.parseDouble(tempItem.toString()); //形如05的数字也可以正常运算
middleStr.add(d);
type.add(0);
tempItem.delete(0,tempItem.length());
middleStr.add(temp);
type.add(1);
}
} else{
return "输入的表达式格式不合法,里面有其他字符。" ;
}
}
if(!tempItem.isEmpty()){
Double d = Double.parseDouble(tempItem.toString());
middleStr.add(d);
type.add(0);
}
middleStr.add('#'); //转后缀表达式所用的间隔符
type.add(2);
/*
中缀转后缀
*/
Stack operatorStack = new Stack();
operatorStack.push('#');
ArrayList backStr = new ArrayList();
for(int i = 0 ; i < middleStr.size() ; i++){
if(type.get(i) == 0){ //数字
backStr.add(middleStr.get(i));
} else if(type.get(i) == 1){ //运算符
if(middleStr.get(i).equals('(')){ //左括号
operatorStack.push(middleStr.get(i));
} else if(middleStr.get(i).equals(')')){ //右括号
while(!operatorStack.peek().equals('(') && !operatorStack.peek().equals('#')){ //非左括号就弹出
backStr.add(operatorStack.pop());
}
if(operatorStack.peek().equals('(')){ //是左括号就消去
operatorStack.pop();
} else if(operatorStack.peek().equals('#')){ //没左括号就报错
return "输入的表达式格式不合法,右括号多了。";
}
} else{
while(biggerThan((Character)operatorStack.peek(), (Character)middleStr.get(i))){ //栈顶大就弹出
backStr.add(operatorStack.pop());
}
operatorStack.push(middleStr.get(i));
}
}
}
while(!operatorStack.peek().equals('#')){ //清除栈中剩下的运算符,如果里面有左括号,说明输入格式不对
if(!operatorStack.peek().equals('(')){
backStr.add(operatorStack.pop());
} else{
return "输入的表达式格式不合法,左括号多了。";
}
}
/*
后缀表达式运算,中途卡住则报错表达式格式错误
*/
int index = 0;
Double d1;
Double d2;
Character op;
while(backStr.size() > 1){
try{
Object current = backStr.get(index);
if(current.equals('+') || current.equals('-') || current.equals('*') || current.equals('/')){
d1 = (Double)backStr.get(index-2);
d2 = (Double)backStr.get(index-1);
op = (Character)backStr.get(index);
Double ans = answer(d1,d2,op);
backStr.remove(index-2);
backStr.remove(index-2);
backStr.remove(index-2);
backStr.add(index-2,ans);
index = index-2;
}
} catch(Exception e){ //强转或进行运算时有一步出错都终止运算,因为肯定是输入格式不对
return "输入的表达式格式不合法,运算符冗余或不足。";
} finally{
if(index >= backStr.size()){
index = 0;
} else{
index++;
}
}
if(backStr.size() == 2){ //最后只剩下两个元素,无法继续运算,也无法退出循环
return "输入的表达式格式不合法,运算符少了。";
}
}
return backStr.get(0).toString();
}
}
Class Main :
package com.example.firstspringboot.controller;
import com.example.firstspringboot.service.MainService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/main")
public class Main{
@Autowired
MainService mainService;
@RequestMapping(value = "calculator" , method = RequestMethod.GET)
public String calculator(@RequestParam String str){
return mainService.calculator(str);
}
}
前端Html :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<form action="http://localhost:8080/main/calculator" method="get">
<input type="text" name="str" />
<input type="submit" name="sub" />
</form>
</body>
</html>
三、学习历程
- 防止注入攻击的办法:整个项目只有一个参数,一个输入框,&p2=4一点用也没有
- 学会了使用StringBuilder类的各种方法来操作字符串,因为Java原本的String不支持修改内容。这种新形式类似C++的string。
- ArrayList在不加泛型的时候,内容全都是Object类型,很多用于比较或运算的方法没法用,此时需要有充足的理由来强转类型,并保证这样做是不会出问题的。
- Java的Stack只能看见栈顶元素,没法在不创建其他容器的条件下无损遍历
- 善用try-catch能在特殊环境下得到极大的便利,比如后缀表达式运算时。运算进行不下去?一定是用户打错字了,这时候直接catch终止,让他重新输入。(因为我前面已经做了其他各种类型的数据检查,所以有理由中断程序)