1.界面设计
使用的ConstraintLayout作为主布局容器。
关于ConstrainLayout如何使用 结果如下
设计代码如下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/output"
android:layout_width="match_parent"
android:layout_height="180dp"
android:gravity="bottom|right"
style="@style/OutputTextStyle"
android:textStyle="bold"
android:hint="0"
android:padding="10dp"
app:layout_constraintBottom_toTopOf="@id/line"
app:layout_constraintLeft_toLeftOf="parent"/>
<View
android:id="@+id/line"
android:layout_width="match_parent"
android:layout_height="3dp"
app:layout_constraintBottom_toTopOf="@id/add"
android:background="@color/black"/>
<Button
android:id="@+id/one"
style="@style/ButtonStyle"
android:layout_width="100dp"
android:layout_height="80dp"
android:text="1"
android:layout_marginLeft="5dp"
app:layout_constraintBottom_toTopOf="@id/dot"
app:layout_constraintRight_toLeftOf="@id/two"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/four"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
android:onClick="onClick" />
<Button
android:id="@+id/two"
style="@style/ButtonStyle"
android:layout_width="100dp"
android:layout_height="80dp"
android:text="2"
app:layout_constraintBottom_toTopOf="@id/zero"
app:layout_constraintLeft_toRightOf="@+id/one"
android:onClick="onClick"/>
<Button
android:id="@+id/three"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="3"
app:layout_constraintBottom_toTopOf="@id/equal"
app:layout_constraintLeft_toRightOf="@id/two"
android:onClick="onClick"
/>
<Button
android:id="@+id/del"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="C"
app:layout_constraintBottom_toTopOf="@id/nine"
app:layout_constraintLeft_toRightOf="@id/rightParenthesis"
android:onClick="onClick"
/>
<Button
android:id="@+id/four"
style="@style/ButtonStyle"
android:layout_width="100dp"
android:layout_height="80dp"
android:layout_marginLeft="5dp"
android:text="4"
app:layout_constraintBottom_toTopOf="@id/one"
app:layout_constraintStart_toStartOf="parent"
android:onClick="onClick"/>
<Button
android:id="@+id/five"
style="@style/ButtonStyle"
android:layout_width="100dp"
android:layout_height="80dp"
android:text="5"
app:layout_constraintStart_toEndOf="@id/four"
app:layout_constraintBottom_toTopOf="@id/two"
android:onClick="onClick"/>
<Button
android:id="@+id/six"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="6"
app:layout_constraintStart_toEndOf="@id/five"
app:layout_constraintBottom_toTopOf="@id/three"
android:onClick="onClick"/>
<Button
android:id="@+id/seven"
style="@style/ButtonStyle"
android:layout_width="100dp"
android:layout_height="80dp"
android:layout_marginLeft="5dp"
android:text="7"
app:layout_constraintBottom_toTopOf="@id/four"
app:layout_constraintLeft_toLeftOf="parent"
android:onClick="onClick"/>
<Button
android:id="@+id/eight"
style="@style/ButtonStyle"
android:layout_width="100dp"
android:layout_height="80dp"
android:text="8"
app:layout_constraintBottom_toTopOf="@id/five"
app:layout_constraintLeft_toRightOf="@id/seven"
android:onClick="onClick"
/>
<Button
android:id="@+id/nine"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="9"
app:layout_constraintBottom_toTopOf="@id/six"
app:layout_constraintLeft_toRightOf="@id/eight"
android:onClick="onClick"/>
<Button
android:id="@+id/dot"
style="@style/ButtonStyle"
android:layout_width="100dp"
android:layout_height="80dp"
android:layout_marginLeft="5dp"
android:text="."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:onClick="onClick"/>
<Button
android:id="@+id/leftParenthesis"
style="@style/ButtonStyle"
android:layout_width="100dp"
android:layout_height="80dp"
android:layout_marginLeft="5dp"
android:text="("
app:layout_constraintBottom_toTopOf="@id/seven"
app:layout_constraintStart_toStartOf="parent"
android:onClick="onClick"/>
<Button
android:id="@+id/rightParenthesis"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text=")"
app:layout_constraintLeft_toRightOf="@id/leftParenthesis"
app:layout_constraintBottom_toTopOf="@id/eight"
android:onClick="onClick"/>
<Button
android:id="@+id/equal"
android:layout_width="200dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="="
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/zero"
android:onClick="onClick"/>
<Button
android:id="@+id/zero"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="0"
app:layout_constraintLeft_toRightOf="@id/dot"
app:layout_constraintBottom_toBottomOf="parent"
android:onClick="onClick"/>
<Button
android:id="@+id/add"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="+"
app:layout_constraintLeft_toRightOf="@id/del"
app:layout_constraintBottom_toTopOf="@id/sub"
android:onClick="onClick"/>
<Button
android:id="@+id/sub"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="-"
app:layout_constraintBottom_toTopOf="@id/mul"
app:layout_constraintLeft_toRightOf="@id/nine"
android:onClick="onClick"/>
<Button
android:id="@+id/mul"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="*"
app:layout_constraintBottom_toTopOf="@id/div"
app:layout_constraintLeft_toRightOf="@id/six"
android:onClick="onClick"/>
<Button
android:id="@+id/div"
android:layout_width="100dp"
android:layout_height="80dp"
style="@style/ButtonStyle"
android:text="/"
app:layout_constraintBottom_toTopOf="@id/equal"
app:layout_constraintLeft_toRightOf="@id/three"
android:onClick="onClick"/>
</androidx.constraintlayout.widget.ConstraintLayout>
使用到的values文件,colors.xml和styles.xml
2.功能需求
能正确计算加、减、乘、除、包含小括号,小数,负数
3.具体实现
用到的技术点
①中缀转后缀生成后缀表达式(计算器的核心)
中最转后缀算法:
需要两个栈:符号栈和数字栈
1.符号栈为空,当前元素直接入栈;或者当前字符为"(“,直接入栈
2.遇到“)”,将符号栈里内容一一出栈,直到遇到”("
3.其他字符的情况,比较当前字符与符号栈顶字符的优先级,若当前元素的优先级大于栈顶元素的优先级,直接入栈;否则,一直出栈,直到符合当前元素的优先级大于栈顶元素优先级的要求,或者遇到“(”
如下是代码实现,包含了后缀表达式的运算
public Result SuffixesToSuffixes(String expression) throws CalException, ArithmeticException {
String resExpression = "";
Stack<Character> stack = new Stack<>();
Stack<BigDecimal> numberStack = new Stack<>();
Result result = new Result();
boolean isMinus = false; //假设不是负数
for(int i = 0 ; i < expression.length(); ){
String str1 = "";
//判断是负数
if(expression.charAt(i) == '-' && (i + 1) < expression.length() && isNumber(expression.charAt(i + 1))){
if(i == 0 || isOperator(expression.charAt(i - 1))){
isMinus = true;
i++;
continue;
}
}
//是数字组成部分
while (i < expression.length() && isNumberOrDot(expression.charAt(i))) {
str1 = str1 + expression.charAt(i++);
}
//数字内容处理
if(!str1.equals("")){
resExpression += str1;
//检查当前数字是否合法
service.checkNumber(str1);
//将String转换为对应的BigDecimal对象
BigDecimal bd = new BigDecimal(str1);
//转换成负数
if(isMinus){
BigDecimal bd1 = new BigDecimal("-1");
bd = bd.multiply(bd1);
isMinus = false;
}
//为了显式好看,处理double类型尾部是0的情况,直接转换成对应整数
if(bd.doubleValue() == bd.intValue()){
bd = new BigDecimal(bd.intValue());
}else{
bd = new BigDecimal(bd.doubleValue());
}
//将BigDecimal对象压栈
numberStack.push(bd);
//其他字符处理
}else{
//1.栈为空,当前元素直接入栈;或者当前字符为"(",直接入栈
if(stack.isEmpty() || expression.charAt(i) == '('){
stack.push(expression.charAt(i));
//2.遇到“)”,将栈里内容一一出栈,直到遇到"("
}else if(expression.charAt(i) == ')'){
while (stack.peek() != '('){
char c = stack.pop();
resExpression += c;
service.calculate(numberStack, c);
}
//“(”出栈
stack.pop();
}else{
//3.其他字符的情况,比较当前字符与栈顶字符的优先级,若当前元素的优先级大于栈顶元素的优先级,直接入栈;否则,一直出栈,
// 直到符合要求,或者遇到“(”
if(operatorPrecedent(expression.charAt(i)) > operatorPrecedent(stack.peek())){
stack.push(expression.charAt(i));
}else{
while(!stack.isEmpty() && stack.peek() != '('
&& operatorPrecedent(expression.charAt(i)) <= operatorPrecedent(stack.peek())) {
char c = stack.pop();
resExpression += c;
service.calculate(numberStack, c);
}
stack.push(expression.charAt(i));
}
}
i++;
}
}
while(!stack.isEmpty()){
char c = stack.pop();
resExpression += c ;
service.calculate(numberStack, c);
}
result.setNumberStack(numberStack);
result.setPostfixExpression(resExpression);
return result;
}
②运算符优先级比较
int operatorPrecedent(char c){
if(c == '*' || c == '/')
return 2;
if(c == '+' || c == '-')
return 1;
return -1;
}
③判断是数字
boolean isNumber(char c){
if(c == '0' || c == '1' || c == '2' || c=='3' || c=='4'
|| c=='5' || c=='6' || c=='7' || c=='8' || c=='9'){
return true;
}
return false;
}
④判断是数字的组成部分(比数字多了一个小数点)
static boolean isNumberOrDot(char c){
if(isNumber(c) || c == '.'){
return true;
}
return false;
}
⑤判断是合法小数
void checkNumber(String str) throws CalException {
int dotCount = 0;
for(int i = 0 ; i < str.length(); i++){
if(str.charAt(i) == '.')
dotCount++;
}
if(dotCount > 1){
throw new CalException("表达式输入有误!");
}
}
⑥判断是操作数符号
boolean isOperator(char ch){
if(ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch =='(')
return true;
return false;
}
⑦负数正确转换成数字
在中缀转后缀表达式的算法中有包含:
判断是负数,则isMinus标志设为true,后期数字转换的过程中判断,如果标志为true则给转换出来的数字乘-1,转换成负数。
//判断是负数
if(expression.charAt(i) == '-' && (i + 1) < expression.length() && isNumber(expression.charAt(i + 1))){
if(i == 0 || isOperator(expression.charAt(i - 1))){
isMinus = true;
i++;
continue;
}
}
⑧大数运算
数字使用BigDecimal
4.可能出现的闪退情况
除0异常
表达式错误(包含乱写表达式,数字输入有问题,符号或者数字多了少了)
大数除法需要指定精度
5.处理闪退情况
自定义异常类,在处理表达式的过程中抛异常,在调用该方法的地方处理异常
6.考虑是否使用架构mvc、mvp、mvvm等…
没考虑
7.可以优化的地方
关于小数点的问题,可以在用户按下小数点,自动补即按 ‘.’ 显示“0.”,并在按’.‘的逻辑中不让用户对一个数字可以按出两个及以上的’.',这样也就不需要检查数字的合法性了。
运行结果截图
1.表达式"-1*2+(-1+2)"
2.表达式"1.1+23(4.4*5+6)*7"
代码及简要讲解
我写了四个类和一个Android主界面类来实现此功能:
①CalException:自定义的异常类
②CalService:处理计算过程和检查数字合法性
③ExpressionTool:完成中缀转后缀以及用到的一些判断方法
④Result:自定义返回的数据类型,属性包含:一个栈(用来保存数字)和一个String(用来保存中缀转后缀,生成的后缀表达式)
⑤MainActivity.java:主界面逻辑代码
其中,最主要的是ExpressionTool类,它完成了中缀转后缀的整个过程.
代码如下:
①CalException.java 这是一个自定义的异常类
public class CalException extends Exception{
static final long serialVersionUID = -3381275169931242948L;
public CalException(String msg){
super(msg);
}
}
②CalService.java 这个类用于计算
package com.example.mycalculate;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Stack;
public class CalService {
//检查数字(小数)
void checkNumber(String str) throws CalException {
int dotCount = 0;
for(int i = 0 ; i < str.length(); i++){
if(str.charAt(i) == '.')
dotCount++;
}
if(dotCount > 1){
throw new CalException("表达式输入有误!");
}
}
//计算两个数的某种运算
public void calculate(Stack<BigDecimal> numberStack , char ch){
BigDecimal num2 = numberStack.pop();
BigDecimal num1 = numberStack.pop();
BigDecimal res = calculate(num1, num2, ch);
numberStack.push(res);
}
//具体计算
public BigDecimal calculate(BigDecimal num1, BigDecimal num2, char c) throws ArithmeticException{
BigDecimal res = new BigDecimal("0");
switch (c){
case '+': res = num1.add(num2);
break;
case '-': res = num1.subtract(num2);
break;
case '*': res = num1.multiply(num2);
break;
case '/':
if(num2.equals(0))
throw new ArithmeticException();
res = num1.divide(num2,16 ,RoundingMode.HALF_UP);//注意相除产生无线小数的情况,需要指明精度,以及舍入模式
break;
}
return res;
}
}
③ExpressionTool.java
主要讲一下方法SuffixesToSuffixes(String expression),这个方法中完成了中缀转后缀并计算,传入一个String类型的表达式。
首先需要两个栈,数字栈numberStack,运算符号栈stack.
表达式处理逻辑为:
package com.example.mycalculate;
import java.math.BigDecimal;
import java.util.Stack;
public class ExpressionTool {
CalService service = new CalService();
/**
* 中缀转后缀表达式
* @param expression
* @return
*/
public Result SuffixesToSuffixes(String expression) throws CalException, ArithmeticException {
String resExpression = "";
Stack<Character> stack = new Stack<>();
Stack<BigDecimal> numberStack = new Stack<>();
Result result = new Result();
boolean isMinus = false; //假设不是负数
for(int i = 0 ; i < expression.length(); ){
String str1 = "";
//判断是负数
if(expression.charAt(i) == '-' && (i + 1) < expression.length() && isNumber(expression.charAt(i + 1))){
if(i == 0 || isOperator(expression.charAt(i - 1))){
isMinus = true;
i++;
continue;
}
}
//是数字组成部分
while (i < expression.length() && isNumberOrDot(expression.charAt(i))) {
str1 = str1 + expression.charAt(i++);
}
//数字内容处理
if(!str1.equals("")){
resExpression += str1;
//检查当前数字是否合法
service.checkNumber(str1);
//将String转换为对应的BigDecimal对象
BigDecimal bd = new BigDecimal(str1);
//转换成负数
if(isMinus){
BigDecimal bd1 = new BigDecimal("-1");
bd = bd.multiply(bd1);
isMinus = false;
}
//为了显式好看,处理double类型尾部是0的情况,直接转换成对应整数
if(bd.doubleValue() == bd.intValue()){
bd = new BigDecimal(bd.intValue());
}else{
bd = new BigDecimal(bd.doubleValue());
}
//将BigDecimal对象压栈
numberStack.push(bd);
//其他字符处理
}else{
//1.栈为空,当前元素直接入栈;或者当前字符为"(",直接入栈
if(stack.isEmpty() || expression.charAt(i) == '('){
stack.push(expression.charAt(i));
//2.遇到“)”,将栈里内容一一出栈,直到遇到"("
}else if(expression.charAt(i) == ')'){
while (stack.peek() != '('){
char c = stack.pop();
resExpression += c;
service.calculate(numberStack, c);
}
//“(”出栈
stack.pop();
}else{
//3.其他字符的情况,比较当前字符与栈顶字符的优先级,若当前元素的优先级大于栈顶元素的优先级,直接入栈;否则,一直出栈,
// 直到符合要求,或者遇到“(”
if(operatorPrecedent(expression.charAt(i)) > operatorPrecedent(stack.peek())){
stack.push(expression.charAt(i));
}else{
while(!stack.isEmpty() && stack.peek() != '('
&& operatorPrecedent(expression.charAt(i)) <= operatorPrecedent(stack.peek())) {
char c = stack.pop();
resExpression += c;
service.calculate(numberStack, c);
}
stack.push(expression.charAt(i));
}
}
i++;
}
}
while(!stack.isEmpty()){
char c = stack.pop();
resExpression += c ;
service.calculate(numberStack, c);
}
result.setNumberStack(numberStack);
result.setPostfixExpression(resExpression);
return result;
}
//判断是否是操作符
static boolean isOperator(char ch){
if(ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch =='(')
return true;
return false;
}
//判断是否是数字
static boolean isNumber(char c){
if(c == '0' || c == '1' || c == '2' || c=='3' || c=='4'
|| c=='5' || c=='6' || c=='7' || c=='8' || c=='9'){
return true;
}
return false;
}
/**
* 返回操作符的优先级,“*”和“/”的优先级为2,“+”和“-”的优先级为“1”
* @param c
* @return
*/
int operatorPrecedent(char c){
if(c == '*' || c == '/')
return 2;
if(c == '+' || c == '-')
return 1;
return -1;
}
//是数字或者'.'
static boolean isNumberOrDot(char c){
if(isNumber(c) || c == '.'){
return true;
}
return false;
}
}
④Result.java 自定义的返回类型,返回一个数字栈用于存中缀转后缀中的数字和一个表达式String用于返回中缀转后缀的最终后缀表达式
import java.math.BigDecimal;
import java.util.Stack;
public class Result {
Stack<BigDecimal> numberStack = null;
String postfixExpression = "";
public Result(){
numberStack = new Stack<>();
}
public void setPostfixExpression(String postfixExpression) {
this.postfixExpression = postfixExpression;
}
public String getPostfixExpression() {
return postfixExpression;
}
public Stack<BigDecimal> getNumberStack() {
return numberStack;
}
public void setNumberStack(Stack<BigDecimal> numberStack) {
this.numberStack = numberStack;
}
}