Calculadora MDAS Java Swing básica

Oct 28 2020

Recentemente, comecei a aprender Java e decidi fazer uma calculadora MDAS básica no Swing. Não sou completamente novo em programação, mas posso estar cometendo alguns erros comuns ou não escrevendo o código mais eficiente.

Eu queria fazer uma calculadora que pudesse pegar vários números e operações antes de encontrar a resposta usando MDAS, em vez de apenas retornar a resposta após cada operação e usá-la na próxima.

por exemplo, 2 * 3 + 4 - 5 / 5 = 9 em vez de 1

Meu código consiste em uma única classe. Não há muito código, então eu não sabia se havia um bom motivo para dividi-lo em várias classes; no entanto, nunca escrevi algo assim, sinta-se à vontade para me corrigir.

Repo com exemplo de gif e jar executável


package calculator;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.util.ArrayList;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class GUI extends JFrame {
    
    private static final long serialVersionUID = 1L;
    
    private String title = "Basic MDAS Calculator";
    private int currentNumber;
    private JLabel displayLabel = new JLabel(String.valueOf(currentNumber), JLabel.RIGHT);
    private JPanel panel = new JPanel();
    
    private boolean isClear = true;
    
    final String[] ops = new String[] {"+", "-", "x", "/"};
    
    private ArrayList<Integer> numHistory = new ArrayList<Integer>();
    private ArrayList<String> opHistory = new ArrayList<String>();
    

    public GUI() {
        setPanel();
        setFrame();
    }
    
    private void setFrame() {
        this.setTitle(title);
        this.add(panel, BorderLayout.CENTER);
        this.setBounds(10,10,300,700); 
        this.setResizable(false);
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    
    private void setPanel() {
        panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        panel.setLayout(new GridLayout(0, 1));
        
        displayLabel.setFont(new Font("Verdana", Font.PLAIN, 42));
        panel.add(displayLabel);
        panel.add(Box.createRigidArea(new Dimension(0, 0)));
        
        createButtons();
    }
    
    private void createButtons() {
        
        // 0-9
        for (int i = 0; i < 10; i++) {
            final int num = i;
            
            JButton button = new JButton( new AbstractAction(String.valueOf(i)) { 
                private static final long serialVersionUID = 1L;

                @Override
                public void actionPerformed(ActionEvent e) {
                    
                    // If somebody presses "=" and then types a number, start a new equation instead
                    // of adding that number to the end like usual
                    if (!isClear) {
                        currentNumber = 0;
                        isClear = true;
                    }
                    
                    if (currentNumber == 0) {
                        currentNumber = num;
                    } else {
                        currentNumber = currentNumber * 10 + num;
                    }
                    
                    displayLabel.setText(String.valueOf(currentNumber));
                }
            });
            
            panel.add(button);
        }
        
        // +, -, x, /
        for (String op : ops) {
            
            JButton button = new JButton( new AbstractAction(op) { 
                private static final long serialVersionUID = 1L;

                @Override
                public void actionPerformed(ActionEvent e) {
                    numHistory.add(currentNumber);
                    currentNumber = 0;
                    
                    opHistory.add(op);
                    displayLabel.setText(op);
                }
            });
            
            panel.add(button);
        }
        
        // =
        JButton button = new JButton( new AbstractAction("=") { 
            private static final long serialVersionUID = 1L;
            private int i;

            @Override
            public void actionPerformed(ActionEvent e) {    
                // Display result
                numHistory.add(currentNumber);
                
                while (opHistory.size() > 0) {
                    
                    if (opHistory.contains("x")) {
                        i = opHistory.indexOf("x");
                        numHistory.set(i, numHistory.get(i) * numHistory.get(i+1));
                    } else if (opHistory.contains("/")) {
                        i = opHistory.indexOf("/");
                        numHistory.set(i, numHistory.get(i) / numHistory.get(i+1));
                    } else if (opHistory.contains("+")) {
                        i = opHistory.indexOf("+");
                        numHistory.set(i, numHistory.get(i) + numHistory.get(i+1));
                    } else if (opHistory.contains("-")) {
                        i = opHistory.indexOf("-");
                        numHistory.set(i, numHistory.get(i) - numHistory.get(i+1));
                    }
                    
                    opHistory.remove(i);
                    numHistory.remove(i+1);
                }
                
                displayLabel.setText(String.valueOf(numHistory.get(0)));
                
                currentNumber = numHistory.get(0);
                numHistory.clear();
                
                if (isClear) {
                    isClear = false;
                }
            }
        });
        
        panel.add(button);
    }

    public static void main(String[] args) {
        new GUI();
    }

}

Eu apreciaria quaisquer dicas.

Respostas

5 Bobby Oct 28 2020 at 23:41
package calculator;

Os nomes dos pacotes devem associar o software ao autor, como com.github.razemoon.basicmdasjavacaluclator.


public class GUI extends JFrame {

Para as convenções do Java nming, você normalmente usaria UpperCamelCase, e usaria minúsculas mesmo para acrônimos, como "Gui", "HtmlWidgetToolkit" ou "HtmlCssParser".


private static final long serialVersionUID = 1L;

Você só precisa desse campo se for altamente provável que a classe seja serializada ... neste caso, provavelmente não.


final String[] ops = new String[] {"+", "-", "x", "/"};

Por que isso package-private?

Além disso, os finalarrays não são finalcomo você pensa, os valores individuais ainda podem ser alterados. Você provavelmente quer um Enum .... na verdade, você quer uma interface, mas neste exemplo, um Enum provavelmente seria suficiente.


    private ArrayList<Integer> numHistory = new ArrayList<Integer>();
    private ArrayList<String> opHistory = new ArrayList<String>();

Sempre tente usar a interface comum mais baixa para declarações, neste caso List.


    private void setFrame() {
        this.setTitle(title);
        this.add(panel, BorderLayout.CENTER);
        this.setBounds(10,10,300,700); 
        this.setResizable(false);
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

Por que você está usando thisaqui, mas em nenhum outro lugar?


        this.setResizable(false);

Por quê? Sua moldura é perfeitamente redimensionável, tanto quanto posso ver. Ao defini-lo como não redimensionável, você apenas garante que seu aplicativo se torne inutilizável sob diferentes LaFs e tamanhos de fonte.


for (int i = 0; i < 10; i++) {

Eu sou um defensor muito persistente de que você só tem permissão para usar nomes de variáveis ​​de uma única letra se estiver lidando com dimensões.

for (int number = 0; number <= 9; number++) {
// Or
for (int digit = 0; digit <= 9; digit++) {

final int num = i;

Não encurte os nomes das variáveis ​​apenas porque você pode, a diminuição da quantidade de digitação não compensa a diminuição da legibilidade.


Em relação à criação de botões, gosto de criar métodos e classes auxiliares que tornam o código mais fácil de ler, neste caso eu escolheria lambdas, assim:

private JButton createButton(String text, Runnable action) {
    return new JButton(new AbstractButton(text) {
        @Override
        public void actionPerformed(ActionEvent e) {
            action.run();
        }
    })
}

// In createButtons:

panel.add(createButton(Integer.toString(number), () -> {
    // Code for the number button goes here.
}));

Outra alternativa seria criar um private class NumberActionque aceite um número em seu construtor e execute a ação associada. Isso também permitiria que você se livrasse da redeclaração final.


private int i;

Esse é um nome de variável muito ruim.


    public GUI() {
        setPanel();
        setFrame();
    }
    
    private void setFrame() {
        this.setTitle(title);
        this.add(panel, BorderLayout.CENTER);
        this.setBounds(10,10,300,700); 
        this.setResizable(false);
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

// ...

    public static void main(String[] args) {
        new GUI();
    }

Seria melhor dividir as responsabilidades aqui. O quadro em si é apenas responsável por fazer seu próprio layout funcionar, enquanto o método principal deve ser responsável por fazer o quadro ser exibido.

    public GUI() {
        setPanel();
        setFrame();
    }
    
    private void setFrame() {
        this.setTitle(title);
        this.add(panel, BorderLayout.CENTER);
        this.setBounds(10,10,300,700); 
        this.setResizable(false);
    }

// ...

    public static void main(String[] args) {
        GUI gui = new GUI();
        gui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        gui.setVisible(true);
    }

Sua lógica não parece conter nenhum tipo de tratamento de erro, acredito que pressionar um botão do operador duas vezes consecutivas deve gerar um erro.


Talvez uma abordagem melhor seja imprimir a expressão inteira na tela conforme inserida e, em seguida, aplicar o algoritmo Shunting Yard para processar essa expressão.


Sua lógica não faz decimais, nem lida com estouros. Ao mudar sua lógica, BigDecimalvocê pode lidar com ambos facilmente. Observe que você deve criar BigDecimals com um MathContextcomportamento adequado para ter precisão e comportamento adequados.


Se você quiser ler uma implementação já existente, posso recomendar exp4j para uma biblioteca de expressão matemática usando floats, EvalEx para um usando BigDecimal, e meu próprio projeto jMathPaper para uma calculadora que ostenta diferentes GUIs (em relação à abstração).

4 Doi9t Oct 29 2020 at 04:33

Você pode usar a declaração de array.

Quando você deseja uma matriz com valores predefinidos que podem ser constantes, você pode declarar a matriz anonimamente.

  final String[] ops = {"+", "-", "x", "/"};

Use Enums para as operações.

Em vez de ter uma matriz de operação, sugiro que você crie um Enum.

public enum Operators {
   PLUS("+"), MINUS("-"), MUL("x"), DIV("/");
   private final String operator;
   Operators(String operator) {
      this.operator = operator;
   }
   public String getOperator() {
      return operator;
   }
}

Isso lhe dará mais vantagem do que a matriz, pois você poderá remover a duplicação.

//[...]
for (Operators op : Operators.values()) {
   JButton button = new JButton( new AbstractAction(op.getOperator()) {
      private static final long serialVersionUID = 1L;

      @Override
      public void actionPerformed(ActionEvent e) {
         numHistory.add(currentNumber);
         currentNumber = 0;

         opHistory.add(op);
         displayLabel.setText(String.valueOf(op.getOperator()));
      }
   });
   panel.add(button);
}
//[...]
//[...]
if (opHistory.contains(Operators.MUL)) {
   i = opHistory.indexOf(Operators.MUL);
   numHistory.set(i, numHistory.get(i) * numHistory.get(i + 1));
} else if (opHistory.contains(Operators.DIV)) {
   i = opHistory.indexOf(Operators.DIV);
   numHistory.set(i, numHistory.get(i) / numHistory.get(i + 1));
} else if (opHistory.contains(Operators.PLUS)) {
   i = opHistory.indexOf(Operators.PLUS);
   numHistory.set(i, numHistory.get(i) + numHistory.get(i + 1));
} else if (opHistory.contains(Operators.MINUS)) {
   i = opHistory.indexOf(Operators.MINUS);
   numHistory.set(i, numHistory.get(i) - numHistory.get(i + 1));
}
//[...]

Além disso, em minha opinião, isso tornará o código mais fácil de trabalhar e refatorar.

Ao dividir, sempre verifique divisorantes de fazer a divisão.

Ao dividir por zero, há um java.lang.ArithmeticExceptionlance de java; Eu sugiro que você adicione um cheque :)

Use o em Queuevez de Listpara manter o histórico.

Ao usar o, Listvocê deve usar um índice, o Queuepara remover o primeiro item ( java.util.Queue#poll); a única desvantagem, você precisará refatorar o código real para remover o indexOf.

private Queue<String> opHistory = new ArrayDeque<>();

Ao fazer isso, você tornará o código mais curto.

while (opHistory.size() > 0) {
   Operators currentOperator = opHistory.poll();

   switch (currentOperator) { //Java 14+ Switch, you can use if or the older version of the switch.
       case MUL -> numHistory.set(i, numHistory.get(i) * numHistory.get(i+1));
       case DIV -> numHistory.set(i, numHistory.get(i) / numHistory.get(i+1));
       case PLUS -> numHistory.set(i, numHistory.get(i) + numHistory.get(i+1));
       case MINUS -> numHistory.set(i, numHistory.get(i) - numHistory.get(i+1));
   }

   numHistory.remove(i + 1);
}

Extraia a expressão para variáveis ​​quando usada várias vezes.

Em seu código, você pode extrair as expressões semelhantes em variáveis; isso tornará o código mais curto e fácil de ler.

final Integer first = numHistory.get(i);
final Integer second = numHistory.get(i + 1);
switch (currentOperator) {
    case MUL -> numHistory.set(i, first * second);
    case DIV -> numHistory.set(i, first / second);
    case PLUS -> numHistory.set(i, first + second);
    case MINUS -> numHistory.set(i, first - second);
}