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, orI/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 (likecollect,forEach) is invoked.
filter() and map() are two of the most fundamental intermediate operations in the Stream API.
.filter(Predicate<T> predicate): Takes aPredicate(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 aFunction(a lambda that transforms an element of typeTto typeR) 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 aCollectoras an argument, which defines how the elements in the stream should be accumulated into a final result.Collectorsis a utility class providing many predefined collectors, such asCollectors.toList(),Collectors.toSet(),Collectors.joining(),Collectors.groupingBy(), etc..count(): Returns the number of elements in the stream as along..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.
- Count how many orders have a
statusof "PENDING". - Collect the
customerNameof all orders that have astatusof "COMPLETED" into aList<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 theComparableinterface. For example,Stringand wrapper classes likeIntegeralready implementComparable..sorted(Comparator<T> comparator): Sorts elements according to the order induced by the providedComparator. 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:
- Sort the employees first by their
departmentalphabetically (ascending). - If employees are in the same department, sort them by
salaryin descending order. - 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,
Optionalacts as a wrapper for that value. - If a value is absent, the
Optionalis empty.
Key methods of Optional:
Optional.of(T value): Creates anOptionalwith the given non-null value. ThrowsNullPointerExceptionifvalueisnull.Optional.ofNullable(T value): Creates anOptionalwith the given value, or an emptyOptionalif the value isnull. This is the safer choice when the value might benull.isPresent(): Returnstrueif a value is present,falseotherwise.isEmpty(): Returnstrueif no value is present (Java 11+).get(): Returns the value if present, otherwise throwsNoSuchElementException. Use with caution, similar to a null check.orElse(T other): Returns the value if present, otherwise returnsother(a default value).orElseGet(Supplier<? extends T> other): Returns the value if present, otherwise invokes theSupplierto 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 theSupplier.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 anOptionaldescribing the result. Otherwise returns an emptyOptional. 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:
- Call
findUserByIdfor an existing user (ID 1L) and, if present, print their name and email. If email is not present, print "No Email Provided". - Call
findUserByIdfor 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:
-
Static method reference:
ClassName::staticMethodName- Equivalent to
(args) -> ClassName.staticMethodName(args) - Example:
Math::maxfor(a, b) -> Math.max(a, b)
- Equivalent to
-
Instance method reference of a particular object:
objectInstance::instanceMethodName- Equivalent to
(args) -> objectInstance.instanceMethodName(args) - Example:
System.out::printlnfor(s) -> System.out.println(s)
- Equivalent to
-
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::lengthfor(s) -> s.length()
- Equivalent to
-
Constructor reference:
ClassName::new- Equivalent to
(args) -> new ClassName(args) - Example:
ArrayList::newfor() -> new ArrayList<>()orInteger::newfor(s) -> new Integer(s)
- Equivalent to
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:
- Sets the
groupId,artifactId, andversiontocom.mycompany,my-database-app,1.0-SNAPSHOTrespectively. - Configures Java 17 for compilation.
- Includes the
sqlite-jdbcdependency.
<!-- 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:
- 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. - Establish a Connection: Connects the Java application to the database using
DriverManager.getConnection(), providing a JDBC URL, username, and password. - Execute SQL Queries: Creates and executes SQL statements (
StatementorPreparedStatement) to perform CRUD operations. Results are typically retrieved using aResultSet. - 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.,
INSERTstatements). - Read: Retrieving existing data (e.g.,
SELECTstatements). - Update: Modifying existing data (e.g.,
UPDATEstatements). - Delete: Removing data records (e.g.,
DELETEstatements).
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:
- Model/Entity Class: Represents the data structure (e.g.,
Product). - DAO Interface: Defines the contract for data operations (e.g.,
ProductDAOwith methods likeadd,getById,update,delete). - DAO Implementation Class: Implements the DAO interface, containing the actual JDBC (or other persistence technology) code. (e.g.,
ProductDAOImpl). - 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.
- Create a
Categorymodel class (id,name). - Create a
CategoryDAOinterface andCategoryDAOImplclass (withadd,getById,getAll). - Modify the
productstable to include acategory_id(INTEGER, REFERENCES categories(id)) column. - Update
Productmodel to include acategoryIdfield. - Update
ProductDAOmethods (addProduct,getProductById,getAllProducts) andProductDAOImplto handle thecategory_id. - Demonstrate usage in
mainby 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.
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);
}
}
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
- 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
LoginModelclass that could potentially hold username/password or perform validation (for this exercise, just a dummyisValidLoginmethod). - 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).
- Create a new Maven project for it.
- Add the necessary JavaFX dependencies for
javafx-controls(andjavafx-fxmlif you were using FXML, though not covered in detail here). - Configure the
maven-shade-pluginin yourpom.xmlto create a fat JAR that includes all JavaFX dependencies and sets your application'smainclass 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,forloops,whileloops,do-whileloops. - Arrays: Declaration, initialization, iteration.
- Object-Oriented Programming (OOP):
- Classes & Objects: Definition, instantiation.
- Constructors: Default, parameterized,
thiskeyword. - Encapsulation: Access modifiers (
public,private,protected), getters/setters. - Inheritance:
extends,superkeyword, method overriding. - Polymorphism: Method overloading, abstract classes, interfaces.
- Abstraction:
abstractkeyword, 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-finallyblocks.throw,throwskeywords.- Checked vs. Unchecked exceptions.
- Basic I/O:
System.out.println(),Scanner.- Reading/writing text files (basic
FileReader/FileWriterorFilesutility).
- Core Concepts:
-
Book 2: Advanced Java Concepts
- Generics:
- Type parameters (
<T>). - Generic classes and methods.
- Bounded type parameters (
<T extends Number>). - Wildcards (
? extends,? super).
- Type parameters (
- Concurrency & Multithreading:
Threadclass,Runnableinterface.synchronizedkeyword (methods, blocks).volatilekeyword.- Basic understanding of thread safety issues (race conditions).
ExecutorServiceandCallable(basic usage).
- Networking (Basic):
- Understanding Sockets (
Socket,ServerSocket). - Basic client-server communication.
- Understanding Sockets (
- 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.
- Understanding built-in annotations (
- Date & Time API (java.time):
LocalDate,LocalTime,LocalDateTime,Instant.Duration,Period.- Formatting and parsing (
DateTimeFormatter).
- Generics:
-
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().
- Intermediate Operations:
OptionalClass: PreventingNullPointerException(ofNullable,isPresent,orElse,map,ifPresent).- Method References:
ClassName::methodName,objectName::methodName,ClassName::new.
- Lambda Expressions: Syntax
- Build Automation (Maven):
pom.xmlstructure: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-resourcesfor resource management.- CRUD Operations:
INSERT,SELECT,UPDATE,DELETEvia JDBC. PreparedStatement: Mandatory for SQL injection prevention (?placeholders).- Transaction Management:
setAutoCommit(false),commit(),rollback().
- Fundamentals: Driver loading,
- DAO Pattern:
- Purpose: Separate persistence logic from business logic.
- Structure: Model, DAO Interface, DAO Implementation.
- Desktop GUIs (JavaFX):
- Basic Architecture:
Stage,Scene,Nodehierarchy. - UI Components:
Label,Button,TextField(basic usage). - Event Handling:
setOnActionwith lambdas. - Layout Managers:
VBox,HBox,GridPane(responsive UI design). - MVC Pattern: Basic application of Model-View-Controller for GUIs.
- Basic Architecture:
- Deployment:
- Maven Shade Plugin: Creating executable "fat JARs" for standalone distribution.
- Functional Programming (Java 8+):
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!
No comments to display
No comments to display