MDAS Java Swing Calculator cơ bản

Oct 28 2020

Gần đây tôi đã bắt đầu học Java và quyết định tạo một máy tính MDAS cơ bản trong Swing. Tôi không phải là người mới hoàn toàn về lập trình nhưng có thể tôi đang mắc một số lỗi phổ biến hoặc không viết được mã hiệu quả nhất.

Tôi muốn tạo ra một máy tính có thể lấy nhiều số và phép toán trước khi tìm ra câu trả lời bằng MDAS, thay vì chỉ trả lại câu trả lời sau mỗi phép toán và sử dụng nó cho lần sau.

ví dụ: 2 * 3 + 4 - 5 / 5 = 9 thay vì 1

Mã của tôi bao gồm một lớp duy nhất. Không có nhiều mã nên tôi không biết liệu có lý do chính đáng để chia nó thành nhiều lớp hay không, tuy nhiên tôi chưa bao giờ viết một cái gì đó như thế này vì vậy xin vui lòng sửa cho tôi.

Repo với ví dụ gif và runnable jar


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();
    }

}

Tôi sẽ đánh giá cao bất kỳ lời khuyên.

Trả lời

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

Tên gói phải liên kết phần mềm với tác giả, như com.github.razemoon.basicmdasjavacaluclator.


public class GUI extends JFrame {

Đối với các quy ước Java nming, bạn thường sử dụng UpperCamelCase và sử dụng chữ thường ngay cả cho các từ viết tắt, như "Gui", "HtmlWidgetToolkit" hoặc "HtmlCssParser".


private static final long serialVersionUID = 1L;

Bạn chỉ cần trường này nếu có nhiều khả năng lớp đó sẽ được tuần tự hóa ... trong trường hợp này, rất có thể là không.


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

Tại sao thế này package-private?

Ngoài ra, finalcác mảng không như finalbạn nghĩ, các giá trị riêng lẻ vẫn có thể được thay đổi. Rất có thể bạn muốn một Enum .... thực ra, bạn muốn một giao diện, nhưng trong ví dụ này, một Enum rất có thể sẽ hoạt động tốt.


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

Luôn cố gắng sử dụng giao diện chung thấp nhất cho các khai báo, trong trường hợp này 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);
    }

Tại sao bạn đang sử dụng thisở đây mà không ở đâu khác?


        this.setResizable(false);

Tại sao? Khung của bạn có thể thay đổi kích thước hoàn hảo theo như tôi thấy. Bằng cách đặt nó không thể thay đổi kích thước, bạn chỉ đảm bảo rằng ứng dụng của bạn không thể sử dụng được dưới các LaF và kích thước phông chữ khác nhau.


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

Tôi là một người ủng hộ rất kiên trì cho việc bạn chỉ được phép sử dụng các tên biến gồm một chữ cái nếu bạn xử lý thứ nguyên.

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

final int num = i;

Đừng rút ngắn tên biến chỉ vì bạn có thể, số lượng gõ giảm đi không đáng để giảm khả năng đọc.


Về việc tạo các nút, tôi muốn tạo các phương thức và lớp trợ giúp làm cho mã dễ đọc hơn, trong trường hợp này, tôi chọn lambdas, như thế này:

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.
}));

Một cách thay thế khác sẽ là tạo một hàm private class NumberActionchấp nhận một số trong hàm tạo của nó và thực hiện hành động liên quan. Điều đó cũng sẽ cho phép bạn loại bỏ việc khai báo lại lần cuối.


private int i;

Đó; một tên biến rất xấu.


    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();
    }

Sẽ tốt hơn nếu phân chia trách nhiệm ở đây. Bản thân khung chỉ chịu trách nhiệm về việc sắp xếp bố cục của chính nó, trong khi phương thức chính sẽ chịu trách nhiệm hiển thị khung.

    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);
    }

Logic của bạn dường như không chứa bất kỳ loại xử lý lỗi nào, tôi tin rằng việc nhấn một nút điều hành hai lần liên tiếp sẽ dẫn đến lỗi.


Có thể một cách tiếp cận tốt hơn sẽ là in toàn bộ biểu thức ra màn hình như đã nhập và sau đó áp dụng Thuật toán Shunting Yard để xử lý biểu thức đó.


Logic của bạn không thực hiện các số thập phân, cũng không xử lý các lỗi tràn một cách duyên dáng. Bằng cách thay đổi cách sử dụng logic BigDecimalcủa bạn, bạn có thể xử lý cả hai một cách dễ dàng. Lưu ý rằng bạn phải tạo BigDecimals với một thích hợp MathContextđể có độ chính xác và hành vi phù hợp.


Nếu bạn muốn đọc một triển khai đã tồn tại, tôi có thể giới thiệu exp4j cho thư viện biểu thức toán học sử dụng float, EvalEx cho một thư viện sử dụng BigDecimalvà dự án jMathPaper của riêng tôi cho một máy tính có các GUI khác nhau (liên quan đến trừu tượng).

4 Doi9t Oct 29 2020 at 04:33

Bạn có thể sử dụng khai báo mảng.

Khi bạn muốn một mảng có các giá trị được xác định trước có thể không đổi, bạn có thể khai báo mảng một cách ẩn danh.

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

Sử dụng Enums cho các hoạt động.

Thay vì có một mảng hoạt động, tôi khuyên bạn nên tạo một Enum thay thế.

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

Điều này sẽ mang lại cho bạn nhiều lợi thế hơn so với mảng, vì bạn sẽ có thể loại bỏ sự trùng lặp.

//[...]
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));
}
//[...]

Ngoài ra, theo ý kiến ​​của tôi, điều này sẽ làm cho mã dễ làm việc và cấu trúc lại.

Khi chia, luôn luôn kiểm tra divisortrước khi thực hiện phép chia.

Khi chia cho 0, có một java.lang.ArithmeticExceptionném bởi java; Tôi đề nghị bạn thêm một séc :)

Sử dụng Queuethay vì Listđể giữ lịch sử.

Bằng cách sử dụng, Listbạn phải sử dụng một chỉ mục, Queueđể loại bỏ mục đầu tiên ( java.util.Queue#poll); hạn chế duy nhất, bạn sẽ cần phải cấu trúc lại mã thực tế để loại bỏ indexOf.

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

Làm như vậy, bạn sẽ làm cho mã ngắn hơn.

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);
}

Trích xuất biểu thức thành các biến khi được sử dụng nhiều lần.

Trong mã của bạn, bạn có thể trích xuất các biểu thức tương tự thành các biến; điều này sẽ làm cho mã ngắn hơn và dễ đọc hơn.

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);
}