FRQ
› csapa
BoxOfCandy: 2023 FRQ 4
A step-by-step solution to the 2023 AP CSA FRQ 4 (BoxOfCandy), covering searching a 2D array of objects for null gaps, moving values within a column, and traversing a grid in reverse row order in Java.
A grid of candy where some spots are empty and others hold real objects sets up this AP Computer Science A free-response question — one method shuffles a single piece of candy up to the front of its column, the other hunts through the whole grid in a specific order to pull out one piece by name.
What This FRQ Tests
- AP CSA units: Unit 8 (2D Array) and Unit 4 (Iteration)
- Core skill: treating a 2D array of objects as a grid where a "gap" is represented by
null, and searching or moving values around those gaps - Secondary skill: traversing a 2D array in a non-default order (last row to first, not first row to last) because the problem specifically defines it that way
- Official category: "2D Array" — always FRQ 4 on the AP CSA exam
The Setup
- The given
Candyclass has one method:String getFlavor()— you never modify this class. BoxOfCandyholds one field:private Candy[][] box— a rectangular grid where each spot is either aCandyobject ornull(empty).- You're asked to write two methods:
boolean moveCandyToFirstRow(int col)— makes sure row0of columncolholds a piece of candy, moving one up if needed; returnsfalseonly if the whole column is emptyCandy removeNextByFlavor(String flavor)— searches the grid from the last row to the first, left to right within each row, removes and returns the first matching piece of candy, or returnsnullif none is found
Part (a): Writing moveCandyToFirstRow(int col)
The Rule, Broken Down
- If row
0ofcolalready has candy, do nothing and returntrue. - Otherwise, search the rest of column
col(row1downward) for the first piece of candy found. - If one is found, move it into row
0ofcol, set its old spot tonull, and returntrue. - If the entire column has no candy anywhere, return
falseand leaveboxunchanged.
Step-by-Step Approach
- Check
box[0][col]first. If it isn'tnull, the job is already done — returntrueimmediately. - Otherwise, loop down the column starting at row
1. - At the first non-
nullspot found, copy that candy intobox[0][col], set the original spot back tonull, and returntrueright away. - If the loop finishes without finding any candy, return
false.
The Code
public boolean moveCandyToFirstRow(int col)
{
if (box[0][col] != null)
{
return true;
}
for (int row = 1; row < box.length; row++)
{
if (box[row][col] != null)
{
box[0][col] = box[row][col];
box[row][col] = null;
return true;
}
}
return false;
}
Why Each Piece Matters
- The
box[0][col] != nullcheck comes first, on its own, before any loop runs. This is what makes an already-filled row 0 a true no-op — nothing gets rearranged that doesn't need to be. - The search loop starts at
row = 1, notrow = 0. Row 0 was already ruled out by the check above; re-checking it would be redundant (and re-reading anullthere wouldn't cause a bug, but it wastes an iteration). - Both assignments —
box[0][col] = box[row][col]andbox[row][col] = null— are required. Skipping the second one would leave the same piece of candy appearing in two places at once, which breaks the "moved" behavior. - Returning immediately after the move (rather than continuing to search) means the first piece of candy found scanning downward is the one moved — matching one of the two equally acceptable outcomes the question describes for column 2.
Tracing the Example
Using the question's sample grid (rows 0–3, columns 0–2):
| Row | Col 0 | Col 1 | Col 2 |
|---|---|---|---|
| 0 | (empty) | "lime" | (empty) |
| 1 | (empty) | "orange" | (empty) |
| 2 | (empty) | (empty) | "cherry" |
| 3 | (empty) | "lemon" | "grape" |
moveCandyToFirstRow(0):box[0][0]isnull. The loop checks rows 1, 2, and 3 of column 0 — allnull. The loop finishes, returning false, andboxis unchanged — matches the question.moveCandyToFirstRow(1):box[0][1]already holds"lime"— the very first check catches this, returning true immediately with nothing changed — matches the question.moveCandyToFirstRow(2):box[0][2]isnull. The loop checks row 1 (null), then row 2, which holds"cherry"— the first match."cherry"moves tobox[0][2],box[2][2]becomesnull, and the method returns true. This produces exactly one of the two grids the question accepts as correct — the one where"cherry"(not"grape") ends up in row 0.
Common Mistakes to Avoid
- Forgetting to null out the candy's old position after moving it — this duplicates the object instead of moving it.
- Starting the search loop at
row = 0instead ofrow = 1, or forgetting the initial "already filled" check entirely, and instead unconditionally overwriting row 0 with whatever the loop finds first (which could destroy an already-present piece of candy). - Continuing to search after finding a match instead of returning right away — not incorrect in principle, but it changes which piece of candy gets picked when a column has more than one, and adds unnecessary work.
Part (b): Writing removeNextByFlavor(String flavor)
The Rule, Broken Down
- Search the grid starting from the last row, moving left to right across each row.
- After finishing a row, move to the row above it, again left to right — continuing until either a match is found or the entire grid has been checked.
- The first candy whose flavor matches
flavorgets removed (its spot set tonull) and returned. - If nothing matches anywhere, return
nulland leaveboxcompletely unchanged.
Step-by-Step Approach
- Loop the row index backwards, starting at the last row (
box.length - 1) down to0. - Inside that, loop the column index forwards as usual, from
0up to the row's length. - At each spot, first check it isn't
null(an empty spot has no flavor to check), then compare its flavor toflavor. - On a match: save a reference to that candy, set the spot to
null, and return the saved reference immediately. - If both loops finish with no match, return
null.
The Code
public Candy removeNextByFlavor(String flavor)
{
for (int row = box.length - 1; row >= 0; row--)
{
for (int col = 0; col < box[row].length; col++)
{
if (box[row][col] != null && box[row][col].getFlavor().equals(flavor))
{
Candy found = box[row][col];
box[row][col] = null;
return found;
}
}
}
return null;
}
Why Each Piece Matters
- The outer loop runs backwards (
row = box.length - 1; row >= 0; row--) because the problem specifically defines the search order as starting from the last row — this is the opposite of the usual top-to-bottom 2D array traversal, and has to be written deliberately, not assumed. - The inner loop still runs forwards, left to right within each row — only the row order is reversed, not the column order.
box[row][col] != nullis checked before.getFlavor()is called, and specifically before, using&&'s short-circuit behavior. Calling.getFlavor()on anullreference would throw aNullPointerException— this check has to come first in the condition, not after..equals(flavor), never==, to compare the twoStrings by their characters rather than by object identity.- Saving the candy into
foundbefore nulling out its spot. Settingbox[row][col] = nullfirst would lose the only reference to the object before it could be returned.
Tracing the Example
Using the question's sample grid (rows 0–2, columns 0–4):
| Row | Col 0 | Col 1 | Col 2 | Col 3 | Col 4 |
|---|---|---|---|---|---|
| 0 | "lime" | "lime" | (empty) | "lemon" | (empty) |
| 1 | "orange" | (empty) | (empty) | "lime" | "lime" |
| 2 | "cherry" | (empty) | "lemon" | (empty) | "orange" |
removeNextByFlavor("cherry"): the search starts at row 2 (the last row). Column 0 of row 2 is"cherry"— an immediate match. That spot is set tonull, and the"cherry"candy is returned — matches the question (row 2, column 0).removeNextByFlavor("lime")(called next, on the updated grid): row 2 has no"lime"anywhere (cherry's spot is now empty, the rest don't match). Row 1: column 0 is"orange"(no match), columns 1–2 are empty, column 3 is"lime"— a match. That spot is nulled and returned — matches the question (row 1, column 3).removeNextByFlavor("grape")(called last, on the updated grid): rows 2, 1, and 0 are all searched in full with no"grape"found anywhere. The method returns null, andboxis left unchanged — matches the question.
Common Mistakes to Avoid
- Traversing rows in the normal
0tobox.length - 1order instead of backwards — this is the single most important detail in this method, and it's easy to default to the "usual" traversal out of habit. - Calling
.getFlavor()before checking fornull, or combining the checks with||instead of&&— either throws aNullPointerExceptionthe first time an empty spot is reached. - Comparing flavors with
==instead of.equals(). Two separateStringobjects holding the same characters are not the same object in memory, so==can incorrectly report "no match" even when the flavors read identically. - Forgetting to set the found spot back to
nullafter removing it — leaving the same candy object "in the box" while also handing it back to the caller.
Key Takeaways
- A 2D array of objects can represent a grid with gaps just by allowing
nullin some spots — searching it means checking fornullbefore doing anything else with a cell's value. - A traversal order isn't always "top row to bottom row, left to right" — read the problem's description of the search order carefully, since it can require looping a dimension backwards.
- "Find, remove, and return the first match" only needs one loop pass: save a reference to the match, clear its spot, and return immediately — no separate cleanup step needed afterward.