Previous: Reflection API for Dynamic Test Frameworks Next: Course Overview

Java Fundamentals Review - Practical Exercises

Exercise 1: Object-Oriented Programming Basics

Task: Create a Banking System

Implement a simple banking system that demonstrates core OOP concepts:

  1. Create an abstract Account class with:

    • Protected fields for account number, holder name, and balance
    • Constructor, getters, and setters
    • Abstract method calculateInterest()
    • Concrete methods deposit() and withdraw()
  2. Create concrete subclasses:

    • SavingsAccount with higher interest rate and withdrawal limit
    • CheckingAccount with lower interest rate and overdraft facility
  3. Implement a Bank class that:

    • Stores multiple accounts in a collection
    • Provides methods to add accounts, find accounts by number
    • Calculates total balance across all accounts
  4. Create a Customer class that:

    • Has name, ID, and contact information
    • Can have multiple accounts (composition)
    • Has methods to calculate total balance across their accounts
  5. Demonstrate polymorphism by:

    • Creating an array of Account objects with different concrete types
    • Calling calculateInterest() on each

Expected Output:

Created Savings Account for John Doe with balance: $1000.0
Created Checking Account for Jane Smith with balance: $2500.0
Deposited $500.0 to John's account. New balance: $1500.0
Withdrew $200.0 from Jane's account. New balance: $2300.0
Withdrawal failed: Insufficient funds in Savings Account
Interest for John's Savings Account: $75.0
Interest for Jane's Checking Account: $46.0
Total bank balance: $3800.0
John's total balance across all accounts: $1500.0

Exercise 2: Collections and Generics

Task: Implement a Custom Data Structure

Create a generic data structure that demonstrates your understanding of collections and generics:

  1. Implement a generic CustomQueue<T> class that:

    • Uses an internal ArrayList to store elements
    • Provides methods: enqueue(), dequeue(), peek(), size(), isEmpty()
    • Throws appropriate exceptions for invalid operations
  2. Create a specialized PriorityCustomQueue<T> that:

    • Extends CustomQueue<T>
    • Takes a Comparator<T> in its constructor
    • Overrides enqueue() to maintain elements in sorted order
  3. Implement a QueueProcessor utility class with:

    • A generic method <T> List<T> processQueue(CustomQueue<T> queue, Function<T, T> processor)
    • A generic method <T> void filterQueue(CustomQueue<T> queue, Predicate<T> filter)
  4. Demonstrate bounded type parameters by:

    • Creating a NumberQueue<T extends Number> that only accepts numeric types
    • Adding a method to calculate the sum of all elements

Expected Output:

Regular Queue Operations:
Enqueued: Apple, Banana, Cherry
Queue size: 3
Peek: Apple
Dequeued: Apple
Queue after dequeue: [Banana, Cherry]

Priority Queue Operations:
Enqueued elements in priority order: [1, 3, 5, 7, 9]
Dequeued highest priority: 1
Queue after dequeue: [3, 5, 7, 9]

Queue Processing:
Original queue: [Hello, World, Java]
Processed queue (uppercase): [HELLO, WORLD, JAVA]
Filtered queue (length > 4): [WORLD]

Number Queue:
Sum of elements: 15.5

Exercise 3: Streams API Practice

Task: Data Analysis with Streams

Implement a data analysis system using Java Streams:

  1. Create a Product class with:

    • Fields: id, name, category, price, stock
    • Constructor, getters, and toString method
  2. Create a ProductAnalyzer class with static methods:

    • List<Product> filterByCategory(List<Product> products, String category)
    • double calculateAveragePrice(List<Product> products)
    • Optional<Product> findMostExpensiveProduct(List<Product> products)
    • Map<String, List<Product>> groupByCategory(List<Product> products)
    • Map<String, Double> calculateAveragePriceByCategory(List<Product> products)
  3. Create a ProductInventory class that:

    • Stores a list of products
    • Has methods to add, remove, and update products
    • Uses streams to find products below a certain stock threshold
    • Uses streams to calculate total inventory value
  4. Implement a method that performs a complex analysis:

    • Find the top 3 categories by total value
    • For each category, find the product with the highest price/stock ratio
    • Return the results as a formatted report

Implementation Example:

// Product class
public class Product {
    private int id;
    private String name;
    private String category;
    private double price;
    private int stock;
    
    // Constructor, getters, setters, toString
}

// ProductAnalyzer class
public class ProductAnalyzer {
    public static List<Product> filterByCategory(List<Product> products, String category) {
        return products.stream()
            .filter(product -> product.getCategory().equals(category))
            .collect(Collectors.toList());
    }
    
    public static double calculateAveragePrice(List<Product> products) {
        return products.stream()
            .mapToDouble(Product::getPrice)
            .average()
            .orElse(0.0);
    }
}

Exercise 4: Optional Class Implementation

Task: Implement a User Profile System

Create a user profile system that demonstrates effective use of the Optional class:

  1. Create a UserProfile class with:

    • Required fields: id, username
    • Optional fields: fullName, email, phoneNumber, address
    • Constructor that takes required fields and uses Optional for optional fields
    • Getters that return Optional for optional fields
  2. Create a UserProfileService class with:

    • A map to store user profiles by ID
    • Methods to add, update, and find users
    • A method findUserByUsername(String username) that returns an Optional
  3. Create a UserNotificationService class that:

    • Has methods to send notifications via email, SMS, or mail
    • Uses Optional to handle missing contact information gracefully
    • Implements a method sendNotification(UserProfile user, String message) that tries different notification methods in order of preference

Implementation Example:

public class UserProfile {
    private final int id;
    private final String username;
    private Optional<String> fullName;
    private Optional<String> email;
    private Optional<String> phoneNumber;
    
    public UserProfile(int id, String username) {
        this.id = id;
        this.username = username;
        this.fullName = Optional.empty();
        this.email = Optional.empty();
        this.phoneNumber = Optional.empty();
    }
    
    public Optional<String> getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = Optional.ofNullable(email);
    }
}

Exercise 5: CompletableFuture for Asynchronous Operations

Task: Build a Parallel Data Processing Pipeline

Implement a system that processes data asynchronously using CompletableFuture:

  1. Create a DataFetcher class with methods that simulate API calls:

    • CompletableFuture<List<String>> fetchUserIds()
    • CompletableFuture<UserData> fetchUserData(String userId)
    • CompletableFuture<List<Order>> fetchUserOrders(String userId)
    • Each method should have a random delay to simulate network latency
  2. Create data classes:

    • UserData with fields: userId, name, email
    • Order with fields: orderId, userId, amount, date
  3. Create a DataProcessor class with methods:

    • CompletableFuture<UserData> enrichUserData(UserData userData) - adds additional information
    • CompletableFuture<List<Order>> filterRecentOrders(List<Order> orders) - filters orders from last 30 days
    • CompletableFuture<OrderSummary> calculateOrderStatistics(List<Order> orders) - calculates total, average, etc.

Implementation Example:

public class DataFetcher {
    private final Random random = new Random();
    
    public CompletableFuture<List<String>> fetchUserIds() {
        return CompletableFuture.supplyAsync(() -> {
            // Simulate network delay
            try {
                Thread.sleep(random.nextInt(1000) + 500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return Arrays.asList("user1", "user2", "user3");
        });
    }
    
    public CompletableFuture<UserData> fetchUserData(String userId) {
        return CompletableFuture.supplyAsync(() -> {
            // Simulate network delay
            try {
                Thread.sleep(random.nextInt(800) + 200);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return new UserData(userId, "User " + userId, userId + "@example.com");
        });
    }
}

Exercise 6: Reflection API for Dynamic Test Frameworks

Task: Create a Simple Test Framework

Implement a simple test framework using the Reflection API:

  1. Create annotation classes:

    • @Test - marks a method as a test
    • @BeforeEach - marks a method to run before each test
    • @AfterEach - marks a method to run after each test
    • @BeforeAll - marks a method to run once before all tests
    • @AfterAll - marks a method to run once after all tests
    • @Ignore - marks a test to be skipped
  2. Create a TestRunner class that:

    • Takes a class as input
    • Uses reflection to find all methods with the above annotations
    • Executes the tests in the correct order
    • Collects and reports test results
  3. Create a TestResult class to store:

    • Test name
    • Pass/fail status
    • Execution time
    • Error message if failed

Implementation Example:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BeforeEach {
}

public class TestRunner {
    public void runTests(Class<?> testClass) {
        Method[] methods = testClass.getDeclaredMethods();
        
        // Find and execute @BeforeAll methods
        Arrays.stream(methods)
            .filter(method -> method.isAnnotationPresent(BeforeAll.class))
            .forEach(this::invokeMethod);
            
        // Find and execute @Test methods
        Arrays.stream(methods)
            .filter(method -> method.isAnnotationPresent(Test.class))
            .forEach(this::executeTest);
    }
}

Exercise 7: Comprehensive Project - E-commerce Order Processing System

Task: Implement a Complete E-commerce Order Processing System

Create a comprehensive system that demonstrates all the concepts learned:

  1. Create domain models:

    • Product (id, name, price, category, inventory)
    • Customer (id, name, email, address)
    • Order (id, customer, items, status, date)
    • OrderItem (product, quantity, price)
    • Payment (id, order, amount, status, method)
  2. Implement service classes:

    • ProductService - manages products and inventory
    • CustomerService - manages customer information
    • OrderService - processes orders
    • PaymentService - handles payments
    • NotificationService - sends notifications
  3. Use Java features:

    • Inheritance and polymorphism for payment methods
    • Generics for service implementations
    • Streams for order processing and reporting
    • Optional for handling nullable fields
    • CompletableFuture for asynchronous operations
    • Reflection for a plugin system

System Architecture:

// Domain Model Example
public class Order {
    private final String id;
    private final Customer customer;
    private final List<OrderItem> items;
    private OrderStatus status;
    private final LocalDateTime orderDate;
    
    public Order(String id, Customer customer) {
        this.id = id;
        this.customer = customer;
        this.items = new ArrayList<>();
        this.status = OrderStatus.PENDING;
        this.orderDate = LocalDateTime.now();
    }
    
    public double calculateTotal() {
        return items.stream()
            .mapToDouble(item -> item.getPrice() * item.getQuantity())
            .sum();
    }
}

// Service Example
public class OrderService {
    private final PaymentService paymentService;
    private final NotificationService notificationService;
    
    public CompletableFuture<Order> processOrder(Order order) {
        return CompletableFuture
            .supplyAsync(() -> validateOrder(order))
            .thenCompose(this::processPayment)
            .thenCompose(this::updateInventory)
            .thenCompose(this::sendNotification);
    }
}

Exercise 8: Debugging and Troubleshooting

Task: Debug and Fix a Broken Application

You are provided with a broken e-commerce application that has several issues. Your task is to identify and fix the problems:

  1. The application has the following issues:

    • Memory leak in the product cache
    • Thread safety issues in the order processing
    • Null pointer exceptions in the customer service
    • Performance bottleneck in the reporting module
    • Resource leaks in the file processing
  2. Use debugging techniques:

    • Add logging to track execution flow
    • Use conditional breakpoints
    • Analyze stack traces
    • Profile memory usage
    • Identify threading issues
  3. Fix the issues using:

    • Proper resource management with try-with-resources
    • Thread-safe collections and synchronization
    • Null checking and Optional
    • Efficient algorithms and data structures
    • Memory-efficient caching

Debugging Example:

// Before: Memory leak in cache
public class ProductCache {
    private final Map<String, Product> cache = new HashMap<>();
    
    public Product getProduct(String id) {
        return cache.computeIfAbsent(id, this::loadProduct);
    }
}

// After: Fixed with LRU cache
public class ProductCache {
    private final Map<String, Product> cache = new LinkedHashMap<String, Product>(100, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Product> eldest) {
            return size() > 100;
        }
    };
    
    public synchronized Product getProduct(String id) {
        return cache.computeIfAbsent(id, this::loadProduct);
    }
}

Exercise 9: Integration with Test Automation Frameworks

Task: Create a Test Automation Framework Integration

Develop a bridge between your Java code and popular test automation frameworks:

  1. Create a WebDriverWrapper class that:

    • Encapsulates WebDriver operations
    • Provides fluent, type-safe methods
    • Handles exceptions gracefully
    • Implements automatic waiting strategies
  2. Implement a PageObjectBase class that:

    • Uses generics for fluent navigation
    • Provides common page object functionality
    • Uses Java 8 features for element interactions
  3. Create a TestDataGenerator utility that:

    • Uses streams to generate test data
    • Implements builder pattern for complex objects
    • Provides methods for common test data needs

Framework Integration Example:

public class WebDriverWrapper {
    private final WebDriver driver;
    private final WebDriverWait wait;
    
    public WebDriverWrapper(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }
    
    public WebDriverWrapper navigateTo(String url) {
        driver.get(url);
        return this;
    }
    
    public WebDriverWrapper clickElement(By locator) {
        wait.until(ExpectedConditions.elementToBeClickable(locator)).click();
        return this;
    }
    
    public WebDriverWrapper enterText(By locator, String text) {
        WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
        element.clear();
        element.sendKeys(text);
        return this;
    }
}

public abstract class PageObjectBase<T extends PageObjectBase<T>> {
    protected final WebDriverWrapper driver;
    
    public PageObjectBase(WebDriverWrapper driver) {
        this.driver = driver;
    }
    
    @SuppressWarnings("unchecked")
    protected T self() {
        return (T) this;
    }
    
    public T waitForPageLoad() {
        // Wait for page-specific elements
        return self();
    }
}

Exercise 10: Performance Optimization Challenge

Task: Optimize a Slow Data Processing Application

You are given a slow data processing application that needs optimization:

  1. The application processes a large dataset (millions of records) with operations:

    • Filtering records based on multiple criteria
    • Transforming data into different formats
    • Aggregating and summarizing results
    • Generating reports
  2. Optimize the application using:

    • Parallel streams for CPU-bound operations
    • CompletableFuture for I/O-bound operations
    • Efficient data structures for lookups
    • Lazy evaluation where appropriate
    • Memory-efficient processing for large datasets
  3. Implement benchmarking to:

    • Measure performance before and after optimization
    • Compare different optimization strategies
    • Identify bottlenecks
    • Verify correctness of results

Performance Optimization Example:

// Before: Sequential processing
public class DataProcessor {
    public List<ProcessedRecord> processRecords(List<RawRecord> records) {
        return records.stream()
            .filter(this::isValid)
            .map(this::transform)
            .map(this::enrich)
            .collect(Collectors.toList());
    }
}

// After: Parallel processing with optimization
public class OptimizedDataProcessor {
    private final ForkJoinPool customThreadPool;
    
    public OptimizedDataProcessor(int parallelism) {
        this.customThreadPool = new ForkJoinPool(parallelism);
    }
    
    public List<ProcessedRecord> processRecords(List<RawRecord> records) {
        try {
            return customThreadPool.submit(() ->
                records.parallelStream()
                    .filter(this::isValid)
                    .map(this::transform)
                    .map(this::enrich)
                    .collect(Collectors.toList())
            ).get();
        } catch (Exception e) {
            throw new RuntimeException("Processing failed", e);
        }
    }
}

Summary

These practical exercises cover all the key topics in the Advanced Java Concepts for Test Automation course and provide hands-on experience with real-world scenarios relevant to test automation. Each exercise builds upon the previous ones, culminating in a comprehensive project that demonstrates mastery of all concepts.

Remember to:

Previous: Reflection API for Dynamic Test Frameworks Next: Course Overview