0.14. Static Members, Main Method, and Exception Handling¶
Throughout this course, you have designed object-oriented systems composed of
interacting classes, instantiated objects in heap memory, and managed individual
object states. In this final chapter, we examine concepts essential for
building standalone production applications: class-level static members, the
standard execution entry point (main), Java’s robust exception handling
architecture, and disciplined systematic debugging methodologies.
0.14.1. Static Fields: Class-Wide State¶
Up to this point, every field you have declared has been an instance field.
When a class defines instance fields, each object created with the new
operator receives its own private copy of those variables in heap memory:
public class Ball
{
private String color;
public Ball(String color)
{
this.color = color;
}
public String getColor()
{
return this.color;
}
}
If you instantiate three Ball objects, there are three separate color
variables stored across the heap. Changing the color of one ball has no effect
on the other two.
However, software designs frequently require properties that belong to the
class as a whole, rather than to any individual instance. In Java, adding the
static modifier to a field declaration designates it as a static field
(also known as a class variable).
0.14.1.2. Generating Unique Identifiers¶
One classic use of static fields is tracking class-wide state, such as generating unique identification numbers for newly created objects:
public class Student
{
private static int nextIdNumber = 1001; // Shared class-wide counter
private int id;
private String name;
public Student(String name)
{
this.name = name;
this.id = Student.nextIdNumber; // Assign current shared counter
Student.nextIdNumber++; // Advance counter for next student
}
public int getId()
{
return this.id;
}
public String getName()
{
return this.name;
}
}
Every time new Student(...) is called, the constructor reads the shared
Student.nextIdNumber, assigns it to the instance field this.id, and
increments the shared counter. Each student receives a unique, sequential ID
number.
0.14.1.3. Class Constants with public static final¶
By combining static with the final modifier, you define class
constants—immutable values that are fixed at compile time and shared across
all instances without duplicating memory:
public class PhysicsSimulation
{
public static final double GRAVITATIONAL_ACCELERATION = 9.80665;
public static final double SPEED_OF_LIGHT = 299792458.0;
}
By convention, constant identifiers are written in uppercase with underscores separating words. Other classes can reference them directly without instantiating an object:
double weight = mass * PhysicsSimulation.GRAVITATIONAL_ACCELERATION;
0.14.2. Static Methods & The Math Class¶
Just as fields can belong to a class, methods can also be declared with the
static modifier. A static method (or class method) can be invoked
directly through the class name without creating an instance of that class.
0.14.2.1. Pure Mathematical Utility Functions¶
Static methods are ideal for utility functions: operations that compute a result based solely on their input arguments without reading or modifying any instance state:
public class GeometryUtils
{
public static double calculateHypotenuse(double sideA, double sideB)
{
return Math.sqrt((sideA * sideA) + (sideB * sideB));
}
public static double toRadians(double degrees)
{
return degrees * (Math.PI / 180.0);
}
}
You invoke these methods directly on the class:
double hyp = GeometryUtils.calculateHypotenuse(3.0, 4.0); // Returns 5.0
0.14.2.2. The Standard java.lang.Math Library¶
Java’s built-in java.lang.Math class is the quintessential example of a
utility class. It contains only static constants (such as Math.PI and
Math.E) and static methods for mathematical operations:
Math.sqrt(double a): Returns the correctly rounded positive square root.Math.pow(double a, double b): Returns the value of $a$ raised to the power of $b$ ($a^b$).Math.abs(int a)/Math.abs(double a): Returns the absolute value.Math.min(a, b)/Math.max(a, b): Returns the smaller or larger of two values.Math.round(double a): Returns the closest integer or long to the argument.
Because all members are static, you never write new Math(). You invoke them
directly:
double area = Math.PI * Math.pow(radius, 2);
double distance = Math.abs(x1 - x2);
0.14.2.3. The Core Restriction of Static Context¶
Because static methods belong to the class and can run before any objects exist, static methods have no `this` reference.
Consequently, a static method cannot directly access instance fields or call non-static methods of its own class:
public class Greeter
{
private String greeting = "Hello";
public static void greet()
{
// COMPILE ERROR: Cannot make a static reference to non-static field
System.out.println(greeting);
// COMPILE ERROR: Cannot use 'this' in a static context
System.out.println(this.greeting);
}
}
To access instance members from inside a static method, you must explicitly create or receive an object instance:
public static void greet()
{
Greeter g = new Greeter();
System.out.println(g.greeting); // Legal: accessed via an explicit object!
}
0.14.3. The Standard Application Entry: main¶
In BlueJ, you have interacted with programs by right-clicking classes and selecting constructors or methods from an interactive popup menu. Outside of specialized educational environments, however, standalone software applications execute from a standard command-line terminal or operating system launcher.
Every standalone Java program begins execution at a single designated entry
point: the main method.
0.14.3.1. The Signature of main¶
The method signature for the application entry point is strictly defined by the Java Virtual Machine:
public static void main(String[] args)
Each keyword serves a specific purpose in the runtime architecture:
public: The method must be accessible to the JVM launcher from outside the class package.static: The JVM must be able to invokemainbefore creating any objects in heap memory.void: The method returns no value when execution concludes.main: The exact identifier the JVM searches for when launching the program.String[] args: An array of strings containing any command-line arguments passed by the operating system upon launch.
0.14.3.2. Command-Line Arguments¶
When a user executes a compiled Java program from a command prompt, words typed
after the class name are packaged into the String[] args array:
java DataAnalyzer dataset1.csv 500
Inside main, Java automatically allocates the array:
public class DataAnalyzer
{
public static void main(String[] args)
{
if (args.length < 2)
{
System.out.println("Usage: java DataAnalyzer <filename> <threshold>");
return;
}
String filename = args[0]; // "dataset1.csv"
int threshold = Integer.parseInt(args[1]); // 500
System.out.println("Analyzing " + filename + " at threshold " + threshold);
}
}
0.14.3.3. The Application Bootstrap Pattern¶
Because main is static, it cannot directly invoke non-static methods or
manipulate instance fields of its enclosing class. Therefore, modern
object-oriented architectures follow the bootstrap pattern: main
instantiates the top-level application object and delegates execution to an
instance method:
public class AdventureGame
{
private World world;
private Player player;
public AdventureGame()
{
this.world = new World();
this.player = new Player("Explorer");
}
public void play()
{
System.out.println("Welcome to the adventure!");
// Run the interactive game loop...
}
public static void main(String[] args)
{
// Bootstrap: instantiate the game object and start execution
AdventureGame game = new AdventureGame();
game.play();
}
}
0.14.4. The Exception Hierarchy: Checked vs. Unchecked¶
Software systems inevitably encounter conditions that prevent normal execution: network connections disconnect, files fail to open, users enter invalid data, or calculations attempt to divide by zero.
In Java, abnormal events are represented as exceptions: objects that encapsulate error state and are propagated up the call stack.
0.14.4.1. The Java Exception Hierarchy¶
All error and exception classes in Java inherit from the common base class
java.lang.Throwable. The hierarchy branches into two primary categories:
Throwable
/ \
Error Exception
/ \
Checked Exceptions RuntimeException
(IOException, etc.) (Unchecked Exceptions)
`java.lang.Error`: Represents severe, catastrophic conditions external to the application code, such as
OutOfMemoryErrororStackOverflowError. Applications should never attempt to catch or handle anError; they indicate that the JVM itself cannot safely continue.`java.lang.Exception`: Represents conditions that a well-written application can reasonably foresee, catch, and handle gracefully.
0.14.4.2. Checked vs. Unchecked Exceptions¶
Java divides the Exception tree into two distinct categories with different
compiler enforcement rules:
Category |
Compiler Enforcement Contract |
Typical Examples |
|---|---|---|
Checked Exceptions (Subclasses of |
The compiler enforces that the calling code must either catch the
exception using a |
|
Unchecked Exceptions (Subclasses of |
The compiler does not enforce handling or declaration. They represent programming logic errors, invalid arguments, or broken preconditions that could be prevented through defensive coding. |
|
Understanding this distinction clarifies what you saw in Chapter 13: opening a
disk file with new Scanner(new File(...)) threw FileNotFoundException (a
checked exception), forcing the compiler to require try-catch or a throws
clause. Conversely, dividing by zero throws ArithmeticException (an unchecked
runtime exception), which halts the program immediately unless handled.
0.14.5. Handling Exceptions with try-catch-finally¶
When an exception is thrown, normal sequential control flow is interrupted. Java provides the structured `try-catch-finally` statement to intercept exceptions and take recovery actions without terminating the program.
0.14.5.1. Execution Flow of try-catch¶
public int parseIntegerSafely(String text)
{
int result = 0;
try
{
// Step 1: Attempt normal execution
result = Integer.parseInt(text);
}
catch (NumberFormatException e)
{
// Step 2: Handle specific failure gracefully
System.out.println("Warning: Invalid number format for '" + text + "'. Defaulting to 0.");
result = 0;
}
return result;
}
The code inside the
tryblock begins execution.If all statements execute without an exception, the entire
catchblock is skipped, and execution continues immediately following the construct.If an exception occurs on any statement inside the
tryblock, execution of thetryblock terminates immediately. Java searches for a matchingcatchblock whose declared parameter type matches the thrown exception.If a matching catch block is found, its code executes. The program does not crash; execution resumes normally after the catch block.
0.14.5.2. Multi-Catch Ordering: Specific to General¶
A single try block can be followed by multiple catch clauses to handle
different error types uniquely. Java matches catch blocks in top-to-bottom
order.
Because Java matches by inheritance (polymorphism), catch blocks must always be ordered from the most specific subclass to the most general superclass:
try
{
Scanner scanner = new Scanner(new File(filename));
String line = scanner.nextLine();
int count = Integer.parseInt(line);
scanner.close();
}
catch (FileNotFoundException e)
{
System.out.println("File could not be found: " + e.getMessage());
}
catch (NumberFormatException e)
{
System.out.println("File contents could not be parsed as an integer: " + e.getMessage());
}
catch (Exception e)
{
// Catch-all fallback for any other unexpected exception
System.out.println("An unexpected error occurred: " + e.getMessage());
}
If you placed catch (Exception e) at the top, the compiler would report an
error (unreachable code) because the general handler would swallow all
subclass exceptions before the specific handlers could be reached.
0.14.5.3. The finally Block: Guaranteed Resource Cleanup¶
The optional finally block follows the last catch clause. Statements
inside finally are guaranteed to execute, whether an exception occurred
or not, and whether an exception was caught or left unhandled:
Scanner scanner = null;
try
{
scanner = new Scanner(new File("data.txt"));
processData(scanner);
}
catch (FileNotFoundException e)
{
System.out.println("Failed to open file: " + e.getMessage());
}
finally
{
// Guaranteed cleanup: closes the stream even if processData threw an exception!
if (scanner != null)
{
scanner.close();
}
}
The finally block is the standard location for releasing external operating
system resources, such as open file descriptors, database connections, or
network sockets.
0.14.6. Throwing Exceptions for Invalid Arguments¶
In addition to catching exceptions generated by the JVM or standard library, well-engineered software actively detects invalid conditions and throws exceptions explicitly.
0.14.6.1. The throw Statement¶
You throw an exception using the throw keyword followed by an instantiated
exception object:
throw new IllegalArgumentException("Dimension must be positive: " + width);
When the throw statement executes:
Normal method execution halts immediately (analogous to an emergency
return).The runtime begins unwinding the call stack until an enclosing
try-catchblock matching the exception type is encountered.If no matching handler exists on the call stack, the thread terminates and prints a stack trace to the console.
0.14.6.2. Defensive Parameter Validation¶
The most common use of explicit exception throwing is defensive precondition
validation in constructors and mutator methods. Rather than silently accepting
corrupt data or returning dummy error values (such as -1 or null),
methods immediately reject invalid arguments:
public class BankAccount
{
private double balance;
public BankAccount(double initialBalance)
{
if (initialBalance < 0.0)
{
throw new IllegalArgumentException(
"Initial balance cannot be negative: " + initialBalance);
}
this.balance = initialBalance;
}
public void deposit(double amount)
{
if (amount <= 0.0)
{
throw new IllegalArgumentException(
"Deposit amount must be strictly positive: " + amount);
}
this.balance += amount;
}
}
Because IllegalArgumentException is an unchecked RuntimeException, the
method headers do not require a throws clause. The compiler allows
unchecked exceptions to propagate cleanly.
0.14.7. Systematic Debugging & Defect Isolation with TestCase¶
Software development is fundamentally an empirical discipline. Writing code is only half the battle; locating, isolating, and repairing unexpected defects underpins professional engineering.
0.14.7.1. The Four-Step Scientific Debugging Method¶
Novice programmers often approach debugging through random trial and error: making ad-hoc modifications and hoping tests turn green. Professional developers employ a systematic, hypothesis-driven cycle:
Reproduce: Never attempt to fix a defect until you can reliably trigger it with an automated unit test. If a bug is intermittent or unobserved in code, you cannot verify whether a proposed fix truly resolved it.
Isolate: Formulate a hypothesis about the root cause. Inspect intermediate values, step through code with a debugger, or insert temporary logging to trace execution state until the exact line causing the incorrect calculation is identified.
Repair: Modify the source code to address the underlying root cause. Avoid band-aid patches that merely mask symptoms for specific test inputs.
Verify Regression: Re-run the entire automated test suite to confirm that the defect is eliminated and that no previously working features were broken (regression testing).
0.14.7.2. Floating-Point Tolerance Testing with TestCase¶
In Java, floating-point numbers (float and double) are represented using
binary fractions according to the IEEE 754 standard. Because most decimal
fractions (such as 0.1) cannot be represented exactly in binary, floating-point
arithmetic introduces tiny rounding discrepancies:
double sum = 0.1 + 0.2;
System.out.println(sum); // Prints: 0.30000000000000004
System.out.println(sum == 0.3); // Prints: false!
In unit tests, evaluating floating-point calculations with direct equality
assertions (assertEquals(0.3, sum)) will fail unpredictably.
To test floating-point outcomes correctly, test classes extending TestCase
use tolerance assertions that specify an acceptable margin of error
(often called delta or epsilon):
assertEquals(expected, actual, delta);
For example, to assert that a calculated physical dimension or weight matches within 0.01 units:
assertEquals(15.75, calculatedVolume, 0.01);
This assertion evaluates whether $|expected - actual| le 0.01$.
0.14.7.3. Testing That Exceptions Are Thrown¶
When verifying defensive precondition checks, unit tests must confirm that
invalid arguments correctly trigger exceptions. In TestCase, the canonical
idiom wraps the illegal invocation in a try block and fails if the exception
is not thrown:
public void testNegativeDepositThrowsException()
{
BankAccount account = new BankAccount(100.0);
try
{
account.deposit(-25.0);
fail("Expected IllegalArgumentException was not thrown for negative deposit");
}
catch (IllegalArgumentException e)
{
// Exception was successfully caught: the test passes!
assertTrue(e.getMessage().contains("strictly positive"));
}
}
Notice the call to fail(...): if account.deposit(-25.0) fails to throw an
exception, execution proceeds to fail(), marking the test as failed. If the
exception is correctly thrown, control skips directly to the catch block,
and the test passes.
0.14.7.4. Multi-Class Defect Diagnosis: The Container Analogy¶
Consider a physical logistics system modeling cylindrical storage canisters and freight containers:
public class Cylinder
{
private double radius;
private double height;
private double density;
public Cylinder(double radius, double height, double density)
{
if (radius <= 0.0 || height <= 0.0 || density <= 0.0)
{
throw new IllegalArgumentException("Dimensions and density must be positive.");
}
this.radius = radius;
this.height = height;
this.density = density;
}
public double getVolume()
{
// Volume = pi * r^2 * h
return Math.PI * Math.pow(this.radius, 2) * this.height;
}
public double getWeight()
{
return this.getVolume() * this.density;
}
}
Now consider a CargoContainer that holds a collection of cylinders:
public class CargoContainer
{
private double tareWeight; // Empty container weight
private List<Cylinder> contents;
public CargoContainer(double tareWeight)
{
this.tareWeight = tareWeight;
this.contents = new ArrayList<Cylinder>();
}
public void addCylinder(Cylinder c)
{
this.contents.add(c);
}
public double getTotalWeight()
{
double total = this.tareWeight;
for (Cylinder c : this.contents)
{
total += c.getWeight();
}
return total;
}
}
When a test for CargoContainer.getTotalWeight() fails, where is the bug?
Is the defect in
CargoContainer.getTotalWeight()?Is the defect in
Cylinder.getWeight()?Is the defect in
Cylinder.getVolume()?Is the defect in the constructor initializing
this.tareWeight?
In multi-class systems, defects in a low-level component (such as an arithmetic
error in Cylinder.getVolume()) ripple upward, causing failures in aggregate
calculations across higher-level classes.
The Golden Rule of Defect Isolation: Test lower-level foundation classes
thoroughly in complete isolation before testing classes that aggregate them.
Once CylinderTest verifies 100% correctness for volume, surface area, and
weight with tolerance assertions, any remaining failure in CargoContainer
must reside within container-specific logic.
0.14.7.5. Maintaining 100% Statement and Condition Coverage¶
Professional software grading environments (such as Web-CAT) assess test suites based on code coverage:
Statement Coverage: The percentage of source code executable statements exercised by at least one test method.
Branch/Condition Coverage: The percentage of conditional decision branches (both
trueandfalseoutcomes of everyif,while, andforcondition) executed during testing.
Achieving 100% coverage requires designing tests for boundary conditions: zero items, maximum capacity, exact thresholds, and illegal inputs that trigger exception branches. By verifying both positive operational pathways and negative defensive branches with automated unit tests, you guarantee software reliability and architectural stability.
