Skip to main content

3º Java - The Java Engineer: Data, Testing & Tooling:

Chapter 1: Lambda Expressions Deep Dive

Technical Theory: Imperative vs Declarative

In software development, we often distinguish between two primary programming styles: imperative and declarative.

  • Imperative Programming: Focuses on how to achieve a result. You provide explicit, step-by-step instructions that directly change the program's state. Think of it like giving a robot precise commands for each movement.
  • Declarative Programming: Focuses on what result you want, without necessarily detailing the exact steps. You describe the desired outcome, and the system figures out the how. Think of it like ordering a coffee – you state "I want a latte," and the barista handles the process.

Lambda expressions in Java lean heavily towards the declarative style. They allow us to treat functionality as a method argument or code as data, making our code more expressive and concise, especially when working with functional interfaces.

The syntax of a lambda expression is (parameters) -> {body}.

  • parameters: The input parameters, similar to method parameters.
  • ->: The lambda arrow operator, separating parameters from the body.
  • body: The logic to be executed. This can be a single expression (which is implicitly returned) or a block of statements.

Functional interfaces are crucial here; they are interfaces with exactly one abstract method. Lambdas provide an inline implementation for these single-method interfaces. Common built-in functional interfaces include Predicate<T> (takes T, returns boolean), and Consumer<T> (takes T, returns void).

Professional Code

Let's see how lambdas replace verbose anonymous inner classes.

Example 1: Implementing Runnable

// Before: Anonymous Inner Class
class TaskRunnerBefore {
    public void execute() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Executing task (old way)");
            }
        });
        thread.start();
    }
}

// After: Lambda Expression
class TaskRunnerAfter {
    public void execute() {
        Thread thread = new Thread(() -> System.out.println("Executing task (new way)"));
        thread.start();
    }
}

public class LambdaRunnableExample {
    public static void main(String[] args) {
        new TaskRunnerBefore().execute();
        new TaskRunnerAfter().execute();
    }
}

Example 2: Filtering a list with Predicate

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}

// Before: Custom filtering logic
class UserFilterBefore {
    public List<User> filterUsersByAge(List<User> users, int minAge) {
        List<User> filteredUsers = new ArrayList<>();
        for (User user : users) {
            if (user.getAge() >= minAge) {
                filteredUsers.add(user);
            }
        }
        return filteredUsers;
    }
}

// After: Using Predicate with Lambda
class UserFilterAfter {
    public List<User> filterUsers(List<User> users, Predicate<User> predicate) {
        List<User> filteredUsers = new ArrayList<>();
        for (User user : users) {
            if (predicate.test(user)) {
                filteredUsers.add(user);
            }
        }
        return filteredUsers;
    }
}

public class PredicateLambdaExample {
    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        users.add(new User("Alice", 25));
        users.add(new User("Bob", 30));
        users.add(new User("Charlie", 20));

        System.out.println("--- Filtering Before ---");
        UserFilterBefore filterBefore = new UserFilterBefore();
        List<User> oldUsersBefore = filterBefore.filterUsersByAge(users, 28);
        oldUsersBefore.forEach(System.out::println);

        System.out.println("--- Filtering After ---");
        UserFilterAfter filterAfter = new UserFilterAfter();
        // Lambda for users older than 28
        List<User> oldUsersAfter = filterAfter.filterUsers(users, user -> user.getAge() > 28);
        oldUsersAfter.forEach(System.out::println);

        // Another lambda for users whose name starts with 'A'
        List<User> usersWithNameA = filterAfter.filterUsers(users, user -> user.getName().startsWith("A"));
        System.out.println("\nUsers with name starting with 'A':");
        usersWithNameA.forEach(System.out::println);
    }
}

Example 3: Processing elements with Consumer

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

// Before: Traditional loop for processing
class ListProcessorBefore {
    public void process(List<String> items) {
        for (String item : items) {
            System.out.println("Processing item (old way): " + item);
        }
    }
}

// After: Using Consumer with Lambda
class ListProcessorAfter {
    public void process(List<String> items, Consumer<String> consumer) {
        for (String item : items) {
            consumer.accept(item);
        }
    }
}

public class ConsumerLambdaExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Java", "Kotlin", "Scala");

        System.out.println("--- Processing Before ---");
        ListProcessorBefore processorBefore = new ListProcessorBefore();
        processorBefore.process(names);

        System.out.println("--- Processing After ---");
        ListProcessorAfter processorAfter = new ListProcessorAfter();
        // Lambda for printing each item
        processorAfter.process(names, item -> System.out.println("Processing item (new way): " + item.toUpperCase()));

        // Another lambda for custom action
        System.out.println("\n--- Custom Processing ---");
        processorAfter.process(names, item -> {
            System.out.println("Item length: " + item.length());
        });
    }
}

Clean Code Tip: Less code = Fewer bugs Lambda expressions significantly reduce boilerplate code, especially when dealing with functional interfaces. Less code means less surface area for bugs to hide, and it's generally easier to read and maintain concise, focused pieces of logic.

Exercise & Solution

Exercise: Given a list of String objects, filter out all strings that have a length less than 5 characters using a Predicate lambda. Then, print the filtered strings.

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class LambdaExercise {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("apple");
        words.add("cat");
        words.add("banana");
        words.add("dog");
        words.add("elephant");
        words.add("go");

        System.out.println("Original words: " + words);

        // Your code here: Filter words with length < 5 using a Predicate lambda
        // And store them in a new list called 'longWords'
        List<String> longWords = new ArrayList<>();

        // ...
        // Solution placeholder
        // ...

        System.out.println("Words with length >= 5: " + longWords);
    }
}

Solution:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors; // Will be covered in Chapter 3, but useful here

public class LambdaExerciseSolution {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("apple");
        words.add("cat");
        words.add("banana");
        words.add("dog");
        words.add("elephant");
        words.add("go");

        System.out.println("Original words: " + words);

        // Solution using Predicate and a loop
        List<String> longWords = new ArrayList<>();
        Predicate<String> isLongEnough = word -> word.length() >= 5;
        for (String word : words) {
            if (isLongEnough.test(word)) {
                longWords.add(word);
            }
        }

        // Alternative (more modern) solution using Streams (covered in next chapters)
        // List<String> longWords = words.stream()
        //                               .filter(word -> word.length() >= 5)
        //                               .collect(Collectors.toList());

        System.out.println("Words with length >= 5: " + longWords);
    }
}

Chapter 2: The Stream API (Filter & Map)

Technical Theory: How to process collections like a pro

The Java Stream API, introduced in Java 8, provides a powerful and declarative way to process collections of data. A stream is a sequence of elements that supports sequential and parallel aggregate operations. Crucially, streams are:

  • Not a data structure: They don't store data themselves. They act as a pipeline for processing data from a source (like a List, Set, array, or I/O channel).
  • Functional in nature: Operations on streams produce a new stream without modifying the underlying data source (non-mutating).
  • Lazy: Intermediate operations (like filter, map) are not executed until a terminal operation (like collect, forEach) is invoked.

filter() and map() are two of the most fundamental intermediate operations in the Stream API.

  • .filter(Predicate<T> predicate): Takes a Predicate (a lambda that returns a boolean) and returns a new stream containing only the elements that satisfy the predicate. It's like sifting through a collection, keeping only what matches your criteria.
  • .map(Function<T, R> mapper): Takes a Function (a lambda that transforms an element of type T to type R) and returns a new stream where each element has been transformed. It's like taking a list of items and converting each one into something else (e.g., users to names, numbers to squares).

Professional Code

We'll use our User class from the previous chapter.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class User {
    private String name;
    private int age;
    private String email;

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + ", age=" + age + ", email='" + email + '\'' + '}';
    }
}

public class StreamFilterMapExample {

    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        users.add(new User("Alice", 25, "alice@example.com"));
        users.add(new User("Bob", 30, "bob@example.com"));
        users.add(new User("Charlie", 20, "charlie@example.com"));
        users.add(new User("David", 35, "david@example.com"));
        users.add(new User("Eve", 28, "eve@example.com"));

        // Example 1: Filtering a list of 'Users' by age
        System.out.println("--- Filtering Users by Age ---");

        // Before: Imperative style
        List<User> youngUsersBefore = new ArrayList<>();
        for (User user : users) {
            if (user.getAge() < 30) {
                youngUsersBefore.add(user);
            }
        }
        System.out.println("Before (age < 30): " + youngUsersBefore);

        // After: Stream API with filter()
        List<User> youngUsersAfter = users.stream()
                                         .filter(user -> user.getAge() < 30)
                                         .collect(Collectors.toList());
        System.out.println("After (age < 30):  " + youngUsersAfter);

        // Example 2: Transforming a list of 'Users' into a list of 'Names'
        System.out.println("\n--- Transforming Users to Names ---");

        // Before: Imperative style
        List<String> userNamesBefore = new ArrayList<>();
        for (User user : users) {
            userNamesBefore.add(user.getName());
        }
        System.out.println("Before (names): " + userNamesBefore);

        // After: Stream API with map()
        List<String> userNamesAfter = users.stream()
                                          .map(User::getName) // Method reference, covered in Chapter 6, but common here. Equivalent to user -> user.getName()
                                          .collect(Collectors.toList());
        System.out.println("After (names):  " + userNamesAfter);

        // Example 3: Chaining Filter and Map - Filter by age, then get names
        System.out.println("\n--- Filter by Age AND Get Names ---");

        // Before: Imperative style (multiple loops or nested conditions)
        List<String> namesOfAdultUsersBefore = new ArrayList<>();
        for (User user : users) {
            if (user.getAge() >= 25) { // Filter
                namesOfAdultUsersBefore.add(user.getName()); // Map
            }
        }
        System.out.println("Before (age >= 25 names): " + namesOfAdultUsersBefore);

        // After: Stream API with chained filter() and map()
        List<String> namesOfAdultUsersAfter = users.stream()
                                                  .filter(user -> user.getAge() >= 25) // Intermediate operation
                                                  .map(User::getName)                 // Intermediate operation
                                                  .collect(Collectors.toList());      // Terminal operation
        System.out.println("After (age >= 25 names):  " + namesOfAdultUsersAfter);
    }
}

Clean Code Tip: Stream operations are declarative and compose well Instead of writing explicit loops and conditional logic (imperative), streams allow you to declare what you want to achieve (filter, map, sort). This makes your code much more readable, especially when chaining multiple operations. Each operation focuses on a single responsibility, leading to cleaner, more maintainable code.

Exercise & Solution

Exercise: Given a list of Product objects, filter for products that are inStock and have a price greater than 50. Then, transform these filtered products into a list of Strings, where each string combines the product's name and price (e.g., "Laptop ($1200.0)").

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class Product {
    private String name;
    private double price;
    private boolean inStock;

    public Product(String name, double price, boolean inStock) {
        this.name = name;
        this.price = price;
        this.inStock = inStock;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    public boolean isInStock() {
        return inStock;
    }

    @Override
    public String toString() {
        return "Product{" + "name='" + name + '\'' + ", price=" + price + ", inStock=" + inStock + '}';
    }
}

public class StreamFilterMapExercise {
    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Product("Keyboard", 75.0, true));
        products.add(new Product("Mouse", 25.0, true));
        products.add(new Product("Monitor", 150.0, false));
        products.add(new Product("Laptop", 1200.0, true));
        products.add(new Product("Webcam", 40.0, true));
        products.add(new Product("Headphones", 90.0, false));

        System.out.println("Original products: " + products);

        // Your code here: Filter in-stock products with price > 50,
        // then map to "Name ($Price)" strings.
        List<String> highValueInStockProducts = new ArrayList<>();

        // ...
        // Solution placeholder
        // ...

        System.out.println("High-value in-stock products (name and price): " + highValueInStockProducts);
    }
}

Solution:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class Product {
    private String name;
    private double price;
    private boolean inStock;

    public Product(String name, double price, boolean inStock) {
        this.name = name;
        this.price = price;
        this.inStock = inStock;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    public boolean isInStock() {
        return inStock;
    }

    @Override
    public String toString() {
        return "Product{" + "name='" + name + '\'' + ", price=" + price + ", inStock=" + inStock + '}';
    }
}

public class StreamFilterMapExerciseSolution {
    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Product("Keyboard", 75.0, true));
        products.add(new Product("Mouse", 25.0, true));
        products.add(new Product("Monitor", 150.0, false));
        products.add(new Product("Laptop", 1200.0, true));
        products.add(new Product("Webcam", 40.0, true));
        products.add(new Product("Headphones", 90.0, false));

        System.out.println("Original products: " + products);

        List<String> highValueInStockProducts = products.stream()
                                                        .filter(product -> product.isInStock() && product.getPrice() > 50.0)
                                                        .map(product -> product.getName() + " ($" + product.getPrice() + ")")
                                                        .collect(Collectors.toList());

        System.out.println("High-value in-stock products (name and price): " + highValueInStockProducts);
    }
}

Chapter 3: Stream Terminal Operations

Technical Theory: Triggering the Stream Pipeline

Intermediate stream operations (like filter, map, sorted) are lazy. They return a new stream and don't perform any actual computation until a terminal operation is invoked. A terminal operation consumes the stream and produces a result or a side effect. After a terminal operation, the stream cannot be reused.

Let's explore some common terminal operations:

  • .collect(Collector<T, A, R> collector): This is one of the most powerful terminal operations. It takes a Collector as an argument, which defines how the elements in the stream should be accumulated into a final result. Collectors is a utility class providing many predefined collectors, such as Collectors.toList(), Collectors.toSet(), Collectors.joining(), Collectors.groupingBy(), etc.
  • .count(): Returns the number of elements in the stream as a long.
  • .forEach(Consumer<T> action): Performs an action for each element in the stream. It's often used for side effects, like printing elements to the console.

Professional Code

We'll continue using our User class.

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

class User {
    private String name;
    private int age;
    private String city;

    public User(String name, int age, String city) {
        this.name = name;
        this.age = age;
        this.city = city;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public String getCity() {
        return city;
    }

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + ", age=" + age + ", city='" + city + '\'' + '}';
    }
}

public class StreamTerminalOperationsExample {

    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        users.add(new User("Alice", 25, "New York"));
        users.add(new User("Bob", 30, "London"));
        users.add(new User("Charlie", 20, "New York"));
        users.add(new User("David", 35, "Paris"));
        users.add(new User("Eve", 28, "London"));
        users.add(new User("Frank", 40, "New York"));

        System.out.println("Original Users: " + users);

        // Example 1: Collecting results into a List
        System.out.println("\n--- Collect to List ---");

        // Before: Manual iteration and adding
        List<String> namesOfAdultsBefore = new ArrayList<>();
        for (User user : users) {
            if (user.getAge() >= 25) {
                namesOfAdultsBefore.add(user.getName());
            }
        }
        System.out.println("Adult names (Before): " + namesOfAdultsBefore);

        // After: Using .filter().map().collect(Collectors.toList())
        List<String> namesOfAdultsAfter = users.stream()
                                              .filter(user -> user.getAge() >= 25)
                                              .map(User::getName)
                                              .collect(Collectors.toList());
        System.out.println("Adult names (After):  " + namesOfAdultsAfter);

        // Example 2: Counting elements
        System.out.println("\n--- Counting Elements ---");

        // Before: Manual counter
        int nyUsersCountBefore = 0;
        for (User user : users) {
            if (user.getCity().equals("New York")) {
                nyUsersCountBefore++;
            }
        }
        System.out.println("Users in New York (Before): " + nyUsersCountBefore);

        // After: Using .filter().count()
        long nyUsersCountAfter = users.stream()
                                      .filter(user -> user.getCity().equals("New York"))
                                      .count();
        System.out.println("Users in New York (After):  " + nyUsersCountAfter);

        // Example 3: Performing an action for each element
        System.out.println("\n--- For Each Element ---");

        // Before: Enhanced for loop
        System.out.println("All users (Before):");
        for (User user : users) {
            System.out.println("- " + user.getName() + " is " + user.getAge());
        }

        // After: Using .forEach()
        System.out.println("All users (After):");
        users.stream()
             .forEach(user -> System.out.println("- " + user.getName() + " is " + user.getAge()));

        // Bonus: Grouping users by city using Collectors.groupingBy
        System.out.println("\n--- Grouping Users by City ---");
        Map<String, List<User>> usersByCity = users.stream()
                                                    .collect(Collectors.groupingBy(User::getCity));
        usersByCity.forEach((city, userList) -> {
            System.out.println("City: " + city);
            userList.forEach(user -> System.out.println("  - " + user.getName()));
        });
    }
}

Clean Code Tip: Use appropriate terminal operations for specific needs Don't collect to a list if all you need is a count. Don't forEach if you need to build a new collection. Choosing the right terminal operation makes your intent clear and often leads to more efficient code by avoiding unnecessary intermediate data structures.

Exercise & Solution

Exercise: You have a list of Order objects.

  1. Count how many orders have a status of "PENDING".
  2. Collect the customerName of all orders that have a status of "COMPLETED" into a List<String>.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class Order {
    private String orderId;
    private String customerName;
    private String status;
    private double totalAmount;

    public Order(String orderId, String customerName, String status, double totalAmount) {
        this.orderId = orderId;
        this.customerName = customerName;
        this.status = status;
        this.totalAmount = totalAmount;
    }

    public String getOrderId() {
        return orderId;
    }

    public String getCustomerName() {
        return customerName;
    }

    public String getStatus() {
        return status;
    }

    public double getTotalAmount() {
        return totalAmount;
    }

    @Override
    public String toString() {
        return "Order{" + "orderId='" + orderId + '\'' + ", customerName='" + customerName + '\'' + ", status='" + status + '\'' + ", totalAmount=" + totalAmount + '}';
    }
}

public class StreamTerminalExercise {
    public static void main(String[] args) {
        List<Order> orders = new ArrayList<>();
        orders.add(new Order("ORD001", "Alice", "PENDING", 150.0));
        orders.add(new Order("ORD002", "Bob", "COMPLETED", 200.0));
        orders.add(new Order("ORD003", "Charlie", "PENDING", 75.0));
        orders.add(new Order("ORD004", "David", "COMPLETED", 300.0));
        orders.add(new Order("ORD005", "Eve", "PROCESSING", 100.0));
        orders.add(new Order("ORD006", "Frank", "COMPLETED", 50.0));

        System.out.println("All Orders: " + orders);

        // Your code here:
        // 1. Count pending orders
        long pendingOrdersCount = 0;

        // 2. Collect customer names of completed orders
        List<String> completedOrderCustomerNames = new ArrayList<>();

        // ...
        // Solution placeholder
        // ...

        System.out.println("\nNumber of PENDING orders: " + pendingOrdersCount);
        System.out.println("Customer names for COMPLETED orders: " + completedOrderCustomerNames);
    }
}

Solution:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class Order {
    private String orderId;
    private String customerName;
    private String status;
    private double totalAmount;

    public Order(String orderId, String customerName, String status, double totalAmount) {
        this.orderId = orderId;
        this.customerName = customerName;
        this.status = status;
        this.totalAmount = totalAmount;
    }

    public String getOrderId() {
        return orderId;
    }

    public String getCustomerName() {
        return customerName;
    }

    public String getStatus() {
        return status;
    }

    public double getTotalAmount() {
        return totalAmount;
    }

    @Override
    public String toString() {
        return "Order{" + "orderId='" + orderId + '\'' + ", customerName='" + customerName + '\'' + ", status='" + status + '\'' + ", totalAmount=" + totalAmount + '}';
    }
}

public class StreamTerminalExerciseSolution {
    public static void main(String[] args) {
        List<Order> orders = new ArrayList<>();
        orders.add(new Order("ORD001", "Alice", "PENDING", 150.0));
        orders.add(new Order("ORD002", "Bob", "COMPLETED", 200.0));
        orders.add(new Order("ORD003", "Charlie", "PENDING", 75.0));
        orders.add(new Order("ORD004", "David", "COMPLETED", 300.0));
        orders.add(new Order("ORD005", "Eve", "PROCESSING", 100.0));
        orders.add(new Order("ORD006", "Frank", "COMPLETED", 50.0));

        System.out.println("All Orders: " + orders);

        // 1. Count pending orders
        long pendingOrdersCount = orders.stream()
                                        .filter(order -> "PENDING".equals(order.getStatus()))
                                        .count();

        // 2. Collect customer names of completed orders
        List<String> completedOrderCustomerNames = orders.stream()
                                                         .filter(order -> "COMPLETED".equals(order.getStatus()))
                                                         .map(Order::getCustomerName)
                                                         .collect(Collectors.toList());

        System.out.println("\nNumber of PENDING orders: " + pendingOrdersCount);
        System.out.println("Customer names for COMPLETED orders: " + completedOrderCustomerNames);
    }
}

Chapter 4: Sorting with Streams

Technical Theory: Sorting Made Easy

The sorted() intermediate operation in the Stream API allows you to sort elements within a stream. It comes in two main forms:

  • .sorted(): Sorts elements according to their natural order. This requires the elements to implement the Comparable interface. For example, String and wrapper classes like Integer already implement Comparable.
  • .sorted(Comparator<T> comparator): Sorts elements according to the order induced by the provided Comparator. This is far more common for custom objects, where you define your own sorting logic using a lambda expression or method reference.

The Comparator interface is a functional interface, making it a perfect candidate for lambdas. The Comparator.comparing() static method is incredibly useful for creating comparators based on extracting a comparable key. You can also chain comparators using thenComparing().

Professional Code

We'll use a slightly modified User class to demonstrate sorting.

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class User implements Comparable<User> { // Implementing Comparable for natural order by name
    private String name;
    private int age;
    private String department;

    public User(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getDepartment() {
        return department;
    }

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + ", age=" + age + ", department='" + department + '\'' + '}';
    }

    // Natural order by name (for .sorted() without a comparator)
    @Override
    public int compareTo(User other) {
        return this.name.compareTo(other.name);
    }
}

public class StreamSortingExample {

    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        users.add(new User("Alice", 30, "HR"));
        users.add(new User("Charlie", 25, "Engineering"));
        users.add(new User("Bob", 35, "HR"));
        users.add(new User("David", 22, "Marketing"));
        users.add(new User("Eve", 30, "Engineering"));

        System.out.println("Original Users:");
        users.forEach(System.out::println);

        // Example 1: Sorting a list of objects by natural order (if Comparable)
        System.out.println("\n--- Sorting by Natural Order (User Name) ---");
        // Before: Collections.sort with custom object
        List<User> sortedUsersNaturalBefore = new ArrayList<>(users);
        sortedUsersNaturalBefore.sort(new Comparator<User>() {
            @Override
            public int compare(User u1, User u2) {
                return u1.getName().compareTo(u2.getName());
            }
        });
        System.out.println("Before (by Name):");
        sortedUsersNaturalBefore.forEach(System.out::println);

        // After: Stream API with .sorted() (relies on User implementing Comparable<User>)
        List<User> sortedUsersNaturalAfter = users.stream()
                                                  .sorted() // Uses User's compareTo method
                                                  .collect(Collectors.toList());
        System.out.println("After (by Name):");
        sortedUsersNaturalAfter.forEach(System.out::println);


        // Example 2: Sorting User objects by age (custom comparator)
        System.out.println("\n--- Sorting Users by Age Ascending ---");

        // Before: Anonymous Comparator
        List<User> sortedByAgeBefore = new ArrayList<>(users);
        sortedByAgeBefore.sort(new Comparator<User>() {
            @Override
            public int compare(User u1, User u2) {
                return Integer.compare(u1.getAge(), u2.getAge());
            }
        });
        System.out.println("Before (by Age):");
        sortedByAgeBefore.forEach(System.out::println);

        // After: Stream API with .sorted(Comparator.comparingInt)
        List<User> sortedByAgeAfter = users.stream()
                                           .sorted(Comparator.comparingInt(User::getAge))
                                           .collect(Collectors.toList());
        System.out.println("After (by Age):");
        sortedByAgeAfter.forEach(System.out::println);

        // Example 3: Sorting User objects by department then by age (chained comparators)
        System.out.println("\n--- Sorting by Department then Age Descending ---");

        // Before: Chained comparators (verbose)
        List<User> sortedByDeptAgeBefore = new ArrayList<>(users);
        sortedByDeptAgeBefore.sort(new Comparator<User>() {
            @Override
            public int compare(User u1, User u2) {
                int deptCompare = u1.getDepartment().compareTo(u2.getDepartment());
                if (deptCompare != 0) {
                    return deptCompare;
                }
                // If departments are same, sort by age descending
                return Integer.compare(u2.getAge(), u1.getAge()); // u2 vs u1 for descending
            }
        });
        System.out.println("Before (by Department then Age Desc):");
        sortedByDeptAgeBefore.forEach(System.out::println);

        // After: Stream API with Comparator.comparing().thenComparing()
        List<User> sortedByDeptAgeAfter = users.stream()
                                              .sorted(Comparator.comparing(User::getDepartment)
                                                                .thenComparing(Comparator.comparingInt(User::getAge).reversed()))
                                              .collect(Collectors.toList());
        System.out.println("After (by Department then Age Desc):");
        sortedByDeptAgeAfter.forEach(System.out::println);
    }
}

Clean Code Tip: Comparator.comparing() and thenComparing() for fluent sorting These static methods on the Comparator interface allow you to build complex sorting logic in a highly readable and fluent way, avoiding nested if statements or anonymous inner classes. Always prefer Comparator.comparing() for its conciseness and clarity.

Exercise & Solution

Exercise: Given a list of Employee objects:

  1. Sort the employees first by their department alphabetically (ascending).
  2. If employees are in the same department, sort them by salary in descending order.
  3. Collect the sorted employees into a new list.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Employee {
    private String name;
    private String department;
    private double salary;

    public Employee(String name, String department, double salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public String getDepartment() {
        return department;
    }

    public double getSalary() {
        return salary;
    }

    @Override
    public String toString() {
        return "Employee{" + "name='" + name + '\'' + ", department='" + department + '\'' + ", salary=" + salary + '}';
    }
}

public class StreamSortingExercise {
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("Alice", "HR", 60000.0));
        employees.add(new Employee("Bob", "Engineering", 90000.0));
        employees.add(new Employee("Charlie", "HR", 75000.0));
        employees.add(new Employee("David", "Engineering", 80000.0));
        employees.add(new Employee("Eve", "Marketing", 65000.0));
        employees.add(new Employee("Frank", "Engineering", 95000.0));

        System.out.println("Original Employees:");
        employees.forEach(System.out::println);

        // Your code here: Sort employees by department (asc) then salary (desc)
        List<Employee> sortedEmployees = new ArrayList<>();

        // ...
        // Solution placeholder
        // ...

        System.out.println("\nSorted Employees (Department ASC, Salary DESC):");
        sortedEmployees.forEach(System.out::println);
    }
}

Solution:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Employee {
    private String name;
    private String department;
    private double salary;

    public Employee(String name, String department, double salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public String getDepartment() {
        return department;
    }

    public double getSalary() {
        return salary;
    }

    @Override
    public String toString() {
        return "Employee{" + "name='" + name + '\'' + ", department='" + department + '\'' + ", salary=" + salary + '}';
    }
}

public class StreamSortingExerciseSolution {
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("Alice", "HR", 60000.0));
        employees.add(new Employee("Bob", "Engineering", 90000.0));
        employees.add(new Employee("Charlie", "HR", 75000.0));
        employees.add(new Employee("David", "Engineering", 80000.0));
        employees.add(new Employee("Eve", "Marketing", 65000.0));
        employees.add(new Employee("Frank", "Engineering", 95000.0));

        System.out.println("Original Employees:");
        employees.forEach(System.out::println);

        List<Employee> sortedEmployees = employees.stream()
                                                  .sorted(Comparator.comparing(Employee::getDepartment) // Sort by department ascending
                                                                    .thenComparing(Comparator.comparingDouble(Employee::getSalary).reversed())) // Then by salary descending
                                                  .collect(Collectors.toList());

        System.out.println("\nSorted Employees (Department ASC, Salary DESC):");
        sortedEmployees.forEach(System.out::println);
    }
}

Chapter 5: The Optional Class

Technical Theory: The ultimate weapon against NullPointerException

NullPointerException (NPE) is one of the most common and frustrating runtime errors in Java. It occurs when you try to use a reference that points to null as if it were a valid object. Java 8 introduced the Optional<T> class to help developers design APIs that explicitly declare when a value might be absent, forcing consumers of these APIs to handle the absence, thus preventing NPEs.

Optional<T> is a container object that may or may not contain a non-null value.

  • If a value is present, Optional acts as a wrapper for that value.
  • If a value is absent, the Optional is empty.

Key methods of Optional:

  • Optional.of(T value): Creates an Optional with the given non-null value. Throws NullPointerException if value is null.
  • Optional.ofNullable(T value): Creates an Optional with the given value, or an empty Optional if the value is null. This is the safer choice when the value might be null.
  • isPresent(): Returns true if a value is present, false otherwise.
  • isEmpty(): Returns true if no value is present (Java 11+).
  • get(): Returns the value if present, otherwise throws NoSuchElementException. Use with caution, similar to a null check.
  • orElse(T other): Returns the value if present, otherwise returns other (a default value).
  • orElseGet(Supplier<? extends T> other): Returns the value if present, otherwise invokes the Supplier to get a default value. Useful when the default value computation is expensive and should only occur if needed.
  • orElseThrow(Supplier<? extends X> exceptionSupplier): Returns the value if present, otherwise throws an exception created by the Supplier.
  • map(Function<? super T, ? extends U> mapper): If a value is present, applies the mapping function to it, and if the result is non-null, returns an Optional describing the result. Otherwise returns an empty Optional. This allows chaining transformations without explicit null checks.
  • ifPresent(Consumer<? super T> action): If a value is present, performs the given action with the value, otherwise does nothing.

Professional Code

We'll use a User class with an optional email.

import java.util.Optional;

class User {
    private String name;
    private Optional<String> email; // Email is optional

    public User(String name, String email) {
        this.name = name;
        this.email = Optional.ofNullable(email); // Use ofNullable to handle potentially null emails
    }

    public String getName() {
        return name;
    }

    public Optional<String> getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + ", email=" + email.orElse("N/A") + '}';
    }
}

public class OptionalExample {

    public static void main(String[] args) {
        User user1 = new User("Alice", "alice@example.com");
        User user2 = new User("Bob", null); // Bob doesn't have an email

        // Example 1: Basic usage with .orElse()
        System.out.println("--- Handling Optional with .orElse() ---");

        // Before: Traditional null check
        String email1Before = user1.getEmail().isPresent() ? user1.getEmail().get() : "No email provided";
        String email2Before = user2.getEmail().isPresent() ? user2.getEmail().get() : "No email provided";
        System.out.println("Alice's email (Before): " + email1Before);
        System.out.println("Bob's email (Before):   " + email2Before);

        // After: Using Optional.orElse()
        String email1After = user1.getEmail().orElse("No email provided");
        String email2After = user2.getEmail().orElse("No email provided");
        System.out.println("Alice's email (After): " + email1After);
        System.out.println("Bob's email (After):   " + email2After);

        // Example 2: Transforming value with .map()
        System.out.println("\n--- Transforming Optional with .map() ---");

        // Before: Null checks before transformation
        String domain1Before = "N/A";
        if (user1.getEmail().isPresent()) {
            String fullEmail = user1.getEmail().get();
            if (fullEmail.contains("@")) {
                domain1Before = fullEmail.substring(fullEmail.indexOf("@") + 1);
            }
        }
        System.out.println("Alice's email domain (Before): " + domain1Before);

        String domain2Before = "N/A"; // Bob has no email, so domain remains N/A
        System.out.println("Bob's email domain (Before):   " + domain2Before);

        // After: Using Optional.map()
        Optional<String> domain1After = user1.getEmail()
                                            .map(email -> email.substring(email.indexOf("@") + 1));
        System.out.println("Alice's email domain (After): " + domain1After.orElse("N/A"));

        Optional<String> domain2After = user2.getEmail()
                                            .map(email -> email.substring(email.indexOf("@") + 1));
        System.out.println("Bob's email domain (After):   " + domain2After.orElse("N/A"));


        // Example 3: Performing actions with .ifPresent()
        System.out.println("\n--- Performing actions with .ifPresent() ---");

        // Before: Conditional action
        System.out.println("Users with email (Before):");
        if (user1.getEmail().isPresent()) {
            System.out.println("User " + user1.getName() + " has email: " + user1.getEmail().get());
        }
        if (user2.getEmail().isPresent()) { // This block won't execute
            System.out.println("User " + user2.getName() + " has email: " + user2.getEmail().get());
        }

        // After: Using Optional.ifPresent()
        System.out.println("Users with email (After):");
        user1.getEmail().ifPresent(email -> System.out.println("User " + user1.getName() + " has email: " + email));
        user2.getEmail().ifPresent(email -> System.out.println("User " + user2.getName() + " has email: " + email)); // Does nothing
    }
}

Clean Code Tip: Use Optional for return types where a value might be absent This makes nullability explicit in your API contracts, forcing callers to consider the absence of a value. Avoid using Optional as a field type or as a method parameter, as it adds unnecessary overhead and doesn't achieve its primary goal of preventing NPEs in those contexts.

Exercise & Solution

Exercise: Create a UserRepository class with a method findUserById(long id) that simulates retrieving a user, which may or may not exist. This method should return Optional<User>. In your main method:

  1. Call findUserById for an existing user (ID 1L) and, if present, print their name and email. If email is not present, print "No Email Provided".
  2. Call findUserById for a non-existing user (ID 99L) and print "User not found" if absent.
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

class User {
    private long id;
    private String name;
    private Optional<String> email;

    public User(long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = Optional.ofNullable(email);
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Optional<String> getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "User{" + "id=" + id + ", name='" + name + '\'' + ", email=" + email.orElse("N/A") + '}';
    }
}

class UserRepository {
    private Map<Long, User> users = new HashMap<>();

    public UserRepository() {
        users.put(1L, new User(1L, "Alice", "alice@example.com"));
        users.put(2L, new User(2L, "Bob", null)); // Bob has no email
        users.put(3L, new User(3L, "Charlie", "charlie@example.com"));
    }

    // Your code here: Method to find user by ID, returning Optional<User>
    public Optional<User> findUserById(long id) {
        // ...
        // Solution placeholder
        return Optional.empty(); // Placeholder
        // ...
    }
}

public class OptionalExercise {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepository();

        // Scenario 1: Existing user with email
        System.out.println("--- Scenario 1: Existing user (ID 1) ---");
        // Your code here: Retrieve user 1, print name and email (or "No Email Provided")
        // ...

        // Scenario 2: Existing user without email (ID 2)
        System.out.println("\n--- Scenario 2: Existing user (ID 2, no email) ---");
        // Your code here: Retrieve user 2, print name and email (or "No Email Provided")
        // ...

        // Scenario 3: Non-existing user
        System.out.println("\n--- Scenario 3: Non-existing user (ID 99) ---");
        // Your code here: Retrieve user 99, print "User not found" if absent
        // ...
    }
}

Solution:

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

class User {
    private long id;
    private String name;
    private Optional<String> email;

    public User(long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = Optional.ofNullable(email);
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Optional<String> getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "User{" + "id=" + id + ", name='" + name + '\'' + ", email=" + email.orElse("N/A") + '}';
    }
}

class UserRepository {
    private Map<Long, User> users = new HashMap<>();

    public UserRepository() {
        users.put(1L, new User(1L, "Alice", "alice@example.com"));
        users.put(2L, new User(2L, "Bob", null)); // Bob has no email
        users.put(3L, new User(3L, "Charlie", "charlie@example.com"));
    }

    public Optional<User> findUserById(long id) {
        return Optional.ofNullable(users.get(id));
    }
}

public class OptionalExerciseSolution {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepository();

        // Scenario 1: Existing user with email (ID 1)
        System.out.println("--- Scenario 1: Existing user (ID 1) ---");
        Optional<User> user1 = userRepository.findUserById(1L);
        user1.ifPresent(u -> {
            System.out.println("User Name: " + u.getName());
            System.out.println("User Email: " + u.getEmail().orElse("No Email Provided"));
        });

        // Scenario 2: Existing user without email (ID 2)
        System.out.println("\n--- Scenario 2: Existing user (ID 2, no email) ---");
        Optional<User> user2 = userRepository.findUserById(2L);
        user2.ifPresent(u -> {
            System.out.println("User Name: " + u.getName());
            System.out.println("User Email: " + u.getEmail().orElse("No Email Provided"));
        });

        // Scenario 3: Non-existing user (ID 99)
        System.out.println("\n--- Scenario 3: Non-existing user (ID 99) ---");
        Optional<User> user99 = userRepository.findUserById(99L);
        System.out.println(user99.map(u -> "Found user: " + u.getName()).orElse("User not found"));
    }
}

Chapter 6: Method References

Technical Theory: Cleaning up your lambdas

Method references are a special syntax in Java 8 that provide a shorthand for lambda expressions, making your code even more concise and readable in specific situations. They are used when a lambda expression just calls an existing method. Instead of providing the lambda body, you simply refer to the method by name.

A method reference is of the form ClassName::methodName or objectName::methodName.

There are four main kinds of method references:

  1. Static method reference: ClassName::staticMethodName

    • Equivalent to (args) -> ClassName.staticMethodName(args)
    • Example: Math::max for (a, b) -> Math.max(a, b)
  2. Instance method reference of a particular object: objectInstance::instanceMethodName

    • Equivalent to (args) -> objectInstance.instanceMethodName(args)
    • Example: System.out::println for (s) -> System.out.println(s)
  3. Instance method reference of an arbitrary object of a particular type: ClassName::instanceMethodName

    • Equivalent to (object, args) -> object.instanceMethodName(args)
    • This is used when the lambda's first parameter is the target of the instance method.
    • Example: String::length for (s) -> s.length()
  4. Constructor reference: ClassName::new

    • Equivalent to (args) -> new ClassName(args)
    • Example: ArrayList::new for () -> new ArrayList<>() or Integer::new for (s) -> new Integer(s)

Method references don't introduce new functionality; they just make existing lambdas more compact and often more readable when applicable.

Professional Code

Let's see these in action.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class MethodReferenceExample {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

        // Example 1: Static method reference (System.out::println, Math::sqrt)
        System.out.println("--- Static Method Reference ---");

        // Before: Lambda for printing
        System.out.println("Printing names (Lambda):");
        names.forEach(s -> System.out.println(s));

        // After: Method reference for printing
        System.out.println("Printing names (Method Reference):");
        names.forEach(System.out::println); // Equivalent to (s) -> System.out.println(s)

        List<Integer> numbers = Arrays.asList(4, 9, 16, 25);
        // Before: Lambda for calculating square root
        List<Double> sqrtNumbersBefore = numbers.stream()
                                                .map(num -> Math.sqrt(num))
                                                .collect(Collectors.toList());
        System.out.println("Square roots (Lambda): " + sqrtNumbersBefore);

        // After: Method reference for Math.sqrt
        List<Double> sqrtNumbersAfter = numbers.stream()
                                               .map(Math::sqrt) // Equivalent to (num) -> Math.sqrt(num)
                                               .collect(Collectors.toList());
        System.out.println("Square roots (Method Reference): " + sqrtNumbersAfter);


        // Example 2: Instance method reference of a particular object
        System.out.println("\n--- Instance Method Reference (Specific Object) ---");

        // We can create a custom instance and refer to its method
        MyPrinter printer = new MyPrinter();

        // Before: Lambda using printer instance
        System.out.println("Printing with custom printer (Lambda):");
        names.forEach(s -> printer.printCustom(s));

        // After: Method reference using printer instance
        System.out.println("Printing with custom printer (Method Reference):");
        names.forEach(printer::printCustom); // Equivalent to (s) -> printer.printCustom(s)


        // Example 3: Instance method reference of an arbitrary object of a particular type
        System.out.println("\n--- Instance Method Reference (Arbitrary Object of Type) ---");

        // Before: Lambda for String::toUpperCase
        List<String> upperNamesBefore = names.stream()
                                            .map(s -> s.toUpperCase())
                                            .collect(Collectors.toList());
        System.out.println("Uppercase names (Lambda): " + upperNamesBefore);

        // After: Method reference for String::toUpperCase
        List<String> upperNamesAfter = names.stream()
                                            .map(String::toUpperCase) // Equivalent to (s) -> s.toUpperCase()
                                            .collect(Collectors.toList());
        System.out.println("Uppercase names (Method Reference): " + upperNamesAfter);

        // Before: Lambda for String::length
        List<Integer> nameLengthsBefore = names.stream()
                                               .map(s -> s.length())
                                               .collect(Collectors.toList());
        System.out.println("Name lengths (Lambda): " + nameLengthsBefore);

        // After: Method reference for String::length
        List<Integer> nameLengthsAfter = names.stream()
                                              .map(String::length) // Equivalent to (s) -> s.length()
                                              .collect(Collectors.toList());
        System.out.println("Name lengths (Method Reference): " + nameLengthsAfter);


        // Example 4: Constructor reference
        System.out.println("\n--- Constructor Reference ---");

        // Before: Lambda for creating a new ArrayList
        List<String> newListBefore = numbers.stream()
                                            .map(String::valueOf) // Convert Integer to String
                                            .collect(() -> new ArrayList<>(), List::add, List::addAll); // Old style collect
        System.out.println("New List (Lambda constructor): " + newListBefore);

        // After: Method reference for creating a new ArrayList (using Collectors.toCollection)
        List<String> newListAfter = numbers.stream()
                                           .map(String::valueOf)
                                           .collect(Collectors.toCollection(ArrayList::new)); // Equivalent to () -> new ArrayList<>()
        System.out.println("New List (Constructor Reference): " + newListAfter);
    }

    static class MyPrinter {
        public void printCustom(String message) {
            System.out.println("Custom Print: " + message);
        }
    }
}

Clean Code Tip: Prefer method references over lambdas when the lambda body simply invokes an existing method Method references are more concise and often clearer because they directly state the intent: "apply this method." They remove the slight cognitive overhead of parsing the (args) -> someObject.someMethod(args) syntax when someMethod is exactly what you want to do.

Exercise & Solution

Exercise: Given a list of Strings, convert all of them to uppercase using a method reference. Then, print each string from the new uppercase list to the console, again using a method reference.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MethodReferenceExercise {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("hello", "world", "java", "streams");

        System.out.println("Original words: " + words);

        // Your code here:
        // 1. Convert words to uppercase using a method reference.
        //    Store the result in a new List<String> called 'upperCaseWords'.
        List<String> upperCaseWords = new ArrayList<>();

        // 2. Print each word from 'upperCaseWords' to the console using a method reference.
        System.out.println("\nUppercase words:");
        // ...
        // Solution placeholder
        // ...
    }
}

Solution:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MethodReferenceExerciseSolution {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("hello", "world", "java", "streams");

        System.out.println("Original words: " + words);

        // 1. Convert words to uppercase using a method reference.
        List<String> upperCaseWords = words.stream()
                                           .map(String::toUpperCase) // Arbitrary object of a particular type
                                           .collect(Collectors.toList());

        System.out.println("\nUppercase words:");
        // 2. Print each word from 'upperCaseWords' to the console using a method reference.
        upperCaseWords.forEach(System.out::println); // Instance method reference of a particular object (System.out)
    }
}

Chapter 7: Introduction to Maven

Quick Theory: Why databases are better than .txt files

When it comes to storing application data, plain text files (like .txt or .csv) are highly inefficient and problematic for anything beyond trivial use cases. They offer no inherent structure, making data retrieval, modification, and deletion complex and error-prone. Concurrency control (multiple users accessing at once) is practically non-existent, and data integrity (ensuring data is valid and consistent) must be entirely handled by the application logic, leading to fragile systems.

Relational databases, on the other hand, provide a robust, structured, and efficient solution for data persistence. They offer powerful features like ACID properties (Atomicity, Consistency, Isolation, Durability) to ensure data integrity, built-in query languages (SQL) for efficient data manipulation, and sophisticated mechanisms for concurrency control and user management. Modern applications almost universally rely on databases for their backend storage, guaranteeing reliable and scalable data management.

Maven is a powerful build automation tool used primarily for Java projects. It simplifies the build process by managing dependencies, compiling code, running tests, and packaging applications. The core of a Maven project is the pom.xml (Project Object Model) file, which describes the project's configuration, dependencies, and build lifecycle.

Professional Code

Let's set up a basic pom.xml for our project. We'll include a dependency for SQLite, a lightweight, file-based database, which is excellent for learning and simple applications as it doesn't require a separate server.

<?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.persistence</groupId>
    <artifactId>java-persistence-app</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SQLite JDBC Driver Dependency -->
        <dependency>
            <groupId>org.xerial</groupId>
            <artifactId>sqlite-jdbc</artifactId>
            <version>3.45.1.0</version> <!-- Always use the latest stable version -->
        </dependency>

        <!-- JUnit 5 for testing (Good practice to include) -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven Compiler Plugin -->
            <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>
        </plugins>
    </build>

</project>

Clean Code Tip: Build tools are essential Always use a build tool like Maven or Gradle for any non-trivial Java project. They automate tedious tasks like dependency management (downloading JARs), compilation, testing, and packaging, ensuring consistency across development environments and significantly reducing manual errors. The pom.xml serves as a central, declarative source of truth for your project's configuration.

Exercise & Solution

Exercise: Create a new directory for a Java project. Inside, create a pom.xml file that:

  1. Sets the groupId, artifactId, and version to com.mycompany, my-database-app, 1.0-SNAPSHOT respectively.
  2. Configures Java 17 for compilation.
  3. Includes the sqlite-jdbc dependency.
<!-- Your pom.xml structure goes here -->

Solution:

<?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.mycompany</groupId>
    <artifactId>my-database-app</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SQLite JDBC Driver -->
        <dependency>
            <groupId>org.xerial</groupId>
            <artifactId>sqlite-jdbc</artifactId>
            <version>3.45.1.0</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>
        </plugins>
    </build>

</project>

Chapter 8: JDBC Fundamentals

Quick Theory: The Java Database Connectivity (JDBC) API

JDBC (Java Database Connectivity) is a standard Java API for connecting to and interacting with relational databases. It provides a common interface for Java applications to communicate with various database systems (like MySQL, PostgreSQL, Oracle, SQLite), abstracting away the vendor-specific details. This means you can write your database access code once, and it will work with different databases by simply changing the JDBC driver.

The core steps involved in JDBC are often summarized as four stages:

  1. Load the Driver: Makes the database driver available to the Java application. For modern JDBC (Java 6+), Class.forName() is often implicit or no longer required for most drivers as they register themselves.
  2. Establish a Connection: Connects the Java application to the database using DriverManager.getConnection(), providing a JDBC URL, username, and password.
  3. Execute SQL Queries: Creates and executes SQL statements (Statement or PreparedStatement) to perform CRUD operations. Results are typically retrieved using a ResultSet.
  4. Close Resources: Releases database resources (Connection, Statement, ResultSet) to prevent leaks and ensure efficient resource management. This is best handled using try-with-resources.

Professional Code

Let's write a simple Java program to connect to an SQLite database and create a Product table.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcFundamentals {

    // Database URL for SQLite. This will create a 'products.db' file in your project root.
    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    public static void main(String[] args) {
        // 1. Load the Driver (No explicit Class.forName() needed for modern JDBC drivers like SQLite)
        //    The driver typically registers itself when it's loaded onto the classpath by Maven.
        System.out.println("Attempting to connect to the database...");

        // 2. Establish a Connection & 3. Execute SQL Query & 4. Close Resources
        //    Using try-with-resources to ensure Connection and Statement are closed automatically.
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {

            System.out.println("Connection to SQLite established successfully.");

            // SQL statement to create the 'products' table if it doesn't already exist.
            String createTableSQL = """
                    CREATE TABLE IF NOT EXISTS products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """;

            // Execute the DDL (Data Definition Language) query.
            statement.execute(createTableSQL);
            System.out.println("Table 'products' checked/created successfully.");

        } catch (SQLException e) {
            // Catch any SQL exceptions that occur during connection or query execution.
            System.err.println("Database error: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("Database operation completed. Resources closed.");
    }
}

Clean Code Tip: Always close resources with try-with-resources JDBC resources like Connection, Statement, and ResultSet consume system resources. Failing to close them leads to resource leaks, which can eventually exhaust your application's memory or database connections. The try-with-resources statement (introduced in Java 7) is the cleanest and safest way to ensure these resources are automatically closed, even if exceptions occur.

Exercise & Solution

Exercise: Modify the JdbcFundamentals example to also create a categories table with id (INTEGER PRIMARY KEY AUTOINCREMENT) and name (TEXT NOT NULL) columns, right after creating the products table.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcExercise {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {

            System.out.println("Connection to SQLite established successfully.");

            String createProductsTableSQL = """
                    CREATE TABLE IF NOT EXISTS products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """;
            statement.execute(createProductsTableSQL);
            System.out.println("Table 'products' checked/created successfully.");

            // Your code here: Add SQL to create the 'categories' table

            System.out.println("Table 'categories' checked/created successfully.");

        } catch (SQLException e) {
            System.err.println("Database error: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("Database operation completed. Resources closed.");
    }
}

Solution:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcExerciseSolution {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {

            System.out.println("Connection to SQLite established successfully.");

            String createProductsTableSQL = """
                    CREATE TABLE IF NOT EXISTS products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """;
            statement.execute(createProductsTableSQL);
            System.out.println("Table 'products' checked/created successfully.");

            // Solution: Add SQL to create the 'categories' table
            String createCategoriesTableSQL = """
                    CREATE TABLE IF NOT EXISTS categories (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL UNIQUE
                    );
                    """;
            statement.execute(createCategoriesTableSQL);
            System.out.println("Table 'categories' checked/created successfully.");

        } catch (SQLException e) {
            System.err.println("Database error: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("Database operation completed. Resources closed.");
    }
}

Chapter 9: The CRUD Operations (Create, Read, Update, Delete)

Quick Theory: Understanding CRUD

CRUD is an acronym that stands for Create, Read, Update, and Delete. These four basic operations are the fundamental functions of persistent storage and are the bedrock of most database-driven applications. Almost every piece of data you interact with in an application (a user, a product, an order) will at some point undergo one of these operations.

  • Create: Adding new data records (e.g., INSERT statements).
  • Read: Retrieving existing data (e.g., SELECT statements).
  • Update: Modifying existing data (e.g., UPDATE statements).
  • Delete: Removing data records (e.g., DELETE statements).

Understanding and implementing these operations efficiently and securely is paramount for any developer working with databases. In this chapter, we'll demonstrate them using basic Statement objects, but remember that for production code, PreparedStatement (covered next) is always the preferred and secure choice.

Professional Code

Let's build a simple Product class and a ProductManager to perform CRUD operations.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// --- Product Model Class ---
class Product {
    private int id;
    private String name;
    private double price;
    private int stockQuantity;

    // Constructor for creating new products (without ID)
    public Product(String name, double price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    // Constructor for retrieving existing products (with ID)
    public Product(int id, String name, double price, int stockQuantity) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    // Getters and Setters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }

    @Override
    public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}';
    }
}

// --- ProductManager Class for CRUD Operations ---
public class ProductManager {

    private static final String JDBC_URL = "jdbc:sqlite:products.db"; // Our database file

    // Helper method to get a database connection
    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL);
    }

    // CREATE operation
    public void addProduct(Product product) {
        // SQL query to insert a new product. ID is AUTOINCREMENT, so we don't include it.
        // NOTE: For security, PreparedStatements are preferred (see Chapter 10).
        String sql = "INSERT INTO products (name, price, stock_quantity) VALUES ('" +
                     product.getName() + "', " + product.getPrice() + ", " + product.getStockQuantity() + ");";

        try (Connection connection = getConnection();
             Statement statement = connection.createStatement()) {

            int rowsAffected = statement.executeUpdate(sql); // executeUpdate for INSERT, UPDATE, DELETE
            if (rowsAffected > 0) {
                // To get the auto-generated ID, we would typically use PreparedStatement.RETURN_GENERATED_KEYS.
                // For this basic example with Statement, we'll just acknowledge creation.
                System.out.println("Product '" + product.getName() + "' added successfully.");
            } else {
                System.err.println("Failed to add product: " + product.getName());
            }

        } catch (SQLException e) {
            System.err.println("Error adding product: " + e.getMessage());
            e.printStackTrace();
        }
    }

    // READ operation - Get all products
    public List<Product> getAllProducts() {
        List<Product> products = new ArrayList<>();
        String sql = "SELECT id, name, price, stock_quantity FROM products;";

        try (Connection connection = getConnection();
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery(sql)) { // executeQuery for SELECT

            while (resultSet.next()) { // Iterate through each row in the result set
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                double price = resultSet.getDouble("price");
                int stockQuantity = resultSet.getInt("stock_quantity");
                products.add(new Product(id, name, price, stockQuantity));
            }

        } catch (SQLException e) {
            System.err.println("Error retrieving all products: " + e.getMessage());
            e.printStackTrace();
        }
        return products;
    }

    // READ operation - Get product by ID
    public Optional<Product> getProductById(int productId) {
        // NOTE: For security, PreparedStatements are preferred (see Chapter 10).
        String sql = "SELECT id, name, price, stock_quantity FROM products WHERE id = " + productId + ";";

        try (Connection connection = getConnection();
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery(sql)) {

            if (resultSet.next()) { // If a row is found
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                double price = resultSet.getDouble("price");
                int stockQuantity = resultSet.getInt("stock_quantity");
                return Optional.of(new Product(id, name, price, stockQuantity));
            }

        } catch (SQLException e) {
            System.err.println("Error retrieving product by ID " + productId + ": " + e.getMessage());
            e.printStackTrace();
        }
        return Optional.empty(); // No product found or an error occurred
    }

    // UPDATE operation
    public void updateProduct(Product product) {
        // NOTE: For security, PreparedStatements are preferred (see Chapter 10).
        String sql = "UPDATE products SET name = '" + product.getName() + "', " +
                     "price = " + product.getPrice() + ", " +
                     "stock_quantity = " + product.getStockQuantity() + " " +
                     "WHERE id = " + product.getId() + ";";

        try (Connection connection = getConnection();
             Statement statement = connection.createStatement()) {

            int rowsAffected = statement.executeUpdate(sql);
            if (rowsAffected > 0) {
                System.out.println("Product ID " + product.getId() + " updated successfully.");
            } else {
                System.out.println("Product ID " + product.getId() + " not found or no changes made.");
            }

        } catch (SQLException e) {
            System.err.println("Error updating product ID " + product.getId() + ": " + e.getMessage());
            e.printStackTrace();
        }
    }

    // DELETE operation
    public void deleteProduct(int productId) {
        // NOTE: For security, PreparedStatements are preferred (see Chapter 10).
        String sql = "DELETE FROM products WHERE id = " + productId + ";";

        try (Connection connection = getConnection();
             Statement statement = connection.createStatement()) {

            int rowsAffected = statement.executeUpdate(sql);
            if (rowsAffected > 0) {
                System.out.println("Product ID " + productId + " deleted successfully.");
            } else {
                System.out.println("Product ID " + productId + " not found.");
            }

        } catch (SQLException e) {
            System.err.println("Error deleting product ID " + productId + ": " + e.getMessage());
            e.printStackTrace();
        }
    }


    // --- Main method to demonstrate CRUD operations ---
    public static void main(String[] args) {
        ProductManager manager = new ProductManager();

        // Ensure the table exists before starting CRUD operations
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            String createTableSQL = """
                    CREATE TABLE IF NOT EXISTS products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """;
            statement.execute(createTableSQL);
            System.out.println("Products table ensured to exist.\n");
        } catch (SQLException e) {
            System.err.println("Setup error: " + e.getMessage());
            return; // Exit if table creation fails
        }


        // 1. Create Products
        System.out.println("--- Adding Products ---");
        manager.addProduct(new Product("Laptop", 1200.00, 10)); // ID will be auto-generated as 1
        manager.addProduct(new Product("Mouse", 25.50, 50));   // ID will be auto-generated as 2
        manager.addProduct(new Product("Keyboard", 75.00, 20)); // ID will be auto-generated as 3
        System.out.println();

        // 2. Read All Products
        System.out.println("--- All Products ---");
        List<Product> allProducts = manager.getAllProducts();
        allProducts.forEach(System.out::println);
        System.out.println();

        // 3. Read a specific Product by ID
        System.out.println("--- Product by ID (ID 2) ---");
        manager.getProductById(2).ifPresentOrElse(
            System.out::println,
            () -> System.out.println("Product with ID 2 not found.")
        );
        System.out.println();

        // 4. Update a Product (change Mouse to a Gaming Mouse, reduce stock)
        System.out.println("--- Updating Product ID 2 ---");
        // We retrieve it first to ensure we have the correct ID
        manager.getProductById(2).ifPresent(p -> {
            p.setName("Gaming Mouse");
            p.setPrice(59.99);
            p.setStockQuantity(30);
            manager.updateProduct(p);
        });
        // Verify update
        System.out.println("Updated Product ID 2:");
        manager.getProductById(2).ifPresent(System.out::println);
        System.out.println();

        // 5. Delete a Product (delete Keyboard by ID 3)
        System.out.println("--- Deleting Product ID 3 ---");
        manager.deleteProduct(3);
        System.out.println();

        // Read all products again to confirm deletion
        System.out.println("--- All Products After Deletion ---");
        manager.getAllProducts().forEach(System.out::println);
        System.out.println();

        // Attempt to delete a non-existent product
        System.out.println("--- Attempting to Delete Non-existent Product ID 99 ---");
        manager.deleteProduct(99);
        System.out.println();
    }
}

Clean Code Tip: Use constants for SQL queries Store your SQL queries as private static final String constants. This improves readability, reduces the chance of typos, and makes queries easier to maintain. For complex queries or applications with many queries, externalizing them (e.g., in properties files) can be even better.

Exercise & Solution

Exercise: Based on the ProductManager example, implement a method int getTotalStockValue() that calculates the sum of (price * stock_quantity) for all products in the database.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// (Product class and existing CRUD methods omitted for brevity, assume they are present)
class Product {
    private int id;
    private String name;
    private double price;
    private int stockQuantity;

    public Product(String name, double price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }
    public Product(int id, String name, double price, int stockQuantity) {
        this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity;
    }
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    @Override
    public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}';
    }
}

public class ProductManagerExercise {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL);
    }

    // (Existing addProduct, getAllProducts, getProductById, updateProduct, deleteProduct methods omitted)
    public void addProduct(Product product) { /* ... */ }
    public List<Product> getAllProducts() { /* ... */ return new ArrayList<>(); }
    public Optional<Product> getProductById(int productId) { /* ... */ return Optional.empty(); }
    public void updateProduct(Product product) { /* ... */ }
    public void deleteProduct(int productId) { /* ... */ }


    // Your code here: Implement getTotalStockValue()
    public double getTotalStockValue() {
        double totalValue = 0.0;
        // ...
        return totalValue;
    }

    public static void main(String[] args) {
        ProductManagerExercise manager = new ProductManagerExercise();

        // (Table creation and adding initial products omitted for brevity, assume they are done)
        // For testing, let's ensure some data is there:
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh test
            statement.execute("""
                    CREATE TABLE products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """);
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('TV', 500.0, 5);");
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Soundbar', 150.0, 10);");
            System.out.println("Test data prepared.\n");
        } catch (SQLException e) {
            System.err.println("Test setup error: " + e.getMessage());
            return;
        }


        System.out.println("Total stock value: " + manager.getTotalStockValue());
    }
}

Solution:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

class Product { // Full Product class for solution context
    private int id;
    private String name;
    private double price;
    private int stockQuantity;

    public Product(String name, double price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }
    public Product(int id, String name, double price, int stockQuantity) {
        this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity;
    }
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    @Override
    public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}';
    }
}

public class ProductManagerExerciseSolution {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL);
    }

    // Add back the necessary methods from ProductManager if running standalone
    public void addProduct(Product product) {
        String sql = "INSERT INTO products (name, price, stock_quantity) VALUES ('" +
                product.getName() + "', " + product.getPrice() + ", " + product.getStockQuantity() + ");";
        try (Connection connection = getConnection(); Statement statement = connection.createStatement()) {
            statement.executeUpdate(sql);
        } catch (SQLException e) { e.printStackTrace(); }
    }
    public List<Product> getAllProducts() {
        List<Product> products = new ArrayList<>();
        String sql = "SELECT id, name, price, stock_quantity FROM products;";
        try (Connection connection = getConnection();
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery(sql)) {
            while (resultSet.next()) {
                products.add(new Product(resultSet.getInt("id"), resultSet.getString("name"),
                        resultSet.getDouble("price"), resultSet.getInt("stock_quantity")));
            }
        } catch (SQLException e) { e.printStackTrace(); }
        return products;
    }
    public Optional<Product> getProductById(int productId) {
        String sql = "SELECT id, name, price, stock_quantity FROM products WHERE id = " + productId + ";";
        try (Connection connection = getConnection();
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery(sql)) {
            if (resultSet.next()) {
                return Optional.of(new Product(resultSet.getInt("id"), resultSet.getString("name"),
                        resultSet.getDouble("price"), resultSet.getInt("stock_quantity")));
            }
        } catch (SQLException e) { e.printStackTrace(); }
        return Optional.empty();
    }
    public void updateProduct(Product product) {
        String sql = "UPDATE products SET name = '" + product.getName() + "', " +
                "price = " + product.getPrice() + ", " +
                "stock_quantity = " + product.getStockQuantity() + " " +
                "WHERE id = " + product.getId() + ";";
        try (Connection connection = getConnection(); Statement statement = connection.createStatement()) {
            statement.executeUpdate(sql);
        } catch (SQLException e) { e.printStackTrace(); }
    }
    public void deleteProduct(int productId) {
        String sql = "DELETE FROM products WHERE id = " + productId + ";";
        try (Connection connection = getConnection(); Statement statement = connection.createStatement()) {
            statement.executeUpdate(sql);
        } catch (SQLException e) { e.printStackTrace(); }
    }


    // Solution: Implement getTotalStockValue()
    public double getTotalStockValue() {
        double totalValue = 0.0;
        // We can either retrieve all products and sum them in Java,
        // or let the database do the calculation which is usually more efficient.
        String sql = "SELECT SUM(price * stock_quantity) AS total_value FROM products;";

        try (Connection connection = getConnection();
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery(sql)) {

            if (resultSet.next()) { // Expecting only one row with the sum
                totalValue = resultSet.getDouble("total_value");
            }

        } catch (SQLException e) {
            System.err.println("Error calculating total stock value: " + e.getMessage());
            e.printStackTrace();
        }
        return totalValue;
    }

    public static void main(String[] args) {
        ProductManagerExerciseSolution manager = new ProductManagerExerciseSolution();

        // For testing, let's ensure some data is there:
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh test
            statement.execute("""
                    CREATE TABLE products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """);
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('TV', 500.0, 5);"); // Value: 2500
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Soundbar', 150.0, 10);"); // Value: 1500
            System.out.println("Test data prepared.\n");
        } catch (SQLException e) {
            System.err.println("Test setup error: " + e.getMessage());
            return;
        }

        System.out.println("Total stock value: " + manager.getTotalStockValue()); // Expected: 4000.0
    }
}

Chapter 10: PreparedStatements

Quick Theory: The Necessity of PreparedStatements

Using plain Statement objects for executing SQL queries where user input is directly concatenated into the SQL string is a critical security vulnerability known as SQL Injection. An attacker can inject malicious SQL code through user input, potentially leading to unauthorized data access, modification, or even deletion of entire tables. For instance, if a user inputs ' OR '1'='1 into a login field, a naive Statement might execute SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '', effectively bypassing authentication.

PreparedStatement is the solution to SQL injection. It pre-compiles the SQL query string with ? placeholders for parameters. When you set parameters using methods like setString(), setInt(), setDouble(), etc., the database driver treats these values as literal data, not as executable SQL code. This ensures that no matter what malicious characters are in the user input, they are never interpreted as part of the query's structure, eliminating the risk of injection. PreparedStatement also offers performance benefits by allowing the database to parse and optimize the query once.

Professional Code

Let's refactor our ProductManager to use PreparedStatement for all CRUD operations. This is a mandatory practice for secure and robust database interactions.


Clean Code Tip: PreparedStatements are mandatory for security Never use Statement for queries involving user input. Always, always use PreparedStatement with ? placeholders for parameters. This is the single most important rule for preventing SQL injection vulnerabilities and ensuring the security and integrity of your database.

Exercise & Solution

Exercise: Given the ProductManagerPreparedStatement class, refactor the getTotalStockValue() method from the previous exercise to use a PreparedStatement (even though it doesn't strictly need parameters, it's good practice for consistency and future parameterization).

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// (Product class and existing CRUD methods omitted for brevity, assume they are present)
class Product {
    private int id; private String name; private double price; private int stockQuantity;
    public Product(String name, double price, int stockQuantity) { this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
    public Product(int id, String name, double price, int stockQuantity) { this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
    public int getId() { return id; } public void setId(int id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
    public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    @Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}'; }
}

public class PreparedStatementExercise {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL);
    }

    // (Existing addProduct, getAllProducts, getProductById, updateProduct, deleteProduct methods omitted)
    // Assume these are implemented using PreparedStatements.

    // Your code here: Refactor getTotalStockValue() to use PreparedStatement
    public double getTotalStockValue() {
        double totalValue = 0.0;
        // ...
        // Solution Placeholder
        // ...
        return totalValue;
    }

    public static void main(String[] args) {
        PreparedStatementExercise manager = new PreparedStatementExercise();

        // (Test data setup omitted for brevity, assume it's there)
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;");
            statement.execute("""
                    CREATE TABLE products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """);
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('TV', 500.0, 5);"); // Value: 2500
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Soundbar', 150.0, 10);"); // Value: 1500
            System.out.println("Test data prepared.\n");
        } catch (SQLException e) {
            System.err.println("Test setup error: " + e.getMessage());
            return;
        }

        System.out.println("Total stock value: " + manager.getTotalStockValue());
    }
}

Solution:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

class Product { // Full Product class for solution context
    private int id; private String name; private double price; private int stockQuantity;
    public Product(String name, double price, int stockQuantity) { this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
    public Product(int id, String name, double price, int stockQuantity) { this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
    public int getId() { return id; } public void setId(int id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
    public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    @Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}'; }
}

public class PreparedStatementExerciseSolution {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL);
    }

    // Solution: Refactor getTotalStockValue() to use PreparedStatement
    public double getTotalStockValue() {
        double totalValue = 0.0;
        String sql = "SELECT SUM(price * stock_quantity) AS total_value FROM products;";

        try (Connection connection = getConnection();
             // Even without parameters, using PreparedStatement is good for consistency and slight performance benefit
             // if the query is executed multiple times in a real application.
             PreparedStatement preparedStatement = connection.prepareStatement(sql);
             ResultSet resultSet = preparedStatement.executeQuery()) {

            if (resultSet.next()) {
                totalValue = resultSet.getDouble("total_value");
            }

        } catch (SQLException e) {
            System.err.println("Error calculating total stock value: " + e.getMessage());
            e.printStackTrace();
        }
        return totalValue;
    }

    public static void main(String[] args) {
        PreparedStatementExerciseSolution manager = new PreparedStatementExerciseSolution();

        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;");
            statement.execute("""
                    CREATE TABLE products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """);
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('TV', 500.0, 5);");
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Soundbar', 150.0, 10);");
            System.out.println("Test data prepared.\n");
        } catch (SQLException e) {
            System.err.println("Test setup error: " + e.getMessage());
            return;
        }

        System.out.println("Total stock value: " + manager.getTotalStockValue());
    }
}

Chapter 11: Transaction Management

Technical Theory: Ensuring Atomicity with Transactions

Database transactions are fundamental for maintaining data integrity and consistency, especially when multiple related database operations need to be treated as a single, indivisible unit. The ACID properties (Atomicity, Consistency, Isolation, Durability) are the standard for reliable transaction processing.

  • Atomicity: All operations within a transaction either succeed completely, or all of them fail completely. There's no half-way state. If any part of the transaction fails, the entire transaction is rolled back, leaving the database in its original state as if nothing happened.
  • Consistency: A transaction brings the database from one valid state to another. It ensures that all data integrity rules (like foreign key constraints, unique constraints) are maintained.
  • Isolation: Concurrent transactions execute as if they were running serially. The intermediate state of one transaction is not visible to other concurrent transactions until it is committed.
  • Durability: Once a transaction has been committed, its changes are permanent and will survive system failures (like power outages).

In JDBC, Connection objects are in auto-commit mode by default, meaning each SQL statement is treated as a separate transaction and committed immediately. To group multiple statements into a single transaction, you must disable auto-commit using connection.setAutoCommit(false). Then, you explicitly call connection.commit() if all operations succeed, or connection.rollback() if any operation fails.

Professional Code

Let's simulate a scenario where money is transferred between two bank accounts to demonstrate transaction management. If either debit or credit fails, the entire transaction should be rolled back.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TransactionManagement {

    private static final String JDBC_URL = "jdbc:sqlite:bank.db"; // A separate DB for bank accounts

    // --- Helper methods to manage bank accounts table ---

    // Initializes the 'accounts' table and inserts some test data.
    private static void setupDatabase() {
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS accounts;"); // Start fresh for demo
            String createTableSQL = """
                    CREATE TABLE accounts (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        account_number TEXT NOT NULL UNIQUE,
                        balance REAL NOT NULL DEFAULT 0.0
                    );
                    """;
            statement.execute(createTableSQL);
            System.out.println("Accounts table created.");

            // Insert test accounts
            statement.executeUpdate("INSERT INTO accounts (account_number, balance) VALUES ('ACC001', 1000.00);");
            statement.executeUpdate("INSERT INTO accounts (account_number, balance) VALUES ('ACC002', 500.00);");
            System.out.println("Test accounts 'ACC001' (1000.00) and 'ACC002' (500.00) created.\n");

        } catch (SQLException e) {
            System.err.println("Database setup error: " + e.getMessage());
            e.printStackTrace();
        }
    }

    // Prints the current balances of all accounts
    private static void printAccountBalances() {
        System.out.println("--- Current Account Balances ---");
        String sql = "SELECT account_number, balance FROM accounts;";
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             PreparedStatement ps = connection.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                System.out.println(rs.getString("account_number") + ": " + rs.getDouble("balance"));
            }
        } catch (SQLException e) {
            System.err.println("Error retrieving balances: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("--------------------------------\n");
    }

    // --- Transactional Transfer Money Method ---

    /**
     * Transfers a specified amount from one account to another in a single transaction.
     * Ensures atomicity: either both debit and credit succeed, or both fail.
     * @param fromAccountNumber The account to debit.
     * @param toAccountNumber The account to credit.
     * @param amount The amount to transfer.
     * @return true if transfer was successful, false otherwise.
     */
    public boolean transferMoney(String fromAccountNumber, String toAccountNumber, double amount) {
        // We will use a single Connection object for the entire transaction.
        // It's crucial that all operations within a transaction use the same connection.
        Connection connection = null;
        try {
            connection = DriverManager.getConnection(JDBC_URL);
            connection.setAutoCommit(false); // Disable auto-commit to start a transaction

            // 1. Debit from the source account
            String debitSql = "UPDATE accounts SET balance = balance - ? WHERE account_number = ? AND balance >= ?;";
            try (PreparedStatement debitPs = connection.prepareStatement(debitSql)) {
                debitPs.setDouble(1, amount);
                debitPs.setString(2, fromAccountNumber);
                debitPs.setDouble(3, amount); // Check for sufficient balance
                int debitRowsAffected = debitPs.executeUpdate();

                if (debitRowsAffected == 0) {
                    System.out.println("Transfer failed: Insufficient funds or source account not found for " + fromAccountNumber);
                    connection.rollback(); // Rollback if debit fails (e.g., insufficient funds)
                    return false;
                }
            }

            // (Optional) Simulate an error here to test rollback
            // if (fromAccountNumber.equals("ACC001") && amount == 150.0) {
            //     throw new SQLException("Simulated error during transfer to test rollback!");
            // }

            // 2. Credit to the destination account
            String creditSql = "UPDATE accounts SET balance = balance + ? WHERE account_number = ?;";
            try (PreparedStatement creditPs = connection.prepareStatement(creditSql)) {
                creditPs.setDouble(1, amount);
                creditPs.setString(2, toAccountNumber);
                int creditRowsAffected = creditPs.executeUpdate();

                if (creditRowsAffected == 0) {
                    System.out.println("Transfer failed: Destination account not found for " + toAccountNumber);
                    connection.rollback(); // Rollback if credit fails (e.g., destination account doesn't exist)
                    return false;
                }
            }

            connection.commit(); // Commit the transaction if both operations succeed
            System.out.println("Transfer of " + amount + " from " + fromAccountNumber + " to " + toAccountNumber + " successful.");
            return true;

        } catch (SQLException e) {
            System.err.println("Transfer failed due to a database error: " + e.getMessage());
            if (connection != null) {
                try {
                    System.out.println("Attempting to rollback changes...");
                    connection.rollback(); // Rollback on any SQLException
                    System.out.println("Rollback successful.");
                } catch (SQLException rollbackEx) {
                    System.err.println("Error during rollback: " + rollbackEx.getMessage());
                }
            }
            e.printStackTrace();
            return false;
        } finally {
            if (connection != null) {
                try {
                    connection.setAutoCommit(true); // Restore auto-commit mode
                    connection.close(); // Close the connection
                } catch (SQLException closeEx) {
                    System.err.println("Error closing connection: " + closeEx.getMessage());
                }
            }
        }
    }

    public static void main(String[] args) {
        setupDatabase(); // Initialize our bank accounts database
        TransactionManagement manager = new TransactionManagement();

        System.out.println("Initial balances:");
        printAccountBalances();

        // Scenario 1: Successful transfer
        System.out.println("--- Attempting successful transfer: ACC001 -> ACC002, $100.00 ---");
        manager.transferMoney("ACC001", "ACC002", 100.00);
        printAccountBalances(); // Balances should be: ACC001: 900.00, ACC002: 600.00

        // Scenario 2: Transfer with insufficient funds (ACC001 has 900, try to transfer 1000)
        System.out.println("--- Attempting transfer with insufficient funds: ACC001 -> ACC002, $1000.00 ---");
        manager.transferMoney("ACC001", "ACC002", 1000.00);
        printAccountBalances(); // Balances should be unchanged: ACC001: 900.00, ACC002: 600.00

        // Scenario 3: Transfer to a non-existent account (will trigger rollback)
        System.out.println("--- Attempting transfer to non-existent account: ACC001 -> ACC999, $50.00 ---");
        manager.transferMoney("ACC001", "ACC999", 50.00);
        printAccountBalances(); // Balances should be unchanged: ACC001: 900.00, ACC002: 600.00
    }
}

Clean Code Tip: Transactions are for critical multi-step operations Use transactions whenever multiple database operations must succeed or fail together to maintain data consistency (atomicity). Always disable auto-commit, explicitly call commit() on success, and rollback() on failure (typically within a catch block). Remember to set auto-commit back to true and close the connection in a finally block to prevent resource leaks and unexpected behavior for subsequent database interactions.

Exercise & Solution

Exercise: Implement a method batchUpdateProductPrices(double percentageIncrease) that updates the price of all products by a given percentage. This operation should be transactional. If any error occurs during the update process (e.g., a constraint violation, although less likely with a simple update), all price changes should be rolled back. For this exercise, you'll need the Product class and basic setup from previous chapters.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

// (Product class and a getConnection helper, setupDatabase method from previous chapters are assumed)
class Product { // Full Product class for solution context
    private int id; private String name; private double price; private int stockQuantity;
    public Product(String name, double price, int stockQuantity) { this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
    public Product(int id, String name, double price, int stockQuantity) { this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
    public int getId() { return id; } public void setId(int id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
    public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    @Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}'; }
}

public class TransactionExercise {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL);
    }

    private static void setupDatabase() {
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh test
            String createTableSQL = """
                    CREATE TABLE products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """;
            statement.execute(createTableSQL);
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Laptop', 1000.00, 10);");
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Monitor', 300.00, 15);");
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Webcam', 50.00, 20);");
            System.out.println("Products table created with initial data.\n");
        } catch (SQLException e) {
            System.err.println("Database setup error: " + e.getMessage());
            e.printStackTrace();
        }
    }

    // Helper to print all products
    public List<Product> getAllProducts() {
        List<Product> products = new ArrayList<>();
        String sql = "SELECT id, name, price, stock_quantity FROM products;";
        try (Connection connection = getConnection();
             PreparedStatement preparedStatement = connection.prepareStatement(sql);
             ResultSet resultSet = preparedStatement.executeQuery()) {
            while (resultSet.next()) {
                products.add(new Product(resultSet.getInt("id"), resultSet.getString("name"),
                        resultSet.getDouble("price"), resultSet.getInt("stock_quantity")));
            }
        } catch (SQLException e) { e.printStackTrace(); }
        return products;
    }


    // Your code here: Implement batchUpdateProductPrices()
    public boolean batchUpdateProductPrices(double percentageIncrease) {
        // ...
        return false; // Placeholder
    }

    public static void main(String[] args) {
        setupDatabase();
        TransactionExercise manager = new TransactionExercise();

        System.out.println("Products before update:");
        manager.getAllProducts().forEach(System.out::println);

        System.out.println("\n--- Attempting 10% price increase ---");
        if (manager.batchUpdateProductPrices(0.10)) { // 10% increase
            System.out.println("\nProducts after successful update:");
            manager.getAllProducts().forEach(System.out::println);
        } else {
            System.out.println("\nPrice update failed. Products should be unchanged:");
            manager.getAllProducts().forEach(System.out::println);
        }

        System.out.println("\n--- Attempting 20% price increase (and simulate an error) ---");
        // To simulate error, you might temporarily introduce a bug in the SQL or trigger a constraint.
        // For example, if you set a price to be negative (if allowed by DB, but bad logic).
        // Or if you update the SQL string to be invalid: String sql = "UPDATE products SET price_INVALID = price * (1 + ?);";
        // This will trigger the rollback.
        if (manager.batchUpdateProductPrices(0.20)) {
            System.out.println("\nProducts after (unexpectedly) successful update:");
            manager.getAllProducts().forEach(System.out::println);
        } else {
            System.out.println("\nPrice update failed as expected. Products should be unchanged (or rolled back):");
            manager.getAllProducts().forEach(System.out::println);
        }
    }
}

Solution:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

class Product { // Full Product class for solution context
    private int id; private String name; private double price; private int stockQuantity;
    public Product(String name, double price, int stockQuantity) { this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
    public Product(int id, String name, double price, int stockQuantity) { this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; }
    public int getId() { return id; } public void setId(int id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
    public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    @Override public String toString() { return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}'; }
}

public class TransactionExerciseSolution {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL);
    }

    private static void setupDatabase() {
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh test
            String createTableSQL = """
                    CREATE TABLE products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """;
            statement.execute(createTableSQL);
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Laptop', 1000.00, 10);");
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Monitor', 300.00, 15);");
            statement.executeUpdate("INSERT INTO products (name, price, stock_quantity) VALUES ('Webcam', 50.00, 20);");
            System.out.println("Products table created with initial data.\n");
        } catch (SQLException e) {
            System.err.println("Database setup error: " + e.getMessage());
            e.printStackTrace();
        }
    }

    public List<Product> getAllProducts() {
        List<Product> products = new ArrayList<>();
        String sql = "SELECT id, name, price, stock_quantity FROM products;";
        try (Connection connection = getConnection();
             PreparedStatement preparedStatement = connection.prepareStatement(sql);
             ResultSet resultSet = preparedStatement.executeQuery()) {
            while (resultSet.next()) {
                products.add(new Product(resultSet.getInt("id"), resultSet.getString("name"),
                        resultSet.getDouble("price"), resultSet.getInt("stock_quantity")));
            }
        } catch (SQLException e) { e.printStackTrace(); }
        return products;
    }


    // Solution: Implement batchUpdateProductPrices()
    public boolean batchUpdateProductPrices(double percentageIncrease) {
        Connection connection = null;
        try {
            connection = getConnection();
            connection.setAutoCommit(false); // Begin transaction

            String sql = "UPDATE products SET price = price * (1 + ?);"; // Update all prices
            // To simulate an error, you could temporarily change `price * (1 + ?)` to `price_INVALID * (1 + ?)`
            // or even just `price_INVALID` to cause a SQL syntax error.
            // String sql = "UPDATE products SET price_INVALID = price * (1 + ?);"; // Uncomment to test rollback

            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setDouble(1, percentageIncrease);
                int rowsAffected = ps.executeUpdate();
                System.out.println("Attempted to update prices for " + rowsAffected + " products.");
            }

            connection.commit(); // Commit if all updates succeed
            System.out.println("Batch price update successful by " + (percentageIncrease * 100) + "%.");
            return true;

        } catch (SQLException e) {
            System.err.println("Batch price update failed due to a database error: " + e.getMessage());
            if (connection != null) {
                try {
                    System.out.println("Attempting to rollback changes...");
                    connection.rollback(); // Rollback on any SQL exception
                    System.out.println("Rollback successful.");
                } catch (SQLException rollbackEx) {
                    System.err.println("Error during rollback: " + rollbackEx.getMessage());
                }
            }
            e.printStackTrace();
            return false;
        } finally {
            if (connection != null) {
                try {
                    connection.setAutoCommit(true); // Restore auto-commit
                    connection.close(); // Close the connection
                } catch (SQLException closeEx) {
                    System.err.println("Error closing connection: " + closeEx.getMessage());
                }
            }
        }
    }

    public static void main(String[] args) {
        setupDatabase();
        TransactionExerciseSolution manager = new TransactionExerciseSolution();

        System.out.println("Products before update:");
        manager.getAllProducts().forEach(System.out::println);

        System.out.println("\n--- Attempting 10% price increase ---");
        if (manager.batchUpdateProductPrices(0.10)) { // 10% increase
            System.out.println("\nProducts after successful update:");
            manager.getAllProducts().forEach(System.out::println);
        } else {
            System.out.println("\nPrice update failed. Products should be unchanged:");
            manager.getAllProducts().forEach(System.out::println);
        }

        System.out.println("\n--- Attempting 20% price increase (and simulate an error) ---");
        // To simulate error for testing:
        // Temporarily change the SQL in batchUpdateProductPrices to be invalid, e.g.,
        // String sql = "UPDATE products SET price_INVALID = price * (1 + ?);";
        // Then run this scenario.
        if (manager.batchUpdateProductPrices(0.20)) {
            System.out.println("\nProducts after (unexpectedly) successful update:");
            manager.getAllProducts().forEach(System.out::println);
        } else {
            System.out.println("\nPrice update failed as expected. Products should be unchanged (or rolled back):");
            manager.getAllProducts().forEach(System.out::println);
        }
    }
}

Chapter 12: DAO Pattern (Data Access Object)

Technical Theory: Professional Architectural Pattern

The Data Access Object (DAO) pattern is a widely used architectural pattern in enterprise applications to separate the low-level data access logic from the high-level business logic. Its primary goal is to abstract how data is persisted, retrieved, updated, and deleted, allowing the rest of the application to interact with data objects without needing to know the specifics of the underlying database (SQL, JDBC, ORM, etc.).

Benefits of the DAO pattern:

  • Separation of Concerns: Business logic doesn't get cluttered with database details. The DAO layer handles all persistence-related operations.
  • Easier Maintenance: Changes to the database schema or underlying persistence technology (e.g., switching from JDBC to JPA, or from SQLite to MySQL) only require modifications to the DAO implementation, not the business logic.
  • Improved Testability: You can easily mock or substitute DAO implementations for unit testing your business logic without needing a live database connection.
  • Reusability: DAO classes (or interfaces) can be reused across different parts of the application or even in different applications.

A typical DAO structure involves:

  1. Model/Entity Class: Represents the data structure (e.g., Product).
  2. DAO Interface: Defines the contract for data operations (e.g., ProductDAO with methods like add, getById, update, delete).
  3. DAO Implementation Class: Implements the DAO interface, containing the actual JDBC (or other persistence technology) code. (e.g., ProductDAOImpl).
  4. Client/Service Class: Uses the DAO interface to perform business operations, without knowing the implementation details.

Professional Code

Let's refactor our Product CRUD operations into the DAO pattern.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// 1. Model/Entity Class (Product) - No changes from previous chapters
class Product {
    private int id;
    private String name;
    private double price;
    private int stockQuantity;

    public Product(String name, double price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    public Product(int id, String name, double price, int stockQuantity) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }

    @Override
    public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + '}';
    }
}

// 2. DAO Interface: Defines the contract for Product data access
interface ProductDAO {
    void addProduct(Product product);
    Optional<Product> getProductById(int id);
    List<Product> getAllProducts();
    void updateProduct(Product product);
    void deleteProduct(int id);
}

// 3. DAO Implementation Class: Contains the JDBC logic
class ProductDAOImpl implements ProductDAO {

    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    // Helper method to get a database connection
    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL);
    }

    @Override
    public void addProduct(Product product) {
        String sql = "INSERT INTO products (name, price, stock_quantity) VALUES (?, ?, ?);";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            ps.setString(1, product.getName());
            ps.setDouble(2, product.getPrice());
            ps.setInt(3, product.getStockQuantity());
            int rowsAffected = ps.executeUpdate();

            if (rowsAffected > 0) {
                try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        product.setId(generatedKeys.getInt(1)); // Set the generated ID back to the product
                        System.out.println("[DAO] Product '" + product.getName() + "' added with ID: " + product.getId());
                    }
                }
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error adding product: " + e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public Optional<Product> getProductById(int id) {
        String sql = "SELECT id, name, price, stock_quantity FROM products WHERE id = ?;";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(new Product(rs.getInt("id"), rs.getString("name"),
                                                   rs.getDouble("price"), rs.getInt("stock_quantity")));
                }
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error retrieving product by ID " + id + ": " + e.getMessage());
            e.printStackTrace();
        }
        return Optional.empty();
    }

    @Override
    public List<Product> getAllProducts() {
        List<Product> products = new ArrayList<>();
        String sql = "SELECT id, name, price, stock_quantity FROM products;";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                products.add(new Product(rs.getInt("id"), rs.getString("name"),
                                         rs.getDouble("price"), rs.getInt("stock_quantity")));
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error retrieving all products: " + e.getMessage());
            e.printStackTrace();
        }
        return products;
    }

    @Override
    public void updateProduct(Product product) {
        String sql = "UPDATE products SET name = ?, price = ?, stock_quantity = ? WHERE id = ?;";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setString(1, product.getName());
            ps.setDouble(2, product.getPrice());
            ps.setInt(3, product.getStockQuantity());
            ps.setInt(4, product.getId());
            int rowsAffected = ps.executeUpdate();
            if (rowsAffected > 0) {
                System.out.println("[DAO] Product ID " + product.getId() + " updated.");
            } else {
                System.out.println("[DAO] Product ID " + product.getId() + " not found for update.");
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error updating product ID " + product.getId() + ": " + e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public void deleteProduct(int id) {
        String sql = "DELETE FROM products WHERE id = ?;";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setInt(1, id);
            int rowsAffected = ps.executeUpdate();
            if (rowsAffected > 0) {
                System.out.println("[DAO] Product ID " + id + " deleted.");
            } else {
                System.out.println("[DAO] Product ID " + id + " not found for deletion.");
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error deleting product ID " + id + ": " + e.getMessage());
            e.printStackTrace();
        }
    }
}

// 4. Client/Service Class: Uses the DAO interface
public class ProductService { // Renamed from main class to represent a higher-level service

    private ProductDAO productDAO; // Depending on the interface, not the implementation

    public ProductService(ProductDAO productDAO) {
        this.productDAO = productDAO;
    }

    // Business logic methods that use the DAO
    public void createNewProduct(String name, double price, int stock) {
        Product product = new Product(name, price, stock);
        productDAO.addProduct(product);
        System.out.println("[Service] Created product: " + product);
    }

    public void displayAllProducts() {
        System.out.println("\n[Service] --- All Products ---");
        List<Product> products = productDAO.getAllProducts();
        if (products.isEmpty()) {
            System.out.println("No products available.");
        } else {
            products.forEach(System.out::println);
        }
    }

    public void updateProductDetails(int id, String newName, double newPrice, int newStock) {
        Optional<Product> existingProduct = productDAO.getProductById(id);
        if (existingProduct.isPresent()) {
            Product productToUpdate = existingProduct.get();
            productToUpdate.setName(newName);
            productToUpdate.setPrice(newPrice);
            productToUpdate.setStockQuantity(newStock);
            productDAO.updateProduct(productToUpdate);
            System.out.println("[Service] Updated product ID " + id);
        } else {
            System.out.println("[Service] Product ID " + id + " not found for update.");
        }
    }

    public void removeProduct(int id) {
        productDAO.deleteProduct(id);
        System.out.println("[Service] Attempted to remove product ID " + id);
    }

    // --- Main method to demonstrate DAO pattern usage ---
    public static void main(String[] args) {
        // --- Database Setup (can be extracted to a separate utility/init) ---
        try (Connection connection = DriverManager.getConnection("jdbc:sqlite:products.db");
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;"); // Clear for fresh demo
            String createTableSQL = """
                    CREATE TABLE IF NOT EXISTS products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL
                    );
                    """;
            statement.execute(createTableSQL);
            System.out.println("Products table ensured to exist and cleared.\n");
        } catch (SQLException e) {
            System.err.println("Database setup error: " + e.getMessage());
            return;
        }
        // --- End Database Setup ---

        // Inject the DAO implementation into the service
        ProductDAO productDAO = new ProductDAOImpl();
        ProductService productService = new ProductService(productDAO);

        // Perform operations via the Service layer
        productService.createNewProduct("Laptop", 1200.00, 10); // ID: 1
        productService.createNewProduct("Mouse", 25.50, 50);    // ID: 2
        productService.createNewProduct("Keyboard", 75.00, 20); // ID: 3

        productService.displayAllProducts();

        productService.updateProductDetails(2, "Wireless Mouse", 35.99, 45); // Update ID 2
        productDAO.getProductById(2).ifPresent(p -> System.out.println("Verified update: " + p));

        productService.removeProduct(3); // Delete ID 3
        productService.removeProduct(99); // Attempt to delete non-existent

        productService.displayAllProducts();
    }
}

Clean Code Tip: DAO decouples business logic from persistence Always implement the DAO pattern (or use an ORM like Hibernate which provides its own abstraction) to separate your application's business logic from its persistence logic. This promotes modularity, makes your code more robust to changes in database technology, and drastically improves testability. Depend on the DAO interface, not the concrete implementation.

Exercise & Solution

Exercise: Extend the DAO pattern to include a Category entity.

  1. Create a Category model class (id, name).
  2. Create a CategoryDAO interface and CategoryDAOImpl class (with add, getById, getAll).
  3. Modify the products table to include a category_id (INTEGER, REFERENCES categories(id)) column.
  4. Update Product model to include a categoryId field.
  5. Update ProductDAO methods (addProduct, getProductById, getAllProducts) and ProductDAOImpl to handle the category_id.
  6. Demonstrate usage in main by adding categories first, then products associated with those categories.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// --- 1. Category Model Class ---
class Category {
    private int id;
    private String name;

    public Category(String name) { this.name = name; }
    public Category(int id, String name) { this.id = id; this.name = name; }
    public int getId() { return id; } public void setId(int id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
    @Override public String toString() { return "Category{id=" + id + ", name='" + name + "'}"; }
}

// --- Product Model Class (Modified) ---
class Product {
    private int id; private String name; private double price; private int stockQuantity; private int categoryId;

    public Product(String name, double price, int stockQuantity, int categoryId) {
        this.name = name; this.price = price; this.stockQuantity = stockQuantity; this.categoryId = categoryId;
    }
    public Product(int id, String name, double price, int stockQuantity, int categoryId) {
        this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; this.categoryId = categoryId;
    }
    public int getId() { return id; } public void setId(int id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
    public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    public int getCategoryId() { return categoryId; } public void setCategoryId(int categoryId) { this.categoryId = categoryId; }
    @Override public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + ", categoryId=" + categoryId + '}';
    }
}

// --- 2. CategoryDAO Interface ---
interface CategoryDAO {
    void addCategory(Category category);
    Optional<Category> getCategoryById(int id);
    List<Category> getAllCategories();
}

// --- ProductDAO Interface (Modified) ---
interface ProductDAO {
    void addProduct(Product product);
    Optional<Product> getProductById(int id);
    List<Product> getAllProducts();
    void updateProduct(Product product);
    void deleteProduct(int id);
}

// --- 3. CategoryDAOImpl Class ---
class CategoryDAOImpl implements CategoryDAO {
    private static final String JDBC_URL = "jdbc:sqlite:products.db";
    private Connection getConnection() throws SQLException { return DriverManager.getConnection(JDBC_URL); }

    @Override public void addCategory(Category category) { /* ... */ }
    @Override public Optional<Category> getCategoryById(int id) { /* ... */ return Optional.empty(); }
    @Override public List<Category> getAllCategories() { /* ... */ return new ArrayList<>(); }
}

// --- ProductDAOImpl Class (Modified) ---
class ProductDAOImpl implements ProductDAO {
    private static final String JDBC_URL = "jdbc:sqlite:products.db";
    private Connection getConnection() throws SQLException { return DriverManager.getConnection(JDBC_URL); }

    @Override public void addProduct(Product product) { /* ... */ }
    @Override public Optional<Product> getProductById(int id) { /* ... */ return Optional.empty(); }
    @Override public List<Product> getAllProducts() { /* ... */ return new ArrayList<>(); }
    @Override public void updateProduct(Product product) { /* ... */ }
    @Override public void deleteProduct(int id) { /* ... */ }
}

// --- Client/Service Class (Modified Main) ---
public class DaoExercise {
    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    private static void setupDatabase() {
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;");
            statement.execute("DROP TABLE IF EXISTS categories;");

            String createCategoriesTableSQL = """
                    CREATE TABLE categories (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL UNIQUE
                    );
                    """;
            statement.execute(createCategoriesTableSQL);
            System.out.println("Categories table created.");

            String createProductsTableSQL = """
                    CREATE TABLE products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL,
                        category_id INTEGER,
                        FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
                    );
                    """;
            statement.execute(createProductsTableSQL);
            System.out.println("Products table created.\n");
        } catch (SQLException e) {
            System.err.println("Database setup error: " + e.getMessage());
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        setupDatabase();

        CategoryDAO categoryDAO = new CategoryDAOImpl();
        ProductDAO productDAO = new ProductDAOImpl();

        // Your code here:
        // 1. Add some categories (e.g., "Electronics", "Books")
        // 2. Retrieve their IDs
        // 3. Add products, associating them with categories
        // 4. Display all categories and products

        System.out.println("\n--- All Categories ---");
        categoryDAO.getAllCategories().forEach(System.out::println);

        System.out.println("\n--- All Products ---");
        productDAO.getAllProducts().forEach(System.out::println);
    }
}

Solution:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// --- 1. Category Model Class ---
class Category {
    private int id;
    private String name;

    public Category(String name) { this.name = name; }
    public Category(int id, String name) { this.id = id; this.name = name; }
    public int getId() { return id; } public void setId(int id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
    @Override public String toString() { return "Category{id=" + id + ", name='" + name + "'}"; }
}

// --- Product Model Class (Modified) ---
class Product {
    private int id; private String name; private double price; private int stockQuantity; private int categoryId; // Added categoryId

    public Product(String name, double price, int stockQuantity, int categoryId) {
        this.name = name; this.price = price; this.stockQuantity = stockQuantity; this.categoryId = categoryId;
    }
    public Product(int id, String name, double price, int stockQuantity, int categoryId) {
        this.id = id; this.name = name; this.price = price; this.stockQuantity = stockQuantity; this.categoryId = categoryId;
    }
    public int getId() { return id; } public void setId(int id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
    public double getPrice() { return price; } public void setPrice(double price) { this.price = price; }
    public int getStockQuantity() { return stockQuantity; } public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    public int getCategoryId() { return categoryId; } public void setCategoryId(int categoryId) { this.categoryId = categoryId; }
    @Override public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stockQuantity=" + stockQuantity + ", categoryId=" + categoryId + '}';
    }
}

// --- 2. CategoryDAO Interface ---
interface CategoryDAO {
    void addCategory(Category category);
    Optional<Category> getCategoryById(int id);
    List<Category> getAllCategories();
}

// --- ProductDAO Interface (Modified) ---
interface ProductDAO {
    void addProduct(Product product);
    Optional<Product> getProductById(int id);
    List<Product> getAllProducts();
    void updateProduct(Product product);
    void deleteProduct(int id);
}

// --- 3. CategoryDAOImpl Class ---
class CategoryDAOImpl implements CategoryDAO {
    private static final String JDBC_URL = "jdbc:sqlite:products.db";
    private Connection getConnection() throws SQLException { return DriverManager.getConnection(JDBC_URL); }

    @Override
    public void addCategory(Category category) {
        String sql = "INSERT INTO categories (name) VALUES (?);";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            ps.setString(1, category.getName());
            int rowsAffected = ps.executeUpdate();
            if (rowsAffected > 0) {
                try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        category.setId(generatedKeys.getInt(1));
                        System.out.println("[DAO] Category '" + category.getName() + "' added with ID: " + category.getId());
                    }
                }
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error adding category: " + e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public Optional<Category> getCategoryById(int id) {
        String sql = "SELECT id, name FROM categories WHERE id = ?;";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(new Category(rs.getInt("id"), rs.getString("name")));
                }
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error retrieving category by ID " + id + ": " + e.getMessage());
            e.printStackTrace();
        }
        return Optional.empty();
    }

    @Override
    public List<Category> getAllCategories() {
        List<Category> categories = new ArrayList<>();
        String sql = "SELECT id, name FROM categories;";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                categories.add(new Category(rs.getInt("id"), rs.getString("name")));
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error retrieving all categories: " + e.getMessage());
            e.printStackTrace();
        }
        return categories;
    }
}

// --- ProductDAOImpl Class (Modified) ---
class ProductDAOImpl implements ProductDAO {
    private static final String JDBC_URL = "jdbc:sqlite:products.db";
    private Connection getConnection() throws SQLException { return DriverManager.getConnection(JDBC_URL); }

    @Override
    public void addProduct(Product product) {
        String sql = "INSERT INTO products (name, price, stock_quantity, category_id) VALUES (?, ?, ?, ?);"; // Added category_id
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            ps.setString(1, product.getName());
            ps.setDouble(2, product.getPrice());
            ps.setInt(3, product.getStockQuantity());
            ps.setInt(4, product.getCategoryId()); // Set category_id
            int rowsAffected = ps.executeUpdate();

            if (rowsAffected > 0) {
                try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        product.setId(generatedKeys.getInt(1));
                        System.out.println("[DAO] Product '" + product.getName() + "' added with ID: " + product.getId());
                    }
                }
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error adding product: " + e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public Optional<Product> getProductById(int id) {
        String sql = "SELECT id, name, price, stock_quantity, category_id FROM products WHERE id = ?;"; // Added category_id
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(new Product(rs.getInt("id"), rs.getString("name"),
                                                   rs.getDouble("price"), rs.getInt("stock_quantity"),
                                                   rs.getInt("category_id"))); // Retrieve category_id
                }
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error retrieving product by ID " + id + ": " + e.getMessage());
            e.printStackTrace();
        }
        return Optional.empty();
    }

    @Override
    public List<Product> getAllProducts() {
        List<Product> products = new ArrayList<>();
        String sql = "SELECT id, name, price, stock_quantity, category_id FROM products;"; // Added category_id
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                products.add(new Product(rs.getInt("id"), rs.getString("name"),
                                         rs.getDouble("price"), rs.getInt("stock_quantity"),
                                         rs.getInt("category_id"))); // Retrieve category_id
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error retrieving all products: " + e.getMessage());
            e.printStackTrace();
        }
        return products;
    }

    @Override
    public void updateProduct(Product product) {
        String sql = "UPDATE products SET name = ?, price = ?, stock_quantity = ?, category_id = ? WHERE id = ?;"; // Added category_id
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setString(1, product.getName());
            ps.setDouble(2, product.getPrice());
            ps.setInt(3, product.getStockQuantity());
            ps.setInt(4, product.getCategoryId()); // Set category_id
            ps.setInt(5, product.getId());
            int rowsAffected = ps.executeUpdate();
            if (rowsAffected > 0) {
                System.out.println("[DAO] Product ID " + product.getId() + " updated.");
            } else {
                System.out.println("[DAO] Product ID " + product.getId() + " not found for update.");
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error updating product ID " + product.getId() + ": " + e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public void deleteProduct(int id) {
        String sql = "DELETE FROM products WHERE id = ?;";
        try (Connection connection = getConnection();
             PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setInt(1, id);
            int rowsAffected = ps.executeUpdate();
            if (rowsAffected > 0) {
                System.out.println("[DAO] Product ID " + id + " deleted.");
            } else {
                System.out.println("[DAO] Product ID " + id + " not found for deletion.");
            }
        } catch (SQLException e) {
            System.err.println("[DAO] Error deleting product ID " + id + ": " + e.getMessage());
            e.printStackTrace();
        }
    }
}

// --- Client/Service Class (Modified Main) ---
public class DaoExerciseSolution {
    private static final String JDBC_URL = "jdbc:sqlite:products.db";

    private static void setupDatabase() {
        try (Connection connection = DriverManager.getConnection(JDBC_URL);
             Statement statement = connection.createStatement()) {
            statement.execute("DROP TABLE IF EXISTS products;");
            statement.execute("DROP TABLE IF EXISTS categories;");

            String createCategoriesTableSQL = """
                    CREATE TABLE categories (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL UNIQUE
                    );
                    """;
            statement.execute(createCategoriesTableSQL);
            System.out.println("Categories table created.");

            String createProductsTableSQL = """
                    CREATE TABLE products (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        price REAL NOT NULL,
                        stock_quantity INTEGER NOT NULL,
                        category_id INTEGER,
                        FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
                    );
                    """;
            statement.execute(createProductsTableSQL);
            System.out.println("Products table created.\n");
        } catch (SQLException e) {
            System.err.println("Database setup error: " + e.getMessage());
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        setupDatabase();

        CategoryDAO categoryDAO = new CategoryDAOImpl();
        ProductDAO productDAO = new ProductDAOImpl();

        // 1. Add some categories
        Category electronics = new Category("Electronics");
        categoryDAO.addCategory(electronics); // ID will be set to 1
        Category books = new Category("Books");
        categoryDAO.addCategory(books);       // ID will be set to 2
        Category homeGoods = new Category("Home Goods");
        categoryDAO.addCategory(homeGoods);   // ID will be set to 3

        System.out.println("\n--- All Categories ---");
        categoryDAO.getAllCategories().forEach(System.out::println);

        // 2. Add products, associating them with categories
        productDAO.addProduct(new Product("Laptop", 1200.00, 10, electronics.getId()));
        productDAO.addProduct(new Product("Mouse", 25.50, 50, electronics.getId()));
        productDAO.addProduct(new Product("The Hobbit", 15.00, 100, books.getId()));
        productDAO.addProduct(new Product("Coffee Maker", 75.00, 15, homeGoods.getId()));
        productDAO.addProduct(new Product("Advanced Java", 45.00, 30, books.getId()));
        productDAO.addProduct(new Product("Mystery Novel", 12.00, 80, books.getId()));


        System.out.println("\n--- All Products ---");
        productDAO.getAllProducts().forEach(System.out::println);

        // Example: Update a product's category
        System.out.println("\n--- Updating Mouse category ---");
        Optional<Product> mouse = productDAO.getProductById(2);
        mouse.ifPresent(p -> {
            p.setCategoryId(homeGoods.getId()); // Change mouse to Home Goods category (just for demo)
            productDAO.updateProduct(p);
        });

        System.out.println("\n--- All Products After Update ---");
        productDAO.getAllProducts().forEach(System.out::println);
    }
}

Chapter 19: Introduction to JavaFX

Quick Theory: The Visual Approach

Desktop applications require a visual interface for user interaction. Historically, Java's primary GUI toolkit was Swing, but it has largely been superseded. Swing applications often suffered from an outdated look and feel, performance issues, and complex API design, making them challenging to develop and maintain in a modern context. While still functional, Swing is considered a legacy technology.

JavaFX emerged as the modern, high-performance, and feature-rich platform for building rich client applications in Java. It leverages hardware-accelerated graphics, offers a cleaner API, and supports modern UI concepts like CSS styling, declarative UI with FXML, and media playback. JavaFX provides a robust framework for creating visually appealing and responsive desktop applications that can run across various operating systems.

Professional Code

Let's set up a basic JavaFX application, understanding the core components: Stage, Scene, and Node.

Example 1: Basic "Hello World" JavaFX Application

This example shows the minimal setup for a JavaFX application that displays "Hello, JavaFX!" in a window.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class HelloWorldFX extends Application {

    // The start method is the main entry point for all JavaFX applications.
    // The primary Stage is the top-level container for a JavaFX application.
    @Override
    public void start(Stage primaryStage) {
        // 1. Create a root node for the scene graph. StackPane is a simple layout
        //    manager that centers its children.
        StackPane root = new StackPane();

        // 2. Create a UI control (a Label in this case) to display text.
        Label helloLabel = new Label("Hello, JavaFX!");

        // 3. Add the label to the root layout pane.
        root.getChildren().add(helloLabel);

        // 4. Create a Scene, which is the container for all content in a scene graph.
        //    A Scene is attached to a Stage. We specify the root node and initial dimensions.
        Scene scene = new Scene(root, 300, 200);

        // 5. Set the title of the primary Stage (the window).
        primaryStage.setTitle("My First JavaFX App");

        // 6. Set the scene on the primary Stage.
        primaryStage.setScene(scene);

        // 7. Show the Stage (make the window visible).
        primaryStage.show();
    }

    // The main method is the standard entry point for Java applications.
    // It calls Application.launch() which handles JavaFX initialization and calls the start method.
    public static void main(String[] args) {
        launch(args);
    }
}

Example 2: Customizing the Window with Background Color

This example builds on the first, showing how to set a custom background color for the scene and incorporate basic styling.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; // Import Color class
import javafx.stage.Stage;

public class CustomWindowFX extends Application {

    @Override
    public void start(Stage primaryStage) {
        StackPane root = new StackPane();
        Label welcomeLabel = new Label("Welcome to Custom JavaFX!");

        // Add some basic CSS styling directly to the label
        welcomeLabel.setStyle("-fx-font-size: 24px; -fx-text-fill: white;");

        root.getChildren().add(welcomeLabel);

        // Create a Scene with a specific background color
        // The third argument to the Scene constructor can be a Paint object (e.g., Color.LIGHTBLUE)
        Scene scene = new Scene(root, 400, 250, Color.DARKBLUE); // Set scene background to DARKBLUE

        primaryStage.setTitle("Custom JavaFX Window");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Clean Code Tip: Start small, understand core concepts When diving into GUI frameworks like JavaFX, resist the urge to build complex UIs immediately. Master the fundamental concepts of Stage (the window), Scene (the content inside), and the Node hierarchy (components and their arrangement) first. Build simple "Hello World" examples, then incrementally add features to solidify your understanding.

Exercise & Solution

Exercise: Create a JavaFX application that displays a window with the title "My Profile" and a Label that says "User: [Your Name]". The window should be 500x150 pixels.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ProfileWindowExercise extends Application {

    @Override
    public void start(Stage primaryStage) {
        // Your code here:
        // 1. Create a StackPane as the root layout.
        // 2. Create a Label with your name.
        // 3. Add the label to the root.
        // 4. Create a Scene with the root and dimensions 500x150.
        // 5. Set the Stage title to "My Profile".
        // 6. Set the scene on the primary stage.
        // 7. Show the primary stage.
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Solution:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ProfileWindowExerciseSolution extends Application {

    @Override
    public void start(Stage primaryStage) {
        StackPane root = new StackPane();
        Label profileLabel = new Label("User: Jane Doe"); // Replace with your name
        profileLabel.setStyle("-fx-font-size: 20px; -fx-text-fill: #333;");

        root.getChildren().add(profileLabel);

        Scene scene = new Scene(root, 500, 150);

        primaryStage.setTitle("My Profile");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Chapter 20: Event Handling (Lambdas)

Quick Theory: Making Your UI Interactive

Graphical User Interfaces are inherently event-driven. Users interact with UI components (buttons, text fields, menus), and these interactions trigger events. To make your UI dynamic and responsive, you need to handle these events by attaching event handlers or listeners to components. An event handler is a piece of code that executes when a specific event occurs.

JavaFX, with its modern API design, fully embraces functional programming and lambda expressions for event handling. This dramatically simplifies the syntax compared to the older, more verbose anonymous inner classes used in Swing. Instead of defining a separate class or an anonymous class for each event listener, you can provide the event-handling logic directly as a lambda expression, making your code more concise and readable.

Professional Code

Let's see how to use lambdas to make buttons perform actions and update UI elements.

Example 1: Button Click Prints to Console

This demonstrates a simple button that, when clicked, prints a message to the console using a lambda.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class EventHandlingConsole extends Application {

    @Override
    public void start(Stage primaryStage) {
        Button clickMeButton = new Button("Click Me!");

        // Set an action for the button using a lambda expression.
        // The lambda takes an ActionEvent object 'e' (or just 'event' or even omit if not used)
        // and defines the code to execute when the button is clicked.
        clickMeButton.setOnAction(e -> {
            System.out.println("Button was clicked! Event: " + e.getEventType());
        });

        StackPane root = new StackPane();
        root.getChildren().add(clickMeButton);

        Scene scene = new Scene(root, 300, 150);
        primaryStage.setTitle("Event Handling Demo (Console)");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Example 2: Button Click Updates a Label

This example shows how a button click can dynamically change the text of another UI component (a Label).

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox; // Using VBox for vertical alignment
import javafx.geometry.Pos;     // For alignment
import javafx.stage.Stage;

public class EventHandlingUpdateUI extends Application {

    // Declare the Label as a field so it can be accessed and updated from the event handler.
    private Label messageLabel;
    private int clickCount = 0; // To track clicks

    @Override
    public void start(Stage primaryStage) {
        // Initialize the Label
        messageLabel = new Label("Click the button below!");
        messageLabel.setStyle("-fx-font-size: 18px; -fx-text-fill: #007bff;");

        Button updateButton = new Button("Update Message");

        // Lambda expression for the button's action.
        // It updates the text of 'messageLabel' and the click count.
        updateButton.setOnAction(event -> {
            clickCount++;
            messageLabel.setText("Button clicked " + clickCount + " time(s)!");
            System.out.println("Label updated.");
        });

        // VBox layout manager to stack components vertically
        VBox root = new VBox(20); // 20 pixels spacing between children
        root.setAlignment(Pos.CENTER); // Center the children in the VBox
        root.getChildren().addAll(messageLabel, updateButton);

        Scene scene = new Scene(root, 400, 200);
        primaryStage.setTitle("Event Handling Demo (UI Update)");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Clean Code Tip: Keep event handlers concise, delegate complex logic Event handlers (especially lambdas) should be lightweight and focused on triggering actions, not performing complex business logic themselves. If an action requires significant computation or multiple steps, delegate that work to a separate method or a dedicated service class. This keeps your UI code clean, readable, and easier to test.

Exercise & Solution

Exercise: Create a JavaFX application with a Label showing "Current Count: 0" and two Buttons: "Increment" and "Decrement".

  • Clicking "Increment" should increase the count shown in the label.
  • Clicking "Decrement" should decrease the count shown in the label.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.geometry.Pos;
import javafx.stage.Stage;

public class CounterAppExercise extends Application {

    private Label countLabel;
    private int currentCount = 0;

    @Override
    public void start(Stage primaryStage) {
        countLabel = new Label("Current Count: " + currentCount);
        countLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");

        Button incrementButton = new Button("Increment");
        Button decrementButton = new Button("Decrement");

        // Your code here: Add setOnAction for incrementButton and decrementButton using lambdas.

        HBox buttonBox = new HBox(10); // 10 pixels spacing between buttons
        buttonBox.setAlignment(Pos.CENTER);
        buttonBox.getChildren().addAll(incrementButton, decrementButton);

        VBox root = new VBox(20);
        root.setAlignment(Pos.CENTER);
        root.getChildren().addAll(countLabel, buttonBox);

        Scene scene = new Scene(root, 300, 180);
        primaryStage.setTitle("Counter App");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Solution:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.geometry.Pos;
import javafx.stage.Stage;

public class CounterAppExerciseSolution extends Application {

    private Label countLabel;
    private int currentCount = 0;

    @Override
    public void start(Stage primaryStage) {
        countLabel = new Label("Current Count: " + currentCount);
        countLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");

        Button incrementButton = new Button("Increment");
        // Lambda for incrementing the count
        incrementButton.setOnAction(e -> {
            currentCount++;
            countLabel.setText("Current Count: " + currentCount);
        });

        Button decrementButton = new Button("Decrement");
        // Lambda for decrementing the count
        decrementButton.setOnAction(e -> {
            currentCount--;
            countLabel.setText("Current Count: " + currentCount);
        });

        HBox buttonBox = new HBox(10);
        buttonBox.setAlignment(Pos.CENTER);
        buttonBox.getChildren().addAll(incrementButton, decrementButton);

        VBox root = new VBox(20);
        root.setAlignment(Pos.CENTER);
        root.getChildren().addAll(countLabel, buttonBox);

        Scene scene = new Scene(root, 300, 180);
        primaryStage.setTitle("Counter App");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Chapter 21: Layout Managers (VBox/HBox/GridPane)

Quick Theory: Arranging Components Visually

Designing a user interface isn't just about placing components; it's about arranging them effectively and ensuring they adapt gracefully to different window sizes and screen resolutions. Hardcoding pixel positions (absolute positioning) is generally a bad practice in modern GUI development because it leads to inflexible UIs that break when elements change size or the window is resized.

JavaFX (like other GUI toolkits) provides layout managers (or layout panes) to handle the positioning and sizing of UI components dynamically. These layout panes follow specific rules to organize their children, allowing for responsive and adaptable interfaces. Key layout managers include VBox (vertical stacking), HBox (horizontal stacking), and GridPane (table-like arrangement), among others. Using these greatly simplifies UI construction and improves maintainability.

Professional Code

Let's explore VBox, HBox, and GridPane for organizing UI elements.

Example 1: Using VBox and HBox for Basic Layouts

This example combines VBox and HBox to create a window with a label at the top and a row of buttons at the bottom.

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class LayoutBasics extends Application {

    @Override
    public void start(Stage primaryStage) {
        // --- Top section: A single Label ---
        Label headerLabel = new Label("Welcome to the App!");
        headerLabel.setStyle("-fx-font-size: 24px; -fx-font-weight: bold;");

        // --- Bottom section: Two buttons horizontally arranged ---
        Button saveButton = new Button("Save");
        saveButton.setOnAction(e -> System.out.println("Save clicked!"));

        Button cancelButton = new Button("Cancel");
        cancelButton.setOnAction(e -> System.out.println("Cancel clicked!"));

        // HBox to arrange buttons horizontally
        HBox buttonBox = new HBox(10); // 10 pixels spacing between children
        buttonBox.getChildren().addAll(saveButton, cancelButton);
        buttonBox.setAlignment(Pos.CENTER); // Center buttons horizontally within the HBox

        // --- Main layout: VBox to stack header and buttonBox vertically ---
        VBox root = new VBox(30); // 30 pixels spacing between children
        root.setAlignment(Pos.TOP_CENTER); // Align children to top-center of the VBox
        root.setPadding(new Insets(20)); // Add 20 pixels padding around the VBox content
        root.getChildren().addAll(headerLabel, buttonBox);

        Scene scene = new Scene(root, 400, 250);
        primaryStage.setTitle("VBox & HBox Layout");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Example 2: Using GridPane for a Form Layout

GridPane is excellent for arranging components in a grid, like a typical form with labels and input fields.

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class GridPaneForm extends Application {

    @Override
    public void start(Stage primaryStage) {
        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER); // Center the grid in the scene
        grid.setHgap(10); // Horizontal gap between columns
        grid.setVgap(10); // Vertical gap between rows
        grid.setPadding(new Insets(25, 25, 25, 25)); // Padding around the grid

        // --- Add components to the grid ---

        // Row 0, Column 0: Label for User Name
        Label userNameLabel = new Label("User Name:");
        grid.add(userNameLabel, 0, 0); // (node, col, row)

        // Row 0, Column 1: TextField for User Name
        TextField userTextField = new TextField();
        userTextField.setPromptText("Enter your username");
        grid.add(userTextField, 1, 0);

        // Row 1, Column 0: Label for Password
        Label passwordLabel = new Label("Password:");
        grid.add(passwordLabel, 0, 1);

        // Row 1, Column 1: PasswordField (TextField variant for passwords)
        TextField passwordField = new TextField(); // Use PasswordField in real app
        passwordField.setPromptText("Enter your password");
        grid.add(passwordField, 1, 1);

        // Row 2, Column 1: Login Button (spanning multiple columns if needed)
        Button loginButton = new Button("Login");
        // Login button action:
        loginButton.setOnAction(e -> {
            String username = userTextField.getText();
            String password = passwordField.getText();
            System.out.println("Attempting login with Username: " + username + ", Password: " + password);
            // In a real app, this would involve authentication logic
        });
        grid.add(loginButton, 1, 2); // Add button to Column 1, Row 2

        Scene scene = new Scene(grid, 350, 250); // Set scene size
        primaryStage.setTitle("Login Form (GridPane)");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Clean Code Tip: Favor declarative layouts over hardcoded coordinates Always use JavaFX's layout panes (VBox, HBox, GridPane, BorderPane, AnchorPane, etc.) to arrange your UI components. Avoid setting explicit x, y coordinates, width, or height values unless absolutely necessary. Declarative layouts automatically handle component resizing and positioning, making your UI responsive, flexible, and much easier to maintain across different screen sizes and resolutions.

Exercise & Solution

Exercise: Create a simple "Registration Form" using a GridPane. It should have Labels and TextFields for:

  • First Name
  • Last Name
  • Email
  • A "Register" Button. Arrange these components neatly within the grid.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class RegistrationFormExercise extends Application {

    @Override
    public void start(Stage primaryStage) {
        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(25, 25, 25, 25));

        // Your code here: Add Labels and TextFields for First Name, Last Name, Email.
        // Also add a Register Button.

        // Row 0: First Name
        // ...

        // Row 1: Last Name
        // ...

        // Row 2: Email
        // ...

        // Row 3: Register Button
        // ...

        Scene scene = new Scene(grid, 400, 300);
        primaryStage.setTitle("Registration Form");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Solution:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class RegistrationFormExerciseSolution extends Application {

    @Override
    public void start(Stage primaryStage) {
        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(25, 25, 25, 25));

        // Row 0: First Name
        Label firstNameLabel = new Label("First Name:");
        grid.add(firstNameLabel, 0, 0);
        TextField firstNameField = new TextField();
        firstNameField.setPromptText("John");
        grid.add(firstNameField, 1, 0);

        // Row 1: Last Name
        Label lastNameLabel = new Label("Last Name:");
        grid.add(lastNameLabel, 0, 1);
        TextField lastNameField = new TextField();
        lastNameField.setPromptText("Doe");
        grid.add(lastNameField, 1, 1);

        // Row 2: Email
        Label emailLabel = new Label("Email:");
        grid.add(emailLabel, 0, 2);
        TextField emailField = new TextField();
        emailField.setPromptText("john.doe@example.com");
        grid.add(emailField, 1, 2);

        // Row 3: Register Button
        Button registerButton = new Button("Register");
        registerButton.setOnAction(e -> {
            String firstName = firstNameField.getText();
            String lastName = lastNameField.getText();
            String email = emailField.getText();
            System.out.println("Registering: " + firstName + " " + lastName + " (" + email + ")");
            // In a real application, you'd send this data to a service/database
        });
        grid.add(registerButton, 1, 3);

        Scene scene = new Scene(grid, 400, 300);
        primaryStage.setTitle("Registration Form");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Chapter 22: MVC Pattern in GUI

Quick Theory: Separating GUI Logic from Data

Building complex GUIs without a proper architectural pattern can quickly lead to spaghetti code, where business logic is intertwined with UI concerns. This makes the application difficult to maintain, extend, and test. The Model-View-Controller (MVC) pattern addresses this by separating an application into three interconnected components:

  • Model: Represents the application's data and business logic. It's independent of the user interface. It notifies the View (or Controller) when its data changes.
  • View: Responsible for displaying the Model's data to the user. It's the visual representation of the application and typically has no knowledge of the Model's internal structure or how it processes data. It also sends user input to the Controller.
  • Controller: Acts as an intermediary between the Model and the View. It receives user input from the View, processes it (potentially updating the Model), and then updates the View to reflect any changes in the Model.

In a GUI context, MVC ensures a clear separation of concerns, making the codebase more modular, reusable, and testable.

Professional Code

Let's refactor our simple Counter App to demonstrate a basic MVC structure. For simplicity, we'll implement it programmatically without FXML.

Example 1: Counter Application with MVC

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

// --- 1. Model: Holds the application's data and logic ---
class CounterModel {
    private int count;

    public CounterModel() {
        this.count = 0;
    }

    public int getCount() {
        return count;
    }

    public void increment() {
        count++;
    }

    public void decrement() {
        count--;
    }
}

// --- 2. View: Displays the UI and sends user input to the Controller ---
// (In a real app, this might be split into an interface and implementation, or use FXML)
class CounterView {
    private Label countLabel;
    private Button incrementButton;
    private Button decrementButton;
    private VBox root;

    public CounterView() {
        // Initialize UI components
        countLabel = new Label("Current Count: 0");
        countLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");

        incrementButton = new Button("Increment");
        decrementButton = new Button("Decrement");

        HBox buttonBox = new HBox(10);
        buttonBox.setAlignment(Pos.CENTER);
        buttonBox.getChildren().addAll(incrementButton, decrementButton);

        root = new VBox(20);
        root.setAlignment(Pos.CENTER);
        root.getChildren().addAll(countLabel, buttonBox);
    }

    public VBox getRoot() {
        return root;
    }

    // Methods to update the view based on model changes
    public void updateCountDisplay(int newCount) {
        countLabel.setText("Current Count: " + newCount);
    }

    // Getters for buttons, so Controller can attach event handlers
    public Button getIncrementButton() {
        return incrementButton;
    }

    public Button getDecrementButton() {
        return decrementButton;
    }
}

// --- 3. Controller: Handles user input, updates the Model, and updates the View ---
class CounterController {
    private CounterModel model;
    private CounterView view;

    public CounterController(CounterModel model, CounterView view) {
        this.model = model;
        this.view = view;
        initView(); // Initialize the view with initial model data
        attachEventHandlers(); // Attach event handlers to view components
    }

    private void initView() {
        // Ensure the view displays the initial state of the model
        view.updateCountDisplay(model.getCount());
    }

    private void attachEventHandlers() {
        view.getIncrementButton().setOnAction(e -> handleIncrement());
        view.getDecrementButton().setOnAction(e -> handleDecrement());
    }

    // Event handling methods
    private void handleIncrement() {
        model.increment(); // Update the model
        view.updateCountDisplay(model.getCount()); // Update the view
        System.out.println("Incremented to: " + model.getCount());
    }

    private void handleDecrement() {
        model.decrement(); // Update the model
        view.updateCountDisplay(model.getCount()); // Update the view
        System.out.println("Decremented to: " + model.getCount());
    }
}

// --- Main Application Class ---
public class MvcCounterApp extends Application {

    @Override
    public void start(Stage primaryStage) {
        // Instantiate Model, View, and Controller
        CounterModel model = new CounterModel();
        CounterView view = new CounterView();
        CounterController controller = new CounterController(model, view); // Controller wires model and view

        Scene scene = new Scene(view.getRoot(), 300, 180);
        primaryStage.setTitle("MVC Counter App");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Clean Code Tip: Strive for thin controllers, fat models In an MVC (or similar MV* patterns like MVP, MVVM) architecture, aim for "thin controllers" and "fat models." Controllers should primarily handle user input and delegate business logic to the Model. The Model, conversely, should contain most of the application's core logic and data manipulation. This makes your business logic more reusable, testable independently of the UI, and easier to manage as the application grows.

Exercise & Solution

Exercise: Refactor the Login Form (GridPane) from Chapter 21 into a basic MVC structure.

  • Model: A simple LoginModel class that could potentially hold username/password or perform validation (for this exercise, just a dummy isValidLogin method).
  • View: The JavaFX UI (labels, text fields, button).
  • Controller: Handles button click, interacts with Model for validation, and updates the View (e.g., displaying a "Login Successful" or "Invalid Credentials" message).
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

// --- Model (to be implemented) ---
class LoginModel {
    public boolean isValidLogin(String username, String password) {
        // Dummy validation for exercise
        return "admin".equals(username) && "password".equals(password);
    }
}

// --- View (to be implemented) ---
class LoginView {
    private TextField usernameField;
    private TextField passwordField;
    private Button loginButton;
    private Label statusLabel; // To display login status
    private VBox root;

    public LoginView() {
        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(25, 25, 25, 25));

        grid.add(new Label("User Name:"), 0, 0);
        usernameField = new TextField();
        usernameField.setPromptText("Enter your username");
        grid.add(usernameField, 1, 0);

        grid.add(new Label("Password:"), 0, 1);
        passwordField = new TextField();
        passwordField.setPromptText("Enter your password");
        grid.add(passwordField, 1, 1);

        loginButton = new Button("Login");
        grid.add(loginButton, 1, 2);

        statusLabel = new Label(""); // Initially empty
        statusLabel.setStyle("-fx-font-size: 14px;");

        root = new VBox(10);
        root.setAlignment(Pos.CENTER);
        root.getChildren().addAll(grid, statusLabel);
    }

    public VBox getRoot() { return root; }
    public TextField getUsernameField() { return usernameField; }
    public TextField getPasswordField() { return passwordField; }
    public Button getLoginButton() { return loginButton; }
    public void setStatusMessage(String message, String color) {
        statusLabel.setText(message);
        statusLabel.setStyle("-fx-text-fill: " + color + "; -fx-font-size: 14px;");
    }
}

// --- Controller (to be implemented) ---
class LoginController {
    private LoginModel model;
    private LoginView view;

    public LoginController(LoginModel model, LoginView view) {
        this.model = model;
        this.view = view;
        attachEventHandlers();
    }

    private void attachEventHandlers() {
        // Your code here: Attach an action to the loginButton
        // When clicked, retrieve username/password from view,
        // call model.isValidLogin(), and update view.statusLabel accordingly.
    }

    // Helper method for login logic
    private void handleLogin() {
        String username = view.getUsernameField().getText();
        String password = view.getPasswordField().getText();

        if (model.isValidLogin(username, password)) {
            view.setStatusMessage("Login Successful!", "green");
            System.out.println("Successful login for: " + username);
        } else {
            view.setStatusMessage("Invalid Credentials!", "red");
            System.out.println("Failed login attempt for: " + username);
        }
    }
}


// --- Main Application Class ---
public class MvcLoginAppExercise extends Application {

    @Override
    public void start(Stage primaryStage) {
        LoginModel model = new LoginModel();
        LoginView view = new LoginView();
        LoginController controller = new LoginController(model, view);

        Scene scene = new Scene(view.getRoot(), 350, 250);
        primaryStage.setTitle("MVC Login Form");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Solution:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

// --- Model: Holds the application's data and logic ---
class LoginModel {
    public boolean isValidLogin(String username, String password) {
        // Simulate a simple validation (e.g., against a hardcoded value)
        // In a real application, this would involve database checks, authentication services, etc.
        return "admin".equals(username) && "password".equals(password);
    }
}

// --- View: Displays the UI and sends user input to the Controller ---
class LoginView {
    private TextField usernameField;
    private TextField passwordField;
    private Button loginButton;
    private Label statusLabel;
    private VBox root;

    public LoginView() {
        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(25, 25, 25, 25));

        grid.add(new Label("User Name:"), 0, 0);
        usernameField = new TextField();
        usernameField.setPromptText("Enter your username");
        grid.add(usernameField, 1, 0);

        grid.add(new Label("Password:"), 0, 1);
        passwordField = new TextField(); // Use PasswordField in a real app for security
        passwordField.setPromptText("Enter your password");
        grid.add(passwordField, 1, 1);

        loginButton = new Button("Login");
        grid.add(loginButton, 1, 2);

        statusLabel = new Label("");
        statusLabel.setStyle("-fx-font-size: 14px;");

        root = new VBox(10);
        root.setAlignment(Pos.CENTER);
        root.getChildren().addAll(grid, statusLabel);
    }

    public VBox getRoot() { return root; }
    public TextField getUsernameField() { return usernameField; }
    public TextField getPasswordField() { return passwordField; }
    public Button getLoginButton() { return loginButton; }
    public void setStatusMessage(String message, String color) {
        statusLabel.setText(message);
        statusLabel.setStyle("-fx-text-fill: " + color + "; -fx-font-size: 14px;");
    }
}

// --- Controller: Handles user input, updates the Model, and updates the View ---
class LoginController {
    private LoginModel model;
    private LoginView view;

    public LoginController(LoginModel model, LoginView view) {
        this.model = model;
        this.view = view;
        attachEventHandlers();
    }

    private void attachEventHandlers() {
        view.getLoginButton().setOnAction(e -> handleLogin());
    }

    private void handleLogin() {
        String username = view.getUsernameField().getText();
        String password = view.getPasswordField().getText();

        if (model.isValidLogin(username, password)) {
            view.setStatusMessage("Login Successful!", "green");
            System.out.println("Successful login for: " + username);
            // In a real application, navigate to main app window
        } else {
            view.setStatusMessage("Invalid Credentials!", "red");
            System.out.println("Failed login attempt for: " + username);
        }
    }
}

// --- Main Application Class ---
public class MvcLoginAppExerciseSolution extends Application {

    @Override
    public void start(Stage primaryStage) {
        LoginModel model = new LoginModel();
        LoginView view = new LoginView();
        LoginController controller = new LoginController(model, view);

        Scene scene = new Scene(view.getRoot(), 350, 250);
        primaryStage.setTitle("MVC Login Form");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Chapter 23: Maven Deployment

Quick Theory: Creating an Executable JAR (Fat JAR)

Once you've developed your Java application, whether it's a desktop GUI or a backend service, the next step is to package it for distribution and execution. The standard way to package Java code is into a JAR (Java Archive) file. However, a simple JAR file (jar -cvf MyProgram.jar .) only contains your compiled classes. If your application relies on external libraries (which almost all do, managed by Maven), those libraries won't be included, leading to NoClassDefFoundError at runtime.

A "fat JAR" (also known as an "uber JAR") solves this problem by bundling not only your application's compiled classes but also all its transitive dependencies (all the .jar files listed in your pom.xml's <dependencies>) into a single, self-contained JAR file. This makes deployment incredibly simple: you just distribute one JAR file, and it contains everything needed to run the application, without needing to manually manage a classpath with multiple JARs. The Maven Shade Plugin is commonly used to create these fat JARs.

Professional Code

Let's configure a pom.xml to create an executable fat JAR for our JavaFX application.

Example 1: Basic Maven JAR Plugin for Executable JAR

This shows how to configure maven-jar-plugin to make a regular JAR executable (if it has no external dependencies), by specifying the main class. This won't include dependencies.

<?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.deployment</groupId>
    <artifactId>basic-executable-jar</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <!-- Maven Compiler Plugin -->
            <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>

            <!-- Maven JAR Plugin: Makes the JAR executable by specifying the main class -->
            <!-- This creates a standard JAR, NOT a fat JAR (dependencies are NOT bundled) -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <!-- Specify the fully qualified name of your main class -->
                            <addClasspath>true</addClasspath>
                            <mainClass>com.example.deployment.BasicApp</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

To run this (after mvn clean install), you would use java -jar target/basic-executable-jar-1.0-SNAPSHOT.jar. Note: A dummy BasicApp.java in src/main/java/com/example/deployment/ with a public static void main method is required for this to build.

Example 2: Maven Shade Plugin for a Fat JAR (with JavaFX)

This is the recommended approach for distributing a standalone JavaFX application. It bundles all dependencies, including JavaFX modules, into one JAR. Note: JavaFX modules need to be added as dependencies for a JavaFX app.

<?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.deployment</groupId>
    <artifactId>javafx-fat-jar</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <javafx.version>17.0.2</javafx.version> <!-- Use a recent LTS version -->
        <main.class>com.example.deployment.FatJarFxApp</main.class>
    </properties>

    <dependencies>
        <!-- JavaFX Core Modules -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <!-- Add other JavaFX modules as needed (e.g., javafx-graphics, javafx-media) -->
    </dependencies>

    <build>
        <plugins>
            <!-- Maven Compiler Plugin -->
            <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>

            <!-- Maven Shade Plugin: Creates a single executable JAR with all dependencies -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.5.0</version> <!-- Use a recent version -->
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                            <transformers>
                                <!-- Specify the main class for the executable JAR -->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>${main.class}</mainClass>
                                </transformer>
                                <!-- Handle JavaFX module-info.class merging for fat JAR -->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                            </transformers>
                            <!-- Optional: Relocate packages to avoid conflicts if needed -->
                            <!--
                            <relocations>
                                <relocation>
                                    <pattern>com.google.common</pattern>
                                    <shadedPattern>shaded.com.google.common</shadedPattern>
                                </relocation>
                            </relocations>
                            -->
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <!-- Workaround for JavaFX module system with older JDKs or when creating fat JARs -->
            <!-- The javafx-maven-plugin is typically used for modular JavaFX,
                 but with shade plugin, the mainClass entry in Manifest handles entry point. -->
            <!-- For JavaFX 11+, you often need a separate launcher class if your main app
                 class extends Application directly and your JDK is <= 10.
                 For JDK 11+, you can directly specify the main Application class.
                 Here, we assume main.class is your JavaFX Application class. -->

        </plugins>
    </build>

</project>

To build this (after mvn clean install), it will create target/javafx-fat-jar-1.0-SNAPSHOT.jar. You can then run it with java -jar target/javafx-fat-jar-1.0-SNAPSHOT.jar. Note: A dummy FatJarFxApp.java that extends javafx.application.Application and has a public static void main method is required.

Clean Code Tip: Automate deployment for consistency and reliability Always automate your build and deployment process using tools like Maven or Gradle. Manual compilation, dependency management, and JAR creation are tedious, error-prone, and inconsistent. Automated builds ensure that your application is always packaged correctly and consistently, which is crucial for reliable delivery to users or production environments.

Exercise & Solution

Exercise: Take any of your previous simple JavaFX applications (e.g., HelloWorldFX from Chapter 19).

  1. Create a new Maven project for it.
  2. Add the necessary JavaFX dependencies for javafx-controls (and javafx-fxml if you were using FXML, though not covered in detail here).
  3. Configure the maven-shade-plugin in your pom.xml to create a fat JAR that includes all JavaFX dependencies and sets your application's main class as the entry point.
<!-- Your pom.xml structure goes here, adapting the JavaFX fat JAR example -->

Solution:

Let's assume the JavaFX application is named MyJavaFxApp located at src/main/java/com/example/app/MyJavaFxApp.java:

// src/main/java/com/example/app/MyJavaFxApp.java
package com.example.app;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class MyJavaFxApp extends Application {

    @Override
    public void start(Stage primaryStage) {
        StackPane root = new StackPane();
        Label helloLabel = new Label("Hello from Fat JAR JavaFX!");
        root.getChildren().add(helloLabel);

        Scene scene = new Scene(root, 350, 150);
        primaryStage.setTitle("Fat JAR Demo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

And the pom.xml to build it:

<?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.app</groupId>
    <artifactId>my-javafx-fat-app</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <javafx.version>17.0.2</javafx.version>
        <!-- Specify your main JavaFX Application class here -->
        <main.class>com.example.app.MyJavaFxApp</main.class>
    </properties>

    <dependencies>
        <!-- JavaFX Modules -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <!-- Add other JavaFX modules if your app uses them (e.g., javafx-graphics, javafx-media) -->
    </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>

            <!-- Maven Shade Plugin for creating a fat JAR -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.5.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <!-- Configure the main class for the executable JAR's manifest -->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>${main.class}</mainClass>
                                </transformer>
                                <!-- Essential for JavaFX fat JARs to correctly merge module-info.class content -->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                            </transformers>
                            <createDependencyReducedPom>false</createDependencyReducedPom> <!-- Prevent POM generation issues -->
                        </configuration>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

</project>

Chapter 24: Final Junior Checklist

Quick Theory: Reflecting on Your Journey

You've now traversed a comprehensive path, from the absolute fundamentals of Java syntax to advanced concepts in functional programming, data persistence, GUI development, and project deployment. This curriculum was designed to equip you with the essential knowledge and practical skills expected of a competent Junior Java Developer. The journey doesn't end here; software development is a continuous learning process. However, you've built a strong foundation.

This checklist serves as a self-assessment tool. If you can confidently explain and practically apply the concepts listed below, you are well-prepared for entry-level Java development roles and are ready to tackle more complex challenges and specialized frameworks (like Spring Boot) that build upon this foundation. Keep practicing, keep building, and never stop learning!

Final Junior Checklist

  • Book 1: Java Fundamentals

    • Core Concepts:
      • JDK, JRE, JVM: Understand their roles.
      • Variables & Data Types: Primitives (int, double, boolean, char), String.
      • Operators: Arithmetic, relational, logical, assignment.
      • Control Flow: if-else, switch, for loops, while loops, do-while loops.
      • Arrays: Declaration, initialization, iteration.
    • Object-Oriented Programming (OOP):
      • Classes & Objects: Definition, instantiation.
      • Constructors: Default, parameterized, this keyword.
      • Encapsulation: Access modifiers (public, private, protected), getters/setters.
      • Inheritance: extends, super keyword, method overriding.
      • Polymorphism: Method overloading, abstract classes, interfaces.
      • Abstraction: abstract keyword, abstract methods, interfaces vs. abstract classes.
    • Collections Framework:
      • List (e.g., ArrayList, LinkedList): Basic operations, when to use each.
      • Set (e.g., HashSet, TreeSet): Basic operations, uniqueness.
      • Map (e.g., HashMap, TreeMap): Key-value pairs, basic operations.
    • Exception Handling:
      • try-catch-finally blocks.
      • throw, throws keywords.
      • Checked vs. Unchecked exceptions.
    • Basic I/O:
      • System.out.println(), Scanner.
      • Reading/writing text files (basic FileReader/FileWriter or Files utility).
  • Book 2: Advanced Java Concepts

    • Generics:
      • Type parameters (<T>).
      • Generic classes and methods.
      • Bounded type parameters (<T extends Number>).
      • Wildcards (? extends, ? super).
    • Concurrency & Multithreading:
      • Thread class, Runnable interface.
      • synchronized keyword (methods, blocks).
      • volatile keyword.
      • Basic understanding of thread safety issues (race conditions).
      • ExecutorService and Callable (basic usage).
    • Networking (Basic):
      • Understanding Sockets (Socket, ServerSocket).
      • Basic client-server communication.
    • Design Patterns:
      • Singleton pattern.
      • Factory pattern.
      • Observer pattern (basic understanding).
    • Reflection (Basic):
      • Class.forName(), obj.getClass().
      • Accessing method/field names.
    • Annotations:
      • Understanding built-in annotations (@Override, @Deprecated).
      • Basic concept of custom annotations.
    • Date & Time API (java.time):
      • LocalDate, LocalTime, LocalDateTime, Instant.
      • Duration, Period.
      • Formatting and parsing (DateTimeFormatter).
  • Book 3: Functional Revolution & Data Persistence

    • Functional Programming (Java 8+):
      • Lambda Expressions: Syntax (params) -> { body }, usage with functional interfaces (Predicate, Consumer, Function, Supplier).
      • Stream API:
        • Intermediate Operations: filter(), map(), sorted().
        • Terminal Operations: collect(Collectors.toList()), count(), forEach(), reduce(), min()/max().
      • Optional Class: Preventing NullPointerException (ofNullable, isPresent, orElse, map, ifPresent).
      • Method References: ClassName::methodName, objectName::methodName, ClassName::new.
    • Build Automation (Maven):
      • pom.xml structure: groupId, artifactId, version, properties.
      • Dependency Management: Adding <dependency> entries.
      • Build Lifecycle: clean, compile, test, package, install.
    • JDBC (Java Database Connectivity):
      • Fundamentals: Driver loading, Connection, Statement, ResultSet.
      • try-with-resources for resource management.
      • CRUD Operations: INSERT, SELECT, UPDATE, DELETE via JDBC.
      • PreparedStatement: Mandatory for SQL injection prevention (? placeholders).
      • Transaction Management: setAutoCommit(false), commit(), rollback().
    • DAO Pattern:
      • Purpose: Separate persistence logic from business logic.
      • Structure: Model, DAO Interface, DAO Implementation.
    • Desktop GUIs (JavaFX):
      • Basic Architecture: Stage, Scene, Node hierarchy.
      • UI Components: Label, Button, TextField (basic usage).
      • Event Handling: setOnAction with lambdas.
      • Layout Managers: VBox, HBox, GridPane (responsive UI design).
      • MVC Pattern: Basic application of Model-View-Controller for GUIs.
    • Deployment:
      • Maven Shade Plugin: Creating executable "fat JARs" for standalone distribution.

Clean Code Tip: Continuous learning and practice are key The journey of a software developer is one of continuous learning. The concepts covered in these books provide a strong foundation, but the landscape of technology is always evolving. Stay curious, experiment with new tools and frameworks, read documentation, contribute to open-source projects, and consistently build things. Practice is the most effective way to solidify your understanding and grow your skills.


END OF BOOK 3: FINAL PART

You have successfully completed the qualification path. Your dedication to learning and mastering these topics is commendable. You are now equipped with the knowledge and foundational skills to embark on your career as a Junior Java Developer. Good luck on your next steps!