一、前言
写这篇文章,是为了整理下思路,记录下自己的学习记录,Java小白一枚,代码仅供参考,如有不合理之处,请谅解。
二、计算器具体要求
1.实现输入表达式完成计算(仅支持加减乘除)。
2.支持正负数小数等运算。
3.设计计算器界面。
4.基本错误处理完成。
三、核心思路
1.表达式处理
使用正则表达式判断表达式是否合理,算数表达式的规律,就是一个数后面接着一个运算符然后再接一个数,先暂时不考虑括号的出现问题,例如:1+3/4*3,分离开来就是1+,3/,4*,3。最后一个数字是不跟运算符的所以由这样的思考,我们用正则表达式来写,现在我们来考虑括号问题,我们目前这个部分的目的是实现检测输入的表达式是否合理,所以要考虑括号的位置和数量,但由于我的技术有限,我就采用了很朴素的方法来验证,就是想到什么情况就写什么情况的检测代码,如下图:
public static Boolean kh_pp(String str){
try {
int zkh = 0;
int ykh = 0;
for (int i =0;i<str.length();i++){
if (String.valueOf(str.charAt(i)).equals("(")){
zkh++;
} else if (String.valueOf(str.charAt(i)).equals(")")) {
ykh++;
}
}
return zkh == ykh;
}catch (Throwable e2){
jLabel.setText("括号匹配错误,未知异常!");
return false;
}
}
public static boolean str_pd(String str){
//先是去掉空格
//(-1)+4-(-2)
//表达式处理规则,思路:
//1.去掉里面所有的左右括号
//2.表达式一个数字后面必定有个运算符,除了最后一个数后没有运算符,其他都有
//3.括号数量相同
//功能要求
//1.实现多位正或负数的匹配,支持小数
//2.不允许出现数字以及运算符外的字符
//3.除数不能为0
try {
boolean flag1 = kh_pp(str); // 这里还有个方法是kh_pp用来判断括号的数量是否相等
boolean flag = false; //先定义表达式为不合理
if (str.matches(".*(\\-?\\d+(\\.\\d+)?)?\\(\\).*")){
return flag; // 检测如:1(),()这种情况
} else if (str.matches(".*\\-?\\d+(\\.\\d+)?\\(.*")) {
return flag; //检测如:1(3+2)这种情况
} else if (str.matches(".*\\-\\(\\d+(\\.\\d+)?\\).*")) {
return flag; //检测如:-(5) 这种写法,应写为(-5)
}
str = str.replace(" ",""); // 去掉空格
str = str.replace("(",""); // 将左右括号全部去掉,例如:(-1)+4-(-2) 变成 -1+4--2
str = str.replace(")",""); //
for (int i =0;i<str.length();i++){
String now = String.valueOf(str.charAt(i));
if (!now.matches("[0-9\\+\\-\\*\\/\\(\\)\\.]")){
jLabel.setText("表达式错误,含有未知字符!");
return flag; // 检测是否有不符合算术表达式的字符
}
}
if (str.matches("(\\-?\\d+(\\.\\d+)?[\\+\\-\\*\\/])+\\-?\\d+(\\.\\d+)?")){ //常规检测 一个数后面跟着一个运算符,最后一个数不跟运算符
if (str.matches(".*\\/0.*")){ // 检测除数是否为零
jLabel.setText("除数不能为0!");
}else {
if (flag1)flag = true; //如果前面的检测和现在的检测都是true 则将flag改为真
}
}
return flag;
}catch (Throwable e2){ //检测一切异常,如果出粗就返回false
jLabel.setText("表达式判断错误,未知异常!");
return false;
}
}
2.把表达式当成字符串处理,先将表达式中的数字和运算符分离。
这个部分完成的目的是表达式的分割,就是将表达式的数字和运算符分离,需要注意的是:正负数的多位数判断以及小数位数判断,使用while遍历拼接(小数点前后小数点后原理一样,就是遍历)。负数和正数为两种检测情况,所以分开写(其实是一开始没想到,后面写的OvO),此功能模块的返回值是一个分割后的列表,比如:(-6)/(-6)+(3-5)/2 ——》》["-6","/","-6","+","(","3","-","5",")","/","2"],为什么“(-6)”的括号不见了,是因为在代码最后做了处理,去掉了包裹着负数的括号,因为这些括号并不是优先级的括号,所以去掉。
具体代码如下:
public static List<String> str_fg(String str1){
//创建一个字符列表用来存储分割后的字符串表达式
//防止用书输入诸如(-1)+(-4)-5+((-6)/(-6)) 无用括号太多 去掉非优先级括号 也就是负数旁的括号
String str = str1;
List<String> ls = new ArrayList<>();
try {
//遍历表达式
String dws; // 多位数处理提前声明
char now=0; // 当前字符
int i = 0;
do {
//负数判断
//如果当前是运算符就直接放入列表,如果不是则进行数字判断
if ( (String.valueOf(str.charAt(i)).equals("-") && i==0) || // 负数在首位
(String.valueOf(str.charAt(i)).equals("-") && // 负数在左括号 如 (-3)
String.valueOf(str.charAt(i+1)).matches("[0-9]") &&
String.valueOf(str.charAt(i-1)).equals("(")) ||
(String.valueOf(str.charAt(i)).equals("-") &&
String.valueOf(str.charAt(i+1)).matches("[0-9]") &&
String.valueOf(str.charAt(i-1)).matches("[\\+\\-\\*\\/]"))){ // 负数在运算符后 如 -6/-6
dws = ""; // 拼接多位负数先定义为空
String dws_1="";
while( i < str.length() && (now = str.charAt(i+1))<=57 && (now = str.charAt(i+1))>=48 || str.charAt(i+1) =='.'){ // while为真执行,逐个遍历
if (i+1<str.length() && str.charAt(i+1)=='.' && str.charAt(i)>=48 && str.charAt(i)<=57){
int now_fs = i+1; // 小数点的位置
while(now_fs+1<str.length() && now_fs<str.length() && str.charAt(now_fs+1)<=57 && str.charAt(now_fs+1)>=48 ){ // 小数点后数字拼接
dws_1+=str.charAt(now_fs+1);
now_fs++;
i++;
}
break;
}else {
dws+=now; // 拼接
i++;
if (i+1>=str.length())break;
}
}
i++; // 负号也占一位
if (!dws_1.isEmpty()){dws += "."+dws_1;i++;} //小数点也占一位
ls.add("-"+dws); //多位负数处理
} else if((now = str.charAt(i))<48 || (now = str.charAt(i))>57){ // 0的ASCII为48 9的ASCII为57
ls.add(""+now); // now是字符 不能添加进String类型的列表 用""+now 即可解决
i++;
}else {
dws = ""; // 拼接多位数先定义为空
String dws_1="";
while( (i < str.length() && (now = str.charAt(i))<=57 && (now = str.charAt(i))>=48) || str.charAt(i) =='.'){ // while为真执行,逐个遍历
if (str.charAt(i)=='.' && str.charAt(i-1)>=48 && str.charAt(i-1)<=57){
int now_zs = i; // 小数点的位置
while(now_zs+1<str.length() && now_zs<str.length() && str.charAt(now_zs+1)<=57 && str.charAt(now_zs+1)>=48 ){
dws_1+=str.charAt(now_zs+1);
now_zs++;
i++;
}
break;
}else {
dws+=now; // 拼接
i++;
if (i>=str.length()) break;
}
}
if (!dws_1.isEmpty()){dws += "."+dws_1;i++;}
ls.add(dws);
}
}while (i<str.length());
for(int j =0;j<ls.size();j++){
if (ls.get(j).matches("\\-\\d+(\\.\\d+)?")){
if (j+1<ls.size() && j-1>=0 && ls.get(j-1).equals("(") && ls.get(j+1).equals(")") ){
ls.remove(j-1);
ls.remove(j);
}//去掉包裹着负数的括号
}else {
if (j+1<ls.size() && j-1>=0 && ls.get(j-1).equals("(") && ls.get(j+1).equals(")") ){
ls.remove(j-1);
ls.remove(j);
}//去掉包裹着正数的括号,以防有人这样输入(5)+(-3)
}
}
return ls;
}catch (ArrayIndexOutOfBoundsException e2){ //基本错误检测
List<String> ls2 = new ArrayList<>();
jLabel.setText("表达式分割错误,数组越界异常!");
return ls2; //返回空列表用于后面调用时的返回值检测
}catch (IndexOutOfBoundsException e3){
List<String> ls2 = new ArrayList<>();
jLabel.setText("表达式分割错误,表达式遍历越界异常!");
return ls2;
}catch (Throwable e4){
List<String> ls2 = new ArrayList<>();
jLabel.setText("表达式分割错误,未知异常!");
return ls2;
}
}
3.将分割后的表达式传入后缀表达式处理模块,可以看b站韩老师的视频,讲的很详细,036_尚硅谷_前缀 中缀 后缀表达式规则_哔哩哔哩_bilibili p36-p42,还有这个逆波兰 - 上(中缀表达式 转 后缀表达式)_哔哩哔哩_bilibili
具体代码如下图:
public static int yxj(String a){ //传入运算符返回运算符优先级
switch (a){
case "+", "-":
return 1;
case "*", "/":
return 2;
default:
return 0;
}
}
public static List<String> str_hz(List<String> ls){
List<String> ls1 = new ArrayList<>();
Stack<String> s1 = new Stack<>(); // 新建两个栈 因为不是计算部分 所以全部当成字符串处理
Stack<String> s2 = new Stack<>();
try{
for (String ele:ls){
//使用正则表达式判断
// \\-?\\d+ 表示匹配 -10 或者 -1 或则 10 或者 1 正负数多位数都考虑
if (ele.matches("\\-?\\d+(\\.\\d+)?")){
s2.push(ele); // 入栈
}else {
//符号入栈要考虑好几种情况
//1.如果s1为空,则直接入栈,或则s1栈的栈顶元素为"("
//2.如果不为空,s2栈顶也不为"(",则判断当前栈顶运算符的和当前要ele元素运算符的优先级大小
//如果ele运算符优先级大于栈顶元素优先级则入栈,否则将当前栈顶元素弹出并压入s2中,
//然后再次进入符号入栈判断情况
//3.ele为")" 则判断s1栈顶元素是不是"("如果不是则弹出当前s1栈顶元素并压入s2中,然后再重复此判断操作
//直到s1栈顶为"(" 此时弹出”(“但不压入s2中
//4.如果ele为"("则直接入s1
if (ele.equals("(") || s1.isEmpty() || s1.get(s1.size()-1).equals("(")){
s1.push(ele);
} else if (!s1.isEmpty() && !s1.get(s1.size()-1).equals("(")) {
//开始判断优先级
if (yxj(ele)>yxj(s1.get(s1.size()-1))){
//情况1 ele优先级大于s1栈顶
s1.push(ele);
}else if(yxj(ele)<=yxj(s1.get(s1.size()-1)) && !ele.equals(")")) {
//情况2
int top = s1.size()-1; // 获取s1栈顶下标
//jLabel.setText();("a"+top);
while (top!=-1 && yxj(ele) <= yxj(s1.get(top)) ){ //一直递减 直到s1栈元素为0或者s1中的栈顶优先级小于ele
//jLabel.setText();("b"+top);
s2.push(s1.pop()); // s1弹出的压入s2
top=top==0?-1:top-1;
}
s1.push(ele);
}else {
//情况3 如果为”)“
int top = s1.size()-1; // 获取s1栈顶下标
while (!s1.get(top).equals("(")){ //一直递减 直到s1栈顶元素为"("
s2.push(s1.pop()); // s1弹出的压入s2
top--;
}
s1.pop(); // 这一步时栈顶已为"(" 直接弹出 不需要压入s2
}
}
}
}
//执行完后 将s1的剩余运算符全部压入s2中
while (!s1.isEmpty()){
s2.push(s1.pop());
}
//将s2的元素从前往后取出即为 后缀
for (int i = 0;i<s2.size();i++){
ls1.add(s2.get(i));
}
//jLabel.setText();("当前s2"+s2);
return ls1;
}catch (EmptyStackException e1){
List<String> ls2 = new ArrayList<>();
jLabel.setText("后缀表达式转换错误,空栈异常!");
return ls2;
}catch (StackOverflowError e3){
List<String> ls2 = new ArrayList<>();
jLabel.setText("后缀表达式转换错误,栈溢出异常!");
return ls2;
}catch (Throwable e2){
List<String> ls2 = new ArrayList<>();
jLabel.setText("后缀表达式转换错误,未知异常!");
return ls2;
}
}
4.最后一个模块就是后缀表达式的计算,具体思路也可观看上面我发的韩老师的视频,讲的也很不错,还有一个小视频也很有启发, 逆波兰 - 下(后缀表达式计算结果)_哔哩哔哩_bilibili,具体代码如下:
public static double str_js(List<String> ls){
//创建一个double类型的栈s2 用来存数字
try{
Stack<Double> s2 = new Stack<>();
for(String ele:ls){ //遍历
if (ele.matches("\\-?\\d+(\\.\\d+)?")){
s2.push(Double.parseDouble(ele)); // 将ele转为double类型再压入栈中
}else {
//符号计算 进行 加减乘除四种情况
double num1 = s2.pop(); // 获取栈顶元素 pop会返回栈顶元素的值并删除当前栈顶
double num2 = s2.pop(); // 次顶元素
//后缀表达式 是 次顶的数 加减乘除 栈顶的数
if (ele.equals("+")){
s2.push(num2+num1);
} else if (ele.equals("-")) {
s2.push(num2-num1);
} else if (ele.equals("*")) {
s2.push(num2*num1);
}else {
s2.push(num2/num1);
}
}
}
return s2.pop(); // 栈中最后的一个结果就是运算结果
}catch (EmptyStackException e1){
jLabel.setText("计算出现错误,空栈异常!");
return 0;
}catch (StackOverflowError e3){
jLabel.setText("计算出现错误,栈溢出异常!");
return 0;
}catch (Throwable e2){
jLabel.setText("计算出现错误,未知异常!");
return 0;
}
}
四、界面设计(仅支持鼠标点击)
1.界面要求有一个文本框,显示结果和表达式,有10个数字按钮,以及+-*/()<—=等按钮,一共20个所以采用循环添加,自定义一个方法来添加组件,自定义一个鼠标单击事件,单击一个数字或运算符就将其添加到文本框中,如果单击”=“就运算,单击"<—"就是退格,具体实现代码如下:
static class MyMouseListener extends MouseAdapter{
@Override
public void mouseClicked(MouseEvent e){ // 单机事件
super.mouseClicked(e);
String str = e.toString(); // e返回的是这个按钮的一推属性 我们要的是按钮上的文字
String[] str_lst = str.split(",");//所以用逗号分割 倒数第二个为我们要的属性 为:text=按钮名字
if (str_lst[str_lst.length-2].charAt(5)=='='){ // 获取按钮上的字符
if (jLabel.getText().isEmpty()){
jLabel.setText("表达式为空");
}else {
String bds_str = jLabel.getText(); // -12
boolean flag = str_pd(bds_str);
if (flag){
List<String> str_fg = str_fg(bds_str);
if (str_fg.isEmpty()){
jLabel.setText("表达式分割Error!");
}else {
List<String> str_hz = str_hz(str_fg);
if (str_hz.isEmpty()){
jLabel.setText("后缀表达式Error!");
}else {
double str_js = str_js(str_hz);
if (str_js==0){
jLabel.setText("后缀表达式计算Error!");
}else {
DecimalFormat double_to3 = new DecimalFormat("#.###");//格式化 转为3位小数
String str_js_end = double_to3.format(str_js);
jLabel.setText(str_js_end);
}
}
}
} else {
jLabel.setText("表达式错误!");
}
}
}else if (str_lst[str_lst.length-2].charAt(5)=='C') {
jLabel.setText("");
}else if (str_lst[str_lst.length-2].charAt(5)=='<'){
String[] jl_text = jLabel.getText().split("");
if (jLabel.getText().matches("[\\u4e00-\\u9fa5]+.*")){
jLabel.setText("");
}else {
jLabel.setText("");
for (int i =0;i<jl_text.length-1;i++){
jLabel.setText(jLabel.getText()+jl_text[i]);
}
}
} else {
if (jLabel.getText().matches("[\\u4e00-\\u9fa5]+.*")){ //正则表达式检测汉字
jLabel.setText("");
jLabel.setText(jLabel.getText()+str_lst[str_lst.length-2].charAt(5));
}else {
jLabel.setText(jLabel.getText()+str_lst[str_lst.length-2].charAt(5));
}
}
}
}
final static JLabel jLabel = new JLabel(""); //将文本框定义为一个全局变量
public static void main(String[] args) throws InterruptedException { //主函数
final JFrame frame = new JFrame("计算器"); // 设置界面标题
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // 设置窗口退出模式
frame.setLayout(new GridBagLayout()); //设置界面布局方式,类似HTML5的表格布局方式,可以随意设置如何合并单元格等
addComponent(frame,jLabel,0,0,4,1); // 此行代码的意思是,这个Label 在第一行,占四列,前面的两个0是起始位置,后面的4和1表示占几列几行
jLabel.setFont(new Font("宋体",Font.BOLD,20)); //设置字体
JButton button;
int num = 0;
String[] text = {"(",")","C","<——","7","8","9","/","4","5","6","*","1","2","3","-","0",".","=","+"};
for (int i = 1;i<6;i++){//一共20个按钮 遍历20次 因为有规律所以用循环添加
for (int j =0;j<4;j++){
button = new JButton(text[num]);
button.setFont(new Font("宋体",Font.BOLD,20));
button.addMouseListener(new MyMouseListener()); // 绑定按钮事件
addComponent(frame,button,j,i,1,1); //设置按钮位置
num++;
}
}
frame.setSize(300,500); //设置窗口大小
frame.setVisible(true);//设置窗口是否可见
}