16.【Java】阶段实战:命令行计算器

摘要: 本文通过一个完整的命令行计算器项目,将Java第二阶段基础知识点(变量、运算符、条件判断、循环、方法、数组、字符串、JDK 21新特性)融会贯通。项目支持六种基本运算、输入校验、历史记录功能,采用模块化设计,使用Record封装结果,展示了如何将零散知识整合为实用程序。适合Java初学者通过实战巩固基础语法,为面向对象编程打下坚实基础。

关键词: Java实战项目, 命令行计算器, 基础语法综合, JDK 21新特性, 模块化设计


恭喜你,我们已经完成了第二阶段"基础篇"的全部知识点。回头看这十篇文章,我们走过了变量与数据类型、常量与字面量、运算符、条件判断、循环、方法、数组、字符串,还有 JDK 21 的那些让代码更简洁的"语法糖"。

知识点学了不少,但如果只是单个地学,它们就像一串散落的珠子。今天,我们要用一根线把这些珠子全部串起来——这根线就是一个完整的项目

我们要写一个命令行版的交互式计算器。它不会像 Windows 计算器那样有图形界面,但它能做的事情一点也不少:支持多种运算、能持续使用、能记录历史、还能处理错误输入。


1. 我们要做什么?—— 需求分析

让我们先明确这个计算器要具备哪些功能:

核心功能

  1. 支持加、减、乘、除、取模、幂运算(六种运算)。
  2. 用户通过菜单选择运算类型,然后输入两个数字,程序输出结果。
  3. 除法时,如果除数为 0,给出友好的错误提示,而不是程序崩溃。
  4. 一次运算完成后,询问用户是否继续,选择"是"则回到菜单,选择"否"则退出。

进阶功能(体现我们的知识积累):
5. 历史记录:记录每次运算的表达式和结果(比如 3 + 5 = 8),用户可以选择查看历史记录。
6. 输入校验:如果用户输入的不是有效数字,提示重新输入,而不是让程序崩溃。

交互流程(用户视角):

===== 命令行计算器 =====
1. 加法
2. 减法
3. 乘法
4. 除法
5. 取模
6. 幂运算
7. 查看历史记录
0. 退出
请选择操作 (0-7):1
请输入第一个数:10
请输入第二个数:20
10.0 + 20.0 = 30.0
是否继续?(y/n):y
...

这个需求正好涵盖了第二阶段的所有知识点:

  • Scanner 处理输入
  • do-while 实现"至少执行一次"的主循环
  • switch 表达式做菜单分支
  • whiledo-while 做输入校验
  • 用方法封装各个功能模块
  • 用数组存储历史记录
  • 用字符串格式化美化输出
  • 用 JDK 21 的 var、文本块、Record 让代码更简洁

准备好了吗?我们开始动手。


2. 设计思路 —— 先想清楚再动笔

在写代码之前,我们先设计一下程序的结构。

① 数据存储
  • 历史记录:我们存储最多 100 条,用 String[] 数组(这是数组的最直接用法)。
  • 记录条数:用一个 int 变量 historyCount 来追踪已存储的条数。
② 方法划分
方法名职责
main程序入口,控制主循环和菜单
showMenu显示菜单选项
getOperation从用户获取有效的运算类型(0-7)
getNumber从用户获取一个有效的数字(含校验)
calculate根据运算类型执行计算,返回结果字符串
addToHistory将一条运算记录添加到历史数组中
showHistory打印所有历史记录
printResult格式化并打印运算结果

这种划分遵循了单一职责原则:每个方法只做一件事。

③ 程序流程
1. 显示欢迎信息
2. 初始化历史记录数组
3. 进入主循环:
   a. 显示菜单
   b. 获取用户选择(0-7)
   c. 如果选择 0,退出
   d. 如果选择 7,显示历史记录,回到 a
   e. 其他:获取两个数字,执行运算,打印结果,存入历史
   f. 询问是否继续,如果"n"则退出,否则回到 a

3. 编写代码 —— 一步一步来

现在开始写代码。我会把完整的代码分成几个部分,逐段讲解。

第1部分:类的骨架和常量定义
package com.example.demo;

import java.util.Scanner;

/**
 * 命令行计算器 —— 第二阶段综合实战项目
 * 功能:加、减、乘、除、取模、幂运算,历史记录,输入校验
 */
public class Calculator {

    // 历史记录数组:最多存储 100 条
    private static final int MAX_HISTORY = 100;
    private static final String[] history = new String[MAX_HISTORY];
    private static int historyCount = 0;  // 当前已存储的记录条数

    // 创建一个静态的 Scanner,供所有方法共用
    private static final Scanner scanner = new Scanner(System.in);
  • MAX_HISTORY 是常量,用 static final 修饰,全大写。
  • history 数组和 historyCount 是静态成员,因为 main 和所有方法都是静态的,它们之间共享数据需要静态变量。
  • scanner 也是静态的,这样所有方法都可以使用同一个 Scanner,不需要反复创建。
第2部分:主方法 —— 控制整体流程
public static void main(String[] args) {
    System.out.println("""
            ╔═══════════════════════════════════╗
            ║     欢迎使用命令行计算器 v1.0     ║
            ║   支持:+ - * / % ^               ║
            ╚═══════════════════════════════════╝
            """);

    boolean running = true;

    while (running) {
        showMenu();                      // 显示菜单
        int choice = getChoice();        // 获取用户选择

        if (choice == 0) {
            System.out.println("感谢使用计算器,再见!");
            running = false;
            break;
        }

        if (choice == 7) {
            showHistory();               // 查看历史
            continue;                    // 回到循环开头
        }

        // 处理运算(choice 为 1-6)
        double num1 = getNumber("请输入第一个数:");
        double num2 = getNumber("请输入第二个数:");

        // 执行计算(用 Record 来封装结果,优雅又简洁)
        var result = calculate(choice, num1, num2);
        printResult(result);

        // 询问是否继续
        System.out.print("是否继续?(y/n):");
        String answer = scanner.next().trim().toLowerCase();
        if (!answer.equals("y") && !answer.equals("yes")) {
            System.out.println("感谢使用计算器,再见!");
            running = false;
        }
    }

    scanner.close();
}

这里用到了:

  • 文本块:用 """ 画了一个简单的欢迎边框。
  • varvar resultcalculate 的返回类型推断更简洁。
  • while 循环:控制程序持续运行。
  • continue:查看历史后直接回到菜单,不执行后续的运算逻辑。
第3部分:显示菜单
/**
 * 显示菜单选项
 */
private static void showMenu() {
    System.out.println("\n========== 计算器菜单 ==========");
    System.out.println("1. 加法 (+)");
    System.out.println("2. 减法 (-)");
    System.out.println("3. 乘法 (*)");
    System.out.println("4. 除法 (/)");
    System.out.println("5. 取模 (%)");
    System.out.println("6. 幂运算 (^)");
    System.out.println("7. 查看历史记录");
    System.out.println("0. 退出");
    System.out.print("请选择操作 (0-7):");
}

很直观,用 println 输出菜单项。

第4部分:获取用户选择(含校验)
/**
 * 获取用户选择的菜单项,并校验输入是否合法
 * @return 0-7 之间的整数
 */
private static int getChoice() {
    while (true) {
        if (scanner.hasNextInt()) {
            int choice = scanner.nextInt();
            if (choice >= 0 && choice <= 7) {
                return choice;
            }
        } else {
            scanner.next();  // 消费掉非整数输入
        }
        System.out.print("输入无效,请输入 0-7 之间的数字:");
    }
}

这里用到了 scanner.hasNextInt() 来判断用户输入的是不是整数。如果不是,就调用 scanner.next() 消费掉这个无效输入,然后继续提示。这比直接 nextInt() 更健壮,不会因为输入字母而崩溃。

第5部分:获取数字(含校验)
/**
 * 获取用户输入的一个有效数字
 * @param prompt 提示信息
 * @return 用户输入的数字(double)
 */
private static double getNumber(String prompt) {
    while (true) {
        System.out.print(prompt);
        if (scanner.hasNextDouble()) {
            return scanner.nextDouble();
        } else {
            scanner.next();  // 消费掉非数字输入
            System.out.println("输入无效,请输入一个有效的数字!");
        }
    }
}

同样用 hasNextDouble() 做了校验,确保用户输入的是有效的数字。

第6部分:执行计算(核心逻辑)

我们用 Record 来封装计算结果,这样返回的数据既清晰又简洁。先定义这个 Record

/**
 * 用于封装计算结果的 Record
 * @param expression 运算表达式(如 "3 + 5")
 * @param result     运算结果
 * @param error      错误信息(null 表示没有错误)
 */
public record CalculationResult(String expression, double result, String error) {
    // 判断计算是否成功
    public boolean isSuccess() {
        return error == null;
    }
}

然后写 calculate 方法:

/**
 * 根据运算类型执行计算
 * @param choice 运算类型(1-6)
 * @param a      第一个数
 * @param b      第二个数
 * @return 包含表达式、结果和错误信息的 CalculationResult
 */
private static CalculationResult calculate(int choice, double a, double b) {
    String operator;
    double result;
    String error = null;

    switch (choice) {
        case 1 -> {
            operator = "+";
            result = a + b;
        }
        case 2 -> {
            operator = "-";
            result = a - b;
        }
        case 3 -> {
            operator = "*";
            result = a * b;
        }
        case 4 -> {
            operator = "/";
            if (Math.abs(b) < 1e-9) {
                return new CalculationResult("", 0, "错误:除数不能为 0!");
            }
            result = a / b;
        }
        case 5 -> {
            operator = "%";
            if (Math.abs(b) < 1e-9) {
                return new CalculationResult("", 0, "错误:取模运算的除数不能为 0!");
            }
            result = a % b;
        }
        case 6 -> {
            operator = "^";
            result = Math.pow(a, b);
        }
        default -> {
            return new CalculationResult("", 0, "未知的运算类型!");
        }
    }

    // 构建表达式字符串(处理整数显示更美观)
    String expr = formatNumber(a) + " " + operator + " " + formatNumber(b);
    return new CalculationResult(expr, result, null);
}
  • switch 表达式配合 ->,简洁清晰。
  • 对除法和取模做了除数校验,如果接近 0(考虑到浮点数精度),返回错误。
  • 使用 Math.pow(a, b) 进行幂运算。
  • 返回一个 CalculationResult Record,同时包含表达式和结果。

formatNumber 是一个辅助方法,让数字显示更美观(如果是整数就不显示 .0):

/**
 * 格式化数字:如果是整数则不显示小数部分
 */
private static String formatNumber(double num) {
    if (num == (long) num) {
        return String.valueOf((long) num);
    }
    return String.valueOf(num);
}
第7部分:打印结果与历史记录
/**
 * 打印计算结果,并存入历史
 */
private static void printResult(CalculationResult result) {
    if (!result.isSuccess()) {
        System.out.println("❌ " + result.error());
        return;
    }

    String output = result.expression() + " = " + formatNumber(result.result());
    System.out.println("✅ " + output);
    addToHistory(output);
}

/**
 * 将一条记录添加到历史数组中
 */
private static void addToHistory(String record) {
    if (historyCount < MAX_HISTORY) {
        history[historyCount] = record;
        historyCount++;
    } else {
        // 如果历史已满,把所有记录向前移动一位,覆盖最旧的
        System.arraycopy(history, 1, history, 0, MAX_HISTORY - 1);
        history[MAX_HISTORY - 1] = record;
        // historyCount 保持不变(仍为 MAX_HISTORY)
    }
}

/**
 * 显示所有历史记录
 */
private static void showHistory() {
    if (historyCount == 0) {
        System.out.println("📭 暂无历史记录");
        return;
    }

    System.out.println("\n========== 历史记录 ==========");
    for (int i = 0; i < historyCount; i++) {
        System.out.println((i + 1) + ". " + history[i]);
    }
    System.out.println("==============================");
}
  • addToHistory 中,如果历史已满,我们用 System.arraycopy 将数组整体左移一位,丢弃最旧的记录,在最末尾插入新记录。这是一个经典的使用数组实现"循环队列"的简化做法。
  • showHistory 遍历 historyCount 条记录并打印。

4. 完整的代码

把上面的所有部分合在一起,就是最终的完整代码:

package com.example.demo;

import java.util.Scanner;

/**
 * 命令行计算器 —— 第二阶段综合实战项目
 * 功能:加、减、乘、除、取模、幂运算,历史记录,输入校验
 * 知识点:变量、运算符、条件判断、循环、方法、数组、字符串、JDK 21 语法糖
 */
public class Calculator {

    // 历史记录数组:最多存储 100 条
    private static final int MAX_HISTORY = 100;
    private static final String[] history = new String[MAX_HISTORY];
    private static int historyCount = 0;

    private static final Scanner scanner = new Scanner(System.in);

    // ---- Record:封装计算结果 ----
    public record CalculationResult(String expression, double result, String error) {
        public boolean isSuccess() {
            return error == null;
        }
    }

    // ---- 主方法 ----
    public static void main(String[] args) {
        System.out.println("""
                ╔═══════════════════════════════════╗
                ║     欢迎使用命令行计算器 v1.0     ║
                ║   支持:+ - * / % ^               ║
                ╚═══════════════════════════════════╝
                """);

        boolean running = true;

        while (running) {
            showMenu();
            int choice = getChoice();

            if (choice == 0) {
                System.out.println("感谢使用计算器,再见!");
                running = false;
                break;
            }

            if (choice == 7) {
                showHistory();
                continue;
            }

            double num1 = getNumber("请输入第一个数:");
            double num2 = getNumber("请输入第二个数:");

            var result = calculate(choice, num1, num2);
            printResult(result);

            System.out.print("是否继续?(y/n):");
            String answer = scanner.next().trim().toLowerCase();
            if (!answer.equals("y") && !answer.equals("yes")) {
                System.out.println("感谢使用计算器,再见!");
                running = false;
            }
        }

        scanner.close();
    }

    // ---- 菜单 ----
    private static void showMenu() {
        System.out.println("\n========== 计算器菜单 ==========");
        System.out.println("1. 加法 (+)");
        System.out.println("2. 减法 (-)");
        System.out.println("3. 乘法 (*)");
        System.out.println("4. 除法 (/)");
        System.out.println("5. 取模 (%)");
        System.out.println("6. 幂运算 (^)");
        System.out.println("7. 查看历史记录");
        System.out.println("0. 退出");
        System.out.print("请选择操作 (0-7):");
    }

    // ---- 获取菜单选择 ----
    private static int getChoice() {
        while (true) {
            if (scanner.hasNextInt()) {
                int choice = scanner.nextInt();
                if (choice >= 0 && choice <= 7) {
                    return choice;
                }
            } else {
                scanner.next();
            }
            System.out.print("输入无效,请输入 0-7 之间的数字:");
        }
    }

    // ---- 获取数字 ----
    private static double getNumber(String prompt) {
        while (true) {
            System.out.print(prompt);
            if (scanner.hasNextDouble()) {
                return scanner.nextDouble();
            } else {
                scanner.next();
                System.out.println("输入无效,请输入一个有效的数字!");
            }
        }
    }

    // ---- 执行计算 ----
    private static CalculationResult calculate(int choice, double a, double b) {
        String operator;
        double result;
        String error = null;

        switch (choice) {
            case 1 -> {
                operator = "+";
                result = a + b;
            }
            case 2 -> {
                operator = "-";
                result = a - b;
            }
            case 3 -> {
                operator = "*";
                result = a * b;
            }
            case 4 -> {
                operator = "/";
                if (Math.abs(b) < 1e-9) {
                    return new CalculationResult("", 0, "错误:除数不能为 0!");
                }
                result = a / b;
            }
            case 5 -> {
                operator = "%";
                if (Math.abs(b) < 1e-9) {
                    return new CalculationResult("", 0, "错误:取模运算的除数不能为 0!");
                }
                result = a % b;
            }
            case 6 -> {
                operator = "^";
                result = Math.pow(a, b);
            }
            default -> {
                return new CalculationResult("", 0, "未知的运算类型!");
            }
        }

        String expr = formatNumber(a) + " " + operator + " " + formatNumber(b);
        return new CalculationResult(expr, result, null);
    }

    // ---- 格式化数字 ----
    private static String formatNumber(double num) {
        if (num == (long) num) {
            return String.valueOf((long) num);
        }
        return String.valueOf(num);
    }

    // ---- 打印结果 ----
    private static void printResult(CalculationResult result) {
        if (!result.isSuccess()) {
            System.out.println("❌ " + result.error());
            return;
        }
        String output = result.expression() + " = " + formatNumber(result.result());
        System.out.println("✅ " + output);
        addToHistory(output);
    }

    // ---- 添加历史记录 ----
    private static void addToHistory(String record) {
        if (historyCount < MAX_HISTORY) {
            history[historyCount] = record;
            historyCount++;
        } else {
            System.arraycopy(history, 1, history, 0, MAX_HISTORY - 1);
            history[MAX_HISTORY - 1] = record;
        }
    }

    // ---- 显示历史记录 ----
    private static void showHistory() {
        if (historyCount == 0) {
            System.out.println("📭 暂无历史记录");
            return;
        }
        System.out.println("\n========== 历史记录 ==========");
        for (int i = 0; i < historyCount; i++) {
            System.out.println((i + 1) + ". " + history[i]);
        }
        System.out.println("==============================");
    }
}

5. 运行测试

运行这个程序,你会看到欢迎界面和菜单。试试这些操作:

  1. 选择 1(加法),输入 1020,看到输出 10 + 20 = 30
  2. 选择 4(除法),输入 100,看到友好的错误提示,而不是程序崩溃。
  3. 选择 6(幂运算),输入 210,看到 2 ^ 10 = 1024
  4. 做几个运算后,选择 7(查看历史记录),看到所有记录按顺序列出。
  5. 输入 无效字符(比如菜单输入 abc),看到提示重新输入。
  6. 选择 0(退出),程序优雅结束。

6. 知识点回顾:我们用了什么?

这个不到 150 行的程序,用到了第二阶段的所有核心知识:

知识点体现在
变量historyCountchoicenum1num2
常量MAX_HISTORY
数据类型intdoubleStringboolean
数组String[] history
运算符算术、比较、逻辑
条件判断if-elseswitch 表达式
循环whiledo-while(隐藏在输入校验中)
方法模块化拆分,各司其职
方法重载无(但体现了方法设计)
Scanner输入处理 + 校验
字符串格式化、拼接、formatted()
文本块欢迎界面
Record封装计算结果
varvar result = calculate(...)
异常处理思路用返回值而不是抛异常来传递错误

7. 如何进一步扩展?(给你的挑战)

你可以在这个基础上,继续完善计算器:

  1. 支持更多运算:比如平方根、三角函数、对数等(用 Math 类)。
  2. 支持括号表达式:像真正的计算器一样,输入 (3+5)*2 自动计算(这需要用栈和逆波兰表达式,我们以后学)。
  3. 持久化历史:把历史记录保存到文件中,下次启动时加载(我们学 I/O 后会做)。
  4. 用集合替代数组ArrayList 比数组更灵活,我们第四阶段会学。
  5. 增加颜色输出:用 ANSI 转义码让输出更炫酷。

今天的总结

今天我们用一整个项目,把第二阶段学到的所有知识融会贯通了。你写出了自己第一个"真正有用"的 Java 程序——它不是一个一次性的 Hello, World!,而是一个可以反复使用、有交互、能处理错误、能记录历史的工具。

你做到了。这个程序的复杂程度已经远远超出了"入门"的范畴。

从下一篇开始,我们将进入一个全新的阶段——第三阶段:面向对象编程。这是 Java 最核心、最精髓的部分。如果说基础语法是"招式",那面向对象就是"心法"。掌握了它,你才能写出真正优雅、可扩展的 Java 程序。


动手试试

  1. 把计算器跑起来,做至少 10 次不同的运算,然后查看历史记录,确保它正确记录了所有操作。
  2. 尝试输入一些"坏"数据:比如在菜单输入 abc,在数字输入 xyz,在继续询问时输入 n,观察程序是否都正确处理了。
  3. 扩展功能:增加一个"科学计算器"模式,支持 sincossqrt 等单参数运算(菜单增加选项)。
  4. 代码阅读:把 System.arraycopy(history, 1, history, 0, MAX_HISTORY - 1); 这一行查一下文档,理解它是怎么实现"移除最旧记录"的。

我们下个阶段见。😃

📌 获取本系列示例代码请访问 GitCode

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值