StudentAnswerSheet / TestResults: 2007 FRQ 3
A step-by-step solution to the 2007 AP CSA FRQ 3 (StudentAnswerSheet / TestResults), covering scoring a multiple-choice test from an ArrayList and finding the highest score across a group of students in Java.
Grading a multiple-choice test with partial-credit penalties is the setup for this AP Computer Science A free-response question — first scoring a single student's answer sheet against an answer key, then sweeping through a whole class's worth of sheets to find the top score.
What This FRQ Tests
- AP CSA units: Unit 7 (ArrayList) and Unit 5 (Writing Classes/methods)
- Core skill: looping through an
ArrayListby index while comparing it, position by position, against a secondArrayList - Secondary skill: scanning a collection of objects for the one with the maximum value, then reusing an already-specified method from a related class instead of recomputing it
- Official category: "Array/ArrayList" — always FRQ 3 on the AP CSA exam
The Setup
- The given (partially complete)
StudentAnswerSheetclass has:private ArrayList<String> answers— one answer per question, in question order; a lone"?"means that question was left blankString getName()— already implemented, returns the student's name
- The given (partially complete)
TestResultsclass has:private ArrayList<StudentAnswerSheet> sheets— one sheet per student, guaranteed non-empty, all the same length
- The scoring rule for
getScore: +1 for each correct answer, −0.25 for each incorrect answer, no change for an omitted ("?") answer. - You're asked to write two methods, one per class:
StudentAnswerSheet.getScore(ArrayList<String> key)TestResults.highestScoringStudent(ArrayList<String> key)
Part (a): Writing StudentAnswerSheet's getScore(ArrayList<String> key)
The Rule, Broken Down
- Compare each of this student's answers to the correct answer in the same position of
key. - An exact match adds
1to the score. - A different answer that isn't
"?"subtracts0.25from the score. - A
"?"answer changes nothing, whether or not it happens to be "close" to correct. - The total needs to support quarter-point values, so it has to be a
double.
Step-by-Step Approach
- Start a running score at
0, declared as adoublefrom the start. - Loop over every index of
key(guaranteed equal in size toanswers). - On each index, pull out this student's answer and the correct answer at that same position.
- If they match, add
1. - Otherwise — only if the student's answer isn't
"?"— subtract0.25. - After the loop finishes, return the accumulated score.
The Code
public double getScore(ArrayList<String> key)
{
double score = 0;
for (int i = 0; i < key.size(); i++)
{
String studentAnswer = answers.get(i);
String correctAnswer = key.get(i);
if (studentAnswer.equals(correctAnswer))
{
score = score + 1;
}
else if (!studentAnswer.equals("?"))
{
score = score - 0.25;
}
}
return score;
}
Why Each Piece Matters
scoredeclared asdouble, notint— the−0.25deduction can never be represented exactly by anint, so the accumulator has to be floating-point from the very first line, not just at the return statement..equals(), never==, for both string comparisons —Stringvalues are objects, so==would compare whether they're the same object in memory rather than the same sequence of characters.- The
else ifonly runs once the exact-match check has already failed, and specifically excludes"?"before subtracting — so an omitted answer falls through both branches completely untouched. key.size()as the loop bound, notanswers.size()— the two are guaranteed equal by the precondition, but looping over the parameter that was actually passed in keeps the method's behavior tied to whatever key it's told to score against.
Tracing the Example
Using the question's own table — key A, C, D, E, B, C, E, B, B, C; answers A, B, D, E, A, C, ?, B, D, C:
| Index | Key | Answer | Match? | Points |
|---|---|---|---|---|
| 0 | A | A | yes | +1 |
| 1 | C | B | no | −0.25 |
| 2 | D | D | yes | +1 |
| 3 | E | E | yes | +1 |
| 4 | B | A | no | −0.25 |
| 5 | C | C | yes | +1 |
| 6 | E | ? | omitted | 0 |
| 7 | B | B | yes | +1 |
| 8 | B | D | no | −0.25 |
| 9 | C | C | yes | +1 |
Running total: 1 - 0.25 + 1 + 1 - 0.25 + 1 + 0 + 1 - 0.25 + 1 = 5.25 — matching the question's stated result, ((6 * 1) - (3 * 0.25)) = 5.25, exactly.
Common Mistakes to Avoid
- Declaring
scoreas anint— the running total would silently truncate every−0.25deduction to0, producing a completely wrong (and always whole-number) score. - Checking for
"?"before checking for a match, instead of after — since"?"will never equal any letter inkey, checking order technically still works here, but structuring it as "check the match first, then check for?only in the failure case" is the clearer, more defensible version of the logic. - Comparing strings with
==instead of.equals()— one of the most common AP CSA point losses anywhereStringcomparison appears. - Looping past the end of one list or the other by using the wrong bound —
key.size()andanswers.size()are guaranteed equal here, but only because the precondition says so, not because Java enforces it automatically.
Part (b): Writing TestResults's highestScoringStudent(ArrayList<String> key)
The Rule, Broken Down
- Score every student's answer sheet against the same
key, usinggetScore. - Find whichever student's score is the largest.
- If there's a tie, returning the name of any one of the tied students is acceptable.
sheetsis guaranteed to have at least one element.
Step-by-Step Approach
- Since
sheetsis guaranteed non-empty, start by assuming the very first sheet is the best one found so far, and record its score. - Loop over the remaining sheets, starting at index
1. - Score each one, and compare it to the best score seen so far.
- If it's strictly greater, replace both the "best sheet" and "best score" trackers.
- After the loop, return the name of whichever sheet ended up in the lead.
The Code
public String highestScoringStudent(ArrayList<String> key)
{
StudentAnswerSheet best = sheets.get(0);
double bestScore = best.getScore(key);
for (int i = 1; i < sheets.size(); i++)
{
StudentAnswerSheet current = sheets.get(i);
double currentScore = current.getScore(key);
if (currentScore > bestScore)
{
best = current;
bestScore = currentScore;
}
}
return best.getName();
}
Why Each Piece Matters
- Starting
bestatsheets.get(0), rather than some sentinel likenull— the precondition guaranteessheets.size() > 0, so the first sheet is always a safe, valid starting point, and the loop only needs to inspect indices1and up. current.getScore(key)calls the method from part (a) — the question explicitly says to assumegetScoreworks as specified, so there's no reason to recompute a score by hand a second time here.- Strict
>, not>=, when replacingbest— this is exactly what makes "if there's a tie, return any one of them" work correctly: once a sheet is in the lead, only a strictly higher score bumps it out. - Returning
best.getName(), notbestitself — the method's declared return type isString, the student's name, not aStudentAnswerSheetobject.
Tracing the Example
The FRQ's own table only walks through a single getScore call — it doesn't include a worked multi-student example for highestScoringStudent. Building on that same scoring logic with a small illustrative class of three students scored against the same key:
| Student (loop step) | Score via getScore |
New best? |
|---|---|---|
Aiko — starting point (best, before the loop even runs) |
5.25 | starts as best |
Brody (i = 1) |
6.0 | yes — 6.0 > 5.25 |
Carmen (i = 2) |
6.0 | no — 6.0 > 6.0 is false, so Brody (found first) stays best |
highestScoringStudent returns "Brody" here — Carmen's tied score does not replace him, which matches the rule that returning the name of any one of several tied top scorers is an acceptable answer.
Common Mistakes to Avoid
- Starting the loop at index
0after already presettingbestto index0— this just re-compares the first sheet against itself, which is harmless but redundant; starting the loop at index1is the cleaner version of this pattern. - Using
>=instead of>when checking for a new best — this makes a later tied student silently overwrite an earlier one. Not technically wrong per the problem's own rules (either tied student is acceptable), but a common source of confusion when tracing through by hand. - Comparing scores with
.equals()as if they were wrapped objects, instead of using>directly —getScorereturns a primitivedouble, which compares with ordinary relational operators. - Returning
bestinstead ofbest.getName()— a type mismatch against the method's declaredStringreturn type.
Key Takeaways
- A score accumulator that can end on a fraction of a point (like
.25) must be declareddoublefrom its very first line, notint. - "Find the best of a collection" is a two-variable running pattern no matter what kind of object is involved: track the best item found so far and its value, compare each new candidate, and replace only on a strict improvement.
- Once a method like
getScoreis confirmed to work as specified, later methods should call it rather than reimplement its logic — less error-prone, and exactly what this FRQ's own directions expect.