Java

Practice: Build a CLI Calculator in Java

Executive Summary

This CLI calculator in Java turns Part 1’s syntax into a working tool: a single-file, JDK-only calculator with one-shot and interactive modes, switch-expression dispatch, and boundary validation. The critical lesson sits at the edge: Double.parseDouble rejects garbage with NumberFormatException, and double division by zero returns Infinity instead of throwing, so the divisor check is yours to write. Interactive mode is a while loop over a Scanner with two explicit exits: the word quit and end-of-input. Quote the * operator on the command line, because shells expand it before Java ever sees it. Build the usage message first, the happy path second, and the failure paths before you show the tool to anyone.

The Problem Statement

You need a command-line calculator with two modes. One-shot mode takes an expression as arguments: java Calculator 3 + 4 prints 3 + 4 = 7.0 and exits. Interactive mode, started with no arguments, reads expressions line by line until the user types quit or closes the input.

Both modes must survive hostile input without a stack trace: letters instead of numbers, unknown operators, and division by zero all get a clear one-line message. In short, that is the difference between a program that works on your machine and a tool other people can use.

Mode Input On bad input
One-shot Three arguments: a op b Print message, stop
Interactive Lines read from the console Print message, keep the session alive

Requirements

  1. One-shot mode: java Calculator <a> <op> <b> prints the result. Supported operators: + – * / %.
  2. Interactive mode with no arguments: prompt, read, evaluate, repeat.
  3. Interactive mode exits on the word quit or on end-of-input.
  4. Non-numeric operands print not a number plus the offending token.
  5. Unknown operators print unknown operator plus the token.
  6. Division and modulo by zero print a clear message instead of Infinity or NaN.
  7. JDK only: one file, no libraries, no build tool.

Building the CLI Calculator in Java, Step by Step

Step 1: Write the Usage Contract First

Real tools start with their contract, so write the usage message before the logic. A text block (a triple-quoted string, covered fully in the modern Java article) keeps it readable:

static final String USAGE = """
        usage:
          java Calculator <a> <op> <b>   one-shot, e.g. java Calculator 3 + 4
          java Calculator                 interactive mode, type 'quit' to exit
        """;

Step 2: Route the Two Modes in main

In other words, main is a router, nothing more. Three arguments means one-shot, zero means interactive, and anything else gets the contract:

public static void main(String[] args) {
    if (args.length == 3) {
        runOneShot(args);
    } else if (args.length == 0) {
        runInteractive();
    } else {
        System.out.println(USAGE);
    }
}

Step 3: Parse at the Boundary

User input arrives as strings, so convert it once, at the edge, and fail with a message instead of an exception dump. Double.parseDouble is the conversion; NumberFormatException is the failure you catch, per the Double class docs:

static double parse(String token) {
    return Double.parseDouble(token);   // throws NumberFormatException if not a number
}

static void runOneShot(String[] args) {
    try {
        double a = parse(args[0]);
        double b = parse(args[2]);
        double result = apply(a, args[1], b);
        System.out.println(a + " " + args[1] + " " + b + " = " + result);
    } catch (NumberFormatException e) {
        System.out.println("not a number: " + e.getMessage());
    } catch (IllegalArgumentException e) {
        System.out.println(e.getMessage());
    }
}

Step 4: Dispatch with a switch Expression

All the arithmetic lives in one method with one switch expression. Also, the arrow form gives you no fall-through, and default catches every operator you did not declare:

static double apply(double a, String op, double b) {
    return switch (op) {
        case "+" -> a + b;
        case "-" -> a - b;
        case "*" -> a * b;
        case "/" -> {
            if (b == 0) throw new IllegalArgumentException("division by zero");
            yield a / b;
        }
        case "%" -> {
            if (b == 0) throw new IllegalArgumentException("modulo by zero");
            yield a % b;
        }
        default -> throw new IllegalArgumentException("unknown operator: " + op);
    };
}

Step 5: The Silent Trap, Division by Zero

This step is the reason the build uses doubles, so see the trap clearly. Wrong code first:

// WRONG: no exception, just a wrong-looking answer
double bad = 3.0 / 0.0;      // Infinity
double worse = 0.0 / 0.0;    // NaN
// RIGHT: check the divisor before dividing
if (b == 0) throw new IllegalArgumentException("division by zero");

The delta matters: integer division by zero throws ArithmeticException, but double division follows IEEE 754 and returns Infinity or NaN without a sound. Therefore, with doubles the check is yours to write, and the calculator refuses both / and % with zero on the right side.

Step 6: Interactive Mode with a Scanner Loop

Interactive mode is one while loop with two exits: quit and end-of-input. The Scanner class reads lines, split(“\\s+”) tokenizes them, and every failure loops instead of crashing:

static void runInteractive() {
    var scanner = new java.util.Scanner(System.in);
    System.out.println("interactive mode: expressions like 3 + 4, or 'quit'");
    while (true) {
        System.out.print("> ");
        if (!scanner.hasNextLine()) break;        // end of input (Ctrl+D)
        String line = scanner.nextLine().strip();
        if (line.equalsIgnoreCase("quit")) break;
        String[] parts = line.split("\\s+");
        if (parts.length != 3) {
            System.out.println("expected: <number> <operator> <number>");
            continue;
        }
        try {
            System.out.println(apply(parse(parts[0]), parts[1], parse(parts[2])));
        } catch (NumberFormatException e) {
            System.out.println("not a number: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
    System.out.println("bye");
}

Notice what the loop never does: it never assumes the next line is valid. So, every pass earns its way through validation, which is the exact posture production request handlers need.

The Complete Program

Assemble the pieces into one file named Calculator.java, compile with javac, and run:

public class Calculator {

    static final String USAGE = """
            usage:
              java Calculator <a> <op> <b>   one-shot, e.g. java Calculator 3 + 4
              java Calculator                 interactive mode, type 'quit' to exit
            """;

    public static void main(String[] args) {
        if (args.length == 3) {
            runOneShot(args);
        } else if (args.length == 0) {
            runInteractive();
        } else {
            System.out.println(USAGE);
        }
    }

    static void runOneShot(String[] args) {
        try {
            double a = parse(args[0]);
            double b = parse(args[2]);
            double result = apply(a, args[1], b);
            System.out.println(a + " " + args[1] + " " + b + " = " + result);
        } catch (NumberFormatException e) {
            System.out.println("not a number: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }

    static void runInteractive() {
        var scanner = new java.util.Scanner(System.in);
        System.out.println("interactive mode: expressions like 3 + 4, or 'quit'");
        while (true) {
            System.out.print("> ");
            if (!scanner.hasNextLine()) break;
            String line = scanner.nextLine().strip();
            if (line.equalsIgnoreCase("quit")) break;
            String[] parts = line.split("\\s+");
            if (parts.length != 3) {
                System.out.println("expected: <number> <operator> <number>");
                continue;
            }
            try {
                System.out.println(apply(parse(parts[0]), parts[1], parse(parts[2])));
            } catch (NumberFormatException e) {
                System.out.println("not a number: " + e.getMessage());
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
        System.out.println("bye");
    }

    static double parse(String token) {
        return Double.parseDouble(token);
    }

    static double apply(double a, String op, double b) {
        return switch (op) {
            case "+" -> a + b;
            case "-" -> a - b;
            case "*" -> a * b;
            case "/" -> {
                if (b == 0) throw new IllegalArgumentException("division by zero");
                yield a / b;
            }
            case "%" -> {
                if (b == 0) throw new IllegalArgumentException("modulo by zero");
                yield a % b;
            }
            default -> throw new IllegalArgumentException("unknown operator: " + op);
        };
    }
}

A session should look exactly like this:

$ javac Calculator.java
$ java Calculator 3 + 4
3 + 4 = 7.0
$ java Calculator 10 / 0
division by zero
$ java Calculator 5 x 2
unknown operator: x
$ java Calculator
interactive mode: expressions like 3 + 4, or 'quit'
> 2 * 8
16.0
> 9 % 4
1.0
> quit
bye

Extensions to Try Yourself

  • Add pow and sqrt using the Math class: case “pow” -> Math.pow(a, b) fits the existing switch with two lines.
  • Keep a session history in a StringBuilder and print it when the user quits; you will feel the difference between String and StringBuilder from the previous article.
  • Let ans work as an operand so users can chain calculations: 2 * 8 then ans + 1.
  • Add an int mode with a –int flag and watch two behaviors change: division truncates, and division by zero now throws ArithmeticException instead of returning Infinity.
  • Stretch goal: support full expressions like 1 + 2 * 3 with precedence. Fair warning, that is a parsing problem, not a switch problem, and it earns you deep respect for compiler authors.

How Real Systems Do This

Everything about this build mirrors production intake code. A REST endpoint receives strings, converts them once at the edge, rejects garbage with a clear message, and never lets a raw stack trace reach the user. The calculator’s apply method is the same shape a request handler takes: validate inputs, dispatch on a known set of operations, and define what happens for every unknown case.

Similarly, the JDK’s own tools model the contract-first habit. Run java with bad flags and you get a usage message, not a stack trace. In my experience, the difference between a service that on-calls well and one that pages you at night is rarely the happy path; it is how the edges behave, and this build put you face to face with three edges in ninety lines.

Real command-line tools eventually graduate to argument-parsing libraries, and Java has excellent ones. Hand-rolled parsing, like the one you just wrote, is still worth knowing: when a library hides a flag from you, you will read its source the same way you wrote yours.

Decision Framework for a CLI Calculator in Java

Use these questions whenever you build a small tool, in the order given.

  1. Will a human explore interactively, or will a script call this repeatedly? Script-first means arguments; human-first means a loop, and this build shows both.
  2. What number type is honest here? Measurements take double, counts take int or long, and money takes long minor units or BigDecimal, as the types article argued.
  3. How should invalid input end the run? A one-shot tool prints a message and stops; an interactive tool prints a message and keeps the session alive.
  4. Is the operator set fixed and small? A switch expression wins. Growing or configurable operations need data-driven dispatch, which enums make clean in Part 2.
  5. Does this need to become a real product CLI? Then adopt a parsing library with help text and typed flags; hand-rolled parsing is for learning and small scripts.

When NOT to Use This

  • Do not grow this into a general expression engine with precedence and parentheses. That is a parser and a grammar, and the professional answer at that point is a library or a course in parsing, not a longer switch.
  • Do not ship hand-rolled argument parsing in a team-facing production tool. Help text, type conversion, and flag conflicts are solved problems in CLI libraries; use one, and keep the hand-rolled version as your mental model.
  • Do not add operators before the failure paths work. Still, the extensions are only worth attempting once division by zero, bad numbers, and unknown operators all behave.

Common Mistakes

  • Running java Calculator 3 * 4 unquoted. Linux and macOS shells expand * into filenames before Java sees it, so the program receives the wrong arguments. Quote it: java Calculator 3 “*” 4.
  • Expecting ArithmeticException from 3.0 / 0. Doubles follow IEEE 754 and return Infinity; only integer division by zero throws.
  • Comparing the operator with ==. Arguments come from the runtime, not the literal pool, so op == “+” fails against equal-looking strings. Use equals or a switch, which compares content.
  • Reading args before checking args.length. Every index access is bounds-checked, and the exception is not a message a user can use.
  • Letting NumberFormatException escape to the console. A stack trace is a confession, not an error message; catch it at the boundary and translate it.
  • Testing only the happy path. If you never typed letters, an unknown operator, or division by zero, you have not tested this program; you have demoed it.

Key Takeaways

  • Write the usage contract before the logic; tools are conversations, and usage is the greeting.
  • Parse once, at the boundary, and translate exceptions into messages the user can act on.
  • Switch expressions dispatch cleanly, force a default, and keep all arithmetic in one method.
  • Double division by zero returns Infinity, not an exception; the divisor check is your job.
  • Interactive loops need two exits: the user’s quit word and end-of-input.
  • Quote * on the command line; shells expand it before your program runs.
  • Ninety honest lines teach more than nine hundred copied ones; type it yourself.

FAQ

How do I run a Java program with command-line arguments?

After compiling, pass arguments after the class name: java Calculator 3 + 4. In IntelliJ, use Run, Edit Configurations, and fill the Program arguments field. Single-file mode works too: java Calculator.java 3 + 4.

Why does my Java calculator print Infinity?

Because double arithmetic follows IEEE 754, and division by zero produces Infinity (or NaN for zero over zero) without throwing. Check the divisor explicitly, the way Step 5 does, before dividing.

How do I handle invalid input in a Java CLI?

Convert with a parse method inside try and catch, print a short message with the offending token, and decide the outcome per mode: stop in one-shot tools, keep the loop alive in interactive ones.

Why does java Calculator 3 * 4 fail on Linux and macOS?

The shell expands * into filenames before Java starts, so your program receives a list of files instead of an operator. Quote it: java Calculator 3 “*” 4. On Windows the habit still pays off for portability.

What does Double.parseDouble do?

It converts a String to a double primitive, or throws NumberFormatException when the text is not a valid number. Integer.parseInt and Long.parseLong are the same idea for whole numbers.

Conclusion

You now own a small, honest tool: contract first, validation at the edge, dispatch in a switch, and failure paths that never show a stack trace. Those four habits are the entire difference between code that works and code people trust, and every practice build from here reinforces them.

The next practice build is the number guessing game, which adds loops with state, random numbers, and richer user feedback. After that, Part 2 begins and your calculator’s static methods grow into real objects.

Ship small tools that treat bad input as a first-class citizen. After all, users forgive missing features; they never forgive a stack trace.

Last updated on 15 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *