Exception Handling
Understanding Exceptions
An exception represents an abnormal condition that arises during program execution. Java groups exceptions into checked and unchecked categories, enabling structured error management.
Custom Exception Classes
Applications often require specific exception types beyond the standard library. Two variants exist:
Unchecked Custom Exception
Extend RuntimeException for exceptions that usually reflect programming errors and do not require explicit handling:
public class AgeValidation {
public static void main(String[] args) {
try {
storeAge(230);
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static void storeAge(int age) {
if (age > 0 && age < 120) {
System.out.println("Age recorded: " + age);
} else {
throw new InvalidAgeException("Invalid age value: " + age);
}
}
}
class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String reason) {
super(reason);
}
}
Checked Custom Exception
Derive from Exception when the caller must be forced to handle or declare the exception:
public class AgeService {
public static void main(String[] args) {
try {
registerAge(350);
} catch (InvalidAgeException e) {
System.err.println("Handled: " + e.getMessage());
}
}
public static void registerAge(int age) throws InvalidAgeException {
if (age > 0 && age < 120) {
System.out.println("Registration successful: " + age);
} else {
throw new InvalidAgeException("Age out of allowed range: " + age);
}
}
}
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
Practical Exception Handling Patterns
Log and notify the user
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateParser {
public static void main(String[] args) {
try {
parseCustomFormat("2025/03/15 09:30:00");
} catch (Exception e) {
e.printStackTrace();
System.out.println("Invalid date format provided.");
}
}
public static void parseCustomFormat(String input) throws Exception {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date result = formatter.parse(input);
System.out.println(result);
}
}
Attempt recovery
import java.util.Scanner;
public class PriceInput {
public static void main(String[] args) {
while (true) {
try {
capturePrice();
break;
} catch (Exception ex) {
System.out.println("Please enter a valid numeric price.");
}
}
}
public static void capturePrice() {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("Enter price: ");
double value = scanner.nextDouble();
if (value >= 0) {
System.out.println("Accepted: " + value);
break;
} else {
System.out.println("Price cannot be negative.");
}
}
}
}
Collections Framework
Single‑collection Hierarchy (Collection Interface)
- List – ordered, allows duplicates, index‑based access
ArrayListLinkedList
- Set – typically unordered, no duplicates, no index
HashSet(unordered, no duplicates)LinkedHashSet(insertion‑ordered, no duplicates)
TreeSet(sorted, no duplicates)
All collection types are generic.
Core Collection Methods
These methods are universal across single‑column collections:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class CollectionBasics {
public static void main(String[] args) {
Collection<String> items = new ArrayList<>();
items.add("Alpha");
items.add("Beta");
System.out.println(items);
items.clear();
System.out.println("Empty? " + items.isEmpty());
System.out.println("Size: " + items.size());
items.add("Gamma");
items.add("Delta");
items.add("Epsilon");
System.out.println("Contains Gamma? " + items.contains("Gamma"));
items.remove("Epsilon");
System.out.println(items);
Object[] array = items.toArray();
System.out.println(Arrays.toString(array));
String[] typedArray = items.toArray(new String[0]);
System.out.println(Arrays.toString(typedArray));
Collection<String> extra = new ArrayList<>();
extra.add("Zeta");
extra.add("Eta");
items.addAll(extra);
System.out.println(items);
}
}
Iteration Techniques
Three common approaches work for all Collection subtypes:
- Iterator
- Enhanced for‑loop
- Lambda
forEach
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class TraversalDemo {
public static void main(String[] args) {
Collection<String> langs = new ArrayList<>();
langs.add("Kotlin");
langs.add("Scala");
langs.add("Rust");
langs.add("Swift");
langs.add("Go");
// Iterator
Iterator<String> it = langs.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
System.out.println("---");
// Enhanced for
for (String lang : langs) {
System.out.println(lang);
}
System.out.println("---");
// Lambda
langs.forEach(System.out::println);
}
}
List Implementations
ArrayList
Built on a dynamic array. Fast random access, slow insertions/deletions in the middle. When the internal array fills up, it grows by 50%.
LinkedList
Uses a doubly‑linked list. Efficient for frequent additions/removals at both ends, but slower positional access.
import java.util.LinkedList;
import java.util.List;
public class LinkedListOps {
public static void main(String[] args) {
List<String> names = new LinkedList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// Iterate using index
for (int i = 0; i < names.size(); i++) {
System.out.println(names.get(i));
}
}
}
Set Implementations
HashSet
Relies on a hash table (array + linked list + red‑black tree from JDK 8). The default initial capacity is 16 with a load factor of 0.75. Duplicate elements are identified via hashCode() and equals(). For custom objects to be recognised as duplicates by content, both methods must be overridden:
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class UniqueUsers {
public static void main(String[] args) {
Set<User> users = new HashSet<>();
users.add(new User("john", "john@example.com"));
users.add(new User("john", "john@example.com"));
System.out.println("Size: " + users.size()); // 1
}
}
class User {
private String username;
private String email;
public User(String username, String email) {
this.username = username;
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(username, user.username) &&
Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(username, email);
}
}
LinkedHashSet
Maintains insertion order through a doubly‑linked list on top of the hash table.
TreeSet
Automatically sorts elements (ascending by default). Sorting can be defined via:
- A
Comparatorpassed to the constructor - Implementing
Comparablein the element class
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class SortedProducts {
public static void main(String[] args) {
// Comparator example
Set<Product> catalog = new TreeSet<>(
Comparator.comparingDouble(Product::getPrice)
);
catalog.add(new Product("Phone", 699.99));
catalog.add(new Product("Tablet", 499.99));
catalog.add(new Product("Laptop", 1299.99));
System.out.println(catalog);
}
}
class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public double getPrice() { return price; }
@Override
public String toString() {
return name + " $" + price;
}
}
Concurrent Modification
When modifying a collection while iterating, prefer Iterator.remove() or a regular for‑loop with index manipulation. Enhanced for and lambdas cannot handle structural changes safely.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class SafeRemoval {
public static void main(String[] args) {
List<String> tasks = new ArrayList<>();
tasks.add("taskA");
tasks.add("taskB");
tasks.add("taskC");
Iterator<String> iter = tasks.iterator();
while (iter.hasNext()) {
if (iter.next().equals("taskB")) {
iter.remove();
}
}
System.out.println(tasks);
}
}
Map (Key‑Value Pairs)
HashMap– unordered, keys unique, permits one null keyLinkedHashMap– insertion‑orderedTreeMap– sorted by keys
Common Map Operations
import java.util.HashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
Map<Integer, String> inventory = new HashMap<>();
inventory.put(101, "Monitor");
inventory.put(102, "Keyboard");
inventory.put(103, "Mouse");
inventory.put(null, "Unknown");
System.out.println(inventory.size());
System.out.println(inventory.get(102));
System.out.println(inventory.remove(103));
// Key set
for (Integer code : inventory.keySet()) {
System.out.print(code + " ");
}
System.out.println();
// Values
for (String item : inventory.values()) {
System.out.print(item + " ");
}
System.out.println();
// Entry set
for (Map.Entry<Integer, String> entry : inventory.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// Lambda
inventory.forEach((k, v) -> System.out.println(k + ": " + v));
}
}
Varargs (Variable Arguments)
A method can accept zero or more arguments of a specified type using Type... name. The varargs parameter is treated as an array and must be the last parameter in the method signature.
public class VarargsExample {
public static void main(String[] args) {
display(42);
display(10, 20, 30);
int[] data = {1, 2, 3, 4};
display(data);
}
public static void display(int... values) {
System.out.println(java.util.Arrays.toString(values));
}
}
File Handling and I/O Streams
The File Class
File from java.io represents file and directory paths. It does not read or write file content; it only provides metadata and file system operations.
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class FileInfo {
public static void main(String[] args) {
File document = new File("docs/readme.txt");
System.out.println("Exists: " + document.exists());
System.out.println("Is file: " + document.isFile());
System.out.println("Name: " + document.getName());
System.out.println("Bytes: " + document.length());
long modified = document.lastModified();
String dateStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(modified));
System.out.println("Last modified: " + dateStr);
System.out.println("Absolute path: " + document.getAbsolutePath());
}
}
import java.io.File;
import java.io.IOException;
public class FileManipulation {
public static void main(String[] args) throws IOException {
File notes = new File("notes.txt");
if (!notes.exists()) notes.createNewFile();
File dir = new File("output");
dir.mkdir();
File deepDir = new File("data/archive/2025");
deepDir.mkdirs();
System.out.println(notes.delete());
}
}
I/O Streams Essentials
Streams handle raw data or characters.
Byte Streams (FileInputStream / FileOutputStream)
Used for binary data (images, videos, any file).
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamCopy {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream("source.png");
FileOutputStream out = new FileOutputStream("target.png")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Character Streams (FileReader / FileWriter)
Designed for text files, handling character encoding internally.
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class TextFileCopy {
public static void main(String[] args) {
try (FileReader reader = new FileReader("input.txt");
FileWriter writer = new FileWriter("output.txt")) {
int ch;
while ((ch = reader.read()) != -1) {
writer.write(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Remember to flush or close a FileWriter to persist data.
Buffered Streams
Wrapping unbuffered streams with BufferedInputStream / BufferedOutputStream or BufferedReader / BufferedWriter adds an internal buffer (default 8 KB) that significantly improves performance.
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
public class BufferedTextExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("story.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("copy_story.txt"))) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Conversion Streams (InputStreamReader / OutputStreamWriter)
Bridge byte streams to character streams with explicit charset control, avoiding encoding mismatches.
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
public class CharsetAwareReader {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("document.txt"), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
Print Streams
PrintStream and PrintWriter provide convenient print/println methods and can redirect standard output.
import java.io.PrintStream;
public class OutputRedirect {
public static void main(String[] args) {
System.out.println("Console message");
try (PrintStream fileOut = new PrintStream("log.txt")) {
System.setOut(fileOut);
System.out.println("This goes to file");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
Data Streams
DataOutputStream / DataInputStream preserve primitive data types when writing/reading.
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class DataStreamDemo {
public static void main(String[] args) {
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("values.dat"));
DataInputStream dis = new DataInputStream(new FileInputStream("values.dat"))) {
dos.writeInt(1024);
dos.writeDouble(3.14);
dos.writeUTF("Hello");
System.out.println(dis.readInt());
System.out.println(dis.readDouble());
System.out.println(dis.readUTF());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Serialization
Objects can be persisted using ObjectOutputStream and restored with ObjectInputStream. The class must implement Serializable.
import java.io.*;
public class SerializationTest {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employee.ser"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("employee.ser"))) {
Employee emp = new Employee("Alice", 30);
oos.writeObject(emp);
Employee recovered = (Employee) ois.readObject();
System.out.println(recovered.getName() + " " + recovered.getAge());
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Employee implements Serializable {
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
Special File Formats
Properties Files
.properties files store key‑value pairs. The Properties class simplifies loading and saving.
import java.io.FileReader;
import java.io.FileWriter;
import java.util.Properties;
public class PropertiesManager {
public static void main(String[] args) throws Exception {
Properties config = new Properties();
config.load(new FileReader("settings.properties"));
System.out.println(config.getProperty("db.host"));
config.setProperty("app.timeout", "30");
config.store(new FileWriter("settings.properties"), "Application Config");
}
}
XML Processing with Dom4j
XML is widely used for configuration and data exchange. Dom4j offers a straightforward DOM‑based approach.
Parsing
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.util.List;
public class XmlReader {
public static void main(String[] args) throws Exception {
SAXReader reader = new SAXReader();
Document doc = reader.read("users.xml");
Element root = doc.getRootElement();
List<Element> userList = root.elements("user");
for (Element user : userList) {
String id = user.attributeValue("id");
String name = user.elementText("name");
System.out.println(id + ": " + name);
}
}
}
Writing
import java.io.BufferedWriter;
import java.io.FileWriter;
public class XmlWriter {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("data.xml"))) {
StringBuilder xml = new StringBuilder();
xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.append("<books>\n");
xml.append(" <book id=\"1\">\n");
xml.append(" <title>Effective Java</title>\n");
xml.append(" <price>45.0</price>\n");
xml.append(" </book>\n");
xml.append("</books>");
bw.write(xml.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Logging with Logback
Motivation
Logging captures runtime events for debugging and monitoring without modifying source code. Logback, built on SLF4J, is a modern, high‑performance logging framework.
Setup
Include slf4j-api, logback-core, and logback-classic on the classpath. Place logback.xml in the source root.
Quick Start
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PaymentService {
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
public static void main(String[] args) {
logger.info("Payment processing started");
try {
processTransaction(100, 0);
} catch (Exception e) {
logger.error("Transaction failed", e);
}
logger.info("Processing completed");
}
public static int processTransaction(int amount, int divisor) {
logger.debug("Amount: {}, Divisor: {}", amount, divisor);
int result = amount / divisor;
logger.info("Result: {}", result);
return result;
}
}
Configuration Snippet (logback.xml)
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
The global level can be set to TRACE, DEBUG, INFO, WARN, ERROR, or OFF to control verbosity.