一、 项目准备
需求分析:
编程语言及开发工具:
JAVA,使用IDEA
二、实现过程
1.中缀表达式->后缀表达式
基于堆栈的算法:从左到右扫描每一个字符。
(1)如果扫描到的字符是操作数(如a、b等),就直接输出这些操作数。
(2)如果扫描到的字符是一个操作符,分三种情况:
①如果堆栈是空的,直接将操作符存储到堆栈中(push it)
②如果该操作符的优先级大于堆栈出口的操作符,就直接将操作符存储到堆栈中(push it)
③如果该操作符的优先级低于堆栈出口的操作符,就将堆栈出口的操作符导出(pop it), 直到该操作符的优先级大于堆栈顶端的操作符。将扫描到的操作符导入到堆栈中(push)。
(3)如果遇到的操作符是左括号"(”,就直接将该操作符输出到堆栈当中。该操作符只有在遇到右括号“)”的时候移除。这是一个特殊符号该特殊处理。
(4)如果扫描到的操作符是右括号“)”,将堆栈中的操作符导出(pop)到output中输出,直到遇见左括号“(”。将堆栈中的左括号移出堆栈(pop )。继续扫描下一个字符
(5)如果输入的中缀表达式已经扫描完了,但是堆栈中仍然存在操作符的时候,我们应该讲堆栈中的操作符导出并输入到output 当中。
2.后缀表达式的计算。
堆栈法计算后缀表达式的值:
(1)从左到右扫描后缀表达式字符串
(2)初始化一个空栈
(3)如果扫描到数字,那么就直接入栈
(4)如果被扫描的字符是一个二元运算符,那么就连续出栈两次,获得两个字符,元素出栈后,应用运算符进行计算,并将结果压栈
(5)重复3)和4)的操作,直至扫描完字符串
(6)扫描完所有字符串之后,栈中只剩一个元素,该元素就是最终结果,将其出栈并返回。
3.GUI界面的设计
1.JAVA swing 各组件的混合运用;
2.按钮的监听;
3.界面的设计;
关于界面设计的学习详见以下博客 ,带着耐心从中慢慢学习使我受益良多。
https://blog.csdn.net/xietansheng/article/details/72814492
三、完整代码
具体细节和难点详见代码注释。
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.math.BigDecimal;
import java.math.RoundingMode;
import javax.swing.*;
//主类,继承JFrame框架,实现事件监听器接口
public class Main extends JFrame implements ActionListener {
String []KEYS = { "7", "8", "9", "C", "4", "5", "6", "back", "1", "2", "3", "+", "0", ".", "pi", "-", "sqrt",
"%", "/", "*", "AC", "(", ")", "=" };
JButton []keys = new JButton[KEYS.length];
JTextArea resultText = new JTextArea("0.0");// 文本域组件TextArea可容纳多行文本;文本框内容初始值设为0.0
JTextArea History = new JTextArea();// 历史记录文本框初始值设为空
JPanel jp1=new JPanel();
JPanel jp2=new JPanel();
JScrollPane gdt1=new JScrollPane(resultText);//给输入显示屏文本域新建一个垂直滚动滑条
JScrollPane gdt2=new JScrollPane(History);//给历史记录文本域新建一个垂直滚动滑条
JLabel label = new JLabel("历史记录");
String b="";
static int index;
static int left,right;//左右括号标记
static int count;//成功运算标记
Font font = new Font("TIMES NEW ROMAN", Font.PLAIN, 14);//设置字体
// 构造方法
public Main() {
super("实用计算器");
resultText.setBounds(20, 18, 255, 115);// 设置文本框大小
resultText.setAlignmentX(RIGHT_ALIGNMENT);// 文本框内容右对齐
//resultText.setEditable(false);// 文本框不允许修改结果
History.setBounds(290, 40, 250,370);// 设置文本框大小
History.setAlignmentX(LEFT_ALIGNMENT);// 文本框内容右对齐
//History.setEditable(false);// 文本框不允许修改结果
label.setBounds(300, 15, 100, 20);//设置标签位置及大小
jp2.setBounds(290,40,250,370);//设置面板窗口位置及大小
jp2.setLayout(new GridLayout());
jp1.setBounds(20,18,255,115);//设置面板窗口位置及大小
jp1.setLayout(new GridLayout());
resultText.setLineWrap(true);// 激活自动换行功能
resultText.setWrapStyleWord(true);// 激活断行不断字功能
resultText.setSelectedTextColor(Color.RED);
History.setLineWrap(true);//自动换行
History.setWrapStyleWord(true);
History.setSelectedTextColor(Color.blue);
gdt1.setViewportView(resultText);//使滚动条显示出来
gdt2.setViewportView(History);
jp1.add(gdt1);//将滚动条添加入面板窗口中
jp2.add(gdt2);
this.add(jp1);//将面板添加到总窗体中
this.add(jp2);//将面板添加到总窗体中
this.setLayout(null);
this.add(label);// 新建“历史记录”标签
// 放置按钮
int x = 20, y = 150;
for (int i = 0; i < KEYS.length; i++) {
keys[i] = new JButton();
keys[i].setText(KEYS[i]);
keys[i].setBounds(x, y, 62, 40);
keys[i].setBackground(new Color(144, 201, 108));//设置按钮颜色
keys[i].setFont(font);//设置按钮字体
if (x < 215) {
x += 65;
} else {
x = 20;
y += 45;
}
this.add(keys[i]);
}
keys[16].setBackground(Color.ORANGE);
for (int i = 0; i < KEYS.length; i++){// 每个按钮都注册事件监听器
keys[i].addActionListener(this);
}
this.setResizable(false);
this.setBounds(500, 200, 567, 480);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setVisible(true);
}
// 事件处理
public void actionPerformed(ActionEvent e) {
String label=e.getActionCommand();//获得事件源的标签
left=0;
right=0;
index=resultText.getText().length()-1;
if(!resultText.getText().equals("")){
//计算括号的数量
for(int i=resultText.getText().length()-1;i>=0;i--){
switch (resultText.getText().charAt(i)) {
case '(' -> left++;
case ')' -> right++;
}
}
//找到最后一个数字的下标
if(!"+-*/()".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
for(;index>0;index--){
if("+-*/(".contains(resultText.getText().charAt(index)+"")){
break;
}
}
}
}
if("0123456789".contains(label)){//输入数字时:
if(count==1){//文本框内为上一个有效算式的答案,则替换为后输入的数字
b=label;
resultText.setText(b);
count=0;
}
else if(("0".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")&&
resultText.getText().length()<=1)){//无法输入0开头的整数
b=label;
resultText.setText(b);
}
else if(("0".contains(resultText.getText().charAt(resultText.getText().length()-1)+""))&&
("+-*/%".contains(resultText.getText().charAt(resultText.getText().length()-2)+""))){
StringBuilder BK = new StringBuilder(this.b);//无法输入0开头的整数
resultText.setText(BK.substring(0, this.b.length() - 1)+label);
b=resultText.getText();
}
else{
this.b = this.b + label;
resultText.setText(b);
}
}
//输入左括号时
else if(label.equals("(")){
if(b.equals("")){
resultText.setText("(");
b=resultText.getText();
}
//前面是数字和右括号时补乘,是小数点时无法输入
else if("0123456789)".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
b=b+"*(";
resultText.setText(resultText.getText()+"*(");
}
else if(".".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
}
else{
b=b+"(";
resultText.setText(resultText.getText()+"(");
}
}
else if (label==")"){//输入右括号时
if (b.equals("")){//表达式为空时不做响应
}
else if("+-*/(".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")
||left==right){//运算符、左括号后不能跟右括号,左右括号数量匹配时不能输入右括号
}
//前面是右括号时补乘
else if(b.length()>0&&(")".contains(resultText.getText().charAt(resultText.getText().length()-1)+""))){
b=b+"*"+label;
resultText.setText(b);
}
else {
resultText.setText(resultText.getText()+")");
b=resultText.getText();
}
}
else if(label=="."){//输入小数点时
if(count==1){//文本框内为上一个有效算式的答案,则替换
b="0"+label;
resultText.setText(b);
count=0;
}
if((b.equals(""))
||"(+-*/".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
b=b+"0.";
resultText.setText(b);
}
else if(")".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
b=b+"*0.";//前面是右括号时补乘
resultText.setText(b);
}
else if (".".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
}//前面是小数点时不作响应
else if(!b.substring(index+1).contains(".")){//前面是不带小数点的数字时正常输入
b=b+".";
resultText.setText(b);
}
else{//前面是带小数点的数字时不响应
}
}
else if(label.equals("=")){//输入等于号时,调用计算方法,得出最终结果
//resultText.setText(this.b);
History.setText(History.getText()+resultText.getText());
if (".".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
StringBuilder BK = new StringBuilder(this.b);
resultText.setText(BK.substring(0, this.b.length() - 1));
this.b=resultText.getText();
}
try {
String s[] = Calculate(this.b);
String result = Result(s);
this.b = result + "";
double Test1;
Test1=Double.parseDouble(this.b);//将答案字符串转换成双精度型数据检测是否有错误
//更新文本框,当前结果在字符串b中,并未删除,下一次输入接着此结果以实现连续运算
resultText.setText(this.b);
History.setText(History.getText() + "=" + resultText.getText() + "\n");
count=1;
}catch (Exception e1){//抓取错误,不参与运算并提示输入错误,并将文本区域初始化
JOptionPane.showMessageDialog(null,"表达式错误");
History.setText(History.getText() +":表达式不完整或将0设置为了除数" + "\n");
this.b="";
resultText.setText("0");
}
}
else if(label=="C"){ //清空按钮,消除显示屏文本框前面所有的输入和结果
this.b="";
resultText.setText("0");//更新文本域的显示,显示初始值;
}
else if(label=="sqrt") {//开方功能并不完善,目前只能用于单目运算
String n=kFys(this.b);
resultText.setText("sqrt"+"("+this.b+")"+"="+n);//使运算表达式显示在输入界面
History.setText(History.getText()+resultText.getText()+"\n");//获取输入界面的运算表达式并使其显示在历史记录文本框
this.b=n;
}
else if(label=="AC") {
this.b="";
resultText.setText("0");
History.setText(" ");
}
else if(label=="pi") {
String m=String.valueOf(3.14159265);
this.b=this.b+m;
resultText.setText(this.b);
}
else if(label=="back"&&this.b.length()>1) {//退格
StringBuilder BK = new StringBuilder(this.b);
resultText.setText(BK.substring(0, this.b.length() - 1));
this.b=resultText.getText();
}
else if((label=="back")&&(this.b.length()<=1)){//防止文本框为空时打印退格键
this.b="";
resultText.setText("0");
}
else if (label=="-"){//运算符后不能输入减号
if("+-*/.".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
}
else{
this.b = this.b + label;
resultText.setText(this.b);
}
}
else if(label=="+"||label=="*"||label=="/"||label=="%"||label=="sqrt") {//输入运算符时
if("+-*/(%.".contains(resultText.getText().charAt(resultText.getText().length()-1)+"")){
}//运算符后不能输入运算符
else if(b.length()==0) {//当文本框为空时无法输入减号以外的运算符
}
else{
this.b = this.b + label;
resultText.setText(this.b);
}
}
else {
b = b + label;
resultText.setText(this.b);
}
}
//将中缀表达式转换为后缀表达式
public String[] Calculate(String str) {
char opStack[] = new char[100];// 静态栈,对用户输入的操作符进行处理,用于存储运算符
String postQueue[] = new String[100];// 后缀表达式字符串数组,为了将多位数存储为独立的字符串
int top = -1, j = 0; // 静态指针top,控制变量j
if(str.charAt(0)=='-'){//如果表达式以负号开头,在最前面补一个0,方便计算
str="0"+str;
}
StringBuilder addZero= new StringBuilder(str);//将表达式中的所有"(-"变为"(0-"以方便计算
if(str.contains("(-")){
do{
addZero.insert(addZero.indexOf("(-")+1,'0');
str=addZero.toString();
}while (str.contains("(-"));
}
// 用于承接多位数的字符串
String s = "";
for (int i = 0; i < str.length(); i++){// 遍历中缀表达式
// indexOf函数,返回字串首次出现的位置;charAt函数返回index位置处的字符;
if ("0123456789.".indexOf(str.charAt(i)) >= 0){ // 遇到数字字符的情况
s = "";// 作为承接字符,每次开始时都要清空
for (; i < str.length() && "0123456789.".indexOf(str.charAt(i)) >= 0; i++) {
s = s + str.charAt(i);
}
i--;
postQueue[j] = s;// 数字字符直接加入后缀表达式
j++;
}
else if ("(".indexOf(str.charAt(i)) >= 0) {// 遇到左括号
top++;
opStack[top] = str.charAt(i);// 左括号入栈
}
else if (")".indexOf(str.charAt(i)) >= 0) {// 遇到右括号
for (;;){// 栈顶元素循环出栈,直到遇到左括号为止
if (opStack[top] != '(') {// 栈顶元素不是左括号
postQueue[j] = opStack[top] + "";// 栈顶元素出栈
j++;
top--;
}
else { // 找到栈顶元素是左括号
top--;// 删除栈顶左括号
break;// 循环结束
}
}
}
else if ("*%/+-".indexOf(str.charAt(i)) >= 0)// 遇到运算符
{
if (top == -1)
{// 若栈为空则直接入栈
top++;
opStack[top] = str.charAt(i);
}
else if ("*%/".indexOf(opStack[top]) >= 0)
{// 当栈顶元素为高优先级运算符时,让栈顶元素出栈进入后缀表达式后,当前运算符再入栈
postQueue[j] = opStack[top] + "";
j++;
opStack[top] = str.charAt(i);
}
else
{
top++;
opStack[top] = str.charAt(i);// 当前元素入栈
}
}
}
while (top != -1) {// 遍历结束后将栈中剩余元素依次出栈进入后缀表达式
postQueue[j] = opStack[top] + "";
j++;
top--;
}
return postQueue;
}
//开方运算方法
public String kFys(String str) {
String result = "";
double a = Double.parseDouble(str), b = 0;
b = Math.sqrt(a);
result = String.valueOf(b);//将运算结果转换为string类型并赋给string类型的变量result
return result;
}
// 计算后缀表达式,并返回最终结果
public String Result(String str[]) {
String[] Result = new String[100];// 顺序存储的栈,数据类型为字符串
int Top = -1;// 静态指针Top
for (int i = 0; str[i] != null; i++) {
if ("+-*%/".indexOf(str[i]) < 0) {
Top++;
Result[Top] = str[i];
}
if ("+-*%/".indexOf(str[i]) >= 0)// 遇到运算符字符,将栈顶两个元素出栈计算并将结果返回栈顶
{
BigDecimal x, y, n;
x = BigDecimal.valueOf(Double.parseDouble(new BigDecimal(Result[Top]).toString()));// 顺序出栈两个数字字符串,并转换为double类型
Top--;
y = BigDecimal.valueOf(Double.parseDouble(new BigDecimal(Result[Top]).toString()));
Top--;
if ("-".indexOf(str[i]) >= 0) {
n = y.subtract(x);
Top++;
Result[Top] = String.valueOf(n);// 将运算结果重新入栈
}
if ("+".indexOf(str[i]) >= 0) {
n = y.add(x);
Top++;
Result[Top] = String.valueOf(n);// 将运算结果重新入栈
}
if ("*".indexOf(str[i]) >= 0) {
n = y.multiply(x);
Top++;
Result[Top] = String.valueOf(n);// 将运算结果重新入栈
}
if ("/".indexOf(str[i]) >= 0){
n = y.divide(x,5,RoundingMode.HALF_UP);
Top++;
Result[Top] = String.valueOf(n);// 将运算结果重新入栈
}
if ("%".indexOf(str[i]) >= 0) {
double num1= Double.parseDouble(String.valueOf(y));
double num2= Double.parseDouble(String.valueOf(x));
n = BigDecimal.valueOf(num1%num2);
Top++;
Result[Top] = String.valueOf(n);// 将运算结果重新入栈
}
}
}
return Result[Top];// 返回最终结果
}
// 主函数
public static void main(String[] args) {
Main test = new Main();
}
}
四、遇到的问题及其解决过程
1.非法表达式的识别
当输入如图非法表达式时,会因为括号数未匹配而无法返回正确值。
解决方法:
在“=”的执行语句后使用以下代码抓取错误并提示
catch (Exception e1){//抓取错误,不参与运算并提示输入错误
JOptionPane.showMessageDialog(null,"表达式错误");
History.setText(History.getText() +":此表达式错误" + "\n");
添加后结果如下:
2.负数的运算问题
程序会因为无法运算负数而直接报错
解决方法:
加入以下“补零”代码
if(str.charAt(0)=='-'){//如果表达式以负号开头,在最前面补一个0,方便计算
str="0"+str;
}
StringBuilder addZero= new StringBuilder(str);//将表达式中的所有"(-"变为"(0-"以方便计算
if(str.contains("(-")){
do{
addZero.insert(addZero.indexOf("(-")+1,'0');
str=addZero.toString();
}while (str.contains("(-"));
}
五、程序功能演示
程序界面分为左边的操作显示区和右侧的历史记录区。
能提示被除数不允许为0等表达式异常产生的错误。
有退格、清空历史记录等便利功能
其中开方功能并不完善,目前只能进行单目运算。
12.20日补充
在经过修改后,计算器有了更智能的输入功能,例如所输入的右括号数量只能小于等于左括号数量,小数点前无数字时自动补零,无法连续输入运算符等等,期间也遇到bug越写越多的情况,但耐心写下来收获甚至不小于刚写完框架,之前没注意到的各种细节被慢慢完善很有成就感。