CompSci.rocks
FRQcsapa

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 ArrayList by index while comparing it, position by position, against a second ArrayList
  • 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) StudentAnswerSheet class has:
    • private ArrayList<String> answers — one answer per question, in question order; a lone "?" means that question was left blank
    • String getName() — already implemented, returns the student's name
  • The given (partially complete) TestResults class 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

  1. Compare each of this student's answers to the correct answer in the same position of key.
  2. An exact match adds 1 to the score.
  3. A different answer that isn't "?" subtracts 0.25 from the score.
  4. A "?" answer changes nothing, whether or not it happens to be "close" to correct.
  5. The total needs to support quarter-point values, so it has to be a double.

Step-by-Step Approach

  1. Start a running score at 0, declared as a double from the start.
  2. Loop over every index of key (guaranteed equal in size to answers).
  3. On each index, pull out this student's answer and the correct answer at that same position.
  4. If they match, add 1.
  5. Otherwise — only if the student's answer isn't "?" — subtract 0.25.
  6. 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

  • score declared as double, not int — the −0.25 deduction can never be represented exactly by an int, so the accumulator has to be floating-point from the very first line, not just at the return statement.
  • .equals(), never ==, for both string comparisonsString values are objects, so == would compare whether they're the same object in memory rather than the same sequence of characters.
  • The else if only 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, not answers.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 score as an int — the running total would silently truncate every −0.25 deduction to 0, 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 in key, 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 anywhere String comparison appears.
  • Looping past the end of one list or the other by using the wrong bound — key.size() and answers.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

  1. Score every student's answer sheet against the same key, using getScore.
  2. Find whichever student's score is the largest.
  3. If there's a tie, returning the name of any one of the tied students is acceptable.
  4. sheets is guaranteed to have at least one element.

Step-by-Step Approach

  1. Since sheets is guaranteed non-empty, start by assuming the very first sheet is the best one found so far, and record its score.
  2. Loop over the remaining sheets, starting at index 1.
  3. Score each one, and compare it to the best score seen so far.
  4. If it's strictly greater, replace both the "best sheet" and "best score" trackers.
  5. 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 best at sheets.get(0), rather than some sentinel like null — the precondition guarantees sheets.size() > 0, so the first sheet is always a safe, valid starting point, and the loop only needs to inspect indices 1 and up.
  • current.getScore(key) calls the method from part (a) — the question explicitly says to assume getScore works as specified, so there's no reason to recompute a score by hand a second time here.
  • Strict >, not >=, when replacing best — 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(), not best itself — the method's declared return type is String, the student's name, not a StudentAnswerSheet object.

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 0 after already presetting best to index 0 — this just re-compares the first sheet against itself, which is harmless but redundant; starting the loop at index 1 is 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 — getScore returns a primitive double, which compares with ordinary relational operators.
  • Returning best instead of best.getName() — a type mismatch against the method's declared String return type.

Key Takeaways

  • A score accumulator that can end on a fraction of a point (like .25) must be declared double from its very first line, not int.
  • "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 getScore is 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.

Related FRQs