Welcome to Chapter 3 of our journey! In this chapter, we will embark on building our very first interactive application: a Simple Calculator. This project, while seemingly basic, is fundamental for grasping core programming concepts such as user input handling, conditional logic, method creation, and basic arithmetic operations. It lays a crucial foundation for more complex applications by demonstrating how to interact with users and process data.
This step is vital because it introduces the practical application of the Java environment we set up in previous chapters. You’ll move beyond “Hello, World!” to create a program that takes user input, performs calculations, and provides output. We’ll focus on building robust code, incorporating error handling from the outset, and ensuring our application is stable and predictable.
By the end of this chapter, you will have a fully functional command-line calculator that can perform addition, subtraction, multiplication, and division, complete with input validation and graceful error handling. You’ll also learn how to write unit tests for your logic, a critical practice for production-ready software.
Planning & Design
For our Simple Calculator, the architecture will be straightforward, adhering to the Single Responsibility Principle. We’ll separate the core calculation logic from the user interaction logic.
Component Architecture
CalculatorClass: This class will encapsulate all the arithmetic operations (add, subtract, multiply, divide). It will be a utility class with static methods, making it stateless and reusable. This separation ensures that our calculation logic can be tested independently of user input.MainClass: This will be our entry point. It will handle user input, parse operations, call theCalculatormethods, and display results. It will also manage the main application loop.
File Structure
We will maintain a standard Maven project structure, which you should have initialized in Chapter 2.
.
├── pom.xml
└── src
├── main
│ └── java
│ └── com
│ └── example
│ └── calculator
│ ├── Calculator.java <-- New file for arithmetic logic
│ └── Main.java <-- Main application entry point
└── test
└── java
└── com
└── example
└── calculator
└── CalculatorTest.java <-- New file for unit tests
Input Handling Strategy
We’ll use Java’s java.util.Scanner class to read user input from the console. This class is suitable for simple command-line applications. We’ll implement a loop to allow multiple calculations and an exit condition.
Step-by-Step Implementation
Let’s begin by setting up our project files and then incrementally building the calculator’s functionality.
a) Setup/Configuration
Assuming you have a Maven project from Chapter 2, let’s add the necessary files.
First, ensure your pom.xml includes JUnit 5 for testing. If you don’t have it, add the following to your <dependencies> section:
<!-- pom.xml -->
<dependencies>
<!-- Existing dependencies (if any) -->
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version> <!-- Use the latest stable version -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version> <!-- Use the latest stable version -->
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version> <!-- Ensure this supports Java 25 -->
<configuration>
<release>25</release> <!-- Target Java 25 -->
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.3</version> <!-- Use latest stable for JUnit 5 support -->
</plugin>
</plugins>
</build>
Why these dependencies?
junit-jupiter-api: Provides the core annotations and interfaces for writing JUnit 5 tests.junit-jupiter-engine: This is the test engine that discovers and runs tests written with the JUnit Jupiter API.<scope>test</scope>: This tells Maven that these dependencies are only needed for compiling and running tests, not for the main application runtime.maven-compiler-plugin: Ensures our project compiles with the specified Java version (Java 25). Thereleasetag is preferred oversourceandtargetfor modern Java versions.maven-surefire-plugin: This plugin is responsible for running the unit tests during the build lifecycle.
Now, create the following empty files:
src/main/java/com/example/calculator/Calculator.javasrc/main/java/com/example/calculator/Main.javasrc/test/java/com/example/calculator/CalculatorTest.java
b) Core Implementation
Step 1: Implement Basic Arithmetic Logic in Calculator.java
First, let’s create the Calculator class with static methods for our four basic operations. We’ll also add basic logging using System.out.println for now, which we will enhance later.
src/main/java/com/example/calculator/Calculator.java
package com.example.calculator;
/**
* Utility class for performing basic arithmetic operations.
* Provides static methods for addition, subtraction, multiplication, and division.
* Includes basic error handling for division by zero.
*/
public class Calculator {
/**
* Performs addition of two numbers.
* @param num1 The first number.
* @param num2 The second number.
* @return The sum of num1 and num2.
*/
public static double add(double num1, double num2) {
System.out.println("LOG: Performing addition: " + num1 + " + " + num2);
return num1 + num2;
}
/**
* Performs subtraction of two numbers.
* @param num1 The first number (minuend).
* @param num2 The second number (subtrahend).
* @return The difference of num1 and num2.
*/
public static double subtract(double num1, double num2) {
System.out.println("LOG: Performing subtraction: " + num1 + " - " + num2);
return num1 - num2;
}
/**
* Performs multiplication of two numbers.
* @param num1 The first number.
* @param num2 The second number.
* @return The product of num1 and num2.
*/
public static double multiply(double num1, double num2) {
System.out.println("LOG: Performing multiplication: " + num1 + " * " + num2);
return num1 * num2;
}
/**
* Performs division of two numbers.
* @param num1 The first number (dividend).
* @param num2 The second number (divisor).
* @return The quotient of num1 and num2.
* @throws ArithmeticException if num2 is zero.
*/
public static double divide(double num1, double num2) {
System.out.println("LOG: Attempting division: " + num1 + " / " + num2);
if (num2 == 0) {
System.err.println("ERROR: Division by zero attempted."); // Log error to standard error
throw new ArithmeticException("Cannot divide by zero.");
}
return num1 / num2;
}
}
Explanation:
package com.example.calculator;: Defines the package for our class, following standard Java naming conventions.public class Calculator { ... }: Declares a public class namedCalculator.public static double add(double num1, double num2): Defines a public static method namedaddthat takes twodoublearguments and returns their sum as adouble. Usingdoubleallows for floating-point calculations, making the calculator more versatile.System.out.println("LOG: ...");: Simple logging statement. In a real production application, we’d use a dedicated logging framework like SLF4J with Logback or Log4j2. We’ll explore this in a later section.divide(double num1, double num2): This method includes crucial error handling.if (num2 == 0): Checks if the divisor is zero.System.err.println("ERROR: Division by zero attempted.");: Logs an error message to the standard error stream (stderr), which is typically used for error output, distinguishing it from regular program output.throw new ArithmeticException("Cannot divide by zero.");: Throws a checkedArithmeticException. This forces any calling code to either handle this exception or declare that it throws it, ensuring that division by zero is explicitly addressed.
Step 2: Implement the Main Application Logic in Main.java
Now, let’s create the Main class to handle user interaction, input parsing, and calling our Calculator methods.
src/main/java/com/example/calculator/Main.java
package com.example.calculator;
import java.util.InputMismatchException;
import java.util.Scanner;
/**
* Main application class for the Simple Calculator.
* Handles user input, parses operations, and displays results.
*/
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Welcome to the Simple Calculator!");
System.out.println("Enter 'exit' at any time to quit.");
while (true) {
System.out.print("\nEnter first number: ");
String input1 = scanner.nextLine();
if (input1.equalsIgnoreCase("exit")) {
System.out.println("LOG: User requested exit.");
break;
}
double num1;
try {
num1 = Double.parseDouble(input1);
} catch (NumberFormatException e) {
System.err.println("ERROR: Invalid input for first number. Please enter a numeric value.");
System.out.println("LOG: Input parsing failed for: " + input1);
continue; // Skip to next iteration of the loop
}
System.out.print("Enter operator (+, -, *, /): ");
String operator = scanner.nextLine();
if (operator.equalsIgnoreCase("exit")) {
System.out.println("LOG: User requested exit.");
break;
}
// Basic operator validation
if (!operator.matches("[+\\-*/]")) {
System.err.println("ERROR: Invalid operator. Please use +, -, *, or /.");
System.out.println("LOG: Invalid operator entered: " + operator);
continue;
}
System.out.print("Enter second number: ");
String input2 = scanner.nextLine();
if (input2.equalsIgnoreCase("exit")) {
System.out.println("LOG: User requested exit.");
break;
}
double num2;
try {
num2 = Double.parseDouble(input2);
} catch (NumberFormatException e) {
System.err.println("ERROR: Invalid input for second number. Please enter a numeric value.");
System.out.println("LOG: Input parsing failed for: " + input2);
continue; // Skip to next iteration of the loop
}
double result = 0;
boolean calculationSuccessful = true;
try {
switch (operator) {
case "+":
result = Calculator.add(num1, num2);
break;
case "-":
result = Calculator.subtract(num1, num2);
break;
case "*":
result = Calculator.multiply(num1, num2);
break;
case "/":
result = Calculator.divide(num1, num2);
break;
default:
// This case should ideally not be reached due to earlier validation,
// but included for defensive programming.
System.err.println("ERROR: Unexpected operator encountered: " + operator);
calculationSuccessful = false;
break;
}
} catch (ArithmeticException e) {
System.err.println("ERROR: Calculation failed: " + e.getMessage());
System.out.println("LOG: ArithmeticException caught during calculation: " + e.getMessage());
calculationSuccessful = false;
}
if (calculationSuccessful) {
System.out.println("Result: " + num1 + " " + operator + " " + num2 + " = " + result);
System.out.println("LOG: Calculation successful: " + num1 + operator + num2 + "=" + result);
}
}
scanner.close(); // Close the scanner to release resources
System.out.println("Thank you for using the Simple Calculator. Goodbye!");
System.out.println("LOG: Application terminated.");
}
}
Explanation:
import java.util.InputMismatchException;andimport java.util.Scanner;: Imports necessary classes for input handling.InputMismatchExceptionis technically not directly used here because we parseStringinput todouble, handlingNumberFormatExceptioninstead.Scanneris used to read input.Scanner scanner = new Scanner(System.in);: Creates aScannerobject to read input from the standard input stream (console).while (true) { ... }: An infinite loop that keeps the calculator running until the user explicitly types “exit”.scanner.nextLine();: Reads an entire line of input from the user.input1.equalsIgnoreCase("exit"): Checks if the user wants to quit, case-insensitively.try { num1 = Double.parseDouble(input1); } catch (NumberFormatException e) { ... }: This is critical for input validation.Double.parseDouble()attempts to convert the input string to adouble.- If the input is not a valid number (e.g., “abc”), a
NumberFormatExceptionis thrown. - The
catchblock catches this exception, prints an error message tostderr, logs the failure, andcontinues the loop to ask for input again, preventing the program from crashing.
if (!operator.matches("[+\\-*/]")) { ... }: Basic validation for the operator using a regular expression.[+\\-*/]matches any single character that is+,-,*, or/. The backslash\escapes the-because inside[]it has special meaning (range).switch (operator) { ... }: Uses aswitchstatement to perform the correct operation based on the user’s input. Each case calls the corresponding static method from ourCalculatorclass.try { ... } catch (ArithmeticException e) { ... }: Thistry-catchblock handles theArithmeticExceptionthat ourCalculator.divide()method throws if division by zero occurs. It prints a user-friendly error and logs the details.scanner.close();: It’s crucial to close theScannerwhen it’s no longer needed to release system resources. This is done after thewhileloop exits.
c) Testing This Component
We’ll implement unit tests for the Calculator class to ensure its arithmetic logic is correct and robust. This is a best practice for any production-ready code.
src/test/java/com/example/calculator/CalculatorTest.java
package com.example.calculator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the Calculator class.
* Ensures that all arithmetic operations function correctly,
* including edge cases like division by zero.
*/
public class CalculatorTest {
@Test
@DisplayName("Test addition of two positive numbers")
void testAddPositiveNumbers() {
// Given
double num1 = 5.0;
double num2 = 3.0;
// When
double result = Calculator.add(num1, num2);
// Then
assertEquals(8.0, result, "5.0 + 3.0 should be 8.0");
}
@Test
@DisplayName("Test addition with negative numbers")
void testAddNegativeNumbers() {
assertEquals(-2.0, Calculator.add(-5.0, 3.0), "-5.0 + 3.0 should be -2.0");
assertEquals(-8.0, Calculator.add(-5.0, -3.0), "-5.0 + -3.0 should be -8.0");
}
@Test
@DisplayName("Test subtraction of two positive numbers")
void testSubtractPositiveNumbers() {
assertEquals(2.0, Calculator.subtract(5.0, 3.0), "5.0 - 3.0 should be 2.0");
}
@Test
@DisplayName("Test subtraction with negative numbers")
void testSubtractNegativeNumbers() {
assertEquals(-8.0, Calculator.subtract(-5.0, 3.0), "-5.0 - 3.0 should be -8.0");
assertEquals(-2.0, Calculator.subtract(-5.0, -3.0), "-5.0 - -3.0 should be -2.0");
}
@Test
@DisplayName("Test multiplication of two positive numbers")
void testMultiplyPositiveNumbers() {
assertEquals(15.0, Calculator.multiply(5.0, 3.0), "5.0 * 3.0 should be 15.0");
}
@Test
@DisplayName("Test multiplication with zero")
void testMultiplyByZero() {
assertEquals(0.0, Calculator.multiply(5.0, 0.0), "5.0 * 0.0 should be 0.0");
assertEquals(0.0, Calculator.multiply(0.0, 3.0), "0.0 * 3.0 should be 0.0");
}
@Test
@DisplayName("Test division of two positive numbers")
void testDividePositiveNumbers() {
assertEquals(2.0, Calculator.divide(6.0, 3.0), "6.0 / 3.0 should be 2.0");
}
@Test
@DisplayName("Test division by one")
void testDivideByOne() {
assertEquals(7.0, Calculator.divide(7.0, 1.0), "7.0 / 1.0 should be 7.0");
}
@Test
@DisplayName("Test division resulting in decimal")
void testDivideDecimalResult() {
assertEquals(2.5, Calculator.divide(5.0, 2.0), "5.0 / 2.0 should be 2.5");
}
@Test
@DisplayName("Test division by zero should throw ArithmeticException")
void testDivideByZeroThrowsException() {
// Assert that an ArithmeticException is thrown when dividing by zero
ArithmeticException exception = assertThrows(ArithmeticException.class, () -> {
Calculator.divide(10.0, 0.0);
}, "Division by zero should throw ArithmeticException");
assertEquals("Cannot divide by zero.", exception.getMessage());
}
}
Explanation:
import org.junit.jupiter.api.Test;andimport static org.junit.jupiter.api.Assertions.*;: Imports JUnit 5 annotations and static assertion methods.@Test: Marks a method as a test method.@DisplayName("..."): Provides a more readable name for the test in test reports.assertEquals(expected, actual, message): An assertion method that checks if two values are equal. If not, the test fails, and the message is displayed.assertThrows(expectedType, executable, message): This is crucial for testing exception handling. It asserts that executing the provided lambda expression (() -> { Calculator.divide(10.0, 0.0); }) throws an exception of theexpectedType(ArithmeticExceptionin this case). It also returns the caught exception, allowing us to assert its message.
How to test what was just built:
Compile and Run Unit Tests: Open your terminal in the project’s root directory (where
pom.xmlis located) and run:mvn clean installThis command will compile your code, run the unit tests, and package your application. You should see output indicating that all tests passed.
Run the Main Application: After
mvn clean installcompletes, you can run the application:mvn exec:java -Dexec.mainClass="com.example.calculator.Main"Alternatively, if you’re using an IDE like IntelliJ IDEA or Eclipse, you can simply right-click
src/main/java/com/example/calculator/Main.javaand select “Run ‘Main.main()’”.
Expected Behavior:
- When running
mvn clean install, all 11 unit tests inCalculatorTest.javashould pass successfully. - When running
Main.java, the program should prompt you for numbers and an operator. - It should correctly perform addition, subtraction, multiplication, and division.
- If you enter non-numeric input for numbers, it should print an error and ask again.
- If you enter an invalid operator, it should print an error and ask again.
- If you attempt to divide by zero, it should print an
ERROR: Calculation failed: Cannot divide by zero.message and not crash. - Typing
exitat any prompt should gracefully terminate the application.
Debugging Tips:
- Check Console Output: Pay close attention to
System.out.println(for regular output and our temporary logs) andSystem.err.println(for error messages). - Stack Traces: If the application crashes, carefully read the stack trace in the console. It tells you exactly which line of code caused the error.
- IDE Debugger: Use your IDE’s debugger. Set breakpoints at the start of
mainmethod, inside thewhileloop, and within thetry-catchblocks. Step through the code line by line to observe variable values and execution flow. - Unit Tests for Isolation: If a specific calculation is failing, write a dedicated unit test for that scenario to isolate the problem in the
Calculatorclass, separate from input handling.
Production Considerations
While our calculator is simple, it’s never too early to consider production readiness.
Error Handling
- Robust Input Validation: We’ve handled
NumberFormatExceptionand basic operator validation. For more complex inputs, consider custom parsers or dedicated libraries. - Specific Exceptions: Throwing
ArithmeticExceptionfor division by zero is good. For other domain-specific errors, consider creating custom exception types (e.g.,InvalidOperationException). - User Feedback: Ensure error messages are clear, concise, and guide the user on how to correct their input. Avoid exposing internal technical details.
Performance Optimization
For a simple command-line calculator, performance is rarely an issue. However, for larger Java applications:
- JIT Compiler: Java’s Just-In-Time (JIT) compiler optimizes frequently executed code paths at runtime. For our simple arithmetic, this happens automatically.
- Primitive Types: Using
doublefor numbers is efficient. Avoid unnecessary object creation if primitives suffice. - Resource Management: Always close resources like
Scannerto prevent resource leaks. We’ve done this withscanner.close().
Security Considerations
For this basic application, security concerns are minimal. However, in a real-world context:
- Input Sanitization: While not strictly necessary for numeric input, for string inputs, always sanitize user input to prevent injection attacks (e.g., SQL injection, XSS).
- Least Privilege: The application should run with the minimum necessary permissions. A console application typically inherits the user’s permissions.
- Code Signing: For distributed applications (e.g., applets, rich internet applications), sign your code with a trusted certificate to assure users of its authenticity and integrity. (As per Oracle’s deployment best practices).
Logging and Monitoring
Our current System.out.println and System.err.println are sufficient for a basic console app, but inadequate for production.
Enhancement: Introduce SLF4J and Logback
Let’s upgrade our logging to use SLF4J (Simple Logging Facade for Java) as an abstraction layer, with Logback as the concrete implementation. This is a standard and highly recommended practice.
Add Dependencies to
pom.xml:<!-- pom.xml --> <dependencies> <!-- Existing dependencies (JUnit, etc.) --> <!-- SLF4J API --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.12</version> <!-- Use the latest stable version for SLF4J 2.x --> </dependency> <!-- Logback Classic (implementation for SLF4J) --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.14</version> <!-- Use the latest stable version --> </dependency> </dependencies>Why SLF4J/Logback?
- Abstraction: SLF4J allows you to switch logging implementations (Logback, Log4j2, java.util.logging) without changing your code.
- Performance: Logback is known for its speed and efficiency.
- Configuration: Highly configurable (output formats, appenders, log levels) via XML or Groovy files, making it easy to manage logs in different environments (console, file, network).
- Structured Logging: Supports structured logging, which is excellent for machine parsing and integration with log analysis tools.
Update
Calculator.javato use SLF4J:src/main/java/com/example/calculator/Calculator.javapackage com.example.calculator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class for performing basic arithmetic operations. * Provides static methods for addition, subtraction, multiplication, and division. * Includes basic error handling for division by zero. */ public class Calculator { private static final Logger logger = LoggerFactory.getLogger(Calculator.class); /** * Performs addition of two numbers. * @param num1 The first number. * @param num2 The second number. * @return The sum of num1 and num2. */ public static double add(double num1, double num2) { logger.debug("Performing addition: {} + {}", num1, num2); // Use parameterized logging return num1 + num2; } /** * Performs subtraction of two numbers. * @param num1 The first number (minuend). * @param num2 The second number (subtrahend). * @return The difference of num1 and num2. */ public static double subtract(double num1, double num2) { logger.debug("Performing subtraction: {} - {}", num1, num2); return num1 - num2; } /** * Performs multiplication of two numbers. * @param num1 The first number. * @param num2 The second number. * @return The product of num1 and num2. */ public static double multiply(double num1, double num2) { logger.debug("Performing multiplication: {} * {}", num1, num2); return num1 * num2; } /** * Performs division of two numbers. * @param num1 The first number (dividend). * @param num2 The second number (divisor). * @return The quotient of num1 and num2. * @throws ArithmeticException if num2 is zero. */ public static double divide(double num1, double num2) { logger.debug("Attempting division: {} / {}", num1, num2); if (num2 == 0) { logger.error("Division by zero attempted for {} / {}.", num1, num2); // Log error using logger throw new ArithmeticException("Cannot divide by zero."); } return num1 / num2; } }Update
Main.javato use SLF4J:src/main/java/com/example/calculator/Main.javapackage com.example.calculator; import java.util.InputMismatchException; // Keep for clarity, though not directly used now import java.util.Scanner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Main application class for the Simple Calculator. * Handles user input, parses operations, and displays results. */ public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { Scanner scanner = new Scanner(System.in); logger.info("Application started. Welcome message displayed."); System.out.println("Welcome to the Simple Calculator!"); System.out.println("Enter 'exit' at any time to quit."); while (true) { System.out.print("\nEnter first number: "); String input1 = scanner.nextLine(); if (input1.equalsIgnoreCase("exit")) { logger.info("User requested exit."); break; } double num1; try { num1 = Double.parseDouble(input1); logger.debug("Parsed first number: {}", num1); } catch (NumberFormatException e) { System.err.println("ERROR: Invalid input for first number. Please enter a numeric value."); logger.warn("Input parsing failed for first number: '{}'. Error: {}", input1, e.getMessage()); continue; } System.out.print("Enter operator (+, -, *, /): "); String operator = scanner.nextLine(); if (operator.equalsIgnoreCase("exit")) { logger.info("User requested exit."); break; } if (!operator.matches("[+\\-*/]")) { System.err.println("ERROR: Invalid operator. Please use +, -, *, or /."); logger.warn("Invalid operator entered: '{}'", operator); continue; } logger.debug("Parsed operator: {}", operator); System.out.print("Enter second number: "); String input2 = scanner.nextLine(); if (input2.equalsIgnoreCase("exit")) { logger.info("User requested exit."); break; } double num2; try { num2 = Double.parseDouble(input2); logger.debug("Parsed second number: {}", num2); } catch (NumberFormatException e) { System.err.println("ERROR: Invalid input for second number. Please enter a numeric value."); logger.warn("Input parsing failed for second number: '{}'. Error: {}", input2, e.getMessage()); continue; } double result = 0; boolean calculationSuccessful = true; try { switch (operator) { case "+": result = Calculator.add(num1, num2); break; case "-": result = Calculator.subtract(num1, num2); break; case "*": result = Calculator.multiply(num1, num2); break; case "/": result = Calculator.divide(num1, num2); break; default: System.err.println("ERROR: Unexpected operator encountered: " + operator); logger.error("Unexpected operator in switch statement: '{}'", operator); calculationSuccessful = false; break; } } catch (ArithmeticException e) { System.err.println("ERROR: Calculation failed: " + e.getMessage()); logger.error("ArithmeticException caught during calculation ({} {} {}): {}", num1, operator, num2, e.getMessage(), e); // Log exception with stack trace calculationSuccessful = false; } if (calculationSuccessful) { System.out.println("Result: " + num1 + " " + operator + " " + num2 + " = " + result); logger.info("Calculation successful: {} {} {} = {}", num1, operator, num2, result); } } scanner.close(); logger.info("Scanner closed. Application terminating."); System.out.println("Thank you for using the Simple Calculator. Goodbye!"); } }Notice the
logger.error("...", e)in thecatchblock. Passing the exception objecteto the logger will automatically include its stack trace in the log output, which is invaluable for debugging in production.Create
logback.xmlfor configuration: Create a new filesrc/main/resources/logback.xml. This file configures how Logback behaves.src/main/resources/logback.xml<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- Root logger configuration --> <root level="INFO"> <appender-ref ref="STDOUT" /> </root> <!-- Specific logger for our calculator package to show DEBUG level --> <logger name="com.example.calculator" level="DEBUG" additivity="false"> <appender-ref ref="STDOUT" /> </logger> </configuration>Explanation of
logback.xml:<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">: Defines an appender namedSTDOUTthat writes logs to the console.<encoder>: Configures the format of the log messages.%d{yyyy-MM-dd HH:mm:ss.SSS}: Timestamp.[%thread]: Name of the current thread.%-5level: Log level (e.g., INFO, DEBUG, ERROR), left-aligned.%logger{36}: Name of the logger (typically the class name), truncated to 36 characters.%msg%n: The log message and a new line.
<root level="INFO">: Sets the default logging level for all loggers toINFO. This means messages withDEBUGlevel from external libraries might not be shown unless explicitly configured.<logger name="com.example.calculator" level="DEBUG" additivity="false">: This is a specific logger for our application’s package.level="DEBUG": Overrides the root level toDEBUGfor classes withincom.example.calculator, allowing us to see ourdebuglogs.additivity="false": Prevents logs from this logger from being passed up to the root logger, avoiding duplicate console output.
Now, when you run your application, you’ll see structured log messages alongside your application’s output, providing much better insight into its behavior.
Code Review Checkpoint
At this point, you have a solid foundation for your Simple Calculator:
Calculator.java: Contains the core arithmetic logic, with methods for addition, subtraction, multiplication, and robust division handling (including division by zero check). It now uses SLF4J for logging.Main.java: Serves as the application’s entry point, handling user input, parsing operations, callingCalculatormethods, and displaying results. It incorporates comprehensive input validation andtry-catchblocks for gracefully handlingNumberFormatExceptionandArithmeticException. It also uses SLF4J for logging.CalculatorTest.java: A suite of JUnit 5 unit tests that thoroughly verifies the correctness of theCalculatorclass’s arithmetic operations, including edge cases.pom.xml: Updated with JUnit 5, SLF4J, and Logback dependencies, and configured for Java 25.logback.xml: Configures Logback for structured console logging, enablingDEBUGlevel logs for our application’s package.
This setup ensures that our calculator is not just functional but also follows best practices for code organization, testing, and production-ready logging.
Common Issues & Solutions
java.lang.NumberFormatException: For input string: "abc"- Issue: Occurs when
Double.parseDouble()tries to convert a non-numeric string (like “abc”) into a number. - Debugging: The stack trace will point to the
Double.parseDouble()call. Ourtry-catchblock inMain.javaspecifically handles this by printing an error and prompting the user again, preventing a crash. - Prevention: Always use
try-catchblocks around parsing operations when dealing with user input, or use utility methods likeStringUtils.isNumeric()(from Apache Commons Lang) for pre-validation if you prefer.
- Issue: Occurs when
java.lang.ArithmeticException: Cannot divide by zero.- Issue: Happens when you attempt to divide a number by zero.
- Debugging: The stack trace will lead to the
Calculator.divide()method. OurCalculatorexplicitly throws this, andMaincatches it, displaying a user-friendly message. - Prevention: Always check if the divisor is zero before performing division, especially with user-provided input.
Incorrect Operator Handling / Unexpected Results
- Issue: The calculator either doesn’t recognize an operator or performs the wrong operation.
- Debugging:
- Check the
operator.matches("[+\\-*/]")line inMain.javato ensure the regex is correct and covers all expected operators. - Use the debugger to step through the
switchstatement inMain.javato see what valueoperatorholds and whichcaseit enters (or if it hitsdefault). - Verify your unit tests for
Calculatorare thorough; if theCalculatormethods themselves have bugs, unit tests will reveal them.
- Check the
- Prevention: Comprehensive input validation for operators and robust unit tests for the core logic are key.
Testing & Verification
Let’s do a final verification of our complete Chapter 3 work.
Rebuild the project:
mvn clean install- Verification: Ensure all unit tests pass, and the build is successful. You should see output similar to:
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
- Verification: Ensure all unit tests pass, and the build is successful. You should see output similar to:
Run the application:
mvn exec:java -Dexec.mainClass="com.example.calculator.Main"- Verification:
- Basic Operations:
5 + 3should be8.010 - 4should be6.06 * 7should be42.09 / 3should be3.05 / 2should be2.5
- Division by Zero:
10 / 0should displayERROR: Calculation failed: Cannot divide by zero.
- Invalid Numeric Input:
- Enter
hellofor a number. It should displayERROR: Invalid input for first number. Please enter a numeric value.
- Enter
- Invalid Operator Input:
- Enter
^for an operator. It should displayERROR: Invalid operator. Please use +, -, *, or /..
- Enter
- Exit Condition:
- Type
exitat any prompt. The application should gracefully terminate with “Thank you for using the Simple Calculator. Goodbye!”.
- Type
- Logging: Observe the console output. You should now see structured log messages (e.g.,
2025-12-04 10:30:00.123 [main] INFO com.example.calculator.Main - Application started...) alongside the calculator’s interactive prompts and results.
- Basic Operations:
- Verification:
Everything should work as described, demonstrating a robust, user-friendly, and well-tested command-line calculator.
Summary & Next Steps
Congratulations! In this chapter, you successfully built your first interactive Java application: a Simple Calculator. You’ve learned to:
- Design a simple application with separate concerns for logic and user interaction.
- Handle user input from the console using
java.util.Scanner. - Implement basic arithmetic operations.
- Incorporate robust error handling for invalid input and critical operations like division by zero.
- Write comprehensive unit tests using JUnit 5 to ensure code correctness.
- Integrate a professional logging framework (SLF4J with Logback) for better observability in development and production environments.
- Consider production aspects like performance, security, and structured logging.
This project, while small, covers many foundational concepts essential for any Java developer. The practices introduced here – modular design, error handling, testing, and logging – are crucial for building high-quality, maintainable, and production-ready applications.
In Chapter 4: Number Guessing Game: Random Numbers & Loop Control, we’ll build on these concepts. You’ll learn how to generate random numbers, implement more complex game logic, and utilize different types of loops and conditional statements to create an interactive guessing game. Get ready to add more dynamic behavior to your Java toolkit!