Core Java Concepts: Exception Handling, Reflection, Annotations, and Generics
Exception Handling
Overview of Exceptions
The Throwable class serves as the root of the exception hierarchy, extending Object. It branches into two main categories: Error and Exception.
Error represents severe system-level issues that are typically beyond the control of the programmer, often arising from the Java Virtual Machine (JVM) or environment. These include problems like OutOfMemoryError, NoClassDefFoundError, and StackOverflowError. Such errors usually indicate conditions from which recovery is not feasible within the application, and terminating the program may be the only recourse.
Exception denotes conditions that an application might anticipate and handle. These are often caused by logical flaws in the code and should be managed to allow the program to continue running where possible. Common examples include:
ArrayIndexOutOfBoundsException: Acccessing an array with an invalid index.ClassCastException: Attmepting an invalid type cast.IllegalArgumentException: Passing an illegal argument to a method.NullPointerException: Dereferencing a null reference.NoSuchElementException: Requesting a non-existent element from a collection.
Catching Exceptions
Use a try-catch block to encapsulate code that may throw exceptions and define handlers for specific exception types.
try {
int[] values = {10, 20};
System.out.println(values[5]); // Potential ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException ex) {
ex.printStackTrace();
System.out.println("Array index is out of bounds.");
}
Multiple catch blocks can be used to handle different exception types specifically. The Exception class can serve as a general catch-all, but it should be placed last to alow more specific exceptions to be caught first.
try {
// Code that may throw various exceptions
} catch (ArrayIndexOutOfBoundsException ex) {
// Handle array index issue
} catch (NullPointerException ex) {
// Handle null reference
} catch (Exception ex) {
// General exception handler
}
The finally block ensures execution of cleanup code regardless of whether an exception occurs.
try {
// Risky operations
} catch (Exception ex) {
// Handle exception
} finally {
System.out.println("Cleanup executed.");
}
Throwing Exceptions
Exceptions can be thrown explicitly using the throw keyword.
// Instantiate and throw an exception
IllegalArgumentException iae = new IllegalArgumentException("Invalid input provided.");
throw iae;
// Or throw directly
throw new NumberFormatException("Cannot parse null string.");
Custom Exceptions
Create custom exceptions by extending an appropriate base class, typically RuntimeException.
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
public ValidationException(String message, Throwable cause) {
super(message, cause);
}
}
Assertions
Assertions are used during development and testing to validate assumptions. They are disabled by default in production.
public class AssertExample {
static int threshold = 100;
public static void main(String[] args) {
assert threshold > 50 : "Threshold must be greater than 50";
System.out.println("Assertion passed.");
}
}
Logging
Modern Java applications use logging frameworks like JDK Logging, Log4j 2, SLF4J, and Logback for structured log output.
JDK Logging Example:
import java.util.logging.Logger;
import java.util.logging.Level;
public class AppLogger {
private static final Logger LOG = Logger.getLogger(AppLogger.class.getName());
public static void main(String[] args) {
LOG.info("Application started.");
LOG.log(Level.WARNING, "Potential issue detected.");
}
}
Log4j 2 Configuration (log4j2.xml snippet):
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
SLF4J with Logback Example:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Service {
private static final Logger LOG = LoggerFactory.getLogger(Service.class);
public void process(int id, String name) {
LOG.debug("Processing item with ID: {} and Name: {}", id, name);
// Business logic
LOG.info("Item processed successfully.");
}
}
Reflection
Concept
Reflection enables inspection and manipulation of classes, fields, methods, and constructors at runtime, allowing dynamic behavior.
The Class Object
Every class loaded in the JVM has a corresponding Class object that provides metadata. There are three primary ways to obtain a Class instance:
// 1. Using Class.forName()
Class<?> clazz1 = Class.forName("java.lang.String");
// 2. Using .class syntax
Class<?> clazz2 = String.class;
// 3. Using getClass() on an instance
String str = "example";
Class<?> clazz3 = str.getClass();
Inspecting Fields
Reflection allows access to field names, types, and values, including private fields (with access override).
import java.lang.reflect.Field;
class Employee {
private String name;
public int id;
}
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Employee emp = new Employee();
Class<?> empClass = emp.getClass();
// Access public field
Field publicField = empClass.getField("id");
System.out.println("Field name: " + publicField.getName());
// Access private field
Field privateField = empClass.getDeclaredField("name");
privateField.setAccessible(true); // Override access control
privateField.set(emp, "Alice");
System.out.println("Employee name: " + privateField.get(emp));
}
}
Invoking Methods
Methods can be discovered and invoked dynamically.
import java.lang.reflect.Method;
class Calculator {
public int add(int a, int b) {
return a + b;
}
private void log(String message) {
System.out.println("Log: " + message);
}
}
public class MethodInvocationDemo {
public static void main(String[] args) throws Exception {
Calculator calc = new Calculator();
Class<?> calcClass = calc.getClass();
// Invoke public method
Method addMethod = calcClass.getMethod("add", int.class, int.class);
int result = (int) addMethod.invoke(calc, 5, 3);
System.out.println("Addition result: " + result);
// Invoke private method
Method logMethod = calcClass.getDeclaredMethod("log", String.class);
logMethod.setAccessible(true);
logMethod.invoke(calc, "Private method called via reflection.");
}
}
Dynamic Proxies
Dynamic proxies create wrapper objects at runtime to intercept method calls, often used for cross-cutting concerns like logging or security.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Greeter {
void greet(String name);
}
class SimpleGreeter implements Greeter {
public void greet(String name) {
System.out.println("Hello, " + name);
}
}
class LoggingHandler implements InvocationHandler {
private Object target;
public LoggingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After method: " + method.getName());
return result;
}
}
public class ProxyDemo {
public static void main(String[] args) {
Greeter realGreeter = new SimpleGreeter();
Greeter proxyGreeter = (Greeter) Proxy.newProxyInstance(
Greeter.class.getClassLoader(),
new Class[]{Greeter.class},
new LoggingHandler(realGreeter)
);
proxyGreeter.greet("World");
}
}