Welcome to Chapter 5 of our Java project series! In this chapter, we’re going to build a practical Temperature Converter application. This project, while seemingly simple, introduces crucial concepts like robust user input handling, data validation, mathematical conversions, and providing a good command-line user experience.
Building this application will solidify your understanding of parsing user input, safely converting data types, implementing core business logic (the conversion formulas), and handling potential errors gracefully. These are fundamental skills applicable to almost any real-world application, regardless of its complexity or UI. We’ll leverage the latest stable Java features as of December 2025, focusing on clean code, testability, and production-readiness from the outset.
By the end of this chapter, you will have a fully functional, console-based temperature converter that can convert between Celsius, Fahrenheit, and Kelvin, complete with input validation, error handling, and a user-friendly interface.
Planning & Design
For our Temperature Converter, we’ll follow a modular design to keep our code clean and maintainable. We’ll separate concerns into distinct components:
TemperatureConverterApp(Main Application Class): Handles the main program loop, user interaction (input/output), and orchestrates calls to the conversion logic.TemperatureConverter(Core Logic Class): Contains the static methods for performing the actual temperature conversions. This class will be purely functional, taking input temperatures and units and returning converted values.ConversionUnit(Enum): Defines the supported temperature units (Celsius, Fahrenheit, Kelvin) to ensure type safety and prevent invalid unit inputs.InvalidInputException(Custom Exception): A custom exception to signal specific input errors, making error handling more explicit.
File Structure:
We’ll maintain our standard Maven project structure. Inside src/main/java/com/example/temperatureconverter, we’ll have:
src/
└── main/
└── java/
└── com/
└── example/
└── temperatureconverter/
├── TemperatureConverterApp.java
├── TemperatureConverter.java
├── ConversionUnit.java
└── InvalidInputException.java
└── test/
└── java/
└── com/
└── example/
└── temperatureconverter/
└── TemperatureConverterTest.java
Step-by-Step Implementation
We’ll build the application incrementally, testing each component as we go.
1. Setup/Configuration: Project Structure and Dependencies
First, ensure your Maven project is set up. If you’re continuing from previous chapters, you already have one. We’ll create the initial files.
a) Create ConversionUnit Enum
This enum will define our supported units and provide utility for parsing string inputs into valid units.
File: src/main/java/com/example/temperatureconverter/ConversionUnit.java
package com.example.temperatureconverter;
import java.util.Optional;
import java.util.logging.Logger;
/**
* Represents the supported temperature units for conversion.
* Provides utility methods for parsing string inputs into valid units.
*/
public enum ConversionUnit {
CELSIUS("C", "Celsius"),
FAHRENHEIT("F", "Fahrenheit"),
KELVIN("K", "Kelvin");
private static final Logger LOGGER = Logger.getLogger(ConversionUnit.class.getName());
private final String symbol;
private final String fullName;
ConversionUnit(String symbol, String fullName) {
this.symbol = symbol;
this.fullName = fullName;
}
public String getSymbol() {
return symbol;
}
public String getFullName() {
return fullName;
}
/**
* Parses a string input into a ConversionUnit.
* The input can be the symbol (e.g., "C") or the full name (e.g., "Celsius"), case-insensitive.
*
* @param input The string to parse.
* @return An Optional containing the ConversionUnit if found, otherwise empty.
*/
public static Optional<ConversionUnit> fromString(String input) {
if (input == null || input.trim().isEmpty()) {
LOGGER.fine("Received null or empty input for unit parsing.");
return Optional.empty();
}
String normalizedInput = input.trim().toUpperCase();
for (ConversionUnit unit : values()) {
if (unit.symbol.equals(normalizedInput) || unit.fullName.toUpperCase().equals(normalizedInput)) {
return Optional.of(unit);
}
}
LOGGER.warning("Could not parse '" + input + "' into a valid ConversionUnit.");
return Optional.empty();
}
/**
* Returns a string representation of all supported units for user display.
* Example: "C (Celsius), F (Fahrenheit), K (Kelvin)"
*
* @return A comma-separated string of units.
*/
public static String getSupportedUnitsString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < values().length; i++) {
ConversionUnit unit = values()[i];
sb.append(unit.getSymbol()).append(" (").append(unit.getFullName()).append(")");
if (i < values().length - 1) {
sb.append(", ");
}
}
return sb.toString();
}
}
Explanation:
- We define three enum constants:
CELSIUS,FAHRENHEIT,KELVIN. - Each constant has a
symbol(e.g., “C”) and afullName(e.g., “Celsius”) for flexibility in user input. - The
fromStringmethod is crucial for safely parsing user input into a validConversionUnit. It returns anOptionalto clearly indicate if a unit was found or not, preventingNullPointerExceptions. We also added basic logging for debugging. getSupportedUnitsStringprovides a helpful string for user prompts.java.util.logging.Loggeris used for internal diagnostics, a standard practice for production applications.
b) Create InvalidInputException Custom Exception
A custom exception allows us to catch specific application-level errors distinct from general Java exceptions.
File: src/main/java/com/example/temperatureconverter/InvalidInputException.java
package com.example.temperatureconverter;
/**
* Custom exception for invalid user input in the temperature converter application.
* This helps to distinguish specific application-level input errors from general
* programming errors or system issues.
*/
public class InvalidInputException extends Exception {
/**
* Constructs a new InvalidInputException with the specified detail message.
*
* @param message The detail message (which is saved for later retrieval by the getMessage() method).
*/
public InvalidInputException(String message) {
super(message);
}
/**
* Constructs a new InvalidInputException with the specified detail message and cause.
*
* @param message The detail message.
* @param cause The cause (which is saved for later retrieval by the getCause() method).
*/
public InvalidInputException(String message, Throwable cause) {
super(message, cause);
}
}
Explanation:
- This is a simple custom checked exception that extends
java.lang.Exception. - It provides constructors to include a message and an optional cause, which is good practice for error propagation.
2. Core Implementation: Temperature Conversion Logic
Now, let’s implement the heart of our application: the conversion formulas.
a) Create TemperatureConverter Class
This class will contain static methods for all conversion types. We’ll start with Celsius to Fahrenheit and then expand.
File: src/main/java/com/example/temperatureconverter/TemperatureConverter.java
package com.example.temperatureconverter;
import java.util.logging.Logger;
/**
* Provides static methods for converting temperatures between Celsius, Fahrenheit, and Kelvin.
* All conversion methods include basic validation to ensure temperatures are within reasonable
* absolute zero limits.
*/
public final class TemperatureConverter {
private static final Logger LOGGER = Logger.getLogger(TemperatureConverter.class.getName());
// Absolute zero constants in Celsius, Fahrenheit, and Kelvin
public static final double ABSOLUTE_ZERO_CELSIUS = -273.15;
public static final double ABSOLUTE_ZERO_FAHRENHEIT = -459.67;
public static final double ABSOLUTE_ZERO_KELVIN = 0.0;
// Private constructor to prevent instantiation, as this is a utility class
private TemperatureConverter() {
// SonarLint rule: Utility classes should not have public constructors.
// This class is purely static and should not be instantiated.
}
/**
* Converts Celsius to Fahrenheit.
* Formula: F = C * 9/5 + 32
*
* @param celsius The temperature in Celsius.
* @return The temperature in Fahrenheit.
* @throws InvalidInputException If the input temperature is below absolute zero Celsius.
*/
public static double convertCelsiusToFahrenheit(double celsius) throws InvalidInputException {
if (celsius < ABSOLUTE_ZERO_CELSIUS) {
LOGGER.warning("Attempted to convert Celsius below absolute zero: " + celsius);
throw new InvalidInputException("Temperature cannot be below absolute zero (" + ABSOLUTE_ZERO_CELSIUS + "°C)");
}
double fahrenheit = celsius * 9 / 5 + 32;
LOGGER.fine(String.format("%.2f°C converted to %.2f°F", celsius, fahrenheit));
return fahrenheit;
}
/**
* Converts Fahrenheit to Celsius.
* Formula: C = (F - 32) * 5/9
*
* @param fahrenheit The temperature in Fahrenheit.
* @return The temperature in Celsius.
* @throws InvalidInputException If the input temperature is below absolute zero Fahrenheit.
*/
public static double convertFahrenheitToCelsius(double fahrenheit) throws InvalidInputException {
if (fahrenheit < ABSOLUTE_ZERO_FAHRENHEIT) {
LOGGER.warning("Attempted to convert Fahrenheit below absolute zero: " + fahrenheit);
throw new InvalidInputException("Temperature cannot be below absolute zero (" + ABSOLUTE_ZERO_FAHRENHEIT + "°F)");
}
double celsius = (fahrenheit - 32) * 5 / 9;
LOGGER.fine(String.format("%.2f°F converted to %.2f°C", fahrenheit, celsius));
return celsius;
}
/**
* Converts Celsius to Kelvin.
* Formula: K = C + 273.15
*
* @param celsius The temperature in Celsius.
* @return The temperature in Kelvin.
* @throws InvalidInputException If the input temperature is below absolute zero Celsius.
*/
public static double convertCelsiusToKelvin(double celsius) throws InvalidInputException {
if (celsius < ABSOLUTE_ZERO_CELSIUS) {
LOGGER.warning("Attempted to convert Celsius below absolute zero: " + celsius);
throw new InvalidInputException("Temperature cannot be below absolute zero (" + ABSOLUTE_ZERO_CELSIUS + "°C)");
}
double kelvin = celsius + 273.15;
LOGGER.fine(String.format("%.2f°C converted to %.2fK", celsius, kelvin));
return kelvin;
}
/**
* Converts Kelvin to Celsius.
* Formula: C = K - 273.15
*
* @param kelvin The temperature in Kelvin.
* @return The temperature in Celsius.
* @throws InvalidInputException If the input temperature is below absolute zero Kelvin.
*/
public static double convertKelvinToCelsius(double kelvin) throws InvalidInputException {
if (kelvin < ABSOLUTE_ZERO_KELVIN) {
LOGGER.warning("Attempted to convert Kelvin below absolute zero: " + kelvin);
throw new InvalidInputException("Temperature cannot be below absolute zero (" + ABSOLUTE_ZERO_KELVIN + "K)");
}
double celsius = kelvin - 273.15;
LOGGER.fine(String.format("%.2fK converted to %.2f°C", kelvin, celsius));
return celsius;
}
/**
* Converts Fahrenheit to Kelvin.
* Achieved by converting Fahrenheit to Celsius, then Celsius to Kelvin.
*
* @param fahrenheit The temperature in Fahrenheit.
* @return The temperature in Kelvin.
* @throws InvalidInputException If the input temperature is below absolute zero Fahrenheit.
*/
public static double convertFahrenheitToKelvin(double fahrenheit) throws InvalidInputException {
// Re-use existing conversion methods for better maintainability and less error-prone code
double celsius = convertFahrenheitToCelsius(fahrenheit);
double kelvin = convertCelsiusToKelvin(celsius);
LOGGER.fine(String.format("%.2f°F converted to %.2fK", fahrenheit, kelvin));
return kelvin;
}
/**
* Converts Kelvin to Fahrenheit.
* Achieved by converting Kelvin to Celsius, then Celsius to Fahrenheit.
*
* @param kelvin The temperature in Kelvin.
* @return The temperature in Fahrenheit.
* @throws InvalidInputException If the input temperature is below absolute zero Kelvin.
*/
public static double convertKelvinToFahrenheit(double kelvin) throws InvalidInputException {
// Re-use existing conversion methods
double celsius = convertKelvinToCelsius(kelvin);
double fahrenheit = convertCelsiusToFahrenheit(celsius);
LOGGER.fine(String.format("%.2fK converted to %.2f°F", kelvin, fahrenheit));
return fahrenheit;
}
}
Explanation:
TemperatureConverteris afinalclass with a private constructor, making it a utility class that cannot be instantiated. This is a best practice for classes containing only static methods.- We define
ABSOLUTE_ZEROconstants for each unit, which are crucial for physical validation. - Each conversion method takes a
doubleand returns adouble. - Absolute Zero Validation: Before performing any calculation, each method checks if the input temperature is below its respective absolute zero. This is a critical piece of validation for physical quantities. If invalid, it throws our custom
InvalidInputException. - Method Chaining: For conversions like Fahrenheit to Kelvin, we chain existing methods (
FahrenheitToCelsiusthenCelsiusToKelvin). This promotes code reuse and reduces potential errors. - Logging:
LOGGER.finemessages are added to trace the conversion process, useful for debugging in more complex scenarios.String.formatis used for formatted logging.
3. Testing This Component: TemperatureConverterTest
It’s crucial to test our core conversion logic independently. We’ll use JUnit 5 for this.
a) Add JUnit 5 Dependency (if not already present)
Open your pom.xml and ensure you have JUnit 5 dependencies. If not, add them within the <dependencies> block:
File: pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>temperature-converter-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source> <!-- Targeting Java 21+ for modern features -->
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.jupiter.version>5.10.0</junit.jupiter.version> <!-- Latest stable JUnit 5 -->
<log4j.version>2.23.0</log4j.version> <!-- Example for external logging, though we use JUL for simplicity -->
</properties>
<dependencies>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<!-- Optional: If you want to use Log4j2 instead of java.util.logging -->
<!--
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
</project>
Note on Java Version: The search results indicate Java 24.0.2 and dev.java mentions “Java 25 is here!” as of Dec 2025. For practical purposes and broad compatibility with modern features, setting maven.compiler.source and target to 21 is a good balance. Java 21 is an LTS (Long-Term Support) release and provides many modern features. If Java 25 is officially released as LTS by Dec 2025, you could update to 25. For now, we proceed with 21 as a robust and current baseline.
b) Create TemperatureConverterTest
File: src/test/java/com/example/temperatureconverter/TemperatureConverterTest.java
package com.example.temperatureconverter;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the TemperatureConverter class.
* Ensures that all conversion logic is accurate and handles edge cases like absolute zero.
*/
class TemperatureConverterTest {
private static final double DELTA = 0.01; // Tolerance for double comparisons
@ParameterizedTest(name = "{0}°C to {1}°F")
@CsvSource({
"0.0, 32.0", // Freezing point
"100.0, 212.0", // Boiling point
"25.0, 77.0",
"-40.0, -40.0" // Point where C and F are equal
})
@DisplayName("Celsius to Fahrenheit Conversions")
void testConvertCelsiusToFahrenheit(double celsius, double expectedFahrenheit) throws InvalidInputException {
double actualFahrenheit = TemperatureConverter.convertCelsiusToFahrenheit(celsius);
assertEquals(expectedFahrenheit, actualFahrenheit, DELTA,
() -> String.format("Expected %.2f°F for %.2f°C, but got %.2f°F", expectedFahrenheit, celsius, actualFahrenheit));
}
@Test
@DisplayName("Celsius to Fahrenheit - Absolute Zero Edge Case")
void testConvertCelsiusToFahrenheit_AbsoluteZero() throws InvalidInputException {
double absoluteZeroC = TemperatureConverter.ABSOLUTE_ZERO_CELSIUS;
double expectedF = -459.67; // -273.15 * 9/5 + 32
assertEquals(expectedF, TemperatureConverter.convertCelsiusToFahrenheit(absoluteZeroC), DELTA);
}
@Test
@DisplayName("Celsius to Fahrenheit - Below Absolute Zero Throws Exception")
void testConvertCelsiusToFahrenheit_BelowAbsoluteZero() {
double belowAbsoluteZeroC = TemperatureConverter.ABSOLUTE_ZERO_CELSIUS - 0.01;
InvalidInputException thrown = assertThrows(InvalidInputException.class, () ->
TemperatureConverter.convertCelsiusToFahrenheit(belowAbsoluteZeroC),
"Should throw InvalidInputException for temperatures below absolute zero Celsius");
assertTrue(thrown.getMessage().contains("absolute zero"));
}
@ParameterizedTest(name = "{0}°F to {1}°C")
@CsvSource({
"32.0, 0.0",
"212.0, 100.0",
"77.0, 25.0",
"-40.0, -40.0"
})
@DisplayName("Fahrenheit to Celsius Conversions")
void testConvertFahrenheitToCelsius(double fahrenheit, double expectedCelsius) throws InvalidInputException {
double actualCelsius = TemperatureConverter.convertFahrenheitToCelsius(fahrenheit);
assertEquals(expectedCelsius, actualCelsius, DELTA,
() -> String.format("Expected %.2f°C for %.2f°F, but got %.2f°C", expectedCelsius, fahrenheit, actualCelsius));
}
@Test
@DisplayName("Fahrenheit to Celsius - Absolute Zero Edge Case")
void testConvertFahrenheitToCelsius_AbsoluteZero() throws InvalidInputException {
double absoluteZeroF = TemperatureConverter.ABSOLUTE_ZERO_FAHRENHEIT;
double expectedC = -273.15; // (-459.67 - 32) * 5/9
assertEquals(expectedC, TemperatureConverter.convertFahrenheitToCelsius(absoluteZeroF), DELTA);
}
@Test
@DisplayName("Fahrenheit to Celsius - Below Absolute Zero Throws Exception")
void testConvertFahrenheitToCelsius_BelowAbsoluteZero() {
double belowAbsoluteZeroF = TemperatureConverter.ABSOLUTE_ZERO_FAHRENHEIT - 0.01;
InvalidInputException thrown = assertThrows(InvalidInputException.class, () ->
TemperatureConverter.convertFahrenheitToCelsius(belowAbsoluteZeroF),
"Should throw InvalidInputException for temperatures below absolute zero Fahrenheit");
assertTrue(thrown.getMessage().contains("absolute zero"));
}
@ParameterizedTest(name = "{0}°C to {1}K")
@CsvSource({
"0.0, 273.15",
"100.0, 373.15",
"-273.15, 0.0" // Absolute zero
})
@DisplayName("Celsius to Kelvin Conversions")
void testConvertCelsiusToKelvin(double celsius, double expectedKelvin) throws InvalidInputException {
double actualKelvin = TemperatureConverter.convertCelsiusToKelvin(celsius);
assertEquals(expectedKelvin, actualKelvin, DELTA,
() -> String.format("Expected %.2fK for %.2f°C, but got %.2fK", expectedKelvin, celsius, actualKelvin));
}
@Test
@DisplayName("Celsius to Kelvin - Below Absolute Zero Throws Exception")
void testConvertCelsiusToKelvin_BelowAbsoluteZero() {
double belowAbsoluteZeroC = TemperatureConverter.ABSOLUTE_ZERO_CELSIUS - 0.01;
assertThrows(InvalidInputException.class, () ->
TemperatureConverter.convertCelsiusToKelvin(belowAbsoluteZeroC));
}
@ParameterizedTest(name = "{0}K to {1}°C")
@CsvSource({
"273.15, 0.0",
"373.15, 100.0",
"0.0, -273.15" // Absolute zero
})
@DisplayName("Kelvin to Celsius Conversions")
void testConvertKelvinToCelsius(double kelvin, double expectedCelsius) throws InvalidInputException {
double actualCelsius = TemperatureConverter.convertKelvinToCelsius(kelvin);
assertEquals(expectedCelsius, actualCelsius, DELTA,
() -> String.format("Expected %.2f°C for %.2fK, but got %.2f°C", expectedCelsius, kelvin, actualCelsius));
}
@Test
@DisplayName("Kelvin to Celsius - Below Absolute Zero Throws Exception")
void testConvertKelvinToCelsius_BelowAbsoluteZero() {
double belowAbsoluteZeroK = TemperatureConverter.ABSOLUTE_ZERO_KELVIN - 0.01;
assertThrows(InvalidInputException.class, () ->
TemperatureConverter.convertKelvinToCelsius(belowAbsoluteZeroK));
}
@ParameterizedTest(name = "{0}°F to {1}K")
@CsvSource({
"32.0, 273.15", // 0°C
"212.0, 373.15", // 100°C
"-459.67, 0.0" // Absolute zero
})
@DisplayName("Fahrenheit to Kelvin Conversions")
void testConvertFahrenheitToKelvin(double fahrenheit, double expectedKelvin) throws InvalidInputException {
double actualKelvin = TemperatureConverter.convertFahrenheitToKelvin(fahrenheit);
assertEquals(expectedKelvin, actualKelvin, DELTA,
() -> String.format("Expected %.2fK for %.2f°F, but got %.2fK", expectedKelvin, fahrenheit, actualKelvin));
}
@Test
@DisplayName("Fahrenheit to Kelvin - Below Absolute Zero Throws Exception")
void testConvertFahrenheitToKelvin_BelowAbsoluteZero() {
double belowAbsoluteZeroF = TemperatureConverter.ABSOLUTE_ZERO_FAHRENHEIT - 0.01;
assertThrows(InvalidInputException.class, () ->
TemperatureConverter.convertFahrenheitToKelvin(belowAbsoluteZeroF));
}
@ParameterizedTest(name = "{0}K to {1}°F")
@CsvSource({
"273.15, 32.0", // 0°C
"373.15, 212.0", // 100°C
"0.0, -459.67" // Absolute zero
})
@DisplayName("Kelvin to Fahrenheit Conversions")
void testConvertKelvinToFahrenheit(double kelvin, double expectedFahrenheit) throws InvalidInputException {
double actualFahrenheit = TemperatureConverter.convertKelvinToFahrenheit(kelvin);
assertEquals(expectedFahrenheit, actualFahrenheit, DELTA,
() -> String.format("Expected %.2f°F for %.2fK, but got %.2f°F", expectedFahrenheit, kelvin, actualFahrenheit));
}
@Test
@DisplayName("Kelvin to Fahrenheit - Below Absolute Zero Throws Exception")
void testConvertKelvinToFahrenheit_BelowAbsoluteZero() {
double belowAbsoluteZeroK = TemperatureConverter.ABSOLUTE_ZERO_KELVIN - 0.01;
assertThrows(InvalidInputException.class, () ->
TemperatureConverter.convertKelvinToFahrenheit(belowAbsoluteZeroK));
}
}
Explanation:
- We use JUnit 5’s
@Testfor individual tests and@ParameterizedTestwith@CsvSourcefor data-driven tests, which is excellent for testing multiple inputs with expected outputs for formulas. DELTAis used forassertEqualswithdoublevalues to account for potential floating-point precision issues.- We test common conversion points (freezing, boiling, equal points).
- Crucially, we test the absolute zero edge cases and verify that
InvalidInputExceptionis thrown when temperatures fall below absolute zero usingassertThrows. This demonstrates robust error handling. DisplayNameprovides clear names for tests in reports.- Lambda expressions in
assertEqualsmessages are used for lazy evaluation, improving performance by only building the message string if the assertion fails.
To Run Tests:
Navigate to your project root in the terminal and run:
mvn test
You should see all tests pass. If any fail, carefully review your TemperatureConverter.java code against the formulas and absolute zero checks.
4. Core Implementation: Main Application Logic (TemperatureConverterApp)
Now we tie everything together with the main application class, handling user interaction.
File: src/main/java/com/example/temperatureconverter/TemperatureConverterApp.java
package com.example.temperatureconverter;
import java.util.InputMismatchException;
import java.util.Optional;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Main application class for the Temperature Converter.
* Handles user input, orchestrates conversions, and provides output.
* Uses `java.util.logging` for internal diagnostics and `Scanner` for console input.
*/
public class TemperatureConverterApp {
private static final Logger LOGGER = Logger.getLogger(TemperatureConverterApp.class.getName());
private final Scanner scanner;
/**
* Constructor for the TemperatureConverterApp.
* Initializes the Scanner for user input.
*/
public TemperatureConverterApp() {
this.scanner = new Scanner(System.in);
// Configure logging for better visibility in development
// For production, this would typically be configured via logging.properties or a framework
LOGGER.setLevel(Level.INFO); // Set to INFO for user-facing logs, FINE for detailed dev logs
}
/**
* Main entry point for the application.
*
* @param args Command line arguments (not used in this application).
*/
public static void main(String[] args) {
LOGGER.info("Starting Temperature Converter Application...");
TemperatureConverterApp app = new TemperatureConverterApp();
app.run();
LOGGER.info("Temperature Converter Application finished.");
}
/**
* Runs the main loop of the temperature converter application.
* Prompts the user for input, performs conversion, and displays results.
*/
public void run() {
System.out.println("----------------------------------------");
System.out.println(" Welcome to the Temperature Converter! ");
System.out.println("----------------------------------------");
System.out.println("Supported units: " + ConversionUnit.getSupportedUnitsString());
System.out.println("Type 'exit' to quit at any time.");
while (true) {
try {
// 1. Get source unit
ConversionUnit fromUnit = promptForUnit("Enter source unit (e.g., C, F, K): ");
if (fromUnit == null) { // User typed 'exit'
break;
}
// 2. Get target unit
ConversionUnit toUnit = promptForUnit("Enter target unit (e.g., C, F, K): ");
if (toUnit == null) { // User typed 'exit'
break;
}
// 3. Get temperature value
double temperature = promptForTemperature("Enter temperature value: ");
// No need to check for null here, as promptForTemperature handles 'exit' internally
// and throws an exception if parsing fails.
// 4. Perform conversion
double convertedTemperature = convertTemperature(temperature, fromUnit, toUnit);
// 5. Display result
System.out.printf("Result: %.2f %s is %.2f %s%n",
temperature, fromUnit.getSymbol(), convertedTemperature, toUnit.getSymbol());
} catch (InvalidInputException e) {
LOGGER.log(Level.WARNING, "Invalid input from user: {0}", e.getMessage());
System.err.println("Error: " + e.getMessage());
} catch (InputMismatchException e) { // Catches non-double input for temperature if not handled by nextLine()
LOGGER.log(Level.WARNING, "Unexpected input type error: {0}", e.getMessage());
System.err.println("Error: Invalid number format. Please enter a valid number.");
scanner.nextLine(); // Consume the invalid input to prevent infinite loop
} catch (Exception e) { // Catch any other unexpected errors
LOGGER.log(Level.SEVERE, "An unexpected error occurred: {0}", e.getMessage(), e);
System.err.println("An unexpected error occurred. Please try again.");
}
System.out.println("\n----------------------------------------\n"); // Separator for next conversion
}
scanner.close(); // Close scanner to release system resources
}
/**
* Prompts the user for a temperature unit and parses it.
*
* @param prompt The message to display to the user.
* @return The parsed ConversionUnit, or null if the user typed 'exit'.
* @throws InvalidInputException If the unit input is invalid.
*/
private ConversionUnit promptForUnit(String prompt) throws InvalidInputException {
while (true) {
System.out.print(prompt);
String input = scanner.nextLine().trim();
if (input.equalsIgnoreCase("exit")) {
LOGGER.info("User requested to exit during unit prompt.");
return null;
}
Optional<ConversionUnit> unit = ConversionUnit.fromString(input);
if (unit.isPresent()) {
return unit.get();
} else {
LOGGER.warning("Invalid unit input received: '" + input + "'");
System.err.println("Invalid unit. Please enter one of: " + ConversionUnit.getSupportedUnitsString());
}
}
}
/**
* Prompts the user for a temperature value and parses it.
*
* @param prompt The message to display to the user.
* @return The parsed temperature value.
* @throws InvalidInputException If the temperature input is not a valid number or user requests exit.
*/
private double promptForTemperature(String prompt) throws InvalidInputException {
while (true) {
System.out.print(prompt);
String input = scanner.nextLine().trim();
if (input.equalsIgnoreCase("exit")) {
LOGGER.info("User requested to exit during temperature prompt.");
// We're treating 'exit' during temperature input as an InvalidInputException
// to break out of the current conversion flow. The run() method will catch this.
throw new InvalidInputException("User exited.");
}
try {
return Double.parseDouble(input);
} catch (NumberFormatException e) {
LOGGER.log(Level.WARNING, "Invalid temperature number format: '{0}'", input);
System.err.println("Invalid temperature value. Please enter a valid number.");
}
}
}
/**
* Orchestrates the temperature conversion based on source and target units.
*
* @param temperature The temperature value to convert.
* @param fromUnit The source unit.
* @param toUnit The target unit.
* @return The converted temperature.
* @throws InvalidInputException If an unsupported conversion path is requested or
* if the temperature is below absolute zero.
*/
private double convertTemperature(double temperature, ConversionUnit fromUnit, ConversionUnit toUnit)
throws InvalidInputException {
if (fromUnit == toUnit) {
LOGGER.info(String.format("Source and target units are the same (%s), returning original temperature.", fromUnit.getSymbol()));
return temperature; // No conversion needed
}
LOGGER.info(String.format("Attempting to convert %.2f %s to %s", temperature, fromUnit.getSymbol(), toUnit.getSymbol()));
// This switch statement leverages Java 17+ enhanced switch expressions for conciseness
// and pattern matching for switch (preview in Java 17, standard in Java 21) if complex.
// For simplicity, we'll use a traditional switch here, but mention the capability.
return switch (fromUnit) {
case CELSIUS -> switch (toUnit) {
case FAHRENHEIT -> TemperatureConverter.convertCelsiusToFahrenheit(temperature);
case KELVIN -> TemperatureConverter.convertCelsiusToKelvin(temperature);
default -> throw new InvalidInputException("Unsupported target unit for Celsius conversion: " + toUnit.getSymbol());
};
case FAHRENHEIT -> switch (toUnit) {
case CELSIUS -> TemperatureConverter.convertFahrenheitToCelsius(temperature);
case KELVIN -> TemperatureConverter.convertFahrenheitToKelvin(temperature);
default -> throw new InvalidInputException("Unsupported target unit for Fahrenheit conversion: " + toUnit.getSymbol());
};
case KELVIN -> switch (toUnit) {
case CELSIUS -> TemperatureConverter.convertKelvinToCelsius(temperature);
case FAHRENHEIT -> TemperatureConverter.convertKelvinToFahrenheit(temperature);
default -> throw new InvalidInputException("Unsupported target unit for Kelvin conversion: " + toUnit.getSymbol());
};
// The outer switch should cover all ConversionUnit enums, so default here implies an unhandled state,
// which should ideally not happen if enum is fully covered.
// However, a defensive approach is good.
default -> {
LOGGER.severe("Unhandled source unit in conversion logic: " + fromUnit.getSymbol());
throw new InvalidInputException("Unsupported source unit: " + fromUnit.getSymbol());
}
};
}
}
Explanation:
mainmethod: The entry point. It creates an instance ofTemperatureConverterAppand callsrun().runmethod: This is the main application loop.- It welcomes the user and explains supported units and how to exit.
- It continuously prompts for source unit, target unit, and temperature value.
- It uses a
try-catchblock to gracefully handleInvalidInputException(from our custom checks andNumberFormatException) and generalExceptions, providing user-friendly error messages. - Resource Management:
scanner.close()is called when the application exits to release system resources, preventing resource leaks.
promptForUnit:- Loops until valid unit input is received or the user types “exit”.
- Uses
ConversionUnit.fromString()to parse input safely. - Provides clear error messages for invalid unit input.
promptForTemperature:- Loops until valid numeric input is received or the user types “exit”.
- Uses
Double.parseDouble()to convertStringtodouble. - Catches
NumberFormatExceptionfor non-numeric input. - Treats “exit” as an
InvalidInputExceptionto simplify flow control inrun().
convertTemperature:- This method acts as a router, delegating to the appropriate static method in
TemperatureConverterbased on thefromUnitandtoUnit. - It includes a check for
fromUnit == toUnitto avoid unnecessary conversion. - The nested
switchstatements (using Java 17+ syntax for clarity, though a traditionalif-else ifchain would also work) ensure all valid conversion paths are covered. Adefaultcase is included for defensive programming, though with a finite enum, all cases should ideally be handled explicitly.
- This method acts as a router, delegating to the appropriate static method in
- Logging:
LOGGER.info,LOGGER.warning, andLOGGER.severeare used throughout to provide insight into application flow, user actions, and errors. This is invaluable for debugging and monitoring in production.
5. Production Considerations
- Error Handling: We’ve implemented robust error handling using
try-catchblocks forNumberFormatExceptionand our customInvalidInputException. This prevents the application from crashing due to bad user input and provides informative feedback. The top-levelcatch (Exception e)inrun()is a safety net for unexpected issues. - Performance Optimization: For this simple console application, performance is not a major concern. However, in larger applications, avoiding repeated expensive operations (e.g., parsing configuration files inside a loop) and choosing efficient data structures would be key. Our conversion logic is purely mathematical and highly efficient.
- Security Considerations:
- Input Validation: Crucial for security. We validate numeric inputs and unit types. For a console app, direct security threats are minimal, but in web applications, input validation prevents injection attacks (SQL, XSS). Our absolute zero checks are a form of domain-specific validation.
- Resource Management: Closing the
Scanner(scanner.close()) prevents resource leaks, which can be a denial-of-service vector in server applications if too many resources are held open.
- Logging and Monitoring: We’ve integrated
java.util.loggingfor internal diagnostics. In a production environment, you would typically configurelogging.propertiesor use a more advanced logging framework like Log4j2 or SLF4J with Logback. This allows for:- Centralized Logging: Sending logs to a file, a log management system (e.g., ELK stack, Splunk, Datadog).
- Log Levels: Adjusting verbosity (e.g.,
INFOin production,FINE/DEBUGin development). - Structured Logging: Outputting logs in JSON for easier parsing by monitoring tools.
- Code Organization: The separation of concerns into
TemperatureConverterApp,TemperatureConverter,ConversionUnit, andInvalidInputExceptionmakes the codebase organized, readable, and easy to maintain or extend.
6. Code Review Checkpoint
At this point, you have:
- Files created/modified:
src/main/java/com/example/temperatureconverter/ConversionUnit.javasrc/main/java/com/example/temperatureconverter/InvalidInputException.javasrc/main/java/com/example/temperatureconverter/TemperatureConverter.javasrc/main/java/com/example/temperatureconverter/TemperatureConverterApp.javasrc/test/java/com/example/temperatureconverter/TemperatureConverterTest.javapom.xml(updated for JUnit 5)
- Core functionality:
- Defined temperature units using an enum.
- Implemented a custom exception for invalid inputs.
- Developed robust temperature conversion logic for Celsius, Fahrenheit, and Kelvin, including absolute zero validation.
- Created a console application that prompts for input, performs conversions, and displays results.
- Integrated comprehensive error handling for invalid user input and unexpected issues.
- Added logging for internal application flow and errors.
- Integration: All components work together to provide a complete temperature conversion experience. The
TemperatureConverterApporchestrates calls toConversionUnitfor parsing andTemperatureConverterfor the actual math.
7. Common Issues & Solutions
Issue:
InputMismatchExceptionorNoSuchElementExceptionwhen usingScanner.- Problem: If you mix
scanner.next()orscanner.nextInt()withscanner.nextLine(), thenextLine()call might consume the leftover newline character from the previousnext()call, leading to unexpected empty inputs or skipping prompts.NoSuchElementExceptionoccurs if the scanner is closed prematurely or no input is available. - Solution: Always use
scanner.nextLine()to read the entire line of input, then parse it (e.g.,Double.parseDouble(),Integer.parseInt()). This avoids the newline character issue. ForNumberFormatException, catch it and re-prompt. Ensurescanner.close()is called only when the application is truly exiting. In ourTemperatureConverterApp, we consistently usescanner.nextLine(). - Prevention: Consistency in
Scannerusage is key. Our code explicitly handlesNumberFormatExceptionwhen parsing numeric values fromnextLine().
- Problem: If you mix
Issue: Floating-point precision errors in conversions.
- Problem: When comparing
doublevalues directly using==, you might encounter issues due to the nature of floating-point representation. For example,0.1 + 0.2might not exactly equal0.3. - Solution: When comparing
doubleorfloatvalues, use a small tolerance (epsilon or delta). In our unit tests, we usedassertEquals(expected, actual, DELTA). For display, useString.format("%.2f", value)to round to a specific number of decimal places. - Prevention: Be aware of floating-point arithmetic limitations and use appropriate comparison techniques and formatting for display.
- Problem: When comparing
Issue: Application crashes on invalid unit input (e.g., “XYZ”).
- Problem: Without proper validation, attempting to use an unparsed unit string can lead to
NullPointerExceptionor other runtime errors. - Solution: Our
ConversionUnit.fromString()method returns anOptional<ConversionUnit>. This forces you to explicitly check if a unit was found (unit.isPresent()) before using it. If not present, thepromptForUnitmethod reprompts the user, preventing the invalid unit from reaching the conversion logic. - Prevention: Use
Optionalfor methods that might not return a value. Implement robust input validation loops that continue prompting until valid input is provided.
- Problem: Without proper validation, attempting to use an unparsed unit string can lead to
8. Testing & Verification
To test and verify the entire chapter’s work:
Compile the project:
mvn clean installThis command cleans the previous build, compiles all source code, runs the unit tests, and packages the application. All unit tests should pass.
Run the application:
java -jar target/temperature-converter-app-1.0-SNAPSHOT.jar(Note: If you haven’t configured the
maven-jar-pluginto create an executable JAR with dependencies, you might need to run it directly from themainclass, ensuring the classpath is correctly set. For simplicity in these early chapters, running from IDE ormvn exec:javamight be easier.)Alternative (simpler for console apps without full JAR configuration):
mvn exec:java -Dexec.mainClass="com.example.temperatureconverter.TemperatureConverterApp"Interact with the application:
- Valid Conversions:
- Convert 0 C to F: Expected 32.00 F
- Convert 100 C to F: Expected 212.00 F
- Convert 32 F to C: Expected 0.00 C
- Convert 212 F to C: Expected 100.00 C
- Convert 0 K to C: Expected -273.15 C
- Convert -40 C to F: Expected -40.00 F
- Convert 25 C to K: Expected 298.15 K
- Convert 298.15 K to F: Expected 77.00 F
- Invalid Temperature Input:
- Enter “abc” for temperature value. It should show an error and re-prompt.
- Enter a value below absolute zero (e.g., -300 C). It should catch the
InvalidInputExceptionand show an error.
- Invalid Unit Input:
- Enter “X” or “unknown” for a unit. It should show an error and re-prompt.
- Exit Functionality:
- Type “exit” at any prompt. The application should gracefully shut down.
- Valid Conversions:
By performing these checks, you verify that the application handles both valid and invalid user inputs correctly, performs accurate conversions, and provides a good user experience.
Summary & Next Steps
Congratulations! In this chapter, you’ve successfully built a robust Temperature Converter application. You started by designing the component architecture, then implemented:
- Type-safe unit definitions using an
enum. - A custom exception for specific application errors.
- The core conversion logic with absolute zero validation.
- Comprehensive unit tests to ensure the accuracy and reliability of your conversion formulas.
- A user-friendly console interface that safely handles user input, parses data, performs conversions, and displays results.
- Production-ready practices including error handling, logging, and proper resource management.
This project was a significant step in understanding how to build interactive command-line tools with robust data handling. You’ve seen how to protect your application from bad input and provide clear feedback to the user.
In the next chapter, we’ll continue to build on these fundamentals as we tackle the Basic To-Do List Application. This will introduce concepts like storing and managing data (initially in memory, later persisting it), handling multiple commands, and further refining the command-line user experience. Get ready to manage tasks!