Unit 10

Project Recursion Game

Project Instructions - Click Here

AP CSA Recursion Project:

Project Overview

You will choose ONE of the following recursive problem-solving challenges:

Option A: Maze Solver

Option B: Sudoku Solver

Your goal is to design and implement a recursive backtracking algorithm that solves your chosen problem.


Important Expectation

You are not being graded on finishing fast—you are being graded on having a deep understanding of recursion.


OPTION A: Maze Solver

Problem: Find a path from Begin (B) to End (E) in a maze.

REQUIREMENTS

You must:

  • Minimum size of the maze must be 5 x 5

  • Use recursion with backtracking

  • Write clean, organized methods

  • Include at least 2 test cases

  • Explain your algorithm clearly using comments

  • Use Java Time API

  • Project Block Header

  • Test Cases

  • Use a 2D array

    • Represent maze as char[][]

    • Symbols:

      • 'B' = Begin

      • 'E' = End

      • '|' = Wall

      • '-' = Open space

    • Mark solution path (e.g., '*')

Required Method

public static boolean solveMaze(char[][] maze, int row, int col)

Optional Extensions for Maze

  • Count number of possible paths

  • Shortest path only

  • GUI Extension

    • Use JPanel grid

    • Color path vs walls


Example

Input

B - |
- - |
| - E

Output

B * |
- * |
| * E

Key Ideas

  • Try moving (up/down/left/right)

  • If blocked → return false

  • If correct path → continue

  • If dead end → backtrack (undo move)


OPTION B: Sudoku Solver

Problem Fill a 9×9 grid so that:

  • Each row has 1–9

  • Each column has 1–9

  • Each 3×3 box has 1–9

Requirements

  • Use a 2D array

    • Use int[][]

  • Empty cells = 0

  • Must validate placement

  • Use Java Time API

  • Project Block Header

  • Test Cases

Optional Extensions for Sudoku

  • Count all solutions

  • Generate a puzzle

  • GUI Extension

  • Use JTextField[][]

  • Button to trigger solver


Required Method

public static boolean solveSudoku(int[][] board)

Example

Input

5 3 0
6 0 0
0 9 8

Output

5 3 4
6 7 2
1 9 8

Key Ideas

  • Find empty cell

  • Try numbers 1–9

  • If valid → recurse

  • If it fails later → backtrack


Regardless of the Program that you chose, you must do the following:

REQUIRED EXPLANATION

  1. What is your base case?

  2. Where does backtracking happen?

  3. What would cause your algorithm to fail?

  4. How does the call stack behave?

TESTING REQUIREMENT

  • Minimum 2 different inputs

  • One must be more complex

  • Show before + after output


Project Rubric

Category

Points

Description

Recursive Logic

3

Correct base case + backtracking

Program Comments

1

Appropriate comments on key concepts

Correctness

1

Produces valid solution

2D Array Use

1

Proper traversal/manipulation

Code Organization

1

Clean structure, readable

Testing

1

Multiple test cases

Explanation

2

Clear understanding of recursion

Total Points:

10


Note: Common Problems

  • “It runs forever” → you are missing base case

  • “It almost works” → you are not backtracking properly

  • “It skips spots” → Your program is not written correctly – bad indexing

  • “It works once only” → You are modifying shared data incorrectly


10.1 Recursion

Goals

  • Learn about methods that call themselves.

  • Trace the results of a recursive call to determine the results.

  • Rewrite a recursive algorithm as an interactive method.

What is Recursion?

Recursion is when a method calls itself to solve a problem. A helpful way to think about recursion is to imagine a task that repeats in smaller and smaller versions of itself. Each step solves a piece of the problem and passes the rest along. A classic analogy is Russian nesting dolls. You open one doll to find a smaller one inside, and keep going until you reach the smallest doll that cannot be opened. That smallest doll is where the process stops. In recursion, each “smaller doll” is a recursive call. The smallest doll is the base case.

Recursion Sample

The Two Key Parts that every recursive method must have:

  1. Base Case (Stopping Point) This is the condition where the method stops calling itself. Without it, the program would run forever (and crash).

  2. Recursive Case (The Work) This is where the method calls a small part of the problem within method itself with a smaller input.

The basic structure of a recursive program will look something like this:

public static void Main()
  // base case
  if (baseCaseCondition) { 
    baseCaseSteps
  } 
  else {
    do something
    // recursive call
    recursiveMethod(); 
  }
}

Recursion works because:

  • Each step makes the problem smaller

  • Eventually, it reaches a case we already know how to solve

  • Then the answers “stack back up”

When Should You Use Recursion?

  • Can be broken into smaller identical problems

  • Have a natural “divide and conquer” structure

  • Involve trees, hierarchies, or nested data

Examples:

  • Factorials

  • Searching (Binary Search)

  • Sorting (Merge Sort)

  • File systems and organization charts


Example: Factorial

The factorial of a number means multiplying it by all smaller positive integers.

5! = 5 × 4 × 3 × 2 × 1 = 120

Recursive Thinking

Instead of doing it all at once, think like this:

  • 5! = 5 × 4!

  • 4! = 4 × 3!

  • 3! = 3 × 2!

  • 2! = 2 × 1!

  • 1! = 1 ← base case

Using recursion:

public class Factorial {

    public static int factorial(int n) {
        if (n == 1) {
            return 1; // Base case
        } else {
            return n * factorial(n - 1); // Recursive step
        }
    }

    public static void main(String[] args) {
        int number = 5;
        int result = factorial(number);
        System.out.println("Factorial of " + number + " is: " + result);
    }
}

Output

Factorial of 5 is 120

How the Calls Work (Step-by-Step)

When you call factorial(5), here’s what happens:

  • factorial(5) → 5 × factorial(4)

  • factorial(4) → 4 × factorial(3)

  • factorial(3) → 3 × factorial(2)

  • factorial(2) → 2 × factorial(1)

  • factorial(1) → 1 (base case)

Now the program builds the answer back up:

  • factorial(2) = 2 × 1 = 2

  • factorial(3) = 3 × 2 = 6

  • factorial(4) = 4 × 6 = 24

  • factorial(5) = 5 × 24 = 120


In Java, multiple inheritance can be achieved through interfaces, and we can use an ArrayList along with recursion to demonstrate the concept. Below is a program that models the hierarchy of employees in a company using multiple inheritance through interfaces. We’ll also use recursion to calculate the total salary of all employees in the hierarchy.

import java.util.ArrayList;

interface Employee {
    double getSalary();
}

class Manager implements Employee {
    private double baseSalary;
    private ArrayList<Employee> subordinates;

    public Manager(double baseSalary) {
        this.baseSalary = baseSalary;
        this.subordinates = new ArrayList<>();
    }

    public void addSubordinate(Employee employee) {
        subordinates.add(employee);
    }

    public double getSalary() {
        double totalSalary = baseSalary;
        for (Employee subordinate : subordinates) {
            totalSalary += subordinate.getSalary();
        }
        return totalSalary;
    }
}

class RegularEmployee implements Employee {
    private double salary;

    public RegularEmployee(double salary) {
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }
}

public class MultipleInheritanceWithArrayListAndRecursion {
    public static void main(String[] args) {
        RegularEmployee employee1 = new RegularEmployee(3000);
        RegularEmployee employee2 = new RegularEmployee(2500);
        RegularEmployee employee3 = new RegularEmployee(2000);

        Manager manager1 = new Manager(5000);
        manager1.addSubordinate(employee1);
        manager1.addSubordinate(employee2);

        Manager manager2 = new Manager(4500);
        manager2.addSubordinate(manager1);
        manager2.addSubordinate(employee3);

        System.out.println("Total salary for manager1 and his subordinates: $" + manager1.getSalary());
        System.out.println("Total salary for manager2 and his subordinates: $" + manager2.getSalary());
    }
}

Sample Output:

Total salary for manager1 and his subordinates: $10500.0
Total salary for manager2 and his subordinates: $17000.0

In this program, we have three classes: Employee, Manager, and RegularEmployee. The Employee interface ensures that both Manager and RegularEmployee classes have a getSalary() method. The Manager class contains an ArrayList of subordinates, and the addSubordinate() method is used to add employees to the manager’s team.

The main method creates instances of employees and managers, arranges them in a hierarchy, and then calculates the total salary using recursion through the getSalary() method of the Manager class. The program outputs the total salaries for the managers and their subordinates.


Let's look at this in another way!

Manager:

  • Has a base salary.

  • Has a list of subordinates (other Employee objects — either RegularEmployee’s or other Manager’s).

  • When getSalary() is called, it returns:

    • Its own base salary

    • Plus, the sum of all subordinates’ salaries (recursively).

manager1: 5000 (own) + 3000 (employee1) + 2500 (employee2) = 10500

manager2: 4500 (own) + 10500 (manager1 + his subs) + 2000 (employee3) = 17000

public double getSalary() {
    double totalSalary = baseSalary;
    for (Employee subordinate : subordinates) {
        totalSalary += subordinate.getSalary();  // Recursive call happens here
    }
    return totalSalary;
}

Another way to look at this:

RegularEmployee e1 = new RegularEmployee(3000);
RegularEmployee e2 = new RegularEmployee(2500);
RegularEmployee e3 = new RegularEmployee(2000);

Manager m1 = new Manager(5000);
m1.addSubordinate(e1);
m1.addSubordinate(e2);

Manager m2 = new Manager(4500);
m2.addSubordinate(m1);
m2.addSubordinate(e3);

Now, calling m2.getSalary() triggers this:

  • m2.getSalary()

    • totalSalary = 4500

    • Loop through subordinates:

      • First subordinate is m1 → m1.getSalary()

        • totalSalary = 5000

        • Loop through subordinates:

          • e1.getSalary() = 3000

          • e2.getSalary() = 2500

        • m1.getSalary() returns 10500

    • Add 10500 to m2’s & e3’s salary → 4500 + 10500 + 2000 = 17000


A recursive method is like saying:
“I’ll solve a small part of the problem, and trust that the rest will be solved the same way.”

Recursion is not just a programming trick, it is a way of thinking. Instead of trying to solve a large problem all at once, recursion teaches you to:

  • Break problems into smaller, manageable pieces

  • Clearly define when to stop

  • Trust a repeating process to build the final solution

At first, recursion can feel confusing because the method calls itself. But once you learn to identify the base case and the recursive step, it becomes a powerful and elegant tool. As you continue practicing, you’ll start to recognize patterns where recursion is the natural solution—and in many of those cases, it leads to code that is simpler, cleaner, and easier to understand than traditional loops. The real goal is not just to write recursive programs, but to think recursively—because that skill applies far beyond programming.

10.2 Recursive Searching and Sorting

Recursive searching and sorting are techniques in Java where the searching or sorting algorithm calls itself repeatedly on smaller portions of the data until the desired result is found or the data is sorted. Here are explanations and examples of recursive searching and sorting in Java:

Recursive Searching (Binary Search): Binary search is a commonly used searching algorithm that works efficiently on sorted arrays. It divides the array into two halves and compares the target element with the middle element. Based on the comparison, it either continues searching in the left half or the right half of the array.

Here’s an example of recursive binary search in Java:

public class BinarySearch {
    public static int binarySearch(int[] array, int target, int low, int high) {
        if (low > high) {
            return -1; // target element not found
        }
        
        int mid = (low + high) / 2;
        
        if (array[mid] == target) {
            return mid; // target element found at mid index
        } else if (array[mid] > target) {
            return binarySearch(array, target, low, mid - 1); // search in the left half
        } else {
            return binarySearch(array, target, mid + 1, high); // search in the right half
        }
    }

    public static void main(String[] args) {
        int[] array = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
        int target = 12;

        int index = binarySearch(array, target, 0, array.length - 1);
        if (index != -1) {
            System.out.println("Element found at index " + index);
        } else {
            System.out.println("Element not found");
        }
    }
}

Output

Element found at index 5

In this example, the binarySearch() method performs a recursive binary search on the sorted array to find the target element. It takes the array, target element, low index, and high index as parameters. If the low index becomes greater than the high index, it means the target element is not present in the array, so it returns -1. Otherwise, it calculates the mid index and compares the element at that index with the target element. If they match, it returns the index. If the element at mid is greater than the target element, it recursively calls binarySearch() on the left half of the array, otherwise on the right half.

Recursive Sorting (Merge Sort): Merge sort is a popular sorting algorithm that uses a divide-and-conquer approach. It divides the array into two halves, recursively sorts each half, and then merges the two sorted halves into a single sorted array. Here’s an example of recursive merge sort in Java:

public class MergeSort {
    public static void mergeSort(int[] array, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(array, left, mid); // sort left half
            mergeSort(array, mid + 1, right); // sort right half
            merge(array, left, mid, right); // merge the two sorted halves
        }
    }

    public static void merge(int[] array, int left, int mid, int right) {
        int n1 = mid - left + 1;
        int n2 = right - mid;

        int[] leftArray = new int[n1];
        int[] rightArray = new int[n2];

        for (int i = 0; i < n1; i++) {
            leftArray[i] = array[left + i];
        }
        for (int j = 0; j < n2; j++) {
            rightArray[j] = array[mid + 1 + j];
        }

        int i = 0, j = 0, k = left;

        while (i < n1 && j < n2) {
            if (leftArray[i] <= rightArray[j]) {
                array[k] = leftArray[i];
                i++;
            } else {
                array[k] = rightArray[j];
                j++;
            }
            k++;
        }

        while (i < n1) {
            array[k] = leftArray[i];
            i++;
            k++;
        }

        while (j < n2) {
            array[k] = rightArray[j];
            j++;
            k++;
        }
    }

    public static void main(String[] args) {
        int[] array = { 8, 3, 1, 5, 9, 2, 6, 4, 7 };
        int n = array.length;

        System.out.println("Original Array: " + Arrays.toString(array));

        mergeSort(array, 0, n - 1);

        System.out.println("Sorted Array: " + Arrays.toString(array));
    }
}

Output

Original Array: [8, 3, 1, 5, 9, 2, 6, 4, 7]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9]

In this example, the mergeSort() method performs recursive merge sort on the array. It takes the array, left index, and right index as parameters. It recursively calls itself to sort the left half and the right half of the array, and then merges the two sorted halves using the merge() method. The merge() method merges the two sorted halves into a single sorted array.

Both examples demonstrate the power of recursion in searching and sorting algorithms. Recursive searching allows for efficient retrieval of elements from sorted data, while recursive sorting algorithms provide efficient ways to sort arrays or collections.


10.3 Lambda Expressions

AP CSA and Oracle Foundations

Learning Goals

  • Foundations: Lambda syntax, functional interfaces (Predicate, Consumer, Function), and Stream basics.

  • Application: “Galactic Zoo” (Static data analysis).

  • Simulation: “Eco-Economy” (Dynamic state mutation & market logic).

  • Extension: Advanced OOP patterns (MarketEvent enum).

Part 1: Lambda Fundamentals

A lambda expression in Java is a shortcut for writing a small method without naming it. You use it when you need to pass behavior (what to do) into another method.

Old Way (loop)

lambda + stream

Image

Image

Below are methods that are defined in the Java Stream API:

  • filter() needs a rule → you give it a lambda

  • map() needs a transformation → you give it a lambda

  • forEach() needs an action → you give it a lambda

How it Works The syntax consists of three main parts:

  • Parameters: The input variables

  • Arrow Token: The -> operator

  • Body: The code or expression

Basic Syntax

  • Zero parameters: ( ) -> System.out.println(“Hello”); //parentheses with no method was intentional

  • One parameter: (x) -> x * x;

  • Multiple parameters: (x, y) -> x + y;

// using a list
List<String> names = Arrays.asList("Zebra", "Apple", "Mango");
// Using the arrow operator to define the sorting logic
names.sort((a, b) -> a.compareTo(b)); 

// using switch case
int days = switch (month) {
    case "FEBRUARY" -> 28;
    case "APRIL", "JUNE" -> 30;
    default -> 31;
};

Key Rules:

  • Must implement a Functional Interface (an interface with exactly one abstract method).

  • The compiler infers types from context.

  • Syntax: (parameters) -> { body }

Common Functional Interfaces

Interface

Translation

Example

Predicate

“Ask a YES/NO question”

a -> a.getDangerLevel() < 3

Consumer

“Do something (no return)”

a -> System.out.println(a)

Function

“Change data into new data”

a -> a.getName()

Comparator

“Decide order”

(a,b) -> a.getIntelligence() - b.getIntelligence()


Concept 1: Lambdas WITHOUT streams

Predicate<Alien> isSafe = a -> a.getDangerLevel() < 3;
System.out.println(isSafe.test(alien));

Concept 2: Lambdas WITH simple collections

aliens.forEach(a -> System.out.println(a.getName()));

Concept 3: Streams

aliens.stream().filter(...).collect(...)

When NOT to Use Lambdas:

  • Logic is long or complex

  • You need multiple steps or variables

  • Readability gets worse


Project 1 – The “Galactic Zoo” Manager

Theme: Analyzing alien populations on Space Station Alpha. Constraint: NO for, while, or do-while loops for data processing.

Requirements

  • Data Structure: class Alien (name, species, intelligenceLevel, dangerLevel, homePlanet, hasWings).

  • Population: Generate a list of 20+ unique aliens.

Features to Implement:

  • The Danger Filter (Predicate): Identify “Safe Visitors” (Danger < 3 AND Intelligence > 5).

  • The Intelligence Sort (Comparator): Sort by intelligence (desc), then name (asc).

    • Hint: Comparator.comparing(…).thenComparing(…)

  • The Planet Report (Collectors.groupingBy): Group by planet and find the one with the highest average intelligence.

  • The “Wing” Transformation (Function): Map to “Can Fly” or “Grounded”.

  • Commander’s Summary (reduce): Calculate total Threat Score (Sum of danger * intelligence).

Visual Model

List → Stream → Filter → Map → Result

Aliens
  ↓
.filter(a -> safe)
  ↓
.map(a -> name)
  ↓
.collect()

Step 1:

aliens.stream()
      .filter(a -> ???) // What rule do you want to apply here?
      .forEach(a -> System.out.println(a));
filter()
a -> a.getDangerLevel() < 3

Step 2:

Write full filter + forEach

Step 3:

Build full features

Level Up Concept: Mutation vs. Immutability Teacher Note: In pure functional programming, we avoid changing data. However, real-world simulations (like our Eco-Economy) often require updating object state (e.g., changing a price). The ‘Hybrid’ Approach: We use streams to traverse data efficiently, but we intentionally use forEach to mutate object fields. This is a pragmatic pattern used in industry for simulations, even if it breaks ‘pure’ functional rules. Rule of Thumb: Use map() for creating new data. Use forEach() with setters only when you must update existing objects.


Project 2 – The “Eco-Economy” Simulator

Theme: Managing a dynamic renewable energy market. Goal: Simulate market fluctuations (Weather, Policy, Demand) and trigger alerts. This project is about applying lambdas inside a larger system.

Setup: The Data Factory

Generate realistic initial data.

public class EnergyDataFactory {
    public static double randomInRange(double min, double max) {
        return min + (Math.random() * (max - min));
    }

    public static List<EnergyResource> generateInitialMarket(int count) {
        List<EnergyResource> market = new ArrayList<>();
        String[] types = {"Solar", "Wind", "Hydro", "Nuclear"};
        String[] names = {"SunRay", "Zephyr", "RiverFlow", "AtomCore", "Breeze", "Tide", "GeoThermal"};

        for (int i = 0; i < count; i++) {
            String type = types[(int)(Math.random() * types.length)];
            
            // Set realistic BASE ranges based on research
            double baseCost = 0;
            double baseEfficiency = 0;
            
            if (type.equals("Solar")) {
                baseCost = randomInRange(40, 60);
                baseEfficiency = randomInRange(0.6, 0.9);
            } else if (type.equals("Wind")) {
                baseCost = randomInRange(30, 50);
                baseEfficiency = randomInRange(0.5, 0.85);
            } else if (type.equals("Hydro")) {
                baseCost = randomInRange(80, 120);
                baseEfficiency = randomInRange(0.8, 0.95);
            } else { // Nuclear
                baseCost = randomInRange(200, 300);
                baseEfficiency = randomInRange(0.9, 0.98);
            }

            EnergyResource res = new EnergyResource(
                names[(int)(Math.random() * names.length)] + "_" + i,
                type,
                baseCost,
                baseEfficiency,
                true, // Initially available
                (int)randomInRange(10, 100) // Carbon footprint
            );
            market.add(res);
        }
        return market;
    }
}

The Simulation Loop

Run this loop for 10 “Days” to simulate market volatility.

List<EnergyResource> resources = EnergyDataFactory.generateInitialMarket(15);

// Print Initial State
System.out.println("=== INITIAL MARKET STATE ===");
resources.forEach(r -> System.out.println(r));

for (int day = 1; day <= 10; day++) {
    System.out.println("\n========== DAY " + day + " ==========");

    // Step A: Weather Fluctuations (Call your existing method)
    applyWeatherFluctuations(resources); 

    // Step B: Policy Shocks
    applyPolicyShocks(resources); 

    // Step C: Demand Surge
    applyDemandSurge(resources); 

    // Step D: Alerts
    generateAlerts(resources); 
}

Step B: Apply Policy Shocks (Consumer)

Scenario: The government subsidizes a random energy type, lowering its cost.

public static void applyPolicyShocks(List<EnergyResource> resources) {
    String[] policyTypes = {"Solar", "Wind", "Hydro", "Nuclear"};
    String targetResource = policyTypes[(int)(Math.random() * policyTypes.length)];
    double discountRate = 0.20; // 20% discount

    System.out.println("POLICY UPDATE: Government subsidizes " + targetResource + "!");

    // Use a Consumer to mutate state
    resources.forEach(resource -> {
        if (resource.getResourceType().equals(targetResource)) {
            double newCost = resource.getCostPerUnit() * (1.0 - discountRate);
            resource.setCostPerUnit(newCost);
            System.out.println("   -> " + resource.getName() + " cost dropped to $" + String.format("%.2f", newCost));
        }
    });
}

Step C: Apply Demand Surge (Function)

Scenario: Market demand fluctuates, changing the “Market Price” for all resources.

public static void applyDemandSurge(List<EnergyResource> resources) {
    // Simulate market condition (0.8 to 1.5)
    double demandMultiplier = 0.8 + (Math.random() * 0.7); 
    String marketCondition = (demandMultiplier > 1.2) ? "HIGH DEMAND" : "LOW DEMAND";

    System.out.println("MARKET STATUS: " + marketCondition + " (Multiplier: " + String.format("%.2f", demandMultiplier) + ")");

    // Calculate new prices using a Function
    // NOTE: We handle division by zero to prevent crashes!
    List<Double> newPrices = resources.stream()
        .map(resource -> {
            double eff = resource.getEfficiencyRating();
            // Safety check: if efficiency is 0, treat as 1 to avoid ArithmeticException
            double safeEff = (eff == 0) ? 1 : eff; 
            double baseValue = resource.getCostPerUnit() / safeEff;
            return baseValue * demandMultiplier;
        })
        .collect(Collectors.toList());

    // Sync the calculated prices back to the objects (Mutation!)
    for (int i = 0; i < resources.size(); i++) {
        resources.get(i).setCurrentMarketPrice(newPrices.get(i));
    }
    System.out.println("Prices updated for all resources.");
}
Step D: Alert Generation (Predicate + Consumer)
Scenario: Identify profitable trades (Market Price < 110% of Base Cost).
public static void generateAlerts(List<EnergyResource> resources) {
    // Predicate: Is this a good deal?
    Predicate<EnergyResource> isProfitableTrade = resource -> {
        return resource.getCurrentMarketPrice() < (resource.getCostPerUnit() * 1.1);
    };

    System.out.println("SCANNING FOR PROFITABLE TRADES...");

    // Filter and Alert
    long count = resources.stream()
        .filter(isProfitableTrade)
        .peek(r -> { // Debugging: see what's being processed
             double profit = r.getCostPerUnit() - r.getCurrentMarketPrice();
             System.out.println("ALERT: Buy " + r.getName() + "! " +
                   "Price: $" + String.format("%.2f", r.getCurrentMarketPrice()) + 
                   " | Profit: $" + String.format("%.2f", profit));
        })
        .count(); // Count triggers the stream

    if (count == 0) {
        System.out.println("No profitable trades found today. Wait for the next cycle.");
    }
}

Debugging Checklist for Students

Before submitting, check these common pitfalls:

  • Division by Zero: Did you handle efficiencyRating == 0 in your math?

    • Fix: double safeEff = (eff == 0) ? 1 : eff;

  • Null Pointers: Are you checking for null before calling .equals()?

    • Fix: “Solar”.equals(resource.getType()) (Put string literal first).

  • State Sync: Did you remember to update the object after mapping?

    • Reminder: map() creates a new list of numbers. You must loop to set them back into the objects.

  • Side Effects: Are you using forEach only for printing or setting values?

    • Check: Don’t use forEach to build a new list; use collect().

Lambda Project Rubric (10 Points)

Criteria

Excellent(2)

Proficient(1)

Developing(0)

Lambda Syntax

Correct -> usage, method references where appropriate.

Minor syntax errors or overuse of anonymous classes.

Relies on loops or anonymous classes; syntax errors.

Functional Interfaces

Distinct use of Predicate, Function, Consumer.

Confuses roles (e.g., using Consumer for filtering).

Logic embedded directly in loops without abstraction.

Stream Operations

Logical chain: filter → map → collect/reduce.

Misses a step or chains incorrectly.

Uses streams only for forEach; procedural logic.

Logic & Accuracy

Business logic (math, sorting) is accurate.

Logic mostly correct but fails edge cases.

Results do not match requirements.

Code Quality

Clean, commented, meaningful variable names.

Readable but lacks comments.

Messy, hard to read, no comments.

Conceptual Understanding

Can clearly explain what a lambda does and why it is used

Is able to explain general lambda functionality

Misuses concepts or misstates functionality

Bonus Challenge (Extra Credit)

  • Method References: Replace x -> System.out.println(x) with System.out::println.

  • Advanced OOP: Refactor Steps B, C, and D into a MarketEvent enum with an apply() method.

  • Research: To make the simulation realistic, research:

    • Solar: How much does efficiency drop at night? (Code: if (night) efficiency = 0;)

    • Wind: Do turbines work better in storms? (Code: if (storm) efficiency *= 1.5;)

    • Nuclear: What is the typical cost vs. Solar? (Set baseCost accordingly).

Common Student Misconceptions

  • “Lambdas replace all loops” → Correction: Lambdas replace data processing loops. You still need loops for initialization or non-stream logic.

  • “Streams are immutable” → Correction: Streams process data immutably, but the objects inside can be mutated if the logic requires it (as seen in the Eco-Economy project).

  • “Lambdas are slower” → Correction: Performance is comparable to anonymous classes; readability is the primary benefit.

This unit is designed to bridge the gap between abstract syntax and real-world software engineering, preparing students for both the AP CSA exam and future professional development.