CompSci.rocks
FRQcsapa

Checker: 2008 FRQ 4

A step-by-step solution to the 2008 AP CSA FRQ 4 (Checker), covering implementing a shared interface with multiple classes and composing them together like building blocks in Java.

Building a small family of interchangeable string-checking classes is the challenge in this AP Computer Science A free-response question — each one implements the same interface, and the final part asks you to combine existing ones together like building blocks instead of writing any new class at all.

What This FRQ Tests

  • AP CSA units: Unit 5 (Writing Classes), plus the interface-based design ideas belonging to what current course materials number as Unit 10 — implementing a shared interface rather than extending a shared parent class
  • Core skill: writing a class that implements an interface, storing whatever state its own logic needs
  • Secondary skill: composing already-existing objects into a new one instead of reimplementing their logic
  • Official category: officially FRQ 4 by this exam's own printed numbering — but 2008 predates the fixed FRQ-number-to-category order introduced with the 2019–2020 CED redesign, where slot 4 is reserved for "2D Array." This question has nothing to do with 2D arrays; content-wise it's a class-design question built around a shared interface, closer in spirit to a modern "Classes" FRQ.

The Setup

  • A Checker interface (not modified) declares exactly one method:
    • boolean accept(String text)
  • (a) SubstringChecker implements Checker:
    • Constructor takes one String — the substring to match
    • accept(text) returns whether text contains that substring anywhere
  • (b) AndChecker implements Checker:
    • Constructor takes two Checker objects
    • accept(text) returns true only if both stored Checkers accept text
  • (c) NotChecker is already described (not asked to be written):
    • Constructor takes one Checker object
    • accept(text) returns true only if its stored Checker does not accept text
    • You're asked to build a single yummyChecker expression out of existing checkers, not write a class

Part (a): Writing the SubstringChecker Class

Step-by-Step Approach

  1. Declare the class as public class SubstringChecker implements Checker.
  2. Store one field: the substring to match.
  3. The constructor takes that substring and stores it.
  4. accept(text) checks whether text contains the stored substring anywhere within it.

The Code

public class SubstringChecker implements Checker
{
    private String substringToMatch;

    public SubstringChecker(String substringToMatch)
    {
        this.substringToMatch = substringToMatch;
    }

    public boolean accept(String text)
    {
        return text.indexOf(substringToMatch) != -1;
    }
}

Why Each Piece Matters

  • implements Checker, not extendsChecker is an interface with no code of its own, only a method signature. SubstringChecker has to provide a full, working body for accept, since there's no inherited implementation to fall back on.
  • this.substringToMatch = substringToMatch — the constructor parameter deliberately shares a name with the field it initializes; this. is what tells Java which one is being assigned to.
  • indexOf(...) != -1 — the standard "does this string contain X anywhere" pattern using a method that's on the AP Quick Reference sheet. It's also case-sensitive by nature, which matters for this problem (see the trace below).

Tracing the Example

Using the question's own broccoliChecker = new SubstringChecker("broccoli"):

Call indexOf("broccoli") Result
accept("broccoli") found at index 0 true
accept("I like broccoli") found true
accept("carrots are great") not found (-1) false
accept("Broccoli Bonanza") not found (-1) false

The last row is the interesting one: "Broccoli Bonanza" does not accept, because indexOf compares characters exactly and "Broccoli" (capital B) is not the same sequence of characters as "broccoli" (lowercase b). All four results match the question's table exactly.

Common Mistakes to Avoid

  • Forgetting implements Checker entirely, which leaves SubstringChecker unrelated to the interface it's supposed to satisfy.
  • Using .equals() instead of indexOf. .equals() checks whether two strings are identical in their entirety — it would only accept a text that is exactly "broccoli" and nothing else.
  • Assuming matching should be case-insensitive. The question's own table confirms it isn't — "Broccoli Bonanza" is correctly rejected.

Part (b): Writing the AndChecker Class

Step-by-Step Approach

  1. Declare the class as public class AndChecker implements Checker.
  2. Store both constructor arguments as fields, declared using the interface type Checker — not any specific class like SubstringChecker.
  3. accept(text) returns true only when both stored checkers accept text.

The Code

public class AndChecker implements Checker
{
    private Checker checker1;
    private Checker checker2;

    public AndChecker(Checker checker1, Checker checker2)
    {
        this.checker1 = checker1;
        this.checker2 = checker2;
    }

    public boolean accept(String text)
    {
        return checker1.accept(text) && checker2.accept(text);
    }
}

Why Each Piece Matters

  • Fields declared as Checker, not a specific class — this is exactly what lets AndChecker combine any two objects that implement Checker, including another AndChecker, as the question's own veggies example does by wrapping bothChecker (itself an AndChecker).
  • &&, not || — the rule is "accepted by both," and && naturally short-circuits: if checker1.accept(text) is already false, checker2.accept(text) never even runs, which is fine since the overall answer is already decided.
  • Delegating entirely to checker1.accept(...) and checker2.accept(...)AndChecker never needs to know how either stored checker decides its answer, only that each one returns a boolean.

Tracing the Example

Using the question's own setup — bChecker matches "beets", cChecker matches "carrots", bothChecker = new AndChecker(bChecker, cChecker), aChecker matches "artichokes", veggies = new AndChecker(bothChecker, aChecker):

Call Reasoning Result
bothChecker.accept("I love beets and carrots") contains both "beets" and "carrots" true
bothChecker.accept("beets are great") contains "beets" but not "carrots" false
veggies.accept("artichokes, beets, and carrots") bothChecker accepts (has both) and aChecker accepts (has "artichokes") true

All three results match the question's table exactly.

Common Mistakes to Avoid

  • Declaring the fields as SubstringChecker instead of Checker. This would compile fine for the bothChecker example, but break the moment an AndChecker (like bothChecker inside veggies) gets passed in instead.
  • Using || instead of &&. The rule is "accepted by both," not "accepted by either."
  • Re-checking substrings directly inside AndChecker instead of delegating to checker1.accept(...)/checker2.accept(...)AndChecker has no idea what criteria its two stored checkers actually use, nor does it need to.

Part (c): Constructing yummyChecker

The Rule, Broken Down

  1. yummyChecker should accept a string only if it contains neither "artichokes" nor "kale".
  2. NotChecker flips a single Checker's answer.
  3. "Neither A nor B" is logically the same as "not A, and not B."

Step-by-Step Approach

  1. Wrap aChecker in a NotChecker — this accepts anything that does not contain "artichokes".
  2. Wrap kChecker in a NotChecker — this accepts anything that does not contain "kale".
  3. Combine the two wrapped checkers with an AndChecker — this accepts only strings that pass both "doesn't contain" checks at once.

The Code

yummyChecker = new AndChecker(new NotChecker(aChecker), new NotChecker(kChecker));

Why Each Piece Matters

  • No new class is needed — the three already-given building blocks (SubstringChecker, already used to build aChecker/kChecker; NotChecker; and AndChecker) are enough to express the whole rule as a single expression.
  • "Neither ... nor ..." becomes "NOT ... AND NOT ..." — this is the same logical rule as De Morgan's law (!(a || b) is equivalent to !a && !b), just expressed with composed objects instead of boolean variables.

Tracing the Example

Call NotChecker(aChecker) NotChecker(kChecker) Combined (AND)
yummyChecker.accept("chocolate truffles") no "artichokes"true no "kale"true true
yummyChecker.accept("kale is great") no "artichokes"true has "kale"false false
yummyChecker.accept("Yuck: artichokes & kale") has "artichokes"false has "kale"false false

All three results match the question's table exactly.

Common Mistakes to Avoid

  • Wrapping the whole AndChecker in a single NotChecker (new NotChecker(new AndChecker(aChecker, kChecker))). That expression means "NOT (has both artichokes AND kale)," which would incorrectly accept a string containing artichokes but not kale — the rule needs "neither," not "not both."
  • Swapping which checker gets wrapped by which NotChecker. It happens not to matter here since AndChecker is symmetric, but mixing them up mentally is exactly where careless errors creep in on a problem like this.
  • Forgetting new when constructing the NotChecker/AndChecker objects, or trying to call .accept() on a class name instead of an instance.

Notes: A Method Not on the AP CSA Quick Reference Sheet

String.contains(...) would let SubstringChecker.accept read a little more directly:

public boolean accept(String text)
{
    return text.contains(substringToMatch);
}
  • contains isn't listed on the real exam's Java Quick Reference sheet — only indexOf is, among the relevant String search methods. That doesn't mean it's off-limits; AP CSA graders accept any correct Java, whether or not it appears on that sheet.
  • What the sheet actually guarantees is that indexOf specifically is printed for you to look up if you forget its exact behavior mid-exam. contains is completely safe to use if you already know it well — the indexOf(...) != -1 version above is simply the one you can double-check against the reference sheet if you're unsure.

Key Takeaways

  • Implementing a shared interface (implements) is what lets completely unrelated classes (SubstringChecker, AndChecker, NotChecker) all be swapped in anywhere a Checker is expected — no shared parent class or inherited implementation required.
  • Storing collaborator objects using the interface type, not a specific concrete class, is what makes composition (a Checker built out of other Checkers) actually work for any combination.
  • "Neither X nor Y" always translates to "NOT X AND NOT Y," whether you're combining raw booleans or combining objects that already encapsulate that logic.

Related FRQs